Api::V1::FloorplansController#create (avg 12523ms, max 12523ms)
RCA: Api::V1::FloorplansController#create (avg 12523ms, max 12523ms)
Overview#
What Happened#
2026-07-13 14:12 KST 에 cupixworks-api 프로덕션(us-west-2, tenant cupix) 에서 POST /api/v1/floorplans 요청 한 건이 12.5s 동안 실행되어 latency 클러스터로 감지되었다. 요청 자체는 200 으로 성공했으나 duration 이 임계치(>500ms)를 크게 초과했다. 단발성 이벤트이며, 동일 endpoint 의 다른 요청은 정상 시간대에 완료되었다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::FloorplansController#create |
| avg_duration_ms | 12523 |
| max_duration_ms | 12523 |
| sample_trace_id | 1202229949995714008 |
| cluster_type | latency |
| env | production, us-west-2 |
| tenant | cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-api (Floorplan 생성) | 1 | Facility 13992 에서 Floorplan 90994 생성 요청 1건이 12.5s 대기. 사용자 경험 저하 (UI 로딩 지연), 성공 응답 반환 |
Timeline#
- 2026-07-13 14:12:10 KST —
POST /api/v1/floorplans요청 시작 (first_seen, cluster start). - 2026-07-13 14:12:12 KST — Floorplan(id=90994) 저장 이후 after_create 콜백 실행: 캐시 무효화, 부모 캐시 리셋, Kinesis 이벤트 발행. 이 시점까지 이미 ~2s 경과.
- 2026-07-13 14:12:24 KST —
[200] POST /api/v1/floorplans완료 로그. 총 12523ms 소요 후 응답. 이후 재발 없음.
Error Log#
{
"resource_name": "Api::V1::FloorplansController#create",
"service": "cupixworks-api",
"occurrences": 1,
"avg_ms": 12523,
"max_ms": 12523,
"sample_trace_id": "1202229949995714008"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-07-13 14:12:10 KST
- 최근 발생: 2026-07-13 14:12:10 KST
Root Cause Summary#
증거로 확정된 root cause 는 아직 없다 — Datadog log 는 트레이스 안에서 6줄만 남았고 (기록된 로그 이벤트 사이에도 이미 최소 2s 이상 간격이 존재), 12s 대부분이 어디에서 소비되었는지 로그가 침묵한다. 유력한 가설은 Floorplan#after_create 콜백 체인 안의 무거운 작업(Cachable::ReviewLoad 캐시 무효화, EntityUpdates::Child#reset_parent_cached_entity_updates 의 ObjectSpace.each_object(Class) 스캔, EventService.publish_event 의 Kinesis put_records! 호출) 이 직렬로 수행되고 그중 하나가 외부 의존성(Kinesis put_records, Rails.cache 백엔드)의 일시 지연을 만난 것이다. 단발성(1건) 이라는 점에서 코드 결함보다는 외부 의존성의 tail latency + 이를 request 스레드에서 동기로 처리하는 구조 조합으로 판단된다. 최종 확정에는 APM span 세부 정보(trace_id 1202229949995714008 의 span breakdown) 확인이 필요하다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/floorplans_controller.rb:28 - Factory create:
app/factories/floorplan_factory.rb:5 super호출 시BaseFactory#create!로 진입 →self.model.save!:app/factories/base_factory.rb:124- Model save 이후 Rails 가 등록된
after_create콜백을 순차 실행:Cachable::ReviewLoad#invalidate_facility_review_cache_on_create:app/models/concerns/cachable/review_load.rb:14EntityUpdates::Child#reset_parent_cached_entity_updates:app/models/concerns/entity_updates/child.rb:22
- Failure/latency suspect:
EntityUpdates::Child#reset_parent_cached_entity_updates및Cupix::EventService.publish_event
def create
@model = factory_instance.create!(params)
super
end
self.set_parameters(params)
begin
self.model.save!
self.model
Floorplan#save! 이 성공한 이후, 다음 after_create 콜백들이 request 스레드에서 순차 실행된다:
class << self
def parent_classes
Cupix::Loader.load
ObjectSpace.each_object(Class).select do |model|
model.superclass == ApplicationRecord && !model.name.include?('::') && !model.name.ends_with?('Permission') && model.respond_to?(:entities) && model.entities.include?(self.name.underscore.to_sym)
end.map(&:name)
end
end
end
def reset_parent_cached_entity_updates
self.class.parent_classes.each do |class_name|
Cupix::Logger.info("reset #{class_name} (ID: #{self.send("#{class_name.underscore}_id")}) cached entity updates", ...)
Rails.cache.delete(entity_updates_cache_key(class_name, self.send("#{class_name.underscore}_id")))
end
end
기대 동작: parent_classes 는 정적 정보이므로 상수/메모이제이션에서 즉시 반환되고, 캐시 delete 두어 번으로 수 ms 안에 끝나야 한다.
실제 동작: 매 호출마다 Cupix::Loader.load 로 Zeitwerk::Loader.eager_load_all 을 호출하고 ObjectSpace.each_object(Class) 전체 클래스를 순회한다 (lib/cupix/loader.rb:14). 프로덕션에서 이미 eager-load 되어 있어도 each_object(Class) 자체가 GC 트리거링/비용이 큰 연산이며, 이 콜백이 하나의 트랜잭션 안에서 여러 번(Floorplan + 자식 모델들) 호출되면 latency 스파이크의 재료가 된다.
이후 Kinesis 로 이벤트를 발행한다:
def self.publish_event(events = [])
return if events.blank?
return if %w[development test].include?(Rails.env)
return unless release_date_by(Rails.env)
...
response = Cupix::Aws::Kinesis.put_records!({ stream_name: stream_name, records: records })
Cupix::Logger.info("Published event - failed_record_count: #{response.failed_record_count} / #{records.size}", ...)
put_records! 는 request 스레드에서 동기로 AWS Kinesis 를 호출한다. Kinesis tail-latency (throttle, retry, DNS 실패 등) 가 발생하면 그대로 요청 지연으로 전파된다. 기대 동작: 100–500ms; 실제 동작: p99 tail 은 초 단위까지 늘어날 수 있다.
Log Evidence#
Datadog 쿼리:
service:cupixworks-api trace_id:1202229949995714008
해당 trace 의 로그 (Datadog Logs API, 시간 범위 now-24h):
{"timestamp":"2026-07-13 14:12:24","status":"info","message":"[200] POST /api/v1/floorplans (Api::V1::FloorplansController#create)"}
{"timestamp":"2026-07-13 14:12:12","status":"info","message":"reset Facility (ID: 13992) cached entity updates","class":"Floorplan","function":"reset_parent_cached_entity_updates"}
{"timestamp":"2026-07-13 14:12:12","status":"info","message":"Published event - failed_record_count: 0 / 1","class":"Cupix::EventService","function":"publish_event"}
{"timestamp":"2026-07-13 14:12:12","status":"info","message":"No custom groups found for user 10131, tim.son@cupix.com","class":"UserFactory","function":"update_user_groups!"}
{"timestamp":"2026-07-13 14:12:12","status":"info","message":"reset Facility (ID: 13992) cached entity updates","class":"Floorplan","function":"reset_parent_cached_entity_updates"}
{"timestamp":"2026-07-13 14:12:12","status":"info","message":"Cachable::ReviewLoad | Invalidated facility review cache on create | model=Floorplan | model_id=90994 | facility_id=13992"}
주요 관찰:
- 6개 이벤트 모두
14:12:12초에 몰려 있고, 완료 응답만 12초 뒤인14:12:24에 찍혔다. 따라서 12.5s 중 대부분이 마지막 로그(Published event) 이후에 소비되었거나, 그 이전 controller 초기 처리 단계(auth, param parsing,save!) 에서 소비되었다. 로그만으로는 어느 쪽인지 확정 불가 (uncertain -- needs verification). reset Facility ... cached entity updates가 같은 초에 두 번 반복된다. 이는 Floorplan 저장 후 한 번, 그리고 Floorplan 이 소속된 상위 모델(예: level) 저장 시 추가 한 번 트리거된 것으로 보이며, callback overhead 가 반복되고 있음을 시사한다.- 동시간대(
now-4h,@duration:>500ms) 필터로도 이 트레이스만 잡혔고,apm.rack.request.durationtimeseries 도 빈 결과였다 (resource_name태그가 metrics 에는 축적되지 않은 것으로 보임). 재발은 관측되지 않았다.
APM 기간 필터 참고 쿼리:
service:cupixworks-api "FloorplansController#create" @duration:>500ms
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | EntityUpdates::Child.parent_classes 의 ObjectSpace.each_object(Class) + Cupix::Loader.load 가 매 after_create 마다 호출되어 spike 를 유발 |
app/models/concerns/entity_updates/child.rb:15 에서 매 호출마다 전체 클래스 스캔; 트레이스에서 reset_parent_cached_entity_updates 로그가 반복 등장; Cupix::Loader.load 는 Zeitwerk::Loader.eager_load_all 호출 (lib/cupix/loader.rb:14) |
통상 프로덕션에서는 eager-load 이후 반복 호출이 상대적으로 저렴 (idempotent) — 12s 를 단독으로 유발한다고 단정하기 어려움 | Inconclusive |
| H2 | Cupix::EventService.publish_event 의 Kinesis put_records! 동기 호출이 AWS 측 tail-latency (retry, throttle, DNS) 에 걸림 |
request 스레드에서 동기 호출 (lib/cupix/event_service.rb:43); 마지막 로그 Published event - failed_record_count: 0 / 1 이후 12s 응답 지연 관측; 단발성 발생 패턴이 AWS tail 과 부합 |
failed_record_count: 0 이므로 최종적으로는 성공. 로그가 put_records 완료 직후 찍혔다면 이후 12s 는 다른 원인 |
Inconclusive (유력한 후보) |
| H3 | DB write 자체 (Floorplan.save!) 가 lock/slow query 로 지연 |
save! 실패 시에도 rescue 로 커스텀 에러가 나오는데, 여기서는 200 반환 → save 성공 |
save 이후 콜백 로그가 정상적으로 찍힘 (즉 save 완료 시점은 늦어도 14:12:12) → save 지연 가설과 어긋남 (12s 대부분이 save 이후) | Rejected |
| H4 | Cachable::ReviewLoad#invalidate_facility_review_cache_on_create 의 Rails.cache.delete 가 캐시 백엔드(예: Redis) 지연 |
request 스레드에서 동기 실행; 캐시 백엔드가 일시 지연되면 그대로 전파 | 로그 하나에 Invalidated facility review cache 가 정상적으로 찍혔고 delete 는 통상 sub-ms |
Rejected |
| H5 | 외부 dependency 인시던트 (S3, Kinesis, DB) 로 인한 광역 latency | status board 조회에서 svc:cupixworks-api::unknown 스코프의 최근 인시던트가 존재하나 dep:* (외부 의존성) 활성 인시던트는 없음 |
단발성 1건이며 같은 시간대에 다른 endpoint 동시 지연 흔적 없음 | Rejected |
| H6 | Sidekiq inline 실행 또는 대량 fixture ingestion 이 request path 에서 발생 | 트레이스 로그에 특별한 worker inline 표시 없음 | 코드상 factory create 흐름에서 Sidekiq 은 async enqueue 만 수행 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- trace_id
1202229949995714008의 APM span breakdown 을 확인: Datadog APM UI 에서 실제로 어느 span (DB, Redis, Kinesis, aws.request) 이 12s 중 대부분을 차지했는지 확인해야 root cause 확정 가능. 로그만으로는 확정 불가 (uncertain -- needs verification). - 확인 파일:
app/controllers/api/v1/floorplans_controller.rb:28-32,app/factories/floorplan_factory.rb:5-66.
단기 개선 (1주 이내)#
EntityUpdates::Child.parent_classes결과를 클래스 단위로 메모이제이션:app/models/concerns/entity_updates/child.rb:12-19. 매 호출마다Cupix::Loader.load+ObjectSpace.each_object(Class)를 수행할 이유 없음. 앱 boot 이후 한 번만 계산해 상수/@@parent_classes로 저장하면 반복 호출 비용을 실질적으로 제거할 수 있다. 근거: 콜백 흐름 상 모든after_create/after_update마다 트리거됨 (entity_updates/child.rb:8-9).Cupix::EventService.publish_event를 fire-and-forget 큐로 이동:lib/cupix/event_service.rb:43의put_records!를 Sidekiq job (또는 별도 스레드/threadpool) 에 위임. 요청 스레드는 이벤트 payload 를 큐에 넣고 즉시 반환하도록 변경. Kinesis 의 tail-latency 가 사용자 요청 시간에 영향을 주지 않도록 분리하는 것이 목적.- 엔드포인트별 alert 조건 강화:
resource_name:"Api::V1::FloorplansController#create"에 대해 p95>3s 이 5분 동안 지속되면 alert 하도록 monitor 추가.
장기 개선 (재발 방지)#
- after_create / after_commit 콜백 감사(audit):
Floorplan등 자주 생성되는 모델이 include 하는 concern 이 20개 이상. 각 concern 이 request 스레드에서 무엇을 하는지 정리하고 (외부 I/O, ObjectSpace, 대량 SQL), 동기로 남길 것과 async 로 옮길 것을 구분한다. ObjectSpace.each_object(Class)사용처 전반 리팩터링:rake태스크나 boot 시 한 번만 실행하는 지점(elasticsearch.rake:12,migrate.rake:214,counter_culture.rb:8등) 은 문제 없지만, request 경로에서 반복 호출되는 지점 (entity_updates/child.rb:15,workspace_entity/facility.rb:24,transferable/descendants.rb:35) 은 반드시 캐싱해야 한다.
Monitoring#
- Floorplan create p95 latency:
p95:trace.rails.request{service:cupixworks-api,resource_name:api::v1::floorplanscontroller#create}
- Kinesis put_records latency (event publish 병목 확인용):
avg:aws.kinesis.putrecords.latency{service:cupixworks-api}
- 12s+ 요청 발생 카운트:
sum:trace.rails.request.hits{service:cupixworks-api,resource_name:api::v1::floorplanscontroller#create,duration:>5s}.as_count()
- p99 request duration timeseries:
p99:trace.rails.request{service:cupixworks-api,resource_name:api::v1::floorplanscontroller#create}
Risk Assessment#
- Risk level: low (단발 1건, 응답은 성공, 사용자 데이터 손실 없음. 다만 재발 시 사용자 경험 저하 가능)
- 예상 복잡도: standard (콜백 캐싱 및 Kinesis 발행 async 화는 별도 spec/모니터링 필요)