LabHub
배우기 러닝패스 코스

관측성 · 그래프를 잘못 읽는 자리 · 이론

그래프는 데이터가 아니라 데이터의 요약이다

LabHub 에서 이어서 보기

한 줄 요약

대시보드는 자료가 아니라 자료의 요약이다. 요약하는 방법이 둘이면 답도 둘이 나오고, 둘 다 맞다. 그래서 "그래프에 안 보였다" 는 "일어나지 않았다" 가 아니다.

왜 이게 필요했나

사고 회의에서 이런 말이 오간다. "오류율이 28% 까지 올라갔습니다." "저희 대시보드에는 10% 로 보이는데요." 두 사람은 같은 Prometheus 를 보고 있고, 둘 다 쿼리를 틀리지 않았다. 한 사람은 5분 창으로, 다른 사람은 한 시간 창으로 물었을 뿐이다.

이 어긋남은 사소해 보이지만 결정을 바꾼다. 28% 는 에러 버짓을 하루치 태우는 숫자고 10% 는 "지켜보자" 는 숫자다. 경보가 15% 에 걸려 있었다면 한쪽 그래프에서는 울려야 했고 다른 쪽에서는 울릴 이유가 없다. 실제로 사고 후 "왜 경보가 안 울렸나" 를 파 보면, 규칙의 임계값이 아니라 규칙이 쓴 창 길이가 범인인 경우가 흔하다.

반대 방향의 착각도 있다. 자료가 있는데도 그래프가 비어 있는 경우다. 스크레이프 간격이 15초인 지표에 rate(...[15s]) 를 쓰면 창 안에 표본이 하나뿐이라 변화율을 낼 수 없고, Prometheus 는 오류가 아니라 빈 결과를 돌려준다. 화면에는 "No data" 가 뜬다. 그걸 보고 "트래픽이 끊겼다" 고 읽으면 없는 장애를 만들어 낸다.

어떻게 동작하나

구간 쿼리([/api/v1/query_range](https://prometheus.io/docs/prometheus/latest/querying/api/))는 세 가지를 받는다 — start·end·step. Prometheus 는 start 부터 end 까지 step 간격의 격자를 만들고, 격자점마다 순간 쿼리를 한 번씩 실행해 그 값을 이어 붙인다. 그래프의 선은 자료가 아니라 이 격자점들이다.

여기에 rate(...[창]) 이 붙으면 각 격자점의 값은 그 점 직전 창 길이만큼의 평균 변화율이 된다. 그래서 창은 저역 통과 필터로 동작한다. 20분짜리 급증을 한 시간 창으로 보면 값이 대략 3분의 1로 눌리고, 대신 급증이 창 안에 들어오는 시간만큼 좌우로 번져 폭이 넓어진다. 높이와 폭이 바뀌지만 아래 면적은 거의 보존된다 — 그래서 "총 몇 건" 은 맞는데 "얼마나 심했나" 만 틀리게 된다.

창의 하한도 있다. [Prometheus 의 질의 함수 문서](https://prometheus.io/docs/prometheus/latest/querying/functions/)와 Grafana 는 스크레이프 간격의 네 배 이상을 권한다. [Grafana 의 $__rate_interval](https://grafana.com/docs/grafana/latest/datasources/prometheus/template-variables/) 은 이 규칙을 자동화한 변수로, max($__interval + scrape_interval, 4 * scrape_interval) 로 계산된다. 여기서 $__interval 은 패널의 시간 범위를 패널의 픽셀 폭으로 나눈 값이다. 즉 패널을 넓게 보면 창이 저절로 넓어진다. 같은 패널을 12시간으로 보다가 30일로 바꾸면 봉우리가 스스로 낮아지는 이유가 이것이다.

집계에도 같은 함정이 있다. avg 로 비율들을 평균 내면 트래픽이 적은 시간과 많은 시간이 같은 무게를 갖는다. 전체 비율을 알고 싶으면 합을 합으로 나눠야 한다. 분위수는 더 심하다 — 백분위는 애초에 평균 낼 수 있는 값이 아니다. [히스토그램](https://prometheus.io/docs/practices/histograms/)은 버킷을 먼저 sum by (le) 로 합친 뒤 histogram_quantile 을 불러야 하고, 이미 나온 p99 들을 평균하면 아무 뜻도 없는 숫자가 된다.

increase()rate() 는 창의 양 끝에서 외삽한다. 표본이 창 경계에 정확히 놓이는 일이 드물기 때문인데, 그 결과 increase() 는 요청 수처럼 정수여야 할 값을 20316.33 같은 소수로 돌려준다. 패널에 그대로 띄우면 "0.33건이 뭐냐" 는 질문을 받는다.

| 묻고 싶은 것 | 써야 하는 것 | 흔히 쓰는 것 |
| --- | --- | --- |
| 가장 나빴을 때 얼마였나 | 짧은 창 + 촘촘한 step | 패널 기본값 그대로 |
| 전체 기간 비율 | 합 ÷ 합 | 비율의 평균 |
| 꼬리 지연 | sum by (le)histogram_quantile | 분위수의 평균 |
| 정확한 건수 | 카운터 차이 | increase() 의 소수점 |

현장에서 만나는 모습

한 팀은 30일 대시보드를 사고 회고에 그대로 썼다. 그 패널의 $__rate_interval 은 두 시간이 넘었고, 20분짜리 사고는 선 위의 아주 작은 혹으로만 남아 있었다. 회고 결론은 "영향이 크지 않았다" 였다. 같은 사고를 6시간 범위로 다시 열자 봉우리가 28% 로 서 있었다. 바뀐 것은 브라우저 주소창의 시간 범위뿐이었다.

다른 팀에서는 "지난 밤 실패 건수" 패널이 20316.33 을 띄웠다. 담당자는 소수점을 없애려고 round() 를 씌웠고, 그 뒤로 아무도 그 숫자가 외삽값이라는 사실을 몰랐다. 카운터가 리셋된 구간에서 그 패널은 조용히 틀린 값을 보여 주고 있었다.

다음 실습에서 할 것

구간 쿼리를 던져 점 수·최댓값·임계값 초과 수를 찍는 작은 도구를 만들고, 그 도구로 같은 12시간을 여러 방법으로 물어본다. 창을 15초로 줄여 그래프가 비는 것을 보고, 창을 1분에서 1시간까지 넓혀 봉우리가 낮아지고 폭이 넓어지는 것을 재고, step 에서 $__rate_interval 을 직접 계산해 패널 폭이 창을 정하는 과정을 재현한다. 비율의 평균과 합 ÷ 합을 비교하고, 긴 창의 p99 가 왜 최악의 p99 가 아닌지 확인한 뒤, 마지막에 운영 대시보드에서 베껴 온 망가진 패널 세 개를 고쳐 제출한다 — 채점기가 고친 쿼리를 실제로 던져 값으로 판정한다.