[DE Ch-08] 지연은 백분위와 최댓값으로 본다: 평균이 숨기는 꼬리와 합칠 수 있는 히스토그램
목차
결론부터
- 지연 분포는 정규분포가 아니고 여러 봉우리와 긴 꼬리를 가집니다. 평균과 표준편차로는 꼬리가 보이지 않습니다. 시뮬레이션에서 평균이 50.0ms와 49.6ms로 같은 두 서버의 p99는 61.6ms와 497.5ms였습니다.
- 백분위는 여러 개(p50, p95, p99, p99.9)를 함께 보고 최댓값도 봅니다. Gil Tene는 백분위와 최댓값을 적지 않았다면 요구사항을 적지 않은 것이라고 말합니다.
- 백분위는 평균 낼 수 없습니다. 1분마다 구한 p99 열 개의 평균은 118.1ms였고 10분 전체의 p99는 239.1ms였습니다. 여러 구간이나 여러 인스턴스를 합치려면 원본 분포(히스토그램)를 합친 뒤 백분위를 구해야 합니다.
- 요청 하나가 여러 서버의 응답을 모두 기다리면 드문 꼬리가 흔해집니다. 서버마다 1%가 느리면 100대를 기다리는 요청의 63%가 느린 응답을 하나 이상 만납니다(The Tail at Scale, 2013).
1. 평균이 같으면 같은 서버인가
두 서버의 평균 응답 시간이 50ms로 같다고 해 봅시다. 같은 성능이라고 말해도 될까요. 시뮬레이션으로 만든 두 분포입니다(1만 건, 시드 고정, labs/de-study/ch08의 percentiles.py).
| 서버 | 평균 | p50 | p95 | p99 | p99.9 | max |
|---|---|---|---|---|---|---|
| A: 고르게 50ms 근처 | 50.0 | 50.0 | 58.2 | 61.6 | 65.6 | 67.5 |
| B: 98%는 40ms, 2%는 400~600ms | 49.6 | 40.1 | 47.6 | 497.5 | 591.4 | 599.7 |
(단위 ms)
평균과 p95까지는 B가 오히려 빠릅니다. p99에서 B는 A의 여덟 배가 됩니다. B의 사용자 100명 중 2명은 0.5초를 기다립니다. 평균만 보면 이 두 명이 숫자에서 사라집니다.
2. 지연 분포에는 봉우리가 여럿 있다
Gil Tene는 2012년 QCon 발표 “How NOT to Measure Latency”에서 흔한 착각 두 가지를 꼽았습니다. 응답 시간이 정규분포를 따른다는 것, 그리고 측정 중 가끔 빠지는 값은 결과에 큰 영향이 없다는 것입니다. 실제 지연 분포는 여러 봉우리를 가지고(multi-modal), 주기적으로 멈추는 것처럼 보인다고 했습니다. GC, 캐시 미스, 락 대기, 재시도 같은 원인이 각자 다른 봉우리를 만듭니다.
정규분포라면 평균과 표준편차 두 숫자로 분포를 그릴 수 있습니다. 봉우리가 여럿이고 꼬리가 긴 분포에서는 이 두 숫자가 분포의 어느 부분도 정확히 말해 주지 않습니다. 발표의 교훈 슬라이드는 이렇게 정리합니다.
- 표준편차를 쓰거나 표준편차로부터 무언가를 끌어내지 말 것
- 최댓값은 항상 재고 그 뜻을 생각할 것
- 백분위를 재되 여러 개를 잴 것
- 측정 방법이 무엇이든 그 방법부터 시험할 것(일부러 멈추는 시스템에 먼저 돌려 볼 것)
마지막 항목은 Ch-09의 coordinated omission으로 이어집니다.
3. 백분위는 히스토그램을 합친 뒤 구한다
3-1. 재현
대시보드는 보통 1분마다 p99를 계산해 점으로 찍습니다. 하루치 p99를 보고 싶을 때 그 점들을 평균 내면 될까요.
10분 동안 요청을 받는데 3분째와 7분째에만 부하가 몰려 요청이 세 배로 늘고 지연이 200ms 근처로 오른다고 시뮬레이션했습니다.
| 계산 | 값 |
|---|---|
| 1분별 p99 열 개의 평균 | 118.1ms |
| 10분 전체 요청의 p99 | 239.1ms |
평균은 실제의 절반쯤입니다. 이유는 둘입니다. 1분마다 요청 수가 다른데 평균은 모든 분을 같은 무게로 셉니다. 그리고 p99는 “그 분 안에서 상위 1%“라서 느린 분의 꼬리와 한가한 분의 꼬리를 더해 나눠도 전체에서 상위 1%가 어디인지 나오지 않습니다. Gil Tene는 이를 “백분위는 평균 낼 수 없다. 끝.”이라는 제목의 글로 남겼습니다.
3-2. 합칠 수 있는 형태로 모은다
Prometheus 문서는 같은 문제를 summary와 histogram의 차이로 설명합니다.
| summary | histogram | |
|---|---|---|
| 백분위를 계산하는 곳 | 클라이언트(애플리케이션)에서 미리 계산 | 서버에서 쿼리할 때 계산 |
| 여러 인스턴스 합치기 | 안 된다. 문서는 미리 계산된 분위수를 합치는 것은 거의 의미가 없다고 적는다 | 된다. 버킷 개수를 더한 뒤 histogram_quantile() |
| 오차 | 분위수 축에서 설정한 만큼 | 버킷 너비만큼 |
# 나쁜 예: 인스턴스별 p95를 평균 낸다avg(http_request_duration_seconds{quantile="0.95"})
# 좋은 예: 버킷을 합친 뒤 p95를 구한다histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))3-3. HdrHistogram
Gil Tene가 만든 HdrHistogram은 지연을 기록하는 히스토그램 라이브러리입니다.
| 특징 | 내용 |
|---|---|
| 정밀도 | 유효숫자 자릿수로 정한다. 3자리면 양자화 오차가 0.1% 이내 |
| 메모리 | 기록한 값의 개수와 상관없이 고정. 범위와 정밀도로만 정해진다. 1μs~1시간을 유효숫자 3자리로 잡으면 약 185KB |
| 기록 비용 | 공식 사이트 기준 3~6ns(2014년 무렵 Intel CPU) |
| 합치기 | add(otherHistogram)으로 다른 히스토그램을 더한다 |
고정된 메모리로 모든 값을 정밀도 안에서 기억하니, 구간별, 인스턴스별 히스토그램을 더해 전체 백분위를 정확히 구할 수 있습니다. Ch-09에서 다룰 coordinated omission 보정 기능도 들어 있습니다.
4. 팬아웃: 드문 꼬리가 흔해지는 구조
4-1. 1%가 63%가 되는 계산
Jeff Dean과 Luiz Barroso의 2013년 글 “The Tail at Scale”은 이 문제를 수치로 보입니다. 서버가 보통 10ms에 응답하지만 p99가 1초인 경우입니다.
- 요청 하나가 서버 하나만 쓰면 100번에 1번 느립니다.
- 요청 하나가 서버 100대의 응답을 모두 모아야 하면 63%의 요청이 1초를 넘깁니다.
- 서버 하나에서 1만 번에 1번만 1초를 넘겨도, 서버 2,000대에 팬아웃하는 서비스는 요청 다섯 번에 한 번꼴로 1초를 넘깁니다.
계산은 1에서 “모든 서버가 빠를 확률”을 빼는 것입니다. 시뮬레이션 스크립트로 같은 값을 냈습니다.
| 기다리는 서버 수 | p99 꼬리를 한 번 이상 만날 확률 |
|---|---|
| 1 | 1.0% |
| 10 | 9.6% |
| 100 | 63.4% |
데이터 파이프라인에서도 같은 구조가 나옵니다. 태스크 100개가 모두 끝나야 다음 단계로 가는 DAG라면, DAG 전체 시간은 가장 느린 태스크가 정합니다. 태스크 하나의 p99가 DAG의 평범한 날이 됩니다.
4-2. 실측과 대응
같은 글의 표는 Google의 큰 팬아웃 서비스에서 루트 서버가 잰 값입니다.
| 기다리는 범위 | p50 | p95 | p99 |
|---|---|---|---|
| 아무 리프 1개 | 1ms | 5ms | 10ms |
| 전체 리프의 95% | 12ms | 32ms | 70ms |
| 전체 리프 100% | 40ms | 87ms | 140ms |
글은 가장 느린 5%를 기다리는 데 전체 p99 지연의 절반이 쓰인다고 적습니다. 대응으로 제안한 것 하나가 hedged request입니다. 응답이 늦으면 같은 요청을 다른 복제본에 한 번 더 보내고 먼저 온 답을 씁니다. 서버 100대에 퍼진 키 1,000개를 읽는 BigTable 벤치마크에서 10ms 뒤 한 번 더 보내니 p99.9가 1,800ms에서 74ms로 줄었고 늘어난 요청은 2%였습니다. Ch-03에서 다룬 재시도와 비슷해 보이지만 실패를 기다리지 않고 느림을 기준으로 보낸다는 점이 다릅니다. 그리고 두 번 처리돼도 되는 읽기 요청이라 가능한 방법입니다.
5. 지연을 적을 때의 형식
이 시리즈에서 지연을 적을 때 쓰는 형식입니다.
| 적을 것 | 예 |
|---|---|
| 조건 | 초당 100건 고정 부하, 10분, 워밍업 1분 제외 |
| 분포 | p50, p95, p99, p99.9 |
| 최댓값 | max |
| 건수 | 몇 건으로 잰 값인지(p99.9는 최소 1,000건이 넘어야 의미가 있다) |
| 합친 방법 | 히스토그램을 합쳤는지, 구간별 값을 그대로 보였는지 |
마지막에서 두 번째 줄은 단순한 계산입니다. 100건으로 p99.9를 구하면 사실상 최댓값입니다.
6. 범위와 한계
- 1절과 3-1절은 시뮬레이션이고 분포 모양을 일부러 골랐습니다. 실제 서버의 분포가 이렇다는 근거로 쓰면 안 됩니다.
- 4절의 수치는 2013년 글에 실린 Google의 측정값입니다.
- Gil Tene의 “백분위는 평균 낼 수 없다” 글은 제목과 날짜만 확인했고 본문 논증은 Prometheus 문서와 이 글의 재현으로 대신했습니다.
7. 헷갈렸던 것
Q. 그럼 평균은 아예 쓸모없나요?
평균에 요청 수를 곱하면 총 처리 시간이 나옵니다. 용량 계산(서버가 몇 대 필요한지)에는 평균이 맞는 숫자입니다. 사용자가 기다리는 경험을 말할 때는 백분위가 맞습니다. 질문이 무엇인지에 따라 고릅니다.
Q. p99를 목표로 잡으면 1%는 포기한다는 뜻인가요?
그 1%가 누구인지에 달렸습니다. 4절처럼 요청 하나가 백엔드 여러 개를 거치면 백엔드의 p99가 사용자 요청의 절반 이상에 닿을 수 있습니다. 그래서 백엔드일수록 더 높은 백분위(p99.9)를 목표로 잡습니다.
Q. 이미 있는 성능 글과는 무엇이 다른가요?
Spring 백엔드 개발자가 성능 지표를 읽는 법이 API 서버의 지표를 읽는 법이라면, 이 글은 지연 숫자 자체를 어떻게 모으고 합치는지에 맞췄습니다. 다음 글(Ch-09)은 그 숫자를 재는 과정에서 생기는 함정을 다룹니다.
참고 자료
- Gil Tene, How NOT to Measure Latency, QCon San Francisco 2012 발표 자료
- Gil Tene, #LatencyTipOfTheDay: You can’t average percentiles. Period., 2014
- HdrHistogram, GitHub README, JavaDoc
- Prometheus, Histograms and summaries
- Jeffrey Dean, Luiz André Barroso, The Tail at Scale, Communications of the ACM 56(2), 2013
- Tyler Treat, Everything You Know About Latency Is Wrong
- 실습 코드: labs/de-study/ch08
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.