ES /docs

Api::V1::TradeTaskTracesController#index (avg 10654ms, max 10654ms)

RCA: Api::V1::TradeTaskTracesController#index (avg 10654ms, max 10654ms)

Overview#

What Happened#

2026-08-04 02:30:59 KST에 cupixworks-api (us-west-2) 의 Api::V1::TradeTaskTracesController#index 요청 하나가 10,640ms 동안 실행되었다. 반환된 레코드는 72건이었지만 응답 전체 시간의 76% (8,038ms) 가 serializer 단계에서 소비되었고, 동일한 facility/record_id 로 15분 후 반복된 동일 요청은 107ms 로 완료되어 100배 차이가 관측되었다. 자동화된 preprocessor agent (cupix-sitetrack-preprocessor-agent) 가 유일한 caller 로, 콜드 캐시 상태에서 associated model (record, capture, workarea, trade) 별 fetch_cache 미스 캐스케이드가 발생한 것으로 판단된다.

Quick Facts#

Field Value
resource_name Api::V1::TradeTaskTracesController#index
top_frame app/repositories/trade_task_trace_repository.rb:47 (SearchResult contents) → app/serializers/trade_task_trace_serializer.rb:11-14 (_record/_capture/_workarea/_trade)
duration 10,640.37 ms
serialization 8,038 ms (76%)
db 768.72 ms (7%)
view 0.11 ms
total_entries 72 (per_page=100, page=1)
runtime Ruby (Rails / FastJsonapi)
deploy production-us-west-2-20260803t0501z0-6432b465-cupixworks
env production, us-west-2
user_agent cupix-sitetrack-preprocessor-agent
tenant / team cupix / accoes (id=816)

Affected Teams#

Team / Domain Error Count Impact
accoes (team_id=816) 1 preprocessor-agent 이 trade_task_trace 목록 조회에서 10초 지연 관찰

동일 endpoint 를 사용하는 프론트엔드 caller 는 발견되지 않았고 자동화 agent 요청이 유일한 소스이다.

Timeline#

  1. 2026-08-04 02:30:59 KST — GET /api/v1/facilities/kgo4s6/trade_task_traces?record_id=140677&per_page=100&page=1 요청 시작 (trace_id 8735584501938793007).
  2. 2026-08-04 02:31:10 KST — 동일 요청 200 응답, duration=10,640ms, serialization=8,038ms.
  3. 2026-08-04 02:46:45 KST — 동일 파라미터 재요청 완료, duration=107ms, serialization=54ms (캐시 warm).
  4. 2026-08-04 02:59:57 KST — 동일 파라미터 재요청 완료, duration=121ms, serialization=86ms (지속적으로 warm).

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::TradeTaskTracesController#index",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 10654,
  "max_ms": 10654,
  "sample_trace_id": "8735584501938793007"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1 (14일 retention 창 내)
  • 최초 발생: 2026-08-04 02:30:59 KST
  • 최근 발생: 2026-08-04 02:30:59 KST

한 건의 slow request 이지만 자동화 agent 의 반복 호출 특성상 콜드 캐시 시나리오 (deploy 직후 · 캐시 eviction · TTL 만료 · 신규 record 대량 유입) 마다 재현 가능하다.

Root Cause Summary#

TradeTaskTraceSerializer 는 belongs_to association (record, capture, workarea, trade) 각각을 application_record.rb:84 에서 동적 정의한 _#{name} 메서드로 직렬화한다. 이 메서드는 preloaded ActiveRecord association 을 사용하지 않고 대신 fetch_cache(model_name, model_id) 를 호출하여 Rails.cache 에서 개별 캐시 엔트리를 조회한다. 결과 72건 × association 4개 = 288회 fetch_cache 가 순차 실행되며, 캐시 미스 시마다 (1) Rails.cache 왕복 (2) Model.find_by_id DB 조회 (3) 대상 모델의 serialized_json 재귀 생성 (4) Rails.cache.write 가 모두 인라인으로 발생한다. Datadog 로그에서 동일 파라미터의 후속 요청이 100배 빠른 (10,640ms → 107ms) 점, serialization 이 8,038ms 로 db(768ms) 를 크게 상회하는 점이 콜드 캐시 미스 캐스케이드를 뒷받침한다. TradeTaskTraceRepository.default_joins.includes(:record, :capture, :workarea, :trade) 로 eager load 를 수행하지만, serializer 가 preload 를 무시하고 캐시 경로로 우회하므로 eager loading 이 무의미하다.

Technical Analysis#

Code Path#

Entry point: app/controllers/api/v1/trade_task_traces_controller.rb:9

app/controllers/api/v1/trade_task_traces_controller.rb:9-18ruby
def index
  query_option = Cupix::QueryOption::TradeTaskTrace.new(get_query_option, params)
  traces = repository_instance.search(query_option)

  render_api Renderable.new({
    search_result: traces,
    is_collection: true,
    serializer_option: @serializer_option
  })
end

Repository search 는 최종적으로 default_joins 로 preload 를 붙여 SearchResult 를 만든다:

app/repositories/trade_task_trace_repository.rb:42-58ruby
traces = query.order(analyzed_at: :desc, id: :desc).paginate(
  per_page: self.query_option.per_page,
  page: self.query_option.page
)

SearchResult.new(
  contents: self.class.default_joins(traces),
  pagination: {
    total_entries: traces.total_entries,
    ...
  }
)
app/repositories/trade_task_trace_repository.rb:76-78ruby
def default_joins(record)
  record.includes(:record, :capture, :workarea, :trade)
end

Serializer 는 preload 된 association 대신 _record/_capture/_workarea/_trade 를 호출한다:

app/serializers/trade_task_trace_serializer.rb:11-14ruby
attribute :record, &:_record
attribute :capture, &:_capture
attribute :workarea, &:_workarea
attribute :trade, &:_trade

이 helper 는 ApplicationRecord.belongs_to override 에서 동적으로 정의된다:

app/models/application_record.rb:81-95ruby
def belongs_to(name, scope = nil, **options)
  super

  define_method "_#{name}" do |*args, **kwargs, &block|
    if options[:polymorphic]
      fetch_cache(send("#{name}_type"), send("#{name}_id")).merge({
        type: send("#{name}_type")
      }) rescue nil
    else
      model_name = options[:class_name] || name
      model_id = options[:foreign_key] || "#{name}_id"
      fetch_cache(model_name.to_s.classify, send(model_id))
    end
  end
end

Failure point: app/models/concerns/cachable.rb:58 — 캐시 미스 경로가 인라인 SQL 조회 + 재귀 직렬화를 수행하고 각 association 마다 반복된다.

app/models/concerns/cachable.rb:58-70ruby
def fetch_cache(model_name = self.class.name, model_id = self.id)
  return nil if model_id.blank?

  Rails.cache.fetch(cache_key(model_name, model_id), skip_nil: true, expires_in: self.class.cache_expires_in) do
    if self.respond_to?("serialized_#{model_name.underscore}_json".to_sym)
      self.send("serialized_#{model_name.underscore}_json")
    else
      record = model_name.constantize.find_by_id(model_id)

      record.serialized_json if record.respond_to?(:serialized_json)
    end
  end
end

기대 동작: default_joins.includes 로 4개 association 이 한 번의 IN 쿼리로 preload 되어, 72건 직렬화 시 추가 DB 왕복 없이 in-memory 로 처리되어야 한다.

실제 동작: serializer 는 preloaded association 을 사용하지 않고 fetch_cache 로 우회하여 (a) 72×4=288회 Rails.cache 왕복 (b) 미스 시 record 별 find_by_id + serialized_json (c) 각 항목마다 Rails.cache.write 를 수행. Warm 상태에서는 (a) 만 발생해 100ms 수준, 콜드 상태에서는 (b)/(c) 가 288회 반복되어 8초대로 급증.

Log Evidence#

Datadog 로그 조회:

text
service:cupixworks-api "kgo4s6" "trade_task_traces"
timerange: 2026-08-03T17:30:00Z .. 2026-08-03T17:32:00Z

Slow 요청 로그 (핵심 필드만 발췌):

json
{
  "@timestamp": "2026-08-03T17:31:10.422Z",
  "message": "[200] GET /api/v1/facilities/kgo4s6/trade_task_traces (Api::V1::TradeTaskTracesController#index)",
  "duration": 10640.37,
  "serialization": { "duration": 8038 },
  "db": 768.72,
  "view": 0.11,
  "pagination": { "per_page": 100, "total_pages": 1, "current_page": "1", "total_entries": 72 },
  "params": {
    "record_id": "140677",
    "per_page": "100",
    "page": "1",
    "fields": ["id", "uuid", "status", "analyzed_at", "record", "capture", "workarea", "trade"],
    "facility_key": "kgo4s6"
  },
  "user_agent": "cupix-sitetrack-preprocessor-agent",
  "team": { "domain": "accoes", "id": 816 },
  "controller": "Api::V1::TradeTaskTracesController",
  "action": "index",
  "environment": "production",
  "request_id": "87f2361d-7f86-4986-9743-0e3e108492c8"
}

동일 endpoint · 동일 facility (kgo4s6) · 동일 total_entries (72) 요청의 지연 비교 (Datadog 쿼리 service:cupixworks-api "trade_task_traces" -bulk, 2026-08-03T14:00Z..18:00Z):

text
17:31:10Z  duration=10640ms  serialization=8038ms  db=768ms  total_entries=72   ← slow
17:46:45Z  duration=  107ms  serialization=  54ms  db= 23ms  total_entries=72   ← warm
17:59:57Z  duration=  121ms  serialization=  86ms  db= 25ms  total_entries=72   ← warm

또한 동일 endpoint 에서 훨씬 더 많은 891건을 반환하는 다른 facility 요청도 warm 상태에서는 400-600ms 로 완료되어 (ciw1lc at 17:28), 데이터 볼륨이 아닌 캐시 상태가 dominant factor 임을 확인.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Serializer 의 _record/_capture/_workarea/_trade 가 preloaded association 대신 fetch_cache 로 우회 → 콜드 캐시 미스 시 288회 (72×4) 캐시/DB 왕복이 인라인 실행되어 8초대 지연 log: serialization.duration=8038ms, db=768ms, view=0.11ms; same params 재호출은 107ms; code: trade_task_trace_serializer.rb:11-14 + application_record.rb:84 + cachable.rb:58-70; default_joins.includes 는 있지만 serializer 가 사용하지 않음 Confirmed
H2 SQL N+1 (associations 를 individually 로 조회) 72건에서 db=768ms 는 288 쿼리 대비 다소 낮지만 배제 못함 default_joins.includes(:record, :capture, :workarea, :trade) 로 이미 eager load; log 의 db 는 serialization 하위 시간을 대부분 포함하지 않음; warm 재요청 db=23ms 로 대부분 감소하는데 이는 캐시 히트 후 DB 조회 자체가 사라졌음을 의미 Rejected
H3 DB slow query / index missing on trade_task_traces (facility_id, record_id) 첫 요청 db=768ms 동일 파라미터 재요청 db=23ms 로 33배 감소; 인덱스 이슈라면 반복해도 유사해야 함; primary query 는 .where(facility_id: ...).where(record_id: ...) 로 단순 Rejected
H4 외부 종속성 (Rails.cache backend/Redis) 또는 리전 이슈 serialization 지연이 fetch_cache 경로에 있음 status-board 결과 active incident 없음, recent 7/29-30 은 이미 resolved; 다른 endpoint 정상 응답 관찰됨 Rejected
H5 대용량 페이지로 인한 CPU-bound 직렬화 serialization=8038ms 는 크다 total_entries=72 로 매우 작음; 891건 요청은 warm 상태 400-600ms; 데이터 볼륨 아닌 캐시 상태가 dominant Rejected

Fix Recommendation#

즉시 조치 (Critical)#

없음. 1건의 slow request 로 error budget 소진이 크지 않고, 자동 preprocessor agent 가 warm 상태 진입 후 정상 latency 로 돌아왔다. 아래 단기 개선 티켓 발행 권장.

단기 개선 (1주 이내)#

  • app/serializers/trade_task_trace_serializer.rb:11-14: _record/_capture/_workarea/_trade 대신 preloaded association 을 직접 사용하는 attribute 정의로 전환하거나, 캐시 미스 배치 로딩을 도입한다. default_joins.includes(:record, :capture, :workarea, :trade) 로 이미 preload 하고 있으므로 associaton 을 그대로 직렬화하는 편이 캐시 경로보다 예측 가능하다. — 방향만 결정하고 다른 serializer 도 동일 패턴을 공유하므로 (app/serializers/editing_entity_serializer.rb:8 등 총 10+ 파일) 영향 범위를 먼저 조사할 것.
  • app/models/concerns/cachable.rb:58-70: fetch_cacheRails.cache.read_multi / write_multi 기반의 배치 인터페이스로 확장하여 288건을 1-2회 왕복으로 병합. 미스 시 find_by_id 대신 where(id: missing_ids) 로 in-clause 조회.
  • APM: Cupix::QueryOption::TradeTaskTraceserialization.duration > 1000ms alarm 추가 (아래 Monitoring 참고).

장기 개선 (재발 방지)#

  • Cachable pattern 을 유지하려면 write-through 캐시 warmup 워커 를 도입하여 신규/변경된 trade_task_trace 및 그 associations 를 after_commit 에서 이미 populate 하고 있는지 검증 (cachable.rb:23-52 write_cache 가 실제로 실행되는 flow 확인 필요).
  • Serializer 계층에서 Rails.cache 를 직접 호출하는 패턴 자체를 재고: FastJsonapi::ObjectSerializer 의 캐시 옵션 또는 페이지 단위 fragment cache 로 전환하면 288회 왕복 문제를 근본적으로 제거할 수 있다.
  • APM 대시보드에 resource_nameserialization.duration / duration 비율 시계열 추가 → 유사한 cache-miss cascade 패턴을 다른 컨트롤러에서 조기 발견.

Monitoring#

Datadog timeseries widget 쿼리 (release dashboard 에 그대로 임베드 가능):

  • p95 latency of the endpoint:
text
p95:trace.rack.request{service:cupixworks-api,resource_name:api::v1::tradetasktracescontroller#index,env:production}
  • Slow (>1s) request 수:
text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::tradetasktracescontroller#index,env:production,@duration:>1s}.as_count()
  • Serialization duration p95 (log-based facet — attribute @serialization.duration):
text
p95:logs.serialization.duration{service:cupixworks-api,@controller:Api::V1::TradeTaskTracesController,@action:index,env:production}

@serialization.duration 는 request 로그에 이미 노출되는 필드이지만 Datadog log metric 으로 export 되어 있어야 위 쿼리가 실데이터를 반환한다. facet 미등록 시 log-based metric 을 먼저 생성.

  • 알림: serialization.duration > 1000ms 30분 창에서 3건 이상 발생 시 warning.

Risk Assessment#

  • Risk level: low (14일 창 1건, 자동 agent 만 영향, 재요청 시 warm 캐시로 정상화)
  • 예상 복잡도: standard (serializer + cachable concern 리팩터, 유사 패턴 공유하는 serializer 다수 존재하여 영향 범위 파악 필요)