Observability/Observability

EP04 [시리즈 1: Observability의 개념과 방향성] #4 히스토그램을 활용한 효율적인 메트릭 관리 방안

ygtoken 2025. 3. 19. 13:21
728x90

이번 글에서는 프로메테우스와 그라파나를 활용한 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>

관측된 총 이벤트 수를 저장합니다.

 

프로메테우스 히스토그램 구조

✅ 히스토그램 메트릭 설정

히스토그램 메트릭을 효과적으로 사용하려면 버킷을 적절히 설정하는 것이 중요합니다.

▶️ 버킷 설정 방법:

  1. 선형 버킷(Linear Buckets): 균등한 간격으로 버킷을 생성합니다.
  2. // 0.1초부터 시작하여 0.1초 간격으로 10개 버킷 생성 (0.1s, 0.2s, ..., 1.0s) prometheus.LinearBuckets(0.1, 0.1, 10)
  3. 지수 버킷(Exponential Buckets): 지수적으로 증가하는 간격으로 버킷을 생성합니다.
  4. // 0.01초부터 시작하여 2배씩 증가하는 8개 버킷 생성 (0.01s, 0.02s, 0.04s, ..., 1.28s) prometheus.ExponentialBuckets(0.01, 2, 8)
  5. 기본 버킷(Default Buckets): 프로메테우스의 기본 버킷 설정을 사용합니다.
  6. // 기본 버킷: 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를 모니터링하는 데 강력한 도구입니다. 효과적인 설계와 관리 전략을 통해 시스템 성능에 대한 심층적인 인사이트를 얻을 수 있으며, 사용자 경험을 지속적으로 개선할 수 있습니다.

 

728x90