Api::V1::AssetsController#download (avg 92945ms, max 92945ms)
RCA: Api::V1::AssetsController#download 92s latency
Overview#
What Happened#
2026-06-26 10:51 KST 시각에 cupixworks-api (ap-southeast-2, tenant cupix) 의 Api::V1::AssetsController#download 요청 한 건이 92.9초 동안 지연되었다. 동일 시간대에 같은 region/tenant 의 다른 엔드포인트들이 ActiveRecord::LockWaitTimeout (Mysql2 lock wait timeout 50초) 으로 502 응답을 다수 반환하고 있었으며, 본 latency 클러스터는 같은 DB lock contention 사건의 일부로 보인다. status board 도 본 클러스터를 2026-06-26-svc-cupixworks-api--unknown-1 인시던트에 묶고 있다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::AssetsController#download |
| avg_duration_ms | 92945 |
| max_duration_ms | 92945 |
| sample_trace_id | 1226871822661237507 |
| env | production, ap-southeast-2 |
| tenant | cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupix (tenant), ap-southeast-2 | 1 trace (이 클러스터) + 6 동시 발생 cluster (parent incident) | 자산 다운로드 redirect 지연 92초 — 사용자가 사실상 실패로 인지. 같은 DB 의 panos write 엔드포인트들도 50초 lock wait 후 502 반환 중. |
Timeline#
- 2026-06-26 10:25 KST — Parent incident
2026-06-26-svc-cupixworks-api--unknown-1시작 (Api::V1::PanosController#update 41.5s latency, cluster943fdcb4). - 2026-06-26 10:46 KST 이후 —
panos테이블 대상Mysql2::Error::TimeoutError: Lock wait timeout exceeded502 응답 다수 발생 (check_tile_uploading,check_mask_uploading,mask_upload_url). duration ≈ 50087ms = innodb_lock_wait_timeout 기본값. - 2026-06-26 10:51:49 KST —
Api::V1::AssetsController#download92945ms (≈ lock wait timeout 1회 + 추가 대기) 후 응답 (trace1226871822661237507). 본 클러스터 first_seen / last_seen. - 2026-06-26 10:53 KST 이후 — 추가 502 LockWaitTimeout 발생 후 parent incident 의 다른 cluster 들이 같은 패턴으로 분류됨.
Error Log#
{
"resource_name": "Api::V1::AssetsController#download",
"service": "cupixworks-api",
"occurrences": 1,
"avg_ms": 92945,
"max_ms": 92945,
"sample_trace_id": "1226871822661237507"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-06-26 10:51 KST
- 최근 발생: 2026-06-26 10:51 KST
Root Cause Summary#
Api::V1::AssetsController#download 엔드포인트 자체는 set_asset (1회 DB lookup) → resource.download_url (S3 presigned URL 생성) → 302 redirect 의 단순 흐름이며 정상 응답은 수십 ms 이내에 끝난다. 그러나 사건 시각 ap-southeast-2 / tenant cupix 의 production DB 에서 panos 테이블을 둘러싼 광범위한 InnoDB row-lock 경합이 발생하여 50초 innodb_lock_wait_timeout 으로 502 를 반환하는 요청이 다수 누적되고 있었다. 본 요청은 같은 DB / connection pool 을 공유하기 때문에 set_asset 단계의 DB SELECT 또는 그 외 before_action 단계 (예: check_team_license, ApplicationRecord lookup) 에서 lock 또는 connection 대기로 묶여 92.9초 지연되었다. 본 클러스터는 단독 root cause 가 아니라 parent incident (2026-06-26-svc-cupixworks-api--unknown-1) 의 부수 피해이다.
Technical Analysis#
Code Path#
download 액션 자체는 redirect 외 비즈니스 로직이 거의 없다:
def download
resource = @model.resource
if resource.nil? || resource.revision.zero?
render_json 404
else
redirect_to resource.download_url, allow_other_host: true
end
end
@model 은 before_action :set_asset 에서 채워지며, 이 단계가 DB 를 한 번 친다:
def set_asset
@model = repository_instance.show(params[:key])
end
resource.download_url 은 Storagable::Resource#download_url 로, 외부 호출이 없고 S3 presigned URL 만 만드는 in-memory 연산이다:
def download_url(opts = {})
ver = opts[:ver] || self.revision
raise Cupix::Errors::Resource.new(code: 'ENT10011', reason: "Resource does not uploaded: #{ver}") if ver.zero?
filename = opts[:filename].presence || self.name
case opts[:distribution]
when 'cloudfront'
_rcd = CGI.escape("attachment; filename=#{filename}")
else
if !opts[:exp].blank?
exp = opts[:exp]
else
exp = 3.hours.to_i
end
self.object(ver).presigned_url(:get, expires_in: exp, response_content_disposition: "filename=#{CGI.escape(filename) rescue nil}")
end
end
- Entry point:
app/controllers/api/v1/assets_controller.rb:51 - DB hit (의심 stall 지점):
app/controllers/api/v1/assets_controller.rb:62-64의set_asset및 상위Api::V1::ApiControllerbefore_action 체인 (check_team_license,set_app_id,set_updated_since) - Presigned URL 생성:
app/models/concerns/storagable/resource.rb:203— 외부 I/O 없음
기대 동작: DB SELECT 1회 + 메모리 연산으로 수십 ms 이내 302 응답. 실제 동작: 92945ms 후 응답. before_action / set_asset 단계에서 MySQL connection 또는 row-lock 대기로 묶인 것으로 추정. 코드 자체에는 본 latency 를 유발하는 결함이 없다.
Log Evidence#
같은 시간대 같은 region 의 DB lock 경합을 보여주는 로그를 Datadog 에서 조회.
쿼리:
service:cupixworks-api "Lock wait timeout"
시간 범위: 2026-06-26T01:25:00Z ~ 2026-06-26T02:00:00Z
대표 로그 (raw, region/tenant 태그 포함):
{
"service": "cupixworks-api",
"host": "ip-10-1-83-103.ap-southeast-2.compute.internal",
"tenant": "cupix",
"team": { "domain": "crossriverrail", "id": 72 },
"controller": "Api::V1::PanosController",
"action": "check_mask_uploading",
"duration": 50087.91,
"error": {
"message": "Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction",
"class": "ActiveRecord::LockWaitTimeout"
}
}
같은 시간 창 (10:46~10:53 KST) 에서 다수 502 응답이 모두 50087ms 부근에서 끊김 — MySQL InnoDB innodb_lock_wait_timeout (기본 50s) 의 정확한 일치:
2026-06-26 10:46:39 [502] PUT /api/v1/panos/14131474/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:46:43 [502] PUT /api/v1/panos/14132644/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:46:47 [502] PUT /api/v1/panos/14131939/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:47:28 [502] PUT /api/v1/panos/14132652/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:47:29 [502] PUT /api/v1/panos/14132647/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:47:34 [502] PUT /api/v1/panos/14131474/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:47:42 [502] PUT /api/v1/panos/14131939/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:48:41 [502] PUT /api/v1/panos/14131939/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:53:28 [502] POST /api/v1/panos/14133747/mask_upload_url ActiveRecord::LockWaitTimeout
2026-06-26 10:53:29 [502] PUT /api/v1/panos/14133757/check_mask_uploading ActiveRecord::LockWaitTimeout
본 클러스터의 다운로드 trace (10:51:49 KST) 는 위 두 502 폭주 구간 사이에 위치하며, status board 가 본 cluster 를 묶은 parent incident 의 다른 cluster 들은 모두 Api::V1::PanosController#* 의 long-duration trace 다 (예: 943fdcb4 Api::V1::PanosController#update 41.5s, 10:25:34 KST 발생).
성공한 동일 엔드포인트의 정상 응답 시간 (참고 — 평소 패턴):
2026-06-26 10:58:39 [302] GET /api/v1/assets/9u5tqsh6o66a/download (Api::V1::AssetsController#download)
2026-06-26 10:58:17 [302] GET /api/v1/reviews/pa3nl4/assets/.../download (Api::V1::AssetsController#download)
이 정상 trace 들의 access log 한 줄로 끝난다 (= 빠른 응답). 92s 지연은 본 한 건만 관측된 outlier.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 같은 시간 ap-southeast-2 / tenant cupix DB 의 InnoDB row-lock 경합으로 set_asset 등 before_action 단계 DB lookup 이 connection / lock 대기에 묶여 92s 지연. |
(a) 동일 시간/리전/테넌트에서 다수 502 ActiveRecord::LockWaitTimeout, duration=50087ms 로 정확히 innodb_lock_wait_timeout 50s 와 일치. (b) status board 가 본 cluster 를 7개 cluster 의 parent incident 에 자동 묶음. (c) 92945ms ≈ 50s lock wait + 추가 대기/재시도 폭에 부합. |
— | Confirmed (외부 기여 원인) |
| H2 | download 액션 코드 자체의 결함 (예: presigned URL 생성 hang, 외부 호출 hang). |
— | download_url 은 in-memory 연산만 수행 (S3 SDK presigned URL 은 네트워크 호출 없음). 정상 시간대의 같은 엔드포인트는 단일 access log 줄로 완료. |
Rejected |
| H3 | CloudFront 분기에서 함수가 _rcd 만 만들고 return value 없이 떨어지는 분기 버그가 hang 을 유발. |
app/models/concerns/storagable/resource.rb:193-194 에 case 의 cloudfront 분기는 escape 만 하고 명시적 반환값 없음. |
이 경우는 nil 반환으로 즉시 redirect 가 깨질 뿐 hang 을 만들지 않음. 본 trace 는 92s 후 응답한 것이지 hang/timeout 으로 끊긴 것이 아님. |
Rejected (별도 이슈 가능성은 있으나 본 latency 의 원인 아님) |
| H4 | S3 presigned URL 발급에 외부 네트워크 호출이 있어 ap-southeast-2 S3 outage 로 인한 지연. | — | AWS SDK presigned URL 은 서명만 로컬 연산. 외부 호출 없음. | Rejected |
| H5 | OpcOperation 호출 또는 다른 외부 통합 (예: Oracle ICS) 이 download 경로에서 호출되어 hang. | 같은 시간대 OpcOperation 409 Conflict 에러 다수 (IntegrationRepository#opc_access_token). |
AssetsController#download 경로에 OpcOperation 호출 없음. 둘 다 단지 같은 시간대 발생한 별개 증상. |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 본 cluster 는 단독 코드 결함이 아닌 parent incident 의 부수 피해이므로 코드 변경 불필요. parent incident
2026-06-26-svc-cupixworks-api--unknown-1의 root cause (panos 테이블 row-lock 경합) 처리에 합류한다. - DB lock 경합의 직접 원인 (어떤 transaction 이 row-lock 을 장기 보유했는가) 은 다음 위치에서 추적해야 한다:
Api::V1::PanosController#update,#check_tile_uploading,#check_mask_uploading,#mask_upload_url의 transaction 경계 (app/controllers/api/v1/panos_controller.rb및PanoRepository)- 같은 capture/cluster 의 pano upload 처리가 동시에 다발하면서 같은 row 또는 동일 부모 record 의 lock 을 잡는지 확인. crossriverrail team (team.id=72) 의 capture 45578 부근 trace 를 별도 RCA 로 추적할 것.
단기 개선 (1주 이내)#
Api::V1::AssetsController#download처럼 단순 redirect/refresh 엔드포인트는 가능한 한 짧은 read-only DB transaction 으로 동작하도록, primary 대신 replica 에서 SELECT 하거나 statement timeout 을 짧게(예: 5s) 두어 lock 경합 시 빠르게 fail-fast 하도록 고려.set_asset의repository.show(params[:key])가 read-only 쿼리임을 보장 (현재 코드상 select 만 수행 — 추가 transaction 시작 여부 확인).
장기 개선 (재발 방지)#
- panos write 경로의 transaction 범위 축소: lock 보유 시간이 긴 transaction 을 찾아 분할.
- ap-southeast-2 production DB 의
innodb_lock_wait_timeout단축 (현재 50s 추정) — read-heavy API 경로에 영향이 크므로 30s 이하로 검토하고, 동시에 long-running write 를 줄이는 작업과 병행. - DB connection pool 가용성 /
pool_wait모니터링: lock 경합 시 connection 회수가 막혀 본 download 같은 무관 엔드포인트까지 막히는 cascading 패턴 감지.
Monitoring#
sum:trace.rack.request.duration.by_resource_service.errors{service:cupixworks-api,env:production,region:ap-southeast-2}.as_count()
avg:trace.rack.request.duration{service:cupixworks-api,env:production,resource_name:api::v1::assetscontroller#download}
sum:mysql.innodb.row_lock_waits{service:cupixworks-api,env:production,region:ap-southeast-2}.as_rate()
max:mysql.innodb.row_lock_time{service:cupixworks-api,env:production,region:ap-southeast-2}
추가 권장:
ActiveRecord::LockWaitTimeout카운트 Datadog Logs 기반 metric:textlogs("service:cupixworks-api @error.class:ActiveRecord::LockWaitTimeout env:production").index("*").rollup("count").by("region")- region 별 p99 응답 시간을 endpoint 별로 비교하여 ap-southeast-2 만 튀는 패턴을 조기 감지.
Risk Assessment#
- Risk level: medium (단일 trace 의 사용자 영향은 한정적이나, 같은 parent incident 의 cluster 다수가 동시에 발생 — DB 광범위 영향).
- 예상 복잡도: standard — 코드 결함은 없고, parent incident (panos write 경로 lock 분석) 와 연계 작업 필요.