최근 Observability 스택에서 Loki + Grafana + Tempo + Prometheus 조합이 많이 사용되고 있다.
하지만 처음 Loki를 접하면 이런 의문이 생긴다.
이 글에서는 Loki의 핵심 개념인
를 중심으로 Loki의 설계 철학을 정리한다.
로그 시스템은 크게 두 가지 방향으로 설계된다.
목표
로그를 자유롭게 빠르게 검색하는 것
예 로그
{
"service": "order",
"tenantId": "acme",
"userId": "42",
"trace_id": "abc",
"message": "order created"
}
Elasticsearch는 대부분의 필드를 index한다.
index
├ service
├ tenantId
├ userId
├ trace_id
└ message
그래서 이런 검색이 가능하다.
tenantId = acme AND status = FAILED
userId = 42
trace_id = abc
보통
로그 1GB
→ index 포함 3~5GB 저장
Loki의 철학은 완전히 다르다.
로그를 최대한 싸게 저장하는 것
Loki는 index를 최소화한다.
그래서 로그는 이렇게 저장된다.
{labels} log line
예
{service="order"} order created
Loki는 label만 index한다.
index
├ service
├ namespace
├ cluster
나머지 데이터는 log line으로 저장된다.
Loki를 이해하려면 다음 개념을 알아야 한다.
Label
Stream
Chunk
Label은 로그의 메타데이터이다.
예
{service="order", pod="order-1"}
이 label들은 Loki에서 index 역할을 한다.
그래서 쿼리는 항상 label로 시작한다.
{service="order"}
이렇게 하면 Loki는
service=order
인 로그만 찾는다.
Loki에서 가장 중요한 개념은 stream이다.
정의
같은 label 조합을 가진 로그들의 그룹
예
{service="order", pod="order-1"}
이 label을 가진 로그들은 모두 같은 stream에 속한다.
stream
├ log
├ log
├ log
label이 달라지면 새로운 stream이 생성된다.
{service="order", pod="order-1"}
{service="order", pod="order-2"}
stream1
stream2
로그는 stream 내부에서 chunk 단위로 저장된다.
stream
└ chunk
├ log
├ log
├ log
보통
1 chunk ≈ 1MB
chunk는 압축되어 object storage(S3 등)에 저장된다.
Loki 쿼리는 다음 단계로 동작한다.
{service="order"}
→ order 관련 stream 찾기
{service="order", pod="order-1"}
{service="order", pod="order-2"}
선택된 stream의 chunk를 읽는다.
|= "error"
같은 조건을 적용한다.
Loki에서 가장 중요한 개념은 cardinality이다.
정의
값의 종류 개수
예
service
├ order
├ payment
├ user
값이 적다.
trace_id
user_id
request_id
값이 매우 많다.
trace_id는 요청마다 생성된다.
예
trace_id
a1
a2
a3
a4
만약 trace_id를 label로 사용하면
{trace_id=a1}
{trace_id=a2}
{trace_id=a3}
요청마다 새로운 stream이 생긴다.
즉
요청 수 = stream 수
이 현상을
stream explosion
이라고 한다.
세 가지 문제가 발생한다.
Loki는 active stream을 메모리에 유지한다.
stream이 많아지면 메모리가 터진다.
stream마다 chunk가 생긴다.
예
1KB chunk
2KB chunk
3KB chunk
원래는
1MB chunk
여야 한다.
그래서 storage 효율이 나빠진다.
로그 쓰기 과정에서
stream lookup
이 계속 발생한다.
stream이 많아지면 ingestion 속도가 떨어진다.
Loki에서 label은 low cardinality만 사용해야 한다.
좋은 label
service
namespace
cluster
pod
나쁜 label
trace_id
user_id
request_id
session_id
| 항목 | Elasticsearch | Loki |
|---|---|---|
| 목표 | 검색 | 저장 |
| index | 많음 | 최소 |
| 비용 | 높음 | 낮음 |
| 검색 자유도 | 높음 | 제한적 |
| 운영 난이도 | 높음 | 낮음 |
Loki가 잘 맞는 경우
Elasticsearch가 잘 맞는 경우
Loki는 Elasticsearch보다 검색 기능이 약한 대신 저장 비용이 훨씬 낮다.
핵심 차이는 이것이다.
Elasticsearch → 검색 최적화
Loki → 저장 최적화
그래서 Loki에서는
low cardinality label
설계가 매우 중요하다.