ES /docs

Api::V1::ElementsController#index (avg 1082ms, max 1082ms)

RCA: Api::V1::ElementsController#index Latency (1082ms)

Overview#

What Happened#

2026-05-27 00:16:25Z에 cupixworks-api의 Api::V1::ElementsController#index 엔드포인트에서 1082ms 지연이 감지되었다. 두 개의 대규모 facility(9,894개, 11,244개 element)를 동시에 페이지네이션하는 자동화 클라이언트(cupix-agent)가 직렬화 병목과 첫 페이지 DB 쿼리 오버헤드를 유발했다.

Quick Facts#

Field Value
resource_name Api::V1::ElementsController#index
top_frame app/repositories/element_repository.rb:41 (permission_joins)
runtime Ruby 3.3.7 / Rails
env production, us-west-2
avg_duration 1082ms
max_duration 1082ms

Affected Teams#

Team / Domain Error Count Impact
pclconstruction (team 739) 다수 요청 >500ms Element 목록 로딩 지연
gilbaneco (team 780) 다수 요청 >500ms Element 목록 로딩 지연

Timeline#

  1. 2026-05-26T23:09:28Z — facility pwwaqh(gilbaneco)에서 첫 slow 요청 감지 (741ms)
  2. 2026-05-27T00:14:09Z — facility umphr6(pclconstruction)에서 slow 요청 시작 (697ms)
  3. 2026-05-27T00:16:25Z — 두 사용자 동시 페이지네이션으로 최고 1,077ms 도달, 클러스터 감지
  4. 2026-05-27T00:16:42Z — siteinsights-service Lambda 호출 4건 확인 (downstream 추가 지연)

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::ElementsController#index",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 1082,
  "max_ms": 1082,
  "sample_trace_id": "1571525165356644939"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1 (>1s threshold), 9건 이상 >500ms
  • 최초 발생: 2026-05-27T00:16:25.321Z
  • 최근 발생: 2026-05-27T00:16:25.321Z
  • 영향 범위: 대규모 facility(10,000+ elements)를 페이지네이션하는 사용자에게 응답 지연 발생

Root Cause Summary#

직렬화(serialization) 비용과 첫 페이지 COUNT 쿼리 오버헤드가 주요 원인이다. 가장 느린 요청(1,077ms)은 전체 시간의 94%를 직렬화에 소비했다(1,010ms serialization vs 80ms DB). 300개 element를 22개 이상의 필드(bim_bounds, custom_properties 등 포함)로 직렬화할 때 JSON 변환 비용이 급증한다. 첫 페이지 요청 시에는 대규모 테이블의 COUNT(*) 쿼리로 DB 시간이 434-607ms까지 증가한다. 추가로 11개의 LEFT JOIN을 사용하는 permission_joins가 DB 쿼리 복잡도를 높이고, downstream siteinsights-service Lambda 호출이 네트워크 지연을 추가한다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/elements_controller.rb:13 (index action)
  • Query option 생성: Cupix::QueryOption::Element.new(get_query_option, params)
  • Repository 검색: element_repository.rb:258 (search 메서드 — ES vs DB 분기)
  • Elasticsearch 검색: element_repository.rb:278 (search_by_elasticsearch)
  • Permission 필터링: element_repository.rb:41 (permission_joins — 11개 LEFT JOIN)
  • 직렬화: ElementSerializer — 22+ 필드를 JSON으로 변환
  • Failure point: 직렬화 단계에서 300개 레코드 × 22+ 필드의 JSON 변환 비용 폭발
app/controllers/api/v1/elements_controller.rb:13-22ruby
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

repository_instance.search는 Elasticsearch 결과를 가져온 후 permission_joins를 적용한다:

app/repositories/element_repository.rb:41-50ruby
def self.permission_joins(response, query_option)
  # 11개의 LEFT JOIN subquery를 적용
  # review_public_permissions, review_user_permissions,
  # review_group_permissions, facility_user_permissions,
  # facility_group_permissions, facility_system_group_permissions,
  # workspace_user_permissions, workspace_group_permissions,
  # team_user_permissions, team_group_permissions,
  # team_system_group_permissions
  # ...
end

Permission 필터링 후 GROUP BY + MAX() 집계:

app/repositories/element_repository.rb:186ruby
.group('id').select(_select)
# _select에는 11개의 MAX() aggregate 함수 포함

기대 동작: 페이지당 300개 element를 <500ms 내에 응답 실제 동작: 직렬화에 최대 1,010ms 소요, 첫 페이지 DB 쿼리에 607ms 소요

Log Evidence#

Datadog에서 고지연 요청을 검색한 쿼리:

text
service:cupixworks-api @http.url_details.path:/api/v1/elements @duration:>500ms
Time range: 2026-05-26T23:00:00Z to 2026-05-27T01:00:00Z

가장 느린 요청 (1,077ms):

json
{
  "duration_ms": 1077,
  "db_duration_ms": 80,
  "serialization_ms": 1010,
  "page": "14/33",
  "total_entries": 9894,
  "facility_key": "pwwaqh",
  "user": "nbelmontelledo@gilbaneco.com",
  "team": "gilbaneco",
  "per_page": 300,
  "user_agent": "cupix-agent"
}

첫 페이지 요청 (843ms, DB 지배적):

json
{
  "duration_ms": 843,
  "db_duration_ms": 607,
  "serialization_ms": 245,
  "page": "1/38",
  "total_entries": 11244,
  "facility_key": "umphr6",
  "user": "mhughes@pcl.com",
  "team": "pclconstruction"
}

Downstream siteinsights-service Lambda 호출 (trace ID 연결):

text
service:siteinsights-service @http.url_details.path:/element_records
X-Datadog-Trace-Id: 1571525165356644939
json
{
  "timestamps": ["00:16:40.738Z", "00:16:41.094Z", "00:16:41.539Z", "00:16:42.398Z"],
  "facility_key": "pwwaqh",
  "level_ids": "68722",
  "per_page": 300,
  "user_agent": "rest-client/2.1.0 (linux x86_64) ruby/3.3.7p123"
}

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 직렬화 병목 — 300개 레코드의 22+ 필드 JSON 변환 비용 1,077ms 요청 중 1,010ms(94%)가 serialization; 대형 필드(bim_bounds, custom_properties) 포함 Confirmed
H2 첫 페이지 COUNT(*) 쿼리 오버헤드 page 1 요청의 DB 시간 434-607ms vs 후속 페이지 30-80ms COUNT 캐싱이 4시간 TTL로 존재(_pagination_total 메서드) Confirmed (캐시 미스 시)
H3 N+1 쿼리 문제 permission_joins의 11개 LEFT JOIN은 단일 쿼리로 실행됨 Datadog에 N+1 경고 없음; 직렬화 시간이 DB 시간보다 지배적 Rejected
H4 Downstream Lambda(siteinsights-service) 지연 동일 trace에서 Lambda 4회 호출 확인 Lambda 호출은 별도 비동기 경로로 추정; 메인 응답 시간에 직접 기여하는지 불확실 Inconclusive
H5 동시 사용자 부하로 인한 리소스 경합 두 사용자가 동시에 대규모 페이지네이션 수행; 3개 인스턴스 분산 인스턴스가 3개로 분산되어 있어 CPU 경합 가능성 낮음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • app/serializers/element_serializer.rb — 기본 응답에서 무거운 필드(bim_bounds, custom_properties) 제외, fields[] 파라미터로 명시 요청 시에만 포함하도록 조건부 직렬화 적용
  • app/repositories/element_repository.rb:474-485_pagination_total의 캐시 키를 facility_id + query_hash 기반으로 단순화하여 캐시 적중률 향상

단기 개선 (1주 이내)#

  • per_page 최대값을 300에서 100으로 축소하거나, 대규모 facility에 대해 cursor-based pagination 도입
  • ElementSerializer에 fields 파라미터 기반의 sparse fieldset 지원 추가 — 클라이언트가 필요한 필드만 요청하도록 유도

장기 개선 (재발 방지)#

  • Permission 필터링을 Elasticsearch 인덱스에 사전 반영하여 11개 LEFT JOIN을 제거
  • Materialized view 또는 denormalized permission 테이블로 permission_joins 쿼리 복잡도를 O(1)로 축소
  • cupix-agent 자동화 클라이언트에 rate limiting 적용하여 동시 대량 페이지네이션 방지

Monitoring#

  • 추가할 메트릭: elements_controller.serialization_duration_ms (serialization 시간 별도 추적)
  • Datadog APM에 @duration:>500ms 임계값 알림 설정
text
service:cupixworks-api resource_name:"Api::V1::ElementsController#index" @duration:>1000ms
  • Facility별 element 수 대시보드 — 10,000+ element facility 모니터링

Risk Assessment#

  • Risk level: low (기능 장애 아닌 성능 저하, 대규모 facility 한정)
  • 예상 복잡도: standard (직렬화 최적화는 비교적 안전한 변경, permission 리팩터링은 장기 과제)