이번 글에서는 프로메테우스와 그라파나를 활용한 Observability 구성 시리즈의 네 번째 포스트로, 히스토그램 메트릭의 심층적 이해와 이를 활용한 효율적인 메트릭 관리 방안에 대해 알아보겠습니다.
📌 히스토그램이란?
히스토그램은 값의 분포를 측정하고 시각화하는 데 사용되는 통계적 도구입니다. 프로메테우스에서 히스토그램은 측정된 값을 미리 정의된 구간(버킷)으로 분류하여 값의 분포를 파악할 수 있게 해줍니다.
✅ 히스토그램의 기본 개념
▶️ 히스토그램의 구성요소:
- 버킷(Bucket): 측정값의 범위를 나타내는 구간
- 카운터(Counter): 각 버킷에 해당하는 값의 개수
- 합계(Sum): 모든 측정값의 합
- 개수(Count): 전체 측정값의 개수
▶️ 히스토그램의 유용성:
- 분포 파악: 데이터가 어떻게 분포되어 있는지 시각적으로 이해할 수 있습니다.
- 분위수 계산: 90번째 백분위수(p90), 95번째 백분위수(p95)와 같은 통계를 산출할 수 있습니다.
- 이상치 식별: 데이터 분포에서 벗어난 이상치를 쉽게 발견할 수 있습니다.
- 성능 목표 수립: SLO(Service Level Objectives)를 정의하고 모니터링하는 데 활용할 수 있습니다.
✅ 히스토그램 vs 서머리
프로메테우스에서는 값의 분포를 측정하기 위해 히스토그램과 서머리라는 두 가지 메트릭 유형을 제공합니다.
▶️ 히스토그램(Histogram):
- 서버 측 계산: 프로메테우스 서버에서 분위수를 계산합니다.
- 사전 정의된 버킷: 버킷 범위를 미리 정의해야 합니다.
- 장점: 여러 히스토그램을 집계할 수 있습니다.
- 단점: 정확도가 버킷 정의에 의존합니다.
▶️ 서머리(Summary):
- 클라이언트 측 계산: 애플리케이션에서 분위수를 계산합니다.
- 스트리밍 분위수: 데이터 수집 시점에 분위수를 계산합니다.
- 장점: 더 정확한 분위수를 제공합니다.
- 단점: 여러 서머리를 집계할 수 없습니다.
# 히스토그램과 서머리 메트릭 예시 (Go 클라이언트 코드)
# 히스토그램 정의
requestDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP 요청 처리 시간(초)",
Buckets: prometheus.LinearBuckets(0.1, 0.1, 10), // 0.1, 0.2, ..., 1.0 초 버킷
})
# 서머리 정의
requestDuration = prometheus.NewSummary(prometheus.SummaryOpts{
Name: "http_request_duration_seconds",
Help: "HTTP 요청 처리 시간(초)",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
})
📌 프로메테우스에서의 히스토그램 구현
프로메테우스에서 히스토그램은 특별한 방식으로 구현되어 있습니다. 이를 이해하면 효과적인 메트릭 설계와 쿼리가 가능해집니다.
✅ 프로메테우스 히스토그램 구조
프로메테우스에서 히스토그램은 실제로 여러 시계열의 집합으로 저장됩니다:
▶️ 버킷 시계열:
<basename>_bucket{le="<upper bound>", ...} <cumulative count>
각 버킷은 상한값(le, less than or equal)을 레이블로 가지며, 해당 값 이하의 모든 관측치 수를 누적 카운트로 저장합니다.
▶️ 합계 시계열:
<basename>_sum{...} <sum of values>
모든 관측된 값의 합계를 저장합니다.
▶️ 개수 시계열:
<basename>_count{...} <count of events>
관측된 총 이벤트 수를 저장합니다.
✅ 히스토그램 메트릭 설정
히스토그램 메트릭을 효과적으로 사용하려면 버킷을 적절히 설정하는 것이 중요합니다.
▶️ 버킷 설정 방법:
- 선형 버킷(Linear Buckets): 균등한 간격으로 버킷을 생성합니다.
- // 0.1초부터 시작하여 0.1초 간격으로 10개 버킷 생성 (0.1s, 0.2s, ..., 1.0s) prometheus.LinearBuckets(0.1, 0.1, 10)
- 지수 버킷(Exponential Buckets): 지수적으로 증가하는 간격으로 버킷을 생성합니다.
- // 0.01초부터 시작하여 2배씩 증가하는 8개 버킷 생성 (0.01s, 0.02s, 0.04s, ..., 1.28s) prometheus.ExponentialBuckets(0.01, 2, 8)
- 기본 버킷(Default Buckets): 프로메테우스의 기본 버킷 설정을 사용합니다.
- // 기본 버킷: 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 prometheus.DefBuckets
▶️ 버킷 설정 모범 사례:
- 관심 있는 범위 포함: 중요한 분위수(p50, p90, p95, p99)가 버킷 안에 포함되도록 설정합니다.
- 적절한 세분화: 너무 많은 버킷은 저장 공간과 성능에 영향을 미칩니다.
- 동적 범위 고려: 지수 버킷은 넓은 범위의 값을 효율적으로 포착할 수 있습니다.
- +Inf 버킷: 가장 큰 버킷은 항상 "+Inf"(무한대)로 설정되어 모든 값을 포함합니다.
// 응답 시간에 적합한 버킷 설정 예시 (100ms ~ 10s 범위)
http_request_duration = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP 요청 처리 시간",
Buckets: []float64{0.1, 0.25, 0.5, 1, 2.5, 5, 10},
})
// 메모리 사용량에 적합한 버킷 설정 예시 (10MB ~ 10GB 범위)
memory_usage = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "memory_usage_bytes",
Help: "메모리 사용량",
Buckets: prometheus.ExponentialBuckets(10*1024*1024, 4, 6), // 10MB, 40MB, 160MB, 640MB, 2.56GB, 10.24GB
})
📌 히스토그램을 활용한 분석과 시각화
히스토그램 데이터를 효과적으로 분석하고 시각화하는 방법을 알아보겠습니다.
✅ 분위수 계산
프로메테우스에서는 histogram_quantile() 함수를 사용하여 히스토그램 데이터에서 분위수를 계산할 수 있습니다.
▶️ 기본 사용법:
# 95번째 백분위수 응답 시간 계산
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
▶️ 레이블별 분위수 계산:
# 경로별로 그룹화하여 90번째 백분위수 계산
histogram_quantile(0.9, sum by(le, path) (rate(http_request_duration_seconds_bucket[5m])))
▶️ 분위수 계산의 정확도:
- 분위수 계산의 정확도는 버킷 설정에 의존합니다.
- 버킷 내에서는 선형 보간법을 사용하여 값을 추정합니다.
- 중요한 분위수가 예상되는 범위에 적절한 버킷을 설정하는 것이 중요합니다.
✅ 히스토그램 시각화 방법
히스토그램 데이터를 다양한 방식으로 시각화하여 성능 패턴을 확인할 수 있습니다.
▶️ 분위수 시각화:
특정 분위수(p50, p90, p95, p99 등)를 시간에 따라 추적하여 성능 추세를 파악합니다.
# 다양한 분위수의 응답 시간 시각화
histogram_quantile(0.5, sum by(le) (rate(http_request_duration_seconds_bucket[5m]))) # p50
histogram_quantile(0.9, sum by(le) (rate(http_request_duration_seconds_bucket[5m]))) # p90
histogram_quantile(0.95, sum by(le) (rate(http_request_duration_seconds_bucket[5m]))) # p95
histogram_quantile(0.99, sum by(le) (rate(http_request_duration_seconds_bucket[5m]))) # p99
▶️ 히트맵 시각화:
시간에 따른 값의 분포를 2차원 히트맵으로 표현하여 패턴과 이상치를 시각적으로 파악합니다.
# 그라파나에서 히트맵 패널 설정 예시
heatmap:
dataFormat: "time_series_buckets"
yBucketBound: "upper"
yAxis:
decimals: 0
format: "ms"
min: 0
color:
mode: "spectrum"
scheme: "blues"
legend:
show: true
▶️ 애플리케이션별 분위수 비교:
여러 애플리케이션 또는 서비스 간의 성능을 비교합니다.
# 서비스별 p95 응답 시간 비교
histogram_quantile(0.95, sum by(le, service) (rate(http_request_duration_seconds_bucket[5m])))
📌 효율적인 히스토그램 관리 전략
히스토그램 메트릭은 강력하지만, 잘못 관리하면 리소스 사용량이 많아질 수 있습니다. 다음은 효율적인 히스토그램 관리 전략입니다.
✅ 카디널리티 관리
히스토그램은 각 버킷마다 별도의 시계열을 생성하므로 카디널리티 관리가 중요합니다.
▶️ 레이블 최소화:
# 나쁜 예: 높은 카디널리티
http_request_duration_seconds_bucket{le="0.1", path="/api/user/12345", user_id="12345", request_id="abcd-1234-5678"}
# 좋은 예: 필수 레이블만 사용
http_request_duration_seconds_bucket{le="0.1", path="/api/user/:id", method="GET", service="user-api"}
▶️ 집계 전략:
중요한 차원으로만 집계하여 카디널리티를 줄입니다.
# 필요한 차원으로만 집계하여 분위수 계산
histogram_quantile(0.95, sum by(le, service, method) (rate(http_request_duration_seconds_bucket[5m])))
✅ 버킷 최적화
적절한 버킷 설정은 메모리 사용량과 정확도 사이의 균형을 유지하는 데 중요합니다.
▶️ 적절한 버킷 수:
- 너무 많은 버킷은 저장 공간과 처리 성능에 영향을 미칩니다.
- 일반적으로 10-15개의 버킷이 적절합니다.
▶️ 관심 영역에 집중:
- SLO와 관련된 중요 임계값 주변에 더 세분화된 버킷을 설정합니다.
- 예: 300ms SLO가 있다면 250ms, 300ms, 350ms 주변에 세밀한 버킷을 설정합니다.
// 사용자 정의 버킷 예시 (300ms SLO 주변에 세밀한 버킷)
customBuckets := []float64{0.05, 0.1, 0.2, 0.25, 0.3, 0.35, 0.4, 0.5, 1, 2.5, 5, 10}
✅ 보존 정책 및 다운샘플링
장기 데이터 보관을 위한 전략을 수립합니다.
▶️ 다운샘플링:
- 오래된 데이터는 해상도를 낮추어 저장 공간을 절약합니다.
- 예: 5분마다 수집 → 1시간 간격으로 다운샘플링
▶️ 선택적 집계:
- 모든 차원을 장기간 보관하지 말고, 중요한 차원만 집계하여 보관합니다.
- 예: 초기에는 모든 레이블 유지, 오래된 데이터는 서비스 수준으로만 집계
# 프로메테우스 보존 정책 설정 예시
storage:
tsdb:
path: /prometheus
retention.time: 15d # 상세 데이터는 15일간 보관
# 타노스(Thanos) 다운샘플링 설정 예시
downsample:
resolution5m: 30d # 5분 해상도 데이터는 30일 보관
resolution1h: 90d # 1시간 해상도 데이터는 90일 보관
📌 실제 활용 사례와 모범 사례
히스토그램 메트릭은 다양한 상황에서 시스템 성능과 사용자 경험을 모니터링하는 데 활용할 수 있습니다.
✅ API 성능 모니터링
REST API나 gRPC 서비스의 응답 시간을 측정하여 성능을 모니터링합니다.
▶️ 구현 예시:
// Go 언어 예시 (Gin 프레임워크)
func prometheusMiddleware() gin.HandlerFunc {
httpRequestDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP 요청 처리 시간 히스토그램",
Buckets: []float64{0.1, 0.25, 0.5, 1, 2.5, 5, 10},
}, []string{"path", "method", "status"})
prometheus.MustRegister(httpRequestDuration)
return func(c *gin.Context) {
start := time.Now()
c.Next()
path := c.FullPath()
status := strconv.Itoa(c.Writer.Status())
duration := time.Since(start).Seconds()
httpRequestDuration.WithLabelValues(path, c.Request.Method, status).Observe(duration)
}
}
▶️ 쿼리 및 대시보드:
# 경로별 p95 응답 시간
histogram_quantile(0.95, sum by(le, path) (rate(http_request_duration_seconds_bucket[5m])))
# 응답 시간 SLO 위반 비율 (200ms 초과)
sum(rate(http_request_duration_seconds_bucket{le="0.2"}[5m])) / sum(rate(http_request_duration_seconds_count[5m]))
✅ 데이터베이스 쿼리 성능
데이터베이스 쿼리 실행 시간을 측정하여 성능 병목 현상을 식별합니다.
▶️ 구현 예시:
# Python 예시 (SQLAlchemy + Prometheus 클라이언트)
from prometheus_client import Histogram
db_query_duration = Histogram(
'db_query_duration_seconds',
'Database query execution time',
['query_type', 'table'],
buckets=[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5]
)
def execute_query(query, query_type, table):
start_time = time.time()
result = session.execute(query)
duration = time.time() - start_time
db_query_duration.labels(query_type=query_type, table=table).observe(duration)
return result
✅ 페이지 로드 시간 측정
웹 애플리케이션에서 페이지 로드 시간을 측정하여 사용자 경험을 모니터링합니다.
▶️ 클라이언트 측 구현:
// JavaScript 예시 (브라우저)
const pageLoadTime = performance.now() - performance.timing.navigationStart;
// 서버에 메트릭 보고
fetch('/metrics/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metric: 'page_load_time_seconds',
value: pageLoadTime / 1000,
labels: { page: window.location.pathname }
})
});
▶️ 서버 측 수집:
// Go 서버 예시
pageLoadTime := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "page_load_time_seconds",
Help: "Time taken to load a page",
Buckets: []float64{0.1, 0.5, 1, 2, 5, 10, 20},
}, []string{"page"})
// 클라이언트에서 보고된 메트릭 수집
func reportMetrics(w http.ResponseWriter, r *http.Request) {
var data struct {
Metric string `json:"metric"`
Value float64 `json:"value"`
Labels map[string]string `json:"labels"`
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if data.Metric == "page_load_time_seconds" {
pageLoadTime.WithLabelValues(data.Labels["page"]).Observe(data.Value)
}
w.WriteHeader(http.StatusOK)
}
📌 결론
이번 글에서 다룬 핵심 내용을 요약하면 다음과 같습니다:
- 히스토그램의 기본 개념
- 값의 분포를 버킷으로 집계하는 통계적 도구
- 프로메테우스에서는 버킷, 합계, 개수로 구성된 메트릭
- 히스토그램 vs 서머리 차이점
- 히스토그램: 서버 측 계산, 집계 가능, 버킷 정의에 의존
- 서머리: 클라이언트 측 계산, 더 정확한 분위수, 집계 불가능
- 효과적인 버킷 설계
- 관심 범위에 적절한 세분화
- SLO 주변에 더 조밀한 버킷 배치
- 지수 또는 선형 분포 활용
- 카디널리티 관리와 보존 정책
- 필수 레이블만 사용하여 카디널리티 제한
- 데이터 수명 주기 관리와 다운샘플링 적용
- 장기 저장을 위한 효율적인 전략 수립
히스토그램은 시스템의 성능 분포를 이해하고 SLO를 모니터링하는 데 강력한 도구입니다. 효과적인 설계와 관리 전략을 통해 시스템 성능에 대한 심층적인 인사이트를 얻을 수 있으며, 사용자 경험을 지속적으로 개선할 수 있습니다.