IT/모니터링

Prometheus remote_write 큐가 막혀서 Thanos에 메트릭이 사라졌던 새벽

gfrog 2026. 6. 5. 09:42
반응형
SMALL

 

지난주 화요일 새벽 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_seconds vs prometheus_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시에 깨달은 교훈이다. 다음에는 부디 자다가 일어나지 않기를.

반응형
LIST