서비스 9개가 동시에 죽었는데 status 페이지는 장애 9건이라고 했다
10월 7일에 서비스 9개가 80분 동안 같이 응답하지 않았다. 다운 메일은 하나도 못 나갔고 복구 메일만 9통이 왔다. 같은 시간대의 실패를 공통 장애로 묶어 status 페이지에 따로 보여주게 바꿨다.
#모니터링 #장애대응 #운영 #Node.js #React
10월 7일 저녁에 메일함을 열었더니 모니터링 서버가 보낸 메일이 9통 와 있었다. 전부 복구 메일이었다. 커플앱 복구, 블로그 복구, 여행 API 복구, 이런 식으로 서비스 9개가 2분 사이에 줄줄이 복구됐다고 한다. 근데 다운 메일은 한 통도 없었다. 모니터링 100일 회고 에 고쳐야 할 것으로 "다 같이 실패하면 서비스 장애가 아니다"를 적어뒀는데, 그게 3주 만에 실제로 터진 거다. 이 글에서는 그날 무슨 일이 있었는지, 그리고 status 페이지가 이걸 어떻게 보여주도록 바꿨는지 정리해본다. 그날 기록 알림 기록 테이블부터 봤다. triggered_at rule_name message notified 2026-10-07 16:40:52 커플앱 API 다운 (연속 2회 실패) 0 2026-10-07 16:43:51 게임 다운 (연속 2회 실패) 0 2026-10-07 16:43:51 여행 API 다운 (연속 2회 실패) 0 2026-10-07 16:44:25 코인 트래커 다운 (연속 2회 실패) 0 2026-10-07 16:45:17 이음 다운 (연속 2회 실패) 0 2026-10-07 16:47:56 블로그 다운 (연속 2회 실패) 0 2026-10-07 16:48:00 모니터링 다운 (연속 2회 실패) 0 2026-10-07 16:48:01 taro 다운 (연속 2회 실패) 0 2026-10-07 16:49:42 답정너(fishing) 다운 (연속 2회 실패) 0 2026-10-07 17:58:14 커플앱 API 복구 (다운타임 약 82분) 1 ... 2026-10-07 18:01:15 답정너(fishing) 복구 (다운타임 약 84분) 1 9분 사이에 서비스 9개가 차례로 다운 판정을 받았고, 80분쯤 뒤에 3분 사이에 전부 복구됐다. notified 가 다운 쪽은 전부 0, 복구 쪽은 전부 1이다. 다운 메일은 하나도 안 나갔다는 뜻이다. 에러 로그에 이유가 있었다. [Scheduler] healthcheck 답정너(fishing): connect ETIMEDOUT [Health] down mail error: connect ENETUNREACH (메일 서버):587 ENETUNREACH 는 네트워크 자체에 닿을 수 없다는 에러다. 서버가 바깥으로 나가는 길이 끊겨 있었던 거다. 헬스체크는 서비스를 실제 도메인으로 부르니까 바깥을 한 바퀴 돌아서 들어와야 하는데 그게 안 되니 전부 타임아웃이 났고, 다운 메일도 같은 길로 나가야 하니 못 나갔다. 길이 다시 열리자 복구 메일만 한꺼번에 나간 거다. 서비스들은 그동안 멀쩡히 떠 있었다. pm2 jlist 로 각 프로세스가 마지막으로 시작된 시각을 보면 블로그는 10월 1일, 커플앱은 9월 22일, taro는 8월 18일부터 한 번도 재시작되지 않았다. 앱 9개가 동시에 죽은 게 아니라 서버의 네트워크가 끊긴 거다. status 페이지는 이걸 9건으로 보여줬다 공개 status 페이지에서 10월 7일 막대에 마우스를 올리면 이렇게 나왔다. 블로그만 그런 게 아니라 9개 서비스 막대가 전부 주황색이고, 각자 "응답 시간 초과"가 따로 붙어 있었다. 보는 사람 입장에서는 그날 서비스 9개가 각자 문제를 일으킨 걸로 읽힌다. 나조차도 처음엔 서비스를 하나씩 열어서 로그를 봤다. 실제로는 원인이 하나였는데 화면은 원인이 9개인 것처럼 보여주고 있었다. 과거 기록에도 있었다 이번 한 번만 이런 건지 궁금해서 남아 있는 실패 기록 171건을 시간순으로 쭉 늘어놓고, 서비스와 상관없이 10분 안에 이어지는 실패끼리 묶어봤다. 10월 7일 16:36 ~ 17:55 서비스 9개 실패 134회 응답 시간 초과 9월 24일 04:07 서비스 3개 실패 3회 DNS 조회 실패 8월 14일 02:04 ~ 02:08 서비스 7개 실패 8회 응답 시간 초과 8월 8일 00:47 서비스 4개 실패 4회 응답 시간 초과 6월 20일 18:04 서비스 3개 실패 3회 응답 시간 초과 서비스 세 개 이상이 같이 실패한 게 다섯 번이었다. 8월 14일 건은 100일 회고 때도 봤던 건데, 이번엔 그보다 훨씬 길었다. 나머지 묶음은 대부분 서비스 하나가 혼자 한두 번 실패한 거였다. 어떻게 묶을까 기준은 단순하게 잡았다. 서비스 구분 없이 실패를 시간순으로 이어서, 앞의 실패와 10분 안에 붙어 있으면 같은 묶음으로 본다. 그 묶음에 서비스가 세 개 이상 섞여 있으면 공통 장애다. const COMMON_GAP_SEC = 10 * 60; const COMMON_MIN_SERVICES = 3; function markCommonOutages(failures, nameById) { const sorted = [...failures].sort((a, b) = a.ts - b.ts); const clusters = []; for (const f of sorted) { const last = clusters[clusters.length - 1]; if (last f.ts - last.endTs = COMMON_GAP_SEC) { last.rows.push(f); last.endTs = f.ts; } else { clusters.push({ rows: [f], endTs: f.ts }); } } const outages = []; for (const c of clusters) { const ids = [...new Set(c.rows.map(r = r.endpoint_id))]; if (ids.length COMMON_MIN_SERVICES) continue; for (const r of c.rows) r.common = true; // ... 시작, 끝, 서비스 수, 가장 많은 실패 사유를 모아서 outages 에 넣는다 } return outages.reverse(); } 10분으로 잡은 건 체크 간격 때문이다. 서비스마다 체크 간격이 3분이나 5분이라, 같은 원인으로 실패해도 기록 시각이 몇 분씩 어긋난다. 10월 7일에도 첫 다운 판정과 마지막 다운 판정이 9분 차이였다. 너무 짧게 잡으면 한 사건이 여러 개로 쪼개지고, 너무 길게 잡으면 상관없는 실패까지 붙는다. 세 개로 잡은 건 두 개는 우연히 겹칠 수 있어서다. 기록에도 서비스 두 개만 같이 실패한 묶음이 세 번 있었는데, 그중 둘은 한 번씩 타임아웃이 난 게 우연히 같은 시각에 겹친 정도였다. 세 개 이상이 같은 시간대에 같이 실패하는 건 우연으로 보기 어렵다. 가동률은 건드리지 않았다 여기서 하나 고민했다. 공통 장애로 묶었으니 각 서비스의 가동률에서 빼줘야 하나. 서비스 탓이 아니니까. 근데 그 80분 동안 방문자가 블로그에 못 들어온 건 사실이다. 원인이 앱이든 네트워크든 사용자 입장에서는 똑같이 안 열리는 사이트였다. 공개 status 페이지의 가동률을 원인에 따라 빼주기 시작하면 숫자를 좋게 보이려고 조작하는 거랑 다를 게 없다. 그래서 가동률과 막대 색은 그대로 두고, 이게 공통 장애였다는 설명만 붙이기로 했다. 바뀐 화면 서비스 목록 위에 공통 장애 칸을 따로 만들었다. 언제, 어떤 사유로, 몇 개 서비스가 같이 실패했는지를 한 줄로 보여준다. 최근 5건까지만 보이고, 공통 장애가 없으면 칸 자체가 안 나온다. 각 서비스 막대의 툴팁에도 그 장애가 공통 장애의 일부였으면 표시를 붙였다. 개별 장애와 섞이지 않게, 같은 날이라도 공통 장애에 속한 실패와 아닌 실패는 따로 묶인다. 바꾼 뒤 90일치를 다시 보면 서비스별 장애 39건 중 23건이 공통 장애 4건에 속해 있었다. 화면에 보이던 장애의 절반 넘게가 사실은 4번의 사건이었던 거다. 서비스 10개의 가동률은 바꾸기 전과 소수점 셋째 자리까지 같다. 아직 못 고친 것 이번에 고친 건 보여주는 방식뿐이다. 진짜 문제는 그대로 남아 있다. 서버의 네트워크가 끊기면 알림 메일도 같은 길로 나가야 해서 못 나간다. 이번에도 장애가 난 80분 동안 나는 아무것도 몰랐고, 다 끝난 뒤에 복구 메일 9통으로 알게 됐다. 이건 서버 안에서는 해결이 안 된다. 바깥에서 내 서버를 봐주는 무언가가 있어야 한다. 외부 heartbeat 서비스에 1분마다 신호를 보내고, 신호가 끊기면 그쪽에서 알려주게 하는 방식이 제일 현실적이다. 100일 회고 때도 적었던 건데 이번 일로 우선순위가 확실히 올라갔다. 그리고 복구 메일 9통도 하나로 묶을 수 있다. 공통 장애로 판정된 구간의 복구는 "서비스 9개 복구, 다운타임 약 80분" 한 통이면 충분하다. 이건 다음에 알림 쪽을 손볼 때 같이 할 생각이다. 직접 만든 모니터링이 서비스마다 따로 알림을 보내고 있다면, 여러 서비스가 같은 시각에 실패한 적이 있는지 기록을 한 번 묶어서 보길 추천한다. 서비스 문제처럼 보이던 게 서버나 네트워크 문제였을 수 있다.