ES /docs

Failed to create event: Mysql2::Error: Data too long for column 'sys' at row 1

RCA: Failed to create event - Data too long for column 'sys'

Overview#

facility share API가 Event.sys 컬럼(MySQL TEXT, 최대 65535 byte)에 직렬화된 페이로드를 저장하다가 길이 초과로 Mysql2::Error: Data too long을 일으켜 share 요청이 HTTP 400으로 실패했다. 동일 facility(t7v38v)에 대한 두 번의 시도가 모두 같은 원인으로 거절되었다.

What Happened#

PUT /api/v1/facilities/t7v38v/share 요청이 권한 적용까지는 성공한 뒤, share event를 기록하기 위해 Event 레코드를 저장하는 단계에서 events.sys 컬럼에 들어갈 직렬화된 JSON이 TEXT 한계를 초과해 ActiveRecord::ValueTooLong으로 거절되었다. 클라이언트에는 400이 응답되었고, share event 생성과 이후 알림 워커 트리거가 누락되었다.

Quick Facts#

Field Value
exception.class ActiveRecord::ValueTooLong (root: Mysql2::Error)
exception.message Mysql2::Error: Data too long for column 'sys' at row 1
top_frame app/models/concerns/eventable/events/base.rb:20 (rescue/log site)
failure_origin app/factories/event_factory.rb:102 (self.model.save)
endpoint PUT /api/v1/facilities/:key/share (Api::V1::FacilitiesController#share)
env production, region us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
facility sharing (cupixworks-api) 2 한 facility(t7v38v)의 share 요청이 400으로 실패하고 share event/알림이 누락

Timeline#

  1. 2026-06-23 22:19 KSTPUT /api/v1/facilities/t7v38v/share 첫 시도, Mysql2::Error: Data too long for column 'sys' at row 1로 400 응답
  2. 2026-06-23 22:19 KST — 약 1초 뒤 동일 facility에 대한 재시도도 같은 오류로 실패
  3. 이후 동일 fingerprint 추가 발생 없음 (occurrence_count: 2, last_seen == first_seen + 0.9 s)

Error Log#

Datadog Logs

text
Failed to create event: Mysql2::Error: Data too long for column 'sys' at row 1

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 2
  • 최초 발생: 2026-06-23 22:19 KST
  • 최근 발생: 2026-06-23 22:19 KST
  • Endpoint: PUT /api/v1/facilities/t7v38v/share
  • 결과: share 요청 400. 권한 추가는 트랜잭션 밖에서 이미 적용되었을 가능성이 있고, share event는 생성되지 않아 후속 워커(RunAfterEventCreatedWorker, segment tracking, Cupix::EventService.publish_event 등)가 작동하지 않음.

Root Cause Summary#

Event 모델은 Metable concern을 통해 sys 컬럼을 Cupix::Util::FlexibleHash로 serialize하고, serialized_eventable_json, event_object_data, custom_action_meta, attribute_changes 등 모든 부가 데이터를 단일 sys Hash에 저장한다. events.sys 컬럼은 마이그레이션에서 :text 타입(MySQL TEXT, 65535 byte)으로 생성되어 있어, share 대상 사용자/그룹 수가 많거나 facility의 직렬화 페이로드가 큰 경우 직렬화 결과가 TEXT 한계를 초과한다. 더 중요한 점은, db/schema.rb 전수 조사 결과 Metable concern을 통해 sys를 serialize하는 86개 테이블 중 rooms 단 한 곳만 t.text "sys", size: :medium(MEDIUMTEXT)로 정의되어 있고, events를 포함한 **나머지 85개 테이블 모두 :text(TEXT 65535 byte)**로 정의되어 있다. 즉 이번 사고는 events 한 테이블의 단순 누락이 아니라 거의 모든 Metable 모델에 잠재된 동일 형태의 용량 한계 결함이며, 다른 모델(예: share/copy 대상이 큰 facilities, bims, floorplans 등)에서도 같은 조건이 갖춰지면 재현될 수 있다.

Technical Analysis#

Code Path#

  1. Entry: Api::V1::FacilitiesController#share (ShareController concern으로 mix-in).
app/controllers/concerns/share_controller.rb:32-45ruby
def share
  validate_project_permissions_feature!

  members = repository_instance.share(
    params.slice(*SHARE_REQUEST_PARAMS),
    params[:permission]
  )

  render_api Renderable.new({
    contents: members,
    is_collection: true,
    serializer: MemberSerializer
  })
end
  1. Repository: 권한 부여 후 share event를 트리거. 대상 사용자/그룹 목록을 통째로 target에 실어 Eventable::Events::Share.create_event로 전달한다.
app/repositories/concerns/sharable_repository.rb:166-173ruby
if @model.respond_to?(:build_event)
  @model.build_event({
    action: 'share',
    reason: 'shared',
    target: @users + @groups + already_joined_or_invited_users + invited_users
  })
  Eventable::Events::Share.create_event(@model)
end
  1. Event factory: target 배열의 모든 사용자에 대해 event_object_data에 직렬화된 JSON을 push하고, eventable(facility)의 직렬화 결과도 serialized_eventable_json에 저장한 뒤 save. 이 모든 필드는 Event 모델 setter를 통해 단일 sys 해시에 누적된다.
app/factories/event_factory.rb:98-104ruby
_create_serialized_eventable(eventable_model)
_create_event_objects(eventable_model) if %w[create share update].include?(self.model.action)
_create_event_attribute_changes(eventable_model) if self.model.action == 'update'

self.model.save
self.model
app/factories/event_factory.rb:129-140ruby
def _create_event_objects(eventable_model)
  return if eventable_model.event_params[:target].blank?

  targets = eventable_model.event_params[:target].is_a?(Array) ? eventable_model.event_params[:target] : [eventable_model.event_params[:target]]
  targets.each do |target_model|
    if target_model.respond_to?(:serialized_json)
      self.model.event_object_data << { target: target_model.serialized_json }
    else
      self.model.event_object_data << { target: target_model.as_json(only: %w[id name]) }
    end
  end
end
  1. Model: sysserialize 컬럼으로 두고, 모든 부가 필드의 setter가 sys[:...]에 값을 누적.
app/models/concerns/metable.rb:7-10ruby
included do
  serialize :sys, coder: Cupix::Util::FlexibleHash.new
  serialize :meta, coder: Cupix::Util::FlexibleHash.new
end
app/models/event.rb:70-85ruby
def event_object_data
  sys[:event_object_data] ||= []
end

def event_object_data=(val)
  sys[:event_object_data] ||= []
  sys[:event_object_data] = val
end

def serialized_eventable_json
  sys[:serialized_eventable_json] || {}
end

def serialized_eventable_json=(val)
  sys[:serialized_eventable_json] = val
end
  1. Failure point: events.sys는 마이그레이션에서 :text로 정의되어 MySQL TEXT(65535 byte) 한계가 적용된다.
db/migrate/20201207070838_add_sys_to_event.rb:1-5ruby
class AddSysToEvent < ActiveRecord::Migration[6.0]
  def change
    add_column :events, :sys, :text
  end
end
db/schema.rb:1914-1926ruby
create_table "events", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
  t.string "action"
  t.datetime "created_at", null: false
  t.text "current_version"
  t.bigint "eventable_id"
  t.bigint "eventable_team_id"
  t.string "eventable_type"
  t.bigint "facility_id"
  t.bigint "level_id"
  t.integer "linked_review_id"
  t.string "reason"
  t.bigint "record_id"
  t.text "sys"
  1. Error surface: Eventable::Events::Base.create_event_create_event (= EventFactory.create!)에서 발생한 ActiveRecord::ValueTooLong을 잡아 로그 후 그대로 raise. 컨트롤러에서 ActiveRecord::ValueTooLong이 응답 변환 미들웨어에 의해 400으로 매핑된다.
app/models/concerns/eventable/events/base.rb:7-26ruby
def create_event(model)
  return nil if invalid_event?(model)

  begin
    event = _create_event(model)
    reason = extract_reason(event, model)
    properties = build_properties(model)

    track_event(model, reason, properties)

    Cupix::EventService.publish_event([event])
    Cupix::Event.publish(event.serializable_hash(stringify_nested_fields: false))
  rescue StandardError => e
    Cupix::Logger.error("Failed to create event: #{e.message}", function: __method__, class: self.name, model: model.class.to_s, id: model.id, event_params: model.event_params, error: e)
    raise e
  else
    model.event_created!
    event
  end
end

기대 동작: share 권한 적용 후 share event를 정상 저장하고 후속 알림/segment tracking을 트리거. 실제 동작: Event#saveevents.sys 길이 초과로 Mysql2::Error: Data too long for column 'sys' at row 1을 반환 → ActiveRecord::ValueTooLong으로 변환되어 controller까지 전파, share API가 400으로 실패.

Log Evidence#

Datadog query (재현용):

text
service:cupixworks-api "Failed to create event" "Data too long"

Error log (Eventable::Events::Share 레벨):

json
{
  "timestamp": "2026-06-23 22:19:53",
  "status": "error",
  "message": "Failed to create event: Mysql2::Error: Data too long for column 'sys' at row 1",
  "class": "Eventable::Events::Share",
  "function": "create_event",
  "error": { "msg": "Mysql2::Error: Data too long for column 'sys' at row 1" }
}

Request log (controller 레벨, ActiveRecord 매핑 결과):

json
{
  "timestamp": "2026-06-23 22:19:53",
  "status": "info",
  "message": "[400] PUT /api/v1/facilities/t7v38v/share (Api::V1::FacilitiesController#share)",
  "error": {
    "message": "Mysql2::Error: Data too long for column 'sys' at row 1",
    "class": "ActiveRecord::ValueTooLong"
  }
}

동일 facility에서 약 1초 간격으로 동일 오류가 두 번 기록됨 (22:19:52, 22:19:53 KST). 이후 같은 fingerprint 발생 없음.

추가 query ("Data too long for column" 24 h 범위)에서는 같은 시점의 share 실패 외에 bookmarks 엔드포인트의 name 컬럼 초과(Cupix::Errors::Parameter)가 별도로 다수 관찰되며, 이는 다른 fingerprint로 본 cluster와 무관한 사용자 입력 검증 영역이다.

Schema-wide sys Column Audit#

db/schema.rb 전수 조사 결과(t.text "sys" 패턴, 2026-06-24 develop 기준), sys 컬럼을 가진 테이블은 총 86개이며 그 중 rooms 단 한 곳만 MEDIUMTEXT (size: :medium, 16 MB)로 정의되어 있다. 나머지 85개는 모두 평문 :text(TEXT, 65535 byte)다. Metable concern을 include 한 모델은 모두 serialize :sys, coder: Cupix::Util::FlexibleHash.new (app/models/concerns/metable.rb:7-10)을 통해 임의 Hash를 직렬화하기 때문에, 아래 테이블들은 모두 동일한 유형의 Data too long for column 'sys' 결함이 잠재되어 있다.

TEXT (65535 byte) 로 남아있는 sys 컬럼 테이블 (85개):

text
activities, aerial_maps, aerial_photos, annotation_layers, annotations,
areas, asset_instances, assets, associations, attachments, aws_tasks,
badges, billing_accounts, bim_revisions, bims, bookmarks, buildings,
cameras, capacity_transactions, captures, categories, clusters, comments,
connects, copies, copy_requests, credit_transactions, custom_properties,
deviations, editing_entities, editings, element_traces, elements,
entity_snapshots, events, facilities, fields, floorplan_sources,
floorplans, form_designs, form_fields, groups, integrations, jobs,
levels, line_items, masks, measurements, meshes, nodes, panos, payments,
phases, pointclouds, products, progresses, purchase_orders, quotes,
record_statuses, records, reference_sources, references, reports,
resources, reviews, revision_requests, schedule_revisions, sessions,
siteinsights_configs, siteinsights_events, sitetracks, sketches,
statuses, storages, tasks, teams, teleports, textures, tracking_plans,
users, videos, work_items, workareas, workflows, workspaces

MEDIUMTEXT 로 정의된 유일한 테이블 (1개): rooms (db/schema.rb:3855, t.text "sys", size: :medium).

추가 migration 검색(change_column.*:sys, sys.*medium|sys.*long in db/migrate/)에서도 후속 확장 마이그레이션은 발견되지 않았다 — schema.rb 의 정의가 운영 DB 의 컬럼 타입을 그대로 반영한다고 볼 수 있다.

따라서 이번 incident 는 events 한 테이블의 누락 수정이 아닌, Metable 패턴 전반의 컬럼 용량 정책 수정 대상이 된다. 단일 모델에서 share/copy/event_object_data 같이 사용자/타깃 수에 비례해 페이로드가 선형 증가하는 경로가 있는 모델(특히 facilities, bims, floorplans, assets, records, captures 등)이 우선 검토 대상이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 events.sys 컬럼이 TEXT(64 KB) 한계라 share event의 직렬화 페이로드가 한계를 초과 db/migrate/20201207070838_add_sys_to_event.rb:3 (add_column :events, :sys, :text); db/schema.rb:1926 (t.text "sys"); Metable concern이 sys에 모든 부가 필드를 누적 (event.rb:70-85); 다른 유사 컬럼은 :mediumtext로 정의됨; 에러 메시지가 정확히 events.sys 길이 초과 Confirmed
H2 사용자 입력값(예: params[:message])이 너무 길어 검증 누락으로 발생 share API에 message 파라미터 존재 (SHARE_REQUEST_PARAMS) messageShareEntityMailerWorker.perform_async 인자로만 전달되고 Event 저장 경로에 들어가지 않음 (sharable_repository.rb:178-185); event_params에는 target만 들어감 Rejected
H3 custom_action_meta JSON 파싱 결과가 거대해서 한계 초과 EventFactory#create!custom_action_meta 처리 경로 존재 (event_factory.rb:79-92) share controller는 custom_action_metaevent_params에 넣지 않고 target만 넣음 (sharable_repository.rb:167-171) Rejected
H4 DB collation/charset 변경으로 동일 데이터의 byte 길이가 늘어남 utf8mb4는 문자당 최대 4 byte이므로 한글/이모지 사용 시 byte 폭증 가능 단일 facility에서만 발생, 다른 facility에서는 같은 시간대 동일 오류 없음 → 코드/스키마 일반 변경이 아니라 데이터 크기 문제 Rejected (보조 요인 가능, root cause 아님)

Fix Recommendation#

즉시 조치 (Critical)#

  • 근본 수정: events.sys 컬럼 타입을 :mediumtext(16 MB)로 확장. 다른 모델(rooms)이 이미 이 패턴을 사용 중이라 일관성도 회복된다. 마이그레이션 파일 위치: db/migrate/에 신규 change_column :events, :sys, :mediumtext 추가. 단, 운영 events 테이블이 1억 row 규모라는 가정 하에 native ALTER TABLE 은 ALGORITHM=COPY 경로로 강제되어 수 시간~수십 시간 락/부하를 유발하므로 (아래 "Migration Cost Estimate (100M rows)" 참고) 반드시 gh-ost 또는 pt-online-schema-change 같은 online schema change tool 을 통해 적용해야 한다.
  • 단기 완화(긴급 배포가 어려울 때): EventFactory#create!에서 event_object_data/serialized_eventable_json 직렬화 직후 Marshal.dump(self.model.sys).bytesize 등을 측정하여 한계 근접 시 event_object_data 저장을 생략하고 정상 흐름은 유지하되 truncation을 Cupix::Logger.warn으로 남기는 방어 코드. (동작 변화가 있으므로 영구 해법은 아니며 H1 수정과 함께 제거.)

Migration Cost Estimate (100M rows)#

운영 events 테이블이 1억 row 라고 가정했을 때 ALTER TABLE events MODIFY sys MEDIUMTEXT 의 비용 추정.

데이터 볼륨 가정#

  • 행 수: 100,000,000 (1억)
  • 인덱스: PK + 9개의 secondary index (db/schema.rb:1931-1939eventable_team_id, eventable_type+eventable_id, facility_id, level_id, linked_review_id, record_id, team_id, user_id, workspace_id)
  • sys 컬럼은 InnoDB DYNAMIC row format 에서 off-page LOB 페이지에 저장되며, share/copy event 의 경우 행당 평균 1–4 KB 사용 (이번 사고에서 65 KB 한계를 친 행은 outlier)
  • 그 외 컬럼(current_version TEXT, bigint × 8, string × 3, datetime × 2): clustered index 행 길이 약 200–400 byte
  • 추정 테이블 + 보조 인덱스 합계: 약 250–400 GB (SHOW TABLE STATUS LIKE 'events' 로 사전 확정 필요)

알고리즘별 동작#

알고리즘 가능 여부 비고
ALGORITHM=INSTANT 불가 InnoDB INSTANT 는 컬럼 추가/삭제/default 변경에만 적용. TEXT → MEDIUMTEXT 는 컬럼 metadata 길이 prefix(2 byte → 3 byte) 변경이라 metadata-only 가 아님. MySQL 8.0 Online DDL Operations 표에서 BLOB/TEXT family 간 변환은 INSTANT/INPLACE 미지원으로 명시.
ALGORITHM=INPLACE 불가 위와 동일 이유. MySQL 은 ERROR 1845 (0A000): ALGORITHM=INPLACE is not supported 로 거부.
ALGORITHM=COPY 가능 새 임시 테이블에 전체 데이터 복사 + 9개 secondary index 재생성. LOCK=SHARED 면 마이그레이션 동안 write 차단(read 만 허용). LOCK=NONE 은 COPY 알고리즘에서 미지원.
pt-online-schema-change 가능 shadow table + trigger 기반. DML 동안 가용. write 부하 약 2배 증가.
gh-ost 가능 binlog tailing 기반. primary 부하가 가장 낮고, replica 또는 별도 노드에서 동작.

소요 시간 추정#

소요 시간은 rebuild throughput 에 의해 결정된다. RDS MySQL 의 일반적인 ALTER 처리량:

인스턴스 등급 효과적 rebuild throughput 100M row × 300 GB 환산
db.r6g.2xlarge (gp3 baseline 3000 IOPS) 5–10 MB/s 8–16시간
db.r6g.4xlarge (gp3 12,000 IOPS) 15–30 MB/s 3–6시간
db.r6g.8xlarge (io1 ≥ 20,000 IOPS) 40–80 MB/s 1–2시간

도구별 추가 오버헤드:

  • native ALGORITHM=COPY: 기준값. 단 락(LOCK=SHARED) 때문에 1억 row 테이블에서는 현실적으로 사용 불가.
  • pt-online-schema-change: trigger 동기화로 인해 native 대비 약 +40–60% 소요 → 약 5–24시간.
  • gh-ost (--cut-over=atomic --throttle-control-replicas=...): chunked copy + binlog catch-up. native 대비 약 +20–40% 소요 → 약 4–20시간. --max-load=Threads_running=25, --critical-load=Threads_running=80 설정 시 primary 부하 자동 조절.

위 추정은 어디까지나 차수(order of magnitude) 수준이며, 실제 시간은 (a) 운영 시점의 평균 row 크기와 sys LOB off-page 분포, (b) 동시 발생 DML 부하, (c) gh-ost throttle 설정에 의해 달라진다. 본 추정의 핵심은 "1시간 미만에 끝나지 않는다, 즉 무중단 + 모니터링 필요한 작업이다" 이다.

서버 부하#

마이그레이션 중 production 서버에 가해지는 부하 항목:

  1. Disk I/O (가장 큰 제약):

    • native COPY: shadow table + 9개 보조 인덱스 재생성으로 총 데이터 크기의 약 2배(~600 GB) write 가 발생. 동일 볼륨에 임시 저장되므로 사용 가능 디스크 공간 ≥ 현재 테이블 크기 × 2 확보 필수.
    • pt-osc / gh-ost: 동일하게 shadow table 분량의 write 가 발생하지만 chunk size 조절로 IOPS spike 회피 가능.
    • gp3 볼륨이라면 baseline IOPS 초과 시 throttle 발생 → 마이그레이션 시작 전 IOPS provisioned 수치 일시 상향 권장.
  2. CPU 및 buffer pool:

    • rebuild 가 buffer pool 을 임시 테이블 페이지로 채우면서 hot working set(facility / user lookups 등)이 evict 됨. 마이그레이션 직후 약 30–60분간 cache miss 로 인한 read latency 상승 예상.
    • pt-osc 의 trigger 는 origin row 1건당 trigger 1회 + shadow table insert/update 1회를 발생시키므로 origin 의 쓰기 trace 가 정확히 2배가 된다. high-traffic 시간대(KST 업무시간) 실행 비권장.
    • gh-ost 는 trigger 를 쓰지 않고 replica 의 binlog 를 읽어 처리하므로 primary CPU 영향이 가장 낮다.
  3. Replication lag:

    • 모든 방식이 binlog 에 row event 를 기록. 100M row 의 일괄 변경 event 가 replica 로 전파될 때 replica replay 가 단일 스레드(또는 parallel replication 구성에 따라 다름)로 처리되어 lag 가 누적될 수 있음.
    • gh-ost 는 자체적으로 --max-lag-millis=1500 같은 옵션으로 replica lag 감시 + 자동 throttle 가능.
    • 마이그레이션 전후로 reporting / 분석 워크로드가 replica 를 사용한다면 lag 누적 시 데이터 신선도 영향 고려.
  4. Lock / latency:

    • native COPY + LOCK=SHARED: events 테이블 write 가 마이그레이션 시간 전체에 걸쳐 차단 → share/copy/update API 가 timeout. 운영상 채택 불가.
    • pt-osc cut-over: 마지막 rename 단계에서 짧은 metadata lock (수 초). cut-over 직전 RunAfterEventCreatedWorker 등 long-running transaction 이 없도록 사전 정리 필요.
    • gh-ost cut-over: atomic rename 으로 sub-second metadata lock.

권장 절차#

  1. 사전 측정: SELECT COUNT(*), AVG(LENGTH(sys)), MAX(LENGTH(sys)) FROM events, SHOW TABLE STATUS LIKE 'events', information_schema.tablesdata_length/index_length 로 실제 데이터 크기 확정.
  2. 스테이징에서 동일 row scale 의 가상 데이터로 gh-ost dry run (--test-on-replica 또는 --noop) 수행하여 실제 throughput 측정.
  3. 운영 적용: gh-ost 를 우선 선택 (--max-load=Threads_running=25 --critical-load=Threads_running=80 --max-lag-millis=2000 --chunk-size=1000). 트래픽 저점 시간대(주말 새벽 KST) 시작.
  4. 진행 모니터링: gh-ost 가 출력하는 progress + Datadog 의 mysql.innodb.row_lock_waits, aws.rds.read_iops, aws.rds.write_iops, aws.rds.replica_lag 지표 watch.
  5. 종료 후 검증: SHOW CREATE TABLE eventssys mediumtext 확인, 기존 RCA 의 Datadog query ("Failed to create event" "Data too long") 가 0 으로 수렴하는지 확인.

85개 테이블 일괄 확장에 대한 함의#

"Schema-wide sys Column Audit" 의 85개 테이블 모두에 같은 작업을 적용한다면, 테이블별 row 수가 다르더라도 위 절차를 테이블당 반복해야 한다. bims, facilities, floorplans, panos, captures, records, assets 처럼 사용량이 큰 테이블은 1억 row scale 에 근접할 수 있으므로 각각 수 시간~수십 시간의 gh-ost run 이 필요하다. 한꺼번에 병렬 실행하면 디스크 IOPS 및 binlog 처리량이 포화되므로 순차 적용 + 사이에 cool-down 을 두는 schedule 이 안전하다. 우선순위는 기존 Fix Recommendation 의 그룹(사용자/타깃 페이로드가 큰 share/copy 흐름 → 첨부/지오메트리 → 일반 metadata) 순서를 그대로 따른다.

단기 개선 (1주 이내)#

  • sharable_repository.rbtarget 전달이 share 대상 전체 사용자/그룹을 그대로 직렬화하는 구조다. _create_event_objects(event_factory.rb:129-140)는 사용자 수에 비례해 payload가 선형 증가하므로, share 대상 수가 큰 경우(대규모 그룹 + 다수 user) 에 대비해 (a) event_object_data에 저장하는 필드 화이트리스트화(id, name, crn, type만 유지), (b) serialized_eventable_json 의 facility 직렬화 결과를 기본 필드로만 축소하는 옵션을 검토. 변경 전후 byte 길이를 측정하는 메트릭 노출 권장.
  • Metable 사용 모델 전체에 대한 sys 컬럼 일괄 확장: 위 "Schema-wide sys Column Audit"에서 식별한 85개 테이블에 대해 change_column ..., :sys, :mediumtext 를 적용하는 후속 마이그레이션 시리즈를 계획한다. 우선순위는 (1) 사용자/타깃 페이로드가 큰 share/copy 흐름이 있는 모델 (events, facilities, bims, floorplans, records, captures, assets), (2) 첨부/지오메트리 데이터를 다루는 모델 (meshes, pointclouds, panos, textures, aerial_maps), (3) 그 외 일반 metadata 위주 모델 순으로 진행. 각 테이블별로 운영 DB에서의 ALTER 비용(데이터 크기, replication lag) 사전 측정 필수.

장기 개선 (재발 방지)#

  • Metable.serialize :sys가 모델 도메인 로직과 강하게 결합되어 모든 부가 데이터를 단일 컬럼에 누적시키는 구조 자체가 위험하다. 재발 방지를 위해 (a) share event 페이로드를 외부 객체 저장소(S3 또는 별도 event_payloads 테이블)로 옮기고 events에는 참조 키만 두거나, (b) 신규 알림 파이프라인(Cupix::Migrate::NotificationService.use_notification_service? 플래그가 코드에 이미 존재) 마이그레이션을 가속해 legacy events 테이블 의존을 줄이는 방향을 검토.
  • DB 스키마 정책: :text vs :mediumtext 사용 기준을 정리하고 RuboCop 또는 schema linter로 신규 컬럼 도입 시 자동 검증.

Monitoring#

  • ValueTooLong 발생 추적 timeseries:
text
sum:trace.rack.request.errors{service:cupixworks-api,@error.type:ActiveRecord::ValueTooLong}.as_count()
  • Share endpoint 400 비율(이상치 감지용 baseline):
text
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api_v1_facilities_share}.as_count()
  • Event 저장 실패 로그 카운트(에러 메시지 키워드 기반):
text
logs("service:cupixworks-api status:error \"Failed to create event\" \"Data too long\"").index("*").rollup("count").by("env").last("1d")

마이그레이션 후 위 첫 번째 쿼리가 0으로 수렴하는지 확인하고, 재발 시 알림이 가도록 monitor로 승격.

Risk Assessment#

  • Risk level: medium-high (현재 단일 facility 영향이지만 schema 감사 결과 85개 테이블이 동일한 TEXT 한계를 공유하므로, share/copy/import 같이 페이로드가 사용자·타깃 수에 비례하는 모든 흐름에서 재현 가능)
  • 예상 복잡도: elevated. events 테이블이 1억 row 규모라고 가정하면 native ALTER TABLE 은 ALGORITHM=COPY 로만 동작하여 LOCK=SHARED 하 수 시간 ~ 수십 시간의 write 차단이 필요하므로 gh-ost / pt-online-schema-change 같은 online schema change tool 사용이 필수. 1회 마이그레이션 자체가 4–20시간 작업이며, 85개 테이블 전체 확장 plan 까지 포함하면 high — 운영 DB IOPS, replica lag, 도구 throttle 설정, 적용 시간대를 별도 plan 으로 분리해야 한다.

Revision History#

Revision 1#

Feedback: "sys 사용하는 모델중에 text 로 되어있는게 또 있는지도 찾아줘바" — Metable 패턴으로 sys 컬럼을 쓰는 다른 모델 중에도 여전히 TEXT 로 정의되어 있는 것이 있는지 schema 전수 조사 요청.

판정:

피드백 항목 판정 근거
sys 를 쓰는 다른 모델 중 :text 로 남아있는 테이블이 더 있는지 확인 수용 db/schema.rb 전수 조사 결과 sys 컬럼을 가진 86개 테이블 중 rooms 단 한 곳만 MEDIUMTEXT(size: :medium, db/schema.rb:3855)이며, events 를 포함한 나머지 85개 테이블 모두 평문 :text(TEXT 65535 byte). db/migrate/ 내 후속 change_column 마이그레이션도 없음(Grep change_column.*:sys 0 hits, Grep sys.*medium|sys.*long 0 hits). 85개 테이블 전체 목록을 "Schema-wide sys Column Audit" 섹션에 명시.

변경 사항:

  • Root Cause Summary 에 schema 전수 조사 결과 및 "events 단일 누락이 아니라 Metable 패턴 전반의 결함" 이라는 해석 추가.
  • Technical Analysis 에 신규 하위 섹션 "Schema-wide sys Column Audit" 추가 — 85개 TEXT 테이블 목록, MEDIUMTEXT 1개(rooms) 위치, migration 검증 결과 포함.
  • Fix Recommendation/단기 개선에 Metable 모델 일괄 sys 확장 마이그레이션 시리즈 항목 추가 (우선순위 그룹화 포함).
  • Risk Assessment level 을 medium → medium-high 로, 예상 복잡도 설명을 단일 수정과 일괄 확장으로 분리.

추가 조사 내용:

  • /home/ec2-user/repos/tesla/db/schema.rb 전수 스캔: t.text "sys" 88건 매칭, awk 로 직전 create_table 테이블명과 결합해 86개 sys 테이블 식별 (일부 라인이 같은 테이블 내 다중 매칭 없음 확인).
  • /home/ec2-user/repos/tesla/app 전체에서 include Metable / serialize :sys 사용처 확인 — app/models/concerns/metable.rb:7-10 이 단일 진입점이며 85개 TEXT 테이블 모델이 모두 이 concern 을 통해 sys 를 직렬화.
  • db/migrate/ 에서 change_column.*:syssys.*medium|sys.*long 검색 — 후속 확장 마이그레이션 없음 확인.

Revision 2#

Feedback: "event row 가 1억개라고 가정하고 마이그레이션 하는데 얼마나 걸릴지 계산좀. 서버 부하도" — events 테이블이 1억 row 일 때 sys 컬럼 MEDIUMTEXT 확장 마이그레이션의 소요 시간과 서버 부하를 정량적으로 추정 요청.

판정:

피드백 항목 판정 근거
1억 row 가정 시 마이그레이션 소요 시간 추정 수용 db/schema.rb:1914-1940 에서 events 테이블 인덱스 구조(PK + 9개 secondary index) 확인. MySQL 8.0 Online DDL 문서에 따르면 BLOB/TEXT family 간 변환(TEXT→MEDIUMTEXT)은 length prefix(2→3 byte) 가 바뀌므로 ALGORITHM=INSTANT/INPLACE 미지원이며 COPY 알고리즘으로만 동작. rebuild throughput(인스턴스 등급별 5–80 MB/s) × 추정 테이블 크기(약 300 GB) 로 native COPY 약 1–16시간, gh-ost / pt-osc 는 +20–60% 오버헤드를 더해 약 4–24시간으로 산정. 인스턴스 등급별 표로 정리.
서버 부하 영향 분석 수용 9개 secondary index 재생성 시 디스크 사용량 약 2배, gp3 baseline IOPS 초과 시 throttle 위험. pt-osc 의 trigger 방식은 write 부하 약 2배, gh-ost 의 binlog 방식은 primary 부하 가장 낮음. native COPY+LOCK=SHARED 는 write 차단으로 운영 적용 불가. buffer pool eviction, replication lag, cut-over metadata lock 각 항목별 영향과 도구 선택지(gh-ost --max-load --critical-load --max-lag-millis)를 명시. Gemfile.lock 에서 mysql2 0.5.4 + Rails 7.2.2 확인하여 MySQL 8.x 가정 적용.

변경 사항:

  • Fix Recommendation/즉시 조치 항목에 "native ALTER TABLE 은 사실상 불가, gh-ost / pt-osc 필수" 문구 보강.
  • Fix Recommendation 아래 신규 하위 섹션 "Migration Cost Estimate (100M rows)" 추가 — 데이터 볼륨 가정, 알고리즘 가능 여부 표, 인스턴스 등급별 소요 시간 표, 도구별 오버헤드, 서버 부하 4개 항목(Disk I/O / CPU+buffer pool / Replication lag / Lock+latency), 권장 절차, 85개 테이블 확장 시 함의 포함.
  • Risk Assessment 의 예상 복잡도를 standard → elevated 로 격상, 1회 마이그레이션 자체가 4–20시간 작업이며 online schema change tool 필수임을 명시.

추가 조사 내용:

  • /home/ec2-user/repos/tesla/db/schema.rb:1914-1940 에서 events 테이블 인덱스 9개 + PK 확인.
  • /home/ec2-user/repos/tesla/Gemfile.lock 에서 Rails 7.2.2, mysql2 0.5.4 (MySQL 8.x 호환) 확인.
  • /home/ec2-user/repos/tesla/config/database.yml 에서 adapter: mysql2 4개 환경 확인.
  • /home/ec2-user/repos/tesla/db/migrate/ 935개 마이그레이션 파일 전체에서 change_column :sys / ALGORITHM / LOCK=NONE / pt-online-schema / gh-ost 키워드 검색 — 프로젝트 내 정해진 online schema change 컨벤션 없음 확인 (따라서 도구 선택을 권장 절차에 명시).
  • MySQL 8.0 Online DDL Operations 표: BLOB/TEXT 컬럼 type 변경은 INPLACE/INSTANT 미지원, COPY only.