Api::V1::PointcloudsController#create_resource (avg 3801ms, max 3801ms)
RCA: Api::V1::PointcloudsController#create_resource Latency (3801ms)
Overview#
What Happened#
2026-06-05 07:49 KST에 cupixworks-api 서비스의 PointcloudsController#create_resource 엔드포인트가 3801ms의 응답 시간을 기록했다. 요청 자체는 HTTP 200으로 성공했으나, 일반적인 응답 시간 대비 지연이 발생했다. 동일 시간대에 시스템 전반적으로 20건 이상의 3000ms 초과 요청이 관측되어 부하 집중 상황이었다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::PointcloudsController#create_resource |
| top_frame | app/controllers/concerns/multiple_resourcable_controller.rb:52 |
| env | production, us-west-2 |
| duration | 3801ms |
| http_status | 200 |
Timeline#
- 2026-06-05 07:49:13 KST — Pointcloud 1128578 생성, cache invalidation 시작
- 2026-06-05 07:49:24 KST — State change:
initializing -> queued - 2026-06-05 07:49:28 KST —
create_resource요청 시작 (3801ms 소요) - 2026-06-05 07:49:32 KST —
create_resource응답 완료 (HTTP 200) - 2026-06-05 07:50:58 KST — Pointcloud 1128578 state:
queued -> done - 2026-06-06 07:49 KST — Error Sweeper가 latency 클러스터로 감지
Error Log#
{
"resource_name": "Api::V1::PointcloudsController#create_resource",
"service": "cupixworks-api",
"occurrences": 1,
"avg_ms": 3801,
"max_ms": 3801,
"sample_trace_id": "1956145270835683743"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-06-06 07:49 KST
- 최근 발생: 2026-06-06 07:49 KST
- 영향: Pointcloud resource 생성 API 응답 지연. 요청 자체는 성공(200)하여 데이터 손실은 없으나, 클라이언트 측 타임아웃 위험이 있음. 동일 시간대 4건의
create_resource요청이 3000ms 초과.
Root Cause Summary#
create_resource 액션의 응답 경로에 동기식으로 실행되는 무거운 I/O 작업들이 누적되어 지연이 발생했다. 핵심 병목은: (1) ResourceSerializer에서 S3 presigned URL을 동기 생성하는 AWS API 호출, (2) after_commit 콜백에서 Elasticsearch 인덱싱 시 ancestry 조회를 위한 9개의 개별 DB 쿼리, (3) serialization 시 state machine transition(uploading)으로 인한 추가 DB UPDATE. 이 병목들이 시스템 전반 부하 상황과 결합되어 3801ms 지연이 발생했다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/concerns/multiple_resourcable_controller.rb:52 create_resource메서드가 호출되면 아래 순서로 실행된다:
1. 중복 체크 (Line 53)
raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Duplicate kind: #{params[:kind]}") and return unless @model.resources.find_by_kind(params[:kind]).blank?
DB 쿼리로 기존 resource 존재 여부 확인.
2. Resource 생성 및 저장 (Lines 55-61)
resource = @model.resources.new kind: params[:kind],
user: current_user,
team: @model.team,
name: params[:name]
begin
unless resource.save
resource.save 시 validation 콜백 → DB INSERT → after_commit 콜백이 순차적으로 실행된다.
3. After-commit: Elasticsearch 인덱싱 (병목 1)
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
as_entity_indexed_json 호출 시 _entity_ancestry가 실행되어 최대 9개의 개별 DB 쿼리 발생:
def _entity_ancestry(cache = nil)
h = {}
_add_ancestor(h, :team, :team_id, cache)
_add_ancestor(h, :workspace, :workspace_id, cache)
_add_facility_ancestor(h, cache)
_add_ancestor(h, :record, :record_id, cache)
_add_ancestor(h, :capture, :capture_id, cache)
_add_ancestor(h, :level, :level_id, cache)
_add_ancestor(h, :bim, :bim_id, cache)
_add_ancestor(h, :review, :review_id, cache)
_add_ancestor(h, :annotation_layer, :annotation_layer_id, cache)
h
end
각 ancestor마다 klass.where(id: ancestor_id).pick(:name) 쿼리가 실행된다 (cache 없이).
4. Serialization: S3 Presigned URL 생성 (병목 2)
def upload_url(revision = nil, **kwags)
self.uploading unless self.uploading? # DB UPDATE (state transition)
self.presigned_upload_url(revision, force: kwags[:force])
end
presigned_upload_url은 AWS S3 Presigner를 통해 동기적으로 signed URL을 생성한다:
def presigned_upload_url(revision, force: false)
signer = Aws::S3::Presigner.new(client: client)
signer.presigned_url(
:put_object,
bucket: bucket_name,
key: object(revision).key,
storage_class: 'ONEZONE_IA',
expires_in: expires_in,
acl: 'bucket-owner-full-control'
)
end
이 AWS API 호출이 응답 경로에서 동기 실행되며, 네트워크 상태에 따라 1000-1500ms 소요 가능.
5. State transition DB UPDATE (병목 3)
self.uploading unless self.uploading?에서 resource의 state를 uploading으로 변경하며 DB UPDATE 실행.
Log Evidence#
Datadog 쿼리:
service:cupixworks-api @http.url_details.path:"/api/v1/pointclouds/1128578/resources" @duration:>3000
핵심 로그 (2026-06-05 UTC):
22:49:32 [200] POST /api/v1/pointclouds/1128578/resources (Api::V1::PointcloudsController#create_resource)
동일 시간대 시스템 부하 확인:
service:cupixworks-api @duration:>3000 (22:48-22:51 UTC)
→ 20+ requests exceeding 3000ms
→ PanosController#bulk, CapturesController#update/index, JobsController 등 다수 엔드포인트 영향
다른 create_resource 지연 (동일 시간대):
22:20:02 - pointcloud 1128488 (>3000ms)
22:20:48 - pointcloud 1128490 (>3000ms)
22:48:06 - pointcloud 1128572 (>3000ms)
22:49:32 - pointcloud 1128578 (>3000ms, 본 인시던트)
S3 region 관련 에러 (인접 시간):
22:53:32 [500] PUT /api/v1/captures/708118
Excon::Error::BadRequest: The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'us-west-2'
Path: /cupix-tesla-static/cupix-tesla-static/uploads/tmp/.../clark-vdc.708118_1366400_pointcloud_thumbnail_2.jpg
이 S3 region 에러는 create_resource의 presigned URL 생성과 동일한 S3 클라이언트 설정 문제를 시사한다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | S3 presigned URL 생성 시 네트워크 지연 (응답 경로 내 동기 AWS API 호출) | storagable/resource.rb:105에서 동기 presigner 호출 확인. 인접 시간(22:53:32)에 S3 region 에러 발생으로 S3 연결 문제 시사. |
직접적인 S3 latency 메트릭은 미확인. | Confirmed |
| H2 | Elasticsearch 인덱싱 시 N+1 ancestry 쿼리 | entity_indexable.rb:108-120에서 cache 없이 최대 9개 개별 쿼리 실행 확인. |
after_commit이므로 HTTP 응답 시간에 직접 포함되는지는 Rails 버전에 따라 다름. | Confirmed |
| H3 | 시스템 전반 부하로 인한 DB 커넥션 풀 경합 | 동시간대 20+ 요청이 3000ms 초과. PanosController#bulk 등 heavy 쿼리 동시 실행. | 에러 로그나 connection timeout 로그는 미확인. | Confirmed (contributing) |
| H4 | 애플리케이션 코드 버그 (무한루프, 잘못된 쿼리 등) | — | HTTP 200 정상 응답, 에러 로그 없음, 일관된 동작 패턴. | Rejected |
| H5 | 외부 서비스 장애 (Elasticsearch/Redis 다운) | — | 다른 요청들도 정상 처리됨, 특정 서비스 에러 로그 없음. | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/models/concerns/storagable/resource.rb:101-103—upload_url메서드에서 S3 presigned URL 생성을 응답 경로에서 제거. Serializer가 URL을 lazy하게 생성하거나, 클라이언트가 별도 엔드포인트로 요청하도록 변경.app/models/concerns/entity_indexable.rb:160—_entity_index_document를 background job(Sidekiq)으로 위임하여 after_commit 콜백이 응답 시간에 영향을 주지 않도록 변경.
단기 개선 (1주 이내)#
_entity_ancestry메서드에서cache파라미터를 활용한 일괄 조회 구현. 현재 미사용 상태인 cache 매커니즘을 활성화하여 9개 개별 쿼리를 1-2개 batch 쿼리로 통합.ResourceSerializer에서upload_url호출 시 state transition(self.uploading)을 제거하고, 별도의 상태 관리 엔드포인트로 분리.
장기 개선 (재발 방지)#
- Serializer에서 I/O 작업(AWS API, DB write)을 금지하는 아키텍처 규칙 수립. Serializer는 읽기 전용으로 제한.
- Elasticsearch 인덱싱을 전면 비동기화 (Sidekiq worker로 위임).
- S3 presigned URL 생성 전용 엔드포인트 분리 (
GET /resources/:id/upload_url).
Monitoring#
create_resourcep95/p99 latency 추적:
service:cupixworks-api resource_name:"Api::V1::PointcloudsController#create_resource" @duration:>2000
- S3 presigner 호출 시간 metric 추가 (Custom instrumentation)
- Elasticsearch 인덱싱 duration metric:
service:cupixworks-api "Entity index error"
- 시스템 전반 slow request 모니터:
service:cupixworks-api @duration:>3000
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- 단일 발생이며 HTTP 200 성공 응답. 데이터 손실 없음. 그러나 동일 패턴이 반복적으로 발생(2시간 내 4건)하고 있어, 부하 증가 시 타임아웃으로 이어질 수 있음.