Alertmanager Routing Fixes to Cut Prometheus Alert Fatigue
Alertmanager 라우팅 수정으로 Prometheus 알림 피로 줄이기
새벽 3시에 같은 장애를 두고 수십 개의 알림이 울리는 알림 피로(alert fatigue)는 Prometheus 규칙 문제가 아니라 Alertmanager 라우팅 설정 문제인 경우가 많습니다. 이 글은 group_by 조정, inhibition 규칙 추가, repeat_interval 재설정 같은 구체적인 설정 수정과 함께 amtool을 이용한 검증 방법과 재발 방지 체크리스트를 제시합니다.
- 주제
본문
새벽 3시에 하나의 노드에서 발생한 디스크 압력 이벤트 때문에 수십 개의 알림이 세 팀의 휴대폰으로 동시에 울리고, 누군가 알림을 읽었을 때는 이미 디스크가 복구된 뒤라 인시던트 채널에는 아무도 믿지 않는 'ack, resolved' 메시지만 가득한 상황이 바로 알림 피로입니다. 원문은 Alertmanager가 그룹핑(grouping), 억제(inhibition), 사일런스(silence)라는 수단을 갖추고 있음에도 팀들이 여전히 알림에 파묻히는 것은 대개 Prometheus 규칙이 아니라 라우팅 설정의 문제라고 지적합니다.
실패 시나리오
60개 노드에 15개의 exporter를 운영하는 중간 규모 플랫폼 팀을 가정합니다. 디스크 공간, 메모리 압력, 파드 재시작을 다루는 새 rules.yml을 추가했지만 alertmanager.yml의 route 블록은 2년 전 튜토리얼에서 복사한 기본 설정, 즉 단일 receiver와 기본 group_by, inhibition 규칙 없음 상태 그대로입니다. 실제 노드 장애가 나면 kubelet이 NotReady를 보고하고 node exporter가 DiskPressure와 MemoryPressure를 발화하며 그 노드의 모든 파드가 재시작되는데, 각 조건이 별도 알림 규칙에 매핑됩니다. 모든 라벨로 그룹핑하는 기본값 때문에 거의 동일한 알림도 병합되지 않아, 장애 노드 하나를 다루는 그룹 메시지 대신 개별 알림 다발이 전송됩니다. 수주간 반복되면 엔지니어들은 pager 앱 알림음 자체를 꺼버리고, 정작 진짜 인시던트가 배경 소음과 동일하게 취급되는 최악의 실패 모드에 도달합니다.
발생 원인
원문은 네 가지 원인이 서로를 증폭시킨다고 설명합니다. 첫째, 그룹핑이 너무 세분화되거나 없는 경우입니다. group_by를 비워 두면 모든 라벨 조합이 별도 알림이 되고, group_by에 alertname을 넣으면 같은 노드의 DiskPressure와 MemoryPressure조차 다른 이름이라 영원히 합쳐지지 않습니다. 해결책은 alertname을 빼고 cluster, instance 같은 스코프 라벨로 그룹핑하는 것입니다. 둘째, inhibition 규칙 부재입니다. inhibition은 상위 심각도 알림이 발화 중일 때 하위 알림을 억제하는 기능인데, 이것이 없으면 NodeDown과 파드 재시작·서비스 불가·고지연 같은 증상 알림이 모두 병렬로 발화합니다. 셋째, severity 라벨의 불일치입니다. 모든 규칙이 severity: warning이면 '사람을 깨울 것'과 '업무 시간에 확인할 것'을 구분할 수 없고, 뒤늦게 severity를 도입하면서 기존 규칙을 backfill하지 않으면 절반만 올바르게 라우팅되는 더 나쁜 상태가 됩니다. 넷째, repeat_interval 기본값입니다. 실제 평균 해결 시간보다 짧으면 미해결 인시던트가 매시간 재통지되어 스팸 취급받습니다. 자식 route의 repeat_interval은 독립적으로 덮어쓸 수 있지만 group_interval은 명시하지 않으면 부모를 상속합니다.
수정 방법
첫 단계는 그룹핑과 타이밍을 조이는 것입니다. 예시 설정은 group_by를 ['cluster', 'namespace', 'instance']로 지정해 규칙 이름이 아니라 스코프로 상관관계를 묶고, group_wait 30초, group_interval 5분, repeat_interval 4시간을 적용합니다. 서브 라우트로 severity="critical"은 pagerduty-oncall receiver에 group_wait 10초, repeat_interval 1시간으로 보내고, warning은 slack-warnings에 repeat_interval 12시간으로, 상시 발화하는 Watchdog은 null-receiver로 보내 파이프라인 생존 확인만 하고 아무도 호출하지 않게 합니다. 같은 노드의 DiskPressure와 MemoryPressure는 한 메시지로 묶이되 관련 없는 노드의 알림은 스코프 라벨이 달라 별도 그룹이 됩니다. 두 번째는 inhibit_rules 추가로, NodeDown이 severity="warning" 알림을 equal: ['node'] 조건으로 억제하고, KubeAPIDown이 KubePodCrashLooping이나 KubeDeploymentReplicasMismatch를 equal: ['cluster']로 억제하는 예시를 듭니다. 단 equal에 지정한 라벨이 source와 target 간에 실제로 공유되는지 스키마를 확인해야 하며, 맞지 않으면 규칙이 조용히 아무것도 억제하지 않습니다. 배포 전에는 amtool config routes test --config.file=alertmanager.yml로 대표 라벨 집합을 검증하고 amtool check-config로 문법·참조 오류를 잡으라고 권합니다. matchers 문법은 Alertmanager 0.22 이상이 필요하며 구형 match/match_re도 동작은 하지만 권장되지 않습니다.
예방 체크리스트
원문은 재발 방지를 위해 다음을 제시합니다. 새 규칙 작성 전 critical·warning·info 수준의 severity 분류 체계를 표준화하고 각 등급의 대응 시간을 문서화합니다. 스코프로 그룹핑하되 alertname을 group_by에 넣을지 의식적으로 결정합니다. 새 '근본 원인' 알림을 추가할 때마다 inhibition 규칙을 함께 추가하고 equal 라벨 공유 여부를 확인합니다. repeat_interval은 전역이 아니라 severity 등급별로 설정합니다. 분기별로 페이징 도구에서 alertname별 알림 수를 뽑아 노이즈 감사를 하고, 설정 변경 시 항상 amtool로 테스트하며, 유지보수 기간에 만든 silence가 인시던트 이후에도 남아 알림을 무기한 무력화하지 않는지 함께 감사합니다. 원문은 이 모든 것이 새 도구 없이 한 번 쓰고 잊어버린 설정을 다시 살피는 작업일 뿐이라고 마무리합니다.
원문: dev.to / 번역·요약: Trawling