前回、自作監視カメラの現在形を紹介して「この構成に至るまでに検知が 4 回死んだ」と書いた。今回はその障害編。
4 回の死はぜんぶ同じ顔をしていた。カメラは映っている。コンテナも動いている。CPU も GPU も静かで、ダッシュボードはどこも赤くない。ただ検知イベントだけが止まっていて、Echo は鳴らず、スマホにも何も来ない。外形上は健全な「サイレント死」。気づくのは決まって「そういえば最近通知が来ないな」と思った時で、その時点で半日から丸一日が失われている。
そして真因は、4 回とも違った。
事件簿 1: 13時間の沈黙と、最初の誤認逮捕
ある朝、前日の夕方から人物検知が 13 時間ゼロになっていることに気づいた。stats を見ると署名はこうだった。
camera_fps = 5.0 # カメラ→ffmpeg は生きているprocess_fps ≈ 0 # 動体処理ループが止まっているdetection_fps = 0.0 # 検知ゼロinference = 数ms # 検知器自体は健全に見えるログには 20 秒周期でこれが延々と続いていた。
Detection appears to be stuck. Restarting detection process...当時この GPU には LLM ランタイムが同居していて、早朝に segfault を繰り返した形跡があった。「同居プロセスが GPU コンテキストを壊した」——それらしい説明だ。実際、LLM 側を止めたら検知は安定した(ように見えた)ので、私は犯人を LLM ランタイムと記録して寝た。
翌日の追検証でこの判決は覆る。真犯人は自作の watchdog だった。検知の生存判定に latest.jpg の更新を使う版が動いていて、動きのない深夜の庭では latest.jpg は更新されない。つまりアイドルな夜を「ストリーム凍結」と誤判定して、2〜4 分ごとにコンテナを再起動し続けていた。再起動のたびに重い初回推論が走り、それを今度は Frigate 内部の watchdog が stuck と誤判定して kill する。監視のための道具が、二重に毒を盛っていた。
しかも修正版の watchdog は 6 日前に書いて push 済みだったのに、GitOps のデプロイが黙って止まっていて実機に届いていなかった。「直したはずのバグに殺され続けていた」わけで、この日からデプロイの着地確認も監視対象になった。
なお「LLM ランタイム犯人説」には後日談がある。番外編まで覚えておいてほしい。
事件簿 2: カメラが自分で壊れる
事件簿 1 の裏でもうひとつ交絡していたのが、ストリーム自体の破損だった。ログに壊れた HEVC NAL のエラーが散発していて、コーデックや WiFi 帯域を疑ったが、決着をつけたのは対照実験だった。検知を止めてメインストリームを単独で pull すると 150 フレーム連続エラーゼロ。同時接続に戻すと破損が再現する。
つまりこの価格帯の WiFi カメラの SoC は、4MP H265 のメインストリームを複数セッションで同時配信できない。録画・検知・HA のライブ表示がそれぞれカメラに直接ぶら下がる構成は、それ自体が破損の生成装置だった。
前回の記事に書いた「カメラへの接続は 1 本に絞る」の鉄則は、この事件の戦訓だ。
事件簿 3: 59秒 vs 20秒
前日に watchdog を退治して安心していたら、翌朝また終日検知ゼロ。今度の署名は奇妙だった。モデルのロードは 0.3 秒で成功し、そのちょうど 20 秒後に必ず stuck 判定で殺される。プロセスのカーネルスタックを読むと、capture も process も detector も全員 futex_wait で誰かを待っている。crash した者はいない。全員無実の顔をしたデッドロック。
watchdog の外で同じモデルを直接推論してみて、ようやく正体が見えた。
CUDAExecutionProvider: first=59.4s second=2.9ms初回推論だけが 59 秒かかる。 onnxruntime が新世代 GPU (Blackwell/sm_120) のビルド済みカーネルを同梱しておらず、初回に全カーネルを PTX→SASS で JIT コンパイルするためだ。ところが Frigate の検知 watchdog は 20 秒でハードコードされている。warmup は毎回 20 秒で殺される。
さらに底意地が悪いのは、CUDA の JIT キャッシュがコンテナの rootfs 上にあってコンテナ再生成のたびに消えること。しかも warmup が完走する前に殺されるのでキャッシュは永遠に書き込まれない。毎回ゼロから 59 秒 → 毎回 20 秒で死亡、の完全な無限ループ。「昨日まで動いていた」のは、たまたまキャッシュが温まった起動を掴んでいただけだった。
修正は 1 行。CUDA_CACHE_PATH を永続ボリュームに向ける。初回だけ JIT が走り、以降は 59.4 秒が 0.3 秒になって watchdog の懐に収まる。デプロイ直後に検知は復活し、以後この障害は再発していない。
事件簿 4: 引っ越し先での 22 時間
その後、検知は専用機(例のラップトップ NVR)へ引っ越した。4 回のうち 3 回は GPU サーバ同居時代の話で、引っ越しは同居起因の層を確かに断った。だが 4 回目は、その引っ越し先で起きた。カメラ由来の層は、箱を替えても付いてくる。平和は 2 週間だけ。今度は 22 時間のサイレント死。
因果の連鎖はドミノ倒しだった。①H265 ストリームが稀に破損して ffmpeg がストール → ②graceful exit を待つ間に capture プロセスが 13.4GB までメモリ暴走 → ③16GB・スワップゼロの箱なのでカーネル OOM killer が発動 → ④OOM の直後、内部 watchdog が「ffmpeg の終了待ち」のまま黙って固まり、パイプラインがデッドロック。camera_fps は 5 のまま、inference も正常値のまま、イベントだけが 22 時間ゼロ。
腹立たしいのは、既存のアラート 3 本が全部素通りしたことだ。camera_fps 監視は「fps が 1 分未満しか落ちなかった」ので発火せず。inference 監視は「正常値のまま」なので発火せず。再起動ループや wedge を狙った process_fps 監視も「今回 process_fps は生きていた」ので発火せず。アラートは前回の犯人の顔しか覚えていない。
対処はコンテナに cgroup のメモリ上限(暴走してもコンテナ内だけで刈られる)、ホストにスワップ、そして OOM カウンタの監視。今回の型なら数分でページが飛ぶ。
番外: 判決後の再鑑定
7 月に入って、この GPU サーバの DRAM が物理故障していたことが確定した。memtest 開始 1 分で 705 エラー。故障域は 43〜56GB 帯——つまり大容量確保をした時だけ踏む場所だ。
となると、6 月の判決文を読み直さないわけにはいかない。壊れた RAM の上で行われた障害調査を、どこまで信用していいのか。
再鑑定の結果、事件簿 1 で一度は「同居の LLM ランタイムのせい」とした segfault 群は、RAM の余罪が濃厚になった。数十 GB のモデルロードは故障域を踏みに行く行為そのものだし、以降のカーネルクラッシュも LLM プロセスに集中していた。一方、watchdog の自家中毒(ログに残っている)、カメラの同時セッション限界(対照実験済み)、59 秒 JIT(watchdog の外で再現実測済み)は RAM がどれだけ化けても揺るがない。
介入・対照・再現測定のある結論だけが、壊れたハードの上から持ち帰れる。 時間相関ともっともらしい物語だけの結論は、あとで静かに覆る。
同じ症状、毎回違う真因
4 つの事件を並べると、嫌な法則が見える。
- 症状は毎回「検知イベントだけが止まる」で完全に同一
- 真因は watchdog 設計ミス / カメラのハード限界 / 上流ライブラリと新 GPU の組み合わせ / メモリ枯渇と、毎回まったく別の層
- そして監視は毎回、前回の障害に合わせて張ったところの隣を抜かれた
それでも収穫はある。死ぬたびにアラートが 1 本ずつ賢くなって、無音の時間は 13 時間 → 22 時間(引っ越し直後の油断)→ 今は数分で気づける見込みになった。検知の切り分け手順も runbook に固まって、次の死は初動 10 分で層が特定できる。
監視カメラは、設置したら終わりではなかった。玄関を見張るシステムを、今度は私が見張っている。 うちで一番手のかかる住人は、監視カメラである。