ES /docs

Api::V1::SitetracksController#captures (avg 13190ms, max 13190ms)

RCA: Api::V1::SitetracksController#captures (avg 13190ms, max 13190ms)

Overview#

What Happened#

2026-07-09 01:39 KST 에 cupixworks-apiGET /api/v1/sitetracks/:id/captures 요청 한 건이 13.07초 걸려 latency threshold 를 넘겼다. Sitetrack 20896 에 대한 요청이었고 응답은 HTTP 200 이었지만, DB 5.78s + serialization 4.29s 로 대부분의 시간을 소비했다. 반환된 capture 는 total_entries: 1 로 단 1건이었다.

Quick Facts#

Field Value
resource_name Api::V1::SitetracksController#captures
top_frame app/repositories/sitetrack_repository.rb:33-47
serializer CaptureSerializer (~45 attributes, ~30 concern mixins)
duration 13072.19 ms (db 5782.8 ms, serialization 4287 ms)
params per_page=100, page=1, id=20896, fields=[45개]
result total_entries: 1 (1 capture 반환)
user team gilbaneco (id 780), user id 49405
user_agent cupix-agent
host ip-10-1-80-134.us-west-2.compute.internal
env production, us-west-2
deploy production-us-west-2-20260708t0836z0-5c918141-cupixworks

Affected Teams#

Team / Domain Error Count Impact
gilbaneco (team 780) 1 cupix-agent 클라이언트에서 sitetrack captures 응답 13초 지연

과거 14일 창에서 같은 endpoint 가 5초 이상 걸린 요청은 27건 관측됐다 (아래 Log Evidence 참고). 다수의 tenant/team 에 걸쳐 나타난다.

Timeline#

  1. 2026-07-08 17:36 KST — 배포 production-us-west-2-20260708t0836z0-5c918141-cupixworks 롤아웃 (참고용, 이 요청 이전).
  2. 2026-07-09 01:39:16 KST — 요청 수신, sitetrack 20896 의 captures 조회 시작.
  3. 2026-07-09 01:39:29 KST — 13.07s 후 [200] 응답 완료. error-sweeper 가 latency cluster 로 캡처.

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::SitetracksController#captures",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 13190,
  "max_ms": 13190,
  "sample_trace_id": "7886792162004020010"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1 (이 클러스터 파일 기준)
  • 최초 발생: 2026-07-09 01:39 KST
  • 최근 발생: 2026-07-09 01:39 KST
  • 더 넓은 관측: 최근 14일 동안 같은 resource 에서 5초 이상 걸린 요청 27건, 최대 247초 (2026-07-03T08:43:50Z, sitetrack 20577)

Root Cause Summary#

SitetracksController#capturesSitetrackRepository#captures 를 호출해 has_many :captures, through: :associated_captures 결과를 will_paginate 로 감싸 반환한다. 이 relation 은 eager load (includes / preload) 없이 CaptureSerializer 로 전달되고, serializer 는 45개 attribute 와 ~30개 concern mixin (StorageAttribute, ThumbnailAttribute, PermissionAttribute, FacilityAttribute, WorkspaceAttribute, TeamAttribute, ReconstructionAttribute 등) 을 통해 다수의 association 을 접근한다. 결과적으로 반환되는 capture 개수만큼 N+1 쿼리와 파생 계산이 발생한다. 문제의 요청은 total_entries: 1 이지만 fields 배열에 record, level, floorplan, user, team, facility, workspace, camera, voxels_result_urls, process_output_upload_url 등 무거운 필드가 45개 포함되어 있어 단일 capture 처리에도 DB 5.78초 + serialization 4.29초가 걸렸다. 즉 근본 원인은 repository 계층에서 preloading 이 없고 serializer 가 sparse-fieldsets 요청과 무관하게 무거운 concern chain 을 실행한다는 점이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/sitetracks_controller.rb:49-62
  • Repository call: app/repositories/sitetrack_repository.rb:33-47
  • Model association: app/models/sitetrack.rb:25-26
  • Serialization: app/serializers/capture_serializer.rb:1-100

Controller 는 repository 결과를 그대로 CaptureSerializer 에 넘긴다.

app/controllers/api/v1/sitetracks_controller.rb:49-62ruby
def captures
  captures = repository_instance.captures(@query_option)

  render_api Renderable.new({
    search_result: captures,
    is_collection: true,
    serializer: CaptureSerializer,
    serializer_option: {
      fields: {
        capture: @fields
      }
    }
  })
end

Repository 의 captures@model.captures (has_many through) 를 그대로 paginate 만 한다. includes / preload / eager_load 없음.

app/repositories/sitetrack_repository.rb:33-47ruby
def captures(query_option)
  captures = @model.captures.paginate(per_page: query_option.per_page, page: query_option.page)

  SearchResult.new({
    contents: captures,
    pagination: {
      total_entries: captures.total_entries,
      total_pages: captures.total_pages,
      per_page: captures.per_page,
      previous_page: captures.previous_page,
      current_page: captures.current_page,
      next_page: captures.next_page
    }
  })
end

Sitetrack 의 captures 는 SitetrackCapture 를 경유하는 join association 이다.

app/models/sitetrack.rb:25-26ruby
has_many :associated_captures, class_name: 'SitetrackCapture', dependent: :destroy
has_many :captures, through: :associated_captures

CaptureSerializer 는 45개 attribute 를 선언하고 permission, storage, thumbnail, facility, team, workspace, camera, reconstruction 등 30여 개 concern module 을 include 한다. sparse-fieldsets (params[:fields]) 가 있어도 concern 들이 include 되어 있으면 클래스 로드/callback 은 그대로 실행된다.

app/serializers/capture_serializer.rb:5-100ruby
attribute :id
attribute :name
attribute :clusters_count
attribute :capture_mode
# ...
attribute :record, &:_record
attribute :level, &:_level
attribute :floorplan, if: proc { |capture| capture.floorplan_id.present? } do |capture|
  _cached_floorplan = capture._floorplan || { 'id': capture.floorplan_id }
  _cached_floorplan.except(*::Floorplan.excluded_fields_to_serialize)
end
# ...
include StorageAttribute
include CaptureTypeAttribute
include ThumbnailAttribute
include PermissionAttribute
# ... (총 30여 개 include)

기대 동작: 단일 capture 응답이 sub-second 이내에 완료. 실제 동작: DB 5.78초 + serialization 4.29초 = 13초. 로그의 db 필드가 5.78초로 지배적이므로 serializer 안에서 lazy association 접근이 다수 SQL 로 이어졌다고 볼 수 있다 (preload 되지 않은 record, level, floorplan, user, team, facility, workspace, camera, permission joins).

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-api "sitetracks/20896/captures"

문제의 요청 원문 (핵심 필드만):

json
{
  "@timestamp": "2026-07-08T16:39:29.225Z",
  "service": "cupixworks-api",
  "controller": "Api::V1::SitetracksController",
  "action": "captures",
  "message": "[200] GET /api/v1/sitetracks/20896/captures (Api::V1::SitetracksController#captures)",
  "duration": 13072.19,
  "db": 5782.8,
  "serialization": { "duration": 4287 },
  "pagination": { "per_page": 100, "total_pages": 1, "total_entries": 1, "current_page": "1" },
  "params": {
    "per_page": "100",
    "page": "1",
    "id": "20896",
    "fields": ["id","name","state","material","error_code","expected_quality","meta","record","level","bim_icp_tm","use_bim_icp_tm","upload_state","running_state","processing_status","preprocessor_path","skatmaster_path","postprocessor_path","selected_unrefined_cluster_id","refinement_state","refinement_floorplan_type","creation_platform","migrated_from","method","skat_version","dnn_version","agent_version","maker_version","ar_data_version","user","team","facility","workspace","camera","floorplan","updated_at","published_at","created_at","voxel_state","voxels_result_urls","process_output_upload_url","reconstruction_state","current_step","analysis_state","summary","summary_state"]
  },
  "team": { "domain": "gilbaneco", "id": 780 },
  "user_agent": "cupix-agent",
  "http": { "status_code": 200, "method": "GET" }
}

동일 endpoint 5초 이상 요청 (service:cupixworks-api "SitetracksController#captures" @duration:>5000, 최근 14일):

text
2026-07-08T16:39:29  dur=13072  db=5782  ser=4287  fields=45  pp=100  id=20896   ← 본 클러스터
2026-07-03T10:53:19  dur=11730  db=8466  ser=810   fields=1   id=20581
2026-07-03T08:43:50  dur=247235 db=11292 ser=12525 fields=1   id=20577  ← 최대 outlier
2026-07-03T08:37:58  dur=24635  db=12942 ser=1156  fields=1   id=20528
2026-07-03T08:36:31  dur=12079  db=1457  ser=116   fields=1   id=20229
2026-07-01T02:40:37  dur=13008  db=2925  ser=215   fields=1   id=20329
2026-07-01T02:27:26  dur=7545   db=5864  ser=5     fields=1   id=20329
2026-07-01T01:44:58  dur=11822  db=491   ser=177   fields=1   id=20342
2026-06-30T22:31:53  dur=6933   db=1911  ser=228   fields=45  pp=100  id=20461
(총 27건, 대부분 2026-07-03 08:20~10:53 UTC 창에 집중)

관찰:

  • 2026-07-03 08:20~10:53 UTC 에 20건이 몰려 있어 DB / 서버 부하 spike 정황 존재. 이는 본 클러스터의 단일 요청과는 별개 사건이지만 같은 endpoint 가 반복적으로 slow 하다는 증거.
  • fields=[id] 만 요청해도 5~11초씩 걸리는 사례가 다수 → serializer sparse-fieldsets 가 대부분 concern 로딩을 우회하지 못하는 것으로 추정.
  • 본 클러스터는 fields=45 로 최대 payload 를 요청한 케이스.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 SitetrackRepository#captures 가 preload 없이 relation 을 반환해 CaptureSerializer 가 45 field 처리 중 N+1 쿼리 유발 app/repositories/sitetrack_repository.rb:33-47includes/preload 없음. 로그 db=5782.8ms 로 DB 시간이 지배적. params.fieldsrecord/level/floorplan/user/team/facility/workspace/camera 등 association 필드가 포함됨. total_entries: 1 인데도 serialization=4287ms Confirmed
H2 CaptureSerializer 의 concern chain (StorageAttribute, ThumbnailAttribute, PermissionAttribute 등 ~30개) 이 sparse-fieldsets 와 무관하게 매 capture 마다 실행되어 latency 상승 app/serializers/capture_serializer.rb:76-99 에 30여 개 include. fields=[id] 만 요청한 다른 요청도 5-11s 걸림 (2026-07-03 로그 다수) 이 요청은 fields=45 로 최대이므로 concern chain 만이 유일한 원인은 아님 Confirmed (contributing)
H3 DB 인프라 spike 로 인한 지연 (RDS CPU, replica lag 등) 2026-07-03 08:20~10:53 UTC 창에 slow 요청 20건 집중 (동일 endpoint) 본 클러스터의 2026-07-08 16:39 UTC 요청은 그 spike 창과 무관한 단일 사건 Inconclusive — needs verification (RDS/Datadog infra metric 확인 필요)
H4 Sitetrack 20896 이 비정상적으로 많은 capture 를 통해 join 되어 지연 로그 total_entries: 1 — 실제 반환 capture 는 1건뿐 데이터 규모가 아님 Rejected
H5 Pagination count 쿼리 (SELECT COUNT(*)) 가 join 을 통해 무거워짐 will_paginatetotal_entries 를 위해 별도 count query 수행. has_many through 는 join 을 강제 단일 count 로 5.7s 를 설명하기는 부족. serializer 단계의 4.29s 는 별도 원인 Inconclusive — needs verification (slow query log 확인)

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 단일 사건이며 5xx 가 아닌 200 응답이므로 즉시 rollback / hotfix 대상은 아니다. 반복 재발 여부를 모니터링한 후 아래 단기 개선을 통해 근본 원인을 제거한다.

단기 개선 (1주 이내)#

  • app/repositories/sitetrack_repository.rb:33-47@model.captures.paginate(...) 앞에 CaptureSerializer 가 실제로 접근하는 association 만 골라 preload 를 추가한다. 후보: record, level, floorplan, user, team, facility, workspace, camera. 넓게 includes 를 걸면 payload 폭발 위험이 있으므로 필요한 것만 명시.
  • 파일: app/repositories/sitetrack_repository.rb:34
  • 근거: 위 H1. 동일 endpoint 에서 반복적으로 5-13초가 발생하는데 총 반환 개수는 대부분 1건 수준. preload 만으로 대부분의 N+1 이 제거되어야 함.
  • params[:fields] sparse-fieldsets 를 존중하도록 serializer 를 최적화 — 최소한 include 된 concern 중 요청에 없는 필드를 참조하는 것들은 attribute 정의를 if: 프록으로 감싸 lazy 화한다. 파일: app/serializers/capture_serializer.rb.

장기 개선 (재발 방지)#

  • Rails APM 상 slow endpoint threshold (예: Api::V1::SitetracksController#captures p95 > 2s) 를 정의하고 Datadog monitor 로 감시.
  • CaptureSerializer 를 두 개로 분리 (경량 list serializer vs 상세 show serializer) — capture 목록 조회는 sparse fieldsets 를 강제로 축소.
  • Rails N+1 detector (bullet gem 등) 를 staging 에서 활성화하고 CI 에서 warning 을 error 로 승격.
  • 반복적인 slow endpoint 를 통합 관리하는 SLO / SLI 대시보드 정비.

Monitoring#

Datadog release dashboard 에 추가할 timeseries widget 쿼리:

text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::sitetrackscontroller#captures}
text
max:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::sitetrackscontroller#captures}
text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::sitetrackscontroller#captures}.as_count()

로그 기반 slow request 카운트 (log-based metric 필요 시):

text
logs("service:cupixworks-api \"SitetracksController#captures\" @duration:>5000").index("*").rollup("count").by("environment")

참고: trace.rack.request.duration 계열 metric 이 tenant 계정에 배출되지 않는 경우, 위 로그 쿼리를 log-based metric 으로 승격시켜 대체한다. 실제 metric 이름은 Datadog Metrics Explorer 에서 trace.*duration* prefix 로 조회해 확정.

알림 임계값 제안:

  • avg p95 (5분 rollup) > 3s 로 warning
  • max 30s 초과 시 page

Risk Assessment#

  • Risk level: medium — 사용자 대면 endpoint 에서 반복적 slow request 존재. 5xx 는 아니지만 UX 열화 및 cupix-agent 같은 자동화 클라이언트의 backoff 유발 가능.
  • 예상 복잡도: standardpreload 추가는 low-risk 이지만 serializer 리팩터는 associated spec 회귀 확인 필요.