네이티브 히스토그램
네이티브 히스토그램 (Native Histograms)
프로메테우스의 핵심 데이터 구조인 "네이티브 히스토그램"을 완전히 설명하는 공식 사양 문서예요. 기존의 "클래식 히스토그램"이 고정 버킷으로 나뉘어 저장되던 것과 달리, 네이티브 히스토그램은 히스토그램 자체를 프로메테우스 데이터 모델의 1급(일급) 시민으로 승격시킨 개념이에요. 데이터 모델, exposition 형식, 저장(TSDB), PromQL 처리, 마이그레이션까지 프로메테우스 스택 전체에 걸친 내용이 담겨 있어요.
네이티브 히스토그램은 2022년 11월 실험 기능으로 도입됐고, 처음 지원한 프로메테우스 서버 버전은 v2.40.0이에요. --enable-feature=native-histograms 기능 플래그로 활성화해야 했죠. v3.8.0부터는 안정 기능으로 지원되지만, 스크레이핑은 여전히 scrape_native_histograms 설정으로 명시적으로 활성화해야 해요. 이 문서는 개념 설명부터 사양 수준의 상세까지 한곳에 모아둔 자료라서, 네이티브 히스토그램을 진지하게 쓰려는 사람에게 가장 권위 있는 참고 문서랍니다.
출처: 문서
본문
네이티브 히스토그램은 2022년 11월 실험 기능으로 도입됐어요. 이는 프로메테우스 스택의 거의 모든 부분에 닿는 개념이에요. 네이티브 히스토그램을 지원하는 첫 번째 프로메테우스 서버 버전은 v2.40.0이었고, --enable-feature=native-histograms 기능 플래그로 활성화해야 했어요. v3.8.0부터 네이티브 히스토그램은 안정 기능으로 지원돼요. 다만 네이티브 히스토그램 스크레이핑은 여전히 scrape_native_histograms 구성 설정으로 명시적으로 활성화해야 해요. 기능 플래그에서 구성 설정으로의 전환을 쉽게 하기 위해, v3.8에서 기능 플래그를 설정하면 남은 효과는 scrape_native_histograms를 기본값 true로 설정하는 것뿐이에요. v3.9부터는 기능 플래그가 진정한 no-op이 되며 명시적으로 scrape_native_histograms를 설정해야 해요. Remote-Write로 보내는 것은 send_native_histograms remote write 구성으로 활성화해야 해요. (v4부터는 scrape_native_histograms와 send_native_histograms 둘 다 기본값 true가 됩니다.)
네이티브 히스토그램과 관련된 변경 사항이 방대하기 때문에, 그 변경 사항과 근본 개념에 대한 문서는 다양한 채널(영향을 받는 프로메테우스 컴포넌트의 문서, 소스 코드의 doc comment, 때로는 소스 코드 자체, 설계 문서, 컨퍼런스 발표 등)에 널리 퍼져 있어요. 이 문서는 이런 모든 정보 조각을 모아 통일된 맥락에서 간결하게 제시하려고 해요. 이 문서는 기존의 상세 문서를 다시 쓰기보다 링크하는 것을 선호하지만, 다른 출처를 참조하지 않고도 이해할 수 있을 만큼 충분한 정보를 포함하고 있어요. 그렇지만 이 문서는 초보자를 위한 입문서로도 적합하지 않고 개발자의 필요에 초점을 맞추지도 않는다는 점을 유의해야 해요. 전자에 대해서는 히스토그램과 요약에 대한 Best Practices 문서의 업데이트 버전을 제공할 계획이에요. (TODO: 블로그 포스트나 어쩌면 시리즈도.) 후자에 대해서는 Carrie Edward의 Developer's Guide to Prometheus Native Histograms가 있어요.
형식 사양은 각각의 맥락에서 이뤄져야 하겠지만(예: OpenMetrics 변경은 일반 OpenMetrics 사양에서 지정될 것), 이 문서의 일부 부분은 사양의 형태를 띠고 있어요. 그 부분에서는 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", "OPTIONAL" 키워드가 RFC 2119에서 설명된 대로 사용돼요.
이 기능이 안정적이고 v4.0.0 전에 파괴적 변경을 기대하지 않음에도 이 문서에는 여전히 많은 TODO가 있어요. 이 TODO는 문서 완성, 사소한 문제 수정, 추가 기능을 위한 알림이에요.
서론 (Introduction)
네이티브 히스토그램의 핵심 아이디어는 히스토그램을 프로메테우스 데이터 모델에서 1급 시민으로 취급하는 것이에요. 히스토그램을 "네이티브" 샘플 타입으로 승격하는 것은 아래에 나열된 핵심 속성의 근본적인 전제 조건이며, 이것이 네이티브 히스토그램이라는 이름을 선택한 이유를 설명해요.
네이티브 히스토그램이 도입되기 전에는 모든 프로메테우스 샘플 값이 64비트 부동 소수점 값(줄여서 float64 또는 그냥 float)이었어요. 이 float는 게이지나 카운터를 직접 나타낼 수 있어요. exposition 형식에 존재하는 summary와 (클래식 버전의) histogram 프로메테우스 메트릭 타입은 수집 시 float 구성 요소로 분해돼요: 두 타입 모두 sum과 count 구성 요소, summary는 여러 quantile 샘플, (클래식) histogram은 여러 버킷 샘플로요.
네이티브 히스토그램으로 새 구조화 샘플 타입이 도입돼요. 단일 샘플이 이전에 알려진 sum과 count에 더해 동적 버킷 세트를 나타내요. 이는 수집에 국한되지 않으며, PromQL 표현식도 이전에는 float 샘플만 반환할 수 있었던 곳에서 새 샘플 타입을 반환할 수 있어요.
네이티브 히스토그램은 다음과 같은 핵심 속성을 가져요:
- 스파스(sparse) 버킷 표현으로, 빈 버킷에 대한 (거의) 제로 비용
- float64 값의 전체 범위를 포함
- 계측 시 버킷 경계 구성이 불필요
- 간단한 구성 파라미터에 따라 선택되는 동적 해상도
- 정교한 지수 버킷팅 스키마로, 그 스키마를 사용하는 모든 히스토그램 간 병합 가능성 보장
- exposition과 저장 모두에 효율적인 데이터 표현
이 핵심 속성들은 표준 버킷팅 스키마에서 완전히 실현돼요. 다른 트레이드오프를 가진 다른 스키마는 이 속성의 부분집합만 가질 수 있어요. 자세한 내용은 아래 Schema 섹션을 참조하세요.
이전에 존재하던 "클래식" 히스토그램과 비교해 네이티브 히스토그램(표준 버킷팅 스키마)은 거의 설정이 필요 없거나 전혀 없이, 임의의 관측값 범위에 걸쳐 더 높은 버킷 해상도를 더 낮은 저장·쿼리 비용으로 허용해요. 심지어 레이블로 히스토그램을 파티셔닝하는 것도 훨씬 저렴해졌어요.
스파스 표현(위 목록의 속성 1)이 네이티브 히스토그램의 많은 다른 이점의 핵심이기 때문에, 스파스 히스토그램(sparse histograms)은 설계 초기에 네이티브 히스토그램의 흔한 이름이었어요. 다만 지수 버킷팅 스키마나 동적 버킷 같은 다른 핵심 속성도 매우 중요하지만, 스파스 히스토그램이라는 용어에는 전혀 잡히지 않아요.
설계 문서 (Design docs)
다음은 네이티브 히스토그램의 개발을 이끈 설계 문서들이에요. 일부 세부 사항은 이제 구식이지만, 근본 개념과 그것이 어떻게 진화했는지를 잘 설명해요.
- Sparse high-resolution histograms for Prometheus — 원본 설계 문서
- Prometheus Sparse Histograms and PromQL — 히스토그램의 PromQL 처리에 관한 문서라기보다 탐구 문서에 가까운 것
컨퍼런스 발표 (Conference talks)
네이티브 히스토그램에 대해 더 접근하기 쉬운 학습 방법은 컨퍼런스 발표를 보는 것이며, 그중 일부를 아래에 소개해요. 입문으로는 이 발표들을 먼저 보고 이 문서로 돌아와 모든 세부 사항과 기술을 배우는 것이 좋을 수 있어요.
- Secret History of Prometheus Histograms — 클래식 히스토그램과 프로메테우스가 왜 그렇게 오래 유지했는지
- Prometheus Histograms – Past, Present, and Future — 네이티브 히스토그램으로 이어진 새 접근의 첫 발표
- Better Histograms for Prometheus — 왜 그 개념이 실전에서 효과가 있는지
- Native Histograms in Prometheus — 실제 구현 이후 네이티브 히스토그램을 소개·설명
- PromQL for Native Histograms — PromQL에서 네이티브 히스토그램 사용 설명
- Prometheus Native Histograms in Production — 성능과 리소스 소비 분석
- Using OpenTelemetry's Exponential Histograms in Prometheus — OpenTelemetry와의 상호운용성
용어 (Glossary)
- 네이티브 히스토그램은 이 문서가 다루는 전체 히스토그램을 나타내는 새 복합 샘플 타입의 인스턴스예요. 맥락이 충분히 명확하면 아래에서 그냥 히스토그램이라고 부르는 경우가 많아요
- 클래식 히스토그램은 고정 버킷을 가진 히스토그램을 나타내는 이전 샘플 타입의 인스턴스로, 이전에는 그냥 히스토그램이라고 불렸어요. exposition 형식에서는 그렇게 존재하지만 프로메테우스에 수집되면 여러 float 샘플로 분해돼요
- 스파스 히스토그램은 네이티브 히스토그램의 이전이자 현재는 deprecated된 이름이에요. 이 이름은 여전히 오래된 문서에서 가끔 발견될 수 있어요. 스파스 버킷은 네이티브 히스토그램의 버킷에 대한 의미 있는 용어로 남아 있어요
데이터 모델 (Data model)
이 섹션은 네이티브 히스토그램의 데이터 모델을 일반적으로 설명해요. 가능한 한 구현 세부 사항을 피해요. 이는 용어를 포함해요. 예를 들어 이 섹션에서 설명하는 목록은 protobuf 구현에서는 repeated message가 되고 Go 구현에서는 (대부분) slice가 돼요.
일반 구조 (General structure)
클래식 히스토그램과 유사하게 네이티브 히스토그램은 관측치 count 필드와 관측치 sum 필드를 가져요. 관측치 count는 일반적으로 음수가 아니지만(유일한 예외는 PromQL의 중간 결과), 관측치 sum은 임의의 float64 값일 수 있어요.
추가로 네이티브 히스토그램은 아래의 전용 섹션에서 자세히 설명하는 다음 구성 요소를 포함해요:
- 인덱스 i를 가진 주어진 버킷의 경계를 결정하는 방법을 식별하는 스키마
- 양수와 음수 관측치에 대해 미러링된 인덱스 버킷의 스파스 표현
- 0에 가까운 관측치를 세는 제로 버킷
- (아마 비어 있는) 사용자 정의 값 목록
- Exemplar
맛 (Flavors)
모든 네이티브 히스토그램은 두 개의 독립적인 차원 각각을 따라 특정 맛(flavor)을 가져요:
- counter 대 gauge: 보통 히스토그램은 "counter형"이에요. 즉 각 버킷이 관측치의 카운터로 동작해요. 하지만 각 버킷이 게이지인 "gauge형" 히스토그램도 있어서 특정 시점의 임의 분포를 나타내요. gauge 히스토그램 개념은 이전에 OpenMetrics로 클래식 히스토그램에 도입됐어요
- 정수 대 부동 소수점(줄여서 float): 히스토그램의 명백한 사용 사례는 관측치를 세는 것이며, 각 버킷(제로 버킷 포함)과 총 관측치 count에서 0 이상의 정수 관측치 수가 되어 서명 없는 64비트 정수(줄여서 uint64)로 표현돼요. 하지만 모든 값이 64비트 부동 소수점(줄여서 float64)으로 표현되는 "가중" 또는 "스케일" 히스토그램으로 이어지는 특정 사용 사례가 있어요. 관측치 sum은 어떤 경우든 float64라는 점에 유의하세요
float 히스토그램은 "가중" 관측치를 위한 직접 계측에서 가끔 사용돼요. 예를 들어 관측된 값이 히스토그램의 다른 버킷에 얼마나 많은 초를 보냈는지 세는 경우예요. 하지만 float 히스토그램의 훨씬 더 흔한 사용 사례는 PromQL 내부예요. PromQL은 일반적으로 float 값에만 동작하므로 PromQL 엔진은 TSDB에서 가져온 모든 히스토그램을 먼저 float 히스토그램으로 변환하고, 기록 규칙을 통해 TSDB에 다시 저장되는 모든 히스토그램은 float 히스토그램이에요. 그런 히스토그램이 효과적으로 정수 히스토그램이라면(sum이 아닌 모든 필드의 값이 uint64로 정확히 표현될 수 있기 때문에), TSDB 구현은 저장 효율을 높이기 위해 정수 히스토그램으로 다시 변환할 수(MAY) 있어요. (프로메테우스 v3.00 기준으로 프로메테우스 내부의 TSDB 구현은 이 옵션을 활용하지 않아요.) 다만 counter 히스토그램에 적용되는 가장 흔한 PromQL 함수는 rate이며, 이것은 일반적으로 정수가 아닌 숫자를 생성하므로 기록 규칙의 결과는 어쨌든 흔히 정수가 아닌 값의 float 히스토그램이 될 것이라는 점에 유의하세요.
PromQL 표현식은 "음수" 히스토그램(예: 히스토그램에 -1을 곱함)도 만들 수 있어요. 그런 음수 히스토그램은 중간 결과로만 허용되며 그 외에는 유효하지 않은 것으로 간주돼요. 어떤 교환 형식(exposition 형식, remote-write, OTLP)으로도 표현할 수 없고 TSDB에도 저장할 수 없어요. 음수 히스토그램에 대한 상세 섹션도 참조하세요.
네이티브 히스토그램을 명시적으로 정수 히스토그램 대 float 히스토그램으로 취급하는 것은, 단순함을 위해 전체 스택에서 항상 float로 취급되는 기존의 단순 숫자 샘플 처리와 뚜렷한 차이가 돼요.
히스토그램을 더 복잡하게 취급하는 주된 이유는 protobuf 기반 exposition 형식에서 쉬운 효율성 이득 때문이에요. Protobuf는 정수에 varint 인코딩을 사용해 추가 압축 계층 없이도 작은 정수 값의 데이터 크기를 줄여줘요. 이 이점은 정수 버킷의 델타 인코딩으로 증폭되는데, 일반적으로 더 작은 정수 값이 돼요. 반대로 float는 protobuf에서 항상 8바이트를 요구해요. 실제로 정수 히스토그램의 많은 정수는 1바이트에 들어가고 대부분은 2바이트에 들어가므로, protobuf-exposition 형식에서 정수 히스토그램의 명시적 존재는 버킷이 많은 히스토그램에서 데이터 크기 감소가 8배에 육박하게 돼요. 이는 계측된 타깃이 노출하는 압도적 다수의 히스토그램이 정수 히스토그램이므로 특히 관련이 있어요.
비슷한 이유로 RAM과 디스크에서의 정수 히스토그램 표현은 일반적으로 float 히스토그램보다 효율적이에요. 다만 이는 exposition 형식에서의 이점보다는 덜 관련 있어요. 우선 프로메테우스는 float에 Gorilla 스타일 XOR 인코딩을 사용해 크기를 줄이지만, 정수에 사용되는 double-delta 인코딩만큼은 아니에요. 더 중요한 것은 구현이 효과적으로 정수 값인 히스토그램 필드에 내부적으로 정수 표현을 사용하기로 결정할 수 있다는 점이에요(위 참조). (역사적 참고: 프로메테우스 v1은 float 샘플의 압축을 개선하기 위해 정확히 이 접근을 사용했고, 프로메테우스 v3는 미래에 이 접근을 다시 채택할 가능성이 매우 높아요.)
counter 히스토그램에서 총 관측치 count와 버킷의 count는 개별적으로 프로메테우스 카운터처럼 동작해요. 즉 카운터 리셋 시에만 감소해요. 다만 관측 sum은 음수 값의 관측 결과로 감소할 수 있어요. PromQL 구현은 히스토그램 전체를 기준으로 카운터 리셋을 감지해야(MUST) 해요. (자세한 내용은 아래 카운터 리셋 고려사항 섹션을 참조하세요.) (이것은 항상 클래식 히스토그램과 summary의 sum 구성 요소에 대한 문제였음을 유의하세요. 지금까지의 접근은 그 경우 sum에 대한 카운터 리셋 감지가 조용히 깨지는 것을 받아들이는 것이었어요. 다행히 음수 관측치는 프로메테우스 히스토그램과 summary의 매우 드문 사용 사례예요.)
스키마 (Schema)
스키마는 8비트 크기의 서명된 정수 값(줄여서 int8)이에요. 버킷 경계가 계산되는 방식을 정의해요. 현재 유효한 값은 -53과 -4에서 +8 사이(더 큰 범위인 -9에서 +52가 예약됨, 자세한 내용은 아래)예요. 향후 더 많은 스키마가 추가될 수 있어요. -53은 소위 사용자 정의 버킷 경계(줄여서 사용자 정의 버킷)를 위한 스키마이고, 다른 스키마 번호는 다양한 표준 지수 스키마(줄여서 표준 스키마)를 나타내요.
표준 스키마는 서로 병합 가능하며 일반적인 사용 사례에 RECOMMENDED(권장)돼요. 더 큰 스키마 번호는 더 높은 해상도에 해당해요. 스키마 n은 스키마 n+1 해상도의 절반을 가지며, 이는 스키마 n+1의 히스토그램이 이웃 버킷을 병합해 스키마 n의 히스토그램으로 변환될 수 있음을 의미해요.
어떤 표준 스키마 n에 대해서도 인덱스 i의 버킷 경계는 다음과 같이 계산돼요(Python 구문 사용):
- 양수 버킷의 포함 상한:
(2**2**-n)**i - 양수 버킷의 제외 하한:
(2**2**-n)**(i-1) - 음수 버킷의 포함 하한:
-((2**2**-n)**i) - 음수 버킷의 제외 상한:
-((2**2**-n)**(i-1))
i는 음수가 될 수 있는 정수예요.
float64로 표현할 수 있는 가장 크고 작은 유한 값(이하 MaxFloat64, MinFloat64)과 양수·음수 무한대 값(+Inf, -Inf)에 관한 위 규칙의 예외가 있어요:
MaxFloat64를 포함하는 (위 경계 공식에 따른) 양수 버킷은 (float64를 오버플로할 위 공식으로 계산된 한계 대신)MaxFloat64의 포함 상한을 가져요- 다음 양수 버킷(이전 항목의 버킷에 상대적인 인덱스 i+1)은
MaxFloat64의 제외 하한과+Inf의 포함 상한을 가져요. (양수 오버플로 버킷이라고 부를 수 있어요) MinFloat64를 포함하는 (위 경계 공식에 따른) 음수 버킷은 (float64를 언더플로할 위 공식으로 계산된 한계 대신)MinFloat64의 포함 하한을 가져요- 다음 음수 버킷(이전 항목의 버킷에 상대적인 인덱스 i+1)은
MinFloat64의 제외 상한과-Inf의 포함 하한을 가져요. (음수 오버플로 버킷이라고 부를 수 있어요) - 위에서 설명한
+Inf와-Inf버킷 너머의 버킷은 사용하면 안(MUST NOT) 돼요
0에 가까운 값에는 더 많은 예외가 있으며, 아래 제로 버킷 섹션을 참조하세요.
가장 낮은 해상도의 -4와 가장 높은 해상도의 8이라는 현재 한계는 실용적 유용성에 기반해 선택됐어요. 더 낮거나 높은 해상도의 실용적 필요가 생기면 범위 확장이 고려될 거예요. 다만 한 버킷에서 다음 버킷으로의 성장 계수가 표현 가능한 float64 숫자 간 차이보다 작아지므로 52보다 큰 스키마는 말이 안 돼요. 마찬가지로 성장 계수가 float64로 표현 가능한 가장 큰 float를 초과하므로 -9보다 작은 스키마도 말이 안 돼요. 따라서 -9와 +52 사이(포함)의 스키마 번호는 향후 표준 스키마를 위해 예약되며(위 버킷 경계 공식을 따름) 다른 어떤 스키마에도 사용하면 안(MUST NOT) 돼요.
네이티브 히스토그램의 수신자는 수집 시 적절히 버킷을 병합해 수신된 히스토그램의 스키마와 그에 따른 해상도를 줄일 수(MAY) 있어요. 수신자는 위 버킷 경계 공식을 따라 9에서 52 사이의 스키마를 수용하고 수집 시 유효한 숫자(즉 -4와 8 사이)로 줄일 수(MAY) 있어요.
이 선택적 스키마 변환 후에도 스키마가 여전히 수신자에게 알려지지 않았다면 다음 옵션이 있어요:
- 스크레이프(연합 포함)에 알 수 없는 스키마를 가진 히스토그램이 하나 이상 포함되면 불완전한 스크레이프를 피하는 프로메테우스 관행에 따라 전체 스크레이프가 실패해야(MUST) 해요
- 다른 수집 경로(WAL/WBL 재생 포함)의 경우 수신자는 알 수 없는 스키마를 가진 히스토그램을 무시할 수(MAY) 있고 이 생략을 적절한 방식으로 사용자에게 알려야(SHOULD) 해요
TSDB 구현이 영구 저장소에서(WAL/WBL 재생 제외) 히스토그램을 읽을 때도 유사한 지침이 적용돼요: 9에서 52 사이의 스키마는 유효한 스키마로 변환될 수(MAY) 있어요. 그렇지 않으면 알 수 없는 스키마는 검색 시 오류를 반환해야(MUST) 하고, 검색을 촉발한 PromQL 쿼리는 실패해야(MUST) 해요.
스키마 -53의 경우 버킷 경계는 아래 사용자 정의 값 섹션에서 자세히 설명하는 사용자 정의 값으로 명시적으로 설정돼요. 이는 사용자 정의 버킷 경계(줄여서 사용자 정의 버킷, NHCB로 더 자주 줄임)를 가진 네이티브 히스토그램이 돼요. 그런 히스토그램은 클래식 히스토그램을 네이티브 히스토그램으로 표현하는 데 사용할 수 있어요. 표준 스키마가 제공하는 지수 버킷팅이 히스토그램이 나타낼 분포에 안 맞을 때도 사용할 수 있어요. 서로 다른 사용자 정의 버킷 경계를 가진 히스토그램은 일반적으로 서로 병합할 수 없어요. 따라서 스키마 -53은 특정 사용 사례에서 정보에 입각한 결정으로만 사용해야(SHOULD) 해요.
버킷 (Buckets)
표준 스키마의 경우 버킷은 두 개의 목록으로 표현돼요. 하나는 양수 버킷용이고 하나는 음수 버킷용이에요. 사용자 정의 버킷(스키마 -53)의 경우 양수 버킷 목록만 사용되지만 모든 버킷에 재사용돼요.
인구가 없는 버킷은 목록에서 제외될 수(MAY) 있어요. (이것이 버킷을 종종 스파스 버킷이라고 부르는 이유예요.)
float 히스토그램의 경우 목록의 요소는 float64이며 버킷 인구를 직접 나타내요. 버킷 인구는 일반적으로 음수가 아니며, 유일한 예외는 PromQL의 중간 결과예요.
정수 히스토그램의 경우 목록의 요소는 서명된 64비트 정수(줄여서 int64)이고, 각 요소는 목록의 이전 버킷에 대한 델타로 버킷 인구를 나타내요. 각 목록의 첫 번째 버킷은 절대 인구를 포함해요(0에 대한 델타로도 볼 수 있음). 델타는 음수 절대 버킷 인구로 평가되면 안(MUST NOT) 돼요.
목록의 버킷을 이전 섹션에서 정의한 인덱스에 매핑하기 위해 양수 버킷용과 음수 버킷용으로 각각 하나씩 두 개의 소위 스팬(span) 목록이 있어요.
각 스팬은 숫자 한 쌍으로 구성돼요: 오프셋(offset)이라고 하는 서명된 32비트 정수(줄여서 int32)와 길이(length)라고 하는 서명 없는 32비트 정수(줄여서 uint32). 각 목록의 첫 번째 스팬만 음수 오프셋을 가질 수 있어요. 그것은 해당 버킷 목록의 첫 번째 버킷의 인덱스를 정의해요. (NHCB의 경우 인덱스는 항상 양수라는 점에 유의하세요. 자세한 내용은 사용자 정의 값 섹션을 참조하세요.) 길이는 버킷 목록이 시작하는 연속 버킷 수를 정의해요. 다음 스팬들의 오프셋은 제외된(따라서 인구가 없는) 버킷 수를 정의해요. 길이는 제외된 버킷 뒤에 따라오는 목록의 연속 버킷 수를 정의해요.
각 스팬 목록의 모든 길이 값의 합은 해당 버킷 목록의 길이와 같아야(MUST) 해요.
빈 스팬(길이가 0)은 유효하며 사용될 수(MAY) 있지만, 일반적으로 유용하지 않고 다음 스팬의 오프셋에 그 오프셋을 더해 제거해야(SHOULD) 해요. 유사하게 목록의 첫 번째 스팬이 아닌 스팬은 오프셋이 0일 수(MAY) 있지만, 그 오프셋은 이전 스팬에 길이를 더해 제거해야(SHOULD) 해요. 두 경우 모두 네이티브 히스토그램의 생산자가 그 순간 가장 좋은 리소스 트레이드오프를 가진 어떤 표현이든 고를 수 있도록 허용돼요. 예를 들어 히스토그램이 다양한 단계를 거쳐 처리된다면 마지막 처리 단계 후에만 중복 스팬을 제거하는 것이 가장 효율적일 수 있어요.
비슷한 정신으로, 인구가 없는 모든 버킷을 버킷 목록에서 제외하는 것이 가장 효율적인 상황도 있지만, 다른 상황에서는 소수의 인구가 없는 버킷을 명시적으로 표현해 스팬 수를 줄이는 것이 더 나을 수 있어요.
미래의 고해상도 스키마는 int32로 표현하기에 너무 큰 오프셋을 요구할 수 있다는 점에 유의하세요. 그 경우 데이터 모델의 확장이 필요할 거예요. (현재 가장 높은 해상도의 표준 스키마는 스키마 8이며, MaxFloat64를 포함하는 버킷의 인덱스가 262144이고 +Inf 오버플로 버킷의 인덱스는 262145이며, int32로 표현 가능한 가장 큰 숫자는 2147483647이에요. int32 오프셋으로 여전히 동작할 가장 높은 표준 스키마는 스키마 20이며, 버킷 간 성장 계수는 약 1.000000661에 불과해요.)
예시
정수 히스토그램에 다음과 같은 양수 버킷이 있다고 해요(인덱스→인구):
-2→3, -1→5, 0→0, 1→0, 2→1, 3→0, 4→3, 5→2
다음과 같이 표현할 수 있어요:
- 양수 버킷 목록:
[3, 2, -4, 2, -1] - 양수 스팬 목록:
[[-2, 2], [2,1], [1,2]]
두 번째와 세 번째 스팬은 인덱스 3의 단일 인구가 없는 버킷을 명시적으로 표현하면 하나로 병합할 수 있고, 다음 결과로 이어져요:
- 양수 버킷 목록:
[3, 2, -4, -1, 3, -1] - 양수 스팬 목록:
[[-2, 2], [2,4]]
또는 모든 인구가 없는 버킷을 명시적으로 표현해 모든 스팬을 하나로 병합할 수 있어요:
- 양수 버킷 목록:
[3, 2, -5, 0, 1, -1, 3, -1] - 양수 스팬 목록:
[[-2, 8]]
제로 버킷 (Zero bucket)
정확히 0의 관측치는 위 표준 스키마가 정의하는 어떤 버킷에도 들어가지 않아요. 그것들은 제로 버킷이라고 하는 전용 버킷에 세어져요.
제로 버킷의 관측치 수는 단일 uint64(정수 히스토그램) 또는 float64(float 히스토그램)로 추적돼요. 일반 버킷과 마찬가지로 이 숫자는 일반적으로 음수가 아니에요.
제로 버킷에는 제로 임계값(zero threshold)이라는 추가 파라미터가 있으며, 이는 float64 ≥ 0이에요. 임계값이 0으로 설정되면 정확히 0의 관측치만 제로 버킷에 들어가는데, 이것이 위에서 설명한 경우예요. 임계값이 양수 값이면 닫힌 구간 [-threshold, +threshold] 안의 모든 관측치가 일반 버킷 대신 제로 버킷으로 가요. 여기에는 두 가지 사용 사례가 있어요:
- 0에 가까운 노이즈가 많은 관측치는 많은 버킷을 채우는 경향이 있어요. 그런 관측치는 수치 부정확성이나 관측치의 출처가 실제 물리적 측정일 때 발생할 수 있어요. 상대적으로 작은 임계값을 가진 제로 버킷은 그런 관측치를 단일 버킷으로 리다이렉트해요
- 사용자가 0에서 먼 분포의 긴 꼬리에 더 관심이 있다면, 상대적으로 큰 제로 버킷 임계값이 관심 없는 범위에 대한 많은 고해상도 버킷을 피하는 데 도움이 돼요
제로 버킷의 임계값은 일반 버킷의 경계와 일치해야(SHOULD) 해요. 이는 제로 버킷이 일반 버킷의 일부와 겹치는 복잡성을 피해요. 다만 그런 겹침이 발생하면 제로 버킷과 겹치는 일반 버킷에 세어지는 관측치는 [-threshold, +threshold] 구간 밖에 있어야(MUST) 해요.
같은 제로 임계값을 가진 히스토그램을 병합하려면 두 제로 버킷을 그냥 더해요. 소스 히스토그램의 제로 임계값이 다르다면 소스 히스토그램 중 가장 큰 임계값이 선택돼요. 그 임계값이 우연히 다른 소스 히스토그램의 인구가 있는 버킷 내에 있다면, 다음 중 하나가 각 소스 히스토그램에 대해 참이 될 때까지 임계값이 증가해요:
- 새 임계값이 인구가 있는 버킷의 경계와 일치
- 새 임계값이 어떤 인구가 있는 버킷에도 없음
그런 다음 소스 제로 버킷과 이제 새 임계값 안의 소스 버킷을 더해 새 제로 버킷의 인구를 만들어요.
제로 버킷은 스키마가 -53(사용자 정의 버킷)이면 사용되지 않아요.
사용자 정의 값 (Custom values)
사용자 정의 값 목록은 표준 스키마에서 사용되지 않아요. 추가 데이터를 저장할 필요가 있을 때 비표준 스키마가 사용자 정의 방식으로 사용해요.
사용자 정의 값을 사용하는 현재 정의된 유일한 스키마는 -53(사용자 정의 버킷)이에요. 이 섹션의 나머지 부분은 이 특정 경우에 대한 사용자 정의 값 사용을 자세히 설명해요.
사용자 정의 값은 사용자 정의 버킷의 포함 상한을 나타내요. 오름차순으로 정렬돼요. 사용자 정의 버킷 자체는 양수 버킷 목록과 양수 스팬 목록을 사용해 저장되지만, 사용자 정의 값으로 결정된 경계는 음수가 될 수 있어요. 이들 "양수" 버킷 각각의 인덱스는 사용자 정의 값 목록 안에 있는 상한의 0 기반 위치를 정의해요.
제외 하한은 상한 앞에 오는 사용자 정의 값으로 정의돼요. 첫 번째 사용자 정의 값(목록의 0 위치)에는 앞에 오는 값이 없으며, 이 경우 하한은 -Inf "포함"으로 간주돼요. 따라서 인덱스 0의 사용자 정의 버킷은 -Inf와 첫 번째 사용자 정의 값 사이(포함)의 모든 관측치를 세요. 양수 관측치만 예상되는 일반적인 경우에는 인덱스 0의 사용자 정의 버킷이 0보다 크거나 같은지 아니면 0 이하에 관측치가 있었는지 명확히 표시하기 위해 상한이 0이어야(SHOULD) 해요. (실제로 양수 관측치만 있으면 인덱스 0의 사용자 정의 버킷은 채워지지 않은 채로 남아 명시적으로 표현되지 않을 거예요. 유일한 비용은 사용자 정의 값 목록 시작 부분의 추가 0 요소예요.)
사용자 정의 값은 +Inf가 되어선 안(MUST NOT) 돼요. 마지막 사용자 정의 값보다 큰 관측치는 +Inf의 상한을 가진 오버플로 버킷으로 가요. 이 오버플로 버킷은 사용자 정의 값 목록의 길이와 같은 인덱스로 추가돼요. 결과적으로 클래식 히스토그램에 흔히 포함되는 +Inf 버킷의 상한은 사용자 정의 값에서 명시적으로 표현되지 않아요.
사용자 정의 값은 NaN이 되어선 안(MUST NOT) 돼요. 이것은 OpenMetrics에서 명시적으로 배제돼 있지만, 다른 exposition 형식은 원칙적으로 클래식 히스토그램에서 NaN의 상한을 가질 수 있어요(아마 일부 오류의 결과일 것 – 그런 경계는 말이 안 됩니다). 그런 클래식 히스토그램은 거부해야(MUST) 하며 NHCB로 변환할 수 없어요.
Exemplar
네이티브 히스토그램 샘플은 0개, 1개 또는 그 이상의 exemplar를 가질 수 있어요. 이는 기존 exemplar와 같은 방식으로 동작하지만 목록으로 구성돼 있고(하나 이상 있을 수 있으므로) 타임스탬프가 반드시 있어야(MUST) 해요.
클래식 히스토그램의 일부로 노출된 exemplar는 타임스탬프가 있으면 네이티브 히스토그램이 사용할 수(MAY) 있어요.
관측값의 특수한 경우
계측된 코드는 NaN과 ±Inf 값을 관측하는 것을 피해야(SHOULD) 해요. 히스토그램 맥락에서 이해가 제한되기 때문이에요. 다만 그런 값은 아래에서 설명하는 대로 여전히 제대로 처리해야(MUST) 해요.
관측치 sum은 일반적인 부동 소수점 산술을 따라 관측치를 sum에 더해 평소처럼 계산돼요. (예를 들어 NaN 관측은 sum을 NaN으로 설정해요. +Inf 관측은 이미 NaN이나 -Inf가 아닌 한 sum을 +Inf로 설정하며, 그 경우 sum은 NaN으로 설정돼요.)
NaN 관측은 어떤 버킷에도 들어가지 않지만 관측치 count를 증가시켜요. 이는 관측치 count가 모든 버킷(음수, 양수, 제로 버킷)의 합보다 클 수 있고, 그 차이가 NaN 관측치 수임을 의미해요. (NaN 관측치가 없는 정수 히스토그램의 경우 모든 버킷의 합은 관측치 count와 같아요. 일반적인 부동 소수점 정밀도 한계 내에서 NaN 관측치가 없는 float 히스토그램에도 동일하게 적용돼요.)
+Inf 또는 -Inf의 관측은 관측치 count를 증가시키고 다음 방식으로 선택된 버킷을 증가시켜요:
- 표준 스키마에서
+Inf관측은 위에서 설명한 양수 오버플로 버킷을 증가시켜요 - 표준 스키마에서
-Inf관측은 위에서 설명한 음수 오버플로 버킷을 증가시켜요 - 스키마 -53(사용자 정의 버킷)에서
+Inf관측은 사용자 정의 값 목록의 길이와 같은 인덱스의 버킷을 증가시켜요 - 스키마 -53(사용자 정의 버킷)에서
-Inf관측은 인덱스 0의 버킷을 증가시켜요
OpenTelemetry 상호운용성
표준 스키마를 가진 프로메테우스(Prom) 네이티브 히스토그램은 아래에서 자세히 설명하는 대로 OpenTelemetry(OTel) 지수 히스토그램으로 쉽게 매핑될 수 있고 그 반대도 마찬가지예요.
Prom 스키마는 OTel의 scale과 같으며, OTel이 -4보다 낮은 값과 +8보다 높은 값을 허용한다는 제약이 있어요. 위에서 설명한 대로 Prom은 실전에서 필요하면 범위를 확장하기 위해 더 많은 스키마 번호를 예약했어요.
인덱스는 1씩 오프셋 돼요. 즉 인덱스 n의 Prom 버킷은 OTel에서 인덱스 n-1이에요.
OTel은 스파스가 아닌 조밀한 버킷 표현을 가져요. OTel을 "스팬이 하나뿐인 Prom"으로 볼 수 있어요.
Prom 제로 버킷은 OTel에서 zero count라고 불러요. (Prom도 제로 버킷의 관측치 count를 저장하는 필드 이름을 zero count로 사용해요.) 둘 다 제로 임계값의 존재를 포함해 같은 방식으로 동작해요. OTel은 주어지지 않으면 임계값이 0이라고 가정한다는 점에 유의하세요.
(TODO: OTel 사양은 다음과 같이 읽습니다: "zero_threshold가 설정되지 않거나 0이면 이 버킷은 표준 지수 공식을 사용해 표현할 수 없는 값과 0으로 반올림된 값을 저장합니다." 이것이 정말로 같은 동작을 만드는지 재확인하세요. 0 근처에 문제가 있으면 Prom의 사양을 더 정밀하게 만들 수 있어요. OTel이 제로 버킷에서 NaN을 센다면 여기에 메모를 추가해야 해요.)
OTel 지수 히스토그램은 이름이 암시하듯 표준 지수 버킷팅 스키마만 지원해요. 따라서 NHCB(또는 다른 미래 버킷팅 스키마를 가진 네이티브 히스토그램)는 OTel 지수 히스토그램으로 깨끗하게 변환될 수 없어요. 다만 고정 버킷을 가진 기존 OTel 히스토그램으로의 변환은 여전히 가능해요.
어떤 종류의 OTel 히스토그램이든 히스토그램에서 관측된 최소·최대 값에 대한 선택적 필드가 있어요. 이 필드들은 프로메테우스에 해당 개념이 없어요. counter 히스토그램이 길고 예측 불가능한 기간에 걸쳐 데이터를 축적하고 언제든 스크레이프될 수 있으므로, 최소·최대 값을 추적하는 것은 실행 불가능하거나 사용이 제한되기 때문이에요. 다만 네이티브 히스토그램은 임의 시간 범위 동안 최대·최소 관측치의 상당히 정확한 추정을 가능하게 한다는 점에 유의하세요. PromQL 섹션을 참조하세요.
Exposition 형식 (Exposition formats)
클래식 프로메테우스 사용 사례의 메트릭 exposition은 문자열이 지배적이에요. 모든 메트릭 이름, 레이블 이름, 레이블 값이 잠재적으로 더 장황한 텍스트 형식으로 표현되더라도 float64 샘플 값보다 훨씬 많은 공간을 차지하기 때문이에요. 이것이 과거에 protobuf 기반 exposition을 버리는 것이 유리해 보였던 이유 중 하나였어요.
대조적으로 네이티브 히스토그램은 위에서 설명한 데이터 모델을 따라 훨씬 더 많은 수치 데이터로 구성돼요. 이는 protobuf 기반 형식의 장점을 증폭해요. 그래서 이전에 버려졌던 protobuf 기반 exposition이 네이티브 히스토그램을 효율적으로 노출하고 스크레이프하기 위해 부활됐어요.
클래식 프로메테우스 형식
네이티브 히스토그램이 구상되던 당시 OpenMetrics 채택은 여전히 부족했고, 특히 OpenMetrics의 protobuf 버전에는 알려진 애플리케이션이 전혀 없었어요. 따라서 초기 접근은 네이티브 히스토그램을 지원하도록 클래식 프로메테우스 protobuf 형식을 확장하는 것이었어요. (추가 실무 고려사항은 Go 계측 라이브러리가 여전히 내부 데이터 모델로 클래식 protobuf 사양을 사용해 초기 개발을 단순화했다는 점이었어요.)
클래식 프로메테우스 텍스트 형식은 네이티브 히스토그램을 위해 확장되지 않았고, 그런 확장도 계획돼 있지 않아요. (아래 OpenMetrics 섹션도 참조하세요.)
같은 wire 형식을 만드는 proto2와 proto3 버전의 protobuf 사양이 있어요:
이 파일들은 포괄적인 주석을 가지고 있어 위에서 설명한 데이터 모델로 proto 사양의 쉬운 매핑을 가능하게 해요.
다음은 proto3 파일의 관련 부분이에요:
// [...]
message Histogram {
uint64 sample_count = 1;
double sample_count_float = 4; // Overrides sample_count if > 0.
double sample_sum = 2;
// Buckets for the classic histogram.
repeated Bucket bucket = 3 [(gogoproto.nullable) = false]; // Ordered in increasing order of upper_bound, +Inf bucket is optional.
google.protobuf.Timestamp start_timestamp = 15;
// Everything below here is for native histograms (also known as sparse histograms).
// schema defines the bucket schema. Currently, valid numbers are -4 8.
sint32 schema = 5;
double zero_threshold = 6; // Breadth of the zero bucket.
uint64 zero_count = 7; // Count in zero bucket.
double zero_count_float = 8; // Overrides sb_zero_count if > 0.
// Negative buckets for the native histogram.
repeated BucketSpan negative_span = 9 [(gogoproto.nullable) = false];
// Use either "negative_delta" or "negative_count", the former for
// regular histograms with integer counts, the latter for float
// histograms.
repeated sint64 negative_delta = 10; // Count delta of each bucket compared to previous one (or to zero for 1st bucket).
repeated double negative_count = 11; // Absolute count of each bucket.
// Positive buckets for the native histogram.
// Use a no-op span (offset 0, length 0) for a native histogram without any
// observations yet and with a zero_threshold of 0. Otherwise, it would be
// indistinguishable from a classic histogram.
repeated BucketSpan positive_span = 12 [(gogoproto.nullable) = false];
// Use either "positive_delta" or "positive_count", the former for
// regular histograms with integer counts, the latter for float
// histograms.
repeated sint64 positive_delta = 13; // Count delta of each bucket compared to previous one (or to zero for 1st bucket).
repeated double positive_count = 14; // Absolute count of each bucket.
// Only used for native histograms. These exemplars MUST have a timestamp.
repeated Exemplar exemplars = 16;
}
message Bucket {
uint64 cumulative_count = 1; // Cumulative in increasing order.
double cumulative_count_float = 4; // Overrides cumulative_count if > 0.
double upper_bound = 2; // Inclusive.
Exemplar exemplar = 3;
}
// A BucketSpan defines a number of consecutive buckets in a native
// histogram with their offset. Logically, it would be more
// straightforward to include the bucket counts in the Span. However,
// the protobuf representation is more compact in the way the data is
// structured here (with all the buckets in a single array separate
// from the Spans).
message BucketSpan {
sint32 offset = 1; // Gap to previous span, or starting point for 1st span (which can be negative).
uint32 length = 2; // Length of consecutive buckets.
}
// [...]
다음에 유의하세요:
- 네이티브 히스토그램과 클래식 히스토그램은 같은
Histogramproto 메시지로 인코딩돼요. 즉 기존Histogram메시지가 네이티브 히스토그램용 필드로 확장됐어요 - 관측치 sum과 count 및
start_timestamp필드는 클래식과 네이티브 히스토그램 사이에 공유되며 둘 다에 같은 방식으로 계속 동작해요 - 이 형식은 원래 클래식 float 히스토그램을 지원하지 않았어요. 네이티브 히스토그램을 위해 형식을 확장하면서 클래식 float 히스토그램 지원이 부산물로 추가됐어요(
sample_count_float,cumulative_count_float필드 참조) Bucket필드와Bucket메시지는 클래식 히스토그램의 버킷에 사용돼요. 같은 히스토그램의 클래식과 네이티브 버전을 모두 나타내는Histogram메시지를 만드는 것은 완전히 가능해요. 파서는 둘 중 하나 또는 둘 다를 고를 자유가 있어요(아래 스크레이프 구성 섹션 참조)- 버킷 인구는 float 히스토그램의 경우 절대 숫자로, 정수 히스토그램의 경우 이전 버킷에 대한 델타(또는 첫 버킷의 경우 0에 대한 델타)로 인코딩돼요. 후자는 더 작은 숫자로 이어지며, protobuf가
sint64타입에 varint 인코딩을 사용하므로 더 작은 메시지 크기로 인코딩돼요 - 아직 관측치를 받지 않은 네이티브 히스토그램과 버킷이 구성되지 않은 클래식 히스토그램은 protobuf 메시지로 정확히 같아 보일 거예요. 따라서 네이티브 히스토그램으로 파싱되도록 의도된
Histogram메시지는 repeatedpositive_span필드에 "no-op 스팬", 즉offset과length가 0으로 설정된BucketSpan을 반드시(MUST) 포함해야 해요 - 네이티브 히스토그램용 exemplar는
Histogram메시지의 repeatedExemplar필드에 얼마든지 추가될 수(MAY) 있지만 각각 타임스탬프가 있어야(MUST) 해요. 이렇게 제공된 exemplar가 없으면 파서는 클래식 버킷에 제공된 타임스탬프가 있는 exemplar(각 버킷당 최대 하나를Bucket메시지의Exemplar필드에)를 사용할 수(MAY) 있어요 - 네이티브 히스토그램 exemplar의 수와 분포는 현재 사용 사례에 맞아야(SHOULD) 해요. 일반적으로 exemplar 페이로드는
Histogram메시지의 나머지 부분보다 훨씬 커서는 안(SHOULD NOT) 되고, exemplar는 서로 다른 버킷에 떨어져 버킷의 전체 분포를 대략 고르게 덮어야(SHOULD) 해요. (이것은 일반적으로 관측치 분포를 비례적으로 나타내는 exemplar 분포보다 선호되는데, 후자는 분포의 긴 꼬리에서 exemplar를 거의 얻지 못하며, 그것이 종종 보기에 가장 흥미로운 exemplar이기 때문이에요.) - NHCB에 필요한 사용자 정의 값의 표현은 없어요. NHCB는 직접 노출되지 않고 클래식 히스토그램으로 표현되며 수집 시 (다시) NHCB로 변환돼요. 이는 연합(federation)에도 마찬가지예요. 미래에 필요가 생기면(예: 사용자 정의 값도 활용하는 미래 스키마) 사용자 정의 값 필드를 추가할 수 있어요
OpenMetrics
현재(2024-11-03) OpenMetrics는 네이티브 히스토그램을 지원하지 않아요.
OpenMetrics의 protobuf 버전에 지원을 추가하는 것은 클래식 프로메테우스 protobuf 형식과 유사해서 상대적으로 간단해요. PR 형태의 제안이 검토 중이에요.
OpenMetrics 텍스트 버전에 지원을 추가하는 것은 더 어렵지만 매우 바람직해요. protobuf 생성이 실행 불가능한 상황이 많기 때문이에요. 텍스트 형식은 사람을 위한 가독성과 머신의 효율적 처리(인코딩, 전송, 디코딩) 사이에서 트레이드오프를 해야 해요. 이에 대한 작업이 진행 중이에요. 자세한 내용은 설계 문서를 참조하세요.
(TODO: 진전에 따라 섹션 업데이트)
계측 라이브러리 (Instrumentation libraries)
protobuf 사양은 protobuf 컴파일러가 만든 언어별 바인딩을 사용해 네이티브 히스토그램을 포함한 메트릭 exposition의 저수준 생성을 가능하게 해요. 다만 직접 코드 계측에는 계측 라이브러리가 필요해요.
현재(2026-06-15) 네이티브 히스토그램을 지원하는 공식 프로메테우스 계측 라이브러리 3개가 있어요:
다른 계측 라이브러리에 네이티브 히스토그램 지원을 추가하는 것은 라이브러리가 이미 protobuf exposition을 지원한다면 상대적으로 쉬워요. 순수 텍스트 기반 라이브러리의 경우 텍스트 기반 exposition 형식의 완성이 전제 조건이에요. (TODO: 필요에 따라 업데이트)
이 섹션은 개별 계측 라이브러리 사용 방법 세부 사항(그것은 위에 링크된 문서를 참조하세요)을 다루지 않고 공통 사용 패턴에 초점을 맞추며 계측 라이브러리의 일부로 네이티브 히스토그램 지원을 구현하는 일반 지침도 제공해요. 이미 존재하는 Go 구현이 예시로 사용돼요. 데이터 모델과 exposition 형식에 대한 섹션은 계측 라이브러리 구현에 매우 관련이 있어요(하지만 이 섹션에서 다시 설명하지는 않아요!).
히스토그램의 실제 계측 API는 네이티브 히스토그램에서 변경되지 않아요. 클래식 히스토그램과 네이티브 히스토그램은 같은 방식으로 관측치를 받아요(exemplar 관련 약간의 차이가 있음, 다음 문단 참조). 계측 라이브러리는 심지어 같은 히스토그램의 클래식과 네이티브 버전을 유지하고 병렬로 노출해서 스크레이퍼가 어느 버전을 수집할지 고를 수 있게 할 수 있어요(자세한 내용은 exposition 형식 섹션 참조). 사용자는 구성 설정으로 클래식 및/또는 네이티브 히스토그램을 노출할지 선택해요.
클래식 히스토그램용 exemplar는 보통 각 버킷에 가장 최근 exemplar를 저장하고 노출해 추적돼요. 클래식 버킷이 정의되어 있는 한 계측 라이브러리는 각 exemplar에 타임스탬프가 있으면 같은 히스토그램의 네이티브 버전에 같은 exemplar를 노출할 수(MAY) 있어요. (실제로 스크레이퍼는 그 외에는 네이티브 버전만 수집하더라도 히스토그램의 클래식 버전과 함께 제공된 exemplar를 사용할 수(MAY) 있어요. 자세한 내용은 exposition 형식 섹션 참조.) 하지만 네이티브 히스토그램에는 얼마든지 exemplar를 지정할 수 있고, 계측 라이브러리는 exposition 형식 섹션에서 설명한 exemplar 모범 사례를 충족하기 위해 이 자유를 사용해야(SHOULD) 해요.
계측 라이브러리는 표준 스키마를 따르는 네이티브 히스토그램에 대해 다음 구성 파라미터를 제공해야(SHOULD) 해요. 이름은 Go 라이브러리의 예시이며 다른 언어의 관용적 스타일에 맞게 조정해야 해요. 괄호 안 값은 라이브러리가 제공해야(SHOULD) 하는 기본값이에요.
NativeHistogramBucketFactor(1.1): 초기 해상도를 결정하는 1보다 큰 float. 라이브러리는 한 버킷에서 다음 버킷으로의 버킷 폭 성장이 제공된 값보다 크지 않은 결과를 낳는 시작 스키마를 골라요. 예시 값은 아래 표를 참조하세요NativeHistogramZeroThreshold(2-128): 제로 버킷의 초기 임계값을 설정하는 0 이상의 float
해상도는 스키마를 직접 제공하기보다 성장 계수로 설정돼요. 대부분의 사용자가 스키마 번호 뒤의 수학을 알지 못하기 때문이에요. 버킷 간 성장 계수의 상한이라는 개념은 네이티브 히스토그램의 내부 동작을 몰라도 이해할 수 있어요. 다음 표는 각 유효한 스키마에 대한 예시 계수를 나열해요.
NativeHistogramBucketFactor |
결과 스키마 |
|---|---|
| 65536 | -4 |
| 256 | -3 |
| 16 | -2 |
| 4 | -1 |
| 2 | 0 |
| 1.5 | 1 |
| 1.22 | 1 |
| 1.13 | 1 |
| 1.054 | 1 |
| 1.03 | 5 |
| 1.02 | 6 |
| 1.01 | 7 |
| 1.005 | 8 |
버킷 수 제한
네이티브 히스토그램의 버킷은 처음 채워질 때 동적으로 생성돼요. 예상 밖으로 넓은 관측값 분포는 예상 밖으로 많은 버킷으로 이어져 예상보다 더 많은 메모리를 요구할 수 있어요. 관측값 분포를 외부에서 조작할 수 있다면, 프로그램에 사용 가능한 모든 메모리를 소진해 DoS 공격 벡터로 사용될 수도 있어요. 따라서 계측 라이브러리는 버킷 제한 전략을 제공해야(SHOULD) 해요. 라이브러리가 사용되는 일반적인 사용 사례에 따라 기본으로 하나를 설정할 수(MAY) 있어요. (TODO: 전략을 기본으로 설정해야(SHOULD) 한다고 말해야 할지도 모르겠어요. Go 라이브러리는 현재 버킷을 기본으로 제한하지 않고 지금까지 문제가 보고된 적이 없어요.)
다음은 Go 계측 라이브러리가 구현한 버킷 제한 전략을 설명해요. 다른 라이브러리도 이 예시를 따를 수 있지만, 라이브러리의 일반적인 사용 패턴에 따라 다른 전략도 가능할 수 있어요.
이 전략은 세 가지 파라미터로 정의돼요: 서명 없는 정수 NativeHistogramMaxBucketNumber, 기간 NativeHistogramMinResetDuration, float NativeHistogramMaxZeroThreshold. NativeHistogramMaxBucketNumber가 0이면(기본값) 버킷이 전혀 제한되지 않고 다른 두 파라미터는 무시돼요. NativeHistogramMaxBucketNumber가 양수 값으로 설정되면 라이브러리는 각 히스토그램의 버킷 수를 제공된 값으로 유지하려고 해요. 한계의 일반적인 값은 160이며, 이는 비슷한 전략에서 OTel 지수 히스토그램이 사용하는 기본값이기도 해요. (레이블로 파티셔닝하면 여러 히스토그램이 생긴다는 점에 유의하세요. 한계는 그것들 모두의 합계가 아니라 각각에 개별적으로 적용돼요.) 한계가 초과될 것 같으면 버킷 수가 다시 한계 안에 들어올 때까지 여러 구제책이 순서대로 적용돼요:
- 마지막 히스토그램 리셋(히스토그램 생성 포함) 이후
NativeHistogramMinResetDuration이 지났으면 전체 히스토그램이 리셋돼요. 즉 모든 버킷이 삭제되고 관측치 sum·count 및 제로 버킷이 0으로 설정돼요. 프로메테우스는 이를 정상적인 카운터 리셋으로 처리하며, 이는 스크레이프 사이에 일부 관측치가 손실된다는 뜻이므로 스크레이프 간격에 비해 리셋이 드물게 발생해야 해요. 추가로 빈번한 카운터 리셋은 TSDB에서 덜 효율적인 저장으로 이어질 수 있어요(자세한 내용은 TSDB 섹션 참조).NativeHistogramMinResetDuration1시간은 대부분의 상황에서 잘 작동하는 값이에요 - 마지막 리셋 이후 충분한 시간이 지나지 않았거나(또는 기본값인 0으로 설정된 경우) 리셋이 수행되지 않아요. 대신 0에 가까운 버킷을 제로 버킷으로 병합하도록 제로 임계값을 증가시켜 그 방식으로 버킷 수를 줄여요. 임계값 증가는
NativeHistogramMaxZeroThreshold로 제한돼요. 이 값에 이미 도달했거나(기본값인 0으로 설정된 경우) 이 단계에서 아무 일도 일어나지 않아요 - 버킷 수가 여전히 한계를 초과하면 히스토그램을 다음 하위 스키마로 변환해(이웃 버킷을 병합해 버킷 폭을 두 배로) 히스토그램의 해상도를 줄여요. 버킷 수가 구성된 한계 안에 들어오거나 스키마 -4에 도달할 때까지 반복돼요
2단계나 3단계가 히스토그램을 변경했다면, 마지막 리셋 이후 NativeHistogramMinResetDuration이 지나면 버킷을 제거할 뿐 아니라 제로 임계값과 버킷 해상도의 초기 값으로 돌아가기 위해 리셋이 수행돼요. 이는 시작 타임스탬프 업데이트를 포함해 모든 면에서 다른 이유의 리셋처럼 취급된다는 점에 유의하세요.
매우 낮은 NativeHistogramBucketFactor(예: 1.005)를 합리적인 NativeHistogramMaxBucketNumber(예: 160)와 함께 설정하고 싶은 유혹이 있어요. 이렇게 하면 각 히스토그램이 항상 주어진 버킷 수 "예산" 안에서 감당할 수 있는 최고 해상도를 가져요. (이것이 OTel 지수 히스토그램이 사용하는 기본 전략이에요. 그것은 현재 프로메테우스 네이티브 히스토그램에서조차 사용할 수 없는 훨씬 더 높은 스키마(20)로 시작해요.) 다만 이 전략은 프로메테우스 사용 사례에 일반적으로 권장되지 않아요. 관측치가 들어오면 생성 후와 각 리셋 후에 해상도가 꽤 자주 줄어들 거예요. 이는 계측된 프로그램과 TSDB 양쪽에서 churn을 만들어내며 특히 후자에게 문제가 돼요. 이 모든 노력은 대부분 헛된 것이에요. 히스토그램을 포함하는 일반적인 쿼리는 많은 히스토그램을 병합해야 하고, 병합 중 가장 낮은 공통 해상도가 사용되므로 사용자는 어쨌든 더 낮은 해상도로 끝나기 때문이에요. TSDB는 수집 시 해상도를 제한해(아래 버킷 수와 해상도 제한 참조) churn으로부터 보호할 수 있지만, 수집 시 합리적으로 낮은 해상도가 어쨌든 강제된다면 계측 중에 이미 이 해상도를 설정하는 것이 더 간단해요. 다만 이 전략은 계측 시점에 합리적인 해상도를 가정할 수 없고 스크레이퍼가 스크레이프 시 원하는 해상도를 고를 수 있는 유연성이 있어야 하는 특정 경우에 계측된 프로그램 내부의 리소스 오버헤드가 가치 있을 수 있어요.
레이블로 파티셔닝
많은 버킷을 가진 클래식 히스토그램을 레이블로 파티셔닝하는 것은 신중히 해야 하지만, 네이티브 히스토그램에서는 상황이 더 여유로워요. 네이티브 히스토그램을 파티셔닝해도 여전히 개별 히스토그램의 다중성을 만들어내요. 다만 결과 파티셔닝된 히스토그램은 종종 원래의 파티셔닝되지 않은 히스토그램보다 각각 더 적은 버킷을 채울 거예요. (예를 들어 HTTP 요청 기간을 추적하는 히스토그램을 HTTP 상태 코드로 파티셔닝하면, 상태 코드 404로 응답한 요청을 추적하는 개별 히스토그램은 알 수 없는 경로를 식별하는 데 걸리는 일반적인 기간 주변에 매우 날카로운 버킷 분포를 가져 소수의 버킷만 채울 수 있어요.) 모든 파티셔닝된 히스토그램의 총 채워진 버킷 수는 여전히 올라가지만, 파티셔닝된 히스토그램 수보다 작은 계수로 올라가요. (예를 들어 이미 꽤 무거운 클래식 히스토그램에 레이블을 추가해 100개의 레이블된 히스토그램이 생기면 총 비용은 100배로 올라가요. 네이티브 히스토그램의 경우 클래식 히스토그램이 고해상도였다면 단일 히스토그램의 비용이 이미 더 낮을 수 있어요. 파티셔닝 후 레이블된 네이티브 히스토그램의 총 채워진 버킷 수는 원래 네이티브 히스토그램의 버킷 수의 100배보다 훨씬 작을 거예요.)
NHCB
현재(2024-11-03) 계측 라이브러리는 사용자 정의 버킷 경계를 가진 네이티브 히스토그램(NHCB)을 직접 구성하는 방법을 제공하지 않아요. NHCB의 사용 사례는 네이티브 히스토그램이 활성화된 스크레이퍼가 수집 시 클래식 히스토그램을 NHCB로 변환하는 것을 허용하는 것이에요(아래 다음 섹션 참조). 다만 계측 중에 직접 사용자 정의 버킷이 바람직한 유효한 사용 사례가 있어요. 그 경우 현재 접근은 클래식 히스토그램으로 계측하고 스크레이퍼가 수집 시 NHCB로 변환하도록 구성하는 것이에요. 하지만 계측 라이브러리에서 NHCB를 더 직접 다루는 것이 미래에 일어날 수 있어요.
스크레이프 구성 (Scrape configuration)
프로메테우스 서버가 네이티브 히스토그램을 스크레이프하도록 하려면 개별 스크레이프 구성이나 전역 설정에서 scrape_native_histograms: true를 설정하세요. scrape_native_histograms를 활성화하면 OpenMetrics 1.x 텍스트 형식보다 클래식 protobuf 기반 exposition 형식을 선호하도록 콘텐츠 협상도 변경돼요.
콘텐츠 협상 세부 조정
scrape_protocols 구성 설정으로 스크레이프 프로토콜 협상을 전역 또는 스크레이프 구성별로 세부 조정할 수 있어요. 콘텐츠 협상 우선순위를 정의하는 목록이에요. 그 값은 어떤 기능 플래그가 활성화됐는지(예: --enable-feature=created-timestamp-zero-ingestion), 사용자가 직접 설정한 값, 마지막으로 scrape_native_histograms가 활성화됐는지에 따라 달라져요.
scrape_native_histograms가 활성화되고 scrape_protocols가 기능 플래그나 사용자가 전역이나 스크레이프 구성별로 설정하지 않았다면, 스크레이프 구성에 대한 효과적 값은 네이티브 히스토그램 스크레이핑을 활성화하기 위해 [ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]로 변경돼요.
scrape_protocols 설정은 네이티브 히스토그램을 수집하지 않고 protobuf 스크레이프를 구성하거나, scrape_native_histograms가 활성화되어 있어도 특정 타깃에 비-protobuf 형식을 강제하는 데 사용할 수 있어요. 클래식 프로메테우스 protobuf 형식(구성된 목록의 PrometheusProto)이 네이티브 히스토그램을 지원하는 유일한 형식인 한, 실제로 네이티브 히스토그램을 수집하려면 scrape_native_histograms와 protobuf 협상이 모두 필요해요.
참고: 사용된 exposition 형식을 텍스트 기반과 protobuf 기반 사이에서 전환하면 몇 가지 명확하지 않은 함의가 있어요. 가장 중요하게는 특정 구현 세부 사항이 텍스트 기반 형식으로 스크레이프하는 것이 일반적으로 protobuf 기반 형식으로 스크레이프하는 것보다 훨씬 리소스를 덜 요구하는 직관에 반하는 효과를 낳아요(자세한 내용은 추적 이슈 참조). 더 미묘한 것은 quantile 레이블(summary에 사용)과 le 레이블(클래식 히스토그램에 사용)의 레이블 값 포매팅에 미치는 영향이에요. 이 문제는 프로메테우스 서버 v2에만 영향을 주고(모든 상황에서 일관된 포매팅을 가진 v3는 아님) 네이티브 히스토그램과 직접 관련이 없지만, 네이티브 히스토그램을 활성화하려면 protobuf exposition 형식이 필요하므로 같은 맥락에서 나타날 수 있어요. 자세한 내용은 v2.55용 native-histograms 기능 플래그 문서를 참조하세요.
버킷 수와 해상도 제한
계측 라이브러리가 네이티브 히스토그램의 해상도와 버킷 수를 제한하는 구성 옵션을 제공해야(SHOULD) 하긴 하지만, 수집 시 그 한계를 강제할 필요는 여전히 있어요. 사용자가 주어진 프로그램의 계측을 변경할 수 없을 수도 있고, 프로그램이 서로 다른 스크레이퍼가 원하는 대로 해상도를 줄일 수 있도록 의도적으로 고해상도 히스토그램으로 계측될 수도 있어요.
프로메테우스 스크레이프 구성은 이 필요를 다루는 두 가지 설정을 제공해요:
native_histogram_bucket_limit은 개별 히스토그램의 버킷 수에 대한 포함 상한을 설정해요. 한계를 초과하면 표준 스키마를 가진 히스토그램의 해상도가 한계에 도달할 때까지 (버킷 폭을 두 배로, 즉 스키마를 낮춰서) 반복적으로 줄어들어요. NHCB가 한계를 초과하거나, 드물게 스키마 -4에서도 한계를 충족할 수 없는 경우 스크레이프가 실패해요native_histogram_min_bucket_factor은 버킷 간 성장 계수에 대한 포함 하한을 설정해요. 이 설정은 표준 스키마에만 관련이 있고 NHCB에는 효과가 없어요. 역시 한계를 초과하면 더 높은 성장 계수가 지정됐더라도 스키마 -4에 도달하면 스크레이프는 여전히 성공하지만, 한계에 도달할 때까지 히스토그램의 해상도가 (버킷 폭을 두 배로, 즉 스키마를 낮춰서) 반복적으로 줄어들어요
두 설정 모두 0을 유효한 값으로 받아들이며, 이는 "제한 없음"을 의미해요. 버킷 제한의 경우 버킷 수가 실제로 전혀 확인되지 않는다는 뜻이에요. 버킷 계수의 경우 프로메테우스는 여전히 표준 스키마가 사용된 저장 백엔드의 성능을 초과하지 않도록 보장해요. 프로메테우스는 현재 최대 8의 표준 지수 스키마를 가진 히스토그램을 저장해요. 하지만 예약된 한계 52까지의 8보다 큰 지수 스키마를 받아들이되 수집 시 해상도를 줄여서 스키마 8에 도달하게 해요(또는 native_histogram_bucket_limit이나 native_histogram_min_bucket_factor 설정이 요구하면 더 낮게).
두 설정 모두 0이 아닌 값을 가지면 두 한계를 모두 충족하도록 스키마가 충분히 감소돼요.
계측 중에 설정된 버킷 계수는 상한(노출된 버킷 성장 계수 ≤ 구성 값)인 반면, 스크레이프 구성에 설정된 버킷 계수는 하한(수집된 버킷 성장 계수 ≥ 구성 값)이라는 점에 유의하세요. 따라서 특정 한계에서 나온 스키마는 약간 달라요. 몇 가지 예시:
native_histogram_min_bucket_factor |
결과 최대 스키마 |
|---|---|
| 65536 | -4 |
| 256 | -3 |
| 16 | -2 |
| 4 | -1 |
| 2 | 0 |
| 1.4 | 1 |
| 1.12 | 1 |
| 1.093 | 1 |
| 1.04 | 4 |
| 1.02 | 5 |
| 1.01 | 6 |
| 1.005 | 7 |
| 1.002 | 8 |
한계 설정에 대한 일반적인 고려사항: native_histogram_bucket_limit은 개별 히스토그램 비용에 대한 하드 한계를 설정하는 데 적합해요. 관측치 분포가 충분히 넓으면 낮은 해상도에서도 히스토그램이 많은 버킷을 가질 수 있으므로 native_histogram_min_bucket_factor로는 같은 것을 달성할 수 없어요. native_histogram_min_bucket_factor는 불필요한 전체 리소스 비용을 피하는 데 잘 맞아요. 예를 들어 현재 사용 사례가 특정 해상도만 요구한다면 모든 히스토그램에 해당하는 native_histogram_min_bucket_factor를 설정하면 넓은 분포를 가진 소수의 히스토그램에서 아주 높은 버킷 수를 받아들일 만큼 리소스를 확보할 수 있어요. 또 다른 예시는 어떤 이유로(어쩌면 이미 계측 측에서) 일부 히스토그램이 낮은 해상도를 가진 경우예요. 집계에 그런 낮은 해상도 히스토그램이 정기적으로 포함되면 결과도 그 낮은 해상도를 가지게 돼요(자세한 내용은 PromQL 세부 사항 참조). 낮은 해상도 히스토그램과 정기적으로 집계되는 다른 히스토그램을 더 높은 해상도로 저장하는 것은 그다지 유용하지 않을 수 있어요.
클래식과 네이티브 히스토그램 동시 스크레이프
위에서 설명했듯 계측된 프로그램이 노출하는 히스토그램은 클래식과 네이티브 히스토그램을 모두 포함할 수 있고 일부 부분은 공유되기도 해요(관측치 count와 sum처럼). 이 섹션은 프로메테우스가 어떤 부분을 스크레이프할지와 동작을 어떻게 제어하는지 설명해요.
스크레이프 구성에서 scrape_native_histograms가 false(v3 기본값)면 프로메테우스는 스크레이핑 중 네이티브 히스토그램 부분을 완전히 무시해요. scrape_native_histograms가 true(v4 이상 기본값)면 프로메테우스는 같은 히스토그램에 둘 다 노출되어 있어도 클래식 부분보다 네이티브 부분을 선호해요. 프로메테우스는 네이티브 히스토그램 데이터가 없는 히스토그램에 대해서는 여전히 클래식 부분을 스크레이프해요.
마이그레이션 시나리오 같은 상황에서는 계측된 프로그램이 두 버전을 모두 노출한다면 같은 히스토그램에 대해 클래식과 네이티브 버전을 모두 스크레이프하고 싶을 수 있어요. 이 동작을 활성화하려면 스크레이프 구성에 always_scrape_classic_histograms 불리언 설정이 있어요. 기본값은 false지만, true로 설정하면 히스토그램마다 두 버전 모두가 스크레이프·수집돼요. 단 클래식 버킷이 하나 이상 있고 네이티브 버킷 스팬이 하나 이상 있을 때만(이것은 no-op 스팬일 수 있음). 이는 TSDB에서 어떤 충돌도 일으키지 않아요. 클래식 히스토그램이 접미사 시리즈 여러 개로 수집되는 반면 네이티브 히스토그램은 수정되지 않은 이름의 단일 시리즈로 수집되기 때문이에요. (예: rpc_latency_seconds라는 히스토그램은 네이티브 히스토그램 시리즈 rpc_latency_seconds와 클래식 부분의 여러 시리즈, 즉 rpc_latency_seconds_sum, rpc_latency_seconds_count, 그리고 서로 다른 le 레이블을 가진 여러 rpc_latency_seconds_bucket 시리즈를 만들어요.)
클래식 히스토그램을 NHCB로 스크레이프
앞서 언급한 NHCB는 클래식 히스토그램을 네이티브 히스토그램으로 모델링할 수 있어요. convert_classic_histograms_to_nhcb 불리언 스크레이프 구성 옵션을 통해 프로메테우스가 클래식 히스토그램을 NHCB로 수집하도록 구성할 수 있어요.
NHCB가 서로 다른 버킷 레이아웃 간 자동 조정을 지원하지만, 병합 가능성은 여전히 근본적으로 제한돼요. 조정은 관련된 NHCB 사이에서 버킷 경계의 정확한 일치만 유지해요. 대부분의 버킷 경계가 일치하면 유용한 결과를 내요. 하지만 버킷 레이아웃의 임의 변경은 경계가 하나도 일치하지 않는 상황을 쉽게 만들 수 있고, 버킷이 하나(오버플로 버킷)만 있는 히스토그램으로 이어져요.
NHCB의 핵심 장점은 일반적으로 저장 비용이 훨씬 낮다는 것이에요. 특히 추가 버킷의 증분 비용이 상대적으로 낮아서 많은 버킷을 가진 클래식 히스토그램의 저렴한 수집을 허용해요.
TSDB
참고: 이 섹션은 TSDB에 네이티브 히스토그램을 저장하는 것의 높은 수준 개요를 제공하고 놓치기 쉬운 몇 가지 중요한 개별 측면도 설명해요. 구현 세부 사항을 설명하거나, 디스크상 형식을 정의하거나, 코드 베이스를 안내하기 위한 것이 아니에요. 다양한 저장 형식에 대한 상세 문서와 물론 일반 생성 GoDoc이 있으며, tsdb 패키지와 storage 패키지가 적절한 시작점이에요. 도움이 되는 자료는 앞서 언급한 Developer's Guide to Prometheus Native Histograms이기도 해요.
정수 히스토그램 대 float 히스토그램
TSDB는 정수 히스토그램과 float 히스토그램을 다르게 저장해요. 일반적으로 정수 히스토그램이 더 잘 압축될 것으로 기대되므로, 모든 버킷 수와 관측치 count가 int64 범위 안에서 정수 값을 가져 정수 히스토그램으로의 변환이 원래 float 히스토그램의 수치적으로 정밀한 표현을 만들면 TSDB 구현은 float 히스토그램을 정수 히스토그램으로 저장할 수(MAY) 있어요. (프로메테우스 TSDB는 아직 이 옵션을 활용하지 않는다는 점에 유의하세요.)
인코딩
네이티브 히스토그램은 TSDB에 두 개의 새 청크 인코딩(Go 타입 chunkenc.Encoding)이 필요해요: 정수 히스토그램용 chunkenc.EncHistogram(문자열 표현 histogram, 숫자 값 2)과 float 히스토그램용 chunkenc.EncFloatHistogram(문자열 표현 floathistogram, 숫자 값 3).
마찬가지로 WAL과 메모리 내 스냅샷에 대한 두 개의 새 레코드 타입(Go 타입 record.Type)이 있어요: 정수 히스토그램용 record.HistogramSamples(문자열 표현 histogram_samples, 숫자 값 9)과 float 히스토그램용 record.FloatHistogramSamples(문자열 표현 float_histogram_samples, 숫자 값 10). 역호환성을 위해 히스토그램 레코드 타입이 두 개 더 있어요: record.HistogramSamplesLegacy(histogram_samples_legacy, 7)와 record.FloatHistogramSamplesLegacy(float_histogram_samples_legacy, 8). 그것들은 NHCB에 필요한 사용자 정의 값이 도입되기 전에 사용됐어요. 오래된 WAL을 여전히 읽을 수 있도록 지원돼요.
프로메테우스는 시계열을 레이블로만 식별해요. 시리즈의 샘플이 float(그래서 counter 또는 gauge)인지 히스토그램인지(어떤 맛이든)는 시리즈의 정체성에 기여하지 않아요. 따라서 시리즈는 서로 다른 타입과 맛의 샘플을 섞어 포함할 수(MAY) 있어요. 시계열 내 샘플 타입의 변경은 실제로 매우 드물 것으로 기대돼요. 그것은 보통 타깃의 계측 변경 후(같은 메트릭 이름이 변경 전에는 예를 들어 gauge float에, 변경 후에는 counter 히스토그램에 사용되는 드문 경우)나 기록 규칙의 변경 후(예: 규칙의 이전 버전이 gauge float를 만들고 새 버전이 이름을 유지하면서 gauge 히스토그램을 만드는 경우)에 일어나요. 샘플 타입의 빈번한 변경은 보통 잘못된 구성의 결과예요(예: 두 개의 다른 기록 규칙이 같은 시리즈에 서로 다른 샘플 타입을 만드는 경우). 따라서 TSDB 구현은 샘플 타입의 변경을 처리해야(MUST) 하지만 상대적으로 비효율적인 방식으로 처리할 수(MAY) 있어요. 프로메테우스 TSDB가 현재 사용 중인 청크에 쓸 수 없는 샘플 타입을 만나면 그 청크를 닫고 적절한 인코딩으로 새 청크를 시작해요. (샘플마다 샘플 타입을 왔다 갔다 전환하는 시계열은 샘플마다 새 청크로 이어지며, 이는 실제로 매우 비효율적이에요.)
히스토그램 청크는 흔한 값을 덜 흔한 값보다 적은 비트로 인코딩해 데이터 크기를 줄이기 위해 여러 사용자 정의 숫자 인코딩을 사용해요. 각 사용자 정의 인코딩의 세부 사항은 저수준 청크 형식 문서(그리고 궁극적으로 거기에서 링크된 코드)에 설명돼 있어요. 다음 세 인코딩이 여러 다른 필드에 사용되므로 나중에 참조하기 위해 여기 이름을 붙여요:
- varbit-int은 서명된 정수용 가변 비트폭 인코딩이에요. 1비트에서 9바이트를 사용해요. 0에 가까운 숫자일수록 적은 비트가 필요해요. 이는 float 샘플용 청크의 타임스탬프 인코딩과 유사하지만, 네이티브 히스토그램에서 흔히 만나는 값 분포에 최적화된 다양한 비트 길이의 다른 버킷팅을 가져요
- varbit-uint는 서명 없는 정수용 유사한 인코딩이에요
- varbit-xor은 float 시퀀스용 가변 비트폭 인코딩이에요. 시퀀스에서 현재와 이전 float 값을 XOR하는 것에 기반해요. float당 1비트에서 77비트를 사용해요. TSDB가 float 샘플에 이미 사용하는 것과 정확히 같은 인코딩이에요
히스토그램 청크는 평소처럼 청크의 샘플 수(uint16)로 시작하고, 히스토그램이 gauge 히스토그램인지 counter 히스토그램인지 설명하고 후자에 대한 counter 리셋 정보를 제공하는 1바이트가 따라와요. 자세한 내용은 아래 해당 섹션을 참조하세요. 이어서 소위 청크 레이아웃이 따르며, 청크의 모든 히스토그램이 공유하는 다음 정보를 포함해요:
- 제로 버킷의 임계값. 흔한 값(0 또는 특정 2의 거듭제곱)을 1바이트로 인코딩하지만 임의 값에는 9바이트를 요구하는 사용자 정의 인코딩을 사용해요
- 스키마, varbit-int로 인코딩
- 양수 스팬, 스팬 수(varbit-uint)와 각 스팬의 길이(varbit-uint)와 오프셋(varbit-int)이 반복 시퀀스로 인코딩됨
- 음수 스팬, 같은 방식
- 스키마 -53(NHCB)에만 사용자 정의 값이 사용자 정의 인코딩을 사용해 반복 시퀀스로 인코딩됨. 사용자 정의 값 수(varbit-uint)와 값들이 포함됨
청크 레이아웃 뒤에 샘플 데이터의 반복 시퀀스가 따라와요. 샘플 데이터는 정수 히스토그램과 float 히스토그램에서 달라요. 정수 히스토그램의 경우 각 샘플의 데이터는 다음을 포함해요:
- 타임스탬프, varbit-int로 인코딩. 1번째 샘플은 절대값, 2번째 샘플은 1번째와 2번째 샘플 간 델타, 이후 샘플은 "델타의 델타"(즉 기존 float 청크의 타임스탬프에 사용된 것과 같은 "double delta" 인코딩, 다만 varbit-int 인코딩의 비트 버킷팅이 다름)
- 관측치 count, 1번째 샘플은 varbit-uint, 이후 샘플은 타임스탬프와 같은 "델타의 델타" 접근을 사용한 varbit-int로 인코딩
- 제로 버킷 인구, 1번째 샘플은 varbit-uint, 이후 샘플은 타임스탬프와 같은 "델타의 델타" 접근을 사용한 varbit-int로 인코딩
- 관측치 sum, 1번째 샘플은 float64, 이후 샘플은 varbit-xor(현재와 이전 샘플 사이 XOR)로 인코딩
- 양수 버킷의 버킷 인구, 각각 이전 버킷에 대한 델타(또는 1번째 버킷의 절대 인구)로, 타임스탬프와 같은 "델타의 델타" 접근을 사용한 varbit-int로 인코딩. (즉 이미 그 자체로 델타인 값에 "double delta" 인코딩이 적용되며, 이것이 때때로 "triple delta" 인코딩이라고 불리는 이유예요)
- 음수 버킷의 버킷 인구, 같은 방식
float 히스토그램의 샘플 데이터는 다음 차이가 있어요:
- 관측치 count와 제로 버킷 인구가 이제 float이며 관측치 sum과 같은 방식으로 인코딩돼요(1번째 샘플은 float64, 이후 샘플은 varbit-xor)
- 버킷 인구가 이제 float일 뿐 아니라 버킷 간 델타가 아닌 절대 인구 수예요. 1번째 샘플에서 모든 버킷 인구는 일반 float64로 표현되고, 이후 샘플에서는 현재와 이전 샘플의 해당 버킷을 XOR해 varbit-xor로 인코딩돼요
다음 이벤트는 새 청크를 자르도록 촉발해요(괄호 안은 이유):
- 정수 히스토그램과 float 히스토그램 간 샘플 타입 변경(둘은 완전히 다른 청크 인코딩을 요구하기 때문)
- gauge 히스토그램과 counter 히스토그램 간 샘플 타입 변경(선행 바이트가 다른 타입을 나타내야 하기 때문)
- counter 히스토그램의 카운터 리셋(선행 바이트에 counter 리셋 정보로 저장하기 위해, 자세한 내용은 아래)
- 스키마 변경(새 청크 레이아웃이 필요하고 청크는 청크 레이아웃 하나만 가질 수 있기 때문)
- 제로 임계값 변경(청크 레이아웃을 바꾸기 때문, 위 참조)
- 사용자 정의 값 변경(청크 레이아웃을 바꾸기 때문, 위 참조)
- staleness 마커 뒤에 일반 샘플이 오는 경우(엄격히 새 청크를 요구하지는 않지만, 대부분의 히스토그램이 사라졌다 돌아올 때 충분히 많이 변할 것으로 가정할 수 있어 새 청크를 자르는 것이 최선의 옵션)
- 청크 크기 한계 초과(아래 세부 사항 참조)
스팬의 차이도 청크 레이아웃을 바꾸지만, 필요에 따라 (명시적으로 표현된) 인구가 없는 버킷을 추가해 청크의 모든 히스토그램이 같은 스팬 구조를 공유하도록 조정돼요. 버킷이 사라지면 누락된 버킷을 인구가 없는 버킷으로 히스토그램을 청크에 추가하면서 새 히스토그램에 추가하기만 하면 되므로 간단해요. 하지만 이전에 채워졌던 버킷의 소멸은 counter 리셋을 구성하므로(아래 참조) 이 경우는 gauge 히스토그램에서만 일어날 수 있어요(counter 리셋이 없는 경우). 훨씬 더 흔한 경우는 새로 추가된 히스토그램에 이전에 추가된 히스토그램에는 없던 버킷이 존재하는 것이에요. 이 경우 그 버킷들을 이전에 추가된 모든 히스토그램에 인구가 없는 버킷으로 명시적으로 추가해야 해요. 이는 전체 청크의 완전한 재인코딩을 요구해요. (영향을 받는 부분만 재인코딩하는 최적화 가능성이 있어요. 이를 구현하는 것은 꽤 복잡할 거예요. 지금까지 전체 재인코딩의 성능 영향은 문제로 두드러지지 않았어요.)
Staleness 마커
참고: 다음 섹션을 이해하려면 TSDB에서 staleness 마커가 어떻게 동작하는지 상기하는 것이 중요해요. float 시리즈의 staleness 마커는 NaN 값을 나타내는 데 사용될 수 있는 여러 비트 패턴 중 하나의 특정 비트 패턴으로 표현돼요. 이 매우 특정한 float 값을 다음 섹션에서 "특수 stale NaN 값"이라고 불러요. 그것은 일반적인 산술 float 연산이 (거의 확실히) 반환하지 않으며, 관측값의 특수한 경우에서 논의된 것을 포함해 "자연 발생" NaN 값과 다르다는 점이에요. 실제로 특수 stale NaN 값은 TSDB를 쿼리할 때 직접 반환되지 않으며 호출자에게 도달하기 전에 내부적으로 처리돼요.
히스토그램 시리즈에 staleness를 표시하려면 일반적인 특수 stale NaN 값을 사용할 수 있어요. 하지만 이는 시리즈를 stale로 표시하는 목적으로만 새 청크를 자르는 것을 요구해요. 히스토그램 값 뒤에 오는 float 값은 다른 청크에 저장해야 하기 때문이에요(위 참조). 따라서 관측치 sum 필드가 특수 stale NaN 값으로 설정된 staleness 마커의 히스토그램 버전도 있어요. 이 경우 다른 모든 필드는 무시되므로 효율적인 저장에 적합한 값으로 설정할 수 있어요(staleness 마커의 히스토그램 버전이 본질적으로 저장 최적화일 뿐이므로). 이는 float와 정수 히스토그램 모두에 동작해요(정수 히스토그램에서도 sum 필드는 float 값이므로), 그리고 적절한 버전을 사용해 새 청크를 자르는 것을 피할 수 있어요. 모든 버전의 staleness 마커(float, 정수 히스토그램, float 히스토그램)는 TSDB가 동등하게 취급해야(MUST) 해요.
청크 크기 한계
float 청크의 크기는 1024바이트로 제한돼요. 히스토그램 청크에도 일반적으로 같은 크기 제한이 사용돼요. 다만 개별 히스토그램은 버킷이 많으면 매우 커질 수 있으므로, 크기 제한을 무작정 적용하면 히스토그램이 거의 없는 청크로 이어질 수 있어요. (가장 극단적인 경우 단일 히스토그램이 1024바이트를 넘어 크기 제한을 전혀 적용할 수 없을 수도 있어요.) 청크당 히스토그램 수가 매우 적으면 압축 비율이 나빠져요. 따라서 1024바이트의 크기 제한이 발동하기 전에 청크당 최소 10개의 히스토그램 수에 도달해야 해요. 이는 히스토그램 청크가 1024바이트보다 훨씬 클 수 있음을 의미해요.
청크당 최소 10개 히스토그램을 요구하는 것은 초기의 매우 단순한 접근이며, 청크 크기와 압축 비율 사이의 더 나은 트레이드오프를 찾기 위해 미래에 개선될 수 있어요.
카운터 리셋 고려사항
일반적으로 프로메테우스는 값이 샘플에서 다음 샘플로 떨어질 때마다 카운터가 리셋된 것으로 간주해요(하지만 아래 시작 타임스탬프에 대한 다음 섹션도 참조). 두 히스토그램 샘플 사이의 카운터 리셋을 감지할 때는 상황이 더 복잡해요.
우선 gauge 히스토그램과 counter 히스토그램은 명시적으로 다르지만(프로메테우스는 일반적으로 모든 float 샘플을 수집 후 같게 취급하며, gauge 또는 counter 메트릭으로 수집됐는지는 무관), 카운터 리셋은 gauge 히스토그램에는 적용되지 않아요.
시계열에서 gauge 히스토그램 뒤에 counter 히스토그램이 오면 카운터 리셋이 일어난 것으로 가정돼요. gauge에서 counter로의 변경이 gauge 삭제와 0에서 새로 만들어진 counter와 동등한 것으로 간주되기 때문이에요.
가장 흔한 경우는 counter 히스토그램 뒤에 다른 counter 히스토그램이 오는 것이에요. 이 경우 가능한 카운터 리셋은 다음 절차로 감지돼요:
두 히스토그램이 모두 표준 스키마를 사용하지만 스키마나 제로 버킷 폭이 다르면, 이 변경은 호환 가능한 해상도 감소(히스토그램의 버킷 수를 줄이기 위해 정기적으로 발생하는 것, 버킷 수 제한 참조)의 일부일 수 있어요. 호환 가능한 해상도 감소에는 다음 둘 다가 참이에요:
- 스키마가 변경됐다면 그 번호가 하나의 표준 지수 스키마에서 다른 표준 스키마로 감소했어요
- 제로 버킷 폭이 변경됐다면 첫 번째 히스토그램의 인구가 있는 일반 버킷이 두 번째 히스토그램의 제로 버킷에 완전히 포함되거나 전혀 포함되지 않아요(즉 이전 일반 버킷과 새 제로 버킷의 부분적 겹침 없음)
조건 중 하나라도 충족되지 않으면 변경은 호환 가능한 해상도 감소가 아니에요. 그런 변경은 히스토그램을 리셋하거나 새로 만드는 것을 통해서만 가능하므로 카운터 리셋으로 간주되고 감지 절차가 종료돼요.
두 조건이 모두 충족되면 첫 번째 히스토그램을 변환해 스키마와 제로 버킷 폭이 두 번째 히스토그램과 일치하도록 해야 해요. 이는 앞서 설명한 것과 같은 방식으로 발생해요: 이웃 버킷을 병합해 스키마를 줄이고, 일반 버킷을 제로 버킷과 병합해 제로 버킷 폭을 늘려요.
두 히스토그램이 모두 NHCB(스키마 -53)면 사용자 정의 값의 차이는 아래 설명대로 조정돼요.
절차의 이 시점에서 두 히스토그램은 처음부터 그랬거나 그에 따라 하나가 변환됐기 때문에 같은 스키마와 제로 버킷 폭을 가져요. (NHCB는 제로 버킷을 사용하지 않는다는 점에 유의하세요. 그들의 제로 버킷 폭과 인구 수는 이 절차를 위해 동등한 것으로 간주돼요.) 이 상황에서 다음 중 하나가 카운터 리셋을 구성해요:
- 관측치 count의 감소(단, 관측치 sum의 감소는 아님)
- 제로 버킷을 포함한 어떤 버킷의 인구 수 감소. 여기에는 채워졌던 버킷의 소멸이 포함돼요. 표현되지 않은 버킷은 인구가 0인 버킷과 동등하기 때문이에요
위 중 어느 것도 해당하지 않으면 카운터 리셋이 없어요.
이 전체 절차가 상대적으로 복잡하므로 카운터 리셋 감지는 수집 중에 한 번 발생하고 결과가 나중에 사용하기 위해 영속화되는 것이 바람직해요. 카운터 리셋이 새 청크를 자르는 트리거 중 하나이므로 수집 중 카운터 리셋 감지는 어쨌든 해야 해요.
카운터 리셋 후 새 청크를 자르는 것은 압축 비율을 개선하는 것을 목표로 해요. 카운터 리셋은 모든 버킷 인구를 0으로 설정하므로 표현할 버킷이 더 적어요. 하지만 청크는 청크의 모든 히스토그램의 모든 버킷의 상위집합을 표현해야 하므로, 새 청크를 자르면 새 청크에 더 단순한 버킷 세트를 활성화할 수 있어요.
이는 결국 청크의 첫 번째 샘플 이후에는 카운터 리셋이 절대 없을 것임을 의미해요. 따라서 영속화해야 하는 유일한 counter 리셋 정보는 청크의 1번째 히스토그램의 것이에요. 이것은 소위 히스토그램 플래그에서 발생하며, 청크의 샘플 수 바로 뒤에 저장되는 단일 바이트예요. 이 바이트는 현재 counter 리셋 정보에만 사용되지만 미래에 다른 플래그에 사용될 수 있어요. counter 리셋 정보는 처음 두 비트를 사용해요. 네 가지 가능한 비트 패턴은 chunkenc 패키지의 CounterResetHeader 타입의 Go 상수로 표현돼요. 이름과 의미는 다음과 같아요:
GaugeType(비트 패턴11): 청크에 gauge 히스토그램이 포함돼요. 카운터 리셋은 gauge 히스토그램에 무관해요CounterReset(비트 패턴10): 이전 청크의 마지막 히스토그램과 이 청크의 1번째 히스토그램 사이에 카운터 리셋이 발생했어요. (카운터 리셋이 실제로 새 청크를 자른 이유였을 가능성이 높아요)NotCounterReset(비트 패턴01): 이전 청크의 마지막 히스토그램과 이 청크의 1번째 히스토그램 사이에 카운터 리셋이 없었어요. (이것은 보통 이전 청크가 크기 한계에 도달해 새 청크가 잘렸을 때 발생해요)UnknownCounterReset(비트 패턴00): 이전 청크의 마지막 히스토그램과 이 청크의 1번째 히스토그램 사이에 카운터 리셋이 있었는지 알 수 없어요
UnknownCounterReset은 항상 안전한 선택이에요. 카운터 리셋 감지를 방해하지 않고, counter 리셋 정보가 필요할 때마다 (다시) 카운터 리셋 감지 절차를 수행해야 한다는 것만 요구해요.
counter 리셋 정보는 TSDB를 쿼리할 때 호출자에게 전파돼요(Go 코드에서 Go 타입 Histogram과 FloatHistogram의 CounterResetHint 타입 필드로, 위 비트 패턴 상수와 같은 이름의 열거형 상수를 사용해요).
gauge 히스토그램의 경우 CounterResetHint는 항상 GaugeType이에요. 다른 CounterResetHint 값은 해당 히스토그램이 counter 히스토그램임을 의미해요. 이렇게 해서 쿼리자(PromQL 엔진 포함, 아래 참조)는 히스토그램이 gauge인지 counter인지에 대한 정보를 얻어요(float 샘플과는 현저히 다름).
counter 히스토그램이 단일 청크에서 순서대로 반환되는 한, 청크의 2번째 이후 히스토그램의 CounterResetHint는 NotCounterReset으로 설정돼요. (겹치는 블록과 순서 없는 수집은 여러 청크에서 오는 히스토그램 시퀀스로 이어질 수 있어 특별한 처리가 필요합니다, 아래 참조.)
counter 히스토그램 청크에서 1번째 히스토그램을 반환할 때, TSDB 구현이 이전에 반환된 히스토그램이 실제로 수집 시 counter 리셋을 감지하는 데 사용된 앞선 히스토그램과 같은 히스토그램임을 보장할 수 있는 경우에만 CounterResetHint를 UnknownCounterReset으로 설정해야(MUST) 해요. 후자의 경우에만 청크의 counter 리셋 정보를 반환된 히스토그램의 CounterResetHint로 직접 사용할 수(MAY) 있어요.
이 예방책은 청크가 제거되거나 삽입될 수 있는 다양한 방법이 있어서 필요해요(예: tombstone으로 삭제하거나 백필링용 블록 추가). 카운터 리셋은 한 샘플에 귀속되지만 실제로는 표시된 샘플과 앞선 샘플 사이에서 발생해요. 앞선 샘플을 제거하거나 두 샘플 사이에 다른 샘플을 삽입하면 이전에 수행된 카운터 리셋 감지가 무효화돼요.
TODO: 현재 프로메테우스 TSDB는 앞선 청크가 수집 중과 같은 청크인지 보장할 수단이 없어요. 따라서 프로메테우스는 현재 counter 히스토그램 청크의 모든 1번째 히스토그램에 UnknownCounterReset을 반환해요. 이를 바꾸려는 노력은 추적 이슈를 참조하세요.
위에서 이미 암시했듯, CounterResetHint가 UnknownCounterReset으로 설정되면 쿼리자는 (다시) 카운터 리셋 감지 절차를 수행해야(MUST) 해요.
겹치는 블록이나 순서 없는 샘플을 처리할 때(쿼리 또는 컴팩션 중) 특별한 주의를 기울여야 해요. 이 경우 카운터 리셋의 과잉 감지와 과소 감지가 모두 일어날 수 있으며, 다음 예시로 설명돼요:
- 과소 감지 예시: 한 청크에 카운터 리셋 없이 샘플 ABC가 포함돼 있어요. 다른 청크에 역시 카운터 리셋 없이 샘플 DEF가 포함돼 있어요. 청크는 겹치고 같은 시리즈를 참조해요. 함께 쿼리하면 샘플의 시간 순서가 ADBECF로 드러나요. 이제 그 샘플들 중 일부 또는 전부 사이에 카운터 리셋이 있을 가능성이 매우 높아요. 이는 사실 두 샘플이 관련 없는 시리즈에서 와서 실수로 같은 시리즈로 병합된 경우에 발생할 가능성이 높아요. 하지만 이렇게 우연한 병합도 TSDB가 올바르게 처리해야 해요. 겹치는 청크가 새 청크로 컴팩트되면 새 카운터 리셋 감지가 일어나 새 카운터 리셋을 잡아야 해요. 겹치는 청크를 (사전 컴팩션 없이) 직접 쿼리하면 이전에 반환된 샘플과 다른 청크에서 온 각 샘플에
UnknownCounterReset의CounterResetHint를 설정해야 하며, 이는 쿼리자가 (위에서 설명한 안전한 폴백을 활용해) 카운터 리셋 감지를 하도록 강제해요 - 과잉 감지 예시: B와 C 사이에 카운터 리셋이 발생하며 샘플 ABCD의 시퀀스가 있어요. 하지만 초기 수집은 B와 C를 놓쳐 A와 D만 수집됐고, A와 D 사이에 카운터 리셋이 감지됐어요. 나중에 B와 C가 (순서 없는 수집으로 또는 나중에 별도 블록으로 TSDB에 추가된 별도 청크로) 수집되고, B와 C 사이에 카운터 리셋이 감지돼요. 이 경우 각 샘플은 자체 청크로 들어가므로 모든 청크를 조립할 때 겹치지도 않아요. 하지만 위 규칙에 따라 counter 리셋 힌트를 반환할 때 C와 D 둘 다
CounterReset의CounterResetHint로 쿼리자에게 반환될 텐데, 지금은 C와 D 사이에 카운터 리셋이 없어요. 이전 예시의 상황과 유사하게, A와 B 사이에 새 카운터 리셋 감지를 수행하고 C와 D 사이에 다른 하나를 수행해야 해요. 또는 B와 D 둘 다UnknownCounterReset의CounterResetHint로 반환해야 해요
요약하면 TSDB가 수집 시 두 샘플 사이에 카운터 리셋 감지가 일어났음을 안전하게 확립할 수 없을 때마다, 다른 카운터 리셋 감지를 수행하거나 두 번째 샘플에 UnknownCounterReset의 CounterResetHint를 반환해야 해요.
위에서 설명한 절차로 감지되지 않는 카운터 리셋의 가능성이 있다는 점에 유의하세요. 즉 리셋된 히스토그램의 count가 충분히 빠르게 증가해서 카운터 리셋 후 첫 번째 샘플이 카운터 리셋 전 마지막 샘플과 비교해 감소된 count가 없을 때예요. (이것은 float 카운터에도 문제이며, 실제로 더 일어날 가능성이 높아요.) 위에서 설명한 메커니즘으로 이 경우에도 카운터 리셋을 저장할 수 있어요. 카운터 리셋이 다른 수단으로 감지됐다면요. 하지만 청크의 삽입·제거, 순서 없는 샘플, 겹치는 블록으로 인한 복잡성(위에서 설명) 때문에 이 정보는 두 번째 카운터 리셋 감지 라운드가 필요하면 손실될 수(MAY) 있어요. (TODO: 현재 이 정보는 확실히 손실되며, 위 TODO 참조.) 카운터 리셋을 안전하게 표시하는 더 나은 방법은 시작 타임스탬프를 통해서예요(다음 섹션 참조).
시작 타임스탬프 처리
OpenMetrics는 카운터, summary, 클래식 counter 히스토그램에 대해 소위 created timestamps를 도입했어요. (용어는 아마 "created-at timestamp"의 줄임말일 거예요. 더 적절한 용어는 "creation timestamp"나 "reset timestamp"였을지도 몰라요.) 용어는 이후 "start timestamp"로 바뀌었어요.
시작 타임스탬프는 메트릭이 생성되거나 리셋된 가장 최근 시간을 제공해요. 프로메테우스가 시작 타임스탬프를 어떻게 처리하는지는 설계 문서가 설명해요.
시작 타임스탬프는 네이티브 히스토그램에도 유용해요. float 카운터에 합성 제로 샘플이 삽입되는 것과 같은 방식으로, counter 히스토그램에는 히스토그램 샘플의 제로 값이 삽입돼요. 히스토그램의 제로 값은 채워진 버킷이 없고 관측치 sum, 관측치 count, 제로 버킷 인구가 모두 0이에요. 스키마, 제로 버킷 폭, 사용자 정의 값, 히스토그램의 float 대 정수 맛은 합성 제로 샘플 직후에 오는 샘플과 일치해야(SHOULD) 해요(거짓 카운터 리셋 감지를 촉발하지 않도록).
합성 제로 샘플의 counter 리셋 정보는 항상 CounterReset으로 설정돼요.
Exemplar
네이티브 히스토그램용 exemplar는 개별 버킷이 아니라 히스토그램 샘플 전체에 붙어요. (exposition 형식 섹션 참조.) 따라서 단일 네이티브 히스토그램 샘플이 여러 exemplar를 붙이는 것이 허용되며(실제로 흔한 경우) 돼요.
Exemplar는 스크레이프에서 다음 스크레이프로 변할 수도 있고 그렇지 않을 수도 있어요. 스크레이퍼는 많은 중복 exemplar를 저장하지 않도록 변하지 않은 exemplar를 감지해야(SHOULD) 해요. 다만 단일 샘플에 많은 exemplar가 있을 수 있고 그 중 아무 부분집합이나 마지막 스크레이프의 반복 exemplar일 수 있으므로 중복 감지는 잠재적으로 비싸요. TSDB는 새 exemplar가 이전에 노출된 exemplar 중 어떤 것보다 더 최근 타임스탬프를 가진다는 가정에 의존할 수(MAY) 있어요. (네이티브 히스토그램의 exemplar는 타임스탬프가 있어야(MUST) 한다는 것을 기억하세요.) 그러면 중복 감지를 효율적인 방식으로 할 수 있어요:
- 새로 수집된 네이티브 히스토그램의 exemplar는 다음 필드로 정렬돼요: 먼저 타임스탬프, 그 다음 값, 그 다음 레이블
- exemplar는 정렬된 순서로 exemplar 저장소에 추가돼요
- 마지막으로 성공적으로 추가된 exemplar(같은 메트릭에 대한 이전 스크레이프의 것일 수 있음)보다 앞에 정렬되거나 같은 exemplar는 추가가 실패해요
- 마지막으로 성공적으로 추가된 exemplar보다 뒤에 정렬되는 exemplar는 추가가 성공해요
수집된 히스토그램의 모든 exemplar가 마지막으로 성공적으로 추가된 exemplar보다 앞에 정렬되는 경우에만 exemplar가 순서 없는 것으로 간주돼요. 이는 더 새로운 exemplar나 마지막으로 성공적으로 추가된 exemplar의 중복과 섞인 순서 없는 exemplar를 감지하지 못하며, 이는 허용 가능한 것으로 간주돼요.
PromQL
이 섹션은 PromQL이 네이티브 히스토그램을 어떻게 처리하는지 설명해요. 개별 연산의 모든 세부 사항보다는 일반 개념에 초점을 맞춰요. 후자에 대해서는 연산자와 함수에 대한 PromQL 문서를 참조하세요.
어노테이션 (Annotations)
네이티브 히스토그램의 도입은 PromQL 표현식이 예상치 못한 결과를 반환하는 특정 상황을 만드는데, 가장 흔하게는 출력 벡터의 일부 또는 모든 요소가 예상치 못하게 누락되는 경우예요. 사용자가 그런 상황을 감지하고 이해하도록 돕기 위해 네이티브 히스토그램에 동작하는 연산은 종종 어노테이션을 사용해요. 어노테이션은 warn과 info 레벨을 가질 수 있고 평가 중 만난 가능한 문제를 설명해요. Warn 레벨은 사용자가 조치해야 할 실제 문제일 가능성이 가장 높은 상황을 표시하는 데 사용돼요. Info 레벨은 의도적일 수도 있지만 충분히 드물어 표시할 가치가 있는 상황에 사용돼요.
정수 히스토그램 대 float 히스토그램
PromQL은 항상 float 히스토그램에 동작해요. 정수 히스토그램으로 저장된 네이티브 히스토그램은 TSDB에서 검색될 때 자동으로 float 히스토그램으로 변환돼요.
히스토그램 간 호환성
연산자나 함수가 두 개 이상의 네이티브 히스토그램에 동작할 때 관련된 히스토그램은 같은 스키마, 같은 제로 버킷 폭, (해당하면) 같은 사용자 정의 값을 가져야 해요. 특정 한계 내에서 히스토그램은 이 호환성 기준을 충족하기 위해 즉석에서 변환될 수 있어요:
- NHCB(스키마 -53)는 서로만 호환돼요. 서로 다른 사용자 정의 값은 다음 방식으로 변환을 통해 조정해야 해요:
- 원래 NHCB 모두가 공유하는 사용자 정의 값을 식별해요. 이것들이 새 조정된 사용자 정의 값이에요
- 각 원래 NHCB를 새 사용자 정의 값으로 변경해 그 버킷을 새 사용자 정의 값이 설명하는 통합 버킷 세트로 병합해요
- 원래 NHCB가 어떤 사용자 정의 값도 공유하지 않는 것이 쉽게 가능하다는 점에 유의하세요. 이 경우 새 버킷 세트는 오버플로 버킷만으로 구성되고, 원래 버킷의 모든 관측치를 가져요
- 사용자 정의 값의 조정을 요구하는 어떤 쿼리든 info 레벨 어노테이션으로 표시돼요
- 표준 스키마를 가진 히스토그램은 더 큰 스키마(즉 더 높은 해상도)를 가진 히스토그램의 해상도를 줄여 항상 가장 작은(즉 가장 낮은 해상도의) 공통 스키마로 변환될 수 있어요. 이것은 보통의 방식으로 이웃 버킷을 더 작은 스키마의 더 큰 버킷으로 병합해 발생해요
- 서로 다른 제로 버킷 폭은 더 작은 제로 버킷을 확장하고, 적절히 채워진 일반 버킷을 확장된 제로 버킷으로 병합해 처리돼요. 가장 큰 공통 폭이 우연히 어떤 채워진 버킷의 중간에 끝나면 그 버킷의 경계와 일치하도록 더 확장돼요. (자세한 내용은 위 제로 버킷 섹션 참조)
비호환성이 연산을 막으면 warn 레벨 어노테이션이 결과에 추가돼요.
카운터 리셋
카운터 리셋은 위 설명대로 정의돼요. TSDB에서 반환된 카운터 리셋 힌트는 명시적 카운터 리셋 감지를 피하고 보통 절차로 감지할 수 없는 카운터 리셋을 올바르게 처리하기 위해 고려될 수(MAY) 있어요. (이는 이 카운터 리셋이 best effort 기반으로만 고려된다는 것을 의미해요. 하지만 TSDB 자체에도 동일하게 적용돼요, 위 참조.) 클래식 히스토그램과 summary의 카운터 리셋 처리와의 주목할 만한 차이는 관측치 sum의 감소가 그 자체로 카운터 리셋을 구성하지 않는다는 것이에요. (예를 들어 네이티브 히스토그램의 rate 계산은 히스토그램이 음수 값을 관측했더라도 여전히 올바르게 동작해요.)
서브쿼리가 반환한 counter 히스토그램의 카운터 리셋 힌트는, PromQL 엔진이 서브쿼리가 반환한 연속 counter 히스토그램이 TSDB에서도 연속적임을 안전하게 감지할 수 있는 경우가 아니면 명시적 카운터 리셋 감지를 피하기 위해 고려해서는 안(MUST NOT) 된다는 점에 유의하세요.
Gauge 히스토그램 대 counter 히스토그램
TSDB에서 반환된 카운터 리셋 힌트를 통해 PromQL은 네이티브 히스토그램이 gauge인지 counter인지 인식해요. float 샘플에 대한 PromQL 처리(float 카운터와 gauge를 안정적으로 구별할 수 없는)를 반영하기 위해, 카운터에 동작하는 함수는 여전히 gauge 히스토그램을 처리하고 그 반대도 마찬가지지만 결과와 함께 warn 레벨 어노테이션이 반환돼요. 그 경우 gauge 히스토그램을 counter 히스토그램인 것처럼 취급해 명시적 카운터 리셋 감지를 수행해야 한다는 점에 유의하세요.
버킷 내 보간
히스토그램에서 분위수나 관측치 비율을 추정하거나 히스토그램에서 관측치를 제거할 때 PromQL은 버킷 내 보간을 적용해야 해요. 클래식 히스토그램에서는 이 보간이 선형 방식으로 발생해요. 관측치가 버킷 내에 균등하게 분포한다는 가정에 기반해요. 실제로 이 가정은 크게 벗어날 수 있어요. (예를 들어 API 엔드포인트가 거의 모든 요청에 110ms의 지연 시간으로 응답할 수 있어요. 중앙값 지연 시간과 어쩌면 90번째 백분위수 지연 시간이 110ms에 가까워질 거예요. 클래식 히스토그램이 100ms와 200ms에 버킷 경계를 가지면 대부분의 관측치를 그 범위에서 보고 중앙값을 150ms, 90번째 백분위수를 190ms로 추정할 거예요.) 최악의 경우는 실제 값이 버킷의 다른 쪽 끝에 있는데 버킷 한쪽 끝에서 추정하는 것이에요. 따라서 최대 가능 오차는 버킷 전체 폭이에요. 보간을 하지 않고 버킷 내부의 어떤 고정 중간점을 사용하면(예: 산술 평균 또는 심지어 조화 평균) 최대 가능 오차(산술 평균의 경우 버킷 폭의 절반)가 최소화되지만, 실제로 선형 보간은 평균적으로 더 낮은 오차를 내요. 이 보간이 수년간의 클래식 히스토그램 사용에서 잘 작동했으므로 네이티브 히스토그램에도 보간이 적용돼요.
NHCB의 경우 PromQL은 클래식 히스토그램과 같은 선형 보간 방법을 적용해 결과를 일관되게 해요. (NHCB의 주요 사용 사례는 클래식 히스토그램의 drop-in 대체예요.) 여기에는 다음 특수한 경우가 포함돼요:
- 모든 사용자 정의 값이 양수면 가장 낮은 버킷의 하한이 0으로 가정돼요. (이 휴리스틱을 피하기 위해 사용자 정의 값으로 0을 포함하는 것이 권장된다는 점에 유의하세요.)
- 사용자 정의 값이 하나라도 0이거나 음수면 가장 낮은 버킷의 하한이 -Inf로 가정돼요. 다만 보간 목적으로 가장 낮은 버킷의 모든 관측치가 버킷의 상한과 같다고 가정돼요
- 유사하게 오버플로 버킷의 모든 관측치가 마지막 사용자 정의 값(즉 가장 높은 일반 버킷의 상한)과 같다고 가정돼요
- 사용자 정의 값이 없는 NHCB의 경우 모든 관측치가 값 0으로 가정돼요
표준 지수 스키마의 경우 선형 보간은 부적합으로 볼 수 있어요. 지수 스키마가 주로 분위수 추정의 상대 오차를 최소화하려 하지만, 적어도 특정 관측값 범위에 걸쳐 버킷의 균형 잡힌 사용으로부터 이익을 얻기도 해요. 기본 가정은 대부분의 실질적으로 발생하는 분포에서 관측치의 밀도가 더 작은 관측값에 대해 더 높은 경향이 있다는 것이에요. 따라서 PromQL은 표준 스키마에 지수 외삽을 사용하며, 이는 스키마 번호를 1 증가시킬 때(즉 해상도를 두 배로) 버킷을 둘로 나누면 평균적으로 두 새 버킷 모두에서 비슷한 인구를 보게 될 것이라는 가정을 모델링해요. 더 자세한 설명은 보간 방법을 구현한 PR에서 찾을 수 있어요.
특수한 경우는 제로 버킷 내 보간이에요. 제로 버킷은 지수 버킷팅 스키마를 깨뜨려요. 따라서 제로 버킷 내에서는 선형 보간이 적용돼요. 게다가 히스토그램의 모든 채워진 일반 버킷이 양수면 제로 버킷의 모든 관측치도 양수라고 가정돼요. 즉 보간이 0과 제로 버킷의 상한 사이에서 이뤄져요. 모든 채워진 일반 버킷이 음수인 히스토그램의 경우 상황이 미러링되며, 즉 제로 버킷 내 보간이 제로 버킷의 하한과 0 사이에서 이뤄져요.
혼합 시리즈
위에서 이미 논의했듯 네이티브 히스토그램의 샘플 타입도 맛도 시리즈의 정체성의 일부가 아니에요. 따라서 하나의 같은 시리즈가 서로 다른 샘플 타입과 맛을 섞어 포함할 수 있어요.
counter 히스토그램과 gauge 히스토그램의 혼합은 어떤 PromQL 연산도 막지 않지만, 일부 입력 샘플이 부적절한 맛을 가지면 결과와 함께 warn 레벨 어노테이션이 반환돼요(위 참조).
float 샘플과 히스토그램 샘플의 혼합은 더 문제가 돼요. 범위 벡터에 동작하는 많은 함수는 입력 요소가 float와 히스토그램을 섞어 포함하면 결과에서 요소를 제거해요. 이것이 발생하면 warn 레벨 어노테이션이 결과에 추가돼요. 구체적인 예시는 아래 참조에서 찾을 수 있어요.
단항 마이너스와 음수 히스토그램
단항 마이너스는 네이티브 히스토그램에 사용할 수 있어요. 모든 버킷 인구와 관측치 count·sum의 부호가 반전된 히스토그램을 반환해요. 카운터 리셋 힌트는 어떤 경우든 GaugeType으로 설정돼요. 다른 모든 것은 같게 유지돼요. GaugeType을 강제하는 것은 명시적 카운터 리셋 감지가 반전된 부호에 의해 흐트러질 것이기 때문에 필요해요.
일반적으로 음수 버킷 인구나 음수 관측치 count를 가진 히스토그램은 그 자체로 말이 안 되고 다른 표현식 내부의 중간 결과로만 사용되도록 설계됐어요. 그것들은 PromQL 내에서 항상 gauge 히스토그램으로 간주돼요. 기록 규칙의 결과로 영속화될 수 없어요. (음수 히스토그램으로 평가되는 규칙은 오류가 돼요.) 어떤 교환 형식(exposition 형식, remote-write, OTLP)에서도 음수 히스토그램을 표현하는 것은 불가능해요.
이항 연산자
대부분의 이항 연산자는 두 히스토그램 사이, 히스토그램과 float 사이, 히스토그램과 스칼라 사이에서 동작하지 않아요. 연산자가 그런 불가능한 조합을 처리하면 해당 요소가 출력 벡터에서 제거되고 info 레벨 어노테이션이 결과에 추가돼요. (이 상황은 레이블 매칭과 다소 비슷하며, 샘플 타입이 레이블과 비슷한 역할을 해요. 따라서 그런 불일치는 알려져 있고 의도적일 수 있으며, 이것이 어노테이션 레벨이 어째서 info뿐인 이유예요.)
다음은 실제로 동작하는 모든 연산을 설명해요.
덧셈과 뺄셈
덧셈(+)과 뺄셈(-)은 두 개의 호환 가능한 히스토그램 사이에서 동작해요. 이 연산자들은 모든 일치하는 버킷 인구와 관측치 count·sum을 더하거나 빼요. 누락된 버킷은 비어 있는 것으로 가정되고 그에 따라 처리돼요.
일반적으로 두 피연산자는 gauge여야 해요. counter 히스토그램을 더하고 빼는 것은 주의를 요구하지만 PromQL은 허용해요. gauge 히스토그램과 counter 히스토그램을 더하면 gauge 히스토그램이 돼요. 두 counter 히스토그램을 더하면 counter 히스토그램이 돼요. 두 피연산자가 같은 카운터 리셋 힌트를 공유하면 결과 counter 히스토그램이 그 카운터 리셋 힌트를 유지해요. 그렇지 않으면 결과 카운터 리셋 힌트가 UnknownCounterReset으로 설정돼요. 뺄셈의 결과는 항상 gauge 히스토그램으로 표시돼요. 음수 히스토그램이 될 수 있기 때문이며, 위 참고를 참조하세요. 직접 모순되는 카운터 리셋 힌트(즉 CounterReset과 NotCounterReset)를 가진 두 counter 히스토그램을 더하거나 빼면 warn 레벨 어노테이션이 촉발돼요. (TODO: 위 설명대로 TSDB는 현재 NotCounterReset을 반환하지 않으므로 이 어노테이션은 추가 카운터 리셋 추적을 포함하는 HistogramStatsIterator와 관련된 특정 상황에서만 발생할 거예요. 추적 이슈 참조.)
곱셈
곱셈(*)은 한쪽의 float 샘플이나 스칼라와 다른 쪽의 히스토그램 사이에서 어떤 순서로든 동작해요. 모든 버킷 인구와 관측치 count·sum을 float(샘플 또는 스칼라)로 곱해요. 이는 "스케일"되고 때로는 음수 히스토그램으로 이어지며, 보통 다른 표현식 내부의 중간 결과로만 유용해요(위 참고 참조).
곱셈은 counter 히스토그램과 gauge 히스토그램 모두에 대해 동작하고, 음수 값으로 곱하면 항상 gauge 히스토그램이 되는 예외를 제외하고 그 맛은 연산에 의해 변하지 않아요.
나눗셈
나눗셈(/)은 왼쪽의 히스토그램과 오른쪽의 float 샘플이나 스칼라 사이에서 동작해요. float(샘플 또는 스칼라)의 역수로 곱하는 것과 동등해요. 0으로 나누면 일반 버킷이 없고 제로 버킷 인구와 관측치 count·sum이 모두 +Inf, -Inf, NaN 중 하나로 설정된 히스토그램이 돼요(입력 히스토그램의 값이 각각 양수, 음수, 0/NaN일 때).
같음과 같지 않음
같음(==)과 같지 않음(!=)은 필터링 버전과 bool 수정자 모두에서 두 히스토그램 사이에 동작해요. 스키마, 사용자 정의 값, 제로 임계값, 모든 버킷 인구, 관측치 sum·count를 비교해요. 히스토그램이 counter 또는 gauge 맛을 가지는지는 비교에 무관해요. (counter 히스토그램이 gauge 히스토그램과 같을 수 있어요.)
논리/집합 연산자
논리/집합 이항 연산자(and, or, unless)는 히스토그램 샘플이 관련되어도 예상대로 동작해요. 벡터 요소의 존재만 확인하고 요소의 샘플 타입이나 맛(float 또는 히스토그램, counter 또는 gauge)에 따라 동작을 바꾸지 않아요.
트림 연산자
"trim upper"(/<)과 "trim lower"(/>) 연산자는 네이티브 히스토그램을 위해 특별히 도입됐어요. 왼쪽의 히스토그램과 오른쪽의 float 샘플이나 스칼라 사이에서만 동작해요. (다른 모든 경우에는info 레벨 어노테이션이 반환돼요.)
이 연산자들은 왼쪽 히스토그램에서 오른쪽 float 값보다 각각 크거나 작은 관측치를 제거하고 결과 히스토그램을 반환해요.
제거는 임계값이 버킷 경계와 일치할 때만 정밀해요. 그렇지 않으면 위 설명대로 영향을 받는 버킷 내 보간을 사용해야 하며, NHCB에 대한 다음 예외가 있어요:
- 가장 낮은 버킷이 -Inf의 하한을 가지면 그 버킷의 모든 관측치가 값 -Inf(하한이 아닌)로 간주돼요
- 가장 높은 버킷이 +Inf의 상한을 가지면 그 버킷의 모든 관측치가 값 +Inf(하한이 아닌)로 간주돼요
- 하지만 히스토그램이 -Inf의 하한과 +Inf의 상한을 가진 버킷 하나만 가지는 병리적 극단의 경우, 모든 관측치는 여전히 값 0으로 간주돼요
관측치가 제거됐다면 결과 히스토그램의 모든 관측치 합이 남은 버킷에서 추정돼요. 주어진 버킷의 각 관측치 값은 다음으로 추정돼요:
- 상한이 음수이거나 0인 NHCB의 가장 낮은 버킷의 상한. (이것은 위에서 설명한 트리밍에 다른 것을 사용함에도 원래의 보간 휴리스틱을 다시 따릅니다)
- NHCB 오버플로 버킷의 하한. (다시 원래 보간 휴리스틱으로 돌아감)
- 표준 지수 히스토그램의 음수 오버플로 버킷에 대한 -Inf
- 표준 지수 히스토그램의 양수 오버플로 버킷에 대한 +Inf
- NHCB의 다른 모든 버킷과 표준 지수 히스토그램의 제로 버킷에 대한 산술 평균, 다음 특수한 경우에 대한 알려진 휴리스틱을 고려:
- NHCB의 가장 낮은 버킷은 상한이 양수이면 하한이 0인 것으로 간주돼요
- 표준 지수 히스토그램의 제로 버킷 하한은 히스토그램에 채워진 음수 버킷이 없으면 0으로 간주돼요
- 표준 지수 히스토그램의 제로 버킷 상한은 히스토그램에 채워진 양수 버킷이 없으면 0으로 간주돼요
- 표준 지수 히스토그램의 다른 모든 버킷에 대한 기하 평균
추가로 부분적으로만 제거된(위에서 설명한 보간을 사용하는) 버킷에 대한 (산술 또는 기하) 평균 계산에 사용되는 경계는 다음 방식으로 수정돼요: (/<에 대한) 관련 상한은 2번째 피연산자로 제공된 컷오프 임계값과 같은 것으로 간주돼요.
이 관측치 합 추정은 (컷오프 임계값이 버킷 경계와 일치하는 경우에도) 명백히 잘못된 결과를 낼 수 있는 지점까지 부정확하다는 점에 유의하세요. 예를 들어 양수 관측치를 제거한 후 추정된 관측치 합이 원래 히스토그램의 관측치 합보다 클 수 있어요. 추정 알고리즘은 그런 경우를 고려하도록 정제될 수 있지만, 추론하기 쉽도록 의도적으로 단순하게 유지돼요. 일반적으로 트림 연산에서 나온 히스토그램은 어쨌든 관측치 합이 무관한 분위수 추정이나 필터링에 사용되도록 설계됐어요. 트림 연산자는 원하는 결과가 관측치 비율이 아니라 관측치 수인 경우 histogram_fraction 함수(아래 설명)의 실행 가능한 대안이기도 해요. 예를 들어 histogram_count(h >/ 0.2)는 0.2초보다 오래 걸린 관측치의 (추정) 개수를 세어요.
집계 연산자 (Aggregation operators)
네이티브 히스토그램과 함께 동작하는 집계 연산자는 sum과 avg뿐이에요.
다음 집계 연산자는 네이티브 히스토그램과 함께 동작하지 않아요. 입력 벡터의 히스토그램은 그냥 무시돼요:
min,max,bottomk,topkquantile(물론 결과는 히스토그램에 대해 계산할 수 없지만 아래histogram_quantile함수를 참조하세요)stddev,stdvargroup(이 집계의 결과는 샘플 값에 의존하지 않음)count(이 집계의 결과는 샘플 값에 의존하지 않음)count_values(GoFloatHistogram.String메서드가 만든 텍스트 표현이 히스토그램의 값으로 사용됨)limitk(샘플된 요소가 변경 없이 반환됨)limit_ratio(샘플된 요소가 변경 없이 반환됨)
sum 집계 연산자는 위에서 + 연산자에 대해 설명한 것과 같은 방식으로 집계할 히스토그램을 합산해 네이티브 히스토그램과 함께 동작해요. avg 집계 연산자는 같은 방식으로 동작하지만 합을 집계된 히스토그램 수로 나눠요(위에서 / 연산자에 대해 설명한 것과 같은 방식).
sum과 avg 둘 다 float 샘플과 히스토그램 샘플의 집계가 필요한 요소를 출력 벡터에서 제거해요. 그런 제거는 warn 레벨 어노테이션으로 표시돼요.
sum과 avg 둘 다 gauge 히스토그램에만 적용해야 해요. PromQL은 counter 히스토그램(그리고 둘의 혼합조차)을 집계하는 것을 허용하지만, 의미 있게 하려면 주의가 필요해요. gauge 대 counter 맛과 결과 카운터 리셋 힌트에 대한 함의는 위 + 연산자의 것에서 파생돼요:
- 모든 집계된 히스토그램이 같은 카운터 리셋 힌트를 공유하면 결과가 그 같은 카운터 리셋 힌트를 유지해요
- 집계된 히스토그램 중 gauge 히스토그램이 하나라도 있으면 결과는 gauge 히스토그램이에요
- 다른 모든 경우 결과의 카운터 리셋 힌트는
UnknownCounterReset으로 설정돼요 - 어떤 경우든 집계된 히스토그램 중 직접 모순되는 카운터 리셋 힌트(즉
CounterReset과NotCounterReset)가 있으면 warn 레벨 어노테이션이 촉발돼요
다른 모든 집계 연산자는 네이티브 히스토그램과 동작하지 않아요. 입력 벡터의 히스토그램은 그냥 무시되고 각 무시된 히스토그램에 대해 info 레벨 어노테이션이 추가돼요.
함수 (Functions)
다음 함수들은 일반 float 연산을 일치하는 버킷(제로 버킷 포함)과 관측치 sum·count에 개별적으로 적용해 새 네이티브 히스토그램을 만들어 네이티브 히스토그램의 범위 벡터에 동작해요:
delta()(gauge 히스토그램용)increase()(counter 히스토그램용)rate()(counter 히스토그램용)idelta()(gauge 히스토그램용)irate()(counter 히스토그램용)
이 함수들은 위에서 언급한 대로 gauge 히스토그램이나 counter 히스토그램에 적용해야(SHOULD) 해요. 다만 그것들은 모두 두 맛 모두에 동작하지만, 범위 벡터에 부적절한 맛의 히스토그램이 하나라도 포함되면 결과에 warn 레벨 어노테이션이 추가돼요.
delta(), increase(), rate()는 범위 내에 float 샘플과 히스토그램 샘플이 섞인 시리즈에 대해 결과를 반환하지 않아요. idelta()와 irate()는 범위 내의 마지막 두 샘플이 float 샘플과 히스토그램 샘플이 섞인 시리즈에 대해 결과를 반환하지 않아요. 어느 경우든 이 이유로 누락된 각 출력 요소에 대해 warn 레벨 어노테이션이 추가돼요.
이 함수들은 모두 결과로 gauge 히스토그램을 반환해요.
평소처럼 이 함수들은 스키마를 가능한 한 공통 스키마로 변환해 서로 다른 스키마를 조정하려고 해요. 다만 카운터에 적용되는 함수(increase(), rate(), irate())는 1번째와 2번째 샘플 사이에 카운터 리셋이 있으면 1번째 샘플에 대해 이 변환을 수행하지 않아요. 이 경우 1번째 샘플은 계산에 포함되지 않으므로 1번째 샘플과 다른 샘플 사이의 비호환 버킷 레이아웃은 조용히 무시돼요.
0 아래로의 외삽을 막기 위해 float 카운터와 같은 휴리스틱이 적용되지만 관측치 count에만 기반해요. 따라서 개별 버킷은 어떤 경우에는 여전히 0 아래로 외삽될 수 있어요. 대안은 count도 버킷도 0 아래로 외삽되지 않는 가장 작은 외삽을 찾는 것이었을 수 있어요. 하지만 이것은 반드시 더 나은 휴리스틱으로 이어지지는 않으면서 상당한 복잡성 비용을 부과해요. 범위의 첫 번째 샘플이 created-at 타임스탬프에서 파생된 합성 제로 샘플인 흔하고 중요한 경우에는 외삽이 제한되는 시점이 합성 샘플의 타임스탬프이므로 그 시점에 count와 모든 버킷이 0이기 때문에 제한된 외삽이 실제로 완벽하게 정밀하게 동작해요. 클래식 히스토그램은 휴리스틱을 각 버킷과 count·sum에 (그것들이 모두 별도 시리즈이므로) 독립적으로 적용한다는 점에 유의하세요. 이것은 불일치로 이어지는 것으로 알려져 있어요. NHCB는 이 문제를 재현하지 않고 다른 네이티브 히스토그램과 같은 방식으로 동작하며, 이는 클래식 히스토그램과 동등한 NHCB를 비교할 때 rate()와 increase()의 결과가 약간 다를 수 있음을 의미해요.
avg_over_time()와 sum_over_time()는 각 집계 연산자에 대응하는 방식으로 네이티브 히스토그램과 동작해요. 특히 시리즈가 범위 내에 float 샘플과 히스토그램 샘플을 섞어 포함하면 해당 결과가 출력 벡터에서 완전히 제거돼요. 그런 제거는 warn 레벨 어노테이션으로 표시돼요.
changes()와 resets() 함수는 float 샘플과 같은 방식으로 네이티브 히스토그램 샘플과 동작해요. 그것들은 심지어 같은 시리즈 내 float 샘플과 히스토그램 샘플의 혼합에서도 동작해요. 이 경우 float 샘플에서 히스토그램 샘플로의 변경과 그 반대는 changes()에서는 변경으로, resets()에서는 리셋으로 계산돼요. counter 히스토그램에서 gauge 히스토그램으로의 맛 변경과 그 반대는 changes()의 변경으로 계산되지 않아요. resets()는 counter float와 counter 히스토그램에만 적용해야(SHOULD) 하지만, 이 경우 명시적 카운터 리셋 감지를 적용해 gauge 히스토그램에서도 여전히 동작해요. 게다가 counter 히스토그램에서 gauge 히스토그램으로의 변경과 그 반대는 리셋으로 계산돼요.
histogram_quantile() 함수는 특정 "마법" 레이블, 즉 클래식 히스토그램이 사용하는 le 레이블을 특별히 취급하는 유일한 함수이므로 아주 특별한 역할을 해요. histogram_quantile()은 le 레이블의 특별한 역할 없이도 유사한 방식으로 네이티브 히스토그램에서도 동작해요. 이 함수는 알려진 방식으로 float 샘플을 계속 취급하면서 네이티브 히스토그램 샘플에는 새 "네이티브" 방식을 사용해요.
클래식 히스토그램의 일반적인 쿼리 예시(rate와 집계 포함):
histogram_quantile(0.9, sum by (job, le) (rate(http_request_duration_seconds_bucket[10m])))
이것이 네이티브 히스토그램에 대한 해당 쿼리예요:
histogram_quantile(0.9, sum by (job) (rate(http_request_duration_seconds[10m])))
클래식 히스토그램과 마찬가지로 히스토그램의 최대·최소 관측치 추정을 histogram_quantile의 첫 번째 파라미터로 각각 1과 0을 사용해 수행할 수 있어요. 다만 표준 스키마를 가진 네이티브 히스토그램은 훨씬 더 유용한 결과를 가능하게 해요. 네이티브 히스토그램의 보통 더 높은 해상도 때문만이 아니라 표준 스키마를 가진 네이티브 히스토그램이 float64 숫자의 전체 범위에 걸쳐 같은 해상도를 유지하기 때문이에요. 클래식 히스토그램의 경우 최대 관측치가 +Inf 버킷에 있을 가능성이 높아서 추정이 단순히 +Inf 버킷 앞 마지막 버킷의 상한을 반환해요. 유사하게 최소 관측치가 가장 낮은 버킷에 있을 때가 많아요.
histogram_quantile은 값 NaN의 관측치((발생해서는 안 되며, 위 참조)를 효과적으로 +Inf 관측치로 취급해요. 이것은 NaN이 histogram_quantile이 반환하는 어떤 값보다 결코 작지 않다는 논리를 따릅니다. 결과가 기존 버킷에 속하는 한 NaN 관측치를 +Inf로 관측한 것처럼 계산한 결과를 반환하고, 결과가 NaN으로 인해 편향됨을 사용자에게 알리기 위해 info 레벨 어노테이션도 발행해요. 이는 클래식 히스토그램이 보통 NaN 관측치를 취급하는 방식(대부분의 구현에서 +Inf 버킷에 끝남)과 일관돼요. 결과가 모든 기존 버킷 위에 속하면 NaN을 반환해요. 자세한 이유는 아래 histogram_fraction을 참조하세요. 직관적으로 이 경우는 NaN이 어떤 숫자와도 비교할 수 없으므로 분위수의 모든 관측치보다 큰 숫자가 없다는 뜻이에요. 이 경우에 특정한 info 레벨 어노테이션도 반환해요.
다음 함수는 네이티브 히스토그램을 위해 특별히 도입됐어요:
histogram_avg()histogram_count()histogram_fraction()histogram_sum()histogram_stddev()histogram_stdvar()
이 함수들은 모두 입력으로 float 샘플을 조용히 무시해요. 각 함수는 float 샘플 벡터를 반환해요.
histogram_count()와 histogram_sum()은 네이티브 히스토그램에 포함된 관측치 수 또는 관측치 합을 각각 반환해요. 일반 함수이므로 그 결과를 범위 셀렉터에 사용할 수 없어요. 서브쿼리를 사용하는 대신 관측치 count나 sum의 rate를 계산하는 권장 방법은 먼저 히스토그램을 rate한 다음 결과에 histogram_count()나 histogram_sum()을 적용하는 것이에요. 예를 들어 다음 쿼리는 네이티브 히스토그램에서 관측치의 rate(이 경우 "초당 요청 수"에 해당)를 계산해요:
histogram_count(rate(http_request_duration_seconds[10m]))
histogram_sum()의 결과에 서브쿼리를 사용할 때는 네이티브 히스토그램의 특별한 카운터 리셋 감지가 적용되지 않는다는 점에 유의하세요. 즉 음수 관측치가 거짓 카운터 리셋을 일으킬 수 있어요.
histogram_avg()는 네이티브 히스토그램의 관측값 산술 평균을 반환해요. (이것은 여러 네이티브 히스토그램에 avg 집계 연산자를 적용하는 것과 현저히 다릅니다. 후자는 평균화된 히스토그램을 반환해요.)
유사하게 histogram_stddev()와 histogram_stdvar()은 네이티브 히스토그램의 관측치의 추정 표준 편차 또는 표준 분산을 각각 반환해요. 이 추정에서 버킷의 모든 관측치가 버킷 경계 평균의 값을 가진다고 가정돼요. 제로 버킷과 사용자 정의 경계를 가진 버킷에는 산술 평균이 사용돼요. 표준 지수 버킷에는 기하 평균이 사용돼요.
histogram_fraction(lower, upper, histogram)은 histogram에서 제공된 경계 스칼라 값 lower와 upper 사이의 관측치 추정 비율을 반환해요. 추정의 오차는 기본 네이티브 히스토그램의 해상도와 제공된 경계가 히스토그램의 버킷 경계와 얼마나 밀접하게 정렬되는지에 따라 달라져요. +Inf와 -Inf는 유효한 경계 값이고 특정 값 위 또는 아래의 모든 관측치의 비율을 추정하는 데 유용해요. 하지만 값 NaN의 관측치는 지정된 경계(심지어 +Inf와 -Inf) 밖에 있는 것으로 항상 간주돼요. 제공된 경계가 포함인지 제외인지는 제공된 경계가 기본 네이티브 히스토그램의 버킷 경계와 정확히 정렬된 경우에만 관련이 있어요. 이 경우 동작은 히스토그램의 스키마의 정확한 정의에 따라 달라져요.
q = histogram_fraction(-Inf, x, histogram)의 값은 x 이하(또는 같음)인 관측치의 비율이 q임을 의미해요. 반면 y = histogram_quantile(q, histogram)은 관측치의 q 비율이 y 이하(또는 같음)임을 의미해요. histogram_quantile이 y의 근사 최소값을 계산하므로 y <= x를 따르고 그 역도 유사하게 성립해요.
네이티브 히스토그램과 동작하는 기타 함수
다음 함수들은 float와 히스토그램 샘플에 같은 방식으로 동작해요:
absent()absent_over_time()count_over_time()info()label_join()label_replace()last_over_time()present_over_time()sort_by_label()sort_by_label_desc()timestamp()
이 섹션에서 언급되지 않은 모든 나머지 함수는 네이티브 히스토그램과 동작하지 않아요. 입력 벡터의 히스토그램 요소는 조용히 무시돼요. deriv(), double_exponential_smoothing(), predict_linear(), 그리고 앞서 언급되지 않은 모든 _over_time() 함수에 대해 네이티브 히스토그램 샘플이 입력 범위 벡터에서 제거돼요. 어떤 시리즈가 범위 내에 float 샘플과 히스토그램 샘플을 섞어 포함하면 히스토그램의 제거가 info 레벨 어노테이션으로 표시돼요.
기록 규칙 (Recording rules)
기록 규칙은 네이티브 히스토그램 값이 될 수(MAY) 있어요. 그것들은 일반 수집처럼 TSDB에 다시 저장돼요. 히스토그램이 gauge 히스토그램인지 counter 히스토그램인지도 포함해요. 후자의 경우 카운터 리셋 힌트로 명시적으로 표시된 카운터 리셋도 저장되지만, 그 외에는 수집 중 새 카운터 리셋 감지가 시작돼요.
TSDB 구현은 기록 규칙이 만든 float 히스토그램을, 이 변환이 원래 히스토그램의 모든 float 값을 정밀하게 표현한다면 정수 히스토그램으로 변환할 수(MAY) 있어요.
경고 규칙 (Alerting rules)
경고는 네이티브 히스토그램에서 평소처럼 동작해요. 다만 경고의 출력 값으로 네이티브 히스토그램을 피하는 것이 RECOMMENDED(권장)돼요. 템플릿에서 네이티브 히스토그램 샘플이 사용되면(Go FloatHistogram.String 메서드가 만든) 간단한 텍스트 형식으로 렌더링되는데, 이는 사람이 읽기 어려워요.
테스트 프레임워크
PromQL 테스트 프레임워크는 promtool을 통한 PromQL 단위 테스트와 규칙 단위 테스트 둘 다 네이티브 히스토그램을 포함할 수 있도록 확장됐어요. 히스토그램 샘플 표기법은 복잡하며 규칙 단위 테스트 문서에서 설명돼요.
단위 테스트 프레임워크에는 load_with_nhcb라는 대체 load 명령이 있어요. 클래식 히스토그램을 NHCB로 변환하고 클래식 히스토그램의 float 시리즈와 변환으로 만들어진 NHCB 시리즈 둘 다를 로드해요.
네이티브 히스토그램에만 해당하는 것은 아니지만 그 맥락에서 매우 유용한 것은 단위 테스트 프레임워크의 expect 키워드로, info 및 warn 레벨 어노테이션에 대한 기대를 정의할 수 있어요.
최적화 (Optimizations)
평소처럼 PromQL 구현은 동작이 같게 유지되는 한 마음대로 최적화를 적용할 수(MAY) 있어요. 네이티브 히스토그램 디코딩은 잠재적으로 많은 버킷으로 꽤 비쌀 수 있어요. 유사하게 PromQL 엔진 내에서 히스토그램 샘플을 deep-copy하는 것은 단순 float 샘플을 복사하는 것보다 훨씬 비싸요. 이는 항상 모든 것을 디코딩하고 항상 모든 것을 복사하는 순진한 접근과 비교해 큰 최적화 가능성의 여지를 만들어요.
프로메테우스는 현재 불필요한 복사를 피하려고 하고(TODO: 하지만 적절한 CoW 같은 접근은 여전히 구현해야 하며, 훨씬 더 깔끔하고 버그가 적을 것) 관측치 sum과 count만 필요한 특수한 경우에는 버킷 디코딩을 건너뜁니다.
Prometheus 쿼리 API
쿼리 API 문서는 네이티브 히스토그램 지원을 포함해요. 이 섹션은 네이티브 히스토그램과 관련된 부분에 초점을 맞추고 API 문서의 일부가 아닌 약간의 맥락을 제공해요.
인스턴트 및 범위 쿼리
인스턴트(query 엔드포인트)와 범위(query_range 엔드포인트) 쿼리의 JSON 응답에서 네이티브 히스토그램을 반환하려면 vector와 matrix 결과 타입 둘 다 새 키에 의한 확장이 필요해요.
vector 결과 타입은 기존 value 키와 같은 레벨에 새 histogram 키를 얻어요. 이 두 키는 상호 배타적이에요. 즉 vector의 각 요소는 value 키(float 결과) 또는 histogram 키(히스토그램 결과)를 가져요. histogram 키의 값은 value 키의 값과 유사하게 구조화되고(2-요소 배열), 차이는 float 샘플 값을 나타내는 문자열이 아래에서 설명하는 특정 히스토그램 객체로 대체된다는 것이에요.
matrix 결과 타입은 기존 values 키와 같은 레벨에 새 histograms 키를 얻어요. 이 키들은 상호 배타적이지 않아요. 시리즈는 float 값과 히스토그램 값 둘 다를 포함할 수 있지만, 주어진 타임스탬프에는 float 또는 히스토그램 중 하나의 샘플만 있어야 해요. histograms 키의 값은 values 키의 값과 유사하게 구조화되고(n개의 2-요소 배열), 차이는 float 샘플 값을 나타내는 문자열이 아래에서 설명하는 특정 히스토그램 객체로 대체된다는 것이에요.
키의 더 나은 이름은 float 값과 히스토그램 값 모두 값이므로 float/histogram과 floats/histograms였을 것이라는 점에 유의하세요. 현재 이름은 역사적 이유가 있어요. (과거에는 오직 하나의 값 타입, 즉 float만 있었으므로 키를 그냥 value와 values라고 부르는 것이 자명한 선택이었어요. 여기의 의도는 네이티브 히스토그램을 모르는 기존 소비자를 깨뜨리지 않는 것이에요.)
위에서 언급한 히스토그램 객체는 다음 구조를 가져요:
{
"count": "<int>",
"sum": "<float>",
"buckets": [ [ <lower_bound>, "<upper_bound>", "<count>", "<bound_type>" ], ... ]
}
count와 sum은 같은 이름의 히스토그램 필드에 직접 대응해요. 각 버킷은 경계와 count, 제로 버킷을 포함해 명시적으로 표현돼요. 따라서 스팬과 스키마는 응답의 일부가 아니고 히스토그램 객체의 구조는 사용된 스키마에 의존하지 않아요.
<bound_type> 플레이스홀더는 0에서 3 사이의 정수로 의미는 다음과 같아요:
- 0: "open left" (왼쪽 경계 제외, 오른쪽 경계 포함)
- 1: "open right" (왼쪽 경계 포함, 오른쪽 경계 제외)
- 2: "open both" (양쪽 경계 제외)
- 3: "closed both" (양쪽 경계 포함)
표준 스키마의 경우 양수 버킷은 "open left", 음수 버킷은 "open right", 제로 버킷(음수 왼쪽 경계와 양수 오른쪽 경계)은 "closed both"예요. NHCB의 경우 모든 버킷이 "open left"예요(클래식 히스토그램의 동작을 미러링). 미래 스키마는 다른 경계 규칙을 활용할 수 있어요.
메타데이터 (Metadata)
series 엔드포인트의 경우 네이티브 히스토그램을 포함하는 시리즈가 float만 포함하는 기존 시리즈와 같은 방식으로 포함돼요. 이 엔드포인트는 어떤 샘플 타입이 포함되는지에 대한 어떤 정보도 제공하지 않아요(그리고 실제로 어떤 시리즈든 어느 타입이나 둘 다를 포함할 수 있어요). 특히 타깃이 request_duration_seconds라는 이름으로 노출한 히스토그램은 네이티브 히스토그램으로 노출·수집되면 request_duration_seconds라는 시리즈로 이어지지만, 클래식 히스토그램으로 노출·수집되면 request_duration_seconds_sum, request_duration_seconds_count, request_duration_seconds_bucket이라는 시리즈 세트로 이어져요. 히스토그램이 네이티브 히스토그램과 클래식 히스토그램 둘 다로 수집되면, 위의 모든 시리즈 이름이 series 엔드포인트에 의해 반환돼요.
타깃과 메트릭 메타데이터(targets/metadata와 metadata 엔드포인트)는 타깃이 노출한 원래 이름에 작용하므로 약간 다르게 동작해요. 이는 request_duration_seconds라는 클래식 히스토그램이 이 메타데이터 엔드포인트들에 의해 request_duration_seconds로만(그리고 request_duration_seconds_sum, request_duration_seconds_count, request_duration_seconds_bucket로는 아님) 표현됨을 의미해요. 네이티브 히스토그램 request_duration_seconds도 이 이름으로 표현돼요. request_duration_seconds가 클래식과 네이티브 히스토그램 둘 다로 수집되는 경우에도 반환된 메타데이터가 실제로 같으므로(가장 주목할 만하게 반환된 type이 histogram일 것) 충돌이 없어요. 즉 현재 메타데이터 엔드포인트만으로 네이티브와 클래식 히스토그램을 구별하는 방법이 없어요. series 엔드포인트를 통한 추가 조회가 필요해요. 이를 바꿀 계획은 없어요. 기존 메타데이터 엔드포인트가 어쨌든 심하게 제한되어 있기 때문이에요(역사적 정보 없음, 규칙이 만든 메트릭의 메타데이터 없음, 서로 다른 타깃 간 충돌하는 메타데이터를 처리하는 제한된 능력). 하지만 프로메테우스에서 메타데이터 처리를 일반적으로 개선할 계획은 있어요. 그 노력은 네이티브 히스토그램을 적절히 지원하는 방법도 고려할 거예요. (TODO: 진전에 따라 업데이트)
Prometheus UI
이 섹션은 프로메테우스 자체 UI가 히스토그램을 렌더링하는 방법을 설명해요. 제3자 그래프 프론트엔드의 지침으로 사용될 수(MAY) 있어요.
Table 뷰에서 히스토그램 데이터 포인트는 그래픽 막대 그래프와 함께 모든 버킷의 하한·상한, 관측치 count·sum의 텍스트 표현으로 렌더링돼요. 막대 그래프의 각 막대는 버킷을 나타내요. x 축의 각 막대 위치는 해당 버킷의 하한과 상한으로 결정돼요. 각 막대의 면적은 해당 버킷의 인구에 비례해요(일반적으로 히스토그램 렌더링의 핵심 원칙).
그래픽 히스토그램은 지수와 선형 x 축 사이에서 선택할 수 있어요. 전자가 기본값이에요. 표준 스키마에 잘 맞아요. (TODO: 비지수 스키마에 대해 기본값으로 선형을 고려하세요.) 편리하게 지수 스키마의 모든 일반 버킷은 지수 x 축에서 같은 폭을 가져요. 이는 y 축이 막대의 면적(높이가 아닌)이 버킷 인구를 대표한다는 위 원칙을 위반하지 않고 실제 버킷 인구를 표시할 수 있음을 의미해요. 제로 버킷은 예외예요. 기술적으로 무한 폭을 가져요. 프로메테우스는 그것을 단순히 일반 지수 버킷과 같은 폭으로 렌더링해요(즉 x 축이 0 포인트 주변에서 엄격히 지수적이지 않다는 뜻). (TODO: 비지수 스키마의 렌더링 방법.)
선형 x 축에서는 버킷이 일반적으로 가변 폭을 가져요. 따라서 y 축은 버킷 인구를 폭으로 나눈 값을 표시해요. 프로메테우스 UI는 어쨌든 사람이 해석하기 어려우므로 y 축에 값을 렌더링하지 않아요. 인구는 여전히 텍스트 표현에서 검사할 수 있어요.
Graph 뷰에서 프로메테우스는 히트맵을 표시해요(TODO: 아직 아님, 아래 참조). 이것은 시간에 따른 히스토그램 시리즈로 볼 수 있고, 90도 회전되어 버킷 인구를 막대의 높이보다 색으로 인코딩해요. counter형 히스토그램을 히트맵으로 렌더링하는 일반적인 쿼리는 rate 쿼리예요. 히트맵은 사람들이 시간에 따라 변하는 분포의 특성을 쉽게 발견할 수 있게 해주는 매우 강력한 표현이에요.
TODO: 히트맵은 아직 구현되지 않았어요. 대신 UI는 관측치 합만 기존 그래프로 그려요. 추적 이슈 참조. 같은 이슈가 Table 뷰에서 범위 벡터 렌더링을 어떻게 다룰지도 논의해요.
템플릿 확장 (Template expansion)
네이티브 히스토그램은 템플릿 확장에서 동작해요. 열린·닫힌 구간의 수학적 표기법에서 영감을 받은 텍스트 표현으로 렌더링돼요. (이것은 Go의 FloatHistogram.String 메서드로 생성돼요.) 네이티브 히스토그램은 많은 버킷을 가질 수 있고 버킷 경계는 많은 소수점 자리 경계를 가지는 경향이 있으므로 그 표현이 반드시 읽기 쉬운 것은 아니에요. 템플릿 확장에서 네이티브 히스토그램을 신중하게 사용하세요.
float 히스토그램의 텍스트 표현 예시:
{count:3493.3, sum:2.349209324e+06, [-22.62741699796952,-16):1000, [-16,-11.31370849898476):123400, [-4,-2.82842712474619):3, [-2.82842712474619,-2):3.1, [-0.01,0.01]:5.5, (0.35355339059327373,0.5]:1, (1,1.414213562373095]:3.3, (1.414213562373095,2]:4.2, (2,2.82842712474619]:0.1}
Remote-write & remote-read
remote-write & remote-read용 protobuf 사양이 네이티브 히스토그램을 위해 확장됐어요. 네이티브 히스토그램을 처리할 수 없는 수신자는 새로 추가된 필드를 그냥 무시해요. 그럼에도 프로메테우스는 remote-write로 네이티브 히스토그램을 보내도록 구성해야 해요(send_native_histograms remote-write 구성 설정을 true로 설정).
remote-write v2에서 네이티브 히스토그램은 안정 기능이에요.
보내거나 받는 동안 클래식 히스토그램을 NHCB로 변환하는 것이 유혹적으로 보일 수 있어요. 하지만 이것은 클래식 히스토그램이 remote-write로 전송될 때 겪는 알려진 일관성 문제를 극복하지 못해요. 대신 클래식 히스토그램은 스크레이핑 동안 NHCB로 변환해야(SHOULD) 해요. 유사하게 명시적 OTel 히스토그램은 OTLP 수집 동안 이미 NHCB로 변환해야(SHOULD) 해요.
TODO: remote-write의 남은 가능한 문제는 원래 같은 네이티브 히스토그램에 대해 수집된 여러 exemplar가 서로 다른 remote-write 요청으로 보내지면 어떻게 할지예요.
연합 (Federation)
네이티브 히스토그램의 연합은, 연합 스크레이프가 protobuf 형식을 사용하면 예상대로 동작해요. OpenMetrics 텍스트 형식을 통한 연합은 네이티브 히스토그램이 그 형식에서 지원되면 적어도 원칙적으로 가능해지겠지만, 효율성 때문에 어쨌든 protobuf를 통한 연합이 선호돼요.
TODO: OM이 NH를 지원하면 업데이트.
NHCB는 연합 엔드포인트를 통해 노출될 때 클래식 float 히스토그램으로 렌더링돼요. 스크레이퍼는 그것들을 NHCB로 다시 변환하거나 클래식 히스토그램으로 수집할 수 있어요. 후자는 이름 충돌로 이어질 수 있어요. OpenMetrics v1은 클래식 float 히스토그램을 지원하지 않는다는 점에 유의하세요. 다행히 프로메테우스 연합은 어쨌든 OpenMetrics v1을 사용하지 않고 protobuf 형식이나 클래식 텍스트 형식을 사용해요.
OTLP
프로메테우스에 내장된 OTLP 수신자는 수신되는 OTel 지수 히스토그램을 위 설명된 호환성을 활용해 프로메테우스 네이티브 히스토그램으로 변환해요. 8보다 큰 스키마(OTel 용어로 "scale")를 사용하는 히스토그램의 해상도는 스키마 8과 일치하도록 줄어들어요. (-4보다 작은 스키마가 사용되는 가능성이 낮은 경우 수집이 실패할 거예요.)
명시적 OTel 히스토그램은 프로메테우스의 클래식 히스토그램과 동등해요. 프로메테우스는 따라서 그것들을 기본적으로 클래식 히스토그램으로 변환하지만, 선택적으로 NHCB로 직접 변환을 제공해요.
Pushgateway
네이티브 히스토그램 지원이 Pushgateway에 점진적으로 추가됐어요. v1.9에서 완전한 지원에 도달했어요. Pushgateway는 항상 클래식 protobuf 형식을 내부 데이터 모델로 기반으로 해서 필요한 변경(대부분 UI 문제)을 쉽게 만들었어요. 결합된 히스토그램(클래식과 네이티브 버킷)을 푸시할 수 있고 /metrics 엔드포인트를 통해 그렇게 노출돼요. (다만 푸시된 메트릭을 JSON으로 쿼리하는 데 사용할 수 있는 쿼리 API는 한 종류의 버킷만 반환할 수 있고, 존재하면 네이티브 버킷을 선호할 거예요.)
promtool
이 섹션은 네이티브 히스토그램을 지원하기 위해 추가되거나 변경된 promtool 명령을 설명해요. 명시적으로 언급되지 않은 명령은 네이티브 히스토그램과 직접 상호작용하지 않으며 변경이 필요 없어요.
promtool query ... 명령은 네이티브 히스토그램과 동작해요. 출력 형식에 대해서는 쿼리 API 문서를 참조하세요. 쿼리 API가 반환하는 클래식과 네이티브 히스토그램 사용 패턴을 분석하기 위해 promtool query analyze라는 새 명령이 특별히 추가됐어요.
promtool test rules를 통한 규칙 단위 테스트는 위 설명된 형식을 사용해 네이티브 히스토그램과 동작해요.
promtool tsdb analyze와 promtool tsdb list는 네이티브 히스토그램과 정상적으로 동작해요. 전자의 --extended 출력에는 히스토그램 청크에 대한 특정 섹션이 있어요.
promtool tsdb dump는 사용자 정의 네이티브 히스토그램의 일반 텍스트 표현(Go 메서드 FloatHistogram.String이 만든)을 사용해요.
promtool tsdb create-blocks-from rules는 네이티브 히스토그램을 발행하는 규칙과 동작해요.
promtool promql ... 명령은 네이티브 히스토그램을 위해 추가된 모든 PromQL 기능을 지원해요.
promtool tsdb bench write는 원칙적으로 네이티브 히스토그램을 포함할 수 있지만, 현재 그런 지원은 계획되지 않아요.
다음 명령들은 OpenMetrics 텍스트 형식에 의존하므로 OpenMetrics에 네이티브 히스토그램 지원이 있는 한 네이티브 히스토그램을 지원할 수 없어요:
promtool check metricspromtool push metricspromtool tsdb dump-openmetricspromtool tsdb create-blocks-from openmetrics
TODO: 진전에 따라 업데이트. 추적 이슈 참조.
prom2json
prom2json은 프로메테우스 /metrics 엔드포인트를 스크레이프하고 메트릭을 사용자 지정 JSON 형식으로 변환해 stdout으로 덤프하는 작은 도구예요. JSON을 처리하는 도구(예: jq)로 추가 처리하기에 편리해요.
prom2json v1.4가 네이티브 히스토그램 지원을 추가했어요. exposition의 히스토그램이 버킷 스팬을 하나 이상 포함하면 prom2json은 기존 클래식 버킷을 네이티브 히스토그램의 버킷으로 프로메테우스 쿼리 API에서 영감을 받은 형식으로 JSON 출력에서 대체해요.
마이그레이션 고려사항
클래식에서 네이티브 히스토그램으로 마이그레이션할 때 고려해야 할 문제의 원천이 세 가지 있어요:
- 네이티브 히스토그램 쿼리는 클래식 히스토그램 쿼리와 다르게 동작해요. 대부분의 경우 변경은 최소이고 간단하지만, 까다로운 극단적인 경우가 있어 신뢰할 수 있는 자동 변환을 어렵게 만들어요
- 클래식과 네이티브 히스토그램은 서로 집계될 수 없어요. 특정 시점에 클래식에서 네이티브 히스토그램으로의 변경은 전환점을 가로지르는 대시보드를 만들기 어렵게 하고, 전환점을 포함하는 범위 벡터는 필연적으로 불완전해져요(즉 클래식 히스토그램을 선택하는 범위 벡터는 범위의 앞부분 데이터 포인트만 포함하고, 네이티브 히스토그램을 선택하는 범위 벡터는 범위의 뒷부분 데이터 포인트만 포함해요)
- 클래식 히스토그램은 관심 지점에 정확히 버킷 경계를 두도록 맞춰질 수 있어요. 표준 스키마를 가진 네이티브 히스토그램은 높은 해상도를 가질 수 있지만 임의 값에 버킷 경계를 둘 수는 없어요. 그런 경우 네이티브 히스토그램의 사용자 경험이 실제로 더 나빠질 수 있어요
(3)을 해결하려면 당연히 해당 클래식 히스토그램을 마이그레이션하지 않고 그대로 두는 것도 가능해요. 또 다른 옵션은 계측을 같게 유지하되 수집 시 클래식 히스토그램을 NHCB로 변환하는 것이에요. 이것은 네이티브 히스토그램의 향상된 저장 성능을 활용하지만, 네이티브 히스토그램으로의 완전한 마이그레이션과 같은 방식으로 (1)과 (2)를 해결해야 해요(다음 문단 참조).
(1)과 (2)를 해결하는 보수적인 방법은 긴 전환 기간을 허용하는 것이며, 이는 잠시 동안 클래식과 네이티브 히스토그램을 병렬로 수집·저장하는 비용이 들게 돼요.
첫 번째 단계는 클래식과 네이티브 히스토그램을 병렬로 노출하도록 계측을 업데이트하는 것이에요. (이 단계는 계측에서 클래식 히스토그램을 유지하고 단순히 스크레이핑 중 NHCB로 변환할 계획이라면 건너뛸 수 있어요.)
그런 다음 프로메테우스가 클래식과 네이티브 히스토그램 둘 다를 스크레이프하도록 구성하세요. 위 클래식과 네이티브 히스토그램 동시 스크레이프 섹션을 참조하세요. (필요하면 클래식 히스토그램의 NHCB 변환도 활성화하세요.)
클래식 히스토그램을 포함하는 기존 쿼리는 계속 동작하지만, 이제부터 사용자는 네이티브 히스토그램으로 작업을 시작하고 대시보드, 경고, 기록 규칙 등의 쿼리를 바꾸기 시작할 수 있어요. 위에서 이미 언급했듯 histogram_quantile(0.9, rate(rpc_duration_seconds[1d])) 같은 더 긴 범위 벡터를 가진 쿼리에 주의하는 것이 중요해요. 이 쿼리는 지난 하루의 90번째 백분위수 지연 시간을 계산해요. 하지만 네이티브 히스토그램이 적어도 하루 동안 수집되지 않았다면 쿼리는 그 더 짧은 기간만 다룰 거예요. 따라서 이 쿼리는 네이티브 히스토그램이 적어도 1d 동안 수집된 후에만 사용해야 해요. 지난 한 달 동안의 일일 90번째 백분위수 지연 시간을 표시하는 대시보드의 경우, 올바른 순간에 클래식에서 네이티브 히스토그램으로 정확히 전환하는 쿼리를 만드는 것이 유혹적이에요. 원칙적으로 가능하긴 하지만 까다로워요. 가능하다면 클래식과 네이티브 히스토그램을 병렬로 수집하는 전환 기간을 꽤 길게 두어 까다로운 전환 구현 필요를 최소화할 수 있어요. 예를 들어 클래식과 네이티브 히스토그램을 한 달 동안 병렬로 수집했다면, 한 달보다 더 뒤를 보지 않는 모든 대시보드는 올바른 전환에 대한 고려 없이 클래식 히스토그램 쿼리에서 네이티브 히스토그램 쿼리로 그냥 전환할 수 있어요.
모든 쿼리가 올바르게 마이그레이션됐다고 확신하면 프로메테우스가 네이티브 히스토그램만 스크레이프하도록 구성하세요(이것이 "일반" 설정). (스크레이프 구성의 relabel 규칙으로 클래식 히스토그램을 점진적으로 제거하는 것도 가능해요.) 여전히 모든 것이 동작하면 계측에서 클래식 히스토그램을 제거할 때예요.
Grafana Mimir 문서에는 이 섹션에서 설명한 것과 같은 철학을 따르는 상세 마이그레이션 가이드가 있어요.
더 알아보기 (Learn more)
- 히스토그램과 요약 Best Practices — φ-분위수와 (클래식) 히스토그램 메트릭 타입 실무
- PromQL 연산자 — 네이티브 히스토그램에 동작하는 이항·집계 연산자
- PromQL 함수 —
histogram_quantile,histogram_avg등 히스토그램 전용 함수 - HTTP API — 쿼리 API의 히스토그램 응답 형식
- Remote Write v2 사양 — 원격 전송에서의 네이티브 히스토그램