LabHub
배우기 러닝패스 코스

Loki — ログを索引しないログストア

LogQLは三つの部分でできている

LabHub 에서 이어서 보기

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

한 줄 요약

LogQL 쿼리는 스트림 셀렉터 → 로그 필터 → 집계의 세 부분이다. 성능은 거의 전적으로 첫 부분이 결정한다.

概念マップ: 스트림 셀렉터 → 로그 필터 → 집계・범위를 안 좁힌 것・1) 스트림 셀렉터 {...}・2) 로그 필터 |= != | !

왜 이게 필요했나

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 는 로그를 청크로 묶어 객체 저장소에 넣습니다. 여기서 두 가지를 정해야 합니다.

경보에 쓰는 쿼리는 짧은 범위(5~15분)로 두고, 조사용 긴 범위 쿼리와 분리합니다. 같은 쿼리를 1분마다 24시간 범위로 돌리면 비용이 조용히 커집니다.

실무에서 진짜 중요한 것

쿼리가 느릴 때 먼저 재야 할 것은 선택된 스트림 수다.

count(count by (pod, container) ({namespace="labhub-prod"}))

Grafana 의 쿼리 통계(Query inspector)에도 Total bytes processed 가 나온다. 그 값이 GB 단위면 범위가 너무 넓은 것이다. 시간 범위를 줄이거나 라벨을 하나 더 거는 것이 쿼리 튜닝보다 효과가 크다.