ES /docs

Api::V1::ClustersController#check_resource_uploading (avg 1028ms, max 1032ms)

RCA: ClustersController#check_resource_uploading Latency (avg 1135ms, max 1643ms)

Overview#

What Happened#

2026-05-26 03:23~11:21 UTC 사이에 cupixworks-api 서비스의 Api::V1::ClustersController#check_resource_uploading 엔드포인트에서 평균 1135ms, 최대 1643ms의 응답 지연이 12회 발생했다. APM 메트릭 분석 결과, 같은 24시간 동안 최대 16.7초까지 치솟는 극단적 지연도 관측되었으며, 이는 S3 HeadObject API 호출의 동기적 실행이 원인이다.

Quick Facts#

Field Value
resource_name Api::V1::ClustersController#check_resource_uploading
top_frame app/models/concerns/storagable/resource.rb:167
env production, us-west-2
avg_duration 1135ms
max_duration 1643ms (cluster), 16.7s (APM 24h max)

Timeline#

  1. 2026-05-25 22:50 UTC — 최초 고지연 요청 감지 (5.7s)
  2. 2026-05-25 23:00~23:25 UTC — 연속 극단적 지연 (13~16s), 배치 업로드 기간과 일치
  3. 2026-05-26 03:23 UTC — 클러스터 최초 감지 (>500ms 임계값 기준)
  4. 2026-05-26 11:21 UTC — 마지막 감지된 지연 요청

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::ClustersController#check_resource_uploading",
  "service": "cupixworks-api",
  "occurrences": 2,
  "avg_ms": 1028,
  "max_ms": 1032,
  "sample_trace_id": "1161675389835610839"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 12 (>500ms 임계값 초과 기준)
  • 최초 발생: 2026-05-26T03:23:18.849Z
  • 최근 발생: 2026-05-26T11:21:10.026Z
  • 영향 범위: preview_image 리소스 업로드 확인을 호출하는 클라이언트. 24시간 동안 약 1070건의 전체 요청 중 15건이 1초 이상 지연됨.

Root Cause Summary#

check_resource_uploading 액션은 S3 HeadObject API 호출(Aws::S3::Object#exists?)을 HTTP 요청 사이클 내에서 동기적으로 실행한다. S3가 정상적으로 응답할 때는 100250ms 수준이지만, 배치 업로드가 집중되는 시간대(22:5002:35 UTC)에 S3 응답 지연 또는 throttling이 발생하면 전체 요청 지연이 1초~16초까지 증가한다. 이 API 엔드포인트에는 S3 호출에 대한 타임아웃이나 비동기 처리가 적용되어 있지 않아, S3 지연이 그대로 클라이언트 응답 시간에 전파된다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/multiple_resourcable_controller.rb:80check_resource_uploading 액션
  • Before action 1: app/controllers/api/v1/clusters_controller.rb:49-51set_cluster (DB 조회)
  • Before action 2: app/controllers/concerns/multiple_resourcable_controller.rb:145-151set_multiple_resource (DB 조회)
  • Failure point: app/models/concerns/storagable/resource.rb:163-183check_uploading 메서드 내 S3 호출

1단계: Controller action 진입

app/controllers/concerns/multiple_resourcable_controller.rb:80-98ruby
def check_resource_uploading
  case @resource.state_name
  when :uploading, :missing
    callback_uploaded_resource(@resource) if @resource.check_uploading
  end
rescue Cupix::Errors::Parameter => e
  raise e
else
  render_api Renderable.new({
    contents: @resource,
    serializer: ResourceSerializer,
    serializer_option: {
      fields: {
        resource: @fields
      },
      is_collection: false
    }
  })
end

리소스 상태가 :uploading 또는 :missing인 경우 @resource.check_uploading을 호출한다.

2단계: S3 HeadObject 호출 (병목)

app/models/concerns/storagable/resource.rb:163-183ruby
def check_uploading
  _revision = self.revision
  _object = self.object(_revision + 1)

  if _object.exists?  # ← S3 HeadObject API 호출 (동기 블로킹)
    self.etag = _object.etag.gsub('"', '') rescue nil
    self.size = _object.size
    self.content_type = _object.content_type rescue nil

    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

_object.exists?Aws::S3::Object#exists?를 호출하며, 내부적으로 S3 HeadObject API를 수행한다. 이 호출은 동기적이며 타임아웃 제한이 별도로 설정되어 있지 않다.

3단계: S3 Object 구성

app/models/concerns/storagable/resource.rb:85-99ruby
def object(ver = nil)
  if !ver.nil?
    _ver = ver
  else
    _ver = self.revision
  end

  raise Cupix::Errors::System.new(code: 'SYS10000', reason: "Invalid revision #{ver}") if !_ver.is_a?(Integer) || !(_ver > -1)

  Cupix::StorageService.object(
    storage_option: storage_option,
    bucket_name: storage_option.s3_source_bucket_name,
    key: self.object_key(_ver)
  )
end
app/models/concerns/decorators/resource.rb:5-10ruby
def object_key(ver = nil, s3_region_code: nil)
  _ver = ver.presence || self.revision
  _s3_region_code = s3_region_code.presence || storage_option.s3_region_code

  "resources/#{key}/#{_s3_region_code}/v#{_ver}"
end

S3 object key 경로는 resources/{key}/{region_code}/v{revision+1} 형태이며, 다음 revision의 존재 여부를 확인하여 업로드 완료를 판단한다.

Log Evidence#

APM 메트릭 조회에 사용한 쿼리:

text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::clusterscontroller_check_resource_uploading}
max:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::clusterscontroller_check_resource_uploading}
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::clusterscontroller_check_resource_uploading}.as_count()

APM 메트릭 분석 결과 (24시간):

  • 전체 요청 수: 약 1070건
  • 1초 초과 요청: 15건 (1.4%)
  • 최대 지연: 16.715초 (2026-05-26 02:15 UTC)
  • 평균 지연 (전체): ~200ms
  • 극단적 지연 집중 시간대: 2026-05-25 22:50 ~ 2026-05-26 02:35 UTC

고지연 요청 타임스탬프 (APM max duration > 1s):

text
2026-05-26 02:15 UTC - 16.715s
2026-05-25 23:00 UTC - 16.024s
2026-05-25 23:05 UTC - 15.609s
2026-05-26 02:20 UTC - 14.867s
2026-05-25 23:15 UTC - 14.779s
2026-05-25 23:20 UTC - 13.446s
2026-05-25 23:25 UTC - 8.109s
2026-05-25 22:50 UTC - 5.744s
2026-05-26 01:40 UTC - 2.196s
2026-05-26 11:20 UTC - 1.837s

요청 볼륨 분석:

  • 가장 바쁜 시간: 2026-05-26 11:20 UTC (36건/5분), 10:55 UTC (31건/5분)
  • 고지연 시간대의 볼륨은 상대적으로 낮음 → S3 지연은 자체 요청 볼륨이 아닌 외부 요인(같은 버킷에 대한 다른 서비스의 대량 PUT/GET, S3 내부 throttling)에 의한 것으로 판단

Datadog 로그 — 정상 응답 확인:

text
[200] PUT /api/v1/clusters/112716/resources/preview_image/check_uploading (Api::V1::ClustersController#check_resource_uploading)
[200] PUT /api/v1/clusters/1346808/resources/preview_image/check_uploading (Api::V1::ClustersController#check_resource_uploading)

모든 요청이 HTTP 200으로 정상 응답함 — 기능적 오류는 없으며 순수 지연 이슈임.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 S3 HeadObject 동기 호출이 네트워크 지연/throttling으로 인해 전체 요청 지연 유발 APM max 16.7s, 지연이 배치 업로드 시간대에 집중됨, 코드에 S3 timeout 미설정 (storagable/resource.rb:167) Confirmed
H2 DB 쿼리 지연 (set_cluster, set_multiple_resource)이 원인 before_action에서 DB 조회 수행 APM avg 200ms이며, 극단적 지연(516s)은 DB로는 설명 불가. 동일 시간대 다른 endpoint에서 DB 이슈 보고 없음 Rejected
H3 callback_uploaded_resource 이후 처리가 지연 유발 ClustersController에서 호출됨 ClustersController는 callback_uploaded_resource를 override하지 않아 기본 no-op (nil 반환, multiple_resourcable_controller.rb:167) Rejected
H4 S3 object가 존재하여 추가 메타데이터 조회 (etag, size, content_type)가 지연 유발 exists? 이후 .etag, .size, .content_type 접근 시 추가 API 호출 가능 AWS SDK는 HeadObject 응답에서 이미 메타데이터를 캐시하므로 추가 네트워크 호출 없음. 또한 .missing 상태(object 미존재)에서도 동일 지연 발생 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • app/models/concerns/storagable/resource.rb:163-183 — S3 client에 명시적 타임아웃 설정 추가
    • Aws::S3::Client 생성 시 http_open_timeout: 5, http_read_timeout: 5 옵션을 설정하여 S3 응답 지연이 5초를 초과하지 않도록 제한
    • app/services/cupix/storage_service.rb:5-12client 메서드에서 기본 타임아웃을 설정하거나, check_uploading 전용 client를 분리

단기 개선 (1주 이내)#

  • check_uploading 로직을 비동기로 전환하는 방안 검토
    • 클라이언트가 check_resource_uploading을 폴링하는 대신, S3 Event Notification → SQS → Worker로 업로드 완료를 감지하는 이벤트 기반 방식으로 변경
    • 또는 check_uploading을 Sidekiq worker로 이동하여 API 응답 시간과 S3 지연을 분리

장기 개선 (재발 방지)#

  • S3 호출이 포함된 모든 API 엔드포인트에 일관된 타임아웃 정책 적용
  • S3 호출 지연을 별도 span으로 추적하는 custom instrumentation 추가 (Datadog APM에서 S3 지연만 분리 관측 가능하도록)
  • 배치 업로드 시간대의 S3 request rate를 모니터링하여 throttling 조기 감지

Monitoring#

  • S3 HeadObject 지연을 추적하는 custom metric 또는 APM span 추가
  • 알림 조건: check_resource_uploading p95 > 2초
text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::clusterscontroller_check_resource_uploading} > 2

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard — 타임아웃 설정은 trivial이나, 비동기 전환은 클라이언트 폴링 패턴 변경이 필요하여 standard 수준