ES /docs

S3 list_objects_v2 pagination delay — large object listing

RCA: PanosController#check_tile_uploading S3 ListObjects Latency

Overview#

What Happened#

2026-05-26 04:30~06:08 UTC 사이에 cupixworks-api 서비스의 Api::V1::PanosController#check_tile_uploading 엔드포인트에서 평균 8.9초, 최대 32.4초의 응답 지연이 발생했다. us-west-2 리전에서 team "gad" (id: 590)의 대량 pano 업로드 처리 중 S3 list_objects_v2 API 호출이 병목이 되었다.

Quick Facts#

Field Value
resource_name Api::V1::PanosController#check_tile_uploading
top_frame app/models/concerns/tile/s3.rb:10
env production, us-west-2 / ap-southeast-1
deploy production-us-west-2-20260526t0452z0-dd7bd097-cupixworks

Affected Teams#

Team / Domain Error Count Impact
gad (team_id: 590) 50+ slow requests Pano tile upload 확인 요청이 16~32초 소요, 클라이언트 타임아웃 가능성

Timeline#

  1. 2026-05-26T04:30:46Z — 최초 고지연 요청 감지 (APM threshold 초과)
  2. 2026-05-26T04:51:24Z — team gad의 대량 pano 처리 시작 (pano IDs 85878xxx 범위)
  3. 2026-05-26T06:08:23Z — 마지막 고지연 요청 기록
  4. 2026-05-26T06:40:37Z — 동일 엔드포인트 정상 응답 확인 (249ms, 작은 pano)

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::PanosController#check_tile_uploading",
  "service": "cupixworks-api",
  "occurrences": 6,
  "avg_ms": 8909,
  "max_ms": 32389,
  "sample_trace_id": "4371587940249882874"
}

Impact#

  • Service: cupixworks-api
  • Team: gad (id: 590)
  • 발생 횟수: 50+ slow requests (6 exceeded APM threshold)
  • 최초 발생: 2026-05-26T04:30:46.721Z
  • 최근 발생: 2026-05-26T06:08:23.404Z

Root Cause Summary#

check_tile_uploading 엔드포인트는 pano의 tile 업로드 완료를 확인하기 위해 S3 list_objects_v2 API를 호출하여 해당 prefix 하위의 모든 오브젝트를 나열한다. 이 호출은 Cupix::StorageService.object_list에서 수행되며, 페이지네이션 없이 단일 호출로 모든 결과를 반환받는다. 고해상도 pano의 경우 tile 오브젝트 수가 수천~수만 개에 달할 수 있으며, S3 list_objects_v2는 한 번에 최대 1,000개만 반환하므로 AWS SDK가 내부적으로 여러 페이지를 순차적으로 가져온다. 로그에서 확인된 일관된 ~16.5초 지연 패턴은 S3 API rate limiting 또는 대량 오브젝트 prefix listing의 지연을 나타낸다. 32초 응답은 2회 연속 페이지네이션 사이클에 해당한다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/tilable_controller.rb:4
  • Repository delegation: app/repositories/concerns/tilable_repository/pano.rb:5
  • S3 listing: app/models/concerns/tile/s3.rb:9-15
  • StorageService call: app/services/cupix/storage_service.rb:64-79
  • Failure point: app/services/cupix/storage_service.rb:69-75 (S3 list_objects_v2 blocking call)
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
app/repositories/concerns/tilable_repository/pano.rb:5-22ruby
def check_tile_uploading(params)
  if params[:revision_type].present?
    case params[:revision_type]
    when 'enhanced_image'
      @model.increase_enhanced_image_revision
    else
      raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: "Invalid revision_type: #{params[:revision_type]}")
    end
  else
    @model.normal_image_revision = @model.tile_upload_revision
    @model.revision_type = 'normal'
  end

  @model.check_tile_uploading!

  @model.done_state if @model.respond_to?(:state_cloning?) && @model.state_cloning?
  @model
end
app/models/concerns/tile/s3.rb:9-15ruby
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
app/models/concerns/tile/s3.rb:17-23ruby
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
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

핵심 문제: list_objects_v2는 단일 응답으로 최대 1,000개의 오브젝트만 반환한다. AWS SDK의 .contents는 자동 페이지네이션을 수행하지만, 대량 오브젝트가 있는 prefix에서는 여러 번의 API 왕복이 필요하다. check_tile_uploading!은 단순히 오브젝트 존재 여부와 개수만 필요한데, 전체 목록을 메모리로 로드한 후 .size를 호출한다.

또한 delimiter: 'delimiter'는 의미 없는 값으로, 실질적으로 delimiter가 없는 것과 동일하게 동작하여 모든 하위 키를 flat하게 나열한다.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-api check_tile_uploading @duration:>5000

시간 범위: 2026-05-26T03:30:00Z ~ 2026-05-26T07:00:00Z

50개 slow 요청의 통계:

json
{
  "count": 50,
  "avg": 18455.28,
  "min": 16515.73,
  "max": 32469.25
}

대표 로그 항목 (일관된 ~16.5초 패턴):

text
[200] PUT /api/v1/panos/85876769/check_tile_uploading duration=16555.39ms db=22.75ms view=0.08ms team=gad
[200] PUT /api/v1/panos/85876792/check_tile_uploading duration=16557.05ms db=18.44ms view=0.07ms team=gad
[200] PUT /api/v1/panos/85876794/check_tile_uploading duration=32463.36ms db=?ms view=?ms team=gad

DB 시간 (~22ms)과 view 시간 (0.07ms)은 미미하며, 나머지 16,500ms+ 전부가 S3 API 호출에 소요됨을 확인. 정상적인 소규모 pano는 155249ms에 응답 (같은 시간대, pano 13288583 등).

동일 team (gad, id: 590)에서 연속적인 pano IDs (85876769~85878191)로 대량 처리 중 발생.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 S3 list_objects_v2가 대량 오브젝트 prefix에서 느림 일관된 ~16.5s 지연 (S3 pagination 단위), db=22ms로 DB는 정상, 정상 pano는 155ms 응답 Confirmed
H2 DB 쿼리가 느려서 전체 응답 지연 duration이 높음 db=22ms로 DB는 정상, view=0.07ms Rejected
H3 네트워크 문제 (cross-region latency) 2개 리전에서 발생 모든 slow 요청이 동일한 ~16.5s 패턴, cross-region이면 더 불규칙할 것 Rejected
H4 동시 대량 요청으로 인한 Rails 스레드 고갈 같은 team에서 수십 개 연속 요청 각 요청이 독립적으로 ~16.5s 소요, 큐잉이면 점진적 증가 패턴 예상 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • app/models/concerns/tile/s3.rb:10tile_uploading_objects.size 대신 S3 ListObjectsV2의 KeyCount 파라미터를 사용하거나, max_keys: 1로 존재 여부만 확인하고 별도로 count를 구하는 방식으로 변경
  • app/services/cupix/storage_service.rb:73delimiter: 'delimiter'는 의미 없는 값이므로 제거하거나 실제 구분자 (/)로 교체 검토

단기 개선 (1주 이내)#

  • check_tile_uploading! 로직 개선: 전체 오브젝트 목록을 가져오지 않고, list_objects_v2max_keys 파라미터로 필요한 만큼만 조회. tile_size 설정이 목적이면, SDK의 페이지네이션 카운팅 대신 S3 Inventory 또는 별도 카운터 관리 방식 도입
  • 타임아웃 설정: S3 클라이언트에 적절한 timeout 설정 추가하여 무한 대기 방지

장기 개선 (재발 방지)#

  • Tile 업로드 완료 확인을 S3 listing이 아닌 이벤트 기반으로 전환 (S3 Event Notification → Lambda/SQS → tile count 업데이트)
  • 대량 pano 처리 시 check_tile_uploading을 비동기 worker로 이동하여 API 응답 시간에서 S3 지연을 분리
  • StorageService.object_list 전반에 대한 페이지네이션 타임아웃 및 max_keys 기본값 도입

Monitoring#

  • check_tile_uploading 엔드포인트 P95 latency 알림 (threshold: 3000ms)
  • S3 list_objects_v2 호출 횟수 및 지연 메트릭 추적
text
service:cupixworks-api check_tile_uploading @duration:>3000

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — S3 호출 최적화는 로직 변경이 필요하지만 영향 범위가 명확하고 격리된 코드 경로