LabHub

Loki — 로그를 색인하지 않는 로그 저장소 · 설계 결정 · 이론

색인하지 않는다는 선택

LabHub 에서 이어서 보기

한 줄 요약

Loki 는 로그 본문을 색인하지 않고 라벨 집합만 색인한다. 그래서 저장이 싸고, 대신 검색은 "라벨로 후보를 좁힌 뒤 본문을 훑는" 방식이 된다.

왜 이게 필요했나

Elasticsearch/OpenSearch 는 로그의 모든 토큰을 역색인에 넣는다. 그래서 "error 라는 단어가 든 로그" 를 즉시 찾는다. 대가는 크다 — 색인이 원본보다 커지는 일이 흔하고, 색인 작업 자체가 CPU 와 메모리를 많이 쓴다.

Loki 는 다르게 물었다. "우리가 로그를 찾을 때 실제로 어떻게 하는가?"

대개는 이렇다. "결제 서비스의, 프로덕션의, 지난 30분" 으로 범위를 좁힌 다음 그 안에서 문자열을 찾는다. 앞의 좁히기는 라벨 몇 개면 되고, 뒤의 훑기는 그 범위가 작으면 빠르다.

그래서 Loki 는 라벨만 색인하고 본문은 압축해 덩어리(chunk)로 저장한다. 프로메테우스와 같은 라벨 모델을 쓰는 것도 의도적이다 — 지표에서 이상을 발견하고 같은 라벨로 로그로 넘어갈 수 있다.

어떻게 동작하나

{namespace="labhub-prod", app="labhub"} |= "error" | json | status >= 500 ^--------- 라벨 셀렉터 (색인됨) -------^  ^--- 본문 필터 (훑음) ---^

앞의 중괄호가 스트림을 고른다. 라벨 조합 하나가 스트림 하나다. 뒤의 파이프가 그 스트림들의 본문을 순차로 훑는다.

성능은 전적으로 앞에서 얼마나 좁히느냐에 달렸다. 라벨 셀렉터가 스트림 10개를 고르면 빠르고, 10만 개를 고르면 느리다.

카디널리티 — Loki 에서 더 치명적이다

프로메테우스에서 라벨 카디널리티가 문제인 것은 알려져 있다. Loki 에서는 더 나쁘다.

라벨 조합 하나마다 별도의 스트림과 별도의 청크 파일이 생긴다. trace_iduser_id 를 라벨로 넣으면 스트림이 수백만 개가 되고, 각각이 작은 청크 파일을 만든다. 저장소가 작은 파일로 뒤덮이고, 쿼리는 그 파일들을 전부 열어야 한다.

규칙: 라벨은 값의 종류가 적고 잘 안 변하는 것만.

| 라벨로 좋은 것 | 라벨로 나쁜 것 |
|---|---|
| namespace, app, pod, level, env | trace_id, user_id, request_id, ip, 타임스탬프 |

나쁜 것들은 본문에 남기고 필요할 때 |=| json 으로 찾는다. 그게 Loki 의 설계 의도다.

OpenSearch 와 어디서 갈리나

| 질문 | 어느 쪽 |
|---|---|
| "지난 30분 이 파드의 에러" | Loki — 라벨로 좁혀지고 범위가 작다 |
| "지난 3개월 전체에서 이 주문번호" | OpenSearch — 좁힐 라벨이 없고 범위가 크다 |
| "에러 메시지별 빈도 상위 20" | OpenSearch — 집계가 필요하다 |
| "지표에서 본 이상 시점의 로그" | Loki — 프로메테우스와 라벨이 같다 |

둘은 경쟁 관계가 아니라 다른 질문에 답한다. 실제로 둘 다 두는 곳이 많다 — Loki 로 최근 것을 싸게, OpenSearch 로 오래된 것을 조사 가능하게.

흔한 착각

"Loki 가 무조건 싸다" — 라벨을 잘못 붙이면 OpenSearch 보다 나빠질 수 있다. 스트림 폭발은 되돌리기도 어렵다(옛 청크가 그대로 남는다).

"grep 이면 충분하다" — 로그가 여러 노드에 흩어져 있고 파드가 죽으면 사라진다. 모으는 이유는 검색이 아니라 보존이다.

실무에서 진짜 중요한 것

Loki 쿼리가 느릴 때 먼저 볼 것은 선택된 스트림 수다.

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

이 값이 크면 라벨 셀렉터를 더 좁히거나, 애초에 라벨 설계가 잘못된 것이다. 쿼리 튜닝이 아니라 수집 설정을 고쳐야 한다.