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-api 의 Api::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#
- 2026-06-26 10:25 KST — 같은 서비스에서 svc-scope incident
2026-06-26-svc-cupixworks-api--unknown-1시작 (related context, 다른 cluster 들로 인한 것). - 2026-06-26 11:03 KST — 문제의
create_resource요청 시작, 15.8초 후 완료 (trace4653776727289616755). - 2026-06-26 11:04 ~ 11:14 KST — 같은 엔드포인트의 후속 요청들은 정상 응답 (200, 수십~수백 ms).
Error Log#
{
"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_resource 는 MultipleResourcableController#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 를 호출한다:
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 하고 있다:
class Resource < ApplicationRecord
include WorkspaceEntity::Resource
include EntityIndexable
include Properties::Resource
include ::Statable::Resource
include ::Cyclable::Resource
...
end
EntityIndexable 은 after_commit on: [:create] 에서 동기적으로 Elasticsearch 에 인덱싱한다:
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 이 다음과 같이 설정되어 있다:
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 쿼리:
service:cupixworks-api "create_resource"
time: 2026-06-26T01:55:00Z .. 2026-06-26T02:15:00Z
같은 시간대의 동일 엔드포인트는 모두 정상 응답 (200) — single spike 임을 확인:
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 와 직접 무관:
{
"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 임을 확인:
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 초 동안 블록됨 |
EntityIndexable 가 after_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_updated 와 before_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-44의after_commit콜백을 동기 ES 호출에서 비동기 (Sidekiq 워커) 인덱싱으로 전환. 컨트롤러 응답이 외부 ES latency 에 결합되지 않도록 분리한다. 워커는 retry/backoff 를 갖는다.- 또는 최소한
_entity_index_document의rescue 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):
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:Api::V1::CapturesController#create_resource}
p95:trace.rack.request.duration{service:cupixworks-api,resource_name:Api::V1::CapturesController#create_resource}
max:trace.rack.request.duration{service:cupixworks-api,resource_name:Api::V1::CapturesController#create_resource}
ES 인덱싱 span 자체의 평균 latency 추적 (Datadog APM 가 elasticsearch span 을 분리 수집한다면):
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 (비동기 인덱싱 전환은 코드 변경 범위가 넓지만 패턴이 일반적)