ES /docs

Api::V1::CapturesController#check_zip_uploading (avg 1143ms, max 1198ms)

RCA: Api::V1::CapturesController#check_zip_uploading Latency

Overview#

What Happened#

2026-05-26 08:58~09:32 UTC 사이에 cupixworks-api 서비스의 check_zip_uploading 엔드포인트가 평균 1,143ms (최대 1,198ms) 응답 시간을 기록했다. us-west-2 리전에서 3건의 느린 요청이 감지되었으며, 동일 시간대 전체 78건의 요청 중 P95 지연이 1,171ms에 달했다.

Quick Facts#

Field Value
resource_name Api::V1::CapturesController#check_zip_uploading
top_frame app/models/concerns/zippable/capture.rb:48-55
runtime Ruby on Rails (cupixworks-api)
env production, us-west-2

Timeline#

  1. 2026-05-26T08:58:26Z — 최초 느린 요청 감지 (1,143ms+)
  2. 2026-05-26T09:32:55Z — 마지막 느린 요청 기록
  3. 2026-05-27 — RCA 분석 완료

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::CapturesController#check_zip_uploading",
  "service": "cupixworks-api",
  "occurrences": 3,
  "avg_ms": 1143,
  "max_ms": 1198,
  "sample_trace_id": "2722044582296379934"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 3 (500ms 초과 요청)
  • 최초 발생: 2026-05-26T08:58:26.826Z
  • 최근 발생: 2026-05-26T09:32:55.793Z
  • 영향 범위: cupix-agent 데스크톱 클라이언트에서 zip 업로드 완료 확인 시 사용자 대기 시간 증가. 특히 us-west-2의 secc(Samsung) 팀 요청에서 평균 879ms 지연 관측.

Root Cause Summary#

check_zip_uploading 엔드포인트는 캡처의 pano 수에 비례하여 S3 HEAD 요청을 순차적으로 실행한다. pano_per_page: 200으로 페이지를 나누어 각 페이지별 zip 파일 존재 여부를 Aws::S3::Object#exists?로 확인하는데, 각 요청마다 50-200ms의 네트워크 라운드트립이 발생한다. 1,000개의 done pano가 있는 캡처의 경우 5회의 순차 S3 HEAD 요청이 필요하며, 이것이 전체 응답 시간의 70-80%를 차지한다. DB 시간은 평균 45ms에 불과하여 전체 지연의 10% 미만이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/zippable_controller.rb:4
  • Before action: app/controllers/api/v1/captures_controller.rb:140 (set_capture — 14 LEFT JOIN 포함 permission query)
  • Repository: app/repositories/concerns/zippable_repository.rb:4-17
  • Failure point (latency source): app/models/concerns/zippable/capture.rb:48-55

컨트롤러는 ZippableController concern을 통해 check_zip_uploading을 호출한다:

app/controllers/concerns/zippable_controller.rb:4-11ruby
def check_zip_uploading
  @model = repository_instance.check_zip_uploading

  render_api Renderable.new({
    contents: @model,
    serializer_option: @serializer_option
  })
end

Repository에서 zip 상태를 확인하고 모델 메서드를 호출한다:

app/repositories/concerns/zippable_repository.rb:4-17ruby
def check_zip_uploading
  case @model.zip_state_name
  when :zipping
    unless @model.check_zip_uploading
      raise Cupix::Errors::Resource.new(code: 'RESC10000', reason: 'Zip does not uploaded')
    end
    @model.done_zip_state
  else
    raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: "Invalid state: #{@model.zip_state}")
  end
  @model
end

핵심 병목 지점 — 페이지 수만큼 S3 HEAD 요청을 순차 실행:

app/models/concerns/zippable/capture.rb:45-61ruby
def check_zip_uploading
  pano_count, pano_page_count = self.get_pano_count(pano_per_page: 200)

  pano_page_count.times do |page|
    page += 1
    _object = self.zip_object(ver: zip_revision + 1, page: page)

    unless _object.exists?   # S3 HEAD request — 50-200ms per call
      self.missing_zip_state
      return false
    end
  end

  increase_zip_revision
  save_zip_source_timestamp
  true
end

zip_objectAws::S3::Object를 생성하고, exists? 호출 시 S3 API의 HeadObject를 실행한다:

app/services/cupix/storage_service.rb:29-36ruby
def object(storage_option: nil, **kwargs)
  opts = parse_storage_option(storage_option).merge(kwargs)
  opts[:force_path_style] = true
  check_required_params(opts, %i[region bucket_name key])
  Aws::S3::Object.new(opts)
end

pano 수를 구하는 쿼리:

app/models/concerns/zippable/capture.rb:107-112ruby
def get_pano_count(pano_per_page: nil)
  pano_count = self.panos.where(state: :done).count
  pano_page_count = (pano_count.to_f / pano_per_page).ceil
  [pano_count, pano_page_count]
end

기대 동작: check_zip_uploading은 zip 업로드 완료를 빠르게 확인해야 한다. 실제 동작: pano 수에 비례하는 순차 S3 HEAD 요청으로 인해 선형적으로 지연이 증가한다.

Log Evidence#

Datadog APM 메트릭에서 확인한 시간 분배:

text
Datadog query: service:cupixworks-api resource_name:"Api::V1::CapturesController#check_zip_uploading" env:production @duration:>500ms
Time range: 2026-05-26T08:00:00Z to 2026-05-26T10:00:00Z

주요 관측 결과:

text
Total requests in window: 78
Mean duration: 467ms
P95 duration: 1,171ms
Max duration: 1,342ms
Mean DB time: 45ms (전체의 ~10%)
Mean non-DB time: 422ms (전체의 ~90%)
Serialization: 0ms
View rendering: 0.05-0.09ms

리전별 지연 비교 — us-west-2에서 2배 높은 지연:

text
us-west-2: avg 706ms, max 1,215ms (26 requests)
ap-southeast-2: avg 351ms, max 1,342ms (50 requests)

InvalidState 에러로 short-circuit된 경우 (S3 호출 없음) 32ms 응답:

text
status:400, duration:32ms, error:"Cupix::Errors::InvalidState: Invalid state: done"

이 증거는 S3 HEAD 요청이 지연의 지배적 원인임을 확인한다 — S3 호출을 건너뛰면 32ms, 실행하면 500-1200ms.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 순차 S3 HEAD 요청이 지연의 주 원인 DB 시간 45ms vs 전체 467ms 평균; short-circuit 시 32ms; 리전별 S3 지연 차이 반영됨; capture.rb:48-55의 루프 구조 Confirmed
H2 DB 쿼리 (permission_joins 14 LEFT JOIN)가 병목 set_capture에서 복잡한 쿼리 실행; 일부 요청에서 214ms DB 시간 평균 DB 시간 45ms로 전체의 10% 미만; slow query 로그 없음 Rejected
H3 Lambda 호출 (Zippable#run_zip)이 지연 유발 같은 시간대 Lambda 호출 로그 존재 Lambda는 check_zip_uploading이 아닌 별도 엔드포인트에서 호출됨; check_zip_uploading 코드에 Lambda 호출 없음 Rejected
H4 복합 인덱스 부재로 pano COUNT 쿼리 느림 (capture_id, state) 복합 인덱스 없음 COUNT 쿼리는 전체 시간 중 5-20ms 추정; DB 자체가 병목이 아님 Rejected (minor contributor)

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: app/models/concerns/zippable/capture.rb:48-55
  • 방향: 순차 S3 HEAD 요청을 병렬로 실행. Ruby의 Concurrent::Promises 또는 Thread pool을 사용하여 모든 페이지의 exists? 체크를 동시에 수행. 5회 순차 요청(250-1000ms)이 1회 병렬 배치(50-200ms)로 감소.

단기 개선 (1주 이내)#

  • S3 list_objects_v2를 prefix 기반으로 한 번 호출하여 모든 페이지 존재 여부를 단일 API 호출로 확인. 이렇게 하면 페이지 수와 관계없이 1회의 S3 API 호출로 완료 가능.
  • save_zip_source_timestamp에서 zip_source_timestamp 조회 시 get_pano_count에서 이미 조회한 pano 정보를 재활용하여 중복 DB 쿼리 제거.

장기 개선 (재발 방지)#

  • zip 업로드 완료를 클라이언트가 polling하는 대신, S3 Event Notification + SNS/SQS 기반의 push 모델로 전환. zip 파일이 S3에 업로드되면 이벤트가 발생하여 서버가 상태를 업데이트하고, 클라이언트는 상태만 조회하면 됨.
  • panos 테이블에 (capture_id, state, updated_at) 복합 인덱스 추가.

Monitoring#

  • S3 HEAD 요청 지연을 트래킹하는 커스텀 메트릭 추가
  • Datadog APM에서 check_zip_uploading P95 지연 알림 설정:
text
avg(last_5m):p95:trace.rack.request{service:cupixworks-api,resource_name:api::v1::capturescontroller#check_zip_uploading} > 1000

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard — S3 호출 병렬화 또는 list_objects_v2 전환은 로직 변경 범위가 제한적이며 기존 동작을 보존할 수 있다.