Api::V1::ElementsController#index (avg 26130ms, max 26130ms)
RCA: Api::V1::ElementsController#index latency spike (26130ms)
Overview#
What Happened#
2026-07-01 07:43 KST (production us-west-2)에 Api::V1::ElementsController#index 한 건이 26.1초 동안 처리되었다. 동일 facility(umphr6) 사용자가 per_page=300으로 38페이지 분량(11,244 elements)을 페이지네이션으로 순회하던 도중 page 34가 outlier로 지연되었으며, 같은 시각 동일 host(ip-10-1-19-190)에서 PanosController의 bulk/create/check_uploading 요청이 대량으로 발생해 Puma worker 가 혼잡 상태였다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::ElementsController#index |
| sample_trace_id | 8674831959568864694 |
| sample_request_id | 66551ef5-e664-46c7-858f-7163bd3fe81e |
| duration | 26089.93ms (db 1729.1ms, serialization 2521ms, view 0.12ms) |
| host | ip-10-1-19-190.us-west-2.compute.internal |
| env | production / us-west-2 |
| version | production-us-west-2-20260630t0635z0-c527a441-cupixworks |
| tenant / team | cupix / pclconstruction (team_id 739) |
| user | mhughes@pcl.com (id 49120) |
| facility_key | umphr6 |
| pagination | page 34 / 38, per_page 300, total_entries 11244 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| pclconstruction (team_id 739) | 1 | 단일 사용자(mhughes@pcl.com)의 element 목록 조회 1건이 26초 지연. 응답은 200으로 성공. 동일 시점 같은 사용자의 다른 페이지(33, 35, 36 등)는 0.4–3.5초 내 정상 처리됨. |
Timeline#
- 2026-07-01 07:42:13 KST — 사용자 mhughes@pcl.com 가 facility
umphr6element 목록을 per_page=300으로 페이지네이션 시작 (page 14부터 로그 관측). 페이지당 0.4–3초로 정상 처리. - 2026-07-01 07:42:41 KST — page 34 요청이 host
ip-10-1-19-190에 도달 (duration 26090ms 역산). - 2026-07-01 07:42:53 KST — 같은 cluster에서
PanosController#create/bulk/check_uploading요청 폭증 시작 (수십 건이 5–16초 소요). - 2026-07-01 07:43:07 KST — page 34 요청 완료 (200 OK, 26089ms). cluster file의
last_seen기준 시각. - 2026-07-01 07:43:08–07:43:17 KST — page 35–38 요청은 다시 0.4–2.5초로 회복.
- 2026-07-01 07:48–07:49 KST — PanosController bulk/index가 본격적으로 느려져 40–109초 소요 (별도 cluster들로 status board incident
2026-06-30-svc-cupixworks-api--unknown-1의 후속 이벤트로 분류됨).
Error Log#
{
"resource_name": "Api::V1::ElementsController#index",
"service": "cupixworks-api",
"occurrences": 1,
"avg_ms": 26130,
"max_ms": 26130,
"sample_trace_id": "8674831959568864694"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-07-01 07:42 KST
- 최근 발생: 2026-07-01 07:42 KST
요청은 200 OK로 성공했으나, 클라이언트 입장에서 26초 대기. 동일 사용자/세션의 다른 페이지는 정상 응답했고 다른 facility의 element 조회에는 영향 없음. 다만 같은 시각 동일 호스트에서 PanosController 트래픽이 폭증하기 시작했고, 6분 뒤(07:48 KST 이후) 같은 서비스의 다수 endpoint가 40–100초대까지 지연되며 service-level degradation incident로 확산됨 (status board 동일 incident에 5개 cluster 그룹됨).
Root Cause Summary#
해당 26초 latency 는 ElementsController#index 코드 자체의 결함이 아니라, 동일 Puma worker host 의 동시 부하 급증에 따른 처리 지연이다. 슬로우 요청의 내역을 보면 DB 1.7s + Serialization 2.5s 로 코드 경로상의 작업 시간은 4.2s 수준에 불과하고 나머지 22s 는 Puma queue wait 또는 GC pause 로 추정된다 (Datadog request 로그의 응답 시점 사이의 wall time). 같은 시각 host duration 은 Rails dispatch 진입ip-10-1-19-190 에서 PanosController#create / #bulk / #check_uploading / #index 가 동시에 수십 건 인입되어 5–24초씩 처리되고 있었고, 이 부하가 ElementsController page 34 요청과 시간적으로 정확히 겹친다. page 34 만 outlier 가 된 이유는 해당 요청이 도착한 순간 worker pool 이 panos 업로드 처리 로 점유되어 있었기 때문이다.
추가로, ElementRepository#search_by_database 와 _pagination_total 은 페이지마다 response.to_sql(LIMIT/OFFSET 포함)을 캐시 키로 사용하기 때문에 페이지마다 별도의 COUNT 쿼리가 실행된다. 단일 요청 기준으로는 영향이 크지 않지만 (정상 페이지의 db 시간이 200–300ms), worker contention 상황에서 추가 DB round-trip 한 번이 latency 의 변동성을 키우는 부차적 요인이 된다.
Technical Analysis#
Code Path#
Entry point: app/controllers/api/v1/elements_controller.rb:13 (Api::V1::ElementsController#index).
def index
element_query_option = Cupix::QueryOption::Element.new(get_query_option, params)
elements = repository_instance.search(element_query_option)
render_api Renderable.new({
search_result: elements,
is_collection: true,
serializer_option: @serializer_option
})
end
record_id 가 없는 일반 element 목록 조회이므로 query_method 가 :database 를 반환하고 search_by_database 가 호출된다.
def search(query_option = nil)
set_query_option(query_option)
# ...
if query_method(self.query_option) == :elasticsearch
search_by_elasticsearch(visibility)
else
search_by_database(visibility)
end
end
DB 경로에서 default_joins 는 단순 select 이며 무거운 permission_joins 는 사용되지 않는다.
def search_by_database(visibility)
# ...
response = self.class.default_joins(self.class.current_class.visibility_scope(visibility))
response = response.where(facilities: { key: self.query_option.facility_key || @review.facility.key })
# ... where(bim_id / category_id / level_id) ...
response = response.paginate(per_page: self.query_option.per_page, page: self.query_option.page)
pagination_total = _pagination_total(response)
SearchResult.new({ contents: response, pagination: pagination_total.merge({...}) })
end
_pagination_total 의 캐시 키에는 paginate 후의 SQL(LIMIT/OFFSET 포함) 이 들어가므로 페이지마다 캐시 미스가 발생해 COUNT 쿼리가 매 페이지마다 실행된다.
def _pagination_total(response)
Rails.cache.fetch({
cached_pagination_total: {
sql: response.to_sql, # paginate 된 SQL이라 페이지마다 키 다름
t: _cached_total_entries_timestamp(self.query_option.facility_key || @review.facility.key)
}
}, expires_in: 4.hours) do
{ total_entries: response.total_entries, total_pages: response.total_pages }
end
end
Failure point: 외부 부하. ElementsController 자체에는 버그 없음. 본 cluster의 latency 는 동일 host 의 다른 endpoint (PanosController#create/bulk/check_uploading/index) 트래픽 폭증으로 worker 가 점유된 결과.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api "ElementsController#index" @duration:>15000
시간 범위 2026-06-30T22:00:00Z – 2026-06-30T23:30:00Z. 결과 1건.
문제의 slow request 원본 로그(요약):
{
"@timestamp": "2026-06-30T22:43:07.155Z",
"request_id": "66551ef5-e664-46c7-858f-7163bd3fe81e",
"host": "ip-10-1-19-190.us-west-2.compute.internal",
"controller": "Api::V1::ElementsController",
"action": "index",
"http": { "status_code": 200, "method": "GET", "url_details": { "path": "/api/v1/elements" } },
"duration": 26089.93,
"db": 1729.1,
"view": 0.12,
"serialization": { "duration": 2521 },
"pagination": { "per_page": 300, "current_page": "34", "total_pages": 38, "total_entries": 11244, "previous_page": 33, "next_page": 35 },
"params": { "facility_key": "umphr6", "per_page": "300", "page": "34", "fields": ["id","bim_external_id","...","custom_properties","bim_bounds","cycle_state","cycle_state_updated_reason"] },
"user": { "id": 49120, "email": "mhughes@pcl.com", "team": { "id": 739 } },
"team": { "domain": "pclconstruction", "id": 739 }
}
같은 facility/user 의 인접 페이지 latency (정상):
ts dur(ms) db(ms) serialization(ms) page
2026-06-30T22:42:31.037Z 2266.44 330.61 955 26
2026-06-30T22:42:33.042Z 2484.69 266.66 1714 27
2026-06-30T22:42:35.047Z 1701.62 317.18 912 29
2026-06-30T22:42:39.059Z 1401.46 257.16 668 32
2026-06-30T22:42:39.780Z 1227.34 294.88 669 33
2026-06-30T22:43:07.155Z 26089.93 1729.10 2521 34 ← outlier
2026-06-30T22:43:07.867Z 383.41 206.41 255 35
2026-06-30T22:43:07.869Z 710.56 313.06 401 36
2026-06-30T22:43:13.178Z 2539.42 334.72 1688 37
2026-06-30T22:43:17.187Z 1578.29 340.65 665 38
page 34 의 시작 시각(완료 시각 – duration ≈ 22:42:41.066Z) 직후 같은 host ip-10-1-19-190 에서 PanosController 부하 폭증:
service:cupixworks-api @duration:>5000 (22:35–22:43)
ts dur(ms) db(ms) controller action
2026-06-30T22:42:53.124Z 7375.02 1228.20 Api::V1::PanosController create
2026-06-30T22:42:55.125Z 6919.10 653.55 Api::V1::PanosController create
2026-06-30T22:42:55.126Z 6255.12 453.90 Api::V1::PanosController check_uploading
2026-06-30T22:42:55.126Z 9255.07 954.95 Api::V1::PanosController create
2026-06-30T22:42:55.126Z 16349.60 905.43 Api::V1::PanosController index
2026-06-30T22:42:57.131Z 13202.97 1011.35 Api::V1::PanosController index
2026-06-30T22:42:59.137Z 15431.94 947.17 Api::V1::PanosController index
2026-06-30T22:42:59.139Z 24192.76 958.61 Api::V1::PanosController index
이후 service-wide 로 확산:
ts dur(ms) db(ms) controller action
2026-06-30T22:48:14.111Z 43681.90 10983.98 Api::V1::PanosController bulk
2026-06-30T22:48:14.112Z 109012.31 12338.68 Api::V1::PanosController bulk
2026-06-30T22:48:34.184Z 81756.59 12501.47 Api::V1::PanosController bulk
Status board 결과: 같은 incident 2026-06-30-svc-cupixworks-api--unknown-1 (started 22:35:25Z) 에 5개 cluster (3ce7061c, 8a18b22e, 6633f842, 3580e20c, 0c05ab76) 가 묶여 있음 — 본 cluster 는 그 incident 의 한 증상이다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 동시 PanosController 부하 폭증으로 Puma worker 가 점유되어 ElementsController page 34 가 host queue 에서 대기/GC pause 를 겪었다 | 같은 host ip-10-1-19-190 에서 22:42:53 부터 PanosController create/bulk/check_uploading/index 가 동시에 수십 건 인입, 각 5–24초 처리. page 34 가 정확히 이 시간대에 시작. 완료 직후의 page 35, 36 은 0.4–0.7초로 회복. status board incident 가 5개 cluster 로 확장됨. |
없음 (시간 정렬과 endpoint 분포가 매우 정확히 일치) | Confirmed |
| H2 | _pagination_total 의 캐시 키에 paginate 된 SQL(response.to_sql) 이 들어가 페이지마다 COUNT 가 발생, 이것이 26초 latency 의 주 원인 |
_pagination_total 의 캐시 키 구조상 페이지마다 cache miss 발생 (app/repositories/element_repository.rb:525-537) |
정상 페이지(26–33, 35–38)도 동일 COUNT 를 수행하지만 db 시간이 200–340ms 수준이고 총 latency 도 0.4–3.5초로 정상. db=1729ms 도 26s를 설명하지 못함 | Rejected (latency spike 의 주원인은 아님 — 다만 page 별 COUNT는 비효율로서 부차적 개선 대상) |
| H3 | Elasticsearch 경로의 무거운 bounding-box / permission join 이 page 34 에서 트리거되었다 | — | params 에 record_id 가 없어 query_method 가 :database 를 반환(app/repositories/element_repository.rb:512-514). ES 경로 자체가 호출되지 않음 |
Rejected |
| H4 | umphr6 facility 의 특정 element 가 비정상적으로 큰 custom_properties / bim_bounds JSON 을 가져 page 34 의 serialization 이 폭주 |
serialization.duration=2521ms 가 정상 페이지(913–1714ms)보다 다소 크고 fields 에 custom_properties, bim_bounds 포함 |
2521ms 는 26s 의 ~10% 에 불과. 동일 dataset의 page 35, 36 은 같은 fields 로도 0.4–0.7초로 처리됨 | Rejected (부차적 기여, 주원인 아님) |
Fix Recommendation#
즉시 조치 (Critical)#
- 별도 코드 변경 불필요. 본 cluster 는 단일 발생이고 응답이 200 OK 로 성공한 latency 이벤트이다. 같은 시각의 service-wide degradation 은 이미 status board incident
2026-06-30-svc-cupixworks-api--unknown-1에 묶여 있으므로, 해당 incident 의 RCA(상위 원인: PanosController bulk/create 폭증) 와 묶어서 처리한다. - 모니터링 측면에서
Api::V1::PanosController#bulk,#create,#check_uploading의 동시성/대기 큐를 추적할 alert (아래 Monitoring 섹션) 를 추가한다.
단기 개선 (1주 이내)#
app/repositories/element_repository.rb:525-537_pagination_total의 캐시 키에서response.to_sql대신 페이지/오프셋을 제외한 base scope의 식별자(facility_key + 필터 hash) 를 사용하도록 변경해 page 마다 COUNT 가 재실행되지 않게 한다. 현재는 페이지마다 cache miss 가 발생하고 4시간 TTL 도 사실상 무의미하다.ElementsController#index에 per_page 상한(예: 300) 을 명시적으로 enforce 하고 클라이언트가 전 페이지를 순회하는 use case (cupix-agent) 가 있다면scroll(이미ScrollableRepository가 include 되어 있음) 또는 search_after 를 우선 사용하도록 클라이언트 통합을 검토한다.
장기 개선 (재발 방지)#
- Puma worker pool 이 panos 업로드 같은 장기 I/O 작업과 짧은 read API 를 같이 처리하면서 short read 가 큐잉으로 지연되는 패턴이 반복된다. read-heavy endpoint(
Elements#index,Panos#index) 와 upload-heavy endpoint(Panos#bulk/#create)를 별도 ASG 또는 별도 puma 프로세스 그룹으로 분리하는 안을 검토한다. cupix-agent(User-Agent) 처럼 자동화된 클라이언트가 facility 의 전체 element 를 페이지네이션으로 풀로 끄는 워크플로는 ES 의 search_after / scroll API 또는 server-side cursor 로 전환해 page-wise OFFSET cost 와 COUNT 중복을 제거한다.- request log 에 Puma queue wait time(
X-Request-Start기반) 을 별도 필드로 노출해 “DB+serialization 이 짧은데 duration 만 큰” 케이스를 자동 분류할 수 있게 한다.
Monitoring#
- ElementsController#index 의 p99 latency:
avg:trace.rack.request.duration.by.resource_service.99p{service:cupixworks-api,resource_name:api::v1::elementscontroller#index}
- Panos bulk/create 의 활성 동시성(상위 원인 endpoint):
avg:trace.rack.request.duration.by.resource_service.95p{service:cupixworks-api,resource_name:api::v1::panoscontroller#bulk}
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::panoscontroller#create}.as_rate()
- Rails request error/slow rate (host 단위로 long-tail 식별):
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::elementscontroller#index} by {host}.as_rate()
- DB 누적 시간 (전체 cupixworks-api):
avg:trace.active_record.duration{service:cupixworks-api}
Risk Assessment#
- Risk level: low (단일 발생, 응답 성공, 동일 사용자의 다른 페이지는 정상)
- 예상 복잡도: standard (즉시 조치는 없음. 단기 개선의
_pagination_total캐시 키 정리는 small/standard scope; 장기 ASG 분리는 critical scope)