히스토그램과 요약
히스토그램과 요약 (Histograms and summaries)
프로메테우스의 히스토그램(histogram)과 요약(summary)은 카운터나 게이지보다 훨씬 복잡한 메트릭 타입이에요. 관측값의 분포를 다룰 때 쓰는 두 가지 방법인데, 역사적인 이유로 히스토그램은 클래식 히스토그램(classic histogram)과 네이티브 히스토그램(native histogram) 두 가지로 나뉘고, 네이티브는 또 몇 가지 하위 변형이 있어요. 이 문서는 이런 모든 메트릭 타입의 차이점을 이해하고, 올바르게 사용하며, 여러분의 사용 사례에 맞는 타입을 고르는 방법을 도와 드려요.
이 문서에서 얻을 수 있는 가장 중요한 교훈은 아주 단순해요. 가능하다면 네이티브 히스토그램을 사용하고, 그것을 클래식 히스토그램과 요약보다 우선시하라는 거죠. 일이 꼬이기 시작하는 지점은 네이티브 히스토그램을 그냥 쓰기 어려운 상황에 처했을 때예요.
출처: 문서
본문
히스토그램과 요약은 더 복잡한 메트릭 타입이에요. 역사적인 이유로 히스토그램은 클래식 히스토그램과 네이티브 히스토그램 두 가지 변형이 존재하며, 후자는 또 여러 하위 변형으로 나뉘어요. 이 문서는 그 모든 메트릭 타입의 차이점을 이해하고, 올바르게 사용하며, 여러분의 사용 사례에 맞는 올바른 메트릭 타입을 고르는 데 도움을 줘요.
이 문서에서 배울 가장 중요한 교훈은 간단해요: 가능하다면 네이티브 히스토그램을 사용하고, 그것을 클래식 히스토그램과 요약보다 선호하세요.
일이 까다로워지기 시작하는 것은 네이티브 히스토그램을 그냥 사용할 수 없는 상황에 처했을 때예요. 가장 흔하게는 클래식 히스토그램이나 요약을 포함하는 기존 메트릭을 다뤄야 하거나, 사용 중인 계측 라이브러리가 아직 네이티브 히스토그램을 지원하지 않을 수 있어요. 게다가 요약이나 클래식 히스토그램을 선호하게 만드는 몇 가지 특정 사용 사례도 있어요.
이 문서를 통해 관련 장애물과 미묘한 부분을 잘 헤쳐 나갈 수 있을 거예요.
개요 (Overview)
역사적으로 프로메테우스 세계의 샘플은 타임스탬프가 붙은 부동소수점 값 하나일 뿐이었어요. 이 값은 카운터나 게이지로 해석될 수 있어요. 즉 대부분의 경우 프로메테우스는 "정적 타입(static typing)" 개념을 유지하지 않고, 여러분이 어떤 종류의 메트릭을 다루고 있는지 알아야 해요(카운터 이름은 _total로 끝나야 한다는 관례가 도움이 돼요).
하지만 카운터와 게이지보다 더 많은 메트릭 타입이 있어요. 특히 관측값의 분포(보통 프로메테우스 용어로 "관측(observations)"이라 부르는)를 표현할 필요가 있어요. 근본적으로 두 가지 다른 접근 방식이 있어요.
-
계측된 프로그램이 사전 구성된 분위수(quantile)(예: 중앙값이나 90번째 백분위수)를 사전 구성된 시간 창(예: 지난 10분)에 걸쳐 계산해 추가 메트릭으로 노출해요. 프로메테우스는 이 접근 방식을 summary라는 메트릭 타입으로 구현해요. 사용된 알고리즘에 따라 사전 계산된 분위수는 보통 매우 정확해요. 하지만 계산은 계측된 프로그램에 리소스 비용이 들어요. 또한 다른 시간 창이나 다른 백분위수를 원한다면 나중에 분위수를 "재계산"할 수 없고, 가장 중요한 것은 분위수를 집계(aggregate)할 수 없다는 거예요(예: 여러 복제 워커가 뒷받침하는 서비스의 총 90번째 백분위수 지연시간을 계산하는 것).
-
계측된 프로그램이 분포를 더 근본적인 방식으로 표현해, 나중에 임의의 시간 창에 걸쳐 임의의 분위수를 계산하는 데 사용할 수 있게 해요. 가장 중요한 것은, 분포가 서로 집계될 수 있는 방식으로 표현된다는 거예요. 이런 표현을 때로 다이제스트(digest)라고 불러요. 프로메테우스는 이 접근 방식을 histogram이라는 메트릭 타입으로 구현하는데, 일반적인 히스토그램 개념에서 알 수 있듯 관측이 버킷(bucket)으로 집계돼요.
두 접근 방식 모두에서 프로메테우스는 관측의 개수(count)와 합(sum)도 수집해요(자세한 내용은 아래 참고).
두 접근 방식의 공통점은 샘플당 이전처럼 단일 부동소수점 값 하나가 아니라 수많은 수치 값을 수집해야 한다는 거예요.
-
어떤 경우든 관측의 개수와 합.
-
요약의 경우 사전 계산된 분위수.
-
히스토그램의 경우 인구 수(population counts)와 경계(boundaries)가 있는 버킷 집합.
이 새로운 유형의 메트릭은 합성 타입(composite types)이라고도 불러요.
첫 번째 접근 방식에서 프로메테우스는 단순한 타임스탬프 부동소수점 값이라는 데이터 모델을 유지하고, 이 다양한 값을 특정 라벨로 구분되는 각각 하나의 타임시리즈에 매핑했어요. 이런 식으로 summary와 classic histogram이 만들어졌어요. 둘 다에서 관측의 개수와 합은 각각 별도 타임시리즈로 추적돼요. 마찬가지로 summary의 각 사전 계산된 분위수와 histogram의 각 버킷도 각자의 타임시리즈에서 추적돼요. PromQL 연산자와 함수는 아래에서 자세히 설명하듯 이 개별 타임시리즈에 작동해요.
한편으로 이 접근 방식은 꽤 잘 작동했어요. 데이터 모델을 단순하게 유지하면서도 많은 사용 사례를 충족했어요. 다른 한편으로는 특히 히스토그램과 관련해 많은 한계를 겪었어요. 그래서 프로메테우스 수명이 꽤 지난 뒤에 네이티브 히스토그램(native histograms)이 도입됐어요. 네이티브 히스토그램 샘플은 "값의 합성"으로, 단일 샘플이 관측의 개수와 합, 그리고 인구 수와 경계가 있는 동적 개수의 버킷을 포함해요. Prometheus TSDB에서 하나의 히스토그램은 독립적인 타임시리즈 여러 개가 아니라 네이티브 히스토그램 샘플 하나의 타임시리즈를 만들어 내요. PromQL 연산자와 함수는 이제 이전의 개별 부동소수점 타임시리즈가 아니라 이런 합성 샘플에 작동해야 해요.
네이티브 히스토그램에 대한 모든 것은 그 명세에서 읽을 수 있지만, 매우 기술적이고 상세한 문서라는 점에 주의하세요. 여기서 계속 읽으면 소화하기 쉽고 사용에 초점을 맞춘 설명을 기대할 수 있어요.
합성 타입의 네이티브 표현을 향한 프로메테우스의 여정에 관심이 있다면 블로그 글에서 더 읽을 수 있어요.
계측 라이브러리 지원 (Instrumentation library support)
먼저 히스토그램과 요약에 대한 라이브러리 지원을 확인하세요.
요약은 보통 모든 라이브러리가 지원하지만, 일부는 관측의 개수와 합만 추적하고 분위수 계산을 생략할 수 있어요. (분위수가 없는 요약도 여전히 합법적인 요약 사용이에요. 아래 참고.)
클래식 히스토그램 지원도 널리 퍼져 있지만, 네이티브 히스토그램 지원은 여전히 드물어요. 현재 후자는 protobuf 형식으로 노출해야 하므로, Java와 Go 라이브러리 같은 protobuf 지원 라이브러리로 제한돼요. 텍스트 기반 형식 지원은 OpenMetrics v2의 일부로 진행 중이에요. 곧 움직임이 있을 테니 여러분의 라이브러리가 무엇을 제공하는지 확실히 확인해 보세요.
계측된 프로그램이 클래식 히스토그램만 노출하더라도, 프로메테우스를 구성해 그것들을 어쨌든 네이티브 히스토그램으로 수집할 수 있어요. 이것은 커스텀 버킷 경계를 가진 네이티브 히스토그램(NHCB, Native Histograms with Custom Bucket boundaries)의 형태로 일어나요. 이 NHCB는 (소위 표준 지수 버킷을 특징으로 하는) 일반적인 네이티브 히스토그램과 비교해 몇 가지 한계가 있지만, 순수 클래식 히스토그램보다는 훨씬 효율적으로 저장돼요. PromQL에서의 NHCB 처리는 다른 네이티브 히스토그램과 동일하므로, 나중에 "진짜" 네이티브 히스토그램으로 마이그레이션하는 것이 쉬울 거예요.
OpenTelemetry를 통한 수집 (Ingestion via Open Telemetry)
어쩌면 여러분은 Prometheus 계측 라이브러리를 전혀 사용하지 않고, 메트릭이 Open Telemetry(OTel) 표준을 따르는 컬렉터에서 나올 수도 있어요. OTel 메트릭을 Prometheus 호환 백엔드로 수집할 때, "일반적인" OTel 히스토그램은 Prometheus 쪽에서 클래식 히스토그램이나 NHCB로 변환될 수 있고(힌트: 후자를 선호하세요), OTel의 지수 히스토그램은 항상 (표준 지수 버킷을 가진) 일반적인 네이티브 히스토그램으로 변환돼요.
관측의 개수와 합 (Count and sum of observations)
히스토그램과 요약은 둘 다 관측을 기록해요. 보통 요청 지속시간이나 응답 크기예요. 모든 변형(분위수가 없는 요약까지)에서 관측 개수와 관측값의 합을 추적해서, 관측값의 평균을 계산할 수 있어요.
그렇게 하려면 일반적으로 먼저 원하는 기간에 걸쳐 rate를 취한 다음 "합의 rate"를 "개수의 rate"로 나누면 돼요.
네이티브 히스토그램(NHCB 포함)의 경우 histogram_sum과 histogram_count 함수로 관측의 합과 개수를 추출해요. 예를 들어 http_request_duration_seconds라는 네이티브 히스토그램에서 지난 5분간의 평균 요청 지속시간을 계산하려면 다음 PromQL 표현식을 사용해요.
histogram_sum(rate(http_request_duration_seconds[5m]))
/
histogram_count(rate(http_request_duration_seconds[5m]))
평균 계산을 위해 histogram_avg 함수를 사용하는 더 짧은 버전도 있어요. 다음은 위와 동등해요.
histogram_avg(rate(http_request_duration_seconds[5m]))
요약이나 클래식 히스토그램의 경우 관측의 합과 개수에 대해 각각 _sum과 _count라는 마법 접미사로 표시된 별도 타임시리즈가 있어요. 따라서 http_request_duration_seconds라는 요약이나 클래식 히스토그램은 http_request_duration_seconds_sum과 http_request_duration_seconds_count 시리즈를 만들고, 지난 5분간 평균 요청 지속시간을 계산하는 표현식은 이렇게 보일 거예요.
rate(http_request_duration_seconds_sum[5m])
/
rate(http_request_duration_seconds_count[5m])
위 두 표현식 모두의 분모는 그 자체로도 유용해요. 지난 5분간 서빙된 초당 요청 수를 나타내요. 다른 말로 하면 http_request_duration_seconds_count 시리즈가 HTTP 요청에 대한 카운터(히스토그램이나 요약이 없었다면 http_requests_total이라고 불렀을)와 정확히 똑같이 동작한다는 뜻이에요. 프로메테우스에서 카운터의 핵심 속성은 카운터 리셋이 없는 한 항상 올라간다는 것이에요.
관측값이 절대 음수가 아니라면 http_request_duration_seconds_sum 시리즈도 (카운터 리셋이 없는 한) 항상 올라가요. 하지만 음수 관측이 섞여 있다면 관측의 합이 내려갈 수도 있고, 이는 PromQL이 만드는 가정을 깨뜨려요. 그러한 하락은 위의 rate(http_request_duration_seconds_sum[5m]) 계산에서 오류로 카운터 리셋으로 간주되어 결과를 왜곡할 거예요. 이 문제는 요약과 클래식 히스토그램에만 영향을 준다는 점에 주목하세요. 네이티브 히스토그램(NHCB 포함)은 전체로서 rate가 적용되므로 카운터 리셋을 올바르게 감지해요. 음수 관측을 피할 수 없고 요약이나 클래식 히스토그램을 써야 하는 드문 경우에는 양수 관측용과 음수 관측용(부호를 뒤집어서)으로 요약이나 히스토그램 두 개를 따로 쓰고, 나중에 적절한 PromQL 표현식으로 결과를 결합할 수 있어요.
관측의 합과 개수는 둘 다 가산적이므로, rate 이후이면서 나눗셈 이전이라면 기본 메트릭 타입이 무엇이든 쉽게 집계할 수 있어요. 각 job에 대한 평균 요청 지속시간을 계산하는 표현식은 다음과 같아요.
네이티브 히스토그램:
sum by (job) (histogram_sum(rate(http_request_duration_seconds[5m])))
/
sum by (job) (histogram_count(rate(http_request_duration_seconds[5m])))
요약 또는 클래식 히스토그램:
sum by (job) (rate(http_request_duration_seconds_sum[5m]))
/
sum by (job) (rate(http_request_duration_seconds_count[5m]))
버킷화 (Bucketing)
히스토그램은 본질적으로 버킷화된 카운터라서, 히스토그램을 요약과 구분짓는 가장 명백한 사용 사례는 특정 버킷에 속하는 관측을 세는 것이에요.
코드를 클래식 히스토그램으로 계측하면 고정된 버킷 경계를 구성하게 돼요. 이 클래식 히스토그램을 프로메테우스가 클래식 방식을 그대로 수집하게 두면, 그렇게 구성된 각 버킷은 채워지든 아니든 _bucket이라는 접미사가 붙은 시리즈를 만들어요. 버킷이 많을수록 다양한 쿼리에서 더 많은 옵션과 정확도를 주지만(아래 참고), "버킷당 하나의 시리즈"라는 비용은 꽤 상당해요.
클래식 히스토그램을 NHCB로 수집하면 채워지지 않은 버킷의 비용은 무시할 만하고, 채워진 버킷도 더 효율적으로 처리돼요. 각 NHCB가 (각 버킷과 관측의 합·개수에 대한 별도 부동소수점 시리즈보다는) 합성 샘플 단일 시리즈로 표현되기 때문이에요.
하지만 올바른 버킷을 미리 고르는 것은 어려울 수 있어요. 그리고 나중에 버킷을 바꾸는 것은 많은 혼란을 만든다(아래에서 볼 거예요). 코드를 직접 네이티브 히스토그램으로 계측하면 버킷 경계를 명시적으로 고르지 않고, 원하는 해상도(resolution)를 구성해요. 버킷은 -Inf에서 +Inf까지의 전체 부동소수점 범위를 덮는 지수 버킷 스키마를 따라 동적으로 만들어져요. 해상도가 높을수록 리소스 사용이 늘어나지만, 일반적으로 같은 리소스 비용으로 클래식 히스토그램보다 훨씬 높은 해상도에 도달할 수 있어요. 계측 라이브러리는 또한 히스토그램의 주기적 리셋이나 적응형 해상도 축소 같은, 채워진 버킷 수를 제한하는 다양한 전략을 제공해요. 자세한 내용은 사용 중인 계측 라이브러리의 문서를 보세요.
네이티브 히스토그램을 기반으로 특정 범위에 속하는 관측의 비율을 쿼리하려면 다음과 같은 표현식을 사용해요.
histogram_fraction(0, 0.3, sum by (job) (rate(http_request_duration_seconds[5m])))
이것은 지난 5분간 0ms에서 300ms 사이에 걸린 각 job의 HTTP 요청 비율을 계산해요. (300ms는 여기서 0.3초로 표현됐는데, 프로메테우스에서는 항상 기본 단위를 사용해야 하기 때문이에요.) sum이 관련 히스토그램의 해당 버킷들을 합산하여 올바르게 집계한다는 점에 주목하세요. 히스토그램의 버킷 레이아웃이 다르면 먼저 조정(reconcile)돼요. 일반적인 지수 버킷 스키마에서는 본질적으로 관련된 모든 히스토그램 중 가장 낮은 공통 해상도로 폴백함으로써 매끄럽게 작동해요. 시간에 따른 다른 버킷 레이아웃을 조정하는 것도 (rate 계산에 사용되는 5m 범위에서) 동일하게 이뤄져요. NHCB의 경우 그 효과는 서로 다른 버킷 레이아웃의 세부 사항에 크게 의존해요. 조정된 집계 히스토그램에 모든 관측을 담은 버킷 하나만 남는 것도 충분히 가능해요. 잠재적으로 심각한 효과 때문에, NHCB를 조정해야 했다면 쿼리 결과에 info 레벨 주석이 붙어요. 이것은 동적 지수 버킷을 가진 네이티브 히스토그램이 다루기 훨씬 쉬운 이유 중 하나이기도 해요.
계산된 비율은 정확히 0.3에 버킷 경계가 있다면 정확해요. 그렇지 않은 일반적인 경우에는 보간(interpolation)을 사용해 추정 비율을 반환해요. 이 추정은 버킷 해상도가 높을수록 더 정확해요. 예를 들어 300ms 안에 요청의 95%를 서빙해야 하는 SLO를 미리 알고 있다면, 정확한 계산을 허용하도록 클래식 히스토그램의 고정 버킷 경계를 사용할 수 있어요. 하지만 나중에 SLO가 바뀌면 고정 버킷 레이아웃을 그에 맞게 바꾸는 것이 꽤 지루할 거예요. (코드 계측을 바꿔야 하고, 위에서 설명한 서로 다른 버킷 레이아웃을 조정하는 문제도 겪게 될 거예요.) 동적 지수 버킷을 가진 네이티브 히스토그램을 고른다면 정확히 0.3에 버킷 경계가 생기진 않지만, 괜찮은 해상도라면 보간된 추정이 여전히 꽤 정확해요. 그 대가로 범위 경계를 마음대로 바꿀 자유를 얻는데, 이는 SLO가 바뀔 때뿐 아니라 "지난 분기의 데이터로 더 엄격한 SLO를 유지할 수 있을까?" 같은 질문을 탐구할 때도 유용해요.
클래식 히스토그램으로 수집된 클래식 히스토그램의 순수 레거시 경우에는, 해당 PromQL 표현식이 꽤 달라 보여요.
sum by (job) (rate(http_request_duration_seconds_bucket{le="0.3"}[5m]))
/
sum by (job) (rate(http_request_duration_seconds_count[5m]))
le 라벨 이름은 "less or equal"(작거나 같음)을 뜻해요. 이 라벨의 값은 누적 버킷의 상한 포함 경계예요(즉 이 버킷은 0.3보다 작거나 같은 모든 관측을 포함하며, 여기엔 음수 관측도 포함되는데 요청 지속시간을 관측하는 경우엔 일어나지 않을 거라고 가정해요).
이 표현식은 0.3에 구성된 버킷 경계를 엄격히 요구한다는 점에 주목하세요. 관련된 히스토그램에 그 경계의 버킷이 없다면 보간이 적용되지 않아요. 추정 대신 결과가 전혀 반환되지 않아요. 관련 히스토그램 중 일부만 그런 버킷을 가진다면 불완전한 결과가 반환되는데 아무 경고도 없어서, 상당히 나쁜 상황이에요. (힌트: 이 "순수 클래식" 경우를 피하세요. 가능하다면 클래식 히스토그램을 NHCB로 수집하거나, 애초에 네이티브 히스토그램으로 계측하세요.)
Apdex 점수 (Apdex score)
특정 지속시간 범위 안에 서빙된 요청의 비율에 대해 읽다 보면 Apdex 점수가 떠오를 수 있어요. 이 점수에 대해 목표 요청 지속시간과 허용된(tolerated) 요청 지속시간(보통 목표의 4배)을 설정해요. 목표 요청 지속시간이 300ms이고 허용 가능 요청 지속시간이 1.2s라고 해 봐요. 지난 5분에 걸쳐 job별 Apdex 점수를 계산하고 싶다면, 네이티브 히스토그램(NHCB 포함)의 PromQL 표현식은 단순해요. 지속시간 목표 안의 요청 비율에 목표와 허용 시간 사이의 요청 비율의 절반을 더하면 돼요.
histogram_fraction(0, 0.3, sum by (job) (rate(http_request_duration_seconds[5m])))
+
histogram_fraction(0.3, 1.2, sum by (job) (rate(http_request_duration_seconds[5m]))) / 2
"순수 클래식" 경우에는 정확한 경계에 버킷이 있어야 해요(그 대가로 정확한 계산을 준답). 해당 PromQL 표현식은 클래식 버킷이 누적적이기 때문에 꽤 달라 보여요.
(
sum by (job) (rate(http_request_duration_seconds_bucket{le="0.3"}[5m]))
+
sum by (job) (rate(http_request_duration_seconds_bucket{le="1.2"}[5m]))
)
/
2
/
sum by (job) (rate(http_request_duration_seconds_count[5m]))
(단순함을 위해 위 표현식들은 엄격하게 올바른 Apdex 계산에서 요구되는 대로 만족·허용 부분에서 실패한 요청을 명시적으로 제외하지 않아요.)
분위수 (Quantiles)
요약과 히스토그램 모두 0 ≤ φ ≤ 1인 소위 φ-분위수를 계산하는 데 사용할 수 있어요. φ-분위수는 N개의 관측 중 φ*N번째 순위에 해당하는 관측값이에요. φ-분위수의 예: 0.5-분위수는 중앙값으로 알려져 있어요. 0.95-분위수는 95번째 백분위수라고도 불러요.
요약과 히스토그램의 본질적인 차이는, 요약은 계측된 프로그램 안에서 스트리밍 φ-분위수를 계산해 직접 노출하는 반면, 히스토그램은 버킷화된 관측 개수를 노출하고 히스토그램 버킷에서 분위수 계산은 histogram_quantile() 함수를 사용해 Prometheus 서버에서 발생한다는 것이에요. 히스토그램은 네이티브 히스토그램과 클래식 히스토그램으로 더 나뉘어요. 다음 표는 서로 다른 접근 방식의 몇 가지 함의를 나열해요.
| 네이티브 히스토그램 | 클래식 히스토그램 | 요약 | |
|---|---|---|---|
| 계측 중 필요한 구성 | 원하는 해상도를 고르고 버킷 수를 제한할 전략을 선택 | 관측값의 예상 범위와 원하는 쿼리에 적합한 버킷 선택 | 원하는 φ-분위수와 슬라이딩 윈도우 선택. 다른 φ-분위수와 슬라이딩 윈도우는 나중에 계산 불가 |
| 계측 비용 | 관측은 카운터만 증가시키면 되므로 저렴 | 관측은 카운터만 증가시키면 되므로 저렴 | 스트리밍 분위수 계산 때문에 관측이 상대적으로 비쌈 |
| 쿼리 성능 | 서버가 복잡한 히스토그램 샘플에서 분위수를 계산해야 함. ad-hoc 계산이 너무 오래 걸리면(예: 큰 대시보드에서) 기록 규칙을 사용할 수 있음 | 서버가 많은 수의 버킷 시리즈에서 분위수를 계산해야 함. ad-hoc 계산이 너무 오래 걸리면 기록 규칙을 사용할 수 있음 | 빠름(서버에서 분위수 계산이 없고, 아래 참고처럼 집계도 어차피 불가능) |
| 히스토그램/요약당 타임시리즈 수 | 하나(합성 샘플 타입으로) | _sum, _count, 그리고 각 구성 버킷당 하나 |
_sum, _count, 그리고 각 구성 분위수당 하나 |
| 분위수 오류(아래 세부 참고) | 구성된 해상도에 의해 제한 | 오류는 분위수가 위치한 버킷의 너비에 의해 제한 | 구성 가능하며 일반적으로 매우 낮음 |
| φ-분위수와 슬라이딩 시간 창의 지정 | PromQL 표현식으로 ad-hoc | PromQL 표현식으로 ad-hoc | 계측 중 사전 구성 |
| 집계 | PromQL 표현식으로 ad-hoc, 버킷은 항상 호환 | PromQL 표현식으로 ad-hoc, 버킷 경계에 변화가 없을 때만 | 집계 불가 |
위에서 언급했듯, 클래식 히스토그램은 NHCB(Native Histograms with Custom Bucket boundaries)라는 네이티브 히스토그램의 특수한 형태로 Prometheus 서버에 수집될 수 있어요. 따라서 클래식 히스토그램과의 몇 가지 함의, 그리고 일반적인 네이티브 히스토그램과의 몇 가지 함의를 공유해요. 계측 측면에서는 클래식 히스토그램과 정확히 똑같이 동작해요. (사실 그것은 클래식 히스토그램과 동일해요. NHCB는 클래식 히스토그램이 NHCB로 수집될 때만 서버 측에서 만들어지거든요.) 쿼리 성능과 타임시리즈 수는 일반적인 네이티브 히스토그램과 같지만, 분위수 오류는 해당하는 클래식 히스토그램과 같아요. NHCB는 클래식 히스토그램보다 버킷 레이아웃의 변경을 조금 더 우아하게 처리하지만, 그래도 문제가 있는 상황이에요(적어도 주석으로 그렇다고 표시돼요).
표의 마지막 항목의 중요성에 주목하세요. 300ms 안에 요청의 95%를 서빙하는 SLO로 돌아가 봐요. 이번에는 300ms 안에 서빙된 요청의 비율을 표시하는 대신 95번째 백분위수, 즉 요청의 95%를 서빙한 요청 지속시간을 표시하고 싶어요. 그렇게 하려면 0.95-분위수와 (예를 들어) 5분 감쇠 시간으로 요약을 구성하거나, 괜찮은 해상도로 네이티브 히스토그램을 구성하거나(예: Go 계측 라이브러리에서 NativeHistogramBucketFactor에 1.1 값을 사용), 300ms 지점 주변의 몇 개 버킷, 예: {le="0.1"}, {le="0.2"}, {le="0.3"}, {le="0.45"}로 클래식 히스토그램을 구성할 수 있어요. 서비스가 여러 인스턴스로 복제 실행된다면 각 인스턴스에서 요청 지속시간을 수집하고, 모든 것을 전체 95번째 백분위수로 집계하고 싶을 거예요. 하지만 요약에서 사전 계산된 분위수를 집계하는 것은 거의 의미가 없어요. 이 특정 경우에 분위수를 평균내는 것은 통계적으로 무의미한 값을 만들어 내요.
avg(http_request_duration_seconds{quantile="0.95"}) // BAD!
히스토그램을 사용하면 집계가 histogram_quantile() 함수로 완벽하게 가능해요.
네이티브 히스토그램 버전(NHCB 포함):
histogram_quantile(0.95, sum(rate(http_request_duration_seconds[5m]))) // GOOD.
클래식 히스토그램 버전:
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) // GOOD.
게다가 SLO가 바뀌어 이제 90번째 백분위수를 그리거나, 지난 5분 대신 지난 10분을 고려하고 싶다면, 위 표현식만 조정하면 되고 감시 대상 프로그램의 계측을 재구성할 필요가 없어요.
분위수 추정의 오류 (Errors of quantile estimation)
분위수는 계측된 바이너리나 Prometheus 서버에서 계산되든 추정된 값이에요. 그 추정의 오류를 이해하는 것이 중요해요.
위의 히스토그램 예를 계속해서, 평소 요청 지속시간이 거의 모두 220ms에 아주 가깝다고 상상해 봐요. 다른 말로 하면 매우 높은 해상도의 히스토그램에서 220ms에 아주 날카로운 스파이크를 볼 것이고, "진짜" 95번째 백분위수도 220ms에 가까워요.
NativeHistogramBucketFactor 1.1(Go 계측 예를 따르면)로, 이 스파이가 들어갈 버킷은 하한이 대략 0.210이고 상한이 대략 0.229예요. (이 문서는 이 경계가 어떻게 계산되는지 세부를 설명하는 것을 의도적으로 피해요. 세부는 앞서 언급한 명세를 보세요.) 간단히 하기 위해 실제로 모든 요청이 이 버킷에 들어간다고 가정해 봐요. histogram_quantile의 보간 로직은 그러면 95번째 백분위수를 228ms로 추정할 거예요(여기서도 계산 세부는 흘려보내요). 하지만 위의 버킷 경계를 고려하면 진짜 값은 버킷 안의 실제 요청 분포에 따라 210ms와 229ms 사이 어디든 될 수 있어요. 그래서 이것은 최악의 경우에도 꽤 정확한 추정이에요(진짜 값이 추정값 228ms보다는 210ms일 수 있지만요).
이제 같은 것을 위에서 구성한 대로 클래식 히스토그램에 적용해 봐요. 모든 관측, 따라서 95번째 백분위수도 {le="0.3"}으로 라벨된 버킷, 즉 200ms에서 300ms 버킷에 들어갈 거예요. 이 경우 보간은 295ms로 추정하고, 진짜 값이 200ms와 300ms 사이임을 보장해요. 오류 여백이 훨씬 더 클 뿐만 아니라, 추정값 295ms도 네이티브 히스토그램(추정이 228ms)의 경우보다 진짜 값 220ms에서 훨씬 멀어요. SLO가 95번째 백분위수에 대해 300ms라고 주어졌을 때, 클래식 히스토그램은 그것을 위반하는 데 매우 가까워 보이는 인상을 주지만, 실제로는 여전히 꽤 잘 지키고 있어요.
사고 실험의 다음 단계: 백엔드 라우팅의 변경이 모든 요청 지속시간에 고정 100ms를 더해요. 이제 요청 지속시간은 320ms에 날카로운 스파이가 있어요.
네이티브 히스토그램의 관련 버킷은 297ms에서 324ms까지 범위를 가지며(여기서도 계산 방법을 말하지 않고 숫자만 제시해요), 95번째 백분위수에 대한 보간 추정은 323ms예요. 거의 완벽한 추측이에요.
하지만 클래식 히스토그램은 거의 모든 관측을 300ms에서 450ms 버킷에서 볼 거예요. 95번째 백분위수는 443ms로 추정되는데, 320ms에 가까운 올바른 값과는 거리가 멀어요. SLO 바깥으로 아주 조금 벗어났지만, 추정된 95번째 분위수는 훨씬 더 나빠 보여요.
요약은 두 경우 모두에서 정확한 백분위수 값을 매우 정확하게 계산하는 데 문제가 없었을 거예요. 적어도 (Go 계측 라이브러리가 사용하는 것 같은) 적절한 알고리즘을 사용한다면요 — 이 알고리즘은 우리 예처럼 좁은 분포에 매우 정확한 결과를 낼 거예요. 불행히도 여러 인스턴스의 관측을 집계해야 한다면 요약을 사용할 수 없어요.
다행히 클래식 히스토그램에 적절한 버킷 경계를 고른 덕분에, 관측값 분포에 아주 날카로운 스파이가 있는 이 인위적인 예에서 클래식 히스토그램은 SLO 안에 있는지 밖에 있는지 올바르게 식별할 수 있었어요(SLO를 위반하거나 지키는 데서 얼마나 떨어져 있는지 말해 주는 것은 나빴지만요). 하지만 분위수의 실제 값이 SLO에 가까울수록(다시 말해 실제로 가장 관심 있는 값일수록) 계산된 값이 더 정확해져요.
이제 실험을 한 번 더 수정해 봐요. 새 설정에서 요청 지속시간 분포는 150ms에 스파이가 있지만, 이전만큼 날카롭진 않고 관측의 90%만 포함해요. 관측의 10%는 150ms와 450ms 사이의 긴 꼬리에 고르게 퍼져 있어요. 이 분포에서 95번째 백분위수는 정확히 SLO인 300ms에 있어요. 클래식 히스토그램에서 계산된 값은 95번째 백분위수의 값이 구성된 버킷 경계 중 하나와 우연히 일치하므로 이 (인위적인) 경우에 정확할 거예요. 약간 다른 값이라도 관련 버킷 안의 고른 분포가 클래식 히스토그램의 보간 알고리즘이 가정하는 것과 정확히 일치하므로 여전히 정확할 거예요.
요약이 보고하는 분위수의 오류는 여기서 더 흥미로워져요. Go 계측 라이브러리의 경우 요약의 분위수 오류는 φ 차원으로 구성돼요. 우리 경우 0.95±0.01을 구성했을 수 있는데, 즉 계산된 값이 94번째와 96번째 백분위수 사이가 된다는 뜻이에요. 위에서 설명한 분포의 94번째 분위수는 270ms이고, 96번째 분위수는 330ms예요. 요약이 보고하는 95번째 백분위수의 계산된 값은 270ms와 330ms 사이 어디든 될 수 있고, 불행히도 그것은 SLO 안에 명확히 있는 것과 밖에 명확히 없는 것 사이의 모든 차이예요.
결론은 이렇습니다: 요약을 사용하면 φ 차원에서 오류를 제어해요. 히스토그램을 사용하면 클래식 히스토그램의 경우 적절한 버킷 레이아웃을 고름으로써(어려움), 네이티브 히스토그램의 경우 버킷 해상도를 고름으로써(쉬움), 관측값 차원에서 오류를 제어해요. 넓은 분포에서는 φ의 작은 변화가 관측값의 큰 편차를 만들어 내요. 날카로운 분포에서는 관측값의 작은 구간이 φ의 큰 구간을 덮어요.
엄지손가락 규칙은 다음과 같아요.
-
네이티브 히스토그램에 접근할 수 있다면, 정확도 요구 사항에 맞는 해상도로 사용해요. 이것은 필요한 정확도를, PromQL 표현식을 통해 ad-hoc으로 집계하고 (백분위수, 슬라이딩 윈도우) 파라미터를 바꾸는 능력과 결합해요.
-
네이티브 히스토그램을 사용할 수 없지만 집계가 필요하다면, 클래식 히스토그램을 사용해야 해요. 이는 올바른 값 범위를 덮도록 적절한 버킷 경계를 설정하고, 버킷 비용과 필요한 정확도 사이의 올바른 트레이드오프를 찾아야 해요.
-
집계가 필요 없을 때만 요약을 생각하기 시작할 수 있어요. 주요 장점은 상대적으로 낮은 전체 비용으로 (φ 차원에서) 매우 정확한 분위수 추정을 준다는 것이에요. 하지만 계측 시점에 원하는 분위수와 슬라이딩 윈도우를 골라야 하는 추가 요구 사항은 요약의 또 다른 심각한 단점이에요.
시각화 (Visualization)
요약의 사전 계산된 분위수는 다른 부동소수점 타임시리즈처럼 시각화할 수 있지만, 히스토그램을 시각화하는 것은 더 복잡해요. Prometheus UI는 Table 뷰에서 단일 히스토그램 샘플의 그래픽 표현을 보여 줘요. 하지만 Graph 뷰에서는 클래식 히스토그램의 각 컴포넌트 시리즈를, 네이티브 히스토그램의 경우엔 관측의 합만 단순히 플롯해요.
시간에 따른 히스토그램의 매우 유용한 시각화는 히트맵(heatmap)이에요. Prometheus UI는 아직 히트맵을 지원하지 않아요(추적 이슈 참고). 하지만 Perses나 Grafana 같은 인기 있는 대시보드 도구는 Prometheus 히스토그램을 기반으로 히트맵을 렌더링할 수 있어요. 클래식 히스토그램의 해상도는 보통 인상적인 히트맵을 만들 만큼 높지 않지만, 네이티브 히스토그램으로 가능한 더 높은 해상도는 매우 상세한 히트맵을 가능하게 해요.
더 알아보기 (Learn more)
- 계측 (Instrumentation) — 코드에 메트릭 심기
- 알림 (Alerting) — 증상 기반 알림 설계
- 네이티브 히스토그램 명세 — 네이티브 히스토그램의 기술적 세부
- 메트릭 타입 — 카운터, 게이지, 히스토그램, 요약
- histogram_quantile 함수 — 분위수 계산 쿼리 함수