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, level 셋일 때는 스트림이 넷이고 각 청크가 꽉 찬 덩어리로 쌓이지만, 거기에 trace_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 이면 충분하다" — 로그가 여러 노드에 흩어져 있고 파드가 죽으면 사라진다. 모으는 이유는 검색이 아니라 보존이다.

무엇을 남기고 무엇을 버릴 것인가

로그는 세 신호 중 가장 싸게 만들어지고 가장 비싸게 보관된다. 그래서 수집 단계에서 양을 줄이는 결정을 하지 않으면, 비용이든 검색 속도든 언젠가 문제가 된다.

버려도 되는 것부터 버린다. 정상 요청 하나마다 남기는 접근 로그는 대개 지표로 대체된다. 초당 요청 수와 상태 코드 분포는 지표가 훨씬 싸게 답하므로, 로그는 정상 흐름에서 벗어난 것에 집중하는 편이 낫다. 헬스 체크와 준비 상태 확인처럼 초당 여러 번 찍히는 것은 특히 먼저 걸러 낼 후보다.

같은 줄이 반복되면 묶는다. 재시도 루프에 빠진 컴포넌트가 초당 수백 줄을 쏟아내는 일이 흔하고, 그 줄들은 전부 같은 내용이다. 애플리케이션 쪽에서 같은 메시지의 반복을 세어 한 줄로 요약하면 양이 자릿수로 줄어든다.

보존 기간을 등급으로 나눈다. 전부를 90일 두는 것보다, 대부분은 짧게 두고 감사가 필요한 것만 길게 두는 편이 훨씬 싸다. 다만 어느 것이 어느 등급인지가 라벨로 구별될 수 있어야 하고, 그것이 앞에서 본 라벨 설계와 이어진다.

그리고 로그의 형식이 곧 나중의 검색 비용이다. 구조화된 형태로 남기면 파싱이 한 번에 끝나고 조건으로 좁힐 수 있지만, 사람이 읽기 좋은 문장으로만 남기면 매번 문자열을 훑어야 한다. 한 줄에 무엇을 넣을지 미리 정해 두는 것이 여기서도 답이고, 앞에서 트레이싱을 다루며 말한 네 가지 — 요청 식별자, 배포 버전, 처리 시간, 실패했을 때의 대상 이름 — 이 로그에서도 그대로 유효하다.

마지막으로 로그가 개인정보를 담고 있는지를 한 번은 점검해야 한다. 요청 본문을 통째로 남기는 코드가 어딘가에 있으면 그 로그 전체가 다른 등급의 자산이 되고, 보존 기간과 접근 권한을 그에 맞게 다시 정해야 한다.

실무에서 진짜 중요한 것

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

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

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