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#
- 2026-05-26T04:30:46Z — 최초 고지연 요청 감지 (APM threshold 초과)
- 2026-05-26T04:51:24Z — team gad의 대량 pano 처리 시작 (pano IDs 85878xxx 범위)
- 2026-05-26T06:08:23Z — 마지막 고지연 요청 기록
- 2026-05-26T06:40:37Z — 동일 엔드포인트 정상 응답 확인 (249ms, 작은 pano)
Error Log#
{
"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)
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
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
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
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 쿼리:
service:cupixworks-api check_tile_uploading @duration:>5000
시간 범위: 2026-05-26T03:30:00Z ~ 2026-05-26T07:00:00Z
50개 slow 요청의 통계:
{
"count": 50,
"avg": 18455.28,
"min": 16515.73,
"max": 32469.25
}
대표 로그 항목 (일관된 ~16.5초 패턴):
[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:10—tile_uploading_objects.size대신 S3 ListObjectsV2의KeyCount파라미터를 사용하거나,max_keys: 1로 존재 여부만 확인하고 별도로 count를 구하는 방식으로 변경app/services/cupix/storage_service.rb:73—delimiter: 'delimiter'는 의미 없는 값이므로 제거하거나 실제 구분자 (/)로 교체 검토
단기 개선 (1주 이내)#
check_tile_uploading!로직 개선: 전체 오브젝트 목록을 가져오지 않고,list_objects_v2의max_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 호출 횟수 및 지연 메트릭 추적
service:cupixworks-api check_tile_uploading @duration:>3000
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard — S3 호출 최적화는 로직 변경이 필요하지만 영향 범위가 명확하고 격리된 코드 경로