项目/监控运维
监控运维 · 已归档

服务器卡顿/卡死故障复盘

两次真实事故的完整复盘:症状、取证、根因、修复与预防措施。

故障排查复盘PhotoPrismOOM

这是什么

这是我在 Homelab 里对两次真实故障的完整复盘归档:2026-10-16 的整机卡死,和 2026-10-17 的服务器卡顿。两次事故都发生在那台跑着一堆服务的 .164 小主机上。它用 Celeron J1900,只有 3.8GB 内存,同时承载反向代理、全屋 DNS、相册、内网穿透、memos、filebrowser 等服务。

为什么写

故障发生时,用户只觉得网站打不开。真正的原因既不是断网,也不是硬盘坏,而是内存被没有上限的容器占满,整机在机械盘 swap 抖动。我想把「症状 → 取证 → 根因 → 修复 → 预防」这条链完整保留下来:load 79、iowait 90%、vmstat b=73、sda 90% busy 却只有 5.42MB/s,这些数字比结论更有说服力。

做了什么

给 PhotoPrism 加了内存、CPU、IO 三重限制和看门狗;给量化回测容器加了自动补内存上限的规则;把 swap 改成 zram → SSD → NAS 机械盘的分级;还把 AdGuard 健康检查改成系统卡顿时不再盲目重启容器。

现状

两次事故均已处置并验证,项目标记为已归档。唯一没做的根治方案是加一根 4GB DDR3L 内存条,主板还空着一个槽。

功能

  • 完整记录两次事故的证据链:load、iowait、swap、阻塞进程数逐项留档
  • PhotoPrism 容器限内存 1200m / 1.5 核 / IO 64MB/s,超限只杀自己不拖整机
  • photoprism-guard 看门狗每 20 秒巡检,整机濒死时自动停相册保网站
  • sim-memcap 给没有内存上限的回测容器自动补 memory limit
  • swap 三级分层:zram > SSD > NAS 机械盘,换页不再抖动 HDD
  • AdGuard 健康检查 v2:高负载时只记日志,连续两轮失败才重启

技术栈

Dockercgroup v2systemd timerzram / swapPSIPhotoPrismAdGuard HomeShell

使用 / 部署教程

复现这套防护的步骤

前提:Linux + Docker + cgroup v2(stat -fc %T /sys/fs/cgroup 返回 cgroup2fs)。

1. 先取证,别急着重启

卡顿时立刻采集现场,重启会毁掉证据:

uptime; vmstat 1 5; free -m; cat /proc/pressure/io
iostat -x 1 3
docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}'

重点看三样:load 与 iowait 是否高、vmstat 的 b 列(不可中断 IO 进程数)、以及磁盘 busy 高但吞吐极低——那是 swap 抖动的典型特征。

2. 给占用大的容器加硬限制

编辑它的 compose.yaml(先备份原文件):

services:
  photoprism:
    mem_limit: 1200m
    memswap_limit: 1600m
    cpus: 1.5
    cpu_shares: 128
    pids_limit: 512
    logging:
      driver: json-file
      options: { max-size: 10m, max-file: "3" }

docker compose up -d <服务> 重建,只影响它自己。

3. 用 cgroup v2 限磁盘 IO

echo '8:0 rbps=67108864 wbps=33554432 riops=80 wiops=60' > /sys/fs/cgroup/system.slice/docker-<ID>.scope/io.max
echo 'default 20' > /sys/fs/cgroup/system.slice/docker-<ID>.scope/io.weight
echo 1000M > /sys/fs/cgroup/system.slice/docker-<ID>.scope/memory.high

容器重建后 cgroup 会重置,所以要写个脚本配合 systemd timer 每 20 秒重新施加一次。

4. 加看门狗

同一个 timer 里判断:/proc/pressure/io 的 full avg60 超过 50% 且 1 分钟负载超过 25,连续两次就 docker stop 掉那个最吃资源的容器,状态写进 /run/ 下的文件。日志用 journalctl -t <tag> 查。

5. swap 分级,别落在机械盘上

fallocate -l 4G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon -p 50 /swapfile
echo '/swapfile none swap pri=50 0 0' >> /etc/fstab
systemctl daemon-reload

优先级要保证 zram 最高、SSD 居中、机械盘只做兜底。

6. 抬升关键容器的 CPU 权重

docker update --cpu-shares 2048 npm adguardhome natfrp-service

规则保存在容器配置里,重启仍然生效。

7. 验证

确认 docker inspect 里的限制值、io.max 是否写入、容器内写文件速度是否真的被压下来,最后逐个 curl 关键服务确认返回码正常。

每次改配置都留一份带日期后缀的备份,回滚时直接覆盖回去再重建即可。

注意

  • 所有配置改动都留了 .bak 备份,回滚前先核对文件名和日期后缀
  • 看门狗会主动停掉相册容器,这是“宁可相册暂时不可用,也不让整机卡死”的取舍,阈值可按自己的服务组合调整
  • 内存上限只是缓解,这台机器只有 3.8GB 内存却跑着一堆服务,根治方案还是加内存条