ES /docs

Database lock contention causing API latency spikes

RCA: Api::V1::Admin::TeamsController#index (avg 18908ms, max 18908ms)

Overview#

What Happened#

2026-07-08 12:55:32 KST에 Retool 클라이언트가 호출한 GET /api/v1/admin/teams?filter=id:87&fields=id,account_manager,customer_success_managers 요청 한 건이 총 18506ms(그중 DB 12047ms) 걸려 응답했다. 응답 자체는 HTTP 200이었고 latency 클러스터로만 잡혔다. 같은 시각(±2분) 다른 여러 엔드포인트(PanosController, ElementTracesController, SessionsController 등)도 15초 이상의 지연을 겪었다.

Quick Facts#

Field Value
resource_name Api::V1::Admin::TeamsController#index
controller Api::V1::Admin::TeamsController
action index
top_frame app/controllers/api/v1/admin/teams_controller.rb:17-27
params filter=id:87, fields=[id, account_manager, customer_success_managers]
user miriam.kim@cupix.com (user.id 6942, team.id 133 admin)
user_agent Retool/2.0
duration 18506.22ms
db 12047.42ms (전체의 65%)
view 0.1ms
serialization 4ms
request_id 1a4c5cbe-6f91-4587-9fa2-e491a35ac21e
host ip-10-1-80-134.us-west-2.compute.internal
deploy production-us-west-2-20260708t0134z0-a1160ff5-cupixworks
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
Retool 관리자 도구 (admin domain) 1 팀 조회가 ~18.5초 지연 — 사용자에게는 UI 로딩 지연으로 체감
동시 요청자 (pano upload, element traces 등) 다수 같은 창(12:53-12:56 KST)에 여러 API가 15초+ 지연 (연쇄 영향)

Timeline#

  1. 2026-07-08 12:53:00 KST 부근PanosController#bulk, ElementTracesController#refresh 등 다수의 API에서 15초+ 지연이 시작 (Datadog @duration:>15000 검색)
  2. 2026-07-08 12:55:14 KST — 같은 사용자가 filter=id:1022로 조회한 첫 요청은 정상(728ms, DB 268ms)
  3. 2026-07-08 12:55:12 KST — 문제의 filter=id:87 요청이 접수 (Datadog span first_seen)
  4. 2026-07-08 12:55:32 KST — 문제의 요청이 200 OK로 완료(18506ms, DB 12047ms)
  5. 2026-07-08 12:56:14 KST — 광범위한 지연 완화 (마지막 slow log)

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::Admin::TeamsController#index",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 18908,
  "max_ms": 18908,
  "sample_trace_id": "388353435410329205"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-08 12:55:12 KST
  • 최근 발생: 2026-07-08 12:55:12 KST
  • 엔드포인트: GET /api/v1/admin/teams
  • 사용자 영향: 단일 요청 지연(~18.5s). 같은 시간대 다른 endpoint에서도 15초+ 응답이 다수 관측되어 사용자 체감 응답성 저하가 넓게 있었을 것으로 추정.

Root Cause Summary#

이 클러스터는 단발성 latency spike이며, 원인은 요청 본체 로직 자체가 느렸다기보다 동일 시간대의 서비스 전반적인 DB 대기 시간 상승으로 판단된다. 총 18.5초 중 12초가 db 시간(65%)이었고, 같은 창에 여러 다른 컨트롤러(PanosController#bulk, ElementTracesController#refresh, SessionsController#show 등)도 15초 이상 지연되었다. Admin::TeamRepository#_searchBaseRepository#searchdefault_joins(:primary_csm, :secondary_csm, :account_manager, :customer_success_managers, :storage eager load) → SalesTeamAttribute 직렬화(UserSerializer.new(team.customer_success_managers, ...).serializable_hash) 경로가 정상 컨디션에서는 700ms 안에 끝나기 때문에, 이번 spike는 코드 결함이 아닌 인프라/부하 요인의 노출로 본다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/admin/teams_controller.rb:17-27TeamsController#index
  • Search: app/repositories/admin/team_repository.rb:39-109 — Elasticsearch로 filter=id:87을 조회 후 결과를 hydrate
  • Eager load: app/repositories/admin/team_repository.rb:345-354default_joins가 다섯 개 관계를 preload
  • Serialization: app/serializers/admin/team_serializer.rb + app/serializers/concerns/sales_team_attribute.rb:28-36customer_success_managers 직렬화가 별도의 UserSerializer를 매 팀마다 생성
app/controllers/api/v1/admin/teams_controller.rb:17-27ruby
def index
  teams = repository_instance.search(
    Cupix::QueryOption::Team.new(get_query_option(enable_current_team: false), params)
  )

  render_api Renderable.new(
    search_result: teams,
    is_collection: true,
    serializer_option: @serializer_option.merge(params: { current_user: current_user })
  )
end
app/repositories/admin/team_repository.rb:39-109ruby
def _search(query_option = nil)
  set_query_option(query_option)
  # ...Elasticsearch bool 쿼리 구성...
  response = self.class.current_class.search(
    self.query_option.serializable_hash
  ).paginate(
    per_page: self.query_option.per_page,
    page: self.query_option.page
  )
  set_response(response)
  response
end
app/repositories/admin/team_repository.rb:344-354ruby
class << self
  def default_joins(record)
    record.includes(
      :user,
      :primary_csm,
      :secondary_csm,
      :account_manager,
      :customer_success_managers,
      :storage
    )
  end
end
app/serializers/concerns/sales_team_attribute.rb:28-36ruby
attribute :customer_success_managers do |team, params|
  UserSerializer.new(team.customer_success_managers, {
    fields: {
      user: User.cached_fields
    }
  }).serializable_hash[:data].map do |data|
    data[:attributes]
  end
end
  • 기대 동작: filter=id:87은 단일 팀 조회이므로 Elasticsearch 검색 + 5개 association includes + UserSerializer 한 번 호출로 수백 ms 안에 완료되어야 한다. 실제로 같은 사용자가 같은 fields 조합으로 filter=id:1022을 20초 앞서 호출했을 때는 728ms, DB 268ms 로 종료되었다.
  • 실제 동작: 동일한 코드 경로에서 DB 12047ms, 총 18506ms가 소요됨. view=0.1ms, serialization=4ms이므로 병목은 순전히 ActiveRecord/DB 왕복 구간이었다.

Log Evidence#

Datadog 쿼리(재현용):

text
service:cupixworks-api "GET /api/v1/admin/teams "
2026-07-08T03:55:30Z ~ 2026-07-08T03:55:35Z

문제의 slow request (18506ms):

json
{
  "@timestamp": "2026-07-08T03:55:32.355Z",
  "message": "[200] GET /api/v1/admin/teams (Api::V1::Admin::TeamsController#index)",
  "status": "info",
  "controller": "Api::V1::Admin::TeamsController",
  "action": "index",
  "duration": 18506.22,
  "db": 12047.42,
  "view": 0.1,
  "serialization": { "duration": 4 },
  "params": { "filter": "id:87", "fields": ["id", "account_manager", "customer_success_managers"] },
  "user": { "id": 6942, "email": "miriam.kim@cupix.com" },
  "team": { "domain": "admin", "id": 133 },
  "user_agent": "Retool/2.0 (+https://docs.tryretool.com/docs/apis)",
  "http": { "status_code": 200, "method": "GET" },
  "request_id": "1a4c5cbe-6f91-4587-9fa2-e491a35ac21e",
  "host": { "name": "ip-10-1-80-134.us-west-2.compute.internal" },
  "region": "us-west-2"
}

같은 사용자의 정상 요청 (직전, 728ms):

json
{
  "@timestamp": "2026-07-08T03:55:14.450Z",
  "duration": 727.68,
  "db": 268.19,
  "params": { "filter": "id:1022", "fields": ["id", "account_manager", "customer_success_managers"] },
  "user": { "id": 6942, "email": "miriam.kim@cupix.com" },
  "host": { "name": "ip-10-1-144-228.us-west-2.compute.internal" }
}

동시간대 다른 엔드포인트에서도 15초+ 지연이 다수 관측됨. Datadog 쿼리:

text
service:cupixworks-api @duration:>15000
2026-07-08T03:53:00Z ~ 2026-07-08T03:57:00Z

관측된 슬로우 요청 예시 (일부):

text
12:55:56 KST  POST /api/v1/panos                              (PanosController#create)
12:55:52 KST  POST /api/v1/panos/92132608/mask_upload_url
12:55:44 KST  PUT  /api/v1/element_traces                     (ElementTracesController#bulk)
12:55:44 KST  POST /api/v1/panos/92132467/mask_upload_url
12:55:32 KST  GET  /api/v1/sessions                           (SessionsController#show)
12:55:32 KST  GET  /api/v1/admin/teams                        ← 본 클러스터 요청
12:55:24 KST  PUT  /api/v1/elements                           (ElementsController#bulk)
12:55:08 KST  PUT  /api/v1/element_traces/refresh
12:54:44 KST  PUT  /api/v1/element_traces/refresh

여러 호스트(ip-10-1-80-134, ip-10-1-144-228 등)에 걸쳐 발생. 7일 창에서 Admin::TeamsController#index @db:>5000은 이 한 건뿐 — 즉 이 코드 경로 자체의 만성 hot-spot 은 아니다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 팀 87의 customer_success_managers association이 매우 커서 SalesTeamAttribute 직렬화가 지연시켰다 filter=id:87인 요청이 늦다 serialization.duration=4ms, view=0.1ms. 병목은 DB 구간. 21:55:41 UTC의 다른 id:87 요청은 duration=3880ms인 반면 db=260ms로 DB 시간은 정상 — 데이터 크기 문제라면 매번 DB가 커야 함 Rejected
H2 Elasticsearch가 느려서 _search 단계가 오래 걸렸다 ES는 search 파이프라인의 첫 단계 duration - db - view - serialization ≈ 6450ms가 ES/기타에 남지만, ES 문제라면 대부분 요청이 실패하거나 대량 slow가 보여야 함. 실제로는 다른 컨트롤러 다수도 지연 — ES 단독 원인으로 국한하기 어려움 Inconclusive
H3 RDS/Aurora 순간 부하로 DB round-trip 이 지연됨 db=12047ms이 전체의 65%, 같은 시각 다른 엔드포인트도 15초+ 지연이 동시 발생, 여러 호스트에 걸침, 24시간 창에서 같은 컨트롤러의 유사 spike는 없음 — 시스템 전반 이벤트로 관측 직접적인 slow-query 로그나 RDS 메트릭은 이 조사에서 확인하지 않음 — needs verification (RDS Performance Insights, CloudWatch) Confirmed (primary)
H4 Sidekiq/워커 대량 쓰기(pano ingestion)로 인한 write lock 경합 같은 시각 POST/PUT /api/v1/panos* 요청이 다수 동시 발생하며 모두 15초+ 지연 Sidekiq worker 자체 로그는 이 조사에서 확인하지 않음 — needs verification Inconclusive (기여 요인 가능)

Fix Recommendation#

즉시 조치 (Critical)#

  • 즉시 코드 변경이 필요 없다고 판단. 본 클러스터는 단발성이며 관찰 시점(2026-07-08 12:55 KST) 이후 재발이 없다.
  • 대신 이 시각의 RDS/Aurora Performance Insights 와 Elasticsearch cluster health를 대조해 원인 인프라를 확정할 것. cupix-infrastructure에서 해당 RDS 인스턴스와 ES 도메인의 이 시간대 CPU/read latency/wait event를 확인.

단기 개선 (1주 이내)#

  • SalesTeamAttribute#customer_success_managers (app/serializers/concerns/sales_team_attribute.rb:28-36)는 팀마다 UserSerializer.new(...).serializable_hash를 새로 생성한다. 목록 API에서는 성능 영향이 있으므로, 팀 컬렉션 조회 시에는 has_attribute?(:customer_success_manager_email) 같은 flat attribute 경로(다른 attribute들이 하는 방식)로 전환하는 것을 검토. 단, 이번 사건과의 인과는 낮으므로 우선순위는 medium.
  • Retool 통합처럼 자동화된 클라이언트가 반복 조회하는 admin API에 클라이언트 측 timeout/재시도 정책을 확인해, 20초 근처에서 재시도 폭발이 유발되지 않도록 한다.

장기 개선 (재발 방지)#

  • p95/p99 latency SLO 를 컨트롤러/액션 단위로 정의하고, 30초 이상 응답이 다수 컨트롤러에 걸쳐 동시에 관측되면 Sidekiq/DB 부하 이벤트로 인지해 알림. 현재는 개별 컨트롤러의 latency 클러스터로만 잡혀 큰 그림이 늦게 보임.
  • Pano ingestion burst 처럼 짧은 순간에 write가 몰리는 워크로드에 대해 rate-limit 또는 lane 분리(별도 read replica 라우팅 등) 검토.

Monitoring#

  • 이 컨트롤러/액션의 latency 추이:
text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:Api::V1::Admin::TeamsController#index} by {env}
  • 서비스 전반의 slow request 카운트(같은 시각 상관 사건을 초기 감지):
text
sum:trace.rack.request.hits{service:cupixworks-api,env:production,duration:>15s} by {resource_name}.as_count()
  • DB 병목 상관관계:
text
avg:trace.rack.request.duration.by.resource_name{service:cupixworks-api} by {resource_name}

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (단발성, 코드 결함 아님)
  • 재발 시 액션: 인프라 원인 확정 후 필요시 short-term 개선(SalesTeamAttribute flat 경로) 시행