Api::V1::FloorplansController#check_tile_uploading (avg 1241ms, max 1403ms)
RCA: FloorplansController#check_tile_uploading Latency
Overview#
What Happened#
2026-05-26 03:26~09:04 UTC 사이에 cupixworks-api 서비스의 Api::V1::FloorplansController#check_tile_uploading 엔드포인트에서 평균 1466ms, 최대 1862ms의 응답 지연이 14건 발생했다. 해당 엔드포인트는 floorplan 타일 업로드 완료를 확인하는 API로, S3 ListObjectsV2 호출이 주된 병목이다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::FloorplansController#check_tile_uploading |
| top_frame | app/models/concerns/tile/s3.rb:17-22 |
| avg_duration | 1466ms |
| max_duration | 1862ms |
| env | production (us-west-2, ap-southeast-2) |
Timeline#
- 2026-05-26T03:26:26Z — 최초 고지연 trace 감지
- 2026-05-26T09:04:54Z — 마지막 고지연 trace 감지
- 2026-05-27 — RCA 수행
Error Log#
{
"resource_name": "Api::V1::FloorplansController#check_tile_uploading",
"service": "cupixworks-api",
"occurrences": 9,
"avg_ms": 1241,
"max_ms": 1403,
"sample_trace_id": "2510671473098672586"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 14
- 최초 발생: 2026-05-26T03:26:26.746Z
- 최근 발생: 2026-05-26T09:04:54.061Z
- 영향: Floorplan 타일 업로드 완료 확인 요청의 응답 시간이 500ms SLO를 크게 초과. 사용자(클라이언트 앱)가 업로드 완료 확인 대기 시간 증가를 체감할 수 있음.
Root Cause Summary#
check_tile_uploading 엔드포인트의 핵심 로직인 Tile::S3#tile_uploading_objects가 AWS S3 ListObjectsV2 API를 호출하여 업로드된 타일 오브젝트 수를 확인한다. 이 호출에서 두 가지 요인이 지연을 유발한다: (1) delimiter: 'delimiter'라는 비정상적 delimiter 값이 설정되어 있어 S3가 논리적 폴더 그룹핑을 수행할 수 없고 모든 오브젝트를 flat하게 열거하며, (2) 고해상도 floorplan의 경우 타일 수가 수백~수천 개에 달해 S3 listing 자체가 느려진다. 특히 ap-southeast-2 리전에서 요청이 올 때 cross-region S3 접근으로 네트워크 지연이 추가된다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/concerns/tilable_controller.rb:4 - Repository 호출:
app/repositories/concerns/tilable_repository.rb:5 - S3 listing:
app/models/concerns/tile/s3.rb:17-22 - S3 client:
app/services/cupix/storage_service.rb:64-79
1. Controller가 repository의 check_tile_uploading을 호출:
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
2. Repository가 모델의 check_tile_uploading!을 호출:
def check_tile_uploading(params)
@model.check_tile_uploading!
@model.done_state if @model.respond_to?(:state_cloning?) && @model.state_cloning?
@model
end
3. 모델에서 S3 listing 수행 (병목 지점):
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
4. S3 ListObjectsV2 API 호출 — 비정상 delimiter와 pagination 미처리:
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 listing이 빠르게 완료되어 200ms 이내로 응답 반환.
실제 동작: S3 ListObjectsV2 호출이 800ms~1800ms+ 소요. delimiter: 'delimiter'는 실질적 구분자 역할을 하지 못하며(실제 key에 "delimiter"라는 문자열이 포함되지 않으므로), S3가 prefix 아래 모든 오브젝트를 flat하게 반환. 또한 단일 API 호출만 수행하므로 1000개 이상 오브젝트가 있을 경우 truncation이 발생하나 이를 처리하지 않음.
Log Evidence#
Datadog APM 메트릭으로 확인된 실제 응답 시간:
Datadog query: avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::floorplanscontroller_check_tile_uploading}
Time range: 24h
대표적 측정값 (단위: seconds):
0.884, 1.116, 0.899, 0.775, 1.064, 1.315, 2.096, 2.544,
0.596, 0.614, 0.739, 0.882, 0.785, 1.346, 1.146, 0.832
최대 2.544초, 평균 약 1초의 응답 시간이 관측됨. 500ms SLO 기준 대부분의 요청이 초과.
Datadog 로그에서 확인된 실제 요청 패턴:
Datadog query: service:cupixworks-api "floorplans" "check_tile_uploading"
Time range: 2026-05-26T02:00:00Z to 2026-05-26T10:00:00Z
[200] PUT /api/v1/floorplans/87009/check_tile_uploading (Api::V1::FloorplansController#check_tile_uploading)
[200] PUT /api/v1/floorplans/87006/check_tile_uploading (Api::V1::FloorplansController#check_tile_uploading)
[200] PUT /api/v1/floorplans/87007/check_tile_uploading (Api::V1::FloorplansController#check_tile_uploading)
[200] PUT /api/v1/floorplans/87008/check_tile_uploading (Api::V1::FloorplansController#check_tile_uploading)
[200] PUT /api/v1/floorplans/87005/check_tile_uploading (Api::V1::FloorplansController#check_tile_uploading)
모든 요청이 HTTP 200으로 성공하나, 응답 시간이 느림. Floorplan ID 87000~87009 범위의 연속적 업로드가 관측됨.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | S3 ListObjectsV2 호출이 floorplan 타일 수가 많아 느림 | APM 메트릭에서 일관된 800msobject_list 메서드가 단일 S3 API 호출에 의존; 고해상도 floorplan은 수백 |
— | Confirmed |
| H2 | delimiter: 'delimiter' 설정으로 인한 불필요한 S3 full listing |
코드에서 delimiter: 'delimiter'로 설정됨 (storage_service.rb:73); 이 값은 실제 오브젝트 key에 존재하지 않아 그룹핑 효과 없음; delimiter가 없는 것과 동일하게 동작 |
delimiter를 올바르게 '/'로 설정해도 모든 타일이 같은 prefix 아래 flat하게 저장되므로 큰 차이 없을 수 있음 | Confirmed |
| H3 | Cross-region S3 접근으로 인한 네트워크 지연 | 클러스터가 us-west-2와 ap-southeast-2 모두에서 발생; parse_storage_option이 storage의 region 설정을 사용하므로 API 서버와 S3 bucket이 다른 region일 수 있음 |
두 리전 모두에서 유사한 패턴을 보이므로 네트워크 지연보다는 S3 listing 자체가 주 원인 | Inconclusive |
| H4 | DB 쿼리 지연 (permission_joins 등 복잡한 JOIN) | FloorplanRepository.permission_joins에 다수의 JOIN과 subquery 존재 |
check_tile_uploading은 이미 set_floorplan before_action에서 로드된 모델을 사용; 별도 DB 쿼리 없이 S3 호출만 수행 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/services/cupix/storage_service.rb:73—delimiter: 'delimiter'를 제거하거나 의미 있는 값으로 변경. 현재 값은 기능적으로 무의미하나 S3 API 파라미터 파싱에 불필요한 오버헤드를 줄 수 있음.app/models/concerns/tile/s3.rb:10—tile_uploading_objects.size대신 S3의list_objects_v2에max_keys: 1파라미터를 추가하여 "타일이 존재하는지"만 확인하는 fast-path를 구현. 전체 오브젝트 목록이 필요한 것은tile_size계산뿐이므로,has_attribute?(:tile_size)가 false인 경우 존재 여부만 확인하면 됨.
단기 개선 (1주 이내)#
tile_size계산이 필요한 경우에도,list_objects_v2의KeyCount응답 필드를 활용하여 오브젝트 전체를 메모리에 로드하지 않고 개수만 파악하도록 변경. 단, pagination이 필요한 경우 (1000개 초과)continuation_token으로 반복 호출하여 총 수를 합산해야 함.object_list메서드에 pagination 처리를 추가하여 1000개 초과 오브젝트도 정확히 카운트.
장기 개선 (재발 방지)#
- 타일 업로드 시 클라이언트가 업로드한 타일 수를 request body로 전달하고, S3 listing 없이 DB에
tile_size를 기록하는 방식으로 전환. S3 listing은 비동기 검증용으로만 사용. - S3 event notification (또는 EventBridge) 기반으로 타일 업로드 완료를 감지하여 polling 대신 event-driven 방식으로 전환 검토.
Monitoring#
- APM 메트릭 알림 추가:
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::floorplanscontroller_check_tile_uploading} > 1.0
- S3 ListObjectsV2 latency 모니터 (CloudWatch):
AWS/S3 FirstByteLatency for bucket with prefix filter
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard — S3 호출 최적화는
object_list메서드와check_tile_uploading!로직 변경으로 해결 가능하나, pagination 처리와 기존 tile_size 의존 코드 검토가 필요.