
先说明一下这篇文章的标题如果原样放出来很容易被当成段子Linux 更新的时候还能继续使用这本来就是很多运维同学的日常怎么反而成了 bug但真正做过生产环境维护的人会心一笑——Linux 的在线更新确实存在一类“看起来像 bug 的诡异现象”apt upgrade跑完服务一个都没停控制台也没有报错但更新之后的某个时刻进程突然崩溃、接口偶发超时、甚至内核日志里出现soft lockup。你回头查变更记录发现唯一的变更就是“更新了几个软件包”。这篇文章想聊的就是这件事。不是说“Linux 更新后服务还能不能用”而是“Linux 更新后为什么有些进程能继续用有些却会在之后崩掉到底怎么定位、怎么修复、怎么避免”。如果你维护过 Debian/Ubuntu 或 CentOS/RHEL 环境遇到过更新后服务出现诡异问题的场景这篇文章应该对你有用。1. 先把结论说清楚这不是玄学是动态链接在起作用网上很多讨论把这类问题归为“Linux 更新不如 Windows 稳定”“apt 有 bug”这种判断其实把事情想简单了。Linux 下绝大多数软件包更新不会直接停止正在运行的进程因为包管理器替换文件的机制并不是“先删旧文件再写新文件”而是“先写新文件再通过 rename 原子替换”。正在运行的进程仍然持有旧文件的 inode在它不重新加载的情况下旧文件的内容依然在内存映射里可用。换句话说**更新成功的瞬间进程还能继续跑是因为它还在用磁盘上已经被替换掉的旧代码。**这不是 bug这是 Linux 文件系统与动态链接机制的自然结果。但这个“自然结果”会埋下一颗定时炸弹。因为进程不可能永远不加载新文件。比如某个长连接服务在运行过程中通过dlopen动态加载模块或者某个子进程在新代码上启动而新代码与旧库不兼容问题就会在更新后的任意时刻爆发。所以标题里的“bug”更准确的描述是更新操作本身没有导致服务停止但更新后的运行环境已经变成了“新旧代码混合”的状态。这种状态如果没有人去收敛就会在某个时刻表现出偶发崩溃、段错误、连接失败等诡异现象。下面我会用一个典型的排查场景把这个问题完整串一遍。2. 先搞懂 Linux 更新时进程为什么还能继续跑2.1 包管理器的“原子替换”逻辑以 Debian/Ubuntu 的apt为例更新一个软件包时dpkg会执行postinst脚本把新文件释放到临时路径再替换目标路径。这个替换过程对正在运行的进程是透明的# 包管理器内部大致是这样的流程简化 # 1. 下载 .deb 包 # 2. 解包到临时目录 # 3. 用 rename() 将新文件替换旧文件 # 4. 执行 ldconfig 刷新动态链接缓存 # 5. 执行 postinst 脚本做后续处理因为rename()是原子的其他进程读写该路径时要么看到旧文件要么看到新文件不会看到“文件写到一半”的中间状态。2.2 进程为什么能继续用旧动态库一个 ELF 可执行文件启动时动态链接器ld.so会把依赖的.so库映射到进程地址空间。这个映射一旦建立磁盘上的文件内容就不再影响进程执行。即使你在磁盘上把libssl.so.1.1删掉再换成libssl.so.3正在运行的进程依然在用内存里的旧版本。这就是为什么很多人执行完apt upgrade后业务服务完全不停看起来一切正常。2.3 文件被删但仍在用lsof L1 的经典场景如果你用lsof去检查能看到大量进程持有着“已经被删除但仍被占用”的文件sudo lsof L1 | head -20输出里会出现类似nginx 1234 www-data mem REG 253,1 12345678 1001 /usr/lib/x86_64-linux-gnu/libssl.so.1.1 (deleted)(deleted)表示磁盘上的该文件已经被替换或删除但进程仍然持有对应 inode。这意味着进程当前运行在旧代码上。磁盘上已经是新代码。如果进程重启就会加载新代码。这就是整个问题的核心矛盾“更新后服务还在跑”并不等于“更新已经生效”只等于“旧代码还在内存里撑着”。3. 那个“更新后继续使用”的 bug 到底长什么样下面还原一个典型的故障场景。假设你维护一台 Ubuntu 20.04 服务器上面跑着一个 Java 服务和一个 Nginx数据库是 PostgreSQL。某天你执行了sudo apt update sudo apt upgrade -y升级了一堆依赖其中包括openssl、libssl1.1、postgresql-12全程没有报错。Nginx 还在跑Java 服务还在监听端口一切看起来岁月静好。但 40 分钟后Nginx 开始随机出现 502Java 服务的日志里出现java.lang.UnsatisfiedLinkError: /usr/lib/jvm/.../libnet.so: /lib/x86_64-linux-gnu/libssl.so.1.1: version OPENSSL_1_1_1 not found你检查磁盘上的/lib/x86_64-linux-gnu/libssl.so.1.1发现该文件确实存在。再仔细看发现apt upgrade把libssl1.1换成了新构建版本但某些第三方编译的二进制动态链接的还是旧符号版本。磁盘上的新库不再提供OPENSSL_1_1_1这个符号而 Java 服务运行过程中当某个 JNI 模块需要重新加载本地库时就崩溃了。这个过程是不是很像“更新后还能继续使用然后突然坏了”再举一个更隐蔽的例子。内核更新后apt upgrade安装了新的linux-image但你还在跑旧内核。此时系统运行正常应用也正常。如果你在没有任何提示下加载了某个内核模块而新安装的模块与当前运行内核版本不匹配dmesg里就可能出现kernel: watchdog: bug: soft lockup - cpu#2 stuck for 23s! [kworker/u32:3:2196]这就是一个典型的“更新后继续运行但运行环境已经不一致”的案例。下次重启进入新内核时模块匹配问题可能消失但如果你一直在旧内核上运行问题就一直在。所以这类问题真正的根因不是“更新导致服务停止”而是“更新让服务处在一个新旧不匹配的过渡态而这个过渡态没有及时收敛”。4. 修复这类问题的完整实操流程从运维角度看你不能直接说“重启一下就好了”因为生产环境不是什么事情都能立刻重启。你得有一套可执行的检测和收敛流程。4.1 更新前记录服务和端口基线更新之前先用命令记录当前运行的服务和监听端口方便更新后对比systemctl list-units --typeservice --staterunning ss -ltnp如果你有配置管理工具Ansible、SaltStack、Puppet最好把服务清单沉淀成配置文件更新前自动校验一遍。4.2 更新后立刻检查哪些进程还在用旧库这一步是整个修复流程的关键。用lsof找出仍然持有已删除 inode 的进程sudo lsof L1 | grep (deleted)如果输出里包含重要服务nginx、java、postgres、docker 等说明这些进程还在跑旧代码。然后确认它们是否监听了生产端口# 查看单个进程的详细情况 sudo lsof -p 1234 | grep (deleted)接下来用一个更智能的工具needrestart来检测需要重启的服务sudo apt install -y needrestart sudo needrestart -lneedrestart -l会列出使用了旧库的服务。needrestart -b可以在非交互模式下尝试自动重启这些服务生产环境建议先看列表再手动处理。4.3 对服务做“受控重启”对不能立刻重启的服务先评估重启窗口。对可以重启的服务按依赖顺序重启# 先重启依赖底层库的服务再重启上层服务 sudo systemctl restart postgresql sudo systemctl restart nginx sudo systemctl restart my-java-app重启完成后再验证systemctl status postgresql curl -I http://127.0.0.1:8080 ss -ltnp这里特别提醒一点**不要用kill -9去强制结束一个还在用旧库的进程。**在它还没完全退出时应用可能有未落盘的数据或未清理的资源。kill -9会直接跳过清理流程带来的问题往往比旧库问题更严重。除非进程已经僵死否则先systemctl restart。4.4 用包锁定来避免关键组件被版本冲掉如果某个库或服务非常重要且你暂时不想让它随系统大版本升级可以用apt-mark hold固定版本sudo apt-mark hold libssl1.1 sudo apt-mark hold linux-image-5.4.0-xxx-generic查看当前锁定列表apt-mark showholdRPM 系对应的方法是sudo dnf install -y dnf-plugins-core sudo dnf versionlock add postgresql锁包不是让你一直不升级而是把升级时机控制在你计划好的维护窗口内避免“全家桶式”升级把某个关键组件一起带偏。4.5 检查是否需要重启进入新内核内核升级后uname -r显示的还是旧内核。你需要确认磁盘上已经安装了哪些内核dpkg -l linux-image-* | grep ^ii uname -r如果发现有新内核未启动说明系统需要一次重启才能进入新内核。如果暂时不能重启至少要记录一个待办事项避免长期跨版本运行内核与模块。5. 从日志看 soft lockup一个更底层的问题更新后如果遇到服务器响应变慢、负载飙升、CPU 软中断卡死很多人的第一反应是看应用日志然后看到soft lockup。kernel: watchdog: bug: soft lockup - cpu#2 stuck for 23s!的含义是内核软锁检测到 CPU#2 上的某个内核线程比如kworker/u32:3在 23 秒内没有调度出去看门狗认为它卡死了。这类问题在系统更新后出现通常有几种可能更新了内核但旧的内核模块仍在加载模块与内核 ABI 不兼容。更新了硬件驱动网卡驱动、存储驱动驱动与当前内核版本不匹配。系统长时间运行某些内核线程遇到资源竞争或 I/O 卡住。排查方式dmesg -T | grep -i soft lockup journalctl -k -b | grep -i watchdog看到soft lockup时更重要的是确认它发生在哪个线程上以及那个线程对应的驱动或子系统。如果确定是最近一次内核更新引起的可以考虑回退内核# 查看可用内核 grep menuentry /boot/grub/grub.cfg | cut -d -f 2 # 临时启动旧内核 # 重启时在 GRUB 里选择 Advanced options 中的旧内核回退到旧内核后再观察dmesg和系统负载是否恢复。如果恢复说明问题与新版内核相关需要在新内核上进一步排查模块兼容性而不是直接上线。6. 不只是 aptRPM 系也要做同样的检查虽然前面以 Ubuntu/Debian 为例但 CentOS/RHEL 系的逻辑是一样的。yum update或dnf update后运行中的进程同样会继续持有旧库。RPM 系可以用needs-restarting检查哪些进程因为库更新需要重启sudo dnf install -y dnf-utils sudo needs-restarting -u也可以从/proc直接查进程启动时间和可执行文件状态ls -l /proc/pid/exe cat /proc/pid/maps | grep (deleted)如果你的服务器是 CentOS 7 这类已经 EOL 的系统更要注意停止维护的系统不会再有安全更新与其研究“更新后如何继续用”不如优先规划迁移或重装到受支持的发行版。7. 常见问题与排查思路以下表格汇总了更新后最容易遇到的现象、原因和处理思路问题现象可能原因排查方式解决方案更新后服务仍在运行但过一段时间偶发崩溃进程使用旧动态库某次 dlopen 加载到不兼容新库lsof L1、dmesg、应用日志受控重启服务或回滚有问题的包升级 libc 后大量命令出现段错误二进制与新版 libc 符号不兼容ldd /usr/bin/ls、查看包更新历史进入救援模式从备份回滚 libcapt update提示 repository check failure第三方源 GPG key 过期或仓库地址失效查看/etc/apt/sources.list.d/下的源文件更新 key或更换为官方/镜像源needrestart或lsof L1无输出但服务故障进程重启过但应用配置不兼容查看服务启动日志优先排应用配置回滚配置变更内核升级后出现soft lockup新内核与模块/驱动不匹配dmesg、journalctl -k回退到旧内核或升级驱动无人值守自动更新后服务异常unattended-upgrades自动升级了大版本依赖查看/var/log/unattended-upgrades/配置安全更新白名单锁定关键包8. 工程上应该怎么避免这类问题8.1 生产环境不原地大升级如果你在管理生产服务器最稳妥的做法不是“升级后尽量让服务不停”而是不要在生产机上直接升级大版本。更好的方式是把业务容器化升级策略变成“重新构建镜像滚动替换容器”。这样旧版本进程和新版本进程可以同时存在通过负载均衡流量切换问题范围可控。但容器化不是万能的。镜像里的基础镜像每天更新的底层库同样可能影响容器内业务所以镜像构建阶段就要做完整的测试和验证而不是apt upgrade一把梭。8.2 给自动更新套上缰绳很多云厂商的默认镜像会开启unattended-upgrades。这个机制本意是自动打安全补丁但如果在生产环境不对它做限制很容易在业务高峰期自动更新了某个关键库埋下故障种子。建议配置/etc/apt/apt.conf.d/50unattended-upgradesUnattended-Upgrade::Allowed-Origins { ${distro_id}:${distro_codename}-security; };这样只允许自动安装安全更新普通版本更新必须在维护窗口内手动操作。8.3 更新前必须有回滚方案任何一次更新前都要能回答一个问题能不能回滚云服务器更新前打磁盘快照。物理机或虚拟机用 LVM 快照或整机备份。数据库更新前至少做逻辑备份或物理备份。回滚不是“把旧包装回来”那么简单。依赖关系、配置变更、数据库迁移都可能是单向的没有快照你很难真正回滚到升级前的状态。8.4 日志和监控要能对上变更排查这类问题最难受的是服务偶发崩溃但应用日志里没有明显错误。这时候要养成把系统日志、内核日志、变更记录放在一起看的习惯# 最近一次系统更新记录 grep -i upgrade\|install /var/log/dpkg.log | tail -30 # 当前时间附近的系统错误 journalctl --since 1 hour ago -p err # 内核日志 dmesg -T | tail -50生产环境建议把变更记录自动归档至少能让运维同学在开故障排查会议时第一眼看到“哪个包在什么时间被更新过”。9. 最后说点实在的回到标题Linux 更新的时候服务还能继续使用不是 bug而是一种需要主动管理的状态。真正的 bug 往往出现在“更新后没有收敛”的窗口期。其实最容易踩坑的不是那些一看就有风险的大版本升级而是那些看起来完全不重要的依赖更新——libssl、libc、libstdc、ca-certificates。它们更新时不会提醒你“需要重启服务”但它们变了之后所有动态链接到它们的进程都在悄悄切换运行环境。这篇文章没有说“Linux 更新一定会出问题”而是想说**更新后的检查不是可选步骤是必选步骤。**至少要跑一遍sudo lsof L1 | grep (deleted) sudo needrestart -l uname -r把这几个命令变成你的更新后固定动作就能避免大部分“更新完过一会儿才崩”的诡异问题。至于那些更深的内核模块、驱动兼容问题建议平时多做小规模验证而不是等到故障爆发再去查dmesg。如果你在维护线上环境建议把这篇文章收藏下来下次更新系统的时候照着清单过一遍。