Skip to content
Published on

네이티브 히스토그램은 stable이 됐는데, 왜 아직 못 켜고 있나

공유하기
Authors

들어가며 — "stable"이라는 단어가 실제로 바꾼 것

Prometheus 네이티브 히스토그램은 오래 기다린 기능입니다. 공식 명세의 첫 문장이 이력을 요약합니다 — 2022년 11월 실험 기능으로 도입됐고, 이를 지원한 첫 서버 버전은 v2.40.0이며, --enable-feature=native-histograms 플래그로 켜야 했습니다. 그리고 같은 문단에 이런 문장이 있습니다.

Starting with v3.8.0, native histograms are supported as a stable feature.

3.8.0은 2025년 12월 2일에 릴리스됐습니다(GitHub 릴리스 기준, CHANGELOG 헤더 표기는 11월 28일). 3년 넘게 실험 딱지를 달고 있던 기능이 드디어 stable이 된 겁니다.

그런데 이 글을 쓰는 시점에서 더 중요한 사건은 따로 있습니다. 3.13이 2026년 7월 1일에 나왔고, 이게 새 LTS입니다. 그리고 지금 많은 조직이 돌리고 있을 3.5 LTS는 2026년 7월 31일에 지원이 끝납니다. 2주 뒤입니다. 릴리스 사이클 문서의 LTS 표를 그대로 옮기면 이렇습니다.

Release          Date          End of support   Status
Prometheus 2.53  2024-06-16    2025-07-31       End of life
Prometheus 3.5   2025-07-14    2026-07-31       Supported
Prometheus 3.13  2026-07-01    2027-07-31       Supported
TBD              2027-06       2028-07-31       Upcoming

두 사실을 겹치면 결론이 하나 나옵니다. 3.5 LTS(2025년 7월)는 네이티브 히스토그램이 stable이 되기 넉 달 전에 잘린 버전입니다. 그리고 LTS 문서에는 지원 범위에서 빠지는 항목이 명시돼 있습니다 — API 안정성 보장에서 unstable로 분류된 것들, 실험적 기능, 그리고 OpenBSD 지원. 즉 3.5 LTS를 타고 있었다면 네이티브 히스토그램은 애초에 LTS 지원 범위 밖의 실험 기능이었고, 3.13은 이 기능이 stable 상태로 들어간 첫 LTS입니다. LTS 트랙을 타는 조직에게는 지금이 "언젠가 보자"가 아니라 실제 결정 시점인 이유입니다.

클래식 히스토그램이 만드는 시계열의 개수

왜 이 기능을 원했는지부터 짚고 가겠습니다. 문제는 카디널리티입니다.

클래식 히스토그램은 노출 포맷에는 히스토그램처럼 생겼지만, 수집되는 순간 float 시계열 여러 개로 쪼개집니다. 명세는 이 분해를 이렇게 설명합니다 — summary와 (클래식 버전의) histogram은 수집 시 float 구성요소로 분해되고, 두 타입 모두 sum과 count 구성요소를 가지며, histogram은 여기에 다수의 bucket 샘플이 붙습니다.

실제로 어떻게 되는지는 명세가 든 예가 정확합니다. rpc_latency_seconds라는 히스토그램 하나는 클래식으로 수집하면 rpc_latency_seconds_sum, rpc_latency_seconds_count, 그리고 서로 다른 le 라벨을 가진 다수의 rpc_latency_seconds_bucket 시계열이 됩니다. 버킷이 N개면 시계열은 N+2개입니다. 네이티브 히스토그램으로 수집하면 이름 그대로 rpc_latency_seconds 시계열 하나입니다.

라벨로 파티셔닝하면 이 차이가 곱해집니다. 명세의 설명을 빌리면, 이미 무거운 클래식 히스토그램에 라벨을 붙여 100개의 히스토그램이 생기면 비용은 100배로 갑니다. 네이티브 히스토그램은 사정이 다릅니다 — 파티셔닝하면 개별 히스토그램들이 각자 더 적은 버킷만 채우는 경향이 있어서(상태 코드 404를 처리한 요청만 담는 히스토그램은 대개 좁은 분포를 가집니다), 전체 채워진 버킷 수는 히스토그램 개수 배수보다 훨씬 작게 늘어납니다. 명세가 "라벨로 파티셔닝하는 것조차 이제 훨씬 감당할 만하다"고 쓴 근거가 이것입니다.

여기서 숫자를 하나 지어내고 싶은 유혹이 있는데, 그러지 않겠습니다. Prometheus 명세와 Grafana Mimir 문서는 네이티브 히스토그램이 "클래식 히스토그램 대비 훨씬 낮은 리소스 비용으로 높은 해상도를 제공한다"고 서술하지만, 이는 벤더/프로젝트 문서의 정성적 서술이지 조건이 명시된 벤치마크 수치가 아닙니다. 절감 폭은 원래 버킷을 몇 개나 쓰고 있었는지, 분포가 얼마나 넓은지에 전적으로 좌우됩니다 — 버킷 5개짜리 히스토그램이라면 얻을 게 별로 없습니다.

스키마 — 버킷 경계가 설정이 아니라 공식인 이유

네이티브 히스토그램의 핵심 발상은 버킷 경계를 사람이 정하지 않고 공식으로 정하는 것입니다. 이걸 정하는 값이 스키마이고, 명세에 따르면 int8 정수로 현재 유효한 값은 -53, 그리고 -4부터 +8까지입니다. -53은 커스텀 버킷 경계(NHCB)용이고, 나머지가 표준 지수 스키마입니다.

표준 스키마 n에서 인덱스 i인 양수 버킷의 경계는 이렇게 계산됩니다(명세는 파이썬 문법으로 표기합니다).

# 상한(포함):  (2**2**-n)**i
# 하한(제외):  (2**2**-n)**(i-1)

여기서 중요한 성질이 나옵니다. 스키마 n은 스키마 n+1의 절반 해상도이고, 따라서 n+1짜리 히스토그램은 이웃 버킷을 병합해 n으로 변환할 수 있습니다. 이게 표준 스키마끼리 서로 병합 가능한 이유이고, 여러 인스턴스의 히스토그램을 합산하는 쿼리가 성립하는 근거입니다. 클래식 히스토그램에서 서로 다른 버킷 경계를 쓰는 두 서비스를 합칠 수 없던 문제가 여기서 사라집니다.

계기(instrumentation) 쪽에서는 스키마를 직접 고르지 않고 성장 인자로 지정합니다. Go 클라이언트의 NativeHistogramBucketFactor가 그것인데, client_golang 소스의 주석이 정확한 값을 알려 줍니다 — 비용과 정확도의 일반적으로 좋은 절충은 1.1이고(각 버킷이 이전 버킷보다 최대 10% 넓음), 이 값은 2의 거듭제곱 구간마다 8개 버킷으로 나뉘는 결과가 됩니다(1과 2 사이에 8개, 2와 4 사이에 8개, 4와 8 사이에 8개…). 실제 인자는 2^(2^-n) 으로 계산되며 n은 -4에서 8 사이 정수 중 지정값 이하로 가장 큰 인자를 주는 값이 선택됩니다. 즉 가능한 최소 인자는 약 1.00271(= 2^(2^-8))입니다.

0 근처는 따로 처리합니다. 지수 버킷은 0에 무한히 가까워지므로, 절댓값이 임계값 이하인 관측은 전부 "제로 버킷"에 모읍니다. Go 클라이언트의 기본 임계값은 DefNativeHistogramZeroThreshold = 2.938735877055719e-39입니다.

그리고 저장은 희소(sparse) 표현입니다 — 비어 있는 버킷은 (거의) 공짜입니다. 이게 "전체 float64 범위를 커버한다"는 말이 파산하지 않는 이유입니다. 커버하는 범위가 넓어도 실제로 채워진 버킷만 비용을 냅니다.

stable이 보장하지 않는 것들

여기서부터가 이 글의 본론입니다. stable 선언은 서버가 이 데이터 타입을 다루는 방식이 v4 전까지 깨지지 않는다는 뜻이지, "이제 그냥 켜면 된다"는 뜻이 아닙니다. 명세 자신이 stable 선언 바로 뒤에 단서를 답니다.

여전히 명시적으로 켜야 합니다. 스크레이프는 scrape_native_histograms 설정으로 활성화해야 하고, Remote-Write 전송은 send_native_histograms로 따로 켜야 합니다. 3.8에서는 기존 피처 플래그가 이 설정을 true로 만드는 효과만 남아 있었고, 3.9부터 플래그는 완전한 no-op이라 설정을 명시해야 합니다. 명세의 괄호가 로드맵을 알려 줍니다 — v4부터는 두 설정 모두 기본값이 true가 됩니다. 그러니까 지금은 "stable이지만 opt-in", v4는 "기본값"인 중간 구간입니다.

노출은 프로토버프로만 됩니다. 이게 실무에서 가장 자주 발목을 잡습니다. 명세는 클래식 Prometheus 텍스트 포맷은 네이티브 히스토그램용으로 확장되지 않았고 그럴 계획도 없다고 못 박습니다. OpenMetrics 텍스트 포맷 지원은 제안 문서가 있지만 구현 상태는 그대로 Not implemented입니다. OpenMetrics 프로토버프 쪽 지원 PR(OpenMetrics#256)은 2026년 3월 19일에 머지되지 않고 닫혔는데, 이유가 흥미롭습니다 — 사실상 브랜딩 정리입니다. Prometheus 프로토버프 노출 포맷이 이미 네이티브 히스토그램과 나머지 기능을 전부 지원하므로 그쪽을 정식 프로토버프 포맷으로 삼고, OpenMetrics는 텍스트 기반 포맷을 뜻하는 것으로 가자는 결정이었습니다. 정리하면 2026년 7월 현재, 네이티브 히스토그램을 노출하는 길은 클래식 Prometheus 프로토버프 포맷 하나뿐입니다.

클라이언트 라이브러리는 아직 따라오지 않았습니다. 이 부분은 직접 확인해 볼 가치가 있습니다. client_golang 최신 릴리스는 v1.23.2(2025년 9월 5일)인데, HistogramOpts의 주석에는 지금도 이 문장이 그대로 있습니다.

NOTE: Native Histograms are still an experimental feature. Their behavior might still change without a major version bump. Subsequently, all NativeHistogram... options here might still change their behavior or name (or might completely disappear) without a major version bump.

v1.23.2는 3.8.0(2025년 12월)보다 먼저 나온 릴리스이니 문구가 낡은 것이라고 볼 수 있고, 실제로 main 브랜치에도 아직 같은 문장이 남아 있습니다. 요점은 stable 선언의 주어가 서버라는 것입니다. 서버의 데이터 모델과 저장·쿼리 동작은 stable이지만, 계기 라이브러리의 옵션 API가 같은 보장을 문서상으로 주고 있지는 않습니다.

Python은 더 단순합니다 — client_python의 네이티브 히스토그램 구현 이슈는 아직 열려 있습니다. 텍스트 포맷 제안서가 밝힌 동기 중 하나가 바로 이것입니다. Python 클라이언트 같은 일부 라이브러리는 네이티브 히스토그램을 노출하자고 프로토버프 의존성을 추가하고 싶어 하지 않는다는 것.

쿠버네티스는 알파입니다. 쿠버네티스 컴포넌트 메트릭에 네이티브 히스토그램이 들어온 건 v1.36(2026년 4월 22일 릴리스)부터인데, NativeHistograms 피처 게이트의 상태는 알파, 기본값 false입니다. 적용 대상은 kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, kube-proxy이고 컴포넌트별로 따로 켤 수 있습니다. 켜면 클래식과 네이티브를 동시에 노출하는 이중 노출(dual exposition) 방식이고, 어느 쪽을 받을지는 요청의 Accept 헤더가 결정합니다.

프로토버프 스크레이프 비용 — 원래 stable의 발목을 잡던 문제

여기 흥미로운 이력이 하나 있습니다. scrape_native_histograms를 켜면 콘텐츠 협상이 바뀌어 해당 스크레이프 설정이 프로토버프를 선호하게 됩니다. 명세가 밝히듯 기본값이 지정되지 않은 경우 유효 우선순위는 이렇게 바뀝니다.

[ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]

문제는 이게 그 스크레이프 설정의 모든 타깃에 적용된다는 점이었습니다. 네이티브 히스토그램 몇 개 받자고 전체 스크레이프를 프로토버프로 갈아타는 셈입니다. 이슈 #14668이 이 문제를 정면으로 다뤘고, 본문의 진단은 이랬습니다 — 프로토버프 파싱이 메모리 처닝을 많이 만들고, 대부분 GC CPU 사용량 증가로 나타나며, 사용 패턴에 따라 CPU가 크게 늘 수 있고 실제 사례에서 2배 증가를 본 적이 있다는 것. 그리고 같은 본문에 이런 문장이 굵게 적혀 있습니다.

(Or in other words: This issue needs to get resolved to declare native histograms a stable feature.)

즉 이 성능 문제는 stable 선언의 전제 조건이었습니다. 이슈는 2025년 2월 13일 completed로 닫혔고, 그 뒤 2025년 12월에 stable 선언이 나왔습니다. 닫히기 전 논의에서 메인테이너는 최적화 이후 현실적인 벤치마크에서 텍스트 파서 대비 3배 빠르고 할당 메모리는 10% 적다는 결과를 보고했습니다.

여기서 정직해야 할 부분이 있습니다. 명세 문서에는 아직도 텍스트 기반 포맷으로 스크레이프하는 것이 프로토버프보다 일반적으로 리소스를 훨씬 덜 쓴다는 note가 남아 있고, 근거로 바로 이 이슈를 링크합니다 — 그런데 그 이슈는 completed로 닫혀 있습니다. 문서의 note가 최적화 이전 상태를 반영한 낡은 서술로 보입니다. 그러니 "프로토버프 스크레이프는 여전히 2배 비싸다"고 말하는 것도, "이제 완전히 해결됐다"고 말하는 것도 지금 자료로는 과합니다. 당신 환경에서 CPU를 직접 재 보는 수밖에 없습니다. 켜기 전후로 스크레이프 대상 프로세스와 Prometheus 자신의 CPU를 비교하십시오.

한편 이 이슈가 남긴 실용적 유산이 있습니다. 노출 범위를 좁히는 수단들입니다. scrape_protocols를 직접 지정하면 scrape_native_histograms가 켜져 있어도 특정 타깃은 비프로토버프 포맷으로 강제할 수 있습니다. 그리고 3.13에는 릴레이블링으로 타깃별 덮어쓰기를 하는 내부 라벨이 추가됐습니다 — __scrape_native_histograms____always_scrape_classic_histograms__(#18929), 그리고 __convert_classic_histograms_to_nhcb__(#18840)입니다. 스크레이프 설정을 쪼개지 않고도 네이티브 히스토그램을 노출하는 타깃에만 정확히 켤 수 있다는 뜻입니다.

NHCB — 계기 코드를 건드리지 않는 경로

모든 클래식 히스토그램을 네이티브로 다시 계기하는 건 큰 작업입니다. 그래서 중간 경로가 있습니다. 스키마 -53, 즉 커스텀 버킷을 가진 네이티브 히스토그램(NHCB)입니다.

convert_classic_histograms_to_nhcb 스크레이프 옵션을 켜면 Prometheus가 수집 시점에 클래식 히스토그램을 NHCB로 변환해 넣습니다. 계기 코드는 그대로 두고 저장 효율만 가져오는 방식입니다. 명세의 표현으로 NHCB의 핵심 장점은 저장 비용이 일반적으로 훨씬 싸다는 것이고, 특히 버킷을 추가하는 한계 비용이 낮아서 버킷이 많은 클래식 히스토그램도 감당할 만하게 수집할 수 있습니다.

대신 대가가 명확합니다. NHCB는 서로 병합이 근본적으로 제한됩니다. 명세는 이렇게 경고합니다 — 서로 다른 버킷 레이아웃 간 자동 조정은 경계가 정확히 일치하는 것만 남기므로, 대부분의 경계가 일치하면 쓸 만한 결과가 나오지만, 버킷 레이아웃이 임의로 달라지면 어느 경계도 일치하지 않아 오버플로 버킷 하나만 남는 히스토그램이 나올 수 있습니다. 그래서 명세는 스키마 -53을 특정 용례에서 정보에 근거한 결정으로만 쓰라고 권합니다. 서비스마다 버킷 경계가 제각각인 코드베이스에서 NHCB로 전부 밀어 넣고 합산 쿼리를 돌리면, 조용히 쓸모없는 결과를 받게 됩니다.

정직한 트레이드오프 — 마이그레이션이 어려운 이유

명세의 마이그레이션 섹션은 문제를 세 가지로 정리합니다. 이 부분이 이 기능 도입의 실제 난이도입니다.

첫째, 쿼리 방식이 다릅니다. 대부분의 경우 변경은 최소한이고 직관적이지만, 까다로운 엣지 케이스가 있어서 신뢰할 만한 자동 변환이 어렵다는 게 명세의 서술입니다. 즉 대시보드와 알림 규칙을 손으로 옮겨야 합니다.

둘째, 클래식과 네이티브는 서로 집계되지 않습니다. 이게 가장 아픕니다. 어느 시점에 클래식에서 네이티브로 갈아타면 그 전환 시점을 걸치는 대시보드를 만들기 어렵고, 전환 시점을 포함하는 레인지 벡터는 필연적으로 불완전합니다 — 클래식을 고르면 앞부분 데이터만, 네이티브를 고르면 뒷부분 데이터만 들어옵니다. 명세가 든 예가 구체적입니다.

histogram_quantile(0.9, rate(rpc_duration_seconds[1d]))

하루치 90분위 지연을 계산하는 이 쿼리는, 네이티브 히스토그램을 최소 하루 이상 모으지 않았다면 그보다 짧은 구간만 커버합니다. 그래서 명세의 권고는 보수적입니다 — 클래식과 네이티브를 한동안 병행 수집하라는 것. 한 달을 병행했다면 한 달보다 더 뒤를 보지 않는 대시보드는 전환 시점을 고민할 필요 없이 그냥 갈아탈 수 있습니다. 병행 수집은 always_scrape_classic_histograms 옵션으로 켭니다. 클래식은 접미사가 붙은 시계열들로, 네이티브는 이름 그대로의 시계열 하나로 들어가므로 TSDB에서 충돌하지 않습니다. 물론 그동안은 양쪽 비용을 다 냅니다.

셋째, 임의 버킷 경계를 못 잡습니다. 이게 SLO를 하는 팀에게 결정적일 수 있습니다. 클래식 히스토그램은 관심 지점에 정확히 경계를 두도록 맞춤 설계할 수 있습니다 — 예컨대 "300ms 이내 응답 비율"이 SLO라면 경계를 300ms에 박아 두면 그 비율은 추정이 아니라 정확한 값입니다. 표준 스키마의 네이티브 히스토그램은 해상도는 높지만 경계를 임의 값에 둘 수 없습니다. Grafana Mimir 문서도 같은 지적을 단점으로 명시합니다 — SLO 정의에 특히 중요한 임계값 같은 임의 버킷 경계를 설정할 방법이 없고, 주어진 임계값 위아래 관측 비율은 일반적으로 보간(interpolation)으로 추정해야 한다는 것. 명세는 이런 경우 "네이티브 히스토그램에서의 사용자 경험이 실제로 더 나빠질 수 있다"고 솔직하게 적어 뒀고, 해법으로 그 히스토그램은 그냥 마이그레이션하지 않고 두는 선택지를 제시합니다.

여기에 하나 더. 버킷은 처음 채워질 때 동적으로 생성되므로, 예상보다 넓은 분포가 들어오면 예상보다 많은 버킷이 생기고 메모리를 더 씁니다. 명세는 관측값이 외부에서 조작 가능하다면 이게 메모리 고갈을 통한 DoS 벡터가 될 수 있다고까지 경고합니다. 그런데 Go 클라이언트의 NativeHistogramMaxBucketNumber 기본값은 0, 즉 무제한입니다(명세도 이 점을 TODO로 남겨 두고 있습니다). 흔히 쓰는 값은 160이고, 이는 OTel 지수 히스토그램의 기본값과 같습니다. 수집 측에서도 방어할 수 있습니다 — 스크레이프 설정의 native_histogram_bucket_limit(개별 히스토그램의 버킷 수 상한)과 native_histogram_min_bucket_factor(성장 인자 하한)가 그것이고, 둘 다 초과 시 해상도를 낮춰 맞춥니다. 다만 NHCB가 버킷 수 상한을 넘기면 해상도를 낮출 수 없으므로 스크레이프가 실패합니다.

그래서, 당신은 켜야 하나

지금 켤 만한 경우

  • 버킷이 많은(수십 개) 클래식 히스토그램을 쓰고 있고, 라벨 파티셔닝 때문에 카디널리티가 이미 아픈 곳. 절감 폭이 원래 버킷 수에 비례하므로 여기가 가장 값을 합니다.
  • 지연 분포처럼 범위를 미리 알기 어려운 관측값. 버킷 경계를 추측해 박아 두는 일 자체가 사라지는 게 실질적 이득입니다.
  • Go로 계기하고 있고 서버를 3.13 LTS로 올릴 계획인 곳. 지금 스택에서 경로가 가장 매끄럽습니다.
  • 계기 코드를 못 건드리지만 저장 비용을 줄이고 싶은 곳 — 이 경우 NHCB 변환만 켜는 선택지가 있습니다. 단 병합 제약을 먼저 이해하고 쓰십시오.

아직 아닌 경우

  • SLO 임계값에 정확히 맞춘 버킷 경계에 의존하는 히스토그램. 보간 추정으로 바꾸는 순간 SLO 계산의 성격이 달라집니다. 이건 그냥 클래식으로 두십시오.
  • 계기가 Python client인 곳. 아직 미구현이라 선택지가 없습니다.
  • 버킷이 몇 개 안 되는 히스토그램. 얻을 게 별로 없는데 마이그레이션 비용은 그대로 냅니다.
  • 스크레이프 대상 대부분이 네이티브 히스토그램을 노출하지 않는데 스크레이프 설정 단위로 전부 프로토버프로 넘어가는 구성. 3.13의 타깃별 릴레이블링으로 범위를 좁히거나, scrape_protocols로 통제하십시오.
  • 쿠버네티스 컴포넌트 메트릭이 목적인 곳. 1.36 알파에 기본 비활성이므로, 프로덕션 클러스터에서 알파 피처 게이트를 켜는 결정을 따로 해야 합니다.

마치며

정리하면 이렇습니다. 네이티브 히스토그램은 실제로 stable이 됐고(3.8.0, 2025년 12월), 3.13이 이 기능을 stable로 담은 첫 LTS이며(2026년 7월 1일), 3.5 LTS는 7월 31일에 끝납니다. 데이터 모델은 진짜 좋습니다 — 시계열 N+2개가 1개가 되고, 버킷 경계를 추측하지 않아도 되고, 표준 스키마끼리 병합됩니다.

그런데 stable이라는 단어의 주어를 정확히 읽어야 합니다. 그건 서버가 이 타입을 다루는 방식이 v4 전까지 안 깨진다는 뜻이지, 생태계가 준비됐다는 뜻이 아닙니다. 노출은 아직 프로토버프 하나뿐이고, Go 클라이언트는 자기 옵션을 여전히 실험적이라고 문서화하고 있고, Python은 미구현이고, 쿠버네티스는 알파입니다. v4에서 기본값이 켜지는 방향은 분명하지만, 그 사이 구간을 지금 건너는 사람은 각자 이 구멍들을 확인해야 합니다.

그러니 실무적인 순서는 이렇습니다. 3.13으로 올리는 건 어차피 7월 31일 때문에 해야 하는 일이니 그것부터 하고, 네이티브 히스토그램은 별개의 결정으로 두십시오. 그다음 카디널리티가 가장 아픈 히스토그램 몇 개만 골라 병행 수집으로 시작하고, 프로토버프 전환의 CPU 영향을 직접 재고, 대시보드를 옮길 시간을 벌 만큼 병행 기간을 길게 잡으십시오. 3년 걸려 stable이 된 기능입니다. 2주 만에 다 옮길 이유는 없습니다.

참고 자료