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-api 의 GET /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#
- 2026-07-08 17:36 KST — 배포
production-us-west-2-20260708t0836z0-5c918141-cupixworks롤아웃 (참고용, 이 요청 이전). - 2026-07-09 01:39:16 KST — 요청 수신, sitetrack 20896 의 captures 조회 시작.
- 2026-07-09 01:39:29 KST — 13.07s 후
[200]응답 완료. error-sweeper 가 latency cluster 로 캡처.
Error Log#
{
"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#captures 는 SitetrackRepository#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 에 넘긴다.
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 없음.
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 이다.
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 은 그대로 실행된다.
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 쿼리:
service:cupixworks-api "sitetracks/20896/captures"
문제의 요청 원문 (핵심 필드만):
{
"@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일):
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-47 에 includes/preload 없음. 로그 db=5782.8ms 로 DB 시간이 지배적. params.fields 에 record/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_paginate 는 total_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#capturesp95 > 2s) 를 정의하고 Datadog monitor 로 감시. - CaptureSerializer 를 두 개로 분리 (경량 list serializer vs 상세 show serializer) — capture 목록 조회는 sparse fieldsets 를 강제로 축소.
- Rails N+1 detector (
bulletgem 등) 를 staging 에서 활성화하고 CI 에서 warning 을 error 로 승격. - 반복적인 slow endpoint 를 통합 관리하는 SLO / SLI 대시보드 정비.
Monitoring#
Datadog release dashboard 에 추가할 timeseries widget 쿼리:
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::sitetrackscontroller#captures}
max:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::sitetrackscontroller#captures}
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::sitetrackscontroller#captures}.as_count()
로그 기반 slow request 카운트 (log-based metric 필요 시):
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 로 조회해 확정.
알림 임계값 제안:
avgp95 (5분 rollup) > 3s 로 warningmax30s 초과 시 page
Risk Assessment#
- Risk level: medium — 사용자 대면 endpoint 에서 반복적 slow request 존재. 5xx 는 아니지만 UX 열화 및 cupix-agent 같은 자동화 클라이언트의 backoff 유발 가능.
- 예상 복잡도: standard —
preload추가는 low-risk 이지만 serializer 리팩터는 associated spec 회귀 확인 필요.