常駐プロセスが落ちたら気づきたい。素直に書くとこうなる。
# 5分ごとに動かす
if not process_is_alive():
notify("プロセスが落ちています")
これは動く。そして一番困る状況で、一番静かになる。
| 障害 | この監視 | デッドマン装置 |
|---|---|---|
| プロセスだけ落ちた | 検知できる | 検知できる |
| マシンがスリープした | 検知できない | 検知できる |
| マシンが落ちた・ネットが切れた | 検知できない | 検知できる |
監視スクリプトが同じ機械に載っている以上、機械ごと死んだら監視も死ぬ。
そして監視が死ぬと、通知は来ない。静かになる。
この記事は、その素直な監視をノートPCに24時間載せて失敗し、
実測で稼働率33%だと分かってから設計を直すまでの話。
① 向きを反転する —— 「落ちたら鳴らす」ではなく「生きていたら送る」
デッドマン装置(dead man's switch)にする。
自分 : 正常なときだけ、外部へ ping を送る
外部 : 予定の ping が来なかったら通知する
判定を外に置くのがすべてで、これで「自分が死んだこと」を自分で言わなくてよくなる。
import urllib.request
PING_URL = "https://hc-ping.com/<uuid>" # healthchecks.io など
def watchdog() -> int:
if not process_is_alive():
return 1 # **異常なら、何もしない。** pingを送らない
try:
urllib.request.urlopen(PING_URL, timeout=10)
except OSError as e:
# ping が送れないこと自体は自分では解決できない。**黙らない**
log(f"ping送信に失敗: {e}")
return 1
return 0
ここで大事なのは 異常時に「異常です」と送らないこと。
送る仕組みに依存すると、送れない状況(=一番困る状況)で通知が消える。
沈黙そのものを信号にする。
② ノートPCに24時間監視は成立しない(実測)
反転させて満足していたら、今度は通知が止まらなくなった。
原因はマシンが寝ることだった。フタを閉じれば寝るし、AC電源でも寝る。
sleep 0 はアイドルスリープの設定で、クラムシェルスリープには効かない。
5日ぶんを実測した数字がこれ。
| 実測 | |
|---|---|
| ping の稼働率 | 33% |
| 15分を超える断絶 | 55件 |
| 「監視が動いていない」通知(このままなら) | 110通 |
最初は「ミュートが効かない」問題に見えた。だが調べると、通知は全部正しかった。
監視は本当に止まっていた。バグは通知側ではなく、「24時間動いている」という前提の側にあった。
通知がうるさいとき、まず黙らせ方を探さない。
その通知が正しいなら、直すのは受け手ではなく期待のほう。
③ 期待を実態に合わせる —— 監視する窓を絞る
監視に意味がある時間帯だけを窓にする。私の場合、対象プロセスが要るのは
特定の時間帯と、月のうち数日だけだった。
WATCH_START, WATCH_END = 22, 7 # JST 22:00 〜 翌07:00
WATCH_DAYS = range(1, 7) # **カレンダー日**の 1〜6日
def in_window(now: datetime) -> bool:
if now.day not in WATCH_DAYS:
return False
h = now.hour
return h >= WATCH_START or h < WATCH_END # 日付をまたぐ窓
def watchdog(now: datetime) -> int:
if not in_window(now):
return 0 # 接続もせず、pingも送らず、**ログも書かない**
...
窓の外でログも書かないのは、書くと日に190行たまって、
夜間の記録が押し流されるからだ。監視のログが監視の役に立たなくなる。
これで実行回数が年3.3万回から約20分の1になった。
月中に落ちても構わない。次に困るのは翌月で、その前の窓で捕まる。
失うのは長期の稼働データだけ。
④ ここで踏んだ罠 —— カレンダー日と「セッション日」
窓を日付でも絞るとき、「セッション日」で実装して外した。
対象は夜からの時間帯なので、「7時より前は前日扱い」にしたくなる。
だが cron はカレンダー日で撃つ。 ずれる。
| 時刻 | cron が ping を期待するか | セッション日で実装した場合 |
|---|---|---|
| 6日 22:00 | する | 前日扱いにならず対象外 → 送らない → アラート |
| 1日 00:30 | する | 前月末扱い → 送らない → アラート |
cron 側とコード側で「日」の定義が違うと、必ずどこかで噛み合わない。
撃つ側に合わせる。つまりカレンダー日。
送りすぎは無害、送らなすぎだけがアラートになる
デッドマン装置の性質として、余分な ping は害がない(外部のタイマーが延びるだけ)。
一方で ping が1つ足りないと通知が飛ぶ。非対称なので、迷ったら広い側に倒す。
これはテストで固定できる。1年分を総当たりして、
cron が期待する時刻を1つも落としていないことを確かめる。
from datetime import datetime, timedelta
def cron_fires(t: datetime) -> bool:
"""crontab: */5 22,23,0-6 1-6 * * を素直に書き下したもの。"""
return (t.day in range(1, 7) and t.minute % 5 == 0
and (t.hour >= 22 or t.hour <= 6))
def test_窓はcronが撃つ時刻を1つも落とさない():
t = datetime(2026, 1, 1)
missed = []
while t < datetime(2027, 1, 1):
if cron_fires(t) and not in_window(t):
missed.append(t) # cronは撃つのに送らない = アラートになる
t += timedelta(minutes=5)
assert missed == [], f"送らない時刻が {len(missed)}件: {missed[:3]}"
逆(窓の中なのに cron が撃たない)は落とさない。 送りすぎは無害だからだ。
テストの非対称性が、設計の非対称性と一致している。
⑤ 監視側と外部側のスケジュールは「対」
窓を変えたら、外部サービス側のスケジュールも同時に変える。
片方だけ変えると、どちらに倒しても悪くなる。
| 変えたほう | 起きること |
|---|---|
| コードだけ | 日中の ping が来ないので、外部が障害と判定して通知が増える |
| 外部だけ | 窓の外の ping で「生きている」と誤判定される |
healthchecks.io なら Simple スケジュールでは表現できないので Cron にする。
Schedule : */5 22,23,0-6 1-6 * *
Timezone : Asia/Tokyo
Grace : 20 minutes
この2つが対になっていることは、コード側からはテストできない。
向こう側は外部サービスの設定だからだ。
だから**「片方だけ変えない」をコメントとREADMEに書いて、人間の側で守る。**
テストで守れないものは、守れないと認めて別の手を打つ。
⑥ 監視を止める手段も、監視と対で要る
うるさいときに黙らせる口は要る。ただし必ず期限付きにする。
def muted_until() -> datetime | None:
"""**無期限ミュートを作らない。** 消し忘れると監視が永久に死ぬ。"""
...
def notify(msg: str) -> None:
until = muted_until()
if until and datetime.now() < until:
log(f"ミュート中({until:%H:%M}まで): {msg}") # **捨てずに記録は残す**
return
send(msg)
無期限のミュートは、外れていることを教えてくれない安全装置になる。
前に書いた「その安全装置、一度も作動したところを見ていないのでは」
と同じ形だ。
まとめ
-
監視は自分の外に置く。「落ちたら鳴らす」ではなく「生きていたら送る」。
沈黙そのものを信号にする - 異常時に通知を送ろうとしない。 送る仕組みごと死ぬのが一番困る状況
- 「24時間動いている」を、寝る機械に前提として載せない。 実測したら33%だった
- 通知がうるさいとき、黙らせ方から探さない。 その通知が正しいなら、直すのは期待のほう
- cron とコードで「日」の定義を揃える。 撃つ側(カレンダー日)に合わせる
- 送りすぎは無害、送らなすぎだけがアラート。 非対称なら広い側に倒し、テストもそう書く
- コードと外部設定は対。 テストで守れないものは、守れないと認めて文書と手順で守る
- ミュートは必ず期限付き。 無期限は「外れていることを教えてくれない安全装置」
監視を作った日に、一度スリープさせてみるといい。
何も鳴らなかったら、それが答えだ。
note でも書いています
このシリーズ以外に、AIエージェントの運用から広がった政治・経済・メディア構造の話を
note で書いています。