
지난주 화요일 새벽 3시쯤이었다. 슬랙 oncall 채널에 "그라파나 대시보드 일부 패널이 No data로 뜬다"는 핑이 올라왔다. 자다가 받은 알림이라 멘탈이 좀 나간 상태에서 노트북을 열었는데, 진짜로 우리 결제 서비스 P99 레이턴시 그래프가 새벽 1시 47분부터 약 한 시간 동안 비어 있었다.
문제는 Prometheus 자체는 멀쩡했다는 거다. up{} 메트릭도 정상이고 로컬 쿼리도 잘 됐다. 그런데 Thanos를 통해 보던 장기 보관 대시보드에서만 그 구간이 통째로 비어 있었다. 결론부터 말하면 remote_write 큐가 막혀서 WAL에서 읽기가 블록됐고, 그 사이에 들어온 샘플들이 일부 누락됐다. 이게 내가 새벽 4시까지 깨어 있었던 이유다.
처음 의심한 것들 (다 틀렸음)
가장 먼저 의심한 건 Thanos receiver였다. 외부 객체 스토리지로 올라가는 경로에서 문제가 났겠지, 했다. 그래서 receiver 파드 로그부터 봤는데 정상이었다. compaction도 잘 돌고 있었고 S3 PUT 에러도 없었다.
그 다음엔 네트워크를 의심했다. 우리 클러스터는 노드 28대 EKS이고, Prometheus가 다른 VPC에 있는 Thanos receiver로 보내는 구조다. VPC 피어링 구간에 뭔가 일시적인 packet loss가 있었나? CloudWatch에서 봤지만 평소 수준이었다. NAT Gateway 카운터도 정상.
세 번째로 Prometheus 자체 메모리/CPU를 봤다. 이것도 정상 범위였다. 메트릭 카디널리티 폭발인가 싶어서 prometheus_tsdb_head_series 그래프도 봤는데 평소랑 다를 게 없었다.
여기서 한 30분 날렸다. 솔직히 이 시점에서 다시 잘까 진지하게 고민했다.
진짜 원인을 찾기까지
결국 단서를 찾은 건 prometheus_remote_storage_* 메트릭들이었다. Prometheus 자체 메트릭 중에서 remote_write 동작 상태를 보여주는 시리즈가 꽤 자세하게 있다. 그중에 봤어야 했던 건:
prometheus_remote_storage_samples_pending— 큐에서 대기 중인 샘플 수prometheus_remote_storage_shards— 현재 활성 shard 수prometheus_remote_storage_samples_dropped_total— 드롭된 샘플 수prometheus_remote_storage_highest_timestamp_in_secondsvsprometheus_remote_storage_queue_highest_sent_timestamp_seconds— 이 둘의 차이가 곧 lag
문제 구간을 다시 보니까 samples_pending이 평소 200-500 수준에서 5만 가까이 치솟았고, shards는 max_shards 설정값인 30에 도달해서 더 못 늘어났다. 그리고 samples_dropped_total이 야금야금 증가하고 있었다.
# 새벽 사고 구간에서 봤어야 했던 쿼리
rate(prometheus_remote_storage_samples_dropped_total[5m]) > 0
# lag 측정
prometheus_remote_storage_highest_timestamp_in_seconds
- ignoring(remote_name, url) group_right
prometheus_remote_storage_queue_highest_sent_timestamp_seconds
그러니까 Thanos receiver 쪽이 일시적으로 느려졌고 (정확한 이유는 receiver gRPC connection이 일부 끊겼다가 재연결되는 동안 발생한 backlog), 그 동안 Prometheus의 remote_write 큐가 가득 찼고, shard도 max에 도달해서 더 못 확장됐고, 결국 buffer overflow로 일부 샘플이 드롭된 거였다.
여기서 한 가지 짚고 갈 게 있는데, Prometheus의 remote_write는 사실상 backpressure가 제한적이다. WAL에서 큐로 읽는 속도와 큐에서 원격지로 보내는 속도가 어긋나기 시작하면, 큐가 가득 차고, capacity가 꽉 차면 가장 오래된 배치부터 드롭된다. "block read from WAL" 같은 메커니즘이 있긴 하지만 이게 무한정 막아주는 게 아니다. 결국 어느 시점에서는 데이터를 버려야 한다.
어떻게 고쳤나
일단 응급조치로 max_shards를 50까지 올렸다. capacity도 만 단위로 키웠다. 우리 기존 설정은 이랬다:
remote_write:
- url: "https://thanos-receive.internal/api/v1/receive"
queue_config:
capacity: 2500
max_shards: 30
min_shards: 1
max_samples_per_send: 500
batch_send_deadline: 5s
이걸 이렇게 바꿨다:
remote_write:
- url: "https://thanos-receive.internal/api/v1/receive"
queue_config:
capacity: 10000
max_shards: 50
min_shards: 2
max_samples_per_send: 2000
batch_send_deadline: 5s
min_backoff: 30ms
max_backoff: 5s
capacity는 max_samples_per_send의 3-10배가 권장된다는 이야기가 공식 가이드에도 있어서 그 비율을 맞췄다. min_shards를 1에서 2로 올린 건, shard가 0에서 1로 갔다가 다시 0으로 떨어지는 oscillation을 줄이기 위해서다. 평소 트래픽이 한 개 샤드로 처리 가능한 경계선에 있을 때 이게 꽤 거슬리게 동작한다.
그리고 알림을 추가했다. 솔직히 이 알림을 진작에 깔아뒀어야 했는데, 우리는 Prometheus 자체의 상태(up, memory, scrape duration)에만 알림을 걸어놓고 있었지 remote_write 큐 상태에는 알림이 없었다. 이건 명백한 우리 잘못이다.
- alert: PrometheusRemoteWriteHighLag
expr: |
(
prometheus_remote_storage_highest_timestamp_in_seconds
- ignoring(remote_name, url) group_right
prometheus_remote_storage_queue_highest_sent_timestamp_seconds
) > 120
for: 5m
labels:
severity: warning
annotations:
summary: "Prometheus remote_write lag {{ $value }}s"
- alert: PrometheusRemoteWriteSamplesDropped
expr: rate(prometheus_remote_storage_samples_dropped_total[5m]) > 0
for: 2m
labels:
severity: critical
annotations:
summary: "remote_write에서 샘플이 드롭되고 있음"
좀 더 근본적인 고민
응급조치는 했는데 사실 이게 진짜 해결이라고 생각하지는 않는다. 우리 환경에서 평소 발생 메트릭 양과 receiver의 처리 능력 사이에 마진이 별로 없다는 건 알고 있었고, 그 마진이 부족할 때 조용히 데이터가 사라진다는 게 가장 큰 문제다.
올해 들어서 remote_write 2.0 spec이 안정화 단계로 가고 있고 Prometheus 3.0에서 정식 지원된다. 2.0에서는 string interning으로 페이로드 크기가 줄어들고 metadata/exemplars/native histograms가 같이 보내진다. 페이로드가 작아지면 같은 네트워크 대역폭에서 더 많이 보낼 수 있으니까 우리 사고 같은 상황에서 약간 더 버틸 수 있을 것 같긴 한데, 근본적으로 receiver가 느려지면 똑같이 큐가 막히는 문제는 그대로다.
진짜로 검토 중인 건 두 가지다. 하나는 Prometheus agent mode를 써서 scrape와 remote_write만 하는 가벼운 인스턴스를 더 많이 띄우는 것. 다른 하나는 Grafana Mimir나 VictoriaMetrics로 옮겨서 receiver 쪽을 더 잘 확장 가능한 구조로 바꾸는 것. 둘 다 검토 단계고, 우리 팀에서는 다음 분기 시작할 때 PoC를 해보기로 했다.
당장 알아둘 건 이거 하나다. remote_write를 쓰고 있으면 큐 메트릭에 무조건 알림을 걸어둬야 한다. samples_dropped_total이 증가하기 시작하는 순간이 곧 데이터를 잃기 시작하는 순간이다. 그리고 그건 대시보드를 들여다보지 않으면 모른다.
새벽 4시에 깨달은 교훈이다. 다음에는 부디 자다가 일어나지 않기를.
'IT > 모니터링' 카테고리의 다른 글
| OTel Collector에 memory_limiter 안 걸어서 OOM 무한루프 겪은 이야기 (0) | 2026.06.03 |
|---|---|
| Loki structured metadata, 이거 모르면 라벨 카디널리티로 계속 운다 (0) | 2026.06.02 |
| Prometheus native histogram, 사실 내부적으로는 이렇게 동작한다 (0) | 2026.05.31 |
| OTel Collector head sampling vs tail sampling, 우리 팀은 결국 뭘 골랐나 (0) | 2026.05.27 |
| OpenTelemetry Collector가 자꾸 OOM 나서, 결국 memory_limiter와 GOMEMLIMIT을 다시 봤다 (0) | 2026.05.26 |