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#
- 2026-05-27T02:48:09Z —
GET /api/v1/admin/teams요청 도착 (ap-southeast-2) - 2026-05-27T02:48:10Z — 응답 완료 (200 OK, 1013ms 소요)
- 2026-05-27T13:24:00Z — RCA 분석 완료
Error Log#
{
"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
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
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
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
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 목록에 대해:
includes(:customer_success_managers, :storage)— 2개 추가 SQL 쿼리 (groups + users JOIN, storages)left_joins(:user)— teams.user_id에 대한 LEFT JOIN- 수동 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
SalesTeamAttribute는 team.customer_success_managers를 UserSerializer로 직렬화하고, StorageAttribute는 model.storage를 접근한다. eager-loading이 되어있으므로 N+1은 방지되지만, 직렬화 자체의 CPU 비용이 추가된다.
- Failure point: 특정 단일 failure point 없음. 크로스 리전 네트워크 지연 + 복합 DB 쿼리의 합산.
Log Evidence#
검색 쿼리:
service:cupixworks-api "Admin::TeamsController#index"
Time range: 2026-05-27T01:00:00Z to 2026-05-27T04:00:00Z
해당 시간대 전후로 동일 엔드포인트가 빈번하게(20-30초 간격) 호출되고 있었음을 확인:
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:345—default_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 감지 중. 추가 조치 불필요.
- 반복 발생 시 확인할 쿼리:
service:cupixworks-api resource_name:"Api::V1::Admin::TeamsController#index" @duration:>1s env:production
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial
- 단발성 이벤트로, 반복적 패턴이 아님. Admin 패널 전용 엔드포인트로 사용자 영향 극히 제한적.