ES /docs

Api::V1::VideosController#create (avg 1563ms, max 4144ms)

RCA: Api::V1::VideosController#create Latency (avg 1268ms, max 4144ms)

Overview#

What Happened#

2026-05-26 03:34~13:17 UTC 사이 cupixworks-api 서비스의 Api::V1::VideosController#create 엔드포인트에서 평균 1268ms, 최대 4144ms의 응답 지연이 52건 발생했다. 전체 리전(ap-southeast-2, us-west-2, eu-central-1, ap-southeast-1)에서 관측되었으며, HTTP 응답은 모두 200 OK로 기능 장애는 아니지만 사용자 체감 성능이 저하되었다.

Quick Facts#

Field Value
resource_name Api::V1::VideosController#create
avg_duration 1268ms
max_duration 4144ms
env production (ap-southeast-2, us-west-2, eu-central-1, ap-southeast-1)
user_agent Dart/3.11 (dart:io) (모바일 앱)

Timeline#

  1. 2026-05-26T03:34:37Z — 최초 고지연 요청 감지
  2. 2026-05-26T04:17:36Z — 최대 지연 4144ms 발생 (ap-southeast-2)
  3. 2026-05-26T13:17:09Z — 마지막 고지연 요청 관측
  4. 2026-05-27 — RCA 완료

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::VideosController#create",
  "service": "cupixworks-api",
  "occurrences": 21,
  "avg_ms": 1563,
  "max_ms": 4144,
  "sample_trace_id": "914670550937679908"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 52
  • 최초 발생: 2026-05-26T03:34:37.268Z
  • 최근 발생: 2026-05-26T13:17:09.869Z
  • 영향: 모바일 앱(Dart 클라이언트)에서 비디오 생성 시 1~4초 대기 발생. 기능 장애는 없으나 사용자 경험 저하.

Root Cause Summary#

VideosController#create의 응답 지연은 DB 시간(평균 48ms)이 아닌 애플리케이션 레벨 처리(평균 1200ms+)에서 발생한다. 코드 분석 결과, 비디오 생성 후 serializer가 upload_url을 생성할 때 Cupix::StorageService.client를 호출하여 매 요청마다 새로운 Aws::S3::Client 인스턴스를 생성하고, Aws::S3::Presigner로 presigned URL을 발급하는 과정이 주요 병목이다. AWS SDK 클라이언트 초기화에는 credential resolution, region endpoint lookup, HTTP connection setup이 포함되어 수백ms가 소요될 수 있으며, 원격 리전(ap-southeast-2)에서 특히 지연이 심하다. 또한 around_create_pano 콜백에서 Resource 레코드 생성, set_storage 콜백에서의 storage 조회, serializer에서의 다수 연관 객체 lazy loading이 복합적으로 지연을 가중시킨다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/videos_controller.rb:35-38
  • Factory: app/factories/video_factory.rb:6-26
  • Base factory save: app/factories/base_factory.rb:124 (model.save!)
  • Around create callback (Resource 생성): app/models/concerns/resourcable/video.rb:10-18
  • Storage 설정: app/models/concerns/storagable.rb:32-52 (before_create :set_storage)
  • Serializer upload_url: app/serializers/video_serializer.rb:19-25
  • Presigned URL 생성: app/models/concerns/storagable/resource.rb:111-131
  • S3 Client 생성: app/services/cupix/storage_service.rb:5-12

1. Controller → Factory → Save

app/controllers/api/v1/videos_controller.rb:35-38ruby
def create
  @model = factory_instance.create!(params)
  super
end

factory_instance.create!가 Video 레코드를 생성하고, super가 serializer를 통해 응답을 렌더링한다.

2. around_create 콜백 — Resource 레코드 생성

app/models/concerns/resourcable/video.rb:8-18ruby
around_create :around_create_pano

def around_create_pano
  yield

  begin
    self.create_video_resource
  rescue => e
    raise e
  end
end

Video INSERT 이후 즉시 Resource 레코드를 추가 INSERT한다. 이 Resource가 이후 presigned URL 생성의 대상이 된다.

3. Serializer — upload_url 생성 (핵심 병목)

app/serializers/video_serializer.rb:19-25ruby
attribute :upload_url do |video, params|
  if %i[created resource_uploading resource_missing].include?(video.state_name)
    video.resource_upload_url
  else
    nil
  end
end

새로 생성된 비디오는 created 상태이므로 항상 resource_upload_url을 호출한다.

4. Presigned URL 생성 — 매 요청마다 S3 Client 신규 생성

app/models/concerns/storagable/resource.rb:111-131ruby
def presigned_upload_url(revision, force: false)
  revision ||= self.revision + 1

  if !force && revision < self.revision
    raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Invalid revision: #{revision}")
  end

  client = Cupix::StorageService.client(storage_option: storage_option)
  signer = Aws::S3::Presigner.new(client: client)
  bucket_name = storage_option.s3_source_bucket_name
  expires_in = 2.hour.to_i

  signer.presigned_url(
    :put_object,
    bucket: bucket_name,
    key: object(revision).key,
    storage_class: 'ONEZONE_IA',
    expires_in: expires_in,
    acl: 'bucket-owner-full-control'
  )
end

매 호출마다 Aws::S3::Client.new를 실행한다 — 클라이언트 캐싱/풀링 없음.

app/services/cupix/storage_service.rb:5-12ruby
def client(storage_option: nil, **kwargs)
  opts = parse_storage_option(storage_option).merge(kwargs)
  opts[:force_path_style] = true

  check_required_params(opts, %i[region])

  Aws::S3::Client.new(opts)
end

AWS SDK 클라이언트 초기화는 credential resolution (IMDSv2 metadata fetch 또는 credential chain), endpoint resolution, HTTP transport setup을 포함한다. 특히 EC2 instance metadata 기반 credential의 경우 IMDS 호출이 추가 네트워크 왕복을 발생시킨다.

5. Serializer의 추가 lazy loading

app/serializers/video_serializer.rb:27-44ruby
attribute :capture do |video|
  {
    id: video.capture_id,
    name: video.has_attribute?(:capture_name) ? video.capture_name : video.capture.name,
    measure_ready_at: video.has_attribute?(:capture_measure_ready_at) ? ...
    method: video.has_attribute?(:capture_method) ? ... : video.capture.try(:capture_type).try(:method),
    ...
  }
end

create 액션에서는 preloaded 속성이 없으므로 capture, camera, record, level 등을 개별 쿼리로 lazy load한다. DB 시간 자체는 48ms로 작지만 추가 round-trip이 발생한다.

Log Evidence#

Datadog APM 트레이스에서 확인된 지연 패턴:

text
service:cupixworks-api resource_name:"Api::V1::VideosController#create" env:production @duration:>500ms

리전별 지연 분포:

text
| Region           | Count | Avg Duration | Max Duration | Avg DB Time | Non-DB Time |
| ap-southeast-2   | 8     | 1620ms       | 3142ms       | 48ms        | 1572ms      |
| us-west-2        | 33    | 1242ms       | 2122ms       | 49ms        | 1194ms      |
| eu-central-1     | 7     | 1094ms       | 1270ms       | 57ms        | 1036ms      |
| ap-southeast-1   | 2     | 1225ms       | 1287ms       | 41ms        | 1184ms      |

최대 지연 요청의 상세:

json
{
  "timestamp": "2026-05-26T04:17:36Z",
  "region": "ap-southeast-2",
  "host": "ip-10-1-145-251.ap-southeast-2.compute.internal",
  "team_id": 128,
  "capture_id": 73277,
  "total_duration_ms": 4142.52,
  "db_time_ms": 51,
  "non_db_time_ms": 4091,
  "file_type": ".insv"
}

핵심 패턴: DB 시간은 모든 요청에서 26-73ms로 정상이며, 총 지연의 95-99%가 non-DB 처리(S3 클라이언트 초기화 + presigned URL 생성)에서 소비된다. ap-southeast-2 리전이 가장 느린 것은 해당 리전에서의 AWS SDK 초기화 및 credential resolution 지연이 더 큰 것으로 추정된다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 S3 presigned URL 생성 시 매번 새 Aws::S3::Client 인스턴스 생성으로 인한 지연 storage_service.rb:11에서 매 호출마다 Aws::S3::Client.new 실행 확인. Non-DB 시간이 1000-4000ms로 SDK 초기화 비용과 일치. ap-southeast-2가 가장 느린 것은 원격 IMDS/credential 지연과 부합 Confirmed
H2 N+1 쿼리로 인한 DB 지연 Serializer에서 capture, camera, record, level 등을 lazy load하는 코드 확인 (video_serializer.rb:30-60) DB 시간이 평균 48ms로 정상 범위. 총 지연의 5% 미만 Rejected
H3 around_create_pano 콜백의 Resource INSERT가 병목 콜백에서 추가 INSERT 확인 (resourcable/video.rb:20-26) DB 시간에 이미 포함되어 있으며 48ms 범위 내. 단독으로 1000ms+ 지연을 설명하지 못함 Rejected
H4 Storage 선택 로직(set_storage)에서의 외부 호출 지연 storagable.rb:32-52에서 facility/team/default storage 조회 DB 쿼리만 수행하며 외부 호출 없음. DB 시간에 포함됨 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: app/services/cupix/storage_service.rb:5-12
  • 방향: Aws::S3::Client 인스턴스를 리전별로 캐싱한다. Thread.current 또는 RequestStore를 활용하여 동일 요청 내에서는 물론, 프로세스 수명 동안 리전+credential 조합별로 클라이언트를 재사용한다.
  • 근거: AWS SDK 클라이언트는 thread-safe하며 재사용이 권장된다. 매 요청마다 새로 생성할 필요가 없다.

단기 개선 (1주 이내)#

  • Serializer에서 upload_url 생성을 비동기/lazy로 분리: create 응답에서 upload_url을 즉시 반환하지 않고, 클라이언트가 별도 POST /videos/:id/upload_url 엔드포인트를 호출하도록 변경하거나, presigned URL 생성을 백그라운드로 이동하여 응답 지연을 줄인다.
  • Serializer preloading: create 액션에서 생성된 Video에 대해 capture, camera, record, level 연관 객체를 eager load하여 N+1 쿼리를 제거한다.

장기 개선 (재발 방지)#

  • Cupix::StorageService에 connection pooling 레이어를 도입하여 전체 서비스에서 S3 클라이언트 재사용을 보장한다.
  • APM에서 presigned URL 생성 시간을 별도 span으로 계측하여 지속적 모니터링이 가능하도록 한다.

Monitoring#

  • avg(trace.rack.request.duration){service:cupixworks-api, resource_name:Api::V1::VideosController#create} — p95/p99 지연 트래킹
  • 알림 조건: p95 > 2000ms 또는 p99 > 3000ms
text
service:cupixworks-api resource_name:"Api::V1::VideosController#create" @duration:>2000ms

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — S3 클라이언트 캐싱은 기존 StorageService 클래스 내 변경으로 완료 가능하며, 다른 resourcable 엔드포인트(panos, pointclouds 등)에도 동일한 개선 효과가 적용된다.