ES /docs

[429] {"error":{"root_cause":[],"type":"search_phase_execution_exception","reason":"","phase":"fetch

RCA: Elasticsearch circuit_breaking_exception — search_phase_execution_exception (reduce_aggs)

Error Log#

Datadog Logs

json
[429] {"error":{"root_cause":[],"type":"search_phase_execution_exception","reason":"","phase":"fetch","grouped":true,"failed_shards":[],"caused_by":{"type":"circuit_breaking_exception","reason":"[parent] Data too large, data for [<reduce_aggs>] would be [8161790449/7.6gb], which is larger than the limit of [8160437862/7.5gb], real usage: [8161790416/7.6gb], new bytes reserved: [33/33b], usages [request=33/33b, fielddata=2447/2.3kb, in_flight_requests=14262/13.9kb, model_inference=0/0b, eql_sequence=0/0b, accounting=593557456/566mb]","bytes_wanted":8161790449,"bytes_limit":8160437862,"durability":"PERMANENT"}},"status":429}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1 (클러스터 기준, 실제 연쇄 에러 11건)
  • 최초 발생: 2026-04-16T08:32:28.743Z
  • 최근 발생: 2026-04-16T08:32:28.743Z
  • 영향 범위: Admin::EditingRepository, ReviewRepository, LevelRepository, PointcloudRepository, WorkareaRepository 등 Elasticsearch를 사용하는 모든 repository에서 약 2분간 (08:32:28 ~ 08:34:22) 502 Bad Gateway 응답 발생. review qfnbs2 관련 요청 포함 다수의 API 호출 실패.

Root Cause Summary#

GET /api/v1/admin/editings 엔드포인트에서 aggregation을 포함한 Elasticsearch 쿼리가 실행될 때, SearchableControllerMAX_BUCKET_SIZE_OF_AGGREGATIONS = 5000 상수가 aggregation 결과의 최대 개수(bucket 수)를 결정한다. 이 값은 Elasticsearch terms aggregation의 size 파라미터와 composite aggregation의 size 파라미터로 직접 전달되어, 각 aggregation당 최대 5,000개의 고유 값(bucket)을 반환하도록 설정된다. 08:00~08:35 시간대에 이 엔드포인트에 대한 높은 빈도의 폴링(50건 이상)이 발생하면서 Elasticsearch JVM heap 사용량이 7.5GB 한도를 초과했고, reduce_aggs 단계에서 parent circuit breaker가 트리거되어 429 에러가 반환되었다. 이 에러는 BaseRepository#searchStandardError catch 블록에서 Cupix::Errors::BadGateway(code BG10002)로 변환되어 502 응답으로 클라이언트에 전달되었다. 초기 circuit breaker 발동 이후 in-flight 요청들이 누적되면서 메모리 사용량이 7.9GB까지 상승했고, 다른 repository 쿼리들도 연쇄적으로 실패했다.

Technical Analysis#

Code Path#

  1. Entry point: Api::V1::Admin::EditingsController#index (app/controllers/api/v1/admin/editings_controller.rb:12-23)
ruby
# app/controllers/api/v1/admin/editings_controller.rb:12-14
def index
  editing_query_option = Cupix::QueryOption::Editing.new(get_query_option(enable_current_team: false), params)
  editings = repository_instance.search(editing_query_option)

get_query_optionSearchableController에서 제공되며, params[:aggs]가 존재할 경우 aggregation을 쿼리에 추가한다.

  1. Aggregation 파싱: SearchableController#get_parse_aggs (app/controllers/concerns/searchable_controller.rb:55-99)
ruby
# app/controllers/concerns/searchable_controller.rb:5,64-72
MAX_BUCKET_SIZE_OF_AGGREGATIONS = 5000

if ActiveRecord::Type::Boolean.new.cast(params[:composite]) == false
  aggs.each do |agg_field|
    query_option.aggs[agg_field.to_sym] = {
      terms: {
        field: agg_field,
        size: MAX_BUCKET_SIZE_OF_AGGREGATIONS  # 5000 buckets per aggregation
      }
    }
  end

MAX_BUCKET_SIZE_OF_AGGREGATIONS는 aggregation이 반환할 수 있는 최대 개수(bucket 수)를 의미한다. 여러 aggregation 필드가 요청되면 각각 최대 5,000개의 고유 값을 반환하는 terms aggregation이 생성된다. composite 모드에서는 size: 5000의 multi-source composite aggregation(id + name)이 생성되어 메모리 사용량이 더 커진다.

  1. 검색 실행: Admin::EditingRepository#_search (app/repositories/admin/editing_repository.rb:176-181)
ruby
# app/repositories/admin/editing_repository.rb:176-181
response = ::Editing.search(
  self.query_option.serializable_hash
).paginate(
  per_page: self.query_option.per_page,
  page: self.query_option.page
)

Aggregation이 포함된 쿼리가 serializable_hash를 통해 Elasticsearch에 전송된다.

  1. 에러 핸들링: BaseRepository#search (app/repositories/base_repository.rb:83-92)
ruby
# app/repositories/base_repository.rb:83-91
rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
  _message = JSON.parse(e.message.split(/[\d{3}]\s/i)[1]) rescue {}
  _reason = _message['error']['root_cause'][0]['reason'] rescue nil
  raise Cupix::Errors::Argument.new(code: 'ARG13000', reason: _reason)
rescue StandardError => e
  Cupix::Logger.error(e.message.to_s, class: self.class.name, method: __method__)
  raise Cupix::Errors::BadGateway.new(code: 'BG10002', reason: 'Bad Gateway error on Elasticsearch')

429 TooManyRequests 에러는 BadRequest로 잡히지 않고 StandardError catch로 떨어져 BG10002 Bad Gateway로 변환된다. 에러의 원인(circuit breaker)에 대한 구체적 정보가 손실된다.

  1. Elasticsearch 설정: config/initializers/elasticsearch.rb:15-34
ruby
# config/initializers/elasticsearch.rb:15,17-18
ES_DEFAULT_MAX_RESULT_WINDOW = 200000

Elasticsearch::Model.client = ConnectionPool::Wrapper.new(size: 10, timeout: 7) {
  Elasticsearch::Client.new(

Connection pool 크기 10, timeout 7초. 다수의 동시 요청이 각각 큰 aggregation을 실행할 경우 Elasticsearch heap이 빠르게 소진된다.

Log Evidence#

검색 쿼리 1: 초기 에러 검색

text
service:cupixworks-api status:error ("circuit_breaking_exception" OR "Data too large" OR "search_phase_execution_exception")
Time: 2026-04-16 07:30 ~ 09:00 UTC
Results: 11건

08:32:28.743Z — 최초 에러 (Admin::EditingRepository)

json
{
  "error": {
    "type": "search_phase_execution_exception",
    "phase": "fetch",
    "caused_by": {
      "type": "circuit_breaking_exception",
      "reason": "[parent] Data too large, data for [<reduce_aggs>] would be [8161790449/7.6gb], which is larger than the limit of [8160437862/7.5gb]",
      "bytes_wanted": 8161790449,
      "bytes_limit": 8160437862,
      "durability": "PERMANENT"
    }
  },
  "status": 429
}

동시에 <http_request> 대상 circuit breaker도 발동 — accounting=566mb, real usage=7.6gb.

08:32:27.607Z — 502 Bad Gateway 응답

text
GET /api/v1/admin/editings → Cupix::Errors::BadGateway (code: BG10002)

08:34:00 ~ 08:34:08Z — 연쇄 에러 (메모리 상승)

text
08:34:00 ReviewRepository#levels — "Failed to get levels" (7.7gb)
08:34:02 LevelRepository — circuit_breaking_exception (7.7gb)
08:34:06 PointcloudRepository — circuit_breaking_exception (7.8gb → 7.9gb, 4건)
08:34:08 WorkareaRepository — circuit_breaking_exception (7.9gb)

08:34:22Z — ES connection timeout

text
"Operation timed out after 10002 milliseconds" — PointcloudRepository

08:34:35Z — 회복 시작

text
Admin::EditingsController#index → 200 OK

08:35:21Z — ES health check 정상

text
GET /status/elasticsearch → 200 OK

검색 쿼리 2: 트래픽 패턴

text
service:cupixworks-api "Admin::EditingsController" OR "Admin::EditingRepository"
Time: 2026-04-16 08:00 ~ 08:35 UTC
Results: 50건 이상

08:0008:35 시간대에 GET /api/v1/admin/editings 요청이 50건 이상 기록됨. add_reviewer PUT 요청도 editing ID 975503975508에 대해 다수 발생. 이 높은 빈도의 폴링이 aggregation 쿼리의 동시 실행을 유발했다.

검색 쿼리 3: 다른 서비스 영향 확인

text
status:error ("circuit_breaking_exception" OR "Data too large")
Time: 2026-04-16 07:30 ~ 09:00 UTC
Results: 11건 (cupixworks-api만 해당)

cupixworks-worker 등 다른 서비스에는 영향 없음 확인.

Fix Recommendation#

즉시 조치 (Critical)#

  • app/controllers/concerns/searchable_controller.rb:5MAX_BUCKET_SIZE_OF_AGGREGATIONS 값을 5000에서 500 이하로 줄여야 한다. 이 상수는 aggregation 결과의 최대 개수(Elasticsearch terms/composite aggregation의 size 파라미터)를 제어한다. Admin editings 페이지에서 실제로 5,000개의 고유 값이 필요한지 검토하고, UI에서 사용하는 실제 개수에 맞춰 적절한 값으로 조정한다.
  • app/repositories/base_repository.rb:83-92Elasticsearch::Transport::Transport::Errors::TooManyRequests (429)를 별도로 catch하여 circuit breaker 에러임을 명확히 로깅하고, 적절한 에러 코드로 클라이언트에 반환한다. 현재는 StandardError로 떨어져 원인 정보가 손실된다.

단기 개선 (1주 이내)#

  • Admin editings index 엔드포인트에서 aggregation 요청 시 bucket 수를 쿼리 파라미터로 제한하는 상한선(예: per-endpoint max)을 추가한다. 특히 composite aggregation은 multi-source로 메모리 사용량이 배로 늘어나므로 별도 상한이 필요하다.
  • 높은 빈도의 폴링을 방지하기 위해 admin editings 리스트 API에 rate limiting을 적용하거나, 클라이언트측 폴링 간격을 늘린다.

장기 개선 (재발 방지)#

  • Elasticsearch cluster의 JVM heap 크기(현재 7.5GB 한도)가 데이터 규모에 비해 충분한지 검토하고, 필요시 heap 크기를 증가하거나 노드를 추가한다.
  • Aggregation이 필요한 엔드포인트에 대해 캐싱 레이어를 도입하여 동일한 aggregation 쿼리의 반복 실행을 줄인다.
  • Elasticsearch 쿼리에 request_cache: true 옵션을 활용하여 aggregation 결과를 ES 레벨에서 캐싱한다.

Monitoring#

  • Elasticsearch circuit breaker 발동 횟수 모니터링:
text
service:cupixworks-api status:error "circuit_breaking_exception"
  • Elasticsearch JVM heap 사용률 메트릭에 대한 알림 (80% 이상 시 경고, 90% 이상 시 위험):
text
avg:elasticsearch.jvm.mem.heap_used_percent{*} > 80
  • Admin editings 엔드포인트의 요청 빈도 모니터링:
text
service:cupixworks-api @http.url:"/api/v1/admin/editings" @http.method:GET

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard
  • 이 에러는 약 2분간의 일시적 장애를 유발했으며 ES가 자동 회복되었다. 그러나 동일 패턴(높은 빈도의 aggregation 폴링)이 반복되면 더 긴 시간의 장애로 이어질 수 있다. MAX_BUCKET_SIZE_OF_AGGREGATIONS 축소가 가장 효과적인 즉시 조치이다.

Revision History#

Revision 1 — 2026-04-16#

피드백: "MAX_BUCKET_SIZE_OF_AGGREGATIONS 이 최대개수랑 관련있는거야"

분류: 사실 확인 질문 — 상수의 역할이 "최대개수"와 관련있는지 명확한 설명 요청

판정: 수용

근거: 코드 확인 결과, MAX_BUCKET_SIZE_OF_AGGREGATIONS = 5000 (searchable_controller.rb:5)은 Elasticsearch aggregation의 최대 개수(bucket 수)를 직접 제어하는 상수이다:

  • terms aggregation (searchable_controller.rb:69): size: MAX_BUCKET_SIZE_OF_AGGREGATIONS → 최대 5,000개의 고유 값 반환
  • composite aggregation (searchable_controller.rb:77): size: MAX_BUCKET_SIZE_OF_AGGREGATIONS → 최대 5,000개의 composite bucket 반환

기존 RCA에서 "최대 5,000개 bucket의 terms 또는 composite aggregation이 생성된다"라고 기술했으나, 이 상수가 곧 "최대개수"를 의미한다는 점이 충분히 명확하지 않았다. Root Cause Summary, Technical Analysis, Fix Recommendation에서 MAX_BUCKET_SIZE_OF_AGGREGATIONS가 aggregation 결과의 최대 개수를 제어한다는 점을 명시적으로 보강했다.

변경 내용:

  • Root Cause Summary: "최대 개수(bucket 수)를 결정한다" 문구 추가, size 파라미터와의 관계 명시
  • Technical Analysis §2: "최대 개수(bucket 수)를 의미한다" 설명 추가
  • Fix Recommendation: "aggregation 결과의 최대 개수" 설명 추가