Api::V1::AerialPhotosController#check_uploading (avg 1267ms, max 1434ms)
RCA: Api::V1::AerialPhotosController#check_uploading Latency
Overview#
What Happened#
2026-05-27 09:14~09:15 UTC에 cupixworks-api 서비스의 AerialPhotosController#check_uploading 엔드포인트에서 평균 1267ms, 최대 1434ms의 응답 지연이 발생했다. 단일 사용자(Nestle 팀)가 CupixConnect 데스크탑 앱에서 Aerial Map 451에 대해 약 29개 aerial photo의 업로드 상태를 동시에 확인하면서, S3 API 호출과 DB 부하가 겹쳐 지연이 증폭되었다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::AerialPhotosController#check_uploading |
| top_frame | app/models/concerns/storagable/resource.rb:163 |
| env | production, us-west-2 |
| avg_duration | 1267ms |
| max_duration | 1434ms |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| Nestle (aerial mapping) | 2 (APM), 29 (log) | 업로드 확인 응답 지연, 사용자 체감 대기 시간 증가 |
Timeline#
- 09:14:49Z — 첫 번째 slow trace 감지 (APM threshold >500ms 초과)
- 09:14:49~09:15:20Z — 29건의 check_uploading 요청 burst 발생 (단일 세션)
- 09:14:47~09:14:59Z — 동시에 Pano/Pointcloud/Record
_update_document대량 발생으로 DB 부하 - 09:15:22Z — AerialMap 451 processing step function 시작
- 09:15:20Z — 마지막 slow request 완료
Error Log#
{
"resource_name": "Api::V1::AerialPhotosController#check_uploading",
"service": "cupixworks-api",
"occurrences": 2,
"avg_ms": 1267,
"max_ms": 1434,
"sample_trace_id": "1461928758746047156"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 2 (APM threshold 초과), 29 (전체 burst)
- 최초 발생: 2026-05-27T09:14:49.358Z
- 최근 발생: 2026-05-27T09:14:58.080Z
Root Cause Summary#
단일 CupixConnect 클라이언트 세션에서 Aerial Map 451에 속한 29개 aerial photo에 대해 4회 수행하며, 동시에 check_uploading 요청을 동시 burst로 전송했다. 각 요청은 S3 HEAD 요청(exists? + etag/size/content_type)을 2Pano#_update_document, Pointcloud#_update_document 등 대량 DB write가 발생해 DB connection 경합이 겹쳤다. S3 레이턴시(요청당 50500ms)와 DB 경합(16206ms)이 합산되어 총 응답 시간이 1000ms를 초과했다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/aerial_photos_controller.rb:33 - Repository:
app/repositories/aerial_photo_repository.rb:206 - Model concern:
app/models/concerns/resourcable/aerial_photo.rb:28 - Failure point (bottleneck):
app/models/concerns/storagable/resource.rb:163
def check_uploading
video = repository_instance.check_uploading
render_api Renderable.new({
contents: @model
})
end
Repository에서 모델의 상태를 확인하고 check_resource_uploading을 호출한다:
def check_uploading
case @model.state_name
when :uploading, :missing, :created
unless @model.check_resource_uploading
raise Cupix::Errors::Resource.new(code: 'RESC10000', reason: 'Resource does not uploaded')
end
else
raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: "Invalid state: #{@model.state}")
end
@model
end
Resource concern에서 S3 object 존재 여부와 메타데이터를 확인하는 핵심 병목 지점:
def check_uploading
_revision = self.revision
_object = self.object(_revision + 1)
if _object.exists? # S3 HEAD request #1
self.etag = _object.etag.gsub('"', '') rescue nil # S3 HEAD request #2
self.size = _object.size # S3 HEAD request #3
self.content_type = _object.content_type rescue nil # S3 HEAD request #4
return false if self.size.blank? || self.size.zero?
MidasOperation.record_attachment_upload(attachment: resourcable, file_size: self.size) if resourcable.is_a?(Attachment) && MidasOperation.enabled?
self.increase_revision
self.done
true
else
self.missing
false
end
end
기대 동작: 각 S3 HEAD 요청은 1050ms 이내 응답해야 하며, 전체 check_uploading은 100200ms 이내 완료되어야 한다.
실제 동작: 29건의 동시 요청이 S3와 DB 리소스를 경합하면서, S3 레이턴시가 증가하고 DB connection pool 대기가 발생하여 450~1434ms까지 지연되었다.
추가로, 상태 전환 시 aerial_map의 상태도 갱신하는 after_transition hook이 DB write를 추가로 발생시킨다:
after_transition any => :uploading do |aerial_photo, transition|
aerial_photo.aerial_map.uploading_state
end
after_transition any => :uploaded do |aerial_photo, transition|
aerial_photo.aerial_map.uploaded_state
end
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api @http.url_details.path:*aerial_photos*check_uploading* from:2026-05-27T08:14:00Z to:2026-05-27T10:15:00Z
대표 로그 항목 (duration 높은 순):
{
"timestamp": "2026-05-27T09:15:13.937Z",
"resource_name": "Api::V1::AerialPhotosController#check_uploading",
"http.method": "PUT",
"http.url": "/api/v1/aerial_maps/451/aerial_photos/147409/check_uploading",
"duration_ms": 1302.05,
"db_time_ms": 206.09,
"host": "ip-10-1-19-190",
"http.status_code": 200
}
{
"timestamp": "2026-05-27T09:15:18.322Z",
"resource_name": "Api::V1::AerialPhotosController#check_uploading",
"http.url": "/api/v1/aerial_maps/451/aerial_photos/147412/check_uploading",
"duration_ms": 1212.94,
"db_time_ms": 128.52,
"host": "ip-10-1-144-228",
"http.status_code": 200
}
동시 DB 부하 증거 — 같은 시간대에 대량의 _update_document 실행:
service:cupixworks-api (status:error OR status:warn) from:2026-05-27T09:14:47Z to:2026-05-27T09:14:59Z
[2026-05-27T09:14:47Z] WARN Pano#_update_document: NotFound - attributes_in_database
[2026-05-27T09:14:47Z] WARN Pointcloud#_update_document: NotFound - attributes_in_database
[2026-05-27T09:14:48Z] WARN Record#_update_document: NotFound - attributes_in_database
50건 이상의 _update_document 경고가 동일 시간대에 집중되어, DB write 경합이 확인된다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | S3 API 호출 다중 실행 + 동시 burst로 인한 레이턴시 증폭 | 코드에서 요청당 2resource.rb:163-183). total_duration - db_time 차이가 200 |
— | Confirmed |
| H2 | DB connection pool 경합 (동시 _update_document 부하) | 동일 시간대 50+ _update_document WARN 로그, db_time이 최대 206ms까지 상승 |
일부 요청은 db_time 16ms로 낮음 — 경합 정도가 요청마다 다름 | Confirmed (contributing) |
| H3 | 네트워크 이슈 또는 S3 장애 | — | 모든 요청이 200 응답, S3 exists? 호출 성공. 특정 시간대만 발생하고 지속적이지 않음 | Rejected |
| H4 | AerialPhoto 상태 전환 lock 경합 | after_transition hook에서 aerial_map 상태 갱신 시 write lock 가능성 | 29개 photo가 각각 다른 레코드이므로 row-level lock 경합은 aerial_map 1건에만 해당. db_time 비중이 전체 duration의 15% 미만 | Rejected (minor) |
Fix Recommendation#
즉시 조치 (Critical)#
app/models/concerns/storagable/resource.rb:163-183— S3_object.exists?호출 후 반환된 HEAD response에서 etag, size, content_type을 한 번에 추출하도록 변경. AWS SDK의head_object단일 호출로 모든 메타데이터를 가져올 수 있으며, 현재 4회 호출을 1회로 줄일 수 있다.
단기 개선 (1주 이내)#
- 클라이언트(CupixConnect)에서 bulk check_uploading 호출 시 동시성을 제한하거나, 서버 측에서 batch endpoint를 제공하여 N건을 한 번에 처리. 현재 29건 동시 요청은 S3 rate limit과 DB pool을 불필요하게 소비한다.
app/models/concerns/resourcable.rb:56-58—resource조회를 eager loading 또는 캐싱하여 N+1 쿼리 제거.
장기 개선 (재발 방지)#
- S3 object 메타데이터를 업로드 완료 시 callback(S3 Event Notification 등)으로 DB에 미리 기록하는 방식으로 전환. check_uploading 시 S3 API 호출 자체를 제거할 수 있다.
- aerial_map 상태 갱신을 비동기 worker로 분리하여 요청 경로의 DB write 부담을 줄인다.
Monitoring#
check_uploading엔드포인트 p95 레이턴시 알림 추가 (threshold: 1000ms)- Datadog 쿼리:
service:cupixworks-api resource_name:"Api::V1::AerialPhotosController#check_uploading" @duration:>1000ms
- S3 HEAD 요청 레이턴시 metric 추가 (
aws.s3.head_object.latencyby bucket)
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- 기능 오류가 아닌 성능 이슈이며, 특정 조건(대량 동시 요청 + 동시 processing 부하)에서만 재현된다. 사용자 영향은 업로드 확인 시 수초 대기로 제한적이다.