ES /docs

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#

  1. 2026-06-04 09:44 KST — video 655857 download 요청 시작 (추정, 완료 - 5107ms)
  2. 2026-06-04 09:44:34 KST — 요청 완료, HTTP 302 반환 (5107.64ms 소요)
  3. 2026-06-04 09:44 KST — 동일 사용자의 video 655856 download도 2615.7ms 소요

Error Log#

Datadog Logs

json
{
  "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:15authenticate! (COGNITO)
  • before_action: app/controllers/api/v1/videos_controller.rb:5set_videorepository_instance.show(params[:id])
  • Permission query: app/repositories/base_repository.rb:337permission_joins(default_joins(current_class), current_user).where(attrs)
  • Resource setup: app/controllers/concerns/single_resourcable_controller.rb:23set_single_resource
  • Download action: app/controllers/concerns/single_resourcable_controller.rb:10download_single_resource
  • Presigned URL: app/models/concerns/storagable/resource.rb:203self.object(ver).presigned_url(:get, ...)
app/controllers/concerns/single_resourcable_controller.rb:10-18ruby
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으로 확인하는 복잡한 쿼리를 생성한다:

app/repositories/video_repository.rb:27-57ruby
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 생성 단계:

app/models/concerns/storagable/resource.rb:185-205ruby
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 쿼리:

text
service:cupixworks-api "videos/655857/download"
Time range: 2026-06-03T23:44:00Z to 2026-06-04T01:15:00Z

핵심 로그 (video 655857, 5107.64ms):

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

동일 시간대 연속 요청 (같은 사용자, 같은 호스트):

text
00:44:32.536Z — video 655856 — 2615.7ms (db: 182.05ms)
00:44:34.542Z — video 655857 — 5107.64ms (db: 719.62ms)

동일 시간대 다른 사용자의 느린 요청:

text
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_video method)
    • 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 쿼리 예시:
text
service:cupixworks-api resource_name:"Api::V1::VideosController#download_single_resource" @duration:>3000
  • DB 쿼리 시간이 500ms 이상인 경우 알림 설정

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial — 단발성 이벤트로 서버 부하 집중 시 발생하는 일시적 지연. 근본적 버그가 아닌 성능 최적화 영역.