writing / 2026-09-14
The kernel panic was a bit flip
A Docker host that kept panicking in kswapd looked like it had a kernel bug; a crash dump showed one flipped bit in a page structure, and the next day the same flaky hardware took down the Docker daemon and every container on the box for ten hours.
The homelab’s main Docker host is nemesis, a RHEL 9 virtual machine on a Proxmox host called sienar. When it panics, every site behind it is down until kdump finishes and the VM reboots, about ten minutes each time. Over the summer it panicked a lot.
A pattern that pointed at the kernel
In the first two weeks of August there were nine panics on one kernel build, seven of them the same NULL pointer dereference inside kswapd0, the kernel’s page-reclaim thread. That reads like a kernel bug, and the obvious fix was a kernel update. The update landed on August 24 and the host looked clean for six days.
Then three more events, all on the new kernel:
- August 30: a panic, “stack-protector: Kernel stack is corrupted in futex_parse_waitv”, on a thread belonging to a headless browser.
- September 12: a
refcount_t: underflow; use-after-freewarning while freeing credentials. - September 13: another
kswapd0Oops, this time a bad page fault in a spinlock slow path, reached from the multi-generational LRU (MGLRU) reclaim code.
Memory pressure was not the trigger: the VM was 22% used with swap idle. kswapd runs constantly only because page cache always fills the VM, so it touches more pages than anything else and is the likeliest code to trip over a damaged one.
Three corruption signatures in unrelated subsystems within two weeks is not what one kernel bug looks like.
What the vmcore said
With crash and the matching kernel-debuginfo, the September 13 dump could be walked by hand. Reclaim had taken a lock through folio->mapping->host, and that lock address was inside the dentry slab. Walking back from the folio:
folio->mapping (observed) ffff89417d547db0
real i_data of that inode ffff89417d547df0
difference 0x40, one bit
The observed pointer sits inside a live XFS inode (a 19 GB movie on the media array), and cannot be the i_data of any inode in that slab layout, so it is corrupted, not stale. One bit in a struct page had flipped.
The August 30 dump agreed: the stack protector had fired, yet the stack, task and per-CPU canaries were all identical at dump time. The comparison had read a wrong value once.
The hardware underneath explains why. sienar is an X299 board with an i7-7820X and non-ECC memory (the kernel log says EDAC skx: ECC is disabled on imc 0), and it was running KSM, which merges identical pages across guests. The leading hypothesis became host memory or CPU flakiness, not a RHEL kernel bug.
The next day
On September 14 the same flakiness showed up in user space. dockerd died with a Go runtime fatal error, found pointer to free object, which is the garbage collector noticing heap corruption. The restarted daemon killed every orphaned container: 49 of them across 21 compose projects, including the edge proxy, this site, the media stack and the Discord bot.
restart: unless-stopped brought back exactly one of them.
What it cost
- About ten hours of a dark host, from morning until evening.
- An uptime monitor that could not see it. The monitor builds its container checks dynamically and drops the ones for containers that are not running, so a mass stop looks like fewer monitors, not red ones. Only five static checks went red.
- A straggler that had been down for four weeks. After the September 13 panic, one service was found still
Exited (255)from a crash in August. Neither the restart policy nor autoheal (which only restarts unhealthy running containers) had noticed.
Recovery was manual: each project brought back with docker compose up -d from the directory named in its containers’ compose labels.
What changed
-
Revive stragglers at boot.
scripts/revive-stragglers.shfinds containers that are exited with code 255, have a restart policy ofalwaysorunless-stopped, and are compose-managed, then runsdocker compose up -dfor those services from their own directories. Deliberately stopped stacks exit with other codes and are left alone. A systemd unit runs it two minutes after Docker starts. -
live-restore configured. Docker’s
live-restoresetting keeps containers running across a daemon restart, so the next dockerd crash should not take the containers with it. It is in the daemon config and takes effect at the next daemon reload. -
KSM off on sienar, so one flipped page cannot be shared into another guest.
-
MGLRU switch in the repo. A tmpfiles.d entry disables MGLRU reclaim:
w /sys/kernel/mm/lru_gen/enabled - - - - nIt is not applied yet, and the RCA marks it optional: it removes the code path that tripped twice, but the next flip would just land somewhere else.
-
vmcore pruning. Since late July, a tmpfiles.d rule prunes kdump dumps older than 14 days, so a run of panics cannot fill the 20 GB
/varvolume (each dump is about 1 GB). -
An RCA in the repo, with the dump tables.
What is still open
- Move the nemesis VM to a different Proxmox node for a few weeks as an A/B test. If the panics stop there, sienar is the cause.
- Run memtest86+ on sienar in a maintenance window.
- Add a monitor on the running-container count, so a mass stop is visible even when the per-container checks vanish.
What carries forward
A crash that keeps landing in the same kernel function is not proof of a kernel bug; it can be proof that the function touches the most memory. Different signatures in unrelated code on the same machine point below the software, and a crash dump plus debug symbols can turn that hunch into a one-bit diff. On the Docker side: a restart policy is not a recovery plan after an unclean crash, and a monitor that only watches what is running cannot report what stopped.
カーネルパニックは1ビットのフリップだった
Docker ホストが kswapd で頻繁にパニックを起こしていたため、カーネルのバグのように見えた。クラッシュダンプによると、ページ構造体に1つのビットが反転しており、翌日には同じ不安定なハードウェアが Docker デーモンとそのホスト上のすべてのコンテナを10時間にわたってダウンさせた。
ホームラボの主要な Docker ホストは nemesis で、これは Proxmox ホスト sienar 上の RHEL 9 仮想マシンです。nemesis がパニックを起こすと、その背後にあるすべてのサイトが kdump が完了し、仮想マシンが再起動するまでダウンします。毎回約10分かかります。今年の夏は頻繁にパニックを起こしました。
カーネルを指し示すパターン
8月の最初の2週間で、あるカーネル構築に対して9回のパニックが発生し、そのうち7回は kswapd0 内部のカーネルのページ再確保スレッドにおける NULL ポインタの dereference でした。これは明らかにカーネルのバグを示しており、明確な対処法はカーネルの更新でした。更新は8月24日に適用され、ホストは6日間パニックを起こさなかったように見えました。
その後、新しいカーネル上で3回のイベントが発生しました:
- 8月30日: ヘッドレスブラウザのスレッドに関連する “stack-protector: Kernel stack is corrupted in futex_parse_waitv” というパニック。
- 9月12日: 資格情報の解放中に
refcount_t: underflow; use-after-freeの警告。 - 9月13日:
kswapd0の Oops、今回はマルチジェネレーション LRU (MGLRU) 再確保コードから到達したスピンロックのスロウパスにおける悪いページフォルト。
メモリの圧力がトリガーではなかったことがわかりました。仮想マシンの使用率は22%で、スワップはアイドル状態でした。kswapd は常に実行されているのは、ページキャッシュが常に仮想マシンを埋め尽くすためで、それによりページを多く触れているため、破損したページに出会う可能性が最も高いコードです。
2週間以内に、関係のないサブシステムで3つの破損のサインが確認されたのは、1つのカーネルのバグとは見えません。
vmcore が語ったこと
crash とマッチする kernel-debuginfo を使用して、9月13日のダンプを手動で解析することが可能でした。再確保は folio->mapping->host を通してロックを取得しており、そのロックのアドレスは dentry スラブ内にありました。folio から戻る経路をたどると:
folio->mapping (observed) ffff89417d547db0
real i_data of that inode ffff89417d547df0
difference 0x40, one bit
観測されたポインタは、メディアアレイ上の19GBの映画が含まれるライブXFSのinode内にあり、そのスラブレイアウトのinodeのi_dataであることはできないため、破損しているが、陳腐化しているわけではない。struct pageの1ビットが反転していた。
8月30日のダンプも同意している。スタックプロテクターが発火しており、ダンプ時のスタック、タスク、およびper-CPUのカナリーはすべて同じ値だった。比較は一度間違った値を読み取った。
下層のハードウェアがその理由を説明している。sienarはX299ボードで、i7-7820Xと非ECCメモリ(カーネルログによるとEDAC skx: ECC is disabled on imc 0)を搭載しており、KSM(Kernel Samepage Merging)がゲスト間で同一のページをマージする状態で動作していた。主な仮説は、RHELカーネルのバグではなく、ホストメモリまたはCPUの不安定性であると考えられるようになった。
次の日
9月14日に、同じような不安定性がユーザースペースでも現れた。dockerdはGoランタイムの致命的なエラー(found pointer to free object)とともに死んだ。これは、ヒープの破損を検出するゴミ収集器の動作である。再起動されたデーモンは、すべての孤立したコンテナを終了させた。21のコンポーズプロジェクトにまたがって49のコンテナが終了し、エッジプロキシ、このサイト、メディアスタック、Discordボットも含まれていた。
restart: unless-stoppedはその中からちょうど1つだけを復元した。
その代償
- 暗黙のホストの約10時間。朝から夕方まで。
- 見逃された uptime モニタ。このモニタは動的にコンテナチェックを構築しており、実行されていないコンテナのチェックはドロップするため、大規模な停止は監視数が減ったように見えるだけで、赤い監視とはならない。静的なチェックは5つだけが赤くなった。
- 4週間もダウンしていたストラグラー。9月13日のパニックの後、8月のクラッシュからまだ
Exited (255)状態だったサービスが1つ見つかった。再起動ポリシーやautoheal(実行中の不健全なコンテナのみを再起動する)はその状態に気づかなかった。
復旧は手動でした: 各プロジェクトは、コンテナのコンポーズラベルに記載されたディレクトリから docker compose up -d で復元されました。
変更点
- 起動時にストラグラーを復活させる。
scripts/revive-stragglers.shは、コード 255 で終了し、再起動ポリシーがalwaysまたはunless-stoppedであり、コンポーズで管理されているコンテナを検出し、それらのサービスを自身のディレクトリからdocker compose up -dを実行します。意図的に停止されたスタックは他のコードで終了し、無視されます。systemd ユニットは、Docker が起動した後 2 分後にこれを実行します。 - live-restore が設定されている。 Docker の
live-restore設定により、デーモンの再起動時にもコンテナが実行状態を維持します。そのため、次の dockerd のクラッシュでもコンテナが一緒にダウンすることはないはずです。この設定はデーモンの設定に含まれており、次のデーモンの再読み込み時に有効になります。 - sienar では KSM がオフになっているため、1 ページの翻訳は他のゲストに共有できません。
- リポジトリで MGLRU スイッチが切り替わった。 tmpfiles.d のエントリにより、MGLRU の回収が無効になります:
w /sys/kernel/mm/lru_gen/enabled - - - - n
この変更はまだ適用されておらず、RCA はこれをオプションとしてマークしています: 2 回トリップしたコードパスを削除しますが、次の翻訳は別の場所に落ちるだけです。
- vmcore の削除。 7 月下旬から、tmpfiles.d のルールにより、14 日以上前の kdump ダンプが削除されるため、パニックの連続が 20 GB の
/varボリュームを埋めることはありません(各ダンプは約 1 GB です)。 - リポジトリ内に RCA が含まれており、ダンプテーブルも含まれています。
未解決の問題
- nemesis VM を数週間間、別の Proxmox ノードに移動して A/B テストとして実施します。パニックがそこで止まる場合、sienar が原因です。
- メンテナンスウィンドウ中に sienar で memtest86+ を実行します。
- 実行中のコンテナ数を監視する機能を追加し、各コンテナのチェックが消えても、大規模な停止が見えるようにします。
今後も継続する内容
カーネル関数内で同じ場所にクラッシュが繰り返されることが、必ずしもカーネルのバグを示すわけではない。これは、その関数が最も多くのメモリにアクセスしていることを示している可能性もある。同じマシン上で関係のないコードで異なるサインチャーナーが発生する場合、それはソフトウェアの下層、つまりハードウェアやオペレーティングシステムの問題を示唆している。クラッシュダンプとデバッグシンボルがあれば、その仮説を1ビットの差分にまで絞り込むことができる。Dockerの観点から見れば、再起動ポリシーは不完全なクラッシュ後の回復計画ではない。また、動作中のみを監視するモニタは、何が停止したかを報告することはできない。
커널 패닉은 비트 플립이었다
kswapd에서 계속 패닉을 일으키는 Docker 호스트는 커널 버그처럼 보였습니다. 크래시 덤프는 페이지 구조에 하나의 뒤집힌 비트를 보여주었고, 다음 날 동일한 불안정한 하드웨어가 Docker 데몬과 해당 호스트의 모든 컨테이너를 10시간 동안 다운시켰습니다.
homelab의 주요 Docker 호스트는 Proxmox 호스트인 sienar에 설치된 RHEL 9 가상 머신인 nemesis입니다. 이 머신이 패닉을 일으킬 경우, kdump가 완료되고 가상 머신이 재부팅될 때까지 뒤에 있는 모든 사이트가 다운됩니다. 이 과정은 매번 약 10분 정도 소요되며, 여름철에는 자주 패닉을 일으켰습니다.
커널을 가리키는 패턴
8월의 처음 두 주 동안 하나의 커널 빌드에서 9번의 패닉이 발생했으며, 그 중 7번은 kswapd0, 커널의 페이지 회수 스레드 내부에서 동일한 NULL 포인터 참조로 인해 발생했습니다. 이는 커널 버그처럼 보이며, 명백한 해결책은 커널 업데이트였습니다. 업데이트는 8월 24일에 적용되었고, 호스트는 6일 동안 문제가 없어 보였습니다.
그 후 새로운 커널에서 발생한 세 가지 이벤트:
- 8월 30일: 헤드리스 브라우저 스레드에 속한 “stack-protector: Kernel stack is corrupted in futex_parse_waitv”라는 패닉.
- 9월 12일: 자격 증명을 해제하는 동안
refcount_t: underflow; use-after-free경고. - 9월 13일: 또 다른
kswapd0Oops, 이번에는 다중 세대 LRU (MGLRU) 회수 코드에서 도달한 스피놀록 느린 경로에서의 나쁜 페이지 오류.
메모리 압력이 원인은 아닙니다: VM 사용률은 22%이며, 스왑은 유휴 상태입니다. kswapd는 항상 실행되지만, 페이지 캐시가 VM을 항상 채우기 때문에, 이는 다른 모든 것보다 더 많은 페이지를 처리하며 손상된 페이지를 만나는 가장 가능성 있는 코드입니다.
2주 내에 관련 없는 서브시스템에서 발생한 세 가지 손상 시그니처는 하나의 커널 버그처럼 보이지 않습니다.
vmcore가 말해주는 것
crash과 일치하는 kernel-debuginfo을 사용하면 9월 13일의 덤프를 손으로 탐색할 수 있습니다. Reclaim은 folio->mapping->host를 통해 락을 취득했고, 그 락 주소는 dentry 슬랩 내부에 있었습니다. folio에서 뒤로 탐색하는 경우:
folio->mapping (observed) ffff89417d547db0
real i_data of that inode ffff89417d547df0
difference 0x40, one bit
관찰된 포인터는 미디어 어레이에 있는 19GB의 영화 파일이 포함된 활성 XFS 인노드 안에 있으며, 해당 슬랩 레이아웃의 인노드 중 하나의 i_data일 수 없으므로 손상되었을 뿐, 오래된 것이 아니다. struct page의 한 비트가 뒤바뀌었다.
8월 30일의 덤프도 동의했다. 스택 보호자(stack protector)가 작동했지만, 덤프 시점에서 스택, 태스크 및 per-CPU의 캐니(canary) 값은 모두 동일했다. 비교는 한 번 잘못된 값을 읽었다.
그 아래의 하드웨어가 그 이유를 설명한다. sienar는 X299 보드로, i7-7820X와 ECC 메모리가 아닌 메모리(커널 로그는 EDAC skx: ECC is disabled on imc 0라고 말함)를 사용했으며, KSM을 실행하고 있었다. KSM은 게스트 간에 동일한 페이지를 병합한다. 주요 가설은 RHEL 커널 버그가 아니라 호스트 메모리 또는 CPU의 불안정성으로 바뀌었다.
다음 날
9월 14일 같은 불안정성은 사용자 공간에서도 나타났습니다. dockerd는 Go 런타임의 치명적인 오류 found pointer to free object, 즉 가비지 컬렉터가 힙 손상을 인식한 것으로 인해 죽었습니다. 재시작된 데몬은 모든 고아 컨테이너를 종료했는데, 이는 21개의 compose 프로젝트에 걸쳐 총 49개의 컨테이너를 포함하며, 엣지 프록시, 이 사이트, 미디어 스택 및 Discord 봇도 포함되어 있었습니다.
restart: unless-stopped은 그 중 정확히 하나만 복구했습니다.
비용은 어떻게 되나요
- 어두운 호스트의 약 10시간, 아침부터 저녁까지.
- 확인이 불가능한 업타임 모니터. 이 모니터는 컨테이너 검사를 동적으로 생성하고, 실행 중이지 않은 컨테이너의 검사를 제거하므로, 대량의 중지가 적은 모니터로 보이지 않고 빨간색으로 표시되지 않는다. 정적 검사만 5개가 빨간색으로 표시되었다.
- 4주간 다운된 잔여 서비스. 9월 13일의 혼란 이후, 8월에 발생한 충돌로 인해 여전히
Exited (255)상태인 서비스가 하나 발견되었다. 재시작 정책이나 autoheal(실행 중인 컨테이너가 비정상 상태일 때만 재시작하는 기능)도 이를 인식하지 못했다.
복구는 수동적이었다: 각 프로젝트는 컨테이너의 compose 라벨에 명시된 디렉터리에서 docker compose up -d을 사용하여 복구되었다.
변경된 사항
- 부팅 시 스트래글러 복구.
scripts/revive-stragglers.sh은 코드 255로 종료된 컨테이너, 재시작 정책이always또는unless-stopped인 컨테이너, 그리고 Docker Compose로 관리되는 컨테이너를 찾아 해당 서비스에 대해docker compose up -d을 실행합니다. 의도적으로 중지된 스택은 다른 코드로 종료되며 무시됩니다. systemd 유닛은 Docker 시작 후 2분 후에 이를 실행합니다. - live-restore 설정. Docker의
live-restore설정은 데몬 재시작 시 컨테이너가 계속 실행되도록 합니다. 따라서 다음 dockerd 크래시는 컨테이너를 함께 가져가지 않을 것입니다. 이 설정은 데몬 구성에 있으며, 다음 데몬 재로드 시 적용됩니다. - sienar에서 KSM 비활성화, 따라서 한 페이지를 뒤집으면 다른 게스트에 공유할 수 없습니다.
- 저장소의 MGLRU 전환. tmpfiles.d 항목은 MGLRU 회수를 비활성화합니다:
w /sys/kernel/mm/lru_gen/enabled - - - - n
이 설정은 아직 적용되지 않았으며, RCA는 이 설정을 선택적으로 표시합니다: 이 설정은 두 번 발생한 문제 경로를 제거하지만, 다음 뒤집기 작업은 다른 위치에 떨어질 것입니다.
- vmcore 정리. 7월 말부터 tmpfiles.d 규칙은 14일 이상 된 kdump 덤프를 정리하므로, 패닉이 연속적으로 발생해도 20 GB의
/var볼륨을 채울 수 없습니다 (각 덤프는 약 1 GB입니다). - 저장소에 있는 RCA, 덤프 테이블과 함께.
여전히 열려 있는 사항
- 몇 주간 A/B 테스트를 위해 nemesis VM을 다른 Proxmox 노드로 이동합니다. 만약 그곳에서 패닉이 멈춘다면, sienar이 원인일 가능성이 있습니다.
- 유지보수 시간에 sienar에서 memtest86+를 실행합니다.
- 실행 중인 컨테이너 수를 모니터링하여, 개별 컨테이너 검사가 사라져도 대량의 중지가 감지될 수 있도록 합니다.
이어나가는 내용
커널 함수가 계속해서 같은 곳에서 충돌하는 것은 커널에 버그가 있다는 증거가 아닙니다. 이는 해당 함수가 가장 많은 메모리를 참조하고 있다는 증거일 수 있습니다. 같은 머신에서 관련 없는 코드에 다른 서명이 나타난다면, 이는 소프트웨어 이하의 문제를 나타내며, 충돌 덤프와 디버그 심볼을 통해 이 추측을 한 비트의 차이로 바꿀 수 있습니다. Docker 측면에서는: 불완전한 충돌 이후에 재시작 정책은 복구 계획이 아니며, 실행 중인 것만 감시하는 모니터는 멈춘 것을 보고할 수 없습니다.
内核恐慌是一个比特翻转的问题
一个持续在 kswapd 中崩溃的 Docker 主机看起来像是存在内核错误;崩溃转储显示页面结构中有一个翻转的位,第二天同样的不稳定硬件导致 Docker 守护进程和该主机上的所有容器停运了十个小时。
家庭实验室的主要 Docker 主机是 nemesis,它是一台运行 RHEL 9 的虚拟机,位于名为 sienar 的 Proxmox 主机上。当它发生恐慌时,其后所有的网站都会瘫痪,直到 kdump 完成并虚拟机重新启动,每次大约需要十分钟。整个夏天它都频繁地发生恐慌。
指向内核的模式
在八月的前两周,同一个内核版本发生了九次恐慌,其中七次是同一个 NULL 指针引用错误,发生在 kswapd0,即内核的页面回收线程中。这看起来像是一个内核的 bug,显然的修复方法就是更新内核。内核更新在 8 月 24 日完成,之后主机在六天内看起来都正常。
然后又发生了三次事件,都是在新内核上:
- 8 月 30 日: 一次恐慌,提示为 “stack-protector: Kernel stack is corrupted in futex_parse_waitv”,发生在属于无头浏览器的线程中。
- 9 月 12 日: 一个
refcount_t: underflow; use-after-free警告,发生在释放凭证时。 - 9 月 13 日: 另一个
kswapd0Oops,这次是多代 LRU(MGLRU)回收代码中的一次坏页面错误,发生在自旋锁的慢路径中。
内存压力并不是触发因素:虚拟机只用了 22%,交换空间处于空闲状态。kswapd 持续运行仅仅是因为页面缓存总是填满虚拟机,因此它接触的页面比其他任何代码都多,因此最有可能遇到损坏的页面。
在两周内,三个不同子系统中的损坏特征并不是一个内核 bug 所能解释的。
vmcore 的内容
使用 crash 和匹配的 kernel-debuginfo,9 月 13 日的转储可以手动分析。回收过程通过 folio->mapping->host 获取了锁,而该锁的地址位于 dentry 的 slab 中。从 folio 向前追溯:
folio->mapping (observed) ffff89417d547db0
real i_data of that inode ffff89417d547df0
difference 0x40, one bit
观察到的指针位于一个正在使用的 XFS inode 内(媒体阵列上的一个 19 GB 电影),它不可能是该 slab 布局中任何 inode 的 i_data,因此是损坏的,而不是过时的。一个 struct page 中的一个位发生了翻转。
8 月 30 日的转储结果一致:堆栈保护机制已触发,但当时堆栈、任务和每个 CPU 的 canary 值都相同。比较过程中有一次读取了错误的值。
底层硬件解释了原因。sienar 是一块 X299 主板,搭载了 i7-7820X 和非 ECC 内存(内核日志显示 EDAC skx: ECC is disabled on imc 0),并且正在运行 KSM,它会将跨虚拟机的相同页面合并。主要假设变成了主机内存或 CPU 的不稳定性,而不是 RHEL 内核的错误。
第二天
9 月 14 日,同样的不稳定性出现在用户空间。dockerd 因 Go 运行时致命错误而崩溃,found pointer to free object,这是垃圾收集器检测到堆损坏时的提示。重启后的守护进程杀死了所有孤立的容器:21 个 compose 项目中共有 49 个,包括边缘代理、此网站、媒体堆栈和 Discord 机器人。
restart: unless-stopped 仅恢复了其中一个。
所付出的代价
- 大约十个小时的暗黑主机时间, 从早上到傍晚。
- 一个无法检测到此问题的运行状态监控器。 该监控器动态构建其容器检查,并会丢弃那些未运行的容器的检查,因此大规模停止看起来像是更少的监控器,而不是红色警报。只有五个静态检查变为红色。
- 一个已经停机四星期的遗留服务。 在 9 月 13 日的崩溃后,发现有一个服务仍然
Exited (255)于 8 月的崩溃。重启策略和 autoheal(仅重启不健康的正在运行的容器)都没有注意到这一点。
恢复是手动的:每个项目都从其容器的 compose 标签所命名的目录中使用 docker compose up -d 恢复。
有什么变化
- 启动时恢复异常容器。
scripts/revive-stragglers.sh会查找那些以代码 255 退出、重启策略为always或unless-stopped,并且由 compose 管理的容器,然后从它们自己的目录中运行docker compose up -d来恢复这些服务。故意停止的堆栈会以其他代码退出,因此不会被处理。一个 systemd 单元在 Docker 启动后两分钟运行它。 - 配置了 live-restore。 Docker 的
live-restore设置可以在守护进程重启时保持容器运行,因此下一次 dockerd 崩溃时,容器不会随之停止。该设置在守护进程配置中,将在下一次守护进程重新加载时生效。 - sienar 上的 KSM 关闭, 因此一页内容不能被共享到另一个客户机中。
- 仓库中的 MGLRU 切换。 一个 tmpfiles.d 条目禁用了 MGLRU 回收:
w /sys/kernel/mm/lru_gen/enabled - - - - n
该设置尚未应用,RCA 标记为可选:它移除了两次触发的代码路径,但下一次触发将只是出现在其他地方。
- vmcore 清理。 自七月底以来,一个 tmpfiles.d 规则会清理 14 天前的 kdump 转储,因此一系列崩溃不会填满 20 GB 的
/var卷(每个转储大约 1 GB)。 - 仓库中的 RCA, 以及转储表。
仍需处理的问题
- 将 nemesis 虚拟机移动到不同的 Proxmox 节点上几周,作为 A/B 测试。如果在那之后崩溃停止,那么 sienar 就是原因。
- 在维护窗口期间对 sienar 运行 memtest86+。
- 添加一个对正在运行的容器数量的监控,这样即使每个容器的检查消失,也可以看到大规模停止的情况。
延续的问题
在同一个内核函数中反复发生崩溃,并不能证明存在内核错误;这可能表明该函数访问了最多的内存。同一台机器上不相关代码中出现不同的签名,意味着问题可能出现在软件之下,而崩溃转储加上调试符号可以将这种猜测转化为一个比特的差异。在 Docker 方面:重启策略并不是在发生不干净崩溃后的恢复计划,而只监视正在运行的监控程序无法报告什么已经停止了。
El pánico del kernel fue un bit invertido
Un host de Docker que seguía paniquando en kswapd parecía tener un error en el núcleo; un volcado de error mostró un solo bit invertido en una estructura de página, y al día siguiente el mismo hardware inestable dejó caer el demonio de Docker y todos los contenedores en la máquina durante diez horas.
La máquina virtual principal del homelab es nemesis, una máquina virtual de RHEL 9 en un host Proxmox llamado sienar. Cuando se pana, cada sitio detrás de ella está caído hasta que kdump termina y la VM se reinicia, aproximadamente diez minutos cada vez. Durante el verano se panió mucho.
Un patrón que apuntaba al kernel
En las primeras dos semanas de agosto hubo nueve pánicos en una construcción del kernel, siete de ellos el mismo error de desreferencia de puntero nulo dentro de kswapd0, el hilo de recuperación de páginas del kernel. Eso parece un error del kernel, y la solución obvia era una actualización del kernel. La actualización se implementó el 24 de agosto y el host parecía limpio durante seis días.
Luego tres más eventos, todos en el nuevo kernel:
- 30 de agosto: un pánico, “stack-protector: El stack del kernel está corrupto en futex_parse_waitv”, en un hilo perteneciente a un navegador sin cabeza.
- 12 de septiembre: una advertencia
refcount_t: underflow; use-after-freemientras se liberaban credenciales. - 13 de septiembre: otro Oops
kswapd0, esta vez un error de página mal en el camino lento de spinlock, alcanzado desde el código de recuperación de LRU multi-generacional (MGLRU).
La presión de memoria no fue el disparador: la VM estaba al 22% usada con swap inactivo. kswapd corre constantemente solo porque el caché de páginas siempre llena la VM, por lo tanto, toca más páginas que cualquier otra cosa y es el código más probable para tropezar con una dañada.
Tres firmas de corrupción en sub sistemas no relacionados dentro de dos semanas no es lo que parece un solo error del kernel.
Lo que dijo el vmcore
Con crash y el correspondiente kernel-debuginfo, el volcado del 13 de septiembre podría ser analizado a mano. La recuperación había tomado un bloqueo a través de folio->mapping->host, y esa dirección del bloqueo estaba dentro del dentry slab. Rastreando hacia atrás desde el folio:
folio->mapping (observed) ffff89417d547db0
real i_data of that inode ffff89417d547df0
difference 0x40, one bit
El puntero observado se encuentra dentro de un inode XFS en ejecución (una película de 19 GB en el array de medios), y no puede ser el i_data de ningún inode en ese diseño de slab, por lo tanto, está corrupto, no caducado. Un bit en un struct page había cambiado.
El volcado del 30 de agosto coincidió: el protector de pila había disparado, pero las canarias de pila, tarea y por-CPU eran todas idénticas en el momento del volcado. La comparación había leído un valor incorrecto una vez.
La hardware subyacente explica por qué. sienar es una placa X299 con un i7-7820X y memoria no ECC (el registro del kernel dice EDAC skx: ECC is disabled on imc 0), y estaba ejecutando KSM, que fusiona páginas idénticas entre invitados. La hipótesis principal se convirtió en la flaqueza de la memoria o del CPU del host, no en un error del núcleo de RHEL.
El día siguiente
El 14 de septiembre la misma flaqueza apareció en el espacio de usuario. dockerd murió con un error fatal del runtime de Go, found pointer to free object, que es cuando el recolector de basura detecta la corrupción del montículo. El demonio reiniciado mató a todos los contenedores huérfanos: 49 de ellos en 21 proyectos de compose, incluyendo el proxy de borde, este sitio, la pila de medios y el bot de Discord.
restart: unless-stopped devolvió exactamente uno de ellos.
¿Cuál fue el costo?
- Aproximadamente diez horas de un host oscuro, desde la mañana hasta la tarde.
- Un monitor de disponibilidad que no lo detectó. El monitor construye sus comprobaciones de contenedor dinámicamente y descarta las que corresponden a contenedores que no están en ejecución, por lo tanto, un cese masivo parece como menos monitores, no como rojos. Solo cinco comprobaciones estáticas se volvieron rojas.
- Un contenedor que había estado caído durante cuatro semanas. Después del pánico del 13 de septiembre, se encontró que un servicio aún estaba
Exited (255)desde un crash en agosto. Ni la política de reinicio ni autoheal (que solo reinicia contenedores en ejecución que se consideran no saludables) lo detectaron.
La recuperación era manual: cada proyecto se recuperaba desde docker compose up -d en el directorio nombrado en las etiquetas de los contenedores de sus componetes.
¿Qué cambió
- Revivir a los contenedores en el arranque.
scripts/revive-stragglers.shencuentra contenedores que terminaron con código 255, tienen una política de reinicio dealwaysounless-stoppedy están gestionados por compose, luego ejecutadocker compose up -dpara esos servicios desde sus propios directorios. Los stacks detenidos deliberadamente terminan con otros códigos y se dejan solos. Una unidad systemd la ejecuta dos minutos después de que Docker inicie. - live-restore configurado. La configuración
live-restorede Docker mantiene los contenedores en ejecución durante una reinicialización del demonio, por lo que el próximo crash de dockerd no debería llevarse los contenedores consigo. Está en la configuración del demonio y se aplica al siguiente recarga del demonio. - KSM desactivado en sienar, por lo que una página volteada no puede compartirse con otro invitado.
- Conmutación de MGLRU en el repositorio. Un entrada en tmpfiles.d desactiva la recuperación de MGLRU:
w /sys/kernel/mm/lru_gen/enabled - - - - n
Aún no se ha aplicado, y el RCA la marca como opcional: elimina el camino de código que se activó dos veces, pero la próxima volteada solo terminaría en otro lugar.
- Pruning de vmcore. Desde finales de julio, una regla en tmpfiles.d elimina los volcados de kdump con más de 14 días, por lo que una serie de fallos no puede llenar el volumen de 20 GB
/var(cada volcado es de aproximadamente 1 GB). - Un RCA en el repositorio, con las tablas de volcados.
¿Qué sigue abierto
- Mover la VM nemesis a un nodo diferente de Proxmox durante unas semanas como prueba A/B. Si los fallos se detienen allí, sienar es la causa.
- Ejecutar memtest86+ en sienar durante una ventana de mantenimiento.
- Añadir un monitor en la cantidad de contenedores en ejecución, para que un paro masivo sea visible incluso cuando las verificaciones por contenedor desaparezcan.
¿Qué se lleva hacia adelante
Un error que siempre ocurre en la misma función del núcleo no es prueba de un error en el núcleo; puede ser prueba de que la función accede a la mayor cantidad de memoria. Diferentes firmas en código no relacionado en la misma máquina indican algo por debajo del software, y un volcado de error más los símbolos de depuración pueden convertir esa suposición en una diferencia de un bit. En el lado de Docker: una política de reinicio no es un plan de recuperación después de un error no limpio, y un monitor que solo observa lo que está en ejecución no puede reportar lo que dejó de funcionar.
Der Kernel-Panic war ein Bit-Flip
Ein Docker-Host, der immer wieder in kswapd abstürzte, schien ein Kernel-Problem zu haben; ein Absturz-Protokoll zeigte einen umgedrehten Bit in einer Seitenstruktur, und am nächsten Tag brachte dieselbe unzuverlässige Hardware den Docker-Daemon und alle Container auf dem Host für zehn Stunden zum Absturz.
Der Haupt-Docker-Host des Homelabs ist nemesis, ein virtueller Maschine mit RHEL 9 auf einem Proxmox-Host namens sienar. Wenn er abstürzt, ist jeder Dienst dahinter bis kdump fertig ist und die VM neu gestartet wird, also ungefähr zehn Minuten pro Mal, nicht erreichbar. Im Sommer stürzte er häufig ab.
Ein Muster, das auf den Kernel hindeutete
In den ersten beiden Wochen im August gab es neun Abstürze bei einem Kernel-Release, sieben davon waren der gleiche NULL-Pointer-Dereferenzierung innerhalb kswapd0, dem Page-Reclaim-Thread des Kernels. Das sieht nach einem Kernel-Bug aus, und die offensichtliche Lösung war ein Kernel-Update. Das Update wurde am 24. August bereitgestellt und der Host sah sechs Tage lang sauber aus.
Dann drei weitere Ereignisse, alle auf dem neuen Kernel:
- 30. August: ein Absturz, “stack-protector: Kernel stack is corrupted in futex_parse_waitv”, auf einem Thread, der einem headless Browser gehört.
- 12. September: eine
refcount_t: underflow; use-after-freeWarnung während des Freigebens von Anmeldeinformationen. - 13. September: ein weiterer
kswapd0Oops, diesmal ein schlechter Seitenfehler in einem Spinlock-slow-Pfad, erreicht von der MGLRU (multi-generational LRU) Reclaim-Code.
Der Auslöser war nicht der Speicherbedarf: die VM war mit 22% genutzt und der Swap-Platz war leer. kswapd läuft kontinuierlich nur, weil der Page-Cache immer die VM füllt, also berührt er mehr Seiten als alles andere und ist der wahrscheinlichste Code, der auf eine beschädigte Seite stößt.
Drei Korruptions-Signaturen in unabhängigen Subsystemen innerhalb von zwei Wochen sehen nicht nach einem einzigen Kernel-Bug aus.
Was der vmcore sagte
Mit crash und dem passenden kernel-debuginfo konnte der September 13 Dump manuell durchlaufen werden. Reclaim hatte einen Lock durch folio->mapping->host genommen, und diese Lock-Adresse lag innerhalb des dentry Slabs. Durchlaufen vom Folio zurück:
folio->mapping (observed) ffff89417d547db0
real i_data of that inode ffff89417d547df0
difference 0x40, one bit
Der beobachtete Zeiger befindet sich innerhalb eines aktiven XFS-Inodes (einem 19 GB großen Film auf dem Medien-Array) und kann nicht der i_data eines Inodes in dieser Slab-Layout sein, also ist er beschädigt, nicht veraltet. Ein Bit in einem struct page hatte sich umgedreht.
Der August 30-Dump bestätigte dies: der Stack-Schutzmechanismus hatte ausgelöst, doch die Stack-, Task- und per-CPU-Canaries waren zu Dump-Zeit alle identisch. Der Vergleich hatte einmal einen falschen Wert gelesen.
Das Hardware-Untergrund erklärt, warum. sienar ist eine X299-Platte mit einem i7-7820X und nicht-ECC-Speicher (der Kernel-Log sagt EDAC skx: ECC is disabled on imc 0), und es wurde KSM ausgeführt, das identische Seiten über Gäste zusammenführt. Die führende Hypothese wurde zu Host-Speicher oder CPU-Instabilität, nicht zu einem RHEL-Kernel-Defekt.
Der nächste Tag
Am 14. September zeigte sich dieselbe Instabilität im Benutzerbereich. dockerd starb mit einem Go-Laufzeit-Fatal-Fehler, found pointer to free object, was bedeutet, dass der Garbage Collector Heap-Korruption bemerkt. Der neu gestartete Daemon tötete alle verwaisten Container: 49 von ihnen über 21 Compose-Projekte, einschließlich des Edge-Proxy, dieser Website, des Medien-Stacks und des Discord-Bots.
restart: unless-stopped brachte genau einen davon zurück.
Was es kostete
- Etwa zehn Stunden eines dunklen Hosts, von morgen bis abend.
- Ein Uptime-Monitor, der es nicht erkennen konnte. Der Monitor erstellt seine Container-Prüfungen dynamisch und entfernt diejenigen für Container, die nicht laufen, also sieht eine Massen-Stop-Prüfung wie weniger Monitore aus, nicht rote. Nur fünf statische Prüfungen wurden rot.
- Ein Straggler, der seit vier Wochen offline war. Nach dem Panik am 13. September wurde ein Dienst noch immer
Exited (255)von einem Crash im August gefunden. Weder die Neustart-Richtlinie noch autoheal (das nur ungesunde laufende Container neu startet) hatten dies bemerkt.
Die Wiederherstellung erfolgte manuell: jedes Projekt wurde mit docker compose up -d aus dem Verzeichnis zurückgerufen, das in den Compose-Labels der Container genannt wurde.
Was sich geändert hat
- Stragglers beim Booten wiederherstellen.
scripts/revive-stragglers.shsucht nach Containern, die mit Code 255 beendet wurden, eine Neustart-Richtlinie vonalwaysoderunless-stoppedhaben und von Compose verwaltet werden, und führt danndocker compose up -dfür diese Dienste aus ihren eigenen Verzeichnissen aus. Absichtlich gestoppte Stacks beenden sich mit anderen Codes und werden in Ruhe gelassen. Eine systemd-Unit führt sie zwei Minuten nach dem Start von Docker aus. - live-restore konfiguriert. Die Einstellung
live-restorevon Docker hält Container während eines Neustarts des Daemons am Laufen, sodass der nächste Crash von dockerd die Container nicht mitnehmen sollte. Sie ist in der Daemon-Konfiguration enthalten und greift beim nächsten Neuladen des Daemons in Kraft. - KSM ausgeschaltet auf sienar, sodass eine umgedrehte Seite nicht in einen anderen Gast übertragen werden kann.
- MGLRU-Schalter im Repository. Ein Eintrag in tmpfiles.d deaktiviert die MGLRU-Rückgabefunktion:
w /sys/kernel/mm/lru_gen/enabled - - - - n
Er ist noch nicht angewendet, und die RCA markiert ihn als optional: sie entfernt den Code-Pfad, der zweimal ausgelöst wurde, aber der nächste Flip würde einfach an einem anderen Ort landen.
- vmcore-Bereinigung. Seit spätem Juli deaktiviert eine tmpfiles.d-Regel kdump-Dumps, die älter als 14 Tage sind, sodass eine Serie von Panics den 20 GB
/var-Volume nicht füllen kann (jeder Dump ist etwa 1 GB groß). - Eine RCA im Repository, mit den Dump-Tabellen.
Was immer noch offen ist
- Den nemesis VM für einige Wochen auf einen anderen Proxmox-Node verschieben, um ein A/B-Test durchzuführen. Wenn die Panics dort aufhören, ist sienar die Ursache.
- Memtest86+ auf sienar in einem Wartungsfenster ausführen.
- Ein Monitor für die Anzahl der laufenden Container hinzufügen, sodass ein Massen-Stop auch sichtbar ist, wenn die pro-Container-Prüfungen verschwinden.
Was weitergeht
Ein Absturz, der immer in derselben Kernel-Funktion auftritt, ist kein Beweis für einen Kernel-Fehler; es kann beweisen, dass die Funktion am meisten Speicher berührt. Unterschiedliche Signaturen in unabhängigen Codeabschnitten auf demselben Gerät deuten unterhalb der Software hin, und ein Absturzdump plus Debug-Symbole können diese Vermutung in einen ein-Bit-Unterschied verwandeln. Auf der Docker-Seite: Eine Neustart-Richtlinie ist kein Wiederherstellungsplan nach einem uncleanen Absturz, und ein Monitor, der nur das laufende beobachtet, kann nicht berichten, was gestoppt wurde.