LabHub
배우기 러닝패스 코스

Observability

The same outage was invisible on the dashboard

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

같은 12시간 자료에 같은 질문을 던지되 rate 창과 step 만 바꿔서 답이 어떻게 달라지는지 숫자로 재고, 운영 대시보드에서 베껴 온 망가진 패널 세 개를 고쳐 제출합니다.

왜 중요한가

대시보드를 믿는다는 것은 그 패널이 어떤 요약을 하고 있는지 안다는 뜻이어야 한다. 그런데 대부분의 패널은 누군가 한 번 만들어 둔 뒤로 아무도 쿼리를 열어 보지 않는다. 창이 한 시간이면 20분짜리 사고는 3분의 1 높이로 눌려 나타나고, 창이 스크레이프 간격보다 짧으면 자료가 있는데도 화면에 '데이터 없음' 이 뜬다. 비율을 평균하면 새벽 한 시간과 낮 한 시간이 같은 무게를 갖고, 분위수를 평균하면 아무 뜻도 없는 숫자가 나온다. 이 실습의 목적은 PromQL 을 더 배우는 것이 아니라, 패널 하나를 보고 '이 그림은 무엇을 지웠는가' 를 물을 수 있게 되는 것이다.

단계

  1. /root/obs-graph-traps/qr.py 를 만드세요. python3 qr.py '<PromQL>' <step초> [<범위 시간>] [<임계값>] 으로 부르면 /api/v1/query_rangestart=지금-범위·end=지금·step=step초 를 주어 한 번 던지고, points=<돌아온 값의 총 개수> max=<최댓값> over=<임계값보다 큰 값의 개수> 한 줄을 출력합니다. 범위 기본값은 12(시간), 임계값 기본값은 0.01 입니다. 값이 하나도 없으면 points=0 max=NA over=0 을 찍습니다. 최댓값은 소수 여섯 자리까지 적습니다.
  2. 5xx 비율을 묻는 식 sum(rate(http_requests_total{job="shop-api",status="500"}[창])) / sum(rate(http_requests_total{job="shop-api"}[창])) 을 창만 바꿔 네 번 던지세요. 창은 15s·30s·1m·5m, step 은 60, 범위는 12시간입니다. /root/obs-graph-traps/window-points.tsv 에 머리글 없이 네 줄, 각 줄은 탭으로 나눈 두 칸 <창> <점 수> 입니다. 그리고 /root/obs-graph-traps/02-why.txtreason= 으로 시작하는 한 줄을 40자 이상으로 적어, 가장 짧은 창에서 점이 없는 이유를 설명하세요.
  3. 같은 5xx 비율 식을 창 1m·5m·15m·30m·1h 로 다섯 번 던지세요(step 60, 범위 12시간, 임계값 0.01). /root/obs-graph-traps/window-shape.tsv 에 머리글 없이 다섯 줄, 각 줄은 탭으로 나눈 세 칸 <창> <최댓값> <0.01 을 넘은 점 수> 입니다. 최댓값은 소수 여섯 자리, 점 수는 정수입니다.
  4. Grafana 는 $__rate_intervalmax($__interval + 스크레이프간격, 4 * 스크레이프간격) 으로 계산합니다. 이 파드의 스크레이프 간격은 15초입니다. step 이 15·108·600·3600 초일 때의 rate 창을 각각 계산한 뒤, 그 창으로 5xx 비율 식을 같은 step 으로 던지세요(범위 12시간). /root/obs-graph-traps/rate-interval.tsv 에 머리글 없이 네 줄, 각 줄은 탭으로 나눈 네 칸 <step초> <rate 창 초> <점 수> <최댓값> 입니다. 그리고 /root/obs-graph-traps/04-note.txtreason= 으로 시작하는 한 줄을 40자 이상으로 적어, step 3600 에서만 같은 쿼리를 다시 던질 때마다 최댓값이 달라지는 이유를 설명하세요.
  5. 같은 12시간의 5xx 비율을 두 가지로 구하세요. 하나는 1분 간격 비율의 단순 평균 avg_over_time((sum(rate(http_requests_total{job="shop-api",status="500"}[5m])) / sum(rate(http_requests_total{job="shop-api"}[5m])))[12h:1m]), 다른 하나는 트래픽으로 가중된 전체 비율 sum(increase(http_requests_total{job="shop-api",status="500"}[12h])) / sum(increase(http_requests_total{job="shop-api"}[12h])) 입니다. /root/obs-graph-traps/weighted.txt 에 세 줄을 적으세요 — avg_of_ratio=<소수 여섯 자리>, weighted_ratio=<소수 여섯 자리>, gap_pct=<소수 두 자리>. gap_pct 는 (앞의 값 − 뒤의 값) ÷ 뒤의 값 × 100 입니다.
  6. histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{job="shop-api"}[창]))) 을 창 5m·15m·12h 로 세 번 던지세요(step 60, 범위 12시간). /root/obs-graph-traps/p99-window.tsv 에 머리글 없이 세 줄, 각 줄은 탭으로 나눈 두 칸 <창> <12시간 최댓값> 입니다. 그리고 /root/obs-graph-traps/06-note.txtreason= 으로 시작하는 한 줄을 40자 이상으로 적어, 왜 긴 창의 분위수가 짧은 창 분위수들의 최댓값보다 작아지는지 설명하세요.
  7. /opt/lab/graph/panels.yml 의 패널 p1 은 '지난 12시간 5xx 건수' 를 묻는데 소수점이 붙은 값을 냅니다. /root/obs-graph-traps/fix-count.promql 에 같은 12시간의 5xx 건수를 정수로 내는 PromQL 을 쓰세요(주석 # 은 남겨도 됩니다). 그리고 /root/obs-graph-traps/count-panel.txt 에 두 줄을 적으세요 — broken_value=<소수 세 자리> 는 p1 의 식이 지금 내는 값, fixed_value=<정수> 는 고친 쿼리가 내는 값입니다. 채점기는 고친 쿼리를 실제로 던져 값을 봅니다.
  8. /opt/lab/graph/panels.yml 의 패널 p2(오류 비율)와 p3(p99 지연)는 각각 '가장 높았을 때 얼마였나' 에 답하지 못합니다. /root/obs-graph-traps/fix-ratio.promql/root/obs-graph-traps/fix-p99.promql 에 고친 쿼리를 쓰세요. 그리고 /root/obs-graph-traps/verdict.tsv 에 머리글 없이 두 줄, 각 줄은 탭으로 나눈 네 칸 <패널id> <고치기 전 12시간 최댓값> <고친 뒤 12시간 최댓값> <무엇이 틀렸었나> 입니다. 첫 줄은 p2, 둘째 줄은 p3 이고, 최댓값은 step 60·범위 12시간으로 재며 소수 여섯 자리입니다. 넷째 칸은 20자 이상이고 숫자를 하나 이상 포함해야 합니다.

참고

구간 쿼리를 숫자로 요약하는 도구를 만든다

/root/obs-graph-traps/qr.py 를 만드세요. python3 qr.py '<PromQL>' <step초> [<범위 시간>] [<임계값>] 으로 부르면 /api/v1/query_rangestart=지금-범위·end=지금·step=step초 를 주어 한 번 던지고, points=<돌아온 값의 총 개수> max=<최댓값> over=<임계값보다 큰 값의 개수> 한 줄을 출력합니다. 범위 기본값은 12(시간), 임계값 기본값은 0.01 입니다. 값이 하나도 없으면 points=0 max=NA over=0 을 찍습니다. 최댓값은 소수 여섯 자리까지 적습니다.

구간 응답은 data.result[].values[][1] 에 문자열로 들어 있습니다. 시계열이 여러 개면 모두 합쳐 세세요. NaN 문자열이 섞여 올 수 있으니 math.isnan 으로 걸러야 최댓값이 망가지지 않습니다. 표준 라이브러리만 씁니다(urllib.request, json, time, math).

빈 그래프는 트래픽이 없다는 뜻이 아니다

5xx 비율을 묻는 식 sum(rate(http_requests_total{job="shop-api",status="500"}[창])) / sum(rate(http_requests_total{job="shop-api"}[창])) 을 창만 바꿔 네 번 던지세요. 창은 15s·30s·1m·5m, step 은 60, 범위는 12시간입니다. /root/obs-graph-traps/window-points.tsv 에 머리글 없이 네 줄, 각 줄은 탭으로 나눈 두 칸 <창> <점 수> 입니다. 그리고 /root/obs-graph-traps/02-why.txtreason= 으로 시작하는 한 줄을 40자 이상으로 적어, 가장 짧은 창에서 점이 없는 이유를 설명하세요.

이 파드의 스크레이프 간격은 15초입니다. rate 는 창 안에 표본이 둘 이상 있어야 변화율을 낼 수 있고, 하나뿐이면 오류가 아니라 빈 결과를 돌려줍니다. 그래서 공식 문서는 창을 스크레이프 간격의 네 배 이상으로 잡으라고 합니다. 백필된 구간의 해상도는 30초라 30초 창도 거의 비어 있습니다.

창을 넓히면 봉우리는 낮아지고 폭은 넓어진다

같은 5xx 비율 식을 창 1m·5m·15m·30m·1h 로 다섯 번 던지세요(step 60, 범위 12시간, 임계값 0.01). /root/obs-graph-traps/window-shape.tsv 에 머리글 없이 다섯 줄, 각 줄은 탭으로 나눈 세 칸 <창> <최댓값> <0.01 을 넘은 점 수> 입니다. 최댓값은 소수 여섯 자리, 점 수는 정수입니다.

step 을 60 으로 고정했으니 한 점이 곧 1분입니다. 셋째 칸은 '그래프가 말하는 사고 길이(분)' 로 읽으면 됩니다. 높이가 몇 분의 일로 줄어드는 동안 폭이 몇 배로 늘어나는지 두 방향을 같이 보세요.

패널 폭이 창을 정한다 — $__rate_interval 을 손으로 계산한다

Grafana 는 $__rate_intervalmax($__interval + 스크레이프간격, 4 * 스크레이프간격) 으로 계산합니다. 이 파드의 스크레이프 간격은 15초입니다. step 이 15·108·600·3600 초일 때의 rate 창을 각각 계산한 뒤, 그 창으로 5xx 비율 식을 같은 step 으로 던지세요(범위 12시간). /root/obs-graph-traps/rate-interval.tsv 에 머리글 없이 네 줄, 각 줄은 탭으로 나눈 네 칸 <step초> <rate 창 초> <점 수> <최댓값> 입니다. 그리고 /root/obs-graph-traps/04-note.txtreason= 으로 시작하는 한 줄을 40자 이상으로 적어, step 3600 에서만 같은 쿼리를 다시 던질 때마다 최댓값이 달라지는 이유를 설명하세요.

$__interval 은 패널의 시간 범위를 픽셀 폭으로 나눈 값이라, 여기서는 step 이 곧 $__interval 입니다. 108 은 12시간 패널을 400픽셀로 그릴 때 나오는 값이고 3600 은 훨씬 넓은 범위를 볼 때 나옵니다. 창은 3615s 처럼 초 단위 문자열로 그대로 넣으면 됩니다. 격자는 start 에서 시작해 step 간격으로 놓이는데 start 는 '지금' 을 따라 움직입니다 — step 이 사고 길이보다 길어지면 격자점이 봉우리의 어디에 떨어지느냐가 최댓값을 바꿉니다.

비율의 평균은 전체 비율이 아니다

같은 12시간의 5xx 비율을 두 가지로 구하세요. 하나는 1분 간격 비율의 단순 평균 avg_over_time((sum(rate(http_requests_total{job="shop-api",status="500"}[5m])) / sum(rate(http_requests_total{job="shop-api"}[5m])))[12h:1m]), 다른 하나는 트래픽으로 가중된 전체 비율 sum(increase(http_requests_total{job="shop-api",status="500"}[12h])) / sum(increase(http_requests_total{job="shop-api"}[12h])) 입니다. /root/obs-graph-traps/weighted.txt 에 세 줄을 적으세요 — avg_of_ratio=<소수 여섯 자리>, weighted_ratio=<소수 여섯 자리>, gap_pct=<소수 두 자리>. gap_pct 는 (앞의 값 − 뒤의 값) ÷ 뒤의 값 × 100 입니다.

두 값은 promq 로 바로 던져 볼 수 있습니다. 앞의 식은 트래픽이 적은 새벽 1분과 붐비는 낮 1분을 같은 무게로 세고, 뒤의 식은 요청 수만큼 무게를 줍니다. 사고가 한산한 시간에 났다면 어느 쪽이 커질지 먼저 생각해 보세요.

긴 창의 p99 는 가장 나빴던 p99 가 아니다

histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{job="shop-api"}[창]))) 을 창 5m·15m·12h 로 세 번 던지세요(step 60, 범위 12시간). /root/obs-graph-traps/p99-window.tsv 에 머리글 없이 세 줄, 각 줄은 탭으로 나눈 두 칸 <창> <12시간 최댓값> 입니다. 그리고 /root/obs-graph-traps/06-note.txtreason= 으로 시작하는 한 줄을 40자 이상으로 적어, 왜 긴 창의 분위수가 짧은 창 분위수들의 최댓값보다 작아지는지 설명하세요.

히스토그램 버킷도 카운터라 rate 의 창이 그대로 적용됩니다. 창이 길면 느린 요청과 빠른 요청이 한 통에 섞여 꼬리가 희석됩니다. 그래서 '지난 12시간 p99' 패널 하나로 '가장 나빴을 때 얼마였나' 에 답할 수 없습니다. 임계값은 기본값을 그대로 두면 됩니다 — 이 단계에서 보는 것은 최댓값뿐입니다.

응용 ① — 건수 패널이 소수점을 보여 준다

/opt/lab/graph/panels.yml 의 패널 p1 은 '지난 12시간 5xx 건수' 를 묻는데 소수점이 붙은 값을 냅니다. /root/obs-graph-traps/fix-count.promql 에 같은 12시간의 5xx 건수를 정수로 내는 PromQL 을 쓰세요(주석 # 은 남겨도 됩니다). 그리고 /root/obs-graph-traps/count-panel.txt 에 두 줄을 적으세요 — broken_value=<소수 세 자리> 는 p1 의 식이 지금 내는 값, fixed_value=<정수> 는 고친 쿼리가 내는 값입니다. 채점기는 고친 쿼리를 실제로 던져 값을 봅니다.

increase()rate() 는 표본이 창 경계에 정확히 놓이지 않기 때문에 양 끝에서 외삽합니다. 그래서 정수여야 할 건수가 소수로 나옵니다. 정확한 건수를 원하면 외삽하지 않는 방법으로 물어야 합니다 — 카운터의 지금 값과 12시간 전 값의 차이를 생각해 보세요. offset 수식어가 도움이 됩니다.

응용 ② — 오류 비율과 p99 패널을 고쳐 제출한다

/opt/lab/graph/panels.yml 의 패널 p2(오류 비율)와 p3(p99 지연)는 각각 '가장 높았을 때 얼마였나' 에 답하지 못합니다. /root/obs-graph-traps/fix-ratio.promql/root/obs-graph-traps/fix-p99.promql 에 고친 쿼리를 쓰세요. 그리고 /root/obs-graph-traps/verdict.tsv 에 머리글 없이 두 줄, 각 줄은 탭으로 나눈 네 칸 <패널id> <고치기 전 12시간 최댓값> <고친 뒤 12시간 최댓값> <무엇이 틀렸었나> 입니다. 첫 줄은 p2, 둘째 줄은 p3 이고, 최댓값은 step 60·범위 12시간으로 재며 소수 여섯 자리입니다. 넷째 칸은 20자 이상이고 숫자를 하나 이상 포함해야 합니다.

p2 는 식이 틀린 것이 아니라 창이 너무 깁니다. p3 은 두 군데가 틀렸습니다 — 창도 길고, 이미 나온 분위수들을 평균하고 있습니다. 히스토그램은 버킷을 먼저 le 별로 합친 뒤 분위수를 구해야 합니다. 고친 값은 3단계·6단계에서 이미 본 숫자와 같아야 정상입니다.