Api::V1::AnnotationLayersController#associate_form_designs (avg 1327ms, max 1763ms)
RCA: AnnotationLayersController#associate_form_designs Latency
Overview#
What Happened#
2026-05-27 04:33~05:10 UTC 사이에 cupixworks-api 서비스의 Api::V1::AnnotationLayersController#associate_form_designs 엔드포인트에서 평균 1327ms, 최대 1763ms(APM 기준)의 응답 지연이 7회 발생했다. 로그에서 확인된 실제 최대 duration은 8712ms(annotation_layer 5786)이며, DB 시간이 대부분을 차지한다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::AnnotationLayersController#associate_form_designs |
| top_frame | app/models/concerns/associatable/annotation_layer.rb:12-18 |
| env | production, ap-southeast-2 |
| deploy | production-ap-southeast-2-20260527t0503z0-650f3601-cupixworks |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| endeavourgroup (team_id: 180) | 7 | Annotation layer 일괄 form design 연결 작업 시 사용자 체감 지연 (1~8초) |
Timeline#
- 2026-05-27T04:33:58Z — 최초 slow trace 감지 (APM, >500ms threshold)
- 2026-05-27T05:07:43Z — 최악 케이스: annotation_layer 5786, duration 8712ms / db 4744ms
- 2026-05-27T05:10:15Z — 마지막 slow trace (annotation_layer 5788, duration 1101ms)
- 2026-05-27 — RCA 수행
Error Log#
{
"resource_name": "Api::V1::AnnotationLayersController#associate_form_designs",
"service": "cupixworks-api",
"occurrences": 7,
"avg_ms": 1327,
"max_ms": 1763,
"sample_trace_id": "3591269114273289778"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 7
- 최초 발생: 2026-05-27T04:33:58.451Z
- 최근 발생: 2026-05-27T05:10:15.239Z
Root Cause Summary#
한 사용자(Emily Holmes, team: endeavourgroup)가 약 20개의 annotation layer에 대해 순차적으로 associate_form_designs를 호출하는 일괄 작업을 수행했다. 이 엔드포인트의 코드 경로에는 두 가지 성능 문제가 존재한다: (1) find_by(associated_resourcable: form_designs)가 ActiveRecord 배열을 전달받을 때 개별 쿼리를 생성하며, (2) form_designs.each { create! } 루프에서 매 레코드마다 INSERT + uniqueness validation 쿼리 + after_commit :write_cache 콜백이 실행된다. associations 테이블에 uniqueness validation 대상인 (associatable_id, associatable_type, associated_resourcable_id, associated_resourcable_type) 복합 인덱스가 없어 validation 시 테이블 스캔이 발생하며, 이것이 DB 시간의 대부분을 차지한다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/concerns/form_design_associatable_controller.rb:10 - before_action
set_annotation_layer→repository_instance.show(params[:id])(annotation layer 로드) associate_form_designs→repository_instance.associate_form_designs(form_design_ids)- Repository concern:
app/repositories/concerns/form_design_associatable_repository.rb:4-10 - Model concern (failure point):
app/models/concerns/associatable/annotation_layer.rb:11-18 - 응답 렌더:
show→render_apiwithAnnotationLayerSerializer
1단계: Repository에서 FormDesign 조회
def associate_form_designs(form_design_ids)
raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied') unless self.model.updatable_by?(self.current_user)
form_designs = ::FormDesign.where(team: self.current_team).where(id: form_design_ids)
self.model.associate_form_designs(form_designs, current_user: self.current_user)
end
이 단계는 정상적이다. FormDesign.where(id: [...]) 는 단일 쿼리.
2단계: 중복 체크 (N+1 문제)
def associate_form_designs(form_designs, current_user: nil)
associations = self.associations.find_by(associated_resourcable: form_designs)
raise Cupix::Errors::InvalidState.new(code: 'STAT40000', reason: 'Annotation Layer already associated with form_design') unless associations.blank?
find_by에 ActiveRecord relation을 전달하면 첫 번째 매칭 레코드만 반환하지만, 내부적으로 polymorphic association에 대한 조건절 생성 시 비효율적 쿼리가 발생한다.
3단계: 개별 INSERT 루프 (핵심 병목)
form_designs.each do |form_design|
self.associations.create!(associated_resourcable: form_design, user: current_user)
end
end
각 create! 호출마다:
before_validation콜백으로user_id,team_id설정 (추가 쿼리 가능)- Uniqueness validation —
SELECT쿼리로(associatable_id, associatable_type, associated_resourcable_id, associated_resourcable_type)조합 확인 INSERT실행after_commit :write_cache(Cachable concern) — Redis에 캐시 기록 +association_cache_keys생성
4단계: Uniqueness validation에 복합 인덱스 부재
validates :associatable_id, uniqueness: {
scope: %i[associatable_type associated_resourcable_id associated_resourcable_type]
}
DB 스키마에는 (associatable_type, associatable_id) 인덱스만 있고, validation에 필요한 4-column 복합 인덱스가 없다:
t.index ["associatable_type", "associatable_id"], name: "index_associations_on_associatable_type_and_associatable_id"
t.index ["associated_resourcable_type", "associated_resourcable_id"], name: "index_associations_on_associated_resourcable"
t.index ["team_id"], name: "index_associations_on_team_id"
t.index ["user_id"], name: "index_associations_on_user_id"
기대 동작: uniqueness check가 인덱스를 타서 <1ms에 완료되어야 함. 실제 동작: 복합 인덱스 부재로 partial index scan 또는 table scan 발생, 테이블 크기에 비례하여 느려짐.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api "AnnotationLayersController" "associate_form_designs"
Time range: 2026-05-27T03:30:00Z to 2026-05-27T06:00:00Z
핵심 로그 항목 (raw 형태):
{
"timestamp": "2026-05-27T05:07:43.589Z",
"action": "associate_form_designs",
"controller": "Api::V1::AnnotationLayersController",
"duration": 8712.62,
"db": 4744.56,
"view": 0.09,
"serialization": { "duration": 0 },
"params": { "id": "5786" },
"user": { "id": 8185, "email": "emily.holmes@edg.com.au" },
"team": { "domain": "endeavourgroup", "id": 180 },
"http": { "status_code": 200, "method": "PUT" },
"host": { "name": "ip-10-1-18-50.ap-southeast-2.compute.internal" }
}
{
"timestamp": "2026-05-27T05:04:55.943Z",
"duration": 2442.79,
"db": 1858.91,
"params": { "id": "5784" },
"host": { "name": "ip-10-1-145-251.ap-southeast-2.compute.internal" }
}
{
"timestamp": "2026-05-27T05:09:01.760Z",
"duration": 1439.28,
"db": 837.13,
"params": { "id": "5787" }
}
패턴 분석:
- 모든 요청이 동일 사용자/세션: session_id
4d784f073142b4eb9fc3465849b99dd1c6874e33 - 순차적 annotation_layer ID (5769~5788): 일괄 연결 작업
- DB 시간이 전체 duration의 50-75% 차지
- view/serialization 시간은 0ms 수준 — 렌더링은 병목 아님
- 두 호스트에 분산 (ip-10-1-18-50, ip-10-1-145-251) — 로드밸런서 뒤에서 처리
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | N+1 쿼리 + 복합 인덱스 부재로 uniqueness validation이 느림 | db 시간이 duration의 50-75% 차지 (585~4744ms). associations 테이블에 4-column 복합 인덱스 없음 (schema.rb:596-599). 코드에서 form_designs.each { create! } 루프 확인 (annotation_layer.rb:16-18) |
— | Confirmed |
| H2 | AP-southeast-2 리전의 DB 인스턴스 자체가 느려서 발생한 일시적 문제 | annotation_layer 5786에서 8712ms/4744ms로 비정상적 spike 존재 | 다른 요청들도 consistently 500ms+ DB 시간 보임 — 일시적이 아닌 구조적 문제. 같은 시간대 다른 엔드포인트는 정상 | Rejected |
| H3 | 많은 수의 form_design을 한 번에 연결하여 루프 횟수가 많음 | 단일 세션에서 20개+ annotation layer를 순차 호출 | 각 요청의 params에 form_design_ids 개수 미확인 (로그에 미포함). 하지만 DB 시간 패턴으로 보아 다수의 form_design일 가능성 높음 |
Inconclusive |
Fix Recommendation#
즉시 조치 (Critical)#
db/migrate/—associations테이블에 복합 인덱스 추가:add_index :associations, [:associatable_type, :associatable_id, :associated_resourcable_type, :associated_resourcable_id], unique: true, name: 'index_associations_uniqueness'- 이 인덱스가 uniqueness validation의 SELECT 쿼리를 O(1) 수준으로 만듦
단기 개선 (1주 이내)#
app/models/concerns/associatable/annotation_layer.rb:16-18—each { create! }루프를insert_all또는import로 교체하여 batch INSERT 사용app/models/concerns/associatable/annotation_layer.rb:12—find_by를where(...).exists?로 변경하여 중복 체크를 단일 쿼리로 수행- Cachable
after_commit :write_cache가 batch insert에서도 작동하는지 확인 필요 (write_cache가 개별 호출되면 추가 최적화 대상)
장기 개선 (재발 방지)#
- Batch API 도입: 여러 annotation layer에 동일 form_design을 연결하는 경우 단일 API call로 처리 가능하도록 bulk endpoint 제공
Associatableconcern 전체에 대한 N+1 감사 —disassociate_form_designs도 동일 패턴 가능
Monitoring#
- APM 모니터 추가:
resource_name:"Api::V1::AnnotationLayersController#associate_form_designs" @duration:>1000ms - 인덱스 추가 후 DB 시간 추이 확인:
avg:trace.rails.request.duration{resource_name:api::v1::annotationlayerscontroller#associate_form_designs,env:production} by {region}
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard (인덱스 마이그레이션은 간단하나, 대형 테이블이면 online DDL 고려 필요)