一、先说结论:这几类故障 90% 不在你刚装的那个软件上
个人站长用 Debian 或 Ubuntu 跑服务器,最常遇到的一类痛苦是:装一个包没装完就报错,之后不管执行什么 apt 命令都失败,重启也救不回来。另一类更隐蔽:系统服务本身是 active (running),业务却一直 502,日志里只有一句 connect() to unix:/run/php/php8.2-fpm.sock failed,看不出到底谁把谁弄坏了。
这两类问题看着毫不相干,实际上都落在同一个地方——包管理器的状态文件与 systemd 对该状态的解释。dpkg 是 Debian 系所有软件的唯一记账本,它一旦记账中断,apt、unattended-upgrades、Docker 镜像构建、甚至你自己写的 cron 部署脚本都会跟着报同一个错。而 systemd 判断一个服务"活着没",看的是主进程在不在,不是端口通不通、socket 能不能连。
下面是我在自家 VPS 和几台客户机上反复踩过的六个现场,每个都给出可直接复制的取证命令和修法。命令都以 Debian 12 为准,Ubuntu 22.04/24.04 基本通用。
二、现场一:dpkg 被中断,所有 apt 命令全部瘫痪
典型报错长这样:
E: dpkg was interrupted, you must manually run 'dpkg --configure -a' to correct the problem.或者更烦人的:
E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 3412 (apt-get)第一种是上次 dpkg 执行到一半被打断(SSH 断开、Ctrl+C、OOM 杀掉、磁盘满),状态文件 /var/lib/dpkg/status 里留下了 half-configured 或 half-installed 的包。第二种只是 lock 被占着,先看进程是不是真的还在干活:
ps -fp 3412
ls -l /proc/3412/cwd如果这个进程已经运行了十几分钟、cwd 指向某个包的源码目录、CPU 还在动,多半是真的在编译或配置大包(比如内核、mysql-server),不要杀,等它。如果进程号已经不存在、或者 cwd 显示为 "(deleted)",那就是锁文件残留,可以清:
# 只在确认没有活着的 apt/dpkg 进程后执行
pgrep -a 'apt|dpkg'
rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock
dpkg --configure -adpkg --configure -a 会按数据库顺序把所有未配置完成的包重新跑一遍 postinst。它可能会停下来交互提问(比如是否需要重启服务、是否保留旧配置文件),在无人值守的机器上会一直卡着。所以跑之前先设成非交互模式:
export DEBIAN_FRONTEND=noninteractive
export DEBCONF_NONINTERACTIVE_SEEN=true
dpkg --configure -a <<< "yes"跑完再看一眼有没有残留:
dpkg -l | awk '$1 !~ /^ii$/ && NR>5 {print}'输出应该为空。只要还有 iF、iU、rc 之外的标记,就说明配置仍未完成,此时做任何 apt 操作都是在给自己挖坑。这一步我的习惯是先记下是哪几个包,再针对性重装:
dpkg -l | awk '$1 ~ /^(iF|iU|iH)$/ {print $2}' | xargs -r apt-get install --reinstall -y三、现场二:依赖破损与 "held broken packages",别一上来就 apt remove 一百个包
apt 给出的建议经常很吓人:The following packages have unmet dependencies ... you have held broken packages,然后建议删掉半个系统。这种时候第一件事永远是看清"谁依赖谁":
apt-get -s install 目标包 # -s 是模拟,不会真改任何东西
apt-cache policy 依赖包名 # 看已装版本、候选版本、来源仓库
apt-get check # 只做依赖完整性检查,输出问题包破坏依赖最常见的原因是混用了不同版本的仓库:比如在 Debian 12 (bookworm) 上留着 Debian 11 (bullseye) 的源,或者把 PHP 的第三方源(deb.sury.org)与官方源同时启用,导致同名包出现两个候选版本,apt 选了更新的那个,但它的依赖在其他仓库里没有对应版本。
排查命令:
# 看某个包到底有几个候选版本、从哪来
apt-cache policy php8.2-cli
# 看所有已启用的源
grep -rhv '^#' /etc/apt/sources.list /etc/apt/sources.list.d/ | grep -v '^$'如果发现同一发行版代号出现两次(bookworm 与 bullseye 并存),正确的修法是只保留当前代号:把旧的源注释掉,然后
apt-get update
apt-get -f install # 让 apt 自己补齐缺失依赖
apt-get --fix-broken install-f install 在绝大多数情况下就能把依赖理顺。如果它想删掉你不愿意删的包(比如 mysql-server),那就先 apt-mark showhold 看看有没有被人为 hold 住的包,hold 会让 apt 无法升级它,从而无法满足新依赖:
apt-mark showhold
apt-mark unhold 包名四、现场三:apt 能连上仓库但下载 404 或 Hash Sum mismatch
"Hash Sum mismatch" 看着像网络问题,实际上八成是缓存文件与新索引不一致,尤其是三次踩过的场景:把 /var/cache/apt/archives 从一个镜像站同步过来、或者用了不干净的代理缓存(CDN 上的 apt mirror 缓存了旧包)。
apt-get clean # 清掉所有下载缓存,比 autoclean 彻底
rm -rf /var/lib/apt/lists/* # 清索引
apt-get update若仍报错,看具体 URL:
apt-get -o Debug::Acquire::http=true update 2>&1 | grep -E 'GET|403|404|Hash' | head -20如果 URL 指向的是自己搭的内网镜像或代理,问题就在那台机器上:它的文件索引更新了但包文件没同步,或者它把带 ?_= 参数的请求缓存成了不同 key。个人站长最省事的做法是直接换成官方源加国内镜像的组合,别自己维护 apt 镜像。
还有一种情况是系统时间不对。HTTPS 仓库在证书有效期校验失败时会报 Release file is not valid yet,看起来完全不像时间问题。修法:
timedatectl status
timedatectl set-ntp true
systemctl restart systemd-timesyncd我有台小机器没装 NTP,主板电池也没电,每次重启时间倒退到 2019 年,apt 全废,前面那个报错就是它。现在把 systemd-timesyncd 设为 enabled 之后彻底消失。
五、现场四:服务显示 active (running),但 socket 根本连不上
这是新手最容易被误导的一类。systemd 的 active (running) 只代表主进程还活着,不代表它在监听什么、监听的地址对不对。判断顺序应该是:
systemctl status php8.2-fpm --no-pager
ss -lntp | grep -E 'php|9000|fpm' # TCP 监听
ss -lxnp | grep -E 'fpm|sock' # Unix socket 监听几年前我遇到过一个特别典型的案例:PHP-FPM 的 pool 配置被 phpdismod 之类的操作干扰,pool 文件被改成了 listen = /run/php/php8.2-fpm.sock,但下面又写着 listen.owner = www-data,而 /run/php/ 目录的权限是 755、owner 是 root,FPM 启动时以 root 打开 socket 后 chown 失败,日志里只有一行 warning,进程照常运行,socket 也生成了,但 Nginx 以 www-data 身份连的时候 EACCES。表现就是"服务好好的,网站 502"。
定位命令:
ls -l /run/php/
sudo -u www-data curl --unix-socket /run/php/php8.2-fpm.sock http://localhost/status
journalctl -u php8.2-fpm -n 50 --no-pager | grep -iE 'warn|error|chown|listen'用 sudo -u www-data 去连是关键——用 root 连永远成功,根本复现不出问题。这个思路可以推广到所有 Unix socket 故障:永远用最终使用者的身份去测,而不是用 root。
六、现场五:systemd 报 "Start request repeated too quickly",看不出真实原因
这个报错只是结果,不是原因。真实错误在服务自己的日志里,而 journalctl -u 默认只显示跟 unit 相关的行,有时候 stderr 被 unit 的 StandardOutput=null 或 StandardError=null 吞掉了,什么都看不到。
按这个顺序剥:
systemctl cat 服务名 # 看所有 drop-in 与真实 ExecStart
systemctl show 服务名 -p StandardOutput -p StandardError -p Restart -p RestartSec
journalctl -u 服务名 -b -n 200 --no-pager -o cat
journalctl -b -n 200 --no-pager | grep -iE 'segfault|oom|denied'如果日志里出现 status=203/EXEC,几乎百分百是 ExecStart 里的路径不存在或没有执行权限——常见于把程序装在 /opt 下但 /opt 挂载点没在 fstab 里、开机时还没挂上,unit 找不到文件。status=200/CHDIR 则是 WorkingDirectory 不存在。
如果是 status=1/FAILURE 且没有任何输出,用前台方式手工跑一次同样的命令:
# 把 ExecStart 里的命令原样复制,去掉 - 前缀测试
sudo -u www-data /usr/bin/php /var/www/app/artisan queue:work --once手工跑报错、systemd 跑不报,通常是环境变量差异:systemd 不读 /etc/profile、不读 ~/.bashrc,PATH 只有 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin。程序如果依赖 PATH 里的 node、composer,就会找不到。修法是显式写全路径或在 unit 里加 Environment=PATH=...,而不是在 ExecStart 里写 bash -c "source ... && ..."(那会让 systemd 管不到真正的子进程,停止时留孤儿)。
七、现场六:磁盘满导致的一连串"莫名其妙"
磁盘写满不会只影响写文件,它会连带弄坏上面所有环节:MySQL 拒绝写入、session 无法保存、日志轮转失败、apt 的 lock 文件写不进去、systemd 的 journal 也存不下启动失败原因。所以任何"服务起不来"的排查,第一步都应该是:
df -h
df -i # inode 也别忘
du -xh / --max-depth=2 2>/dev/null | sort -rh | head -15常见的几个隐形大户:
/var/log/journal:systemd 持久日志,没设上限可以吃到几个 G。journalctl --disk-usage看用量,journalctl --vacuum-size=200M立即收缩,再在/etc/systemd/journald.conf里写SystemMaxUse=200M。/var/lib/docker:镜像层与容器日志。docker system df -v看清楚是镜像占的还是日志占的。容器日志默认无限增长,必须在/etc/docker/daemon.json里加log-opts限制,否则一个刷屏的应用几天就能写满盘。- 已删除但未释放的文件:
lsof +L1找出来,通常是某个还在跑的进程握着被删日志文件。
写满之后的善后顺序也很重要:先腾出空间,再重启服务。反过来的话,MySQL 可能在磁盘仍满的情况下启动,进而把 InnoDB 表空间写坏,那就从"磁盘满"升级成"数据恢复"了。
八、把上面这些变成日常习惯,比故障后救火省事得多
我在每台机器上都留了一个 healthcheck.sh,每天 via cron 跑一次,只做只读检查,出问题就退出码非零,触发告警:
#!/bin/sh
fail=0
# 1. dpkg 一致性
dpkg -l | awk '$1 ~ /^(iF|iU|iH)$/ {print "BROKEN PKG: "$2; f=1} END{exit f}' || fail=1
# 2. 磁盘与 inode
df -h / | awk 'NR==2 && int($5)>90 {print "DISK HIGH: "$5; exit 1}' || fail=1
df -i / | awk 'NR==2 && int($5)>90 {print "INODE HIGH: "$5; exit 1}' || fail=1
# 3. failed units
systemctl --failed --no-legend | grep . && fail=1
# 4. 关键 socket 是否真的存在
test -S /run/php/php8.2-fpm.sock || { echo "NO FPM SOCKET"; fail=1; }
echo "healthcheck fail=$fail"
exit $fail再接一个极简的"下游连通性"检查,比 ping 有用得多:
curl -s -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 10 https://你的域名/ 把这两条塞进同一个 cron,用 MAILTO 或 ntfy 之类推到手机。我自己的经验是:百分之七十的宕机,在发生前一天的 healthcheck 里就有征兆——磁盘 88%、inode 91%、某个 unit 重启了 3 次而你没注意。等到用户来报错才动手,往往已经从"改一行配置"变成"恢复数据"。
最后一条个人体会:apt、systemd、日志、磁盘这四样看起来是四个话题,其实是同一个习惯——任何"情况不对"的瞬间,先取证再动手。先 ps、先 df、先 journalctl、先 apt-get -s 模拟,把现场留下来,再决定改什么。我吃过的最大的亏,不是不会修,而是急着修,把能说明问题的日志和进程都清掉了。