ES /docs

Api::V1::PanosController#check_tile_uploading (avg 16622ms, max 16622ms)

RCA: Api::V1::PanosController#check_tile_uploading slow request (16.6 s)

Overview#

What Happened#

2026-06-24 20:35 KST에 cupixworks-apiPUT /api/v1/panos/:id/check_tile_uploading 요청 한 건이 16,622 ms 동안 실행되어 latency 임계치를 초과했다. 같은 시간대에 동일 엔드포인트 호출이 분당 수십 건씩 정상 응답(200)으로 처리되고 있었으므로 systemic outage가 아니라 개별 trace의 외부 의존성 지연으로 보인다.

Quick Facts#

Field Value
resource_name Api::V1::PanosController#check_tile_uploading
service cupixworks-api
cluster_type latency
avg_duration_ms 16622
max_duration_ms 16622
sample_trace_id 4385947387163049037
env production, us-west-2
tenant cupix

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (Pano tile upload finalize) 1 단일 클라이언트가 타일 업로드 검증 단계에서 약 16 초 대기. 200 응답으로 종료되어 데이터 손상은 없음

Timeline#

  1. 2026-06-24 20:35:40 KST — trace 4385947387163049037 시작. 동일 컨트롤러로의 다른 200 응답이 같은 초에 다수 기록됨
  2. 2026-06-24 20:35:56 KST — 16,622 ms 경과 후 응답 완료 (Datadog latency cluster 트리거)
  3. 2026-06-24 21:35 KST — error-sweeper collector가 cluster 생성

active service-level incident 2026-06-24-svc-cupixworks-api--unknown-1 (started 2026-06-24 14:02 KST)이 같은 서비스에 열려 있어 이 클러스터도 동일 incident에 묶였다. 해당 incident는 root_cause_types: ["unknown"]으로 분류되어 있어 강제 연관은 아니다.

Error Log#

Datadog Logs

errors/f3667392-99ff-475a-8000-ec9ca6cc0463.mdjson
{
  "resource_name": "Api::V1::PanosController#check_tile_uploading",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 16622,
  "max_ms": 16622,
  "sample_trace_id": "4385947387163049037"
}

이 cluster는 exception이 아니라 latency trace 한 건이다. Datadog Logs Search에서 동일 trace id로 조회한 결과 매칭 로그가 없었다 (service:cupixworks-api "4385947387163049037" → 0건). 즉 application-level error log 없이 200으로 완료된 느린 요청이다.

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-06-24 20:35 KST
  • 최근 발생: 2026-06-24 20:35 KST

Root Cause Summary#

PanosController#check_tile_uploading은 호출될 때마다 S3 list_objects_v2 API를 동기적으로 호출하여 업로드된 타일 객체 개수를 센다. 이 요청 한 건에서 S3 list 호출이 약 16 초 동안 블로킹된 것이 직접적 latency 원인이다. 동시간대에 같은 엔드포인트의 다른 호출은 정상 응답되었으므로 코드/데이터 버그가 아니라 S3 측 transient slowness 또는 특정 prefix의 비정상적으로 큰 키 개수(첫 page를 채우기 위한 S3 내부 스캔 시간 증가)가 유력하다. exception이 발생하지 않은 점, 후속 동일 trace가 재발하지 않은 점, error/warn 로그가 비어있는 점이 이를 뒷받침한다.

Technical Analysis#

Code Path#

Entry point: app/controllers/concerns/tilable_controller.rb:4

app/controllers/concerns/tilable_controller.rb:4-11ruby
def check_tile_uploading
  @model = repository_instance.check_tile_uploading(params.permit(:revision_type).to_h)

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

Repository delegation: app/repositories/concerns/tilable_repository.rb:4

app/repositories/concerns/tilable_repository.rb:4-9ruby
def check_tile_uploading(params)
  @model.check_tile_uploading!

  @model.done_state if @model.respond_to?(:state_cloning?) && @model.state_cloning?
  @model
end

Model logic — 여기서 S3를 호출한다: app/models/concerns/tile/s3.rb:9-15

app/models/concerns/tile/s3.rb:9-23ruby
def check_tile_uploading!
  objects_count = tile_uploading_objects.size
  self.tile_size = objects_count if self.has_attribute?(:tile_size)
  raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: 'No tile objects found in S3') if objects_count.zero?

  self.uploaded_tile_state!
end

def tile_uploading_objects
  Cupix::StorageService.object_list(
    storage_option: storage_option,
    bucket_name: storage_option.s3_hosting_bucket_name,
    prefix: tile_object_key_base(ver: tile_upload_revision)
  )
end

Failure point — S3 호출 자체: app/services/cupix/storage_service.rb:64-79

app/services/cupix/storage_service.rb:64-79ruby
def object_list(storage_option: nil, **kwargs)
  opts = parse_storage_option(storage_option).merge(kwargs)

  check_required_params(opts, %i[region bucket_name prefix])

  client(storage_option: storage_option)
    .list_objects_v2(
      bucket: storage_option.s3_hosting_bucket_name,
      prefix: opts[:prefix],
      delimiter: 'delimiter'
    )
    .contents
    .reject do |object|
      object.key.end_with?('/')
    end
end

기대 동작: S3 list_objects_v2가 prefix 아래 키 수백~수천 개에 대해 100 ms 이내에 첫 page를 반환한다. PUT 요청은 1 초 안에 200을 응답해야 한다.

실제 동작: 동일 호출이 16,622 ms 동안 블로킹되었다. 코드 경로상 추가 외부 호출은 없고 (DB write는 uploaded_tile_state! 한 번뿐), 90 % 이상의 시간이 S3 round-trip에서 소요된 것으로 추정된다. Ruby AWS SDK는 기본 retry 정책이 있어 503/timeout 시 backoff 재시도를 수행하므로, 한 번의 transient slowness가 두세 차례 재시도로 누적되면 십수 초로 늘어날 수 있다.

Log Evidence#

쿼리 1 — trace id로 application log 조회 (0건):

text
service:cupixworks-api "4385947387163049037"
time: 2026-06-24T11:00:00Z .. 2026-06-24T12:30:00Z

쿼리 2 — 같은 endpoint의 동시간대 호출 빈도 (정상 200 다수):

text
service:cupixworks-api "check_tile_uploading"
time: 2026-06-24T11:00:00Z .. 2026-06-24T12:30:00Z

샘플 응답 (incident 분 ±30 초):

text
2026-06-24 20:35:59 KST  info  [200] PUT /api/v1/panos/89880130/check_tile_uploading (Api::V1::PanosController#check_tile_uploading)
2026-06-24 20:35:59 KST  info  [200] PUT /api/v1/panos/89879205/check_tile_uploading (Api::V1::PanosController#check_tile_uploading)
2026-06-24 20:35:59 KST  info  [200] PUT /api/v1/panos/89879702/check_tile_uploading (Api::V1::PanosController#check_tile_uploading)
2026-06-24 20:35:59 KST  info  [200] PUT /api/v1/panos/89880200/check_tile_uploading (Api::V1::PanosController#check_tile_uploading)
2026-06-24 20:35:59 KST  info  [200] PUT /api/v1/panos/553795/check_tile_uploading  (Api::V1::PanosController#check_tile_uploading)

쿼리 3 — cupixworks-api warn/error 동시간대 (0건):

text
service:cupixworks-api status:error
time: 2026-06-24T11:30:00Z .. 2026-06-24T11:45:00Z

쿼리 4 — tile/S3 관련 warn (0건):

text
service:cupixworks-api "S3" "tile" status:warn
time: 2026-06-24T10:00:00Z .. 2026-06-24T13:00:00Z

종합: 같은 분에 다수의 정상 200 응답이 기록되어 서비스 전체 가용성은 정상이었다. 한 trace만 16 초를 기록했고 관련 exception/warn 로그가 없다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 S3 list_objects_v2 transient slowness (network jitter, AWS-side throttling, 또는 retry backoff 누적) Code path에서 외부 호출은 S3 한 곳뿐 (app/models/concerns/tile/s3.rb:18-22). 동시간대 다른 호출은 정상. error/warn 로그 없음. AWS SDK 기본 retry는 ms→초 단위로 누적 가능 직접적 S3 metric은 cluster 파일에 없음 — APM trace span 확인 필요 (uncertain — needs verification) Confirmed (most likely)
H2 대상 pano의 prefix에 비정상적으로 많은 타일 키가 존재해 첫 page scan 시간이 폭증 tile_object_key_base 는 prefix 하나이며 페이지네이션 없이 .contents.reject로 첫 page만 사용. 단일 prefix scan 시간은 키 분포에 따라 길어질 수 있음 trace id가 로그에 남지 않아 어느 pano id인지 식별 불가. 16 초는 단일 S3 page 응답으로는 매우 길어 단독 원인 가능성은 낮음 Inconclusive
H3 DB transaction lock 또는 uploaded_tile_state! write 지연 동시간대 동일 endpoint 호출 다수 정상 code path상 write는 단일 row update. PostgreSQL slow query 흔적 없음 (logs 0건). 코드상 lock 획득 구간 없음 Rejected
H4 부모 incident 2026-06-24-svc-cupixworks-api--unknown-1의 공통 원인 (예: 호스트 GC/CPU spike) 같은 서비스에서 동시간대 다수 cluster가 묶임 부모 incident는 root_cause_types: ["unknown"]이며 다른 클러스터는 별개 endpoint. 공통 호스트 지표 미확인 (uncertain) Inconclusive
H5 Application-level bug (무한 루프, 잘못된 query) 없음 code path가 단순 (controller → repo → model.check_tile_uploading! → S3 list → state update). exception 없음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 직접 조치는 불필요. 단일 trace, 200 응답, 데이터 손상 없음
  • APM에서 trace 4385947387163049037의 span breakdown을 확인하여 16 초가 S3 호출에 집중되었는지 검증
  • 같은 cluster가 24 시간 내 재발하면 aws.s3.request.duration{bucket:<s3_hosting_bucket_name>} p95를 함께 검토

단기 개선 (1주 이내)#

  • Cupix::StorageService.object_list 호출에 명시적 timeout/circuit breaker 적용 검토. 현재 코드(app/services/cupix/storage_service.rb:64-79)는 SDK 기본값에 의존하므로 retry 누적 시 십수 초까지 늘어날 수 있음
  • check_tile_uploading!에서 objects_count.zero? 판정만 필요하다면 list_objects_v2(max_keys: 1) 한 번 호출로 단축 가능 (app/models/concerns/tile/s3.rb:10). 단, tile_size = objects_count 갱신 로직과 양립하지 않으므로 tile_size 의존 모델을 식별한 뒤 적용 — needs verification

장기 개선 (재발 방지)#

  • 타일 검증을 동기 API에서 분리해 background worker (Sidekiq)로 이전하고, 클라이언트는 polling 또는 webhook으로 결과 수신
  • S3 list 호출량을 줄이기 위해 client-side에서 업로드 완료 manifest를 전송하도록 protocol 개편 검토

Monitoring#

다음 쿼리를 Datadog dashboard timeseries widget에 등록한다.

S3 list 호출 p95 (이 endpoint가 동기 호출하는 의존):

text
avg:aws.s3.request.duration{servicename:cupixworks-api}.rollup(avg, 60)

check_tile_uploading p95 duration:

text
p95:trace.rack.request{service:cupixworks-api,resource_name:api::v1::panoscontroller#check_tile_uploading}

check_tile_uploading 호출 처리량:

text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::panoscontroller#check_tile_uploading}.as_count()

10 초 이상 걸린 trace 개수 (alert candidate):

text
sum:trace.rack.request.duration.by.resource_service.95p{service:cupixworks-api,resource_name:api::v1::panoscontroller#check_tile_uploading}

(주의: cluster 생성 시점 기준으로 위 trace.* metric 쿼리는 빈 결과를 반환했다. APM metric tag 명을 환경에서 재확인 후 dashboard 등록 — needs verification.)

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 없이 monitoring 보강 권장. 재발 시 단기 개선 항목 검토)