ES /docs

Failed to save partial JSON Element 15382518

RCA: Failed to save partial JSON Element 15382518

Overview#

What Happened#

2026-05-22 20:20 UTC부터 23:57 UTC까지 약 3.5시간 동안 cupixworks-worker 서비스의 data_changes Sidekiq queue에서 partial JSON 파일 저장이 대량 실패했다. 단일 호스트(ip-10-1-18-233.us-west-2.compute.internal)의 /var/data_changes/ 파티션 디스크 용량 부족(No space left on device)이 원인이며, 264,144건의 에러가 발생했다.

Quick Facts#

Field Value
exception.class Errno::ENOSPC
exception.message No space left on device @ rb_sysopen - /var/data_changes/elements/partial/2026-05-22/uswe2/1779481236606406-elements-15382518.json.gz
top_frame app/models/concerns/data_ware_house/partial_json.rb:144
runtime Ruby 3.3.0, Sidekiq 7.3.9, Rails 7.2.2
deploy production-us-west-2-20260519T0920Z0-3e770a15-cupixworks
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
Data Warehouse (partial JSON pipeline) 264,144 모든 모델(Element, ElementTrace, Record, Capture)의 partial JSON export 실패 — 데이터 웨어하우스 동기화 지연
Elasticsearch indexing ~수십건 BulkPartialIndexDeadWorker 후속 실패 (document_missing_exception)

Timeline#

  1. 2026-05-22T20:20:42Z — 최초 에러 발생. Element 15382518 partial JSON 저장 실패 (Errno::ENOSPC)
  2. 2026-05-22T20:20~23:57Z — 동일 호스트에서 모든 partial JSON 저장 작업 연쇄 실패 (264,144건)
  3. 2026-05-22T23:41~23:42Z — 후속 영향: BulkPartialIndexDeadWorker에서 Elasticsearch document_missing_exception 발생
  4. 2026-05-22T23:57:59Z — 마지막 에러 로그 기록

Error Log#

Datadog Logs

text
Failed to save partial JSON Element 15382518

Impact#

  • Service: cupixworks-worker
  • 발생 횟수: 264,144
  • 최초 발생: 2026-05-22T20:20:42.030Z
  • 최근 발생: 2026-05-22T23:57:59.483Z
  • 영향 범위: 단일 호스트(ip-10-1-18-233)에서 처리되는 모든 data warehouse partial JSON export 작업. Element, ElementTrace, Record, Capture 모델 전체에 영향. 데이터 웨어하우스로의 변경사항 전파가 3.5시간 동안 중단됨.

Root Cause Summary#

호스트 ip-10-1-18-233.us-west-2.compute.internal/var/data_changes/ 파티션 디스크 공간이 고갈되어 Errno::ENOSPC (No space left on device) 예외가 발생했다. DataWareHouse::PartialJson concern의 save_partial_json_file 메서드에서 gzip 압축된 JSON 파일을 로컬 디스크에 쓰려 할 때 File.open 호출이 실패했다. 이 에러는 rescue 절에서 로깅만 수행하고 예외를 re-raise하지 않으므로, 각 실패가 조용히 무시되면서 264,144건의 에러 로그만 생성되었다.

Technical Analysis#

Code Path#

  • Entry point: BulkSavePartialJsonToFileWorker#perform (app/workers/bulk_save_partial_json_to_file_worker.rb:71-74) 또는 SavePartialJsonToFileWorker#perform (app/workers/save_partial_json_to_file_worker.rb:63)
  • 두 worker 모두 각 model 인스턴스에 대해 save_partial_json_to_file을 호출
app/models/concerns/data_ware_house/partial_json.rb:11-18ruby
def save_partial_json_to_file(all_data: false, operation: '(updated)', changes: nil, timestamp: nil)
  if $FORWARD_DATA_CHANGES != true
    Cupix::Logger.debug('Data changes forwarding is disabled', class: self.class, module: 'DataWareHouse', function: 'save_partial_json_to_file')
    return nil
  end

  generate_and_save_partial_json(operation, all_data: all_data, changes: changes || saved_changes, timestamp: timestamp)
end
  • generate_and_save_partial_json은 JSON 데이터를 준비하고 gzip 압축한 뒤 save_partial_json_file을 호출
app/models/concerns/data_ware_house/partial_json.rb:115-118ruby
def generate_and_save_partial_json(operation, all_data: false, changes: nil, timestamp: nil)
  json_data = prepare_partial_json_data(operation, all_data: all_data, changes: changes)
  json_content = ActiveSupport::Gzip.compress(json_data.to_json)
  save_partial_json_file(json_content, partial_json_object_key(timestamp: timestamp))
end
  • Failure point: save_partial_json_file 메서드의 File.open 호출 (line 144)
app/models/concerns/data_ware_house/partial_json.rb:121-149ruby
def save_partial_json_file(content, path)
  filepath = Rails.env.development? ? "./tmp/storage/#{path}" : "/var/data_changes/#{path}"
  create_directory_if_not_exists(filepath)

  # Verify directory is writable before attempting file write
  dirname = File.dirname(filepath)
  unless File.writable?(dirname)
    dir_stat = File.stat(dirname)
    Cupix::Logger.error(
      'Directory not writable before file write attempt',
      class: self.class.name, module: 'DataWareHouse', function: 'save_partial_json_file',
      directory: dirname, permissions: format('%o', dir_stat.mode & 0o777),
      uid: dir_stat.uid, gid: dir_stat.gid, process_uid: Process.uid, process_euid: Process.euid
    )
    raise Errno::EACCES, "Directory #{dirname} is not writable"
  end

  File.open(filepath, 'wb') { |file| file.write(content) }
rescue Errno::EACCES, Errno::ENOSPC => e
  Cupix::Logger.error("Failed to save Partial JSON #{self.class.name} #{self.id} to file #{filepath}",
    class: self.class.name, module: 'DataWareHouse', function: 'save_partial_json_file',
    error: e.message, backtrace: e.backtrace)
else
  Cupix::Logger.debug("Successfully saved Partial JSON #{self.class.name} #{self.id} to file #{filepath}",
    class: self.class.name, module: 'DataWareHouse', function: 'save_partial_json_file')
end

기대 동작: File.open(filepath, 'wb')가 gzip 압축된 JSON을 /var/data_changes/elements/partial/2026-05-22/uswe2/{timestamp}-elements-{id}.json.gz에 기록.

실제 동작: 디스크 용량 부족으로 Errno::ENOSPC 예외 발생 → rescue 절에서 에러 로깅 후 nil 반환 (silent failure). Worker는 retry: false로 설정되어 있어 재시도 없이 종료.

  • Bulk worker의 에러 전파 부재 (app/workers/bulk_save_partial_json_to_file_worker.rb:71-78):
app/workers/bulk_save_partial_json_to_file_worker.rb:71-78ruby
models.find_in_batches(batch_size: 50) do |groups|
  groups.each do |model|
    model.save_partial_json_to_file(all_data: true, operation: operation, timestamp: timestamp)
  end
end
rescue NoMethodError => e
  Cupix::Logger.error(e.message, class: self.class, function: 'perform')
end

Bulk worker는 Errno::ENOSPC를 catch하지 않지만, 해당 예외는 이미 save_partial_json_file 내부에서 rescue되므로 worker까지 전파되지 않는다. 결과적으로 디스크가 가득 찬 상태에서도 worker는 계속 다음 모델을 처리하며 264,144건의 에러 로그를 생성했다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-worker status:error @environment:production "Failed to save partial JSON"
text
service:cupixworks-worker status:error @environment:production "Failed to save Partial JSON" @function:save_partial_json_file

핵심 로그 항목 (Path A - Bulk save, 초기 에러):

json
{
  "timestamp": "2026-05-22T20:20:42.030Z",
  "message": "Failed to save partial JSON Element 15382518",
  "host": "ip-10-1-18-233.us-west-2.compute.internal",
  "class": "Element",
  "module": "DataWareHouse",
  "function": "bulk_partial_save_to_file",
  "error": "No space left on device @ rb_sysopen - /var/data_changes/elements/partial/2026-05-22/uswe2/1779481236606406-elements-15382518.json.gz"
}

핵심 로그 항목 (Path B - Individual save, 후기 에러):

json
{
  "timestamp": "2026-05-22T23:50:00Z",
  "message": "Failed to save Partial JSON Record 129985 to file /var/data_changes/records/partial/2026-05-22/uswe2/1779494399564283-records-129985.json.gz",
  "host": "ip-10-1-18-233.us-west-2.compute.internal",
  "class": "Record",
  "module": "DataWareHouse",
  "function": "save_partial_json_file",
  "error": "No space left on device @ rb_sysopen"
}

후속 영향 (Elasticsearch indexing 실패):

json
{
  "timestamp": "2026-05-22T23:41:00Z",
  "message": "ES bulk partial index dead",
  "class": "BulkPartialIndexDeadWorker",
  "function": "perform",
  "reason": "doc_missing_after_max_retries",
  "model": "ElementTrace"
}

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 디스크 용량 부족 (Errno::ENOSPC) 에러 메시지에 "No space left on device @ rb_sysopen" 명시. 단일 호스트 ip-10-1-18-233에서만 발생. 모든 모델(Element, ElementTrace, Record, Capture) 동시 실패. Confirmed
H2 파일 퍼미션 문제 (Errno::EACCES) 코드가 Errno::EACCES도 같은 rescue 절에서 처리 에러 메시지가 "No space left on device"이지 "Permission denied"가 아님. File.writable? 사전 검사 통과 (별도 에러 로그 없음) Rejected
H3 Worker가 존재하지 않는 모델을 조회하여 실패 Bulk worker에 retry 로직 존재 (models.count != ids.count 체크) 에러가 save_partial_json_file 단계에서 발생 — 모델 조회는 성공한 뒤 파일 쓰기에서 실패 Rejected
H4 특정 Element만의 데이터 이상 (큰 JSON) 클러스터 제목이 특정 Element ID를 참조 동일 시간대에 Element, ElementTrace, Record, Capture 전부 실패 — 특정 모델/레코드 문제가 아닌 인프라 문제 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 해당 호스트의 /var/data_changes/ 파티션 디스크 공간 확보 (오래된 partial JSON 파일 정리 또는 볼륨 확장)
  • 현재 디스크 사용량 모니터링하여 다른 호스트도 동일 상태인지 확인

단기 개선 (1주 이내)#

  • save_partial_json_file (partial_json.rb:145-146)에서 Errno::ENOSPC 발생 시 디스크 용량 정보를 로그에 포함하고, 연속 실패 시 circuit breaker 패턴을 적용하여 불필요한 반복 시도 방지
  • /var/data_changes/ 디렉토리의 자동 정리 cron job 추가 — 예: 7일 이상 된 파일 삭제 또는 S3 업로드 완료된 파일 정리
  • BulkSavePartialJsonToFileWorker에서 첫 번째 Errno::ENOSPC 감지 시 배치 처리를 즉시 중단하도록 early exit 로직 추가 (현재는 모든 레코드에 대해 실패를 반복)

장기 개선 (재발 방지)#

  • /var/data_changes/ 파티션에 대한 디스크 사용량 임계값 알림 설정 (80% 도달 시 경고)
  • Partial JSON 파일을 로컬 디스크 대신 직접 S3에 streaming upload하는 방식으로 아키텍처 변경 검토 — 로컬 디스크 의존성 제거
  • Worker 호스트의 EBS 볼륨 자동 확장 또는 ephemeral storage 정리 자동화

Monitoring#

추가할 메트릭/알림:

text
service:cupixworks-worker status:error "No space left on device"
text
service:cupixworks-worker status:error @function:save_partial_json_file
  • CloudWatch 또는 Datadog Agent를 통해 /var/data_changes/ 마운트 포인트의 disk usage percentage 모니터링
  • 임계값: 80% warning, 90% critical
  • BulkPartialIndexDeadWorker 에러 카운트 모니터링 — partial JSON 실패의 하류 지표

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard
  • 데이터 유실 가능성: partial JSON이 기록되지 않은 3.5시간 동안의 변경사항은 데이터 웨어하우스에 반영되지 않았음. 해당 모델들의 변경 이력이 data warehouse에서 누락될 수 있으며, full sync로 복구 필요 여부 확인 필요.