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#
- 2026-05-25 22:50 UTC — 최초 고지연 요청 감지 (5.7s)
- 2026-05-25 23:00~23:25 UTC — 연속 극단적 지연 (13~16s), 배치 업로드 기간과 일치
- 2026-05-26 03:23 UTC — 클러스터 최초 감지 (>500ms 임계값 기준)
- 2026-05-26 11:21 UTC — 마지막 감지된 지연 요청
Error Log#
{
"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:80—check_resource_uploading액션 - Before action 1:
app/controllers/api/v1/clusters_controller.rb:49-51—set_cluster(DB 조회) - Before action 2:
app/controllers/concerns/multiple_resourcable_controller.rb:145-151—set_multiple_resource(DB 조회) - Failure point:
app/models/concerns/storagable/resource.rb:163-183—check_uploading메서드 내 S3 호출
1단계: Controller action 진입
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 호출 (병목)
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 구성
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
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 메트릭 조회에 사용한 쿼리:
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):
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 로그 — 정상 응답 확인:
[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 |
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-12의client메서드에서 기본 타임아웃을 설정하거나,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_uploadingp95 > 2초
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 수준