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#
- 2026-05-26T08:58:26Z — 최초 느린 요청 감지 (1,143ms+)
- 2026-05-26T09:32:55Z — 마지막 느린 요청 기록
- 2026-05-27 — RCA 분석 완료
Error Log#
{
"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을 호출한다:
def check_zip_uploading
@model = repository_instance.check_zip_uploading
render_api Renderable.new({
contents: @model,
serializer_option: @serializer_option
})
end
Repository에서 zip 상태를 확인하고 모델 메서드를 호출한다:
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 요청을 순차 실행:
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_object는 Aws::S3::Object를 생성하고, exists? 호출 시 S3 API의 HeadObject를 실행한다:
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 수를 구하는 쿼리:
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 메트릭에서 확인한 시간 분배:
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
주요 관측 결과:
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배 높은 지연:
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 응답:
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또는Threadpool을 사용하여 모든 페이지의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_uploadingP95 지연 알림 설정:
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전환은 로직 변경 범위가 제한적이며 기존 동작을 보존할 수 있다.