대시보드에 지연 시간 히스토그램을 붙이려는데 buckets=에 무슨 숫자를 넣어야 할지, 기본값을 그대로 둬도 되는지, 요즘 말하는 네이티브 히스토그램으로 바로 가야 하는지부터 막히죠. 결론부터 말하면 클래식 히스토그램은 "보고 싶은 백분위수와 SLO 값 근처에 경계를 두는 것"이 전부이고, 쓰는 언어 라이브러리가 네이티브 히스토그램을 내보낼 수 있다면 버킷 계수 1.1 안팎의 네이티브 히스토그램이 비용과 정확도 모두 유리해요. 클래식의 p99 오차는 그 값이 떨어진 버킷의 폭을 넘지 않지만, 폭이 0.4초면 오차도 0.4초까지 열려 있어요.
histogram_quantile의 보간 원리는 알고 있다고 보고, 이 글은 설정을 고르고 문제를 찾는 데 필요한 숫자만 다뤄요. 기준은 2026-11-08 현재 최신 안정판 Prometheus 3.16.0과 공식 문서 latest예요.
서머리·클래식·네이티브, 무엇을 고르나
세 형식의 차이는 "백분위수를 어디서 계산하느냐"와 "여러 인스턴스를 합칠 수 있느냐"로 갈려요. 공식 문서의 비교 표를 실무 질문 기준으로 다시 묶으면 이래요.
| 따질 것 | 서머리 | 클래식 히스토그램 | 네이티브 히스토그램 |
|---|---|---|---|
| 파드 여러 개를 합친 p99 | 불가 | 가능(버킷 경계가 같을 때) | 가능(버킷이 항상 호환) |
| 나중에 p95→p99.9로 바꾸기 | 재배포 필요 | 쿼리만 바꾸면 됨 | 쿼리만 바꾸면 됨 |
| 계측 때 정할 것 | φ 목록과 시간 창 | 버킷 경계 전부 | 해상도와 버킷 수 한도 |
| 라벨 조합당 시계열 | φ 수 + 2 | 버킷 수 + 2 | 1 |
| 오차를 정하는 것 | φ 축 허용 오차 | 그 값이 든 버킷의 폭 | 설정한 해상도 |
| 앱 쪽 관측 비용 | 상대적으로 비쌈 | 카운터 증가만 | 카운터 증가만 |
판단은 두 질문이면 끝나요. 첫째, 파드가 둘 이상이거나 리전별로 합쳐 봐야 하면 서머리는 빠져요. 서머리의 quantile 시계열을 평균 내는 건 수학적으로 의미가 없어요. 둘째, 계측 라이브러리가 네이티브 히스토그램을 내보내는지 확인해요. 공식 문서는 현재 네이티브 히스토그램이 protobuf 노출 형식이 필요해서 Go·Java 같은 protobuf 지원 라이브러리 위주로 쓸 수 있다고 적었어요. 못 쓰면 클래식으로 내보내고, 서버에서 NHCB(사용자 지정 경계 네이티브 히스토그램)로 바꿔 받는 길이 있어요. 이건 아래 전환 절에서 다뤄요.
서머리는 합칠 일이 없고 백분위수가 처음부터 정해진 단일 프로세스 작업에 맞아요. 다만 공식 문서의 예처럼 꼬리가 넓은 분포에서는 φ를 0.95±0.01로 잡아도 관측값으로는 270ms~330ms만큼 흔들릴 수 있어요.
클래식 히스토그램 비용, 버킷 수 × 라벨 조합
클래식 히스토그램은 라벨 조합마다 _bucket이 버킷 수만큼, _sum과 _count가 하나씩 생겨요. +Inf 버킷도 시계열 하나예요. 그래서 버킷을 몇 개 두느냐가 그대로 저장·메모리 비용이에요.
예시로 route 40종 × code 5종에 파드 10개를 곱해 라벨 조합 2,000개인 지연 지표를 생각해 볼게요. 파이썬 공식 클라이언트의 기본 버킷은 5ms~10초 사이 유한 경계 14개에 +Inf를 더해 15개예요.
버킷을 15개에서 31개로 늘리면 시계열이 거의 두 배가 돼요. 시계열 수가 디스크와 메모리를 어떻게 정하는지는 프로메테우스 용량 계산, scrape_interval·보존 기간·시계열 수 정하는 법에 계산식이 있어요. 클래식은 버킷을 늘리는 만큼 비용이 선형으로 늘어서, 경계를 많이 두기보다 필요한 자리에 두는 게 중요해요.
네이티브 히스토그램은 조합당 시계열이 1개라 이 표에서는 압도적으로 적어요. 다만 샘플 하나에 채워진 버킷이 통째로 들어가므로 "17분의 1 비용"으로 단순 환산하면 안 돼요. 공식 문서는 NHCB조차 클래식보다 저장 비용이 훨씬 낮고, 버킷을 하나 더할 때의 추가 비용이 작다고만 적었어요. 실제 비용은 도입 전에 스테이징에서 prometheus_tsdb_head_series와 디스크 증가량을 재 보는 게 정확해요.
버킷 경계 정하기, 배치 4가지 오차 시뮬레이션
같은 데이터에 버킷 배치만 바꾸면 추정치가 얼마나 달라지는지 직접 계산했어요. 중앙값 120ms, 로그 표준편차 0.6인 로그정규 분포에서 20만 건을 뽑고(시드 7), histogram_quantile의 클래식 규칙(구간 안 직선 보간, +Inf에 걸리면 두 번째로 높은 경계)을 파이썬으로 옮겨 추정했어요. 실제 값은 p50 0.120초, p90 0.259초, p99 0.485초, p99.9 0.766초예요.
| 배치 | 유한 경계 | p50 추정 | p99 추정 | p99.9 추정 |
|---|---|---|---|---|
| 기본값 | 0.005~10초 14개 | 0.135초 (+12.7%) | 0.497초 (+2.6%) | 0.791초 (+3.2%) |
| 성긴 4개 | 0.1·0.5·1·5 | 0.178초 (+48.5%) | 0.499초 (+3.0%) | 0.952초 (+24.2%) |
| SLO 300ms 중심 | 0.05~1초 9개 | 0.122초 (+2.1%) | 0.533초 (+10.1%) | 0.903초 (+17.8%) |
| 위쪽이 0.5초에서 끝 | 0.025~0.5초 6개 | 0.128초 (+6.9%) | 0.495초 (+2.3%) | 0.500초 (−34.8%) |
이 표에서 읽을 점은 세 가지예요.
- 점 추정치보다 버킷 폭을 보세요. 성긴 배치의 p99가 3.0%로 우연히 잘 맞았지만, p99가 든 버킷은 0.1~0.5초라 최악이면 0.4초까지 틀릴 수 있어요. 분포가 조금만 바뀌어도 48%짜리 p50처럼 크게 어긋나요.
- 경계가 촘촘한 구간 밖은 오히려 나빠져요. SLO 중심 배치는 0.3초 아래가 촘촘해 p50·p90은 1~2%로 맞췄지만, 0.4~0.6초 버킷이 넓어 p99는 10% 틀렸어요.
- 맨 위 유한 경계보다 큰 백분위수는 그 경계에 붙어요. 0.5초에서 끝나는 배치는 p99.9가 정확히 0.500초로 나왔어요. 실제 0.766초 요청이 있어도 그래프는 0.5에서 평평해요.
그래서 경계는 이 순서로 정해요.
- SLO 값을 경계로 넣어요. 300ms가 목표면
0.3이 꼭 있어야 해요. 그래야 아래 절의 비율 쿼리가 정확해요. - 알림·대시보드에 쓰는 백분위수가 평소 떨어지는 자리 앞뒤에 경계를 둬요. 위 예라면 p99가 0.45~0.55초 근처라
0.4, 0.5, 0.6을 두면 오차 상한이 0.1초로 줄어요. - 맨 위 유한 경계는 타임아웃 값 이상으로 둬요. 게이트웨이 타임아웃이 30초면
30이 있어야 장애 때 p99가 경계에 붙지 않아요. - 지연은 배수 간격이 효율적이에요. 1·2.5·5 반복이나 2배씩 늘리는 배치가 적은 버킷으로 넓은 범위를 덮어요. 파이썬 클라이언트 기본값도 이 패턴이에요.
SLO 판정은 백분위수 대신 비율로
"p95가 300ms 이하인가"는 "300ms 이하 요청 비율이 95% 이상인가"와 같은 질문이에요. 그런데 뒤쪽은 보간이 필요 없어요. le="0.3" 버킷이 있으면 그 버킷의 증가량을 전체 개수로 나누면 끝이에요.
# 최근 5분 300ms 이하 요청 비율 — 클래식, 보간 없이 정확
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m]))
/
sum(rate(http_request_duration_seconds_count[5m]))
# 네이티브(NHCB 포함) — 0.3 이 버킷 경계와 맞지 않으면 추정치
histogram_fraction(0, 0.3, sum(rate(http_request_duration_seconds[5m])))
위 시뮬레이션 데이터에서 0.3초 이하 비율은 93.67%였어요. 백분위수로 보면 "p95가 0.3초를 조금 넘는다"인데, 각 배치의 p95 추정치는 버킷 폭만큼 흔들리지만 비율은 경계가 맞으면 정확히 93.67%로 나와요. 함수 문서도 histogram_fraction()을 클래식 히스토그램에 쓰면 경계에서 먼 값을 고르기 쉬워 오차가 커진다며, 버킷 시계열을 직접 다루는 방식을 더 안정적이라고 권해요. 공식 문서의 Apdex 예제도 같은 원리로 목표값과 허용값 버킷을 바로 나눠요.
네이티브 히스토그램 전환 설정과 해상도 고르기
네이티브 히스토그램 스펙에 따르면 v3.8.0부터 정식 기능이고, v3.9부터는 --enable-feature=native-histograms 플래그가 아무 효과가 없어요. 대신 스크레이프 설정에서 직접 켜야 해요. v4부터는 scrape_native_histograms와 원격 쓰기의 send_native_histograms 기본값이 true가 될 예정이에요.
# prometheus.yml — 전환 기간 예시
scrape_configs:
- job_name: api
scrape_native_histograms: true # v3 기본 false
always_scrape_classic_histograms: true # 전환 중 대시보드용으로 클래식도 같이 받기
native_histogram_bucket_limit: 160 # 히스토그램 하나당 버킷 수 상한 (기본 0 = 제한 없음)
- job_name: legacy-python
convert_classic_histograms_to_nhcb: true # 클래식만 내는 앱을 NHCB로 받기
설정별 판단 기준은 이래요. 값과 기본값은 설정 문서에서 확인했어요.
| 설정 | 기본값 | 언제 켜나 |
|---|---|---|
scrape_native_histograms | false (v3) | 앱이 네이티브를 내보낼 때. 켜면 protobuf 형식을 우선 협상해요 |
always_scrape_classic_histograms | false | 전환 중 옛 대시보드·알림이 _bucket을 쓸 때만. 끝나면 꺼요 |
convert_classic_histograms_to_nhcb | false | 앱을 못 고치는데 저장 비용을 줄이고 싶을 때 |
native_histogram_bucket_limit | 0 | 버킷 폭주를 막는 상한. 넘으면 해상도를 반씩 낮추고, NHCB가 넘으면 스크레이프가 실패해요 |
native_histogram_min_bucket_factor | 0 | 해상도가 쓸데없이 높은 히스토그램을 서버에서 깎을 때 |
해상도는 스키마 번호로 정해지고, 인접 경계의 비율(버킷 계수)이 2^(2^−n)이에요. 버킷 계수에서 1을 뺀 값이 버킷의 상대 폭이라 오차 상한이 돼요. 계측 쪽 예시로 자주 쓰는 계수 1.1은 스키마 3(계수 1.0905, 상대 폭 약 9%)에 해당해요. 위 시뮬레이션 데이터를 스키마 3으로 담으면 채워진 버킷은 60개였고, 지수 보간(버킷 안을 로그 균등으로 가정)으로 추정한 p99 오차는 0.5%였어요.
스키마를 하나 올릴 때마다 해상도가 두 배, 최악의 버킷 수도 두 배예요. 1ms에서 60초까지 퍼질 수 있는 지표에 스키마 4 이상을 주면 버킷이 254개를 넘을 수 있어서, native_histogram_bucket_limit: 160 같은 상한을 함께 거는 게 안전해요. 스펙은 Go 라이브러리의 버킷 수 제한 예시로 160을 들면서, 이 값이 OTel 지수 히스토그램의 기본값과 같다고 적었어요.
NHCB는 경계가 다른 히스토그램끼리 합칠 때 정확히 같은 경계만 남겨요. 배포 중에 버킷 배치가 크게 바뀌면 공통 경계가 하나도 없어 +Inf 버킷 하나짜리 히스토그램이 될 수 있어요. 지수 스키마는 경계가 수학적으로 정해져 있어서 이 문제가 없어요.
p99 그래프가 이상할 때 확인 순서
| 증상 | 먼저 볼 것 | 고치는 방법 |
|---|---|---|
| p99가 특정 값에 붙어 평평 | 그 값이 맨 위 유한 경계인지 | 타임아웃 이상으로 경계 추가 |
| 결과가 비거나 NaN | +Inf 버킷이 있는지, 창 안 요청이 0인지 | rate 범위를 늘리고 트래픽 없는 시간은 NaN이 정상 |
| "needed to be fixed for monotonicity" 주석 | 배포 중 버킷 경계가 섞였는지 | 경계 변경 시 메트릭 이름을 새로 두기 |
| 배포 직후 크게 출렁임 | rate 없이 _bucket을 그대로 넣었는지 | rate → sum by (le) → histogram_quantile 순서 |
| 파드 합계 p99가 파드별 값과 안 맞음 | 파드별 p99를 avg했는지 | 버킷을 먼저 합치고 백분위수는 마지막에 한 번 |
| 같은 경계가 두 줄로 갈림 | le="1"과 le="1.0"이 섞였는지 | v3은 수집 때 정규화. v2 데이터와 섞을 때 주의 |
첫 줄이 가장 흔해요. 위 시뮬레이션의 마지막 배치처럼 백분위수가 +Inf 버킷에 떨어지면 두 번째로 높은 경계가 그대로 나와요. 이걸 확인하는 쿼리는 이래요.
# 0 보다 크면 맨 위 유한 경계(여기선 10초)를 넘는 요청이 있다는 뜻
sum(rate(http_request_duration_seconds_bucket{le="+Inf"}[5m]))
- sum(rate(http_request_duration_seconds_bucket{le="10.0"}[5m]))
두 번째 줄의 NaN은 대부분 정상이에요. 함수 문서 기준으로 관측값이 0개거나 버킷이 2개 미만이거나 가장 높은 경계가 +Inf가 아니면 NaN이라, 밤사이 트래픽 없는 엔드포인트는 NaN 구간이 생기는 게 맞아요.
histogram_quantile은 누적값이 rank 이상인 첫 버킷을 이분 탐색으로 찾아요. 경계 조건(이상·초과)이 하나만 달라져도 결과 버킷이 바뀌는 구조라, 직접 재현 스크립트를 짤 때는 이분 탐색 bisect·lower_bound 경계 버그 체크리스트의 bisect_left·bisect_right 구분을 참고하세요. 원본 구현은 promql/quantile.go의 BucketQuantile이에요.
도입 전에 따질 것
- 원격 저장소가 받는지: 원격 쓰기는
send_native_histograms를 따로 켜야 하고, 받는 쪽(장기 저장소)이 네이티브 히스토그램을 지원해야 해요. 지원하지 않으면 전환 기간 내내always_scrape_classic_histograms를 켜 둬야 해서 비용이 오히려 늘어요. - 대시보드·알림 쿼리 개수: 네이티브는
_bucket접미사와le라벨이 없어서 쿼리를 다시 써야 해요.sum by (le)가sum으로 바뀌는 정도지만, 패널과 알림 규칙이 수백 개면 그 자체가 작업량이에요. - 라이브러리 지원: 공식 문서 기준 네이티브는 protobuf 지원 라이브러리 위주예요. 쓰는 언어가 아니라면 앱은 클래식 그대로 두고
convert_classic_histograms_to_nhcb로 서버에서 바꾸는 게 현실적인 첫 단계예요. - 경계 변경 정책: 클래식·NHCB를 계속 쓴다면 "버킷 경계를 바꿀 때는 새 메트릭 이름"을 팀 규칙으로 정해 두세요. 롤링 배포 중 경계가 섞이면 합계가 깨져요.
- 레코딩 규칙: 라벨 조합이 많은 지표에서
histogram_quantile을 대시보드마다 바로 돌리면 무거워요. 공식 비교 표도 느릴 때는 레코딩 규칙을 쓰라고 해요. 자주 보는 p99는 미리 계산해 두는 편이 싸요.
자주 묻는 질문
파이썬 클라이언트 기본 버킷을 그대로 써도 되나요?
5ms~10초 범위의 일반 웹·RPC 요청이면 출발점으로 괜찮아요. 위 시뮬레이션에서도 p99 오차가 2.6%로 무난했어요. 다만 SLO 값이 0.3초처럼 기본 경계에 없으면 비율 쿼리를 정확히 낼 수 없으니 그 값은 추가하고, 타임아웃이 10초를 넘으면 위쪽 경계도 늘려야 해요.
버킷은 몇 개가 적당한가요?
정답은 없지만 클래식은 10~15개 안에서 SLO 값과 자주 보는 백분위수 근처에 몰아 두는 게 비용 대비 효과가 좋아요. 라벨 조합 2,000개 기준으로 버킷 하나를 더할 때마다 시계열이 2,000개씩 늘어요. 15개를 넘겨야 할 만큼 정확도가 필요하면 네이티브나 NHCB를 먼저 검토해요.
네이티브 히스토그램 해상도는 어떻게 정하나요?
버킷 계수 1.1(스키마 3, 상대 폭 약 9%)로 시작하는 경우가 많아요. 공식 문서의 예시도 이 값을 써요. 더 정밀하게 보고 싶으면 스키마를 올리되 버킷 수가 두 배씩 늘어나니 native_histogram_bucket_limit으로 상한을 같이 걸어요.
클래식에서 네이티브로 바꾸면 과거 데이터는 어떻게 되나요?
클래식 시계열은 _bucket 이름으로, 네이티브는 원래 이름 그대로 저장돼서 서로 충돌하지 않아요. 전환 기간에 always_scrape_classic_histograms로 둘 다 받으면 옛 대시보드를 유지하면서 새 쿼리를 검증할 수 있어요. 보존 기간이 지나 옛 클래식 데이터가 빠지면 그 설정을 끄면 돼요.
NHCB로 받으면 클래식의 오차 문제도 사라지나요?
아니요. NHCB는 저장 형식만 네이티브이고 경계는 앱이 정한 클래식 경계 그대로라, 오차 상한도 클래식과 같아요. 얻는 것은 저장 비용 절감과 네이티브용 쿼리 문법이에요. 정확도까지 올리려면 앱이 지수 스키마 네이티브 히스토그램을 직접 내보내야 해요.
정리
- 여러 인스턴스를 합쳐 봐야 하면 히스토그램, 라이브러리가 지원하면 네이티브가 기본 선택이에요. 못 쓰면 클래식으로 내보내고 서버에서 NHCB로 받을 수 있어요.
- 클래식의 오차 상한은 백분위수가 든 버킷의 폭이에요. SLO 값과 자주 보는 백분위수 근처에 경계를 두고, 맨 위 경계는 타임아웃 이상으로 잡아요.
- 버킷을 늘리기 전에 라벨 수부터 줄이는 게 싸요.
- SLO는
le버킷 ÷_count비율로 판정하고, 그래프가 평평하거나 NaN이면 위 확인 순서표의 첫 두 줄부터 봐요.
더 깊이 읽기
이 글은 실무에서 판단할 때 필요한 것 위주로 정리했어요. 같은 주제의 내부 동작 원리는 devboi 기술 블로그의 프로메테우스 histogram_quantile 원리 — 버킷과 보간에서 도식과 코드로 단계별로 풀었어요.