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#
- 2026-06-23 22:19 KST —
PUT /api/v1/facilities/t7v38v/share첫 시도,Mysql2::Error: Data too long for column 'sys' at row 1로 400 응답 - 2026-06-23 22:19 KST — 약 1초 뒤 동일 facility에 대한 재시도도 같은 오류로 실패
- 이후 동일 fingerprint 추가 발생 없음 (
occurrence_count: 2, last_seen == first_seen + 0.9 s)
Error Log#
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#
- Entry:
Api::V1::FacilitiesController#share(ShareControllerconcern으로 mix-in).
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
- Repository: 권한 부여 후 share event를 트리거. 대상 사용자/그룹 목록을 통째로
target에 실어Eventable::Events::Share.create_event로 전달한다.
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
- Event factory:
target배열의 모든 사용자에 대해event_object_data에 직렬화된 JSON을 push하고, eventable(facility)의 직렬화 결과도serialized_eventable_json에 저장한 뒤save. 이 모든 필드는Event모델 setter를 통해 단일sys해시에 누적된다.
_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
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
- Model:
sys를serialize컬럼으로 두고, 모든 부가 필드의 setter가sys[:...]에 값을 누적.
included do
serialize :sys, coder: Cupix::Util::FlexibleHash.new
serialize :meta, coder: Cupix::Util::FlexibleHash.new
end
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
- Failure point:
events.sys는 마이그레이션에서:text로 정의되어 MySQLTEXT(65535 byte) 한계가 적용된다.
class AddSysToEvent < ActiveRecord::Migration[6.0]
def change
add_column :events, :sys, :text
end
end
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"
- Error surface:
Eventable::Events::Base.create_event가_create_event(=EventFactory.create!)에서 발생한ActiveRecord::ValueTooLong을 잡아 로그 후 그대로 raise. 컨트롤러에서ActiveRecord::ValueTooLong이 응답 변환 미들웨어에 의해 400으로 매핑된다.
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#save가 events.sys 길이 초과로 Mysql2::Error: Data too long for column 'sys' at row 1을 반환 → ActiveRecord::ValueTooLong으로 변환되어 controller까지 전파, share API가 400으로 실패.
Log Evidence#
Datadog query (재현용):
service:cupixworks-api "Failed to create event" "Data too long"
Error log (Eventable::Events::Share 레벨):
{
"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 매핑 결과):
{
"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개):
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) |
message는 ShareEntityMailerWorker.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_meta를 event_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 규모라는 가정 하에 nativeALTER 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-1939—eventable_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_versionTEXT, 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 서버에 가해지는 부하 항목:
-
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 수치 일시 상향 권장.
-
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 영향이 가장 낮다.
-
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 누적 시 데이터 신선도 영향 고려.
-
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.
권장 절차#
- 사전 측정:
SELECT COUNT(*), AVG(LENGTH(sys)), MAX(LENGTH(sys)) FROM events,SHOW TABLE STATUS LIKE 'events',information_schema.tables의data_length/index_length로 실제 데이터 크기 확정. - 스테이징에서 동일 row scale 의 가상 데이터로 gh-ost dry run (
--test-on-replica또는--noop) 수행하여 실제 throughput 측정. - 운영 적용: gh-ost 를 우선 선택 (
--max-load=Threads_running=25 --critical-load=Threads_running=80 --max-lag-millis=2000 --chunk-size=1000). 트래픽 저점 시간대(주말 새벽 KST) 시작. - 진행 모니터링: gh-ost 가 출력하는 progress + Datadog 의
mysql.innodb.row_lock_waits,aws.rds.read_iops,aws.rds.write_iops,aws.rds.replica_lag지표 watch. - 종료 후 검증:
SHOW CREATE TABLE events로sys 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.rb의target전달이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-widesysColumn 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?플래그가 코드에 이미 존재) 마이그레이션을 가속해 legacyevents테이블 의존을 줄이는 방향을 검토.- DB 스키마 정책:
:textvs:mediumtext사용 기준을 정리하고 RuboCop 또는 schema linter로 신규 컬럼 도입 시 자동 검증.
Monitoring#
- ValueTooLong 발생 추적 timeseries:
sum:trace.rack.request.errors{service:cupixworks-api,@error.type:ActiveRecord::ValueTooLong}.as_count()
- Share endpoint 400 비율(이상치 감지용 baseline):
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api_v1_facilities_share}.as_count()
- Event 저장 실패 로그 카운트(에러 메시지 키워드 기반):
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 규모라고 가정하면 nativeALTER 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
sysColumn 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.*:sys및sys.*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: mysql24개 환경 확인./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.