LogQLは三つの部分でできている
한국어 원문으로 표시합니다.
한 줄 요약
LogQL 쿼리는 스트림 셀렉터 → 로그 필터 → 집계의 세 부분이다. 성능은 거의 전적으로 첫 부분이 결정한다.
왜 이게 필요했나
Loki 쿼리가 느리다는 신고의 대부분은 쿼리 문법이 아니라 범위를 안 좁힌 것이다. 앞의 중괄호가 스트림 10만 개를 고르면 뒤에서 무엇을 해도 느리다.
세 부분
sum by (pod) (
rate({namespace="labhub-prod", app="labhub"} |= "error" | json | status >= 500 [5m])
)
^--집계--^ ^------- 스트림 셀렉터 -------^ ^------ 로그 필터 ------^
1) 스트림 셀렉터 {...} — 색인된 라벨로 후보를 고른다. 반드시 하나 이상 있어야 하고, 등호 매처(=)가 정규식(=~)보다 훨씬 싸다.
2) 로그 필터 |= != |~ !~ — 고른 스트림의 본문을 훑는다. 순서가 중요하다.
# 나쁨 — 파싱부터 하고 거른다
{app="labhub"} | json | level = "error"
# 좋음 — 문자열로 먼저 줄이고 파싱한다
{app="labhub"} |= "error" | json | level = "error"
|= 는 단순 부분 문자열 검사라 아주 싸다. | json 은 줄마다 파싱하므로 비싸다. 싼 것을 먼저 두면 파싱할 줄 수가 크게 줄어든다.
3) 집계 — rate, count_over_time, sum by 등. 여기서 로그가 지표가 된다.
# 초당 오류 수를 파드별로
sum by (pod) (rate({app="labhub"} |= "error" [5m]))
# 오류 메시지 상위 10개
topk(10, sum by (msg) (count_over_time({app="labhub"} | json [1h])))
파서 세 가지
| 파서 | 언제 |
|---|---|
| ` | json` |
| ` | logfmt` |
| ` | pattern` |
| ` | regexp` |
파싱해서 만든 필드는 색인되지 않는다. 그래서 | status >= 500 은 훑기지 색인 조회가 아니다. 자주 쓰는 값이면 수집 시점에 라벨로 승격하는 것을 고려하되, 카디널리티를 먼저 계산한다.
흔한 착각
"라벨을 늘리면 쿼리가 빨라진다" — 라벨 하나를 늘리면 스트림 수가 그 라벨의 값 개수만큼 곱해진다. status 를 라벨로 넣으면 스트림이 5배(2xx/3xx/4xx/5xx/기타)가 되고, user_id 를 넣으면 사용자 수만큼이 된다. 스트림이 많아지면 모든 쿼리가 느려진다.
"|~ 정규식이 편하다" — |~ "error|warn" 대신 |= "error" or |= "warn" 이 훨씬 싸다. 정규식은 줄마다 엔진을 돌린다.
쿼리가 도는 순서를 알면 비용이 보인다
LogQL 은 파이프라인이고, 앞에서 줄이는 것이 언제나 가장 쌉니다.
{namespace="labhub-prod", app="api"} ← 1. 색인으로 스트림을 고른다 (거의 공짜)
|= "timeout" ← 2. 원문 문자열 필터 (싸다)
| json ← 3. 파싱 (비싸다)
| status >= 500 ← 4. 파싱된 필드로 필터
| line_format "{{.msg}}" ← 5. 출력 성형
순서를 바꾸면 값이 달라집니다. | json 을 먼저 하고 |= "timeout" 을 나중에
하면, 모든 줄을 파싱한 뒤에 버리는 셈입니다. 문자열 필터를 최대한 앞으로
당기는 것이 LogQL 튜닝의 첫 번째 규칙입니다.
지표로 바꾸기 — 로그에서 그래프를 뽑는다
로그를 세면 지표가 됩니다. 계측이 없는 시스템에서 특히 유용합니다.
# 5분간 5xx 비율
sum(rate({app="api"} |= "HTTP/1.1" 5" [5m]))
/
sum(rate({app="api"} [5m]))
# 파싱한 필드로 p95 지연
quantile_over_time(0.95,
{app="api"} | json | unwrap duration_ms [5m]) by (route)
다만 이것을 상시 대시보드에 두면 안 됩니다. 로그 스캔은 시계열 조회보다 훨씬 비쌉니다. 값이 확인되면 애플리케이션에 진짜 지표를 넣고, 로그 기반 쿼리는 조사할 때만 씁니다. 로그로 만든 지표는 "지표를 넣을 가치가 있는지" 를 알아보는 정찰 도구입니다.
보존과 압축을 미리 정한다
Loki 는 로그를 청크로 묶어 객체 저장소에 넣습니다. 여기서 두 가지를 정해야 합니다.
- 보존 기간. 30일이 흔한 출발점입니다. 규제가 있으면 감사 로그만 따로 긴
보존 정책을 겁니다(
retention_stream으로 라벨별 지정). - 청크 크기와 유휴 시간. 스트림이 조용하면 작은 청크가 많이 생겨 조회가
느려집니다.
chunk_idle_period를 늘리면 청크가 커지지만 최신 로그가 검색에 나타나는 시각이 늦어집니다.
경보에 쓰는 쿼리는 짧은 범위(5~15분)로 두고, 조사용 긴 범위 쿼리와 분리합니다. 같은 쿼리를 1분마다 24시간 범위로 돌리면 비용이 조용히 커집니다.
실무에서 진짜 중요한 것
쿼리가 느릴 때 먼저 재야 할 것은 선택된 스트림 수다.
count(count by (pod, container) ({namespace="labhub-prod"}))
Grafana 의 쿼리 통계(Query inspector)에도 Total bytes processed 가 나온다. 그 값이 GB 단위면 범위가 너무 넓은 것이다. 시간 범위를 줄이거나 라벨을 하나 더 거는 것이 쿼리 튜닝보다 효과가 크다.