평균 응답 시간은 120ms 인데 왜 느리다고 할까
대시보드 평균 응답 시간은 120ms 인데 느리다는 문의가 계속 온다. 로그를 뒤지면 5초, 10초짜리 요청이 하루 수만 건 숨어 있다. 평균은 그 꼬리를 뭉개 버린다.
대시보드의 평균 응답 시간은 120ms 다. 목표치 안에 잘 들어와 있고 그래프도 평평하다.
그런데 CS 문의가 끊이질 않는다. "너무 느려요", "안 돼요". 재현해 보면 대부분 잘 된다.
로그를 뒤져 보니 5초, 10초 걸린 요청들이 실제로 있었다. 전체의 1% 도 안 되지만 절대 수로는 하루 수만 건이다.
평균이 거짓말을 한 건 아니다. 자기 일은 정확히 했다.
다만 평균이 답할 수 없는 질문에 평균으로 답하고 있었다. 물어야 했던 건 "우리 서비스는 빠른가"가 아니라 "느린 사람이 얼마나 되는가"였다.
평온한 그래프 뒤에서 10명이 기다린다
응답 시간 분포는 대칭이 아니다. 대부분은 빠르고 소수가 아주 느린 모양이다. 이런 분포에선 평균이 실제 경험을 대표하지 못한다.
빠른 요청 990개가 100ms, 느린 요청 10개가 5초라면 평균은 149ms 다. 그래프는 평온한데 10명은 5초를 기다렸다.
1% 라는 숫자도 오해하기 쉽다. "100명 중 1명이 느리다"로 읽으면 작아 보인다. 실제로는 그렇게 굴러가지 않는다.
한 화면을 그리는 데 API 를 20번 부른다고 해 보자. 호출마다 1% 확률로 느리다면, 20개 중 하나라도 느릴 확률은 약 18% 다. 다섯 명 중 한 명은 느린 화면을 보는 셈이다. 호출이 많을수록 이 확률은 더 올라간다.
18%.
그래서 사용자가 느끼는 지연은 개별 호출의 평균이 아니라 가장 느린 구성 요소가 정한다. 꼬리 지연을 봐야 하는 이유가 여기 있다.
하나 더 있다. 느린 요청을 겪는 사람이 무작위가 아닐 수도 있다. 데이터가 많은 사용자, 특정 지역, 특정 기기가 매번 느린 쪽에 걸린다면? 그 사람들에게는 1% 가 아니라 100% 다.
평균 대신 분포를 내보내기
평균 대신 분포를 본다:
avg(response_time)
→ 꼬리를 뭉갠다. 사용자 경험을 대표하지 못한다
백분위수를 함께 본다: p50 · p95 · p99 · 최댓값
p50 과 p99 의 간격 자체가 신호다
· 히스토그램으로 내보내고 조회 시점에 계산한다
histogram_quantile(0.99,
sum by (le) (rate(http_duration_bucket[5m])))
미리 계산된 분위수를 저장하면 합칠 수 없다
· 사용자 관점 지표 — SLO 위반 비율
1초를 넘긴 요청의 비율
이 값은 합쳐도 정확하고 사용자 경험에 가깝다✕ 문제 지점 · ✓ 해법 · 그대로 돌리기 전에 자기 환경에 맞게 고친다.
백분위수에도 함정이 있다
봐야 할 것부터 꼽아 보자.
우선 백분위수다. p50 은 전형적인 경험이고 p99 는 나쁜 쪽 경험이다. 둘 사이가 벌어진다면 소수가 크게 나빠지고 있다는 뜻인데, 평균으로는 이게 절대 안 보인다.
그런데 백분위수에도 함정이 있다. 분위수는 평균낼 수가 없다. 인스턴스별 p99 를 평균하면 아무 의미 없는 숫자가 나온다. 히스토그램으로 모아 두고 조회할 때 계산해야 한다.
요청 단위보다 사용자 단위로 보는 게 더 정확하기도 하다. 한 사람이 요청을 여러 번 보내는데 그중 하나만 느려도 그 사람 경험은 느린 거니까. 세션이나 화면 단위로 묶어 세면 실제 체감에 가까워진다.
꼬리 지연의 원인은 대개 뻔한 얼굴들이다. 가비지 컬렉션 정지, 커넥션 풀 대기, 캐시 미스, 락 경합, 재시도, 그리고 특정 사용자의 데이터 크기. 이런 문제들이 전부 꼬리에 나타난다. 꼬리를 보는 게 곧 이 문제들을 찾아내는 길이다.
대응 중에 유난히 잘 듣는 게 하나 있다. 헤지 요청이다. 응답이 p95 를 넘으면 같은 요청을 다른 서버로 한 번 더 보내고 먼저 오는 걸 쓴다. 일부 요청에만 해당하니 부하는 조금 늘고 꼬리는 크게 준다. 단, 멱등한 요청에만 쓸 수 있다.
알림 기준도 바꿔야 한다. 평균에 알림을 걸면 상황이 꽤 심각해져야 울린다. SLO 위반 비율에 걸면 훨씬 일찍 안다.
대시보드에서 평균부터 치우기
- 대시보드에서 평균을 지우거나, 적어도 백분위수와 나란히 둔다. 평균만 있으면 반드시 오해한다.
- p50·p95·p99·최댓값은 한자리에서 봐야 한다. p50 과 p99 의 간격이 신호다.
- 분위수를 미리 계산해 저장하면 합칠 수 없다. 히스토그램으로 내보내고 조회 시점에 계산한다.
- 요청 하나가 느린 것과 사용자가 느린 건 다르다. 사용자·세션 단위로도 집계해 두자.
- 알림은 SLO 위반 비율로 건다. "1초를 넘긴 요청이 1% 초과" 같은 식으로.
- 느린 요청의 표본이 없으면 원인을 못 찾는다. 어떤 사용자, 어떤 파라미터였는지 남겨 둔다.
- 멱등한 요청이라면 헤지 요청을 검토해 볼 만하다. 부하는 조금 늘고 꼬리는 크게 준다.
- 한 화면이 몇 번의 호출로 그려지는지 세어 본다. 호출이 많을수록 꼬리에 걸릴 확률이 곱해진다.
- 느린 요청이 특정 사용자에게 몰린다면 그 사람들에게는 1% 가 아니라 100% 다. 몰리는지부터 확인한다.
분포를 직접 들여다보기
분포를 보는 법:
· 백분위수를 나란히 — 간격이 신호다
p50 / p95 / p99 / max 를 한 그래프에
p50 은 그대로인데 p99 만 오르면 소수가 크게 나빠지는 중
· 엔드포인트별로 나눈다
전체 p99 는 트래픽 많은 엔드포인트에 지배된다
· SLO 위반 비율 — 합쳐도 정확하고 사용자에 가깝다
sum(rate(http_duration_bucket{le='1'}[5m]))
/ sum(rate(http_duration_count[5m]))
· 한 화면이 몇 번의 호출로 만들어지나
20번이면 각 1% 만 느려도 화면의 18% 가 느리다
· 느린 요청의 표본을 남기는 규칙이 있는가
샘플링이 하필 그것을 버리면 원인을 못 찾는다핵심 한 줄평균은 아무도 겪지 않는 값이다. 꼬리를 봐라.
자주 묻는 것
인스턴스별 p99 를 평균 내서 보면 안 될까?
안 된다. 분위수는 평균낼 수 없어서 의미 없는 숫자가 나온다. 히스토그램으로 모아 두고 조회할 때 분위수를 계산해야 한다.