Api::V1::VideosController#download_single_resource (avg 5109ms, max 5109ms)
RCA: VideosController#download_single_resource Latency (5109ms)
Overview#
What Happened#
2026-06-04 09:44 KST에 cupixworks-api 서비스의 Api::V1::VideosController#download_single_resource 엔드포인트에서 단일 요청이 5109ms의 응답 시간을 기록했다. 정상 응답 시간(30-200ms)의 25배 이상으로, video 655857 다운로드 요청에서 발생했다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::VideosController#download_single_resource |
| http.status_code | 302 |
| duration | 5107.64ms |
| db_time | 719.62ms |
| env | production, us-west-2 |
| deploy | production-us-west-2-20260602t0634z0-bfe9a388-cupixworks |
| user_agent | got (https://github.com/sindresorhus/got) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| exyte / 783 | 1 | integration@cupix.com 사용자의 video 다운로드 리다이렉트 지연 |
Timeline#
- 2026-06-04 09:44 KST — video 655857 download 요청 시작 (추정, 완료 - 5107ms)
- 2026-06-04 09:44:34 KST — 요청 완료, HTTP 302 반환 (5107.64ms 소요)
- 2026-06-04 09:44 KST — 동일 사용자의 video 655856 download도 2615.7ms 소요
Error Log#
{
"resource_name": "Api::V1::VideosController#download_single_resource",
"service": "cupixworks-api",
"occurrences": 1,
"avg_ms": 5109,
"max_ms": 5109,
"sample_trace_id": "5215379744891743914"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-06-04 09:44 KST
- 최근 발생: 2026-06-04 09:44 KST
Root Cause Summary#
Video 다운로드 요청 시 VideoRepository.permission_joins에서 실행되는 복잡한 권한 체크 쿼리(11개 LEFT JOIN)가 DB에서 719.62ms를 소비했고, 나머지 ~4388ms는 COGNITO 인증 검증(JWT 디코딩 + 사용자/팀 조회)과 Aws::S3::Object#presigned_url 생성에서 소요된 것으로 판단된다. 동일 시간대에 동일 호스트(ip-10-1-19-190)에서 처리 중이던 다수의 동시 요청으로 인한 서버 부하가 복합적으로 작용했다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/videos_controller.rb:1— VideosController - Authentication:
app/controllers/concerns/verification_controller.rb:15—authenticate!(COGNITO) - before_action:
app/controllers/api/v1/videos_controller.rb:5—set_video→repository_instance.show(params[:id]) - Permission query:
app/repositories/base_repository.rb:337—permission_joins(default_joins(current_class), current_user).where(attrs) - Resource setup:
app/controllers/concerns/single_resourcable_controller.rb:23—set_single_resource - Download action:
app/controllers/concerns/single_resourcable_controller.rb:10—download_single_resource - Presigned URL:
app/models/concerns/storagable/resource.rb:203—self.object(ver).presigned_url(:get, ...)
def download_single_resource
if @resource.revision == 0
raise Cupix::Errors::Resource.new(code: 'ENT10011', reason: "Resource does not uploaded: #{@resource.revision}")
else
download_opts = {}
download_opts[:filename] = params[:filename] if params[:filename].present?
redirect_to @resource.download_url(download_opts), allow_other_host: true
end
end
set_video before_action에서 VideoRepository.show를 호출하면 permission_joins가 실행된다. 이 메서드는 record, facility, workspace, team 각각에 대해 user/group/system_group 권한을 LEFT JOIN으로 확인하는 복잡한 쿼리를 생성한다:
def self.default_joins(record)
record.includes(:storage).joins("
LEFT JOIN facilities ON facilities.id = videos.facility_id
LEFT JOIN records ON records.id = videos.record_id
LEFT JOIN captures ON captures.id = videos.capture_id
LEFT JOIN cameras ON cameras.id = captures.camera_id
LEFT JOIN workspaces ON workspaces.id = videos.workspace_id
LEFT JOIN levels ON levels.id = captures.level_id
LEFT JOIN capture_types ON capture_types.id = captures.capture_type_id
").joins(:facility, :capture).select('
videos.*,
captures.name AS capture_name,
workspaces.name AS workspace_name,
facilities.name AS facility_name,
...
')
end
이후 permission_joins에서 추가로 11개의 LEFT JOIN 서브쿼리가 실행된다 (line 91-229). 단일 video 조회임에도 불구하고 전체 권한 매트릭스를 계산하며, 이 쿼리가 DB 시간의 대부분(719.62ms)을 차지한다.
presigned URL 생성 단계:
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
presigned_url은 AWS SDK를 통해 로컬에서 서명을 생성하므로 네트워크 호출 없이 수행된다. 그러나 Aws::S3::Object.new 생성 시 Cupix::StorageService.object (app/services/cupix/storage_service.rb:29-36)에서 Aws::S3::Client를 매번 새로 초기화하며, 이 과정에서 credential 로딩과 client 설정에 시간이 소요될 수 있다.
Log Evidence#
Datadog 쿼리:
service:cupixworks-api "videos/655857/download"
Time range: 2026-06-03T23:44:00Z to 2026-06-04T01:15:00Z
핵심 로그 (video 655857, 5107.64ms):
{
"timestamp": "2026-06-04T00:44:34.542Z",
"message": "[302] GET /api/v1/videos/655857/download (Api::V1::VideosController#download_single_resource)",
"duration": 5107.64,
"db": 719.62,
"remote_ip": "44.228.8.68",
"user_agent": "got (https://github.com/sindresorhus/got)",
"user": { "id": 36575, "email": "integration@cupix.com" },
"team": { "domain": "exyte", "id": 783 },
"location": "https://s3.amazonaws.com/cupixworks-source-dc9dcff32488-usea1/resources/59to9b/usea1/v1",
"host": "ip-10-1-19-190.us-west-2.compute.internal",
"cupix_auth_method": "COGNITO"
}
동일 시간대 연속 요청 (같은 사용자, 같은 호스트):
00:44:32.536Z — video 655856 — 2615.7ms (db: 182.05ms)
00:44:34.542Z — video 655857 — 5107.64ms (db: 719.62ms)
동일 시간대 다른 사용자의 느린 요청:
00:38:51.566Z — video 666880 — 3338.33ms (team: tpc/778)
00:39:01.598Z — video 667242 — 2285.58ms (team: tpc/778)
시간대 분석:
- 정상 요청은 30-200ms에 완료됨
- 같은 호스트에서 동시간대에 여러 느린 요청이 발생 → 서버 부하 집중 시점
- video 655857의 DB 시간이 719.62ms로 655856(182.05ms)에 비해 4배 높음 → DB 연결 대기 또는 lock contention 발생 가능
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 복잡한 permission_joins 쿼리 + 서버 부하로 인한 DB/앱 레벨 지연 | DB 시간 719.62ms (정상 대비 4배), 동일 시간대 다수의 느린 요청이 동일 호스트에서 발생 | 단독 원인이라면 DB 시간만으로 5초 설명 불가 (나머지 ~4.4초 존재) | Confirmed (복합 원인) |
| H2 | S3 presigned URL 생성 시 네트워크 지연 | 총 시간에서 DB 제외 시 ~4.4초 미설명 | presigned_url은 로컬 서명 생성으로 네트워크 호출 없음. Aws::S3::Object.new도 lazy initialization |
Rejected |
| H3 | COGNITO 인증 검증에서 외부 API 호출 지연 | JWT 검증 시 JWKS 키 fetch가 필요할 수 있음. 총 4.4초 중 상당 부분이 인증 단계에서 소요 가능 | JWKS는 일반적으로 캐싱됨. 별도 인증 에러 로그 없음. Uncertain — 정확한 분석에 APM trace span 필요 | Inconclusive |
| H4 | Ruby GC pause 또는 서버 전체 부하 (CPU/Memory contention) | 동일 호스트에서 동시간대 PanosController#bulk 등 78초 걸리는 요청 존재 (subagent 조사 결과), 단일 인스턴스에 요청 집중 | 직접적인 GC 로그/메트릭 미확인 | Confirmed (기여 요인) |
Fix Recommendation#
즉시 조치 (Critical)#
- 즉시 조치 불필요. 단발성 이벤트(1건)로, HTTP 302 정상 반환됨. 사용자 영향 최소.
단기 개선 (1주 이내)#
VideoRepository.show에서download_single_resource액션 시skip_permission: true옵션을 사용하거나, download 전용 경량 쿼리 경로를 추가하는 것을 검토. 현재 단순 다운로드 리다이렉트에도 11개 LEFT JOIN이 포함된 전체 권한 쿼리가 실행됨.- 대상 파일:
app/controllers/api/v1/videos_controller.rb:63-65(set_videomethod) set_video에서 download action 시에는 간소화된 조회를 사용하도록 분기.
- 대상 파일:
장기 개선 (재발 방지)#
permission_joins쿼리 최적화: 11개 LEFT JOIN 서브쿼리를 materialized view 또는 캐싱된 권한 테이블로 대체 검토.- 단일 인스턴스 부하 분산:
ip-10-1-19-190호스트에 요청이 집중되는 패턴 분석. Auto-scaling 임계값 또는 load balancer 설정 검토. - S3 클라이언트 재사용:
Cupix::StorageService.object(app/services/cupix/storage_service.rb:29-36)에서 매 호출마다 새Aws::S3::Client인스턴스를 생성하므로, connection pooling 또는 client 재사용 패턴 적용 검토.
Monitoring#
download_single_resource엔드포인트의 p95/p99 latency 모니터링 추가- Datadog 쿼리 예시:
service:cupixworks-api resource_name:"Api::V1::VideosController#download_single_resource" @duration:>3000
- DB 쿼리 시간이 500ms 이상인 경우 알림 설정
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial — 단발성 이벤트로 서버 부하 집중 시 발생하는 일시적 지연. 근본적 버그가 아닌 성능 최적화 영역.