ES /docs

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

RCA: Api::V1::Admin::TeamsController#index Latency (1013ms)

Overview#

What Happened#

2026-05-27T02:48:09Z에 ap-southeast-2 리전의 cupixworks-api 서비스에서 GET /api/v1/admin/teams 요청이 1013ms 소요되었다. 500ms 임계값을 초과하는 단발성 latency 이벤트로, 요청 자체는 정상 응답(200)으로 완료되었다.

Quick Facts#

Field Value
resource_name Api::V1::Admin::TeamsController#index
top_frame app/controllers/api/v1/admin/teams_controller.rb:17
env production, ap-southeast-2
duration 1013ms (threshold: 500ms)

Timeline#

  1. 2026-05-27T02:48:09ZGET /api/v1/admin/teams 요청 도착 (ap-southeast-2)
  2. 2026-05-27T02:48:10Z — 응답 완료 (200 OK, 1013ms 소요)
  3. 2026-05-27T13:24:00Z — RCA 분석 완료

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::Admin::TeamsController#index",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 1013,
  "max_ms": 1013,
  "sample_trace_id": "3334212471377718378"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-05-27T02:48:09.184Z
  • 최근 발생: 2026-05-27T02:48:09.184Z

단일 발생으로, admin 패널 사용자 1명에게 약 1초 지연이 발생했다. 데이터 유실이나 기능 장애는 없다.

Root Cause Summary#

Admin::TeamsController#index의 요청 처리 경로가 Elasticsearch 쿼리 + PostgreSQL re-fetch(다중 LEFT JOIN 포함) + association eager-loading + 직렬화를 순차적으로 수행하는 구조로, ap-southeast-2 리전에서 Elasticsearch(타 리전) 왕복 지연과 default_joins의 3개 LEFT JOIN + includes(:customer_success_managers, :storage) 조합이 합산되어 1013ms에 도달했다. 단발성 이벤트로, 크로스 리전 네트워크 지연이 주된 트리거이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/admin/teams_controller.rb:17
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
  • Step 1 — Elasticsearch query: app/repositories/admin/team_repository.rb:39-109
app/repositories/admin/team_repository.rb:100-108ruby
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

Elasticsearch에 팀 검색 쿼리를 전송하고, must_not (admin 팀 제외) 등의 필터를 적용한다. default_per_page는 30이므로 최대 30개 팀 ID를 반환.

  • Step 2 — PostgreSQL re-fetch with JOINs: app/repositories/base_repository.rb:70-112
app/repositories/base_repository.rb:80-81ruby
contents = self.class.permission_joins(self.class.default_joins(self.response.records), self.current_user, skip_join: _skip_join?)
  • Step 3 — default_joins 수행: app/repositories/admin/team_repository.rb:345-365
app/repositories/admin/team_repository.rb:345-365ruby
def default_joins(record)
  record.includes(:customer_success_managers, :storage).left_joins(:user).joins('
    LEFT JOIN users AS primary_csm_users ON primary_csm_users.id = teams.primary_csm_id
    LEFT JOIN users AS secondary_csm_users ON secondary_csm_users.id = teams.secondary_csm_id
    LEFT JOIN users AS account_manager_users ON account_manager_users.id = teams.account_manager_id
  ').select('
    teams.*,
    users.email AS user_email,
    users.firstname AS user_firstname,
    users.lastname AS user_lastname,
    primary_csm_users.email AS primary_csm_email,
    primary_csm_users.firstname AS primary_csm_firstname,
    primary_csm_users.lastname AS primary_csm_lastname,
    secondary_csm_users.email AS secondary_csm_email,
    secondary_csm_users.firstname AS secondary_csm_firstname,
    secondary_csm_users.lastname AS secondary_csm_lastname,
    account_manager_email AS account_manager_email,
    account_manager_users.firstname AS account_manager_firstname,
    account_manager_users.lastname AS account_manager_lastname
  ')
end

이 메서드는 Elasticsearch가 반환한 team ID 목록에 대해:

  1. includes(:customer_success_managers, :storage) — 2개 추가 SQL 쿼리 (groups + users JOIN, storages)
  2. left_joins(:user) — teams.user_id에 대한 LEFT JOIN
  3. 수동 JOIN 3개 — primary_csm, secondary_csm, account_manager 각각 users 테이블 LEFT JOIN

총 1개 메인 쿼리(5 LEFT JOIN) + 2개 eager-load 쿼리 = 최소 3개 DB 쿼리가 순차 실행된다.

  • Step 4 — Serialization: app/serializers/admin/team_serializer.rb

SalesTeamAttributeteam.customer_success_managersUserSerializer로 직렬화하고, StorageAttributemodel.storage를 접근한다. eager-loading이 되어있으므로 N+1은 방지되지만, 직렬화 자체의 CPU 비용이 추가된다.

  • Failure point: 특정 단일 failure point 없음. 크로스 리전 네트워크 지연 + 복합 DB 쿼리의 합산.

Log Evidence#

검색 쿼리:

text
service:cupixworks-api "Admin::TeamsController#index"
Time range: 2026-05-27T01:00:00Z to 2026-05-27T04:00:00Z

해당 시간대 전후로 동일 엔드포인트가 빈번하게(20-30초 간격) 호출되고 있었음을 확인:

text
2026-05-27 12:59:20 KST - [200] GET /api/v1/admin/teams (Api::V1::Admin::TeamsController#index)
2026-05-27 12:58:46 KST - [200] GET /api/v1/admin/teams (Api::V1::Admin::TeamsController#index)
2026-05-27 12:58:22 KST - [200] GET /api/v1/admin/teams (Api::V1::Admin::TeamsController#index)
2026-05-27 12:58:09 KST - [200] GET /api/v1/admin/teams (Api::V1::Admin::TeamsController#index)
2026-05-27 12:58:01 KST - [200] GET /api/v1/admin/teams (Api::V1::Admin::TeamsController#index)
  • Elasticsearch 에러 없음 (service:cupixworks-api status:error "Elasticsearch" → 0건)
  • 단발성 latency spike로 반복적인 패턴은 아님 (occurrence_count: 1)
  • 이 클러스터는 APM trace 기반 latency 감지로, application-level 에러 로그는 없음

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 크로스 리전(ap-southeast-2 → Elasticsearch) 네트워크 지연 + 복합 DB 쿼리 합산 단발성 1회, ap-southeast-2 리전, Elasticsearch+PostgreSQL 순차 접근 구조, 에러 없이 정상 완료 Confirmed
H2 Elasticsearch circuit breaker 또는 과부하 base_repository.rb:88-93에 circuit breaker 핸들링 존재 에러 로그 없음, 정상 200 응답, Elasticsearch error 0건 Rejected
H3 N+1 쿼리 문제 customer_success_managers 접근이 직렬화에 존재 default_joins에서 includes(:customer_success_managers, :storage) eager-loading으로 방지됨 Rejected
H4 대량 데이터 반환 (per_page 초과) 이론적으로 가능 default_per_page는 30, 최대 300 제한 있음. 단일 요청이므로 기본값 30 사용 가능성 높음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 조치 불필요. 단발성 1회 이벤트(1013ms)로, 서비스 장애나 데이터 유실 없음. 500ms 임계값 대비 약간의 초과이며, 크로스 리전 특성상 허용 가능한 범위.

단기 개선 (1주 이내)#

  • app/repositories/admin/team_repository.rb:345default_joins에서 수동 LEFT JOIN 3개를 단일 쿼리로 수행하는 것은 적절하나, includes(:customer_success_managers)가 별도 쿼리를 발생시킨다. customer_success_managers 데이터를 LEFT JOIN으로 통합하거나, response에 불필요한 경우 직렬화에서 제외하면 쿼리 수를 줄일 수 있다.
  • Elasticsearch 쿼리와 PostgreSQL 쿼리 사이에 캐싱 레이어를 고려할 수 있으나, admin 엔드포인트 특성상 데이터 실시간성이 중요하므로 우선순위 낮음.

장기 개선 (재발 방지)#

  • ap-southeast-2 리전에 Elasticsearch replica를 배치하면 크로스 리전 왕복 지연을 제거할 수 있다.
  • Admin API 전용 read-replica DB를 AU 리전에 두는 것을 검토할 수 있으나, 비용 대비 효과 분석 필요.
  • APM latency 임계값을 리전별로 설정하여 크로스 리전 요청의 불필요한 알림을 줄이는 것을 고려.

Monitoring#

  • 현재 APM trace로 latency 감지 중. 추가 조치 불필요.
  • 반복 발생 시 확인할 쿼리:
text
service:cupixworks-api resource_name:"Api::V1::Admin::TeamsController#index" @duration:>1s env:production

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 단발성 이벤트로, 반복적 패턴이 아님. Admin 패널 전용 엔드포인트로 사용자 영향 극히 제한적.