ES /docs

Api::V1::PanosController#upload_url (avg 23576ms, max 23576ms)

RCA: Api::V1::PanosController#upload_url latency spike (23.5s)

Overview#

What Happened#

2026-07-09 01:46 KST에 cupixworks-api 서비스에서 Api::V1::PanosController#upload_url 요청 1건이 23,576ms (평상시 avg 대비 약 100배)로 완료되었다. 요청 자체는 HTTP 200을 반환했으나 latency 임계값을 초과하여 error-sweeper의 latency cluster로 수집되었다. 동일 인스턴스에서 7분 전에는 SitetracksController#captures가 13.2초로 별도 cluster(e49dced0)로 감지되어 status board가 두 cluster를 하나의 svc:cupixworks-api::unknown 인시던트(2026-07-08-svc-cupixworks-api--unknown-2)로 묶었다.

Quick Facts#

Field Value
resource_name Api::V1::PanosController#upload_url
top_frame app/controllers/api/v1/panos_controller.rb:86
avg_duration_ms 23576
max_duration_ms 23576
occurrence_count 1
env production, us-west-2
tenant cupix
sample_trace_id 1527333471675385253

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (Pano upload flow) 1 업로드 클라이언트 1회 요청이 약 24초 대기. HTTP 200으로 성공했으므로 데이터 유실 없음. 사용자 체감 지연 가능성 있음.

Timeline#

  1. 2026-07-09 01:39 KST — 페어 cluster e49dced0 발생: SitetracksController#captures 13,190ms (동일 인시던트의 첫 이벤트)
  2. 2026-07-09 01:46:13 KST — 본 cluster 발생: PanosController#upload_url 23,576ms (trace_id 1527333471675385253)
  3. 2026-07-09 01:46:37 ~ 01:47:59 KST — 동일 endpoint로 정상 요청 27건 모두 HTTP 200 (Datadog logs 확인)
  4. 2026-07-09 01:46:13 KST — status board가 인시던트를 resolved로 마감 (이후 재발 없음)

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::PanosController#upload_url",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 23576,
  "max_ms": 23576,
  "sample_trace_id": "1527333471675385253"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-09 01:46 KST
  • 최근 발생: 2026-07-09 01:46 KST

Root Cause Summary#

Endpoint 본연의 로직은 매우 가볍다 — set_pano로 pano 1건 조회 후 resource_uploadable? 상태 확인과 파라미터 검증만 수행한다 (PanoRepository#upload_url, pano_repository.rb:436-444). 24시간 동안의 avg latency는 100-400ms 범위이며 max도 대부분 1.5s 이하다. 23.5초라는 값은 이 코드 경로의 정상 실행 시간으로 설명할 수 없다.

같은 시간대 동일 서비스에서 SitetracksController#captures도 13.2초로 지연되었고, error-sweeper의 status board는 두 이벤트를 svc:cupixworks-api::unknown scope의 동일 인시던트(2026-07-08-svc-cupixworks-api--unknown-2)로 그룹핑했다. 서로 다른 controller가 동시에 지연됐다는 사실은 특정 endpoint의 코드 결함이 아니라 인프라/공유 리소스 레벨의 일시적 저하 (DB connection pool 포화, RDS I/O spike, application server GC pause, 또는 노드 성능 저하 등)로 인한 tail latency로 해석하는 것이 가장 정합적이다. 단일 샘플이고 error/warn 로그가 동반되지 않아 root cause를 코드 수준에서 특정할 수 있는 증거는 없다 (uncertain — needs verification).

Technical Analysis#

Code Path#

Entry point: app/controllers/api/v1/panos_controller.rb:86

Controller 진입 시 before_action :set_pano가 먼저 실행되어 pano를 조회한다.

app/controllers/api/v1/panos_controller.rb:8ruby
before_action :set_pano, except: %i[index create bulk_update upload_candidates untrash purge show bulk mock nearest]
app/controllers/api/v1/panos_controller.rb:141-143ruby
def set_pano
  @model = repository_instance.show(params[:id])
end

PanoRepository#showBaseRepository.show로 위임되어 permission join + id로 단일 레코드를 조회한다.

app/repositories/base_repository.rb:306-343ruby
def self.show(key_or_id_or_model, current_user: nil, ...)
  # ...
  query =
    if skip_permission || current_user == ::User.unauthorized_user
      where(attrs)
    elsif current_class == ::Review || (review_id || capture_id).present?
      permission_joins(default_joins(current_class), current_user, review_id: review_id || -1, capture_id: capture_id || -1).where(attrs)
    elsif current_user.present?
      permission_joins(default_joins(current_class), current_user).where(attrs)
    else
      raise Cupix::Errors::System.new(code: 'SYS30000', reason: 'current_user or review is required on Repository')
    end

  scope = current_class.visibility_scope(visibility)
  model = query.merge(scope).first

이후 controller action 본문 자체는 상태 확인과 파라미터 검증만 수행한다.

app/controllers/api/v1/panos_controller.rb:86-93ruby
def upload_url
  pano = repository_instance.upload_url(params.permit(:revision_type))

  render_api Renderable.new({
    contents: pano,
    serializer_option: @serializer_option
  })
end
app/repositories/pano_repository.rb:436-444ruby
def upload_url(params)
  @model.uploading_resource_state if @model.resource_uploadable?

  if params[:revision_type].present? && !%w[enhanced_image].include?(params[:revision_type])
    raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: "Invalid revision_type: #{params[:revision_type]}")
  end

  @model
end

resource_uploadable?는 in-memory boolean check + S3 head 요청 1건 (resource.object(revision + 1).exists?)이다.

app/models/concerns/statable/pano.rb:222-228ruby
def resource_uploadable?
  return true if revision.zero?
  return true if resource_state_uploaded? && !resource.object(revision + 1).exists? && revision == 1
  return true if tile_state_uploaded?

  false
end

Failure point: 특정 지점을 코드 상에서 확정할 수 없다. Endpoint의 정상 실행 시간(수백 ms)과 관측된 23.5s 사이의 차이는 아래 경로 중 하나에서 stall이 발생했음을 시사하지만, trace-level span 데이터 없이는 확정 불가하다.

  • set_panoBaseRepository.show → permission join SQL (RDS 지연 시 지연 확산)
  • resource_uploadable? → S3 HeadObject (S3 지연 시 지연 확산)
  • Rack/Middleware 레벨 GC pause 또는 Puma worker starvation

Log Evidence#

Datadog logs query (본 endpoint에 대한 트래픽 확인):

text
service:cupixworks-api "PanosController" "upload_url"

인시던트 창(2026-07-08T16:44:00Z ~ 16:48:00Z) 동안 30개 요청 모두 HTTP 200 성공. 이 endpoint에 error/warn 로그는 없었다. 대표 정상 요청 로그:

text
{
  "timestamp": "2026-07-09 01:46:37 KST",
  "status": "info",
  "message": "[200] POST /api/v1/panos/92440139/upload_url (Api::V1::PanosController#upload_url)"
}

같은 창의 warn 로그는 별개의 Elasticsearch sync 관련 항목(무관):

json
{
  "timestamp": "2026-07-09 01:49:59 KST",
  "status": "warn",
  "message": "NotFound - attributes_in_database",
  "class": "Pano",
  "function": "_update_document"
}

에러 로그 조회 결과 (0건):

text
service:cupixworks-api status:error
Time range: 2026-07-08T16:39:00Z ~ 2026-07-08T16:50:00Z
Result: 0 logs

24h avg latency 프로파일 metric:

text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::panoscontroller_upload_url}

수치 (24h, 5-min buckets):

text
정상 창 avg: 0.08 ~ 0.40s
spike 이전 15분 avg: 0.24s
spike 발생 시간버킷 avg: 4.34s   ← 본 클러스터의 23.5s 이벤트가 반영됨

Host CPU (avg:system.cpu.user{service:cupixworks-api})는 인시던트 창 전후로 8-15% 수준으로 특이 스파이크 없음.

Status board 결과 (bun run cli/incident-board.ts for-cluster 98a16a9f-1206-4abe-a925-a6a31d1fb5ee):

json
{
  "scope": "svc:cupixworks-api::unknown",
  "active": null,
  "recent": [{
    "id": "2026-07-08-svc-cupixworks-api--unknown-2",
    "title": "cupixworks-api service degraded",
    "status": "resolved",
    "started_at": "2026-07-08T16:39:16.005Z",
    "resolved_at": "2026-07-08T16:46:13.574Z",
    "cluster_ids": [
      "e49dced0-5cfe-4d49-a823-9abb88cbcd67",
      "98a16a9f-1206-4abe-a925-a6a31d1fb5ee"
    ]
  }]
}

페어 cluster e49dced0는 별도 controller(SitetracksController#captures)이며 13.2s 지연. 두 endpoint의 코드 경로는 겹치지 않는다 → 공유 인프라 요인 정황.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 일시적 인프라/공유 리소스 저하 (DB pool 포화, RDS I/O spike, GC pause 등)로 인한 tail latency 같은 시간창에 서로 다른 controller (SitetracksController#captures)도 13s 지연됨. Status board가 두 cluster를 동일 인시던트로 그룹핑. Endpoint 코드는 O(1)이며 정상 avg가 100-400ms. 단발성 이벤트 후 즉시 정상화. Host CPU (system.cpu.user)는 5-15% 정도로 낮아 인프라 지표에 명확한 스파이크는 확인되지 않음. Root cause를 어느 리소스로 특정할 증거 부족. Inconclusive (가장 유력)
H2 Api::V1::PanosController#upload_url 코드 경로의 N+1/비효율 쿼리 같은 endpoint로 인시던트 창 전후 27건이 100~500ms로 정상 처리됨(Datadog logs). 코드 경로가 단일 record 조회 + 상태 확인 + S3 head 1건으로 단순. Latency profile이 안정적. Rejected
H3 특정 pano ID의 데이터 이슈 (거대한 associated 데이터, 큰 permission graph 등) 요청은 HTTP 200 반환. 재발 없음(occurrence_count=1). 동일 pano에 대한 span 상세가 Datadog에 없어 실제 pano_id 확인 불가. Inconclusive
H4 S3 (us-west-2) 일시적 latency spike Endpoint가 resource.object(...).exists? (S3 HeadObject)를 호출함. SitetracksController#captures는 S3를 직접 호출하지 않음 (DB heavy). 두 controller 동시 지연을 S3만으로 설명하기 어려움. Rejected
H5 특정 배포로 인한 회귀 재발 없음. 다른 latency cluster들이 이후 발생하지 않음. Spike가 단일 시점 후 즉시 해소. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 단일 tail-latency 이벤트이며 코드 수준 회귀 근거 없음. 사용자에게는 HTTP 200으로 성공했다. 코드 변경 불필요.

단기 개선 (1주 이내)#

  • 관측성 강화: PanosController#upload_url 및 유사 endpoint의 slow trace (>1s) 에 대해 set_pano 하위 SQL 및 S3 call span이 Datadog APM에 확실히 기록되도록 확인. 재현 시 원인 특정을 가능하게 하기 위함.
  • 인프라 상관관계 확인: 2026-07-08T16:39Z ~ 16:47Z 창의 RDS WriteLatency, ReadLatency, DatabaseConnections, EC2 host system.load.norm.1, Puma queue depth 를 확인해 tail latency의 upstream 원인을 후속 조사한다. 본 RCA 창에서는 명시적 스파이크 미확인, 재조사 필요.

장기 개선 (재발 방지)#

  • Latency cluster 그룹핑 개선 검토: 서로 다른 controller의 tail latency가 같은 인시던트로 묶이는 지금의 status board 동작은 유용하다. 여기에 인프라 signal(RDS latency, host CPU)과 자동 상관하는 hook을 추가하면 svc:*::unknown scope 사건을 dep:rds-...로 자동 승격 가능하다.
  • S3 HeadObject 타임아웃 명시: resource.object(...).exists? (statable/pano.rb:224)는 기본 SDK 타임아웃에 의존한다. S3 tail latency로 인한 request-level stall을 방지하려면 short read timeout + retry를 명시하는 것이 안전하다.

Monitoring#

Release dashboard에 추가할 timeseries widget:

text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::panoscontroller_upload_url}
text
max:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::panoscontroller_upload_url}
text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::panoscontroller_upload_url}.as_count()

동일 인시던트에 묶인 다른 controller와 함께 tail latency를 관찰하기 위한 상관 지표:

text
max:trace.rack.request.duration{service:cupixworks-api} by {resource_name}

Risk Assessment#

  • Risk level: low (단일 이벤트, 자연 해소, 데이터 유실 없음)
  • 예상 복잡도: trivial (코드 변경 없음; 관측성/모니터링만 강화 권장)