ES /docs

Api::V1::PointcloudsController#entity_parameters (avg 33888ms, max 33888ms)

RCA: Api::V1::PointcloudsController#entity_parameters (avg 33888ms, max 33888ms)

Overview#

What Happened#

2026-07-16 19:53 KST 프로덕션 cupixworks-api 에서 GET /api/v1/pointclouds/1224014/entity_parameters 요청 한 건이 약 33.9초 만에 200 응답으로 완료됐다. 동일 endpoint 의 정상 응답은 서비스 전체 p99 (~1.5s) 를 크게 넘지 않았고, 이번 outlier 는 새로 업로드된 pointcloud(id=1224014, facility=15950)의 상승 요청 흐름 중 처음 호출된 entity_parameters 요청에서만 관측됐다.

Quick Facts#

Field Value
resource_name Api::V1::PointcloudsController#entity_parameters
service cupixworks-api
region us-west-2
tenant cupix
avg_duration_ms 33888
max_duration_ms 33888
sample_trace_id 1353218467795904081
pointcloud.id (from logs) 1224014 (facility_id 15950)

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (tenant: cupix) 1 특정 pointcloud 업로드 완료 시점의 클라이언트 요청 1건 지연 (200 OK 응답, 실패 아님)

Timeline#

  1. 2026-07-16 19:50:45 KST — pointcloud 1224014 생성 (facility 15950, user 37878). Cachable::ReviewLoad cache 무효화.
  2. 2026-07-16 19:50:51 KST — pointcloud state initializing → queued.
  3. 2026-07-16 19:53:08 KSTGET /api/v1/pointclouds/1224014/entity_parameters 요청 시작 (span first_seen).
  4. 2026-07-16 19:53:42 KST — 동 요청 [200] 응답 로그 기록. 총 33888ms.
  5. 2026-07-16 19:54:43 KST — pointcloud state queued → done, publish_on_finish 트리거.

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::PointcloudsController#entity_parameters",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 33888,
  "max_ms": 33888,
  "sample_trace_id": "1353218467795904081"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-16 19:53 KST
  • 최근 발생: 2026-07-16 19:53 KST

정상 응답(HTTP 200)이며 유저 노출 에러는 없다. 다만 클라이언트 관점에서 30초 이상 응답 지연은 UX 저하 및 upstream(ALB/LB idle-timeout, 클라이언트 재시도) 리스크가 있다. 동일 요청 패턴은 pointcloud 업로드 완료 직후 반복 호출되는 workflow 의 일부라 재발 시 blast radius 는 클 수 있다.

Root Cause Summary#

Api::V1::PointcloudsController#entity_parametersEntityParameterableController#entity_parameters (app/controllers/concerns/entity_parameterable_controller.rb:16-29) 로 위임되며, 최종적으로 Pointcloud 인스턴스의 EntityParameterable#entity_parameters (app/models/concerns/entity_parameterable.rb:15-33) 를 호출한다. 이 메서드는 self → parent_model 체인 (Pointcloud → Record → Facility → Workspace → Team, applied_entity_parameter? 를 리턴하는 모델만) 을 재귀적으로 순회하며 각 단계에서 EntityParameter.eager_load(:entity_parameter_group).where(...).where.not(name: ...) 쿼리를 별도로 실행한다. 이번 outlier 는 이 chained lookup 이 pointcloud 1224014 (facility 15950) 컨텍스트에서 유일하게 33.8s 로 소요됐으며, 동시간대 (10:52:30–10:54:00 UTC) 서비스 p99 는 1.5s 수준, MySQL/RDS CPU 는 정상(~17-23%)이었다. 즉 광범위 인프라 장애가 아니라 이 요청 개별의 slow-path (parent chain 순회 × N+1 형태의 eager_load 쿼리 + where.not(name: [...]) 배열 확장) 로 인한 request-level outlier 로 판단된다. 단, 33.8s 는 순수 쿼리 시간만으로는 재현되기 어려운 크기이므로 그 순간의 GC pause / connection pool acquisition / 순간적 DB slow query 요인이 겹쳤을 가능성이 남는다 (uncertain — needs verification via APM trace flame graph on trace_id 1353218467795904081).

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/pointclouds_controller.rb:1
  • Concern dispatch: app/controllers/concerns/entity_parameterable_controller.rb:16
  • Repository dispatch: app/repositories/entity_parameterable_repository.rb:2
  • Traversal hotspot: app/models/concerns/entity_parameterable.rb:15
  • Failure point (latency hotspot): app/models/concerns/entity_parameterable.rb:19-22 (parent-chain iteration)

Controller entry:

app/controllers/concerns/entity_parameterable_controller.rb:16-29ruby
def entity_parameters
  _results = repository_instance.entity_parameters(params)

  render_api Renderable.new({
    contents: _results,
    is_collection: true,
    serializer: EntityParameterSerializer,
    serializer_option: {
      fields: {
        entity_parameter: @fields
      }
    }
  })
end

Repository looks up the group entity (a Pointcloud in this case) via repository_class.show(id) and delegates to the model:

app/repositories/entity_parameterable_repository.rb:2-18ruby
def entity_parameters(params = {})
  _id_or_key = params[:id] || params[:key]
  if _id_or_key.present?
    if self.class.current_class == ::Review
      review = ::Review.find_by(key: _id_or_key) || (raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid id or key'))
      _group_entity = review.facility
    else
      _group_entity = self.class.current_class.repository_class.new(current_user: current_user, current_team: current_team).show(_id_or_key)
    end
  elsif self.class.current_class == ::Team
    _group_entity = current_team
  else
    raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid group entity')
  end

  _group_entity.entity_parameters
end

The concern that produces the parameter list:

app/models/concerns/entity_parameterable.rb:11-33ruby
def entity_parameter
  EntityParameter.eager_load(:entity_parameter_group).where(entity_parameter_groups: { entity: self })
end

def entity_parameters
  _entity_params = entity_parameter

  parent = self.class.parent_model if self.class.respond_to?(:parent_model)
  entity_parameter_models(parent).each do |model_class|
    model_name = model_class.name.downcase
    _entity_params += self.send(model_name).entity_parameter.where.not(name: _entity_params.map(&:name))
  end

  _system_params = system_entity_parameter.where.not(
    name: _entity_params.map(&:name)
  )

  _default_params = default_entity_parameter.where.not(
    name: _entity_params.map(&:name) + _system_params.map(&:name)
  )

  _entity_params + _system_params + _default_params
end

Parent chain resolution (recursive collection of ancestors that respond applied_entity_parameter? == true):

app/models/concerns/entity_parameterable.rb:41-48ruby
def entity_parameter_models(current_model_class)
  return [] if current_model_class.nil?
  return [current_model_class] if current_model_class == ::Team

  _current_model_class = current_model_class.respond_to?(:applied_entity_parameter?) && current_model_class.applied_entity_parameter? ? [current_model_class] : []
  current_model_class = current_model_class.parent_model if current_model_class.respond_to?(:parent_model)
  _current_model_class + entity_parameter_models(current_model_class)
end

Pointcloud 의 조상 체인은 Pointcloud → Record (applied) → Facility (applied) → Workspace → Team (applied) 로 정의된다:

app/models/concerns/record_entity.rb:22-24ruby
def self.parent_model
  ::Record
end
app/models/concerns/facility_entity.rb:77-79ruby
def self.parent_model
  ::Facility
end
app/models/concerns/workspace_entity.rb:22-24ruby
def self.parent_model
  ::Workspace
end
app/models/concerns/entity_parameterable/record.rb:5-11ruby
included do
  include EntityParameterable
  def self.applied_entity_parameter?
    true
  end
end

기대 동작: 각 반복은 이미 획득한 이름을 WHERE name NOT IN (...) 로 제외하므로, entity_parameter_group_idname 인덱스가 존재하는 상태에서 각 SQL 은 O(수십ms) 수준으로 완료되고 총합도 수백ms 이내여야 한다. 동시간대 다른 pointcloud 들의 동일 요청은 초 단위 이내로 완료됨(19:53:42 이후 19:54:11, 19:54:14, 19:54:50 등 로그 참조).

실제 동작: pointcloud 1224014 요청은 33888ms 소요. 정상 요청 대비 30배 이상. where.not(name: [...]) 의 IN 리스트는 각 iteration 마다 _entity_params.map(&:name) 을 다시 계산해 넘기므로 배열이 커지면 파라미터 바인딩과 planner 비용이 증가한다 (단, 지속적으로 slow 하지 않은 것을 보면 이 자체만으로 33s 를 유발했다고 단정하기 어렵다 — GC / conn-pool acquisition / 특정 순간의 DB slow lookup 이 겹쳤을 가능성이 큼).

Log Evidence#

Datadog 쿼리 (사용):

text
service:cupixworks-api "PointcloudsController" "entity_parameters"
text
service:cupixworks-api "1224014"

동일 pointcloud (1224014) 흐름의 관련 로그 (KST):

text
2026-07-16 19:50:45  info  Cachable::ReviewLoad | Invalidated facility review cache on create | model=Pointcloud | model_id=1224014 | facility_id=15950
2026-07-16 19:50:51  info  pointcloud state changed from initializing to queued. id: 1224014 (StateMachines::Machine)
2026-07-16 19:53:04  info  [200] PUT /api/v1/pointclouds/1224014 (Api::V1::PointcloudsController#update)
2026-07-16 19:53:04  info  [302] GET /api/v1/pointclouds/1224014/download (Api::V1::PointcloudsController#download_single_resource)
2026-07-16 19:53:42  info  [200] GET /api/v1/pointclouds/1224014/entity_parameters (Api::V1::PointcloudsController#entity_parameters)
2026-07-16 19:54:43  info  pointcloud state changed from queued to done. id: 1224014

정상 응답 대비(같은 endpoint, 동일 시간대):

text
2026-07-16 19:53:42  [200] .../1224014/entity_parameters      (요청 시작 10:53:08 UTC → 총 33888ms)
2026-07-16 19:54:11  [200] .../1224016/entity_parameters      (초 단위 이내 완료)
2026-07-16 19:54:14  [200] .../1224018/entity_parameters      (초 단위 이내 완료)
2026-07-16 19:54:50  [200] .../1224024/entity_parameters      (초 단위 이내 완료)

동시간대 서비스 전체 지표 (동 시각 근접):

text
p99:trace.rack.request{service:cupixworks-api}  ≈ 0.9 ~ 1.6s
max:aws.rds.cpuutilization{*}                    ≈ 17-23%

ThrottlingException / AccessDenied 에러 로그가 동시간대 다수 존재하나 이는 별도 소스(CloudWatch Logs FilterLogEvents 권한/쓰로틀 — arn:aws:logs:...) 로, entity_parameters request path 와는 무관하다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Request-level outlier: parent-chain 순회 (Pointcloud→Record→Facility→Workspace→Team) 를 실행하는 entity_parameters 가 단발적인 slow-path (GC / connection pool acquisition / DB 순간 slow lookup) 와 겹쳐 33.8s 소요 entity_parameterable.rb:15-33 은 iteration 마다 .map(&:name) 배열 확장 + where.not(name: [...]) 쿼리를 발행. 동일 요청 log 상 33.8s 로 정상 200 완료. 서비스 p99 는 1.6s 수준으로 이 요청만 개별 outlier APM flame graph 미확인 → 어느 스팬에서 시간이 소진됐는지 정확한 확증 없음 Confirmed (with uncertainty on exact sub-cause)
H2 DB / RDS 성능 저하로 인한 전체 서비스 지연 entity_parameters endpoint 가 늦음 RDS CPU 17-23% 정상, MySQL slow log 없음, 동시간대 다른 endpoint 및 다른 pointcloud 의 entity_parameters 는 초 단위로 정상 완료 Rejected
H3 ThrottlingException (AWS) 이 direct cause 동시간대 ThrottlingException / AccessDenied 다수 관측 해당 에러들은 CloudWatch Logs (logs:FilterLogEvents, arn:aws:logs:ap-southeast-2:.../cupix-navigate-production-thk7) 관련이며 tenant/region 및 code path (entity_parameters) 와 무관 Rejected
H4 특정 응용 로직 (upload / state transition) 이 entity_parameters 를 블로킹 pointcloud 1224014 는 요청 직전 (19:53:04) 에 PUT update, 이후 (19:54:43) state queued→done entity_parameters 자체는 read-only 쿼리로 상태 전이 락을 잡지 않음. Ruby 소스 상 lock/transaction 사용 흔적 없음 Rejected
H5 예외 발생(ARG10001 등) 후 재시도 루프 entity_parameterable_repository.rb:6,14Cupix::Errors::Parameter raise 존재 최종 응답 [200] OK. 에러 로그 없음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • APM trace 1353218467795904081 의 flame graph 를 열어 33.8s 가 어떤 span (SQL / GC / connection wait) 에 소진됐는지 확증한다. 이 데이터 없이는 (H1) 의 sub-cause 를 특정할 수 없음. Datadog APM UI 에서 trace_id 검색 필요.
  • 재발 여부 모니터링: 아래 Monitoring 섹션의 쿼리를 대시보드/모니터에 추가한다. 단발성이 아니라 반복 outlier 라면 코드 개선 우선순위 상승.

단기 개선 (1주 이내)#

  • app/models/concerns/entity_parameterable.rb:15-33 의 iteration 을 리팩터하여, parent chain 순회 결과를 단일 join 쿼리(또는 entity_parameter_group_id 목록에 대한 IN 쿼리 한 번)로 대체하는 방향을 검토한다. 현재 구조는:
    • 각 iteration 마다 _entity_params.map(&:name) 을 Ruby-side 로 재계산
    • 각 iteration 마다 EntityParameter.eager_load(:entity_parameter_group).where(entity_parameter_groups: { entity: X }).where.not(name: [...]) SQL 발행 이 patterns 은 chain 이 깊을수록 N+1 유사한 read 부하를 만든다. 목표: Pointcloud → Team 까지의 조회를 1-2 회 SQL 로 축약.
  • where.not(name: _entity_params.map(&:name) + _system_params.map(&:name)) 처럼 큰 IN 리스트가 발생하는 경우, MySQL 쿼리 플래너가 range scan 대신 full scan 을 택할 수 있다. entity_parameters.name 인덱스는 이미 존재(index_entity_parameters_on_namedb/schema.rb:1851)하나 selectivity 확인 필요.

장기 개선 (재발 방지)#

  • entity_parameter 조회 결과를 Rails cache 로 감싸는 것을 검토. Cachable::ReviewLoad 처럼 create/update 시 invalidate 하는 훅을 사용하면 동일 facility 의 반복 조회를 상수 시간에 서비스할 수 있다 (특히 pointcloud 업로드 완료 flow 처럼 짧은 시간에 같은 chain 을 다수 호출하는 경우 효과가 큼).
  • APM 상 이 endpoint 에 대한 SLO (예: p99 < 2s) 를 정의하고 위반 시 알림을 걸어 outlier 를 개별 트래킹.

Monitoring#

  • 이 endpoint 의 outlier 를 상시 감시하고 위 request-level slow-path 재발 여부를 확인.
text
service:cupixworks-api resource_name:"Api::V1::PointcloudsController#entity_parameters" @duration:>5s
text
service:cupixworks-api "PointcloudsController#entity_parameters"

APM 지표 (release dashboard timeseries widget 용):

text
avg:trace.rack.request{service:cupixworks-api}
text
p99:trace.rack.request{service:cupixworks-api}
text
max:aws.rds.cpuutilization{*}

Risk Assessment#

  • Risk level: low (단일 발생, 정상 200 응답, 유저 노출 에러 없음)
  • 예상 복잡도: standard (short-term: SQL 리팩터, long-term: cache 도입)