LabHub

Loki — 로그를 색인하지 않는 로그 저장소 · LogQL 로 묻는다 · 이론

LogQL 은 세 부분이다

LabHub 에서 이어서 보기

한 줄 요약

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 | 로그가 JSON 일 때. 중첩은 field_sub 로 평탄해진다 |
| | logfmt | key=value key2=value2 형식 |
| | pattern | 고정 위치 필드. <_> <method> <path> <status> 처럼 |
| | regexp | 마지막 수단. 가장 비싸다 |

파싱해서 만든 필드는 색인되지 않는다. 그래서 | status >= 500 은 훑기지 색인 조회가 아니다. 자주 쓰는 값이면 수집 시점에 라벨로 승격하는 것을 고려하되, 카디널리티를 먼저 계산한다.

흔한 착각

"라벨을 늘리면 쿼리가 빨라진다" — 라벨 하나를 늘리면 스트림 수가 그 라벨의 값 개수만큼 곱해진다. status 를 라벨로 넣으면 스트림이 5배(2xx/3xx/4xx/5xx/기타)가 되고, user_id 를 넣으면 사용자 수만큼이 된다. 스트림이 많아지면 모든 쿼리가 느려진다.

"|~ 정규식이 편하다"|~ "error|warn" 대신 |= "error" or |= "warn" 이 훨씬 싸다. 정규식은 줄마다 엔진을 돌린다.

실무에서 진짜 중요한 것

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

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

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