ES /docs

Api::V1::CapturesController#create_resource (avg 15824ms, max 15824ms)

RCA: Api::V1::CapturesController#create_resource (avg 15824ms, max 15824ms)

Overview#

What Happened#

2026-06-26 11:03 KST 에 cupixworks-apiApi::V1::CapturesController#create_resource 엔드포인트에서 단일 요청이 15.8초 동안 지연되었다. 같은 시간대의 다른 create_resource 요청들은 모두 200 OK 로 빠르게 처리되었으므로 일반적인 회귀가 아닌 단일 spike 형태이다. 동일 시간대에 svc-scope incident (2026-06-26-svc-cupixworks-api--unknown-1) 가 열려 있어 같은 서비스에서 다른 latency/error 가 함께 발생하고 있었다.

Quick Facts#

Field Value
resource_name Api::V1::CapturesController#create_resource
sample_trace_id 4653776727289616755
avg_duration_ms 15824
max_duration_ms 15824
occurrence_count 1
env production / ap-southeast-2
tenant cupix

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (Capture upload flow) 1 단일 요청이 15.8초 지연. 클라이언트 측에서 capture resource 업로드 시작 직전 단계의 latency 체감 가능. HTTP 상태 코드 자체는 200 으로 응답 완료된 것으로 보임 (status:error 로그 없음).

Timeline#

  1. 2026-06-26 10:25 KST — 같은 서비스에서 svc-scope incident 2026-06-26-svc-cupixworks-api--unknown-1 시작 (related context, 다른 cluster 들로 인한 것).
  2. 2026-06-26 11:03 KST — 문제의 create_resource 요청 시작, 15.8초 후 완료 (trace 4653776727289616755).
  3. 2026-06-26 11:04 ~ 11:14 KST — 같은 엔드포인트의 후속 요청들은 정상 응답 (200, 수십~수백 ms).

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::CapturesController#create_resource",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 15824,
  "max_ms": 15824,
  "sample_trace_id": "4653776727289616755"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-06-26 11:03 KST
  • 최근 발생: 2026-06-26 11:03 KST

Root Cause Summary#

Api::V1::CapturesController#create_resourceMultipleResourcableController#create_resource 가 호출되어 Resource 레코드를 save 한다. Resource 모델은 EntityIndexable concern 을 include 하고 있으며, 이 concern 은 after_commit on: [:create] 에서 동기적으로 Elasticsearch index API 를 호출한다. Elasticsearch client 는 connection pool checkout timeout: 7 초 + request timeout: 10 초로 설정되어 있어, ES 가 일시적으로 느려지거나 connection pool 이 포화되면 단일 요청이 최대 ~17 초 까지 블록될 수 있다. 이 cluster 의 15.8 초 spike 는 이 동기 ES 인덱싱 구간에서 발생한 일시적 latency 와 정확히 부합한다. 동일 시간대의 svc-scope incident 도 같은 원인의 다른 cluster 들 (capture 외 다른 entity 의 save 경로) 일 가능성이 높다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/captures_controller.rb:20 (include MultipleResourcableController)
  • Action: app/controllers/concerns/multiple_resourcable_controller.rb:52-78 (create_resource)
  • Save trigger: app/controllers/concerns/multiple_resourcable_controller.rb:61 (resource.save)
  • After-commit callback: app/models/concerns/entity_indexable.rb:42 (after_commit :_entity_index_document, on: [:create])
  • Failure point: app/models/concerns/entity_indexable.rb:160-167 (Elasticsearch::Model.client.index(...) — synchronous network I/O inside request lifecycle)
  • Elasticsearch client config: config/initializers/elasticsearch.rb:17-27 (pool timeout 7s + request timeout 10s)

create_resource 컨트롤러 액션은 새 Resource 를 빌드하고 resource.save 를 호출한다:

app/controllers/concerns/multiple_resourcable_controller.rb:52-78ruby
def create_resource
  raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Duplicate kind: #{params[:kind]}") and return unless @model.resources.find_by_kind(params[:kind]).blank?

  resource = @model.resources.new kind: params[:kind],
                                  user: current_user,
                                  team: @model.team,
                                  name: params[:name]

  begin
    unless resource.save
      raise Cupix::Errors::Parameter.new(code: 'ENT10003', reason: resource.errors.full_messages)
    end
  rescue Cupix::Errors::Parameter => e
    raise e
  else
    render_api Renderable.new({
      contents: resource,
      serializer: ResourceSerializer,
      serializer_option: {
        fields: {
          resource: @fields
        },
        is_collection: false
      }
    })
  end
end

Resource 모델은 EntityIndexable 을 include 하고 있다:

app/models/resource.rb:1-15ruby
class Resource < ApplicationRecord
  include WorkspaceEntity::Resource
  include EntityIndexable
  include Properties::Resource
  include ::Statable::Resource
  include ::Cyclable::Resource
  ...
end

EntityIndexableafter_commit on: [:create] 에서 동기적으로 Elasticsearch 에 인덱싱한다:

app/models/concerns/entity_indexable.rb:41-45,160-170ruby
included do
  after_commit :_entity_index_document, on: [:create]
  after_commit :_entity_update_document, on: [:update]
  after_commit :_entity_delete_document, on: [:destroy]
end
# ...
def _entity_index_document
  return if @skip_index_document == true

  Elasticsearch::Model.client.index(
    index: self.class.entity_index_name,
    id: entity_document_id,
    body: as_entity_indexed_json
  )
rescue StandardError => e
  Cupix::Logger.error("Entity index error - #{e.message}", class: self.class.name, function: __method__)
end

Elasticsearch client 는 connection pool 과 request timeout 이 다음과 같이 설정되어 있다:

config/initializers/elasticsearch.rb:17-27ruby
Elasticsearch::Model.client = ConnectionPool::Wrapper.new(size: 10, timeout: 7) {
  Elasticsearch::Client.new(
    host: ENV.fetch('RAILS_ES_HOST') { 'localhost' },
    port: ENV.fetch('RAILS_ES_PORT') { DEFAULT_RAILS_ES_PORT },
    user: ENV['RAILS_ES_USER'],
    password: ENV['RAILS_ES_PASSWORD'],
    transport_options: {
      request: {
        timeout: 10
      }
    }
  )
  # ...
}

기대 동작: create_resource 는 DB INSERT (수십 ms) 후 즉시 응답해야 하며, 외부 인덱싱은 백그라운드 작업으로 위임되어야 한다.

실제 동작: save 가 커밋되는 순간 _entity_index_document 가 동기 실행되어 HTTP 으로 Elasticsearch 에 인덱싱 요청을 보낸다. ES 응답이 느리거나 pool 이 포화되면 컨트롤러 응답 시간이 그만큼 지연된다. Pool checkout timeout: 7s + request timeout: 10s 의 합이 본 cluster 의 15.8 초 latency 와 일관된다 (uncertain — pool 포화/단순 ES 느림 둘 다 가능, 정확한 구간 분해는 trace span 분석 필요).

Log Evidence#

검색에 사용한 Datadog 쿼리:

text
service:cupixworks-api "create_resource"
time: 2026-06-26T01:55:00Z .. 2026-06-26T02:15:00Z

같은 시간대의 동일 엔드포인트는 모두 정상 응답 (200) — single spike 임을 확인:

text
2026-06-26 11:14:48  [200] POST /api/v1/pointclouds/1176062/resources (Api::V1::PointcloudsController#create_resource)
2026-06-26 11:14:47  [200] POST /api/v1/clusters/43624/resources    (Api::V1::ClustersController#create_resource)
2026-06-26 11:14:47  [200] POST /api/v1/captures/46488/resources    (Api::V1::CapturesController#create_resource)
2026-06-26 11:13:56  [200] POST /api/v1/captures/722388/resources   (Api::V1::CapturesController#create_resource)
...

같은 시간대 (11:04~11:14 KST) status:error 로그는 모두 외부 OPC 통합 (OpcOperation) 의 409 Conflict 이며 본 cluster 와 직접 무관:

json
{
  "timestamp": "2026-06-26 11:08:04",
  "status": "error",
  "message": "[Integration] Failed to get OPC API access token for integration(1849): 409 Conflict",
  "class": "IntegrationRepository",
  "function": "opc_access_token"
}

Entity index error 로그는 검색 결과 0건이므로 ES 호출이 timeout 직전 단계까지 갔으나 결국 성공했거나, callback 의 rescue StandardError 가 swallow 한 경우 외에는 명시적 실패가 기록되지 않았다 (uncertain — needs verification via trace span breakdown).

Status-board 결과: 같은 시간대에 svc-scope incident 2026-06-26-svc-cupixworks-api--unknown-1 (시작 10:25 KST) 가 열려 있으며 7개 cluster 가 묶여있어 서비스 전반의 일시적 degradation context 임을 확인:

text
scope: svc:cupixworks-api::unknown
active.id: 2026-06-26-svc-cupixworks-api--unknown-1
started_at: 2026-06-26T01:25:34.396Z (10:25 KST)
cluster_ids: 7

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 after_commit 의 동기 Elasticsearch index 호출이 일시적 ES 지연/connection pool 포화로 15.8 초 동안 블록됨 EntityIndexableafter_commit on: [:create] 에서 동기 Elasticsearch::Model.client.index(...) 실행 (app/models/concerns/entity_indexable.rb:42,160-167). ES pool timeout:7 + request timeout:10 합 ≈ 17s, 관측 15.8s 와 일관. 같은 시간대 다른 cluster 들이 묶인 svc-scope incident 존재. trace span 별 시간 분해를 확보하지 못해 ES 구간이 정확히 15s 차지했다는 직접 증거는 없음. Confirmed (primary, pending trace span verification)
H2 DB INSERT 자체가 느림 (lock 경합/슬로우 쿼리) 같은 시간대 동일 컨트롤러의 다른 create_resource 호출은 모두 빠르게 200. DB 인서트 한 건이 단독으로 15초 걸리는 패턴은 슬로우 쿼리/락 로그가 동반되어야 하나 미관측. Rejected
H3 S3/storage 호출이 save 경로에 포함되어 지연 Storagable::Resource concern 이 after_update :after_size_updatedbefore_destroy :destroy_s3_objects 를 가지나, create 시점 에는 S3 호출 hook 없음 (app/models/concerns/storagable/resource.rb:9,11). create_resource 는 record 생성만 하고 upload URL 발급은 별도 endpoint (resource_upload_url) 임. create 콜백 체인에서 S3 호출 경로 부재. Rejected
H4 외부 OPC API (OpcOperation) 409 가 connection 자원을 점유해 간접 영향 같은 시간대에 OpcOperation 409 에러 다수 (class:IntegrationRepository). create_resource 코드 경로에 OPC integration 호출 없음. 시간상 OPC 에러는 11:04~11:08 KST 사이에 집중되어 11:03 KST 발생 spike 와 정확히 일치하지 않음. Rejected
H5 단일 외부 의존성 (dep) 의 전면 outage status-board 결과 scope 가 svc:cupixworks-api::unknown 으로 외부 dep 매칭 없음. dep scope 활성 없음. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 운영상 즉시 조치는 불필요 (이번 spike 는 단일 요청, 200 OK 로 응답 완료, 사용자 데이터 손실 없음).
  • 다만 svc-scope incident 2026-06-26-svc-cupixworks-api--unknown-1 의 다른 cluster 들과 함께 ES cluster 상태 (CPU/JVM heap/indexing latency) 를 ap-southeast-2 리전 한정으로 확인. 동일 원인이면 같은 incident 안에서 묶어 해결.

단기 개선 (1주 이내)#

  • app/models/concerns/entity_indexable.rb:42-44after_commit 콜백을 동기 ES 호출에서 비동기 (Sidekiq 워커) 인덱싱으로 전환. 컨트롤러 응답이 외부 ES latency 에 결합되지 않도록 분리한다. 워커는 retry/backoff 를 갖는다.
  • 또는 최소한 _entity_index_documentrescue StandardError 가 timeout 도 흡수하도록 Faraday::TimeoutError / Elastic::Transport::Transport::Errors 를 명시적으로 catch + warn 레벨로 로깅하여 무엇이 지연을 일으키는지 가시화한다 (현재 Entity index error 로그가 0 건이라 timeout 인지 swallow 인지 불명확).
  • ES request timeout (현재 10s) 을 더 짧게 (예: 2~3s) 줄이고 비동기 큐잉으로 이동시켜 user-facing latency 상한을 명확히 한다.

장기 개선 (재발 방지)#

  • 모든 after_commit 외부 I/O (ES indexing, S3 cleanup, webhook publish 등) 를 일관되게 비동기 패턴으로 정리하는 정책 수립. 동기 외부 호출은 데코레이터 메서드/명시적 API 에서만 허용.
  • Resource/Capture 같은 hot path 모델에 대해 ApplicationRecord 차원에서 외부 I/O callback 을 탐지/금지하는 lint 규칙 도입 검토.

Monitoring#

추가/확인할 Datadog timeseries 쿼리 (release dashboard widget 호환 syntax):

text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:Api::V1::CapturesController#create_resource}
text
p95:trace.rack.request.duration{service:cupixworks-api,resource_name:Api::V1::CapturesController#create_resource}
text
max:trace.rack.request.duration{service:cupixworks-api,resource_name:Api::V1::CapturesController#create_resource}

ES 인덱싱 span 자체의 평균 latency 추적 (Datadog APM 가 elasticsearch span 을 분리 수집한다면):

text
avg:trace.elasticsearch.command.duration{service:cupixworks-api,env:production}

p95 가 1초 이상 지속되거나 max 가 5초 이상 spike 가 분당 3건 이상이면 monitor 알림.

Risk Assessment#

  • Risk level: low (단일 요청 spike, 200 OK 응답, 데이터 무결성 영향 없음)
  • 예상 복잡도: standard (비동기 인덱싱 전환은 코드 변경 범위가 넓지만 패턴이 일반적)