ES /docs

Api::V1::Admin::TeamsController#create (avg 10471ms, max 10471ms)

RCA: Api::V1::Admin::TeamsController#create Latency (10.5s)

Overview#

What Happened#

2026-05-29 15:19 KST, ap-southeast-2 리전에서 POST /api/v1/admin/teams 요청이 10,471ms(약 10.5초) 소요되었다. 요청 자체는 HTTP 200으로 성공했으나, 단일 request 내에서 외부 서비스 호출(Notification Lambda, Cognito), DB 작업, 캐시 무효화, 이메일 발송 등이 모두 동기적으로 실행되어 극심한 latency가 발생했다.

Quick Facts#

Field Value
resource_name Api::V1::Admin::TeamsController#create
duration 10,471ms (DB: 671ms, non-DB: 9,800ms)
top_frame app/factories/admin/team_factory.rb:28
env production, ap-southeast-2
request_id 6b7991da-c0cd-4533-ac8b-7b4fce4e270d
user Jonathan Tulloch (id: 1613, jonathan.tulloch@cupix.com)
user_agent Prism/CS/1.1.6

Timeline#

  1. 2026-05-29 15:19:46 KST — 요청 수신 시작
  2. 2026-05-29 15:19:48 KST — Team 188 생성, trial_state none -> active, 5개 default 그룹 생성
  3. 2026-05-29 15:19:48~15:19:52 KST — 4명의 사용자에 대해 notification recipe 생성 (외부 Lambda 순차 호출 x 12회)
  4. 2026-05-29 15:19:52 KST — Cognito 사용자 조회/생성
  5. 2026-05-29 15:19:54 KST — FormDesign/FormField skeleton 복사, Facility 생성, 이메일 발송
  6. 2026-05-29 15:19:54~15:19:56 KST — Default facility 구조 생성 (Level, Sketch, Review 등)
  7. 2026-05-29 15:19:58 KST — Team state initializing -> active, 응답 반환 (HTTP 200)

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::Admin::TeamsController#create",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 10471,
  "max_ms": 10471,
  "sample_trace_id": "3108698813328453581"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-05-29 15:19 KST
  • 최근 발생: 2026-05-29 15:19 KST
  • 사용자 영향: Admin이 팀을 생성할 때 10초 이상 대기해야 함. 기능적 오류는 아니나 UX가 크게 저하됨. ap-southeast-2 리전 특성상 외부 서비스 호출 latency가 추가로 가중됨.

Root Cause Summary#

Admin::TeamFactory#create!가 단일 HTTP request 내에서 모든 팀 초기화 작업을 동기적으로 수행하는 것이 근본 원인이다. 특히 notification recipe 생성 시 외부 Lambda API (POST /api/recipes/v2)를 사용자 수 x 3회 순차 호출하는 로직이 전체 소요 시간의 약 60%를 차지한다. DB 작업은 671ms에 불과하나, 외부 HTTP 호출(Lambda recipe API + Cognito), permission cache flush, 이메일 발송 등 non-DB I/O가 9,800ms를 소비했다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/admin/teams_controller.rb:11-14
app/controllers/api/v1/admin/teams_controller.rb:11-14ruby
def create
  @model = factory_instance.create!(params.permit!)

  show
end
  • Factory orchestration: app/factories/admin/team_factory.rb:28-39
app/factories/admin/team_factory.rb:28-39ruby
def create!(params = {})
  check_params!(params)

  team = create_team!(params)        # Step 1: Team + Groups + Permissions
  user = create_user!(params)        # Step 2: User + Cognito + Owner 설정
  team.create_default_form_designs   # Step 3: FormDesign 템플릿 복사
  create_workspace_and_facility(params) # Step 4: Workspace + Facility + 하위 구조
  team.active_state!                 # Step 5: State transition

  IspringCreateDepartmentWorker.perform_async(team.id)  # 유일한 비동기 작업

  team
end

모든 단계가 동기적으로 실행되며, IspringCreateDepartmentWorker만 유일하게 비동기(Sidekiq)로 처리된다.

  • Notification recipe 생성 (주요 병목): PubSub subscriber가 permission 생성 이벤트를 수신하면 동기적으로 Lambda API를 호출한다.
lib/cupix/pub_sub/subscribers/user_recipe_generator.rb:34-41ruby
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
lib/cupix/notification_service.rb:11-22ruby
def create_user_recipe(recipe_name, team_id: nil, facility_key: nil)
  return if @service_url.nil?

  params = email_recipe_params(recipe_name, team_id, facility_key)
  response = Cupix::HttpClient.post(
    "#{@service_url}/api/recipes/v2",
    params.to_json,
    {
      content_type: :json,
      'x-cupix-auth': @user.api_token
    }
  )
  # ...
end

사용자 4명 x recipe 3개 = 12회의 순차 HTTP POST 호출이 발생. ap-southeast-2에서 Lambda API까지의 network latency가 각 호출마다 추가되어, 이 단계에서만 약 6초가 소요된 것으로 추정된다.

  • FormDesign skeleton 복사:
app/models/concerns/skeleton/team.rb:8-32ruby
def create_default_form_designs
  return if self.domain == TEMPLATE_TEAM_DOMAIN

  already_copied_form_ids = self.reload.form_designs.untrashed.select(&:cupix_template?).pluck(:sys).map { |form_design| form_design[:cupix_template_id] }
  template_form_designs = ::FormDesign.eager_load(:team).where(teams: { domain: TEMPLATE_TEAM_DOMAIN }).where.not(form_designs: { id: already_copied_form_ids })

  template_form_designs.map do |form_design|
    copied_form_design = form_design.hardcopy({
      team_id: self.id,
      user_id: self.user_id,
      sys: { cupix_template_id: form_design.id }
    })

    form_design.form_fields.map do |form_field|
      copied_form_field = form_field.hardcopy({
        team_id: self.id,
        user_id: self.user_id,
        form_design_id: copied_form_design.id,
        sys: { cupix_template_id: form_field.id }
      })
    end
  end
end

템플릿 FormDesign이 N개이고 각각 M개의 FormField를 가지면, N + N*M 회의 DB insert가 발생한다. hardcopy는 개별 save! 호출로 추정되며, bulk insert를 사용하지 않는다.

  • Failure point: 실패는 없으나, 전체 flow가 단일 request thread에서 동기 실행되어 latency가 누적됨.

Log Evidence#

Datadog에서 확인된 request 로그:

text
service:cupixworks-api @http.method:POST @http.url:/api/v1/admin/teams env:production region:ap-southeast-2
json
{
  "controller": "Api::V1::Admin::TeamsController#create",
  "status": 200,
  "duration": "10469.51ms",
  "db": "670.71ms",
  "view": "0.07ms",
  "host": "ip-10-1-83-190.ap-southeast-2.compute.internal",
  "request_id": "6b7991da-c0cd-4533-ac8b-7b4fce4e270d",
  "user_email": "jonathan.tulloch@cupix.com",
  "user_agent": "Prism/CS/1.1.6"
}

동일 trace ID로 확인된 Lambda 호출 (5건, 06:19:46~06:19:54 UTC):

text
trace_id:3108698813328453581 service:cupixworks-api

trace에서 POST /api/recipes/v2 호출이 5건 순차적으로 확인되며, 각 호출 간 약 1~2초의 간격이 존재한다.

Warn-level 로그 2건 (Elasticsearch 인덱싱 시점 불일치):

text
[WARN] NotFound - attributes_in_database | class: Team | function: _update_document
[WARN] NotFound - attributes_in_database | class: Workspace | function: _update_document

신규 생성된 Team 188, Workspace 499가 아직 ES에 인덱싱되기 전에 _update_document가 호출되어 발생한 경고. 기능 오류는 아님.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Notification recipe Lambda 순차 호출이 주요 병목 trace에서 5건의 Lambda 호출이 06:19:46~06:19:54 (8초간) 확인됨. user_recipe_generator.rb:34-41에서 순차 반복 Confirmed
H2 DB 쿼리 자체가 느린 것 DB time은 671ms로 전체의 6.4%에 불과. non-DB I/O가 9,800ms Rejected
H3 FormDesign skeleton 복사가 N+1로 느린 것 skeleton/team.rb:16-31에서 template별 개별 hardcopy 호출 이번 케이스에서 FormDesign 복사 구간은 06:19:54 시점에 완료되며, 전체 12초 중 일부만 차지 Partially confirmed (부차적 요인)
H4 Cognito API 호출 latency 로그에서 Cognito user lookup/creation 확인 전체 flow 중 1~2초 수준으로 주요 병목은 아님 Partially confirmed (부차적 요인)
H5 Permission cache flush cascade가 느린 것 5개 그룹 생성 시 각각 cache flush 발생 DB time에 이미 포함됨. non-DB 시간의 주요인은 외부 HTTP 호출 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/pub_sub/subscribers/user_recipe_generator.rb:34-41: notification recipe 생성을 비동기 worker로 전환. _create_recipes 내 HTTP 호출을 Sidekiq worker에서 수행하도록 변경.
  • 이미 IspringCreateDepartmentWorker가 비동기 패턴을 사용하고 있으므로, 동일하게 NotificationRecipeWorker.perform_async(user_id, team_id, facility_key) 형태로 변경.

단기 개선 (1주 이내)#

  • app/models/concerns/skeleton/team.rb:16-31: create_default_form_designs에서 bulk insert 패턴 적용. 개별 hardcopy 대신 insert_all/import 사용으로 DB round-trip 감소.
  • app/factories/admin/team_factory.rb:28-39: create_workspace_and_facility 단계에서 발생하는 FacilityMailer 발송을 deliver_later로 변경하여 비동기화.

장기 개선 (재발 방지)#

  • Team 생성 flow를 saga 패턴으로 재설계: TeamFactory#create!는 Team record 생성 + state를 initializing으로 설정만 수행하고, 나머지 초기화(groups, forms, facility, notifications)는 단계별 background worker chain으로 처리. 완료 시 state를 active로 전환.
  • PubSub subscriber의 동기 HTTP 호출을 금지하는 아키텍처 가이드라인 수립. 외부 서비스 호출은 반드시 worker를 경유하도록 제한.

Monitoring#

  • APM에서 TeamsController#create duration 모니터 추가:
text
avg(trace.rack.request.duration){resource_name:api::v1::admin::teamscontroller#create,env:production} > 5000
  • Notification recipe Lambda 호출 latency 추적:
text
service:cupixworks-api @http.url:*/api/recipes/v2 @http.method:POST | stats avg(@duration) by @http.status_code
  • Team 생성 단계별 소요 시간을 custom metric으로 계측하여 병목 구간 가시화.

Risk Assessment#

  • Risk level: low (기능적 오류 없음, UX latency 이슈)
  • 예상 복잡도: standard (recipe 생성 비동기화는 기존 worker 패턴 활용 가능)