지난주 금요일 새벽 3시, 폰이 미친 듯이 울렸다. LokiIngesterMemoryHigh 알람. 눈이 번쩍 떠졌다.
솔직히 이 알람이 처음 뜬 건 아니었다. 근데 그날은 좀 달랐다. ingester 파드 12개 중 4개가 OOMKilled로 계속 재시작 중이었고, 그 사이에 유입되던 로그는 500 에러 뱉으면서 다 흘려 보내고 있었다. 팀 슬랙에 사고 채널 만들고 노트북 열었을 땐 이미 5분치 로그가 유실된 상태였다.
원인은 결국 stream 카디널리티
Loki 운영해본 사람은 알겠지만, "카디널리티"는 반쯤 저주 같은 단어다. 라벨 하나 잘못 붙이면 몇 시간 뒤에 클러스터가 죽는다. 우리 팀도 그걸 알고 있었고, 그래서 애초에 라벨 조합을 5~6개로 제한하는 룰이 있었다.
문제는 그 룰이 깨진 게 아니었다. 어떤 서비스가 pod name을 라벨로 안 넣었는데, 대신 로그 라인 안에 pod=abcd-1234-xyz9 형태로 문자열을 박고 있었다. 여기까지는 정상. 그런데 새로 배포된 파드가 log-format을 바꾸면서 라벨 하나를 추가했다. deployment_id. 배포 파이프라인이 도는 시점마다 값이 유니크하게 바뀌는 UUID였다.
한 서비스에서 초당 수천 라인을 뱉는데, 라벨 조합이 배포할 때마다 새로 생기니까 active stream이 시간당 수만 개씩 계속 늘어났다. TSDB index를 쓰는 우리 클러스터 입장에서는 그냥 metadata가 폭발한 거다.
level=warn msg="stream limit exceeded"
tenant=team-payments
reason="Maximum active stream limit exceeded"
streams=125430
이 로그가 초당 수천 건씩 찍히고 있었다. 그 때서야 "아, 이거 라벨 문제구나" 감이 왔다.
급한 불부터 끄기
새벽 3시에 근본 원인 잡을 여유는 없었다. 일단 트래픽을 살리는 게 먼저였다. 아래 세 가지를 순서대로 했다.
첫째, 문제 tenant에만 max_global_streams_per_user를 임시로 크게 올렸다. 이렇게 하면 카디널리티는 더 올라가지만 최소한 500 에러는 안 뜬다. 유실 대신 지저분한 인덱스를 택한 거다.
overrides:
team-payments:
max_global_streams_per_user: 500000
ingestion_rate_mb: 40
ingestion_burst_size_mb: 60
둘째, ingester 메모리를 임시로 2배 올렸다. HPA가 걸려있긴 했지만 memory 기반이 아니라 CPU 기반이었어서 그날은 스케일이 안 붙었다. 이건 사실 이번 사고 회고 때 팀에서 크게 지적된 부분이기도 하다.
셋째, 문제 로그 소스에 임시 relabel_config를 붙여서 deployment_id 라벨을 그냥 drop 해버렸다. Promtail에서 처리했는데, 지금은 Alloy로 옮기는 중이라 alloy 설정이랑 promtail 설정을 둘 다 관리하고 있어 이게 좀 짜증났다.
- action: labeldrop
regex: deployment_id
30분 정도 지나니까 파드가 안정화됐고 유입도 정상으로 돌아왔다. 새벽 4시쯤 되어서 잠시 숨을 돌릴 수 있었다.
근본 원인은 사실 두 개였다
월요일에 팀 포스트모템 돌렸는데, 원인이 두 개로 나왔다.
하나는 물론 서비스 팀의 라벨 실수. 이건 명확한 실수고 코드 리뷰에서 걸렸어야 했다. 근데 우리 팀 쪽에도 문제가 있었다. 카디널리티 폭발을 감지하는 알람이 너무 늦게 떴다는 거다. LokiIngesterMemoryHigh는 이미 상황이 다 터진 뒤에나 뜨는 알람이었고, 진짜로 필요했던 건 LokiActiveStreamsGrowthRate 같은 선행 지표였다.
Loki 3.4부터 __time_shard__ 자동 주입으로 stream이 1시간 이상 길어지지 않게 하는 기능이 들어왔는데, 우리는 아직 3.2를 쓰고 있어서 이 안전장치가 없었다. 업그레이드 우선순위를 이번 분기에 넣기로 했다.
알람 쪽은 이렇게 바꿨다.
- alert: LokiStreamGrowthAnomaly
expr: |
(
sum by (tenant) (rate(loki_ingester_streams_created_total[5m]))
/
sum by (tenant) (rate(loki_ingester_streams_created_total[1h] offset 1h))
) > 5
for: 10m
labels:
severity: warning
지난 1시간 대비 최근 5분 stream 생성률이 5배 이상 튀면 경고. 이게 완벽하진 않은데, 최소한 새벽 3시가 아니라 새벽 1시 반쯤 감지는 될 것 같다. 아직 튜닝 중이라 threshold는 좀 더 조정할 예정이다.
배운 것
라벨 표준 문서 아무리 잘 써놔도 사람은 실수한다. 그리고 그 실수가 정말로 클러스터를 죽인다. Loki를 운영한다는 건 결국 "카디널리티 폭발이 언제 어디서 올지 모른다는 걸 전제로 방어선을 몇 겹 쌓아두는 일"인 것 같다. tenant 격리, per-tenant 리밋, 선행 알람, 자동 샤딩, 그리고 사람이 개입할 수 있는 시간을 벌어주는 회로 차단기.
혹시 Loki에서 카디널리티 사고 겪어본 분들, 어떻게 감지하고 대응하시는지 궁금하다. 우리 팀에서도 아직 정답을 못 찾은 느낌이라, 다른 팀 경험담이 있으면 좀 배우고 싶다.
'IT > 모니터링' 카테고리의 다른 글
| OTel Collector 파이프라인, 사실 내부는 이렇게 돌아간다 (0) | 2026.08.27 |
|---|---|
| Prometheus 라벨 카디널리티가 왜 죽음인가 — TSDB 내부 관점에서 (0) | 2026.08.24 |
| Grafana Agent에서 Alloy로 넘어가면서 밟은 지뢰들 (0) | 2026.08.19 |
| Fluent Bit vs Vector, 로그 수집기 뭘 쓸까 (0) | 2026.08.15 |
| Grafana Alloy vs OpenTelemetry Collector, 우리 팀은 뭘 골랐나 (0) | 2026.08.14 |