ES /docs

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#

  1. 09:14:49Z — 첫 번째 slow trace 감지 (APM threshold >500ms 초과)
  2. 09:14:49~09:15:20Z — 29건의 check_uploading 요청 burst 발생 (단일 세션)
  3. 09:14:47~09:14:59Z — 동시에 Pano/Pointcloud/Record _update_document 대량 발생으로 DB 부하
  4. 09:15:22Z — AerialMap 451 processing step function 시작
  5. 09:15:20Z — 마지막 slow request 완료

Error Log#

Datadog Logs

json
{
  "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에 대해 check_uploading 요청을 동시 burst로 전송했다. 각 요청은 S3 HEAD 요청(exists? + etag/size/content_type)을 24회 수행하며, 동시에 Pano#_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
app/controllers/api/v1/aerial_photos_controller.rb:33-38ruby
def check_uploading
  video = repository_instance.check_uploading
  render_api Renderable.new({
    contents: @model
  })
end

Repository에서 모델의 상태를 확인하고 check_resource_uploading을 호출한다:

app/repositories/aerial_photo_repository.rb:206-217ruby
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 존재 여부와 메타데이터를 확인하는 핵심 병목 지점:

app/models/concerns/storagable/resource.rb:163-183ruby
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를 추가로 발생시킨다:

app/models/concerns/statable/aerial_photo.rb:31-37ruby
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 쿼리:

text
service:cupixworks-api @http.url_details.path:*aerial_photos*check_uploading* from:2026-05-27T08:14:00Z to:2026-05-27T10:15:00Z

대표 로그 항목 (duration 높은 순):

json
{
  "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
}
json
{
  "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 실행:

text
service:cupixworks-api (status:error OR status:warn) from:2026-05-27T09:14:47Z to:2026-05-27T09:14:59Z
text
[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로 인한 레이턴시 증폭 코드에서 요청당 24회 S3 HEAD 호출 확인 (resource.rb:163-183). total_duration - db_time 차이가 2001100ms로 S3 대기 시간 증명 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-58resource 조회를 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 쿼리:
text
service:cupixworks-api resource_name:"Api::V1::AerialPhotosController#check_uploading" @duration:>1000ms
  • S3 HEAD 요청 레이턴시 metric 추가 (aws.s3.head_object.latency by bucket)

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 기능 오류가 아닌 성능 이슈이며, 특정 조건(대량 동시 요청 + 동시 processing 부하)에서만 재현된다. 사용자 영향은 업로드 확인 시 수초 대기로 제한적이다.