감시 스크립트 monitor-a.php 를 검토하다가 같은 계열의 monitor-b.php 에서 정반대 성격의 결함을 발견했다. 원래 고치려던 것은 알람이 조용히 멎는 문제였는데 그 수정이 반대편으로 넘어가 있었다.
Table of contents
Open Table of contents
배경 — 변화 게이트와 침묵 버그
이 계열의 감시는 멤버 상태를 모아 error_ratio 를 계산하고 last_ok_at 과 비교해 상태가 변했는지 판정한다. 같은 상태가 이어지면 알리지 않는 변화 게이트가 알림 피로를 막는 장치였다.
문제는 그 상태를 메모리에만 들고 있으면 프로세스가 재시작될 때 사라진다는 점이었다. 장애가 이어지는 중인데 기준 시각이 초기화되어 알람이 조용히 멎는다.
원인 — 만료 없는 영속화가 만든 폭주
그래서 상태를 monitor_states 테이블에 저장하도록 고쳤고 재시작해도 last_ok_at 이 남게 됐다. 침묵은 사라졌지만 expires_at 을 걸지 않은 것이 다음 문제를 만들었다.
전 멤버가 계속 오류인 상황에서는 error_ratio 가 1.0 에 고정되고 last_ok_at 이 영구히 보존되므로 게이트가 항상 참이 된다. 주기마다 알람이 발화해 나흘이면 천 건이 넘고 그만한 알림은 아무도 보지 않으므로 0건과 다르지 않다.
스스로 적어 둔 상한을 넘기는 비용
이 스크립트는 특정 비율 구간에서 유료 판정 API 를 호출하는데 비율이 최댓값에 고정되면 매 주기 호출된다. 스크립트 주석에는 하루 스무 건 남짓을 가정한 월 비용 상한이 적혀 있었다.
실제로는 하루 수백 건이 되므로 이틀이면 그 상한을 넘는다. 자기 문서에 적어 둔 제한을 자기 동작이 깨는 구조였다.
대안 — 옆 스크립트에는 있던 장치
monitor-a.php 를 열어 보니 한 번 알린 뒤 suppress_until 까지 다시 알리지 않는 재알림 억제 창이 있었다. 같은 문제를 이미 막아 둔 것인데 이쪽으로는 이관되지 않았다.
더 나쁜 것은 그 스크립트의 주석이 monitor-b.php 에는 억제가 필요 없고 변화 게이트와 비율이 자연히 제한한다고 주장하고 있었다는 점이다. 다른 파일에 대해 주장하는 주석은 읽는 사람이 확인된 사실로 받아들이게 만든다.
결론 — 짝으로 도입해야 하는 장치들
그 주장을 그대로 믿지 않고 해당 코드를 직접 열어 last_ok_at 이 만료 없이 보존되므로 게이트가 항상 참임을 확인했다. 비용 수치도 기억이 아니라 스크립트 원문의 해당 행을 열어 대조했다. 두 가지 방법이 있었는데 expires_at 만료만 거는 것과 suppress_until 억제 창까지 짝으로 넣는 것이다. 뒤쪽을 골랐다. 이유는 만료만 걸면 만료 직후 재시작이 겹칠 때 침묵이 다시 열려서다. 대신 억제 창 동안 새 장애가 겹쳐도 한 번만 알리는 것을 감수했다.
영속화만 넣으면 폭주하고 억제만 넣으면 재시작 시 침묵하므로 둘은 반드시 한 쌍으로 들어가야 한다. 재시도와 백오프나 캐시와 만료처럼 한쪽을 넣을 때 짝이 있는지 확인하는 것이 이 사례의 일반 규칙이었고 실제 배포 여부에 따라 실재 리스크인지 잠재 리스크인지가 갈리므로 그 확인을 결론에 함께 넣었다.
정리
- 만료 없는 상태 영속화는 침묵을 폭주로 바꾼다
- 알림 천 건은 0건과 같다
- 주기마다 붙는 외부 호출이 비용 상한을 깬다
- 다른 파일에 대해 주장하는 주석은 위험하다
- 주장은 그 코드를 직접 열어 확인한다
- 수치도 원문 행으로 교차 확인한다
- 배포 여부에 따라 실재와 잠재가 갈린다
expires_at영속화와suppress_until억제는 한 쌍으로 도입한다