광고
~ / posts / developer

프로메테우스 히스토그램 버킷 정하는 법, p99 오차 계산과 네이티브 전환

클래식 히스토그램의 p99 오차는 그 값이 든 버킷의 폭만큼이에요. 버킷 배치 4가지를 20만 건으로 시뮬레이션한 오차표, 버킷 수별 시계열 비용, 네이티브 히스토그램 전환 설정과 p99 그래프가 이상할 때의 확인 순서를 정리했어요.

대시보드에 지연 시간 히스토그램을 붙이려는데 buckets=에 무슨 숫자를 넣어야 할지, 기본값을 그대로 둬도 되는지, 요즘 말하는 네이티브 히스토그램으로 바로 가야 하는지부터 막히죠. 결론부터 말하면 클래식 히스토그램은 "보고 싶은 백분위수와 SLO 값 근처에 경계를 두는 것"이 전부이고, 쓰는 언어 라이브러리가 네이티브 히스토그램을 내보낼 수 있다면 버킷 계수 1.1 안팎의 네이티브 히스토그램이 비용과 정확도 모두 유리해요. 클래식의 p99 오차는 그 값이 떨어진 버킷의 폭을 넘지 않지만, 폭이 0.4초면 오차도 0.4초까지 열려 있어요.

histogram_quantile의 보간 원리는 알고 있다고 보고, 이 글은 설정을 고르고 문제를 찾는 데 필요한 숫자만 다뤄요. 기준은 2026-11-08 현재 최신 안정판 Prometheus 3.16.0과 공식 문서 latest예요.

광고

서머리·클래식·네이티브, 무엇을 고르나

세 형식의 차이는 "백분위수를 어디서 계산하느냐"와 "여러 인스턴스를 합칠 수 있느냐"로 갈려요. 공식 문서의 비교 표를 실무 질문 기준으로 다시 묶으면 이래요.

따질 것서머리클래식 히스토그램네이티브 히스토그램
파드 여러 개를 합친 p99불가가능(버킷 경계가 같을 때)가능(버킷이 항상 호환)
나중에 p95→p99.9로 바꾸기재배포 필요쿼리만 바꾸면 됨쿼리만 바꾸면 됨
계측 때 정할 것φ 목록과 시간 창버킷 경계 전부해상도와 버킷 수 한도
라벨 조합당 시계열φ 수 + 2버킷 수 + 21
오차를 정하는 것φ 축 허용 오차그 값이 든 버킷의 폭설정한 해상도
앱 쪽 관측 비용상대적으로 비쌈카운터 증가만카운터 증가만
여러 인스턴스를 합쳐 봐야 하는지, 라이브러리가 네이티브 히스토그램을 내는지에 따라 서머리·클래식·네이티브를 고르는 순서도
쿠버네티스처럼 파드가 여러 개면 서머리는 거의 후보에서 빠져요

판단은 두 질문이면 끝나요. 첫째, 파드가 둘 이상이거나 리전별로 합쳐 봐야 하면 서머리는 빠져요. 서머리의 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개예요.

버킷 수별 시계열 수 (라벨 조합 2000개 지연 지표)
클래식 버킷 7개클래식 버킷 7개 · 18,000개18,000개클래식 버킷 11개클래식 버킷 11개 · 26,000개26,000개클래식 버킷 15개(기본값)클래식 버킷 15개(기본값) · 34,000개34,000개클래식 버킷 31개클래식 버킷 31개 · 66,000개66,000개네이티브네이티브 · 2,000개2,000개

네이티브는 시계열 수가 적은 대신 샘플 하나가 버킷 묶음을 담아 더 커요

단위: 개 · 자료: 라벨 조합 × (+Inf 포함 버킷 수 + _sum·_count 2개)로 직접 계산. 네이티브는 조합당 1개
표로 보기
구분시계열 수
클래식 버킷 7개18,000
클래식 버킷 11개26,000
클래식 버킷 15개(기본값)34,000
클래식 버킷 31개66,000
네이티브2,000

버킷을 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·50.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%)
버킷 배치별 백분위수 추정 오차 (로그정규 20만 건, 절댓값)
p50p99p99.9
01020304050기본값 14개 · p50 12.7%기본값 14개 · p99 2.6%기본값 14개 · p99.9 3.2%기본값 14개성긴 4개 · p50 48.5%성긴 4개 · p99 3%성긴 4개 · p99.9 24.2%성긴 4개SLO 중심 9개 · p50 2.1%SLO 중심 9개 · p99 10.1%SLO 중심 9개 · p99.9 17.8%SLO 중심 9개0.5초에서 끝 6개 · p50 6.9%0.5초에서 끝 6개 · p99 2.3%0.5초에서 끝 6개 · p99.9 34.8%0.5초에서 끝 6개

0.5초에서 끝나는 배치의 p99.9는 실제보다 낮게(−34.8%) 나왔어요. 나머지는 모두 높게 나왔어요

단위: % · 자료: random.seed(7) 로그정규(중앙값 0.12초, σ 0.6) 20만 건으로 직접 시뮬레이션
표로 보기
구분p50p99p99.9
기본값 14개12.72.63.2
성긴 4개48.5324.2
SLO 중심 9개2.110.117.8
0.5초에서 끝 6개6.92.334.8

이 표에서 읽을 점은 세 가지예요.

성긴 4개·기본값·SLO 중심 9개 배치에서 실제 p99 0.485초가 든 버킷이 각각 0.1~0.5초, 0.25~0.5초, 0.4~0.6초임을 보여 주는 수평 축 그림
색 칸의 폭이 그 배치에서 p99 추정이 틀릴 수 있는 최대 범위예요

그래서 경계는 이 순서로 정해요.

  1. SLO 값을 경계로 넣어요. 300ms가 목표면 0.3이 꼭 있어야 해요. 그래야 아래 절의 비율 쿼리가 정확해요.
  2. 알림·대시보드에 쓰는 백분위수가 평소 떨어지는 자리 앞뒤에 경계를 둬요. 위 예라면 p99가 0.45~0.55초 근처라 0.4, 0.5, 0.6을 두면 오차 상한이 0.1초로 줄어요.
  3. 맨 위 유한 경계는 타임아웃 값 이상으로 둬요. 게이트웨이 타임아웃이 30초면 30이 있어야 장애 때 p99가 경계에 붙지 않아요.
  4. 지연은 배수 간격이 효율적이에요. 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_histogramsfalse (v3)앱이 네이티브를 내보낼 때. 켜면 protobuf 형식을 우선 협상해요
always_scrape_classic_histogramsfalse전환 중 옛 대시보드·알림이 _bucket을 쓸 때만. 끝나면 꺼요
convert_classic_histograms_to_nhcbfalse앱을 못 고치는데 저장 비용을 줄이고 싶을 때
native_histogram_bucket_limit0버킷 폭주를 막는 상한. 넘으면 해상도를 반씩 낮추고, NHCB가 넘으면 스크레이프가 실패해요
native_histogram_min_bucket_factor0해상도가 쓸데없이 높은 히스토그램을 서버에서 깎을 때

해상도는 스키마 번호로 정해지고, 인접 경계의 비율(버킷 계수)이 2^(2^−n)이에요. 버킷 계수에서 1을 뺀 값이 버킷의 상대 폭이라 오차 상한이 돼요. 계측 쪽 예시로 자주 쓰는 계수 1.1은 스키마 3(계수 1.0905, 상대 폭 약 9%)에 해당해요. 위 시뮬레이션 데이터를 스키마 3으로 담으면 채워진 버킷은 60개였고, 지수 보간(버킷 안을 로그 균등으로 가정)으로 추정한 p99 오차는 0.5%였어요.

스키마별 1ms~60초 범위를 덮는 데 필요한 버킷 수
스키마 0 (계수 2)스키마 0 (계수 2) · 16개16개스키마 1 (1.414)스키마 1 (1.414) · 32개32개스키마 2 (1.189)스키마 2 (1.189) · 64개64개스키마 3 (1.091)스키마 3 (1.091) · 127개127개스키마 4 (1.044)스키마 4 (1.044) · 254개254개스키마 5 (1.022)스키마 5 (1.022) · 508개508개

실제로는 관측값이 들어온 버킷만 저장돼서 분포가 좁으면 훨씬 적어요

단위: 개 · 자료: ln(60 ÷ 0.001) ÷ ln(버킷 계수)를 올림해 직접 계산. 계수 = 2^(2^-n)
표로 보기
구분버킷 수
스키마 0 (계수 2)16
스키마 1 (1.414)32
스키마 2 (1.189)64
스키마 3 (1.091)127
스키마 4 (1.044)254
스키마 5 (1.022)508

스키마를 하나 올릴 때마다 해상도가 두 배, 최악의 버킷 수도 두 배예요. 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이에요.

도입 전에 따질 것

광고
광고

자주 묻는 질문

파이썬 클라이언트 기본 버킷을 그대로 써도 되나요?

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는 저장 형식만 네이티브이고 경계는 앱이 정한 클래식 경계 그대로라, 오차 상한도 클래식과 같아요. 얻는 것은 저장 비용 절감과 네이티브용 쿼리 문법이에요. 정확도까지 올리려면 앱이 지수 스키마 네이티브 히스토그램을 직접 내보내야 해요.

정리

더 깊이 읽기

이 글은 실무에서 판단할 때 필요한 것 위주로 정리했어요. 같은 주제의 내부 동작 원리는 devboi 기술 블로그의 프로메테우스 histogram_quantile 원리 — 버킷과 보간에서 도식과 코드로 단계별로 풀었어요.

참고한 자료

#프로메테우스 히스토그램 버킷#histogram_quantile 오차#p99 계산#네이티브 히스토그램 설정#scrape_native_histograms#NHCB#SLO 쿼리#PromQL 실무
← 이전 글테라폼 상태 잠금 설정 고르기, S3 use_lockfile 이전과 lock-timeout
광고

더 둘러보기

💻 개발자 다른 도구

🔥 많이 찾는 도구

🔮 재미로 한 번 더

📝 읽어 볼 만한 글

글 전체 보기 →