
지난 주말, 새벽 2시쯤 PagerDuty가 울렸다. central monitoring 클러스터의 Prometheus가 OOMKill로 재시작 루프를 돌고 있다는 알람이었다. 메모리 limit을 32Gi로 잡아둔 인스턴스인데, 이게 몇 분 만에 한계를 찍고 죽고 있었다. 멘탈이 좀 흔들렸다. 평소엔 14~16Gi 정도에서 안정적으로 돌던 녀석이었다.
원인을 추적하다 보니 결국 remote_write 큐 동작에 대해 내가 잘못 알고 있었던 부분이 꽤 있었다. 이번 글은 그날 새벽 삽질의 기록이다.
배경: 우리 팀의 metric pipeline
우리는 Thanos 대신 Mimir로 1년 전쯤 옮겼고, 각 워크로드 클러스터의 Prometheus가 remote_write로 Mimir 게이트웨이에 메트릭을 밀어넣는 구조다. 이번 사고가 난 곳은 가장 큰 워크로드 클러스터로, 시리즈 카드널리티가 약 700만 정도, scrape interval은 30s.
평상시 remote_write 큐 동작은 거의 의식하지 않았다. Prometheus 기본값을 거의 그대로 썼고, 그동안 별 문제 없이 돌고 있었으니까. 그런데 이날 새벽에 그게 문제가 됐다.
1차 가설: 카드널리티 폭증?
새벽에 일어나서 처음 의심한 건 카드널리티 폭증이었다. 누가 라벨에 이상한 걸 박았나 싶어서 prometheus_tsdb_head_series 그래프부터 봤는데, 평소랑 거의 같았다. 700만 언저리에서 잔잔하게 움직이고 있었다.
다음으로 본 게 prometheus_remote_storage_samples_pending. 이 값이 평소엔 수만 단위였는데, 사고 시점에는 8천만까지 치솟아 있었다. 큐가 빠져나가질 못하고 있다는 뜻이다.
여기서 한 가지 깨달은 게 있는데, Prometheus 공식 문서에도 적혀 있는 내용이지만 그동안 실감하진 않았던 부분이다. shard 메모리는 대략 샤드 수 × (capacity + max_samples_per_send) 에 비례한다. 우리는 큐가 밀리면서 자동으로 max_shards까지 샤드 수가 올라가 있었고, 그 만큼 큐 메모리를 통째로 점유하고 있었다.
2차 가설: Mimir 쪽이 받지 못하고 있다
up 메트릭으로 Mimir distributor 상태를 확인해보니, 한 두 인스턴스가 멤버십에서 빠져 있었다. consistency hash ring이 흔들리면서 일부 요청이 timeout을 먹고 재전송되고 있었다. 그 재전송이 큐를 더 부풀리고, 부풀어 오른 큐가 메모리를 더 먹고, 결국 OOMKill — 이게 한 사이클로 굴러가고 있었다.
Mimir 쪽 노드 한 대가 디스크 압박으로 응답이 느려진 게 시작점이었다. 거기서 시작된 backpressure가 Prometheus 메모리 한계까지 밀고 들어온 것이다.
근데 솔직히 이게 이렇게까지 클 수 있다는 게 내 멘탈모델에는 없었다. remote_write가 쌓이면 옛날 데이터부터 버린다는 막연한 이미지가 있었는데, 실제로는 큐를 늘려가며 버텨보다가 메모리부터 터진다.
임시 처방과 튜닝
새벽엔 일단 살리는 게 우선이었으므로 두 가지를 했다. Mimir 쪽 문제 노드를 cordon 시키고, Prometheus의 remote_write 설정에서 max_shards를 일시적으로 줄였다.
remote_write:
- url: https://mimir-gw.internal/api/v1/push
queue_config:
capacity: 10000
max_samples_per_send: 2000
max_shards: 50 # 기존 200 → 50으로 일시적으로 다운
min_shards: 4
batch_send_deadline: 5s
max_shards를 줄인다는 건 처리량 상한을 깎는다는 뜻이다. 큐가 잠깐 더 밀릴 수는 있지만, 메모리를 무한히 끌어다 쓰는 걸 막을 수 있다. 우리는 backend가 회복될 때까지 처리량보다 안정성이 더 중요하다고 판단했다.
이 변경 후 메모리 사용량이 22Gi 근처에서 안정화됐고, OOMKill은 멈췄다. 30분쯤 지나서 Mimir 쪽이 회복되니 큐가 다시 빠져나갔다.
사고 후 정리한 것
며칠 뒤 회고에서 몇 가지를 다시 정리했다.
첫째, prometheus_remote_storage_samples_pending 와 prometheus_remote_storage_shards 에 SLO 기반 알람을 새로 걸었다. pending이 백만을 넘기면 warning, 천만 넘기면 page. 이걸 안 걸어둔 게 가장 후회됐다. 사실 이 메트릭들은 처음부터 있었는데 우리 팀은 보고 있질 않았다.
둘째, max_shards의 의미를 팀 내에서 다시 정리했다. 기본값(보통 200)을 그대로 쓰는 게 항상 옳지는 않다. 백엔드의 capacity와 Prometheus 메모리 limit을 모두 고려해서 정하는 값이지, 무조건 크게 둔다고 좋은 게 아니다. 우리는 결과적으로 max_shards를 80 정도로 정착시켰다.
셋째, remote endpoint 장애 시 Prometheus가 어떻게 동작하는지에 대한 멘탈모델을 팀 위키에 글로 정리했다. WAL은 보존되지만 큐는 메모리를 잡아먹으며 버틴다, 라는 한 줄을 머릿속에 박아둘 필요가 있었다.
마무리
remote_write 큐가 이렇게 위험할 수 있다는 걸 사실 한참 늦게 깨달았다. Prometheus 자체의 메모리 모델은 어느 정도 안다고 생각했는데, 큐 동작은 무관심했던 영역이었다.
비슷한 환경에서 운영 중인 분이 있다면, 한 번 prometheus_remote_storage_samples_pending 그래프부터 띄워보길 권한다. 평상시엔 잔잔하지만, 백엔드가 잠깐 비틀거리면 거기서 모든 게 시작된다.
혹시 다른 식으로 이 문제를 다루는 분 계시면 댓글로 알려주세요. 우리 팀도 아직 다 정리한 건 아니라서.
'IT > 모니터링' 카테고리의 다른 글
| Thanos vs Mimir, 둘 다 1년쯤 굴려보고 정리한 트레이드오프 (0) | 2026.05.09 |
|---|---|
| OpenTelemetry Collector 메모리 누수, 며칠 싸운 기록 (0) | 2026.05.08 |
| Vector vs Fluent Bit, 1년 반 운영하다 다시 비교한 이야기 (1) | 2026.05.04 |
| Pyroscope 2.0 + eBPF로 continuous profiling 시작하기 (0) | 2026.05.03 |
| 새벽 2시, Loki 인덱스가 터졌다 (0) | 2026.05.01 |