Api::V1::ReviewsController#bulk_share (avg 8675ms, max 8675ms)
RCA: Api::V1::ReviewsController#bulk_share Latency (8675ms)
Overview#
What Happened#
2026-05-27 05:25 UTC에 cupixworks-api 서비스의 PUT /api/v1/reviews/share 엔드포인트에서 단일 요청이 8,675ms 소요되었다. 이 요청은 3개의 Review를 새 사용자에게 공유하는 과정에서, 신규 사용자를 AWS Cognito에 동기적으로 생성하고, notification recipe를 순차적으로 생성하며, permission cascade를 처리하느라 응답 시간이 크게 지연되었다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::ReviewsController#bulk_share |
| top_frame | app/controllers/concerns/share_controller.rb:62 |
| duration | 8675ms (DB: 432ms, app: ~8243ms) |
| env | production, us-west-2 |
| deploy | production-us-west-2-20260527t0524z0-650f3601-cupixworks |
Timeline#
- 05:25:04Z —
PUT /api/v1/reviews/share요청 수신 (3 reviews, 1 new email) - 05:25:07Z — 신규 사용자 Cognito 생성 완료 (~2.9s)
- 05:25:07Z — Segment event 전송, PubSub 이벤트 발행
- 05:25:10~11Z — FacilityPermission 생성 → PubSub
UserRecipeGenerator트리거 → notification recipe 3건 순차 생성 (~5.5s) - 05:25:13Z — Permission flush, event publish, 응답 반환 (HTTP 200)
Error Log#
{
"resource_name": "Api::V1::ReviewsController#bulk_share",
"service": "cupixworks-api",
"occurrences": 1,
"avg_ms": 8675,
"max_ms": 8675,
"sample_trace_id": "1189013322334403148"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-05-27T05:25:04.751Z
- 최근 발생: 2026-05-27T05:25:04.751Z
- 사용자 영향: 단일 사용자(user_id: 46182, nishiyama.satoshi@takenaka.co.jp)가 3개 Review를 공유하는 동안 ~8.7초 대기. 기능적 실패 없음 (HTTP 200).
Root Cause Summary#
bulk_share 액션이 신규 이메일로 공유할 때, HTTP 요청 내에서 (1) AWS Cognito 사용자 생성, (2) FacilityPermission 생성 시 PubSub를 통해 UserRecipeGenerator가 notification recipe 3건을 순차적 외부 HTTP 호출로 생성, (3) 복수 Review에 대한 permission cascade + Elasticsearch 동기 인덱싱이 모두 동기적으로 실행된다. 이 중 notification recipe 순차 생성(~5.5초)과 Cognito 사용자 생성(~2.9초)이 전체 8.7초 지연의 대부분을 차지한다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/concerns/share_controller.rb:62—bulk_share액션
def bulk_share
validate_project_permissions_feature!
check_share_requests!(params)
members = params[:items].map do |item|
@model = repository_instance.show(item[:model_id])
repository_instance.share(item.slice(*SHARE_REQUEST_PARAMS), item[:permission])
end.flatten.compact
render_api Renderable.new({
contents: members.uniq,
is_collection: true,
serializer: MemberSerializer
})
end
각 item에 대해 repository_instance.show(11-JOIN SQL)와 share를 반복 호출한다.
- Share 실행 흐름:
app/repositories/concerns/sharable_repository.rb:131
if params[:emails].present?
# ...
if invite_candidate_user_emails.present?
invited_users = InvitationOperation.invite_people(
{ user_emails: invite_candidate_user_emails, skip_permission_check: true },
self.current_user, @current_team, @model
)
end
end
(@users + @groups + already_joined_or_invited_users + invited_users).each do |user_or_group|
@model.add_permission! user_or_group, permission
shared_users_and_groups << user_or_group
end
새 이메일이 포함되면 InvitationOperation.invite_people이 동기적으로 사용자를 생성한다.
- Cognito 사용자 생성 (동기):
lib/cupix/aws/cognito/user.rb:10— User model의after_create콜백
def create_cognito_user
if Cupix::Aws::Cognito.user_exists?(self.email)
cognito_user = Cupix::Aws::Cognito.get_user_by_email(self.email)
self.update!(cognito_user_id: cognito_user.attributes.find { |attr| attr.name == 'sub' }.value)
else
Cupix::Logger.info("Creating user in Cognito: #{self.email}", ...)
cognito_user_creation_response = Cupix::Aws::Cognito.create_user!(self)
self.update!(cognito_user_id: cognito_user_creation_response.user.attributes.find { |attr| attr.name == 'sub' }.value)
end
end
AWS Cognito API 호출 2회 (user_exists? + create_user!)가 동기적으로 HTTP 요청 내에서 실행된다.
- Notification recipe 생성 (동기, 최대 병목): FacilityPermission 생성 → PubSub →
UserRecipeGenerator
def _create_recipes(opts = {})
%w[
record_preview_ready
record_processing_completed
facility_new_project
].each do |recipe_name|
Cupix::NotificationService.new(user: opts[:user]).create_user_recipe(recipe_name, team_id: opts[:team_id], facility_key: opts[:facility_key])
end
end
3개의 notification recipe를 each로 순차적 외부 HTTP 호출(DynamoDB 기반 notification-service)하여 ~5.5초 소요.
- Permission cascade (동기):
app/models/concerns/permissionable.rb:60
def add_permission!(user_or_group, permission)
_permission = permissions.find_or_initialize_by(accessor: user_or_group)
_permission.permission = permission
_permission.save!
Permissionable.flush_cached_permissions(user_or_group)
Permissionable.flush_cached_permissions_by_user(user_or_group, self)
_permission
end
save!가 ReviewPermission → before_create :create_facility_permission → before_create :create_workspace_permission → before_create :create_team_permission cascade를 트리거하고, 각 after_commit에서 Elasticsearch 동기 인덱싱이 실행된다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api @http.url_details.path:"/api/v1/reviews/share"
service:cupixworks-api status:info @class:Cognito
service:cupixworks-api status:info @class:Cupix::NotificationService
요청 완료 로그:
{
"message": "[200] PUT /api/v1/reviews/share (Api::V1::ReviewsController#bulk_share)",
"duration": 8671.15,
"db": 432.56,
"view": 0.14,
"user": "nishiyama.satoshi@takenaka.co.jp",
"user_id": 46182,
"team": "umedafm",
"request_id": "54b0029a-b6fd-40fa-804c-5fbee9a1ea53"
}
Cognito 사용자 생성 로그 (05:25:07.689Z):
Creating user in Cognito: h_kazuhiro_okedoi@eng.sanki.co.jp
User created in Cognito: h_kazuhiro_okedoi@eng.sanki.co.jp
Notification recipe 순차 생성 로그 (05:25:10~11Z):
recipe created: record_preview_ready (created_at: 05:25:10.964Z)
recipe created: record_processing_completed (created_at: 05:25:11.318Z)
recipe created: facility_new_project (created_at: 05:25:11.578Z)
subscription of h_kazuhiro_okedoi@eng.sanki.co.jp on Facility 5v5ar2 created
Permission flush 시 warn 로그:
[warn] ReviewPermission#_update_document: NotFound - attributes_in_database
시간 분석:
| Phase | 시간 | 소요 |
|---|---|---|
| 요청 시작 → Cognito 생성 완료 | 05:25:04.7 → 05:25:07.7 | ~2.9s |
| Cognito 완료 → Recipe 생성 시작 | 05:25:07.7 → 05:25:10.9 | ~3.2s (permission cascade + ES indexing) |
| Recipe 순차 생성 | 05:25:10.9 → 05:25:11.6 | ~0.7s (recipe HTTP calls) |
| Recipe → PubSub 완료 → 응답 | 05:25:11.6 → 05:25:13.7 | ~2.1s (flush + events) |
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 동기적 Cognito 사용자 생성 + notification recipe 순차 호출이 주요 병목 | Cognito 생성에 ~2.9s, recipe 생성이 PubSub 경유로 ~5.5s 소요 (로그 타임스탬프 확인). DB 시간은 432ms로 전체의 5%에 불과 | — | Confirmed |
| H2 | N+1 DB 쿼리 (review show 반복) 가 병목 | params[:items].map에서 각 item마다 11-JOIN show 쿼리 실행 (share_controller.rb:67) |
DB 총 시간이 432ms로 비교적 작음. 이번 케이스는 items가 3개라 DB 영향은 제한적 | Rejected (이번 케이스에서는 주요 원인 아님) |
| H3 | Elasticsearch 동기 인덱싱이 병목 | ReviewPermission, FacilityPermission 등의 after_commit에서 ES 동기 호출 실행 (searchable.rb:12). Permission cascade로 최대 12건의 ES 호출 |
로그에서 ES 관련 에러/지연 로그 미확인. 전체 ~8.2s 중 recipe 생성이 명확히 5.5s 차지 | Inconclusive (기여 요소일 수 있으나 주요 원인은 아님) |
Fix Recommendation#
즉시 조치 (Critical)#
lib/cupix/pub_sub/subscribers/user_recipe_generator.rb:34-41: notification recipe 생성을 비동기 worker로 이동. 현재 PubSub 이벤트 핸들러 내에서 동기적으로 3회 외부 HTTP 호출을 하고 있어, 이를 Sidekiq worker로 위임하면 ~5.5초를 즉시 절감할 수 있다.lib/cupix/aws/cognito/user.rb:10:after_create :create_cognito_user콜백을 비동기 worker로 변환. Cognito 사용자 생성은 즉시 필요하지 않으며 (초대 메일이 이미 비동기), worker에서 처리 가능하다.
단기 개선 (1주 이내)#
UserRecipeGenerator._create_recipes에서 3건의 recipe를 병렬(Concurrent 또는 Thread)로 생성하도록 변경. 순차 호출 대비 ~3배 속도 향상 가능.share_controller.rb:66-70:params[:items]에 대한 반복을 최적화 — 동일 사용자에 대한 permission cascade가 중복 실행되지 않도록 items를 그룹핑하여 사용자당 1회만 초대/권한 부여 처리.
장기 개선 (재발 방지)#
bulk_share전체 흐름에서 외부 서비스 호출(Cognito, NotificationService, EventService Kinesis, Elasticsearch)을 모두 비동기로 분리하는 아키텍처 리팩토링.- Permission cascade의
after_commitElasticsearch 인덱싱을 bulk indexing worker로 교체. bulk_share요청의 SLO를 설정하고 (예: P95 < 2000ms), 초과 시 알림.
Monitoring#
- 추가할 메트릭:
bulk_share요청 duration의 P95/P99 추적 - Datadog 쿼리 예시:
service:cupixworks-api resource_name:"Api::V1::ReviewsController#bulk_share" @duration:>5000000000
- NotificationService recipe 생성 latency 모니터링:
service:cupixworks-api @class:Cupix::NotificationService @function:create_user_recipe
- Cognito user creation latency 추적:
service:cupixworks-api @class:Cognito @function:create_cognito_user
Risk Assessment#
- Risk level: low (기능 실패 없이 느린 응답만 발생, 발생 빈도 낮음)
- 예상 복잡도: standard (비동기화 패턴은 기존 Sidekiq 인프라 활용 가능하나, PubSub subscriber 구조 변경 필요)