전체 비유: 응급실 대기 시간#

Latency를 응급실 대기 시간에 비유하면 이해하기 쉽습니다:

응급실 비유Latency 개념의미
평균 대기 시간평균 응답시간전체 환자 평균 (왜곡 가능)
절반 환자 대기 시간P50 (중앙값)일반적인 경험
대부분 환자 대기 시간P95거의 모든 환자 경험
최악 대기 시간P99SLA 기준, 1%의 경험
응급 vs 일반 환자성공/실패 분리요청 유형별 분석
대기 시간 목표SLA/SLO허용 가능한 한계
대기 시간 급증 알림알림 규칙지연 발생 시 감지

이처럼 응급실 대기 시간에서 평균보다 “최악의 대기 시간"이 중요하듯, 서비스에서도 P99가 사용자 경험을 좌우합니다.


대상 독자: 서비스 응답시간을 개선하려는 개발자, SRE 선수 지식: histogram_quantile 소요 시간: 약 20분 이 문서를 읽으면: 지연시간을 정확히 측정하고 SLA 기반 알림을 설정할 수 있습니다

TL;DR#

핵심 요약:

  • P50 (중앙값): 일반 사용자 경험
  • P95: 대부분의 사용자 경험
  • P99: 최악의 사용자 경험 (SLA 기준)
  • 평균보다 백분위가 실제 경험을 더 잘 반영
  • 성공/실패 요청의 지연시간을 분리 측정

왜 백분위가 중요한가?#

Latency(지연시간)는 사용자가 직접 체감하는 품질입니다. 아무리 기능이 훌륭해도 5초를 기다려야 한다면 사용자는 떠납니다. Amazon의 연구에 따르면 100ms의 지연 증가가 매출 1% 감소로 이어집니다.

평균은 거짓말쟁이#

비유: 평균 연봉의 함정

10명의 연봉이 각각 5,000만원인 회사에 연봉 100억인 CEO가 입사했습니다. 평균 연봉은 갑자기 9.5억이 됩니다. 하지만 10명 중 누구도 9.5억을 받지 않습니다.

마찬가지로 “평균 응답시간 200ms"는 대부분의 사용자 경험을 반영하지 못합니다. 99명이 100ms를 경험하고 1명이 10초를 경험해도 평균은 199ms입니다. 그 1명의 사용자는 끔찍한 경험을 하지만, 평균에는 거의 반영되지 않습니다.

백분위(Percentile)는 이 문제를 해결합니다. P99가 10초라면 “100명 중 1명은 10초를 기다린다"는 것을 명확히 알 수 있습니다.

평균의 함정#

graph LR
    subgraph "100개 요청"
        A["99개: 100ms"]
        B["1개: 10,000ms"]
    end

    AVG["평균: 199ms<br>❌ 왜곡됨"]
    P99["P99: 10,000ms<br>✅ 최악 경험"]
지표의미
평균199ms1%의 느린 요청에 왜곡됨
P50100ms절반은 100ms 이하
P9910,000ms1%는 10초 이상 대기

각 백분위의 의미#

백분위커버리지용도
P5050%일반적인 사용자 경험
P9090%대부분 사용자 경험
P9595%거의 모든 사용자 경험
P9999%SLA 기준, 최악 경험
P99.999.9%매우 엄격한 SLA

측정 방법#

기본 PromQL#

# P50 (중앙값)
histogram_quantile(0.5,
  sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
)

# P95
histogram_quantile(0.95,
  sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
)

# P99
histogram_quantile(0.99,
  sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
)

# 평균 (비교용)
rate(http_request_duration_seconds_sum[5m])
/ rate(http_request_duration_seconds_count[5m])

성공/실패 분리 측정#

실패 요청은 빠를 수 있습니다. 타임아웃으로 실패하면 느리고, 즉시 거부되면 빠릅니다. 분리 측정이 중요합니다.
# 성공 요청의 P99
histogram_quantile(0.99,
  sum by (service, le) (
    rate(http_request_duration_seconds_bucket{status!~"5.."}[5m])
  )
)

# 실패 요청의 P99
histogram_quantile(0.99,
  sum by (service, le) (
    rate(http_request_duration_seconds_bucket{status=~"5.."}[5m])
  )
)

엔드포인트별 측정#

# 엔드포인트별 P99
histogram_quantile(0.99,
  sum by (path, le) (rate(http_request_duration_seconds_bucket[5m]))
)

# 느린 엔드포인트 상위 5개
topk(5,
  histogram_quantile(0.99,
    sum by (path, le) (rate(http_request_duration_seconds_bucket[5m]))
  )
)

SLA/SLO 설정#

SLA 정의 예시#

서비스P99 목표P99.9 목표
API Gateway100ms500ms
주문 서비스500ms2s
검색 서비스200ms1s
배치 작업30s2m

SLA 준수율 계산#

# P99가 500ms 이하인 비율 (SLA 준수율)
avg_over_time(
  (
    histogram_quantile(0.99,
      sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
    ) < 0.5
  )[24h:5m]
)

에러 버짓 계산#

# 목표: P99 < 500ms
# 허용 위반 시간: 월 43.2분 (99.9% SLA)

# 현재 SLA 위반 시간 (시간/일)
count_over_time(
  (
    histogram_quantile(0.99,
      sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
    ) > 0.5
  )[24h:5m]
) * 5 / 60  # 5분 단위 → 시간 변환

알림 규칙#

기본 알림#

groups:
  - name: latency_alerts
    rules:
      # P99가 목표 초과
      - alert: HighP99Latency
        expr: |
          histogram_quantile(0.99,
            sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
          ) > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.service }} P99 latency is {{ $value | humanizeDuration }}"
          runbook_url: "https://wiki/runbook/high-latency"

      # P99가 심각 수준
      - alert: CriticalP99Latency
        expr: |
          histogram_quantile(0.99,
            sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
          ) > 2
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.service }} P99 latency critical: {{ $value | humanizeDuration }}"

급격한 변화 감지#

# P99가 평소 대비 2배 이상 증가
- alert: LatencySpike
  expr: |
    histogram_quantile(0.99, sum by (service, le) (rate(http_request_duration_seconds_bucket[5m])))
    >
    histogram_quantile(0.99, sum by (service, le) (rate(http_request_duration_seconds_bucket[5m] offset 1h)))
    * 2
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "{{ $labels.service }} latency doubled compared to 1 hour ago"

대시보드 설계#

권장 패널 구성#

┌─────────────────────────────────────────────────────┐
│ Stat: Current P99 │ Stat: P99 Change (vs 1h ago)    │
├─────────────────────────────────────────────────────┤
│ Time Series: P50 / P95 / P99 추이                   │
├─────────────────────────────────────────────────────┤
│ Heatmap: 응답시간 분포                               │
├─────────────────────────────────────────────────────┤
│ Table: 엔드포인트별 P99 (상위 10개)                  │
└─────────────────────────────────────────────────────┘

Grafana 쿼리 예시#

# Stat: 현재 P99
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

# Time Series: 백분위 비교
# P50
histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket[$__rate_interval])))
# P95
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[$__rate_interval])))
# P99
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[$__rate_interval])))

개선 전략#

지연시간이 높을 때#

graph TD
    START["P99 증가 감지"] --> Q1{"어떤 구간?"}
    Q1 --> |"앱 내부"| A1["프로파일링<br>CPU/메모리 점검"]
    Q1 --> |"DB 쿼리"| A2["슬로우 쿼리<br>인덱스 점검"]
    Q1 --> |"외부 API"| A3["서킷 브레이커<br>타임아웃 설정"]
    Q1 --> |"네트워크"| A4["DNS/연결 풀<br>점검"]

일반적인 원인#

원인증상해결책
DB 쿼리특정 엔드포인트만 느림인덱스, 쿼리 최적화
외부 API특정 의존성 호출 시 느림캐싱, 서킷 브레이커
GC주기적 스파이크힙 튜닝, GC 알고리즘
연결 풀 고갈동시성 높을 때 느림풀 사이즈 증가
CPU 포화전반적으로 느림스케일 아웃

핵심 정리#

지표용도임계값 예시
P50일반 경험 모니터링-
P95대시보드 주요 지표200ms
P99SLA 기준, 알림500ms
P99.9엄격한 SLA2s

Recording Rules 템플릿:

- record: service:http_request_duration_seconds:p99
  expr: |
    histogram_quantile(0.99,
      sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
    )

다음 단계#

추천 순서문서배우는 것
1Traffic처리량 모니터링
2높은 지연시간 진단문제 해결 가이드