p50과 p99
평균 응답시간은 멀쩡한데 왜 사용자는 느리다고 할까?
엘리베이터 평균은 괜찮은데
아파트에 엘리베이터가 있다고 해 보세요. 관리 사무소는 “평균 대기 시간은 1분”이라고 자랑합니다.
그런데 주민 불만이 끊기지 않습니다. 대부분은 40초 만에 타는데, 100명 중 1명은 가끔 20분을 기다립니다. 평균을 내면 여전히 1분 근처입니다. 불평하는 사람은 그 1명입니다.
서버 응답시간도 같습니다. 대시보드의 평균은 빠른 요청에 가려지기 쉽습니다. 사용자가 “느리다”고 느끼는 순간은 느린 쪽 꼬리에 몰려 있습니다.
| 엘리베이터 | 서버 |
|---|---|
| 평균 대기 1분 | 평균 응답시간 |
| 절반의 사람이 기다리는 시간 | p50(느린 쪽 50%의 경계) |
| 가장 불운한 1%가 기다리는 시간 | p99(느린 쪽 1%의 응답시간) |
이 랩에서는 같은 평균을 가진 두 분포를 나란히 놓고, 왜 한쪽만 사용자 불만을 만드는지 눈으로 확인합니다.
직접 돌려보세요
왼쪽은 거의 모든 요청이 비슷한 분포입니다. 오른쪽은 평균을 똑같이 맞춘 채 느린 꼬리만 키운 분포입니다.
프리셋 같은 평균·긴 꼬리를 눌러 보세요. 평균 칸은 둘 다 100ms 근처인데, p99만 크게 벌어집니다.
왼쪽 · 고른 분포
거의 모든 요청이 100ms
구성: 빠름 100ms / 느림 100ms (0.0%)
오른쪽 · 꼬리 있는 분포
빠른 59ms · 느린 2.1초 (2%)
구성: 빠름 59ms / 느림 2.1초 (2.0%)
왜 이렇게 되는지, 산수로
공식은 곱셈과 덧셈입니다.
평균 ≈ (빠른 요청 × 그 비율) + (느린 요청 × 그 비율)
예를 들어 요청 100개 중 99개가 50ms, 1개가 5050ms이면:
| 구성 | 계산 | 결과 |
|---|---|---|
| 평균 | 50×0.99 + 5050×0.01 | 100ms |
| p50(가운데) | 대부분 50ms 구간 | 50ms |
| p99(느린 쪽 1% 경계) | 그 1개 느린 요청 | 5050ms |
평균은 “괜찮음”입니다. p99는 “장애”에 가깝습니다. 1%만 망가져도 대시보드 평균은 멀쩡해 보일 수 있습니다.
흔한 오해
“평균이 목표 안이면 SLO 달성 아닌가요?”
평균 SLO만 보면 위 예시처럼 통과합니다. 그런데 그 1% 사용자는 장바구니·결제·로그인에서 이탈할 수 있습니다. 체감은 평균이 아니라 느린 꼬리에 가깝습니다.
“p99를 보면 너무 예민한 거 아닌가요?”
트래픽이 초당 100요청이면, p99는 초당 1요청입니다. “극소수”처럼 들려도 하루로 치면 수만 번일 수 있습니다. 서비스 규모에 따라 p95나 p99.9를 쓰기도 하지만, 평균만 보는 것보다는 항상 낫습니다.
“그럼 p50은 왜 보나요?”
p50은 “보통의 사용자”입니다. p50이 나빠지면 전체가 느린 겁니다. p99만 나빠지면 꼬리 문제(GC, 잠금, 느린 쿼리, 재시도, 원격 호출)를 의심합니다. 둘을 같이 보면 원인이 갈립니다.
챌린지
평균 한도는 지키되, 극단적인 1% 꼬리로 숫자를 속이는 해법은 통과하지 못하게 막아 두었습니다. 꼬리 높이를 직접 자르세요.
🎯 챌린지 — 평균은 지켜도 꼬리는 자르라
대시보드 평균은 100ms 이하로 유지해야 합니다. 동시에 사용자 체감인 p99는 400ms 미만이어야 합니다. 느린 요청 비율은 5%까지만 허용됩니다. 평균만 맞추려고 1%를 극단적으로 늘리는 해법은 일부러 실패하게 만들어 두었습니다.
확인 문제
⏱ 30초 리마인드
- 평균은 빠른 요청에 가려질 수 있다.
- p50은 보통의 사용자, p99는 불운한 1%의 경계다.
- 같은 평균이라도 꼬리 모양이 다르면 체감이 다르다.
- p50은 괜찮은데 p99만 나쁘면 꼬리 원인을 의심한다.
- SLO·알람에는 평균만 두지 말고 백분위를 함께 둔다.
현실에서는
Generic pattern은 단순합니다. 지연시간 목표를 평균이 아니라 백분위로 정하고, 꼬리를 만드는 작업(동기 잠금, N+1, 거대 페이로드, 무분별한 재시도)을 줄입니다.
예: 읽기 API 목표
p50 < 80ms
p99 < 300ms
평균만 100ms 미만 ← 이것만으로는 부족
구현 위치만 달라집니다.
- TypeScript / Node.js: APM·OpenTelemetry histogram,
p50/p99패널.Promise타임아웃과 동시성 제한이 꼬리를 만드는지 확인합니다. - Go:
prometheushistogram quantile, 또는 hdrhistogram.p99알람을 평균 알람보다 먼저 둡니다. - JVM: Micrometer/
Timerpercentile, GC 일시정지가 p99에 찍히는지 봅니다. - Kubernetes: Pod 평균 CPU는 멀쩡한데 특정 Pod만 느릴 수 있습니다. 백분위는 요청 단위로 모으는 편이 안전합니다.
- AWS: ALB/API Gateway/CloudWatch latency percentile, Lambda duration percentile. 콜드 스타트는 평균보다 p99에 먼저 보입니다.
이 수치는 browser에서 만든 학습용 혼합 분포입니다. 실제 서비스의 p99는 측정 창, 샘플링, 히스토그램 버킷에 따라 달라지니 raw measurement로 다시 검증하세요.
다음 랩
평균에 가려진 꼬리를 보게 되었습니다. 다음은 그 꼬리가 주기적으로 한꺼번에 터지는 장면 — 캐시가 동시에 만료되며 DB로 쏟아지는 캐시 스탬피드입니다.