SearchesController#counts — sequential ES queries bottleneck
RCA: Api::V1::SearchesController#counts High Latency (avg 2997ms, max 4329ms)
Overview#
What Happened#
2026-05-26 04:56~13:08 UTC 동안 cupixworks-api의 Api::V1::SearchesController#counts 엔드포인트에서 평균 2997ms, 최대 4329ms의 높은 응답 지연이 ap-southeast-2, us-west-2, eu-central-1 세 리전에서 14건 발생했다. 모든 요청은 HTTP 200으로 정상 응답했으나 사용자 체감 성능이 크게 저하되었다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::SearchesController#counts |
| top_frame | app/controllers/api/v1/searches_controller.rb:4 |
| runtime | Ruby on Rails + Elasticsearch (Searchkick) |
| env | production (ap-southeast-2, us-west-2, eu-central-1) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| admin | 1 | 최대 지연(4329ms) — state 필터 기반 쿼리 |
| tpc | 1 | 4270ms — 텍스트 검색 |
| voith | 3 | 3170~3931ms — Elasticsearch 검색 지연 주도 |
| nestle | 2 | 3705ms, 2132ms — 대규모 데이터셋 |
| performance | 1 | 3507ms — DB 시간 2369ms |
| graymont | 1 | 2173ms — DB 시간 1885ms |
Timeline#
- 2026-05-26T04:56:42Z — 최초 고지연 요청 감지
- 2026-05-26T05:14:17Z — 최대 지연(4329ms) 발생 (admin 팀, state:"holding" 필터)
- 2026-05-26T13:08:04Z — 마지막 고지연 요청 기록
- 2026-05-27 — RCA 분석 완료
Error Log#
{
"resource_name": "Api::V1::SearchesController#counts",
"service": "cupixworks-api",
"occurrences": 4,
"avg_ms": 2652,
"max_ms": 4329,
"sample_trace_id": "3914089223142541906"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 14
- 최초 발생: 2026-05-26T04:56:42.761Z
- 최근 발생: 2026-05-26T13:08:04.418Z
- 영향: 검색 카운트 API 응답 지연으로 UI 검색 결과 패널의 카운트 뱃지 로딩이 3~4초 이상 소요. 대규모 데이터셋을 가진 팀(admin, nestle, performance, voith, tpc, graymont)의 사용자가 영향을 받음.
Root Cause Summary#
SearchesController#counts 액션이 10개 엔티티 타입(Workspace, Facility, Record, Report, Review, Capture, Bim, Pointcloud, Room, RealityCapture)에 대해 순차적으로 개별 Elasticsearch 검색을 수행한다. 각 검색은 Elasticsearch 쿼리 실행 후 total_entries만 반환하지만, 10개 쿼리가 직렬로 실행되어 각 쿼리의 지연이 누적된다. 대규모 팀에서는 Elasticsearch 쿼리 자체의 지연(permission 필터의 복잡한 terms 쿼리)과 DB 시간(permission JOIN)이 결합되어 총 응답 시간이 3~4초에 달한다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/searches_controller.rb:4—counts액션 시작 - 10개의 Repository가 순차적으로
total_entries(query_option)호출 - 각 Repository는
_search(query_option)실행 → Elasticsearch 검색 - Elasticsearch 결과의
total_entries만 반환
def counts
workspace_count = WorkspaceRepository.new(current_user: current_user, current_team: @current_team).total_entries(
Cupix::QueryOption::Workspace.new(get_query_option, params)
)
facility_count = FacilityRepository.new(current_user: current_user, current_team: @current_team).total_entries(
Cupix::QueryOption::Facility.new(get_query_option, params)
)
# ... 8개 더 (record, report, review, capture, bim, pointcloud, room, reality_capture)
render_json 200, {
workspace_count: workspace_count,
facility_count: facility_count,
# ...
}
end
total_entries 메서드는 _search를 호출하여 전체 Elasticsearch 쿼리를 실행한 뒤 카운트만 반환한다:
def total_entries(query_option = nil)
_search(query_option) if query_option.present?
return nil if self.response.nil?
self.response.total_entries
end
각 Repository의 _search는 복잡한 permission 필터를 Elasticsearch 쿼리에 추가한 후 전체 검색을 실행한다:
response = ::Facility.search(
self.query_option.serializable_hash
).paginate(
per_page: self.query_option.per_page,
page: self.query_option.page
)
set_response(response)
핵심 문제: total_entries만 필요하지만 .paginate()까지 포함한 전체 검색이 실행된다. 또한 10개 쿼리가 순차 실행되어 각 쿼리의 지연이 합산된다.
- Failure point: 특정 코드 실패는 없음. 구조적으로 순차 실행 + 불필요한 pagination이 지연의 원인.
Log Evidence#
Datadog APM 트레이스에서 확인한 지연 패턴:
service:cupixworks-api resource_name:"Api::V1::SearchesController#counts" env:production @duration:>500ms
Pattern A: DB-bound 고지연 (대규모 팀)
{
"timestamp": "2026-05-26T05:14:17Z",
"duration_ms": 4328,
"db_ms": 2460,
"team_domain": "admin",
"params": {"state": "holding"},
"host": "ip-10-1-19-190.us-west-2.compute.internal"
}
{
"timestamp": "2026-05-26T11:45:31Z",
"duration_ms": 3507,
"db_ms": 2369,
"team_domain": "performance",
"params": {"q": "new albany"}
}
Pattern B: Elasticsearch-bound 고지연 (voith 팀)
{
"timestamp": "2026-05-26T06:42:00Z",
"duration_ms": 3931,
"db_ms": 349,
"team_domain": "voith",
"params": {"q": "palm"}
}
DB 시간이 349ms에 불과하지만 총 응답 3931ms → 나머지 ~3500ms가 Elasticsearch 검색 시간. 10개 쿼리를 순차 실행하면 각 쿼리당 ~350ms의 ES 지연도 누적됨.
Pattern C: 빠른 응답 (소규모 팀)
{
"timestamp": "2026-05-26T06:49:33Z",
"duration_ms": 177,
"db_ms": 3,
"team_domain": "sinsw",
"params": {"q": "central mangrove"}
}
소규모 팀(sinsw)은 170~200ms로 정상 응답 — 데이터셋 크기가 핵심 변수임을 확인.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 10개 순차 Elasticsearch 쿼리의 누적 지연 | voith 팀: DB 349ms, total 3931ms → ES 쿼리 시간이 ~3500ms 누적. counts 액션이 10개 Repository를 순차 호출 (searches_controller.rb:4-35) | — | Confirmed |
| H2 | 대규모 팀의 DB permission JOIN 지연 | admin: DB 2460ms/total 4328ms, performance: DB 2369ms/total 3507ms. permission_joins에 5-8개 LEFT JOIN 포함 | 소규모 팀(sinsw)은 DB 3ms로 정상 | Confirmed |
| H3 | Elasticsearch 클러스터 장애 또는 과부하 | — | 동일 시간대 ES 에러 로그 0건. 소규모 팀은 정상 응답(177ms). ES circuit breaker 미발생 | Rejected |
| H4 | MySQL deadlock으로 인한 지연 | 05:44:02Z에 EditingsController#update deadlock 발생 | deadlock은 다른 컨트롤러. searches에는 SELECT만 수행. 시간대가 일치하지 않음 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/controllers/api/v1/searches_controller.rb:4-35— 10개의total_entries호출을 병렬 실행으로 전환. Ruby의Concurrent::Future또는Parallelgem을 사용하여 10개 Elasticsearch 쿼리를 동시에 실행하면 총 응답 시간이 가장 느린 단일 쿼리 시간으로 줄어든다.app/repositories/base_repository.rb:114-119—total_entries전용 경로를 추가하여 Elasticsearch의_countAPI를 사용. 현재.search().paginate()로 전체 검색 후 count만 추출하는 방식을Model.search(..., size: 0)으로 변경하여 document fetch를 생략.
단기 개선 (1주 이내)#
- Elasticsearch 쿼리에
size: 0과track_total_hits: true옵션을 적용하여 카운트 전용 쿼리로 최적화. 현재 paginate로 실제 문서까지 조회하는 것은 불필요. - 대규모 팀(admin, nestle, performance)에 대한 카운트 결과를 Redis에 30~60초 TTL로 캐싱. 동일 검색어에 대한 반복 요청 시 Elasticsearch 부하 감소.
장기 개선 (재발 방지)#
- 각 엔티티의 permission 필터를 Elasticsearch 인덱스에 사전 계산(denormalize)하여 복잡한
terms쿼리 제거. 현재readable_workspace_ids,directly_accessible_facility_ids등을 매 요청마다 조회하여 ES 쿼리에 주입하는 패턴이 대규모 팀에서 쿼리 복잡도를 증가시킨다. - Multi-search API(
_msearch)를 활용하여 10개 쿼리를 단일 HTTP 요청으로 배치 실행.
Monitoring#
- APM 모니터:
avg(trace.duration) by resource_namewhereresource_name:Api::V1::SearchesController#counts> 2000ms - Datadog 쿼리 예시:
service:cupixworks-api resource_name:"Api::V1::SearchesController#counts" @duration:>2000ms
- Elasticsearch slow query log 활성화하여 개별 ES 쿼리 지연 추적
- 팀별 카운트 응답 시간 P95 대시보드 추가
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard — 병렬화 변경은 기존 로직을 크게 수정하지 않으며, thread safety 검증이 필요하지만 각 Repository 인스턴스가 독립적이므로 안전한 병렬화가 가능하다.