文章目录案例psql 分页器 more 进程占用 CPU 满载排查一、故障现象1.1 系统级指标1.2 异常进程二、排查过程2.1 排查思路自上而下2.2 还原完整调用链2.3 判断终端是否已断开SSH_TTY fd deleted2.4 定位操作来源 IPSSH_CONNECTION2.5 为什么 su 子进程也继承了这些环境变量2.6 排除 cron 自动任务的干扰2.7 确认 more 阻塞位置strace2.8 排查工具速查表三、根因分析3.1 直接原因3.2 触发链条还原3.3 谁执行的人工 SSH 登录操作3.4 为什么 psql 也不退出3.5 进程文件描述符状态3.6 深入原理一psql 分页器机制禁用分页器的各方案对比3.7 深入原理二终端断开后进程为什么不退出3.8 术语说明此处的僵尸并非 Z 状态进程3.9 次因r8169 网卡中断不均3.10 因果链小结四、解决方案4.1 应急处置终止异常进程链已执行4.2 根治措施全局设置 PAGERcat已执行4.3 附加修复网络中断不均部分修复4.4 预防措施4.4.1 psql 分页器自旋预防4.4.2 僵尸/孤儿进程监控4.4.3 SSH 会话管理4.4.4 网卡中断均衡长期4.5 修复前后对比五、经验总结5.1 关键教训5.2 方法论沉淀5.3 遗留问题与待办附录排查命令速查案例psql 分页器 more 进程占用 CPU 满载排查项目内容故障载体PostgreSQL 数据库服务器4 核网卡eth0r8169 驱动仅 1 个硬件中断向量IRQ 140异常持续时间自 Aug24 起持续 4 天未退出发现方式巡检发现 load average 异常偏高一、故障现象1.1 系统级指标指标值说明load average66.20 / 55.95 / 53.674 核机器load 高达核数的 15 倍%sy55.2%系统态 CPU 极高%hi %si6.9% 13.8%中断开销大次因%idle0%完全满载运行时间持续 4 天自 Aug24 起未退出1.2 异常进程top中最显眼的 3 个进程PID USER %CPU TIME COMMAND 1261844 postgres 48.1 2809:37 more 1030191 postgres 48.1 2872:52 more 300789 postgres 41.4 1175:48 more现象特征3 个more进程均属postgres用户各占约 48% CPU约等于吃满 1 个核TIME高达 2809 / 2872 / 1175 分钟即已连续自旋数天4 核机器被 3 个more占满 3 个核剩 1 个核处理中断与业务二、排查过程2.1 排查思路自上而下top 发现 3 个 more 进程吃满 CPU ↓ strace 查看 more 的系统调用 → poll 死循环fd0 返回 POLLHUP 但不退出 ↓ 查看进程树 → more 的父进程是 psql -c SELECT... ↓ 查看 psql 的父进程 → su - postgres -c psql -c ... ↓ 查看 su 进程环境变量 → SSH_TTYpts/X (deleted) → 终端已断开 ↓ 查看 SSH_CONNECTION → 来源 IP 192.115.1.41 ↓ 结论人工 SSH 登录执行 psql 查询断开终端后 more 陷入自旋 ↓ 检查 PAGER 环境变量 → postgres 用户 PAGER 未设置 → psql 启动了默认分页器 more ↓ 根因psql 默认分页器 终端断开 more poll 死循环2.2 还原完整调用链通过ps命令可以还原这 3 个more进程的完整调用链root (SSH 登录 192.115.1.41) └─ su - postgres -c psql -c SELECT ... └─ psql -c SELECT ... └─ more ← 自旋吃满一个核PID父 PID命令启动时间SSH 终端3001751su - postgres -c psql -d xwall -c SELECT ...bgwriter_monitor...Aug26 16:04pts/3300386300175psql -d xwall -c SELECT ...Aug26(已删除)300789300386moreAug26(已删除)10298161su - postgres -c psql -c SELECT ...cache_hit_pct...Aug24 11:55pts/810298191029816psql -c SELECT ...Aug24(已删除)10301911029819moreAug24(已删除)12608891su - postgres -c psql -t -A -c SELECT ...pg_settings...Aug24 14:10pts/412613161260889psql -t -A -c SELECT ...Aug24(已删除)12618441261316moreAug24(已删除)注意 3 个su进程的父 PID 都是1说明它们的原父进程bash早已退出已被 init 收养——这是典型的孤儿进程特征。2.3 判断终端是否已断开SSH_TTY fd deleted第一步读取进程启动时的环境变量cat/proc/300175/environ|tr\0\n关键输出SSH_TTY/dev/pts/8第二步查看进程的文件描述符ls-la/proc/300175/fd/输出0 - /dev/pts/8 (deleted) 1 - /dev/pts/8 (deleted) 2 - /dev/pts/8 (deleted)判断逻辑证据含义SSH_TTY/dev/pts/8进程启动时它的终端是 pts/8SSH 分配的伪终端/dev/pts/8 (deleted)pts/8 对应的设备文件已被删除为什么(deleted) 终端已断开SSH 会话建立时sshd 在/dev/pts/下创建伪终端设备如 pts/8并分配给该会话SSH 断开时sshd 关闭并销毁这个 pts 设备但进程中 fd 0/1/2 仍指向该已销毁的设备 → 内核标记为(deleted)这与文件被rm后进程仍持有 fd 的表现一致所以SSH_TTY说明这个进程启动时用的是哪个终端fd 中的(deleted)说明这个终端现在已经不存在了。2.4 定位操作来源 IPSSH_CONNECTION同样从进程环境变量中读取cat/proc/300175/environ|tr\0\n|grepSSH_CONNECTION输出SSH_CONNECTION192.115.1.41 60116 192.117.20.62 22格式客户端IP 客户端端口 服务端IP 服务端端口字段值含义客户端 IP192.115.1.41SSH 连接的来源 IP客户端端口60116来源端口随机分配服务端 IP192.117.20.62本机 IP服务端端口22SSH 监听端口2.5 为什么 su 子进程也继承了这些环境变量因为su -会继承当前 shell 的环境变量执行链如下用户 SSH 登录 → bashsshd 设置了 SSH_TTY、SSH_CONNECTION 等环境变量 ↓ 用户执行 su - postgres -c psql -c SELECT... ↓ su 进程继承了 bash 的所有环境变量包括 SSH_TTY、SSH_CONNECTION ↓ psql 和 more 也继承了这些环境变量补充如果进程是通过 cron 或 systemd 启动的环境变量中就不会有SSH_TTY和SSH_CONNECTION这也是排除 cron 的依据之一。2.6 排除 cron 自动任务的干扰crontab 中虽有 psql 相关定时任务但都使用/dev/null 21重定向且直接调用/program/bin/psql不会产生morecron 启动的进程环境变量中不含SSH_TTY/SSH_CONNECTIONlast日志中 Aug27 以前的记录已被轮转wtmp 从 Aug27 开始因此无法追溯具体操作人2.7 确认 more 阻塞位置stracepoll([{fd3, eventsPOLLIN}, {fd0, eventsPOLLIN}], 2, -1) 1 ([{fd0, reventsPOLLHUP}]) read(2, , 8) 0 # ↑ 无限循环fd0 (pipe) 返回 POLLHUP但 more 不退出2.8 排查工具速查表需求命令输出含义查看进程环境变量cat /proc/PID/environ | tr \0 \n进程启动时的完整环境查看进程打开的 fdls -la /proc/PID/fd/(deleted)表示文件已不存在查看进程系统调用strace -p PID -e traceall观察进程在做什么查看进程树pstree -spal PID父子关系链查看进程 wchancat /proc/PID/wchan进程正在等待的内核函数查看进程状态cat /proc/PID/statusName/State/Threads/VmRSS查看进程 cmdlinecat /proc/PID/cmdline | tr \0 完整启动命令三、根因分析3.1 直接原因psql 默认启用分页器 SSH 终端断开导致more陷入poll死循环自旋。postgres用户未设置PAGER环境变量psql 在检测到 stdout 是终端时启动了默认分页器more终端断开后more的 stdout 变成(deleted)、输入管道返回POLLHUP但more不将其视为结束条件持续自旋。3.2 触发链条还原运维人员通过 SSH 登录执行su - postgres -c psql -c SELECT ...psql 默认将查询结果通过管道传给分页器morePAGER未设置时使用系统默认分页器psql 查询执行完毕将结果通过管道写入more然后关闭写端运维人员关闭了 SSH 会话终端 pts 变为(deleted)more检测到 stdout 终端已消失进入 poll 死循环见 2.7 的 strace 证据more的 stdout 和 stderr 指向/dev/pts/3 (deleted)无法写入但进程不退出3 个more进程各自吃满一个 CPU 核心持续数天3.3 谁执行的人工 SSH 登录操作结论人工通过 SSH 登录执行来源 IP 为192.115.1.41。证据3 个su进程的环境变量中均包含SSH_CONNECTION192.115.1.41:xxxxx - 192.117.20.62:233333 个进程分别绑定到 pts/3、pts/8、pts/4这些终端现已(deleted)last日志中 Aug27 以前的记录已被轮转无法追溯具体操作人crontab 中的 psql 任务均已重定向且不走分页器可排除3.4 为什么 psql 也不退出因为psql在等待more子进程退出后才退出而more卡在死循环中形成父子进程互相等待的死锁。3.5 进程文件描述符状态PID 300789 (more): fd0 - pipe:[240316421] ← psql 写入端的管道已关闭写端 fd1 - /dev/pts/3 (deleted) ← 终端已消失 fd2 - /dev/pts/3 (deleted) ← 终端已消失 fd3 - anon_inode:[signalfd]3.6 深入原理一psql 分页器机制psql 在以下条件下会启动分页器条件行为终端输出 结果超过屏幕行数 PAGER未设置为 cat启动$PAGER默认more非终端输出管道/重定向不启动分页器PAGERcat使用 cat直接输出不缓冲psql 交互中\pset pager off禁用分页器关键su - postgres -c psql -c SELECT...虽然由 SSH 触发但 psql 检测到 stdout 是终端pts就会启用分页器。终端断开后pts 变为(deleted)但管道仍然连接more不退出。禁用分页器的各方案对比方案写入位置影响范围交互模式非交互模式 (-c)脚本/cronexport PAGERcat/etc/profile.d/pager.sh全局所有用户生效生效生效export PAGERcat~/.bashrc单个用户生效生效生效\pset pager off~/.psqlrc单个用户生效不生效不生效psql --no-psqlrc -c ...命令行参数单次执行—生效生效psql -t -A -c ... file命令行参数单次执行—生效生效为什么\pset pager off不能写入.bashrc\pset pager off是 psql 内部命令不是 shell 命令只能在 psql 交互式会话中执行-- 在 psql 交互模式中执行postgres# \pset pager off而.bashrc是 shell 启动脚本其中的命令必须是 shell 命令# .bashrc 中这样写无效\pset pager off# bash 不认识这个命令报错 command not found正确的 psql 级配置方式是~/.psqlrc# 写入 postgres 用户的 .psqlrcsu- postgres-cecho \pset pager off ~/.psqlrc但注意这只能影响交互式 psql 会话对psql -c SELECT...这种非交互模式无效因为psql -c不加载.psqlrc。结论对于本次问题场景su - postgres -c psql -c SELECT...只有PAGERcat能解决\pset pager off无效。对比项PAGERcat\pset pager off生效层级OS 环境变量所有程序都能看到psql 内部设置只在 psql 进程内有效非交互模式生效psql 读取$PAGER用 cat 替代 more不生效.psqlrc在非交互模式下不被加载-c参数场景生效不生效本次问题的场景正是这个3.7 深入原理二终端断开后进程为什么不退出很多人以为SSH 断开 → 所有进程都退出但实际上不是。完整链路如下用户关闭 SSH 窗口 / 网络中断 ↓ sshd 检测到连接断开 ↓ sshd 向 session leaderbash发送 SIGHUP ↓ bash 收到 SIGHUP退出前向其所有子进程发送 SIGHUP ↓ 但此时 su 已经通过 setsid() 创建了新 session ↓ su 是新 session 的 leader和 bash 不在同一个 session ↓ bash 的 SIGHUP 传不到 su → su 收不到退出信号 ↓ bash 退出后su 的 ppid 变为 1被 init 收养 ↓ su 等待 psql 退出psql 等待 more 退出 ↓ more 在 poll 死循环中 → 不会退出 ↓ 整条进程链全部活着为什么su创建了新 sessionsu -做了两件关键的事setsid()创建新的 session脱离原来的控制终端fork exec启动 psql 子进程setsid()之后su不再受原来 bash 的 SIGHUP 影响。更直观的对比场景终端断开后进程是否退出原因直接在 SSH 里运行top退出bash 向 top 发 SIGHUPnohup top 不退出nohup 忽略 SIGHUPsu - user -c psql -c ...不退出su -通过 setsid 脱离 sessionscreen/tmux里的进程不退出screen/tmux 是独立的伪终端more的 poll 死循环详解终端断开后more的 stdoutpts/X变为(deleted)但more不是卡在 write 上而是卡在read等待用户按键翻页poll([{fd3, eventsPOLLIN}, {fd0, eventsPOLLIN}], 2, -1) 1 ([{fd0, reventsPOLLHUP}]) ← fd0 (pipe) 返回 POLLHUP read(2, , 8) 0 ← read 返回 0EOF # 但 more 不把 read0 当作退出条件而是继续 poll → 无限循环more的工作模式从 fd0pipe读取 psql 输出的内容从 fd3终端等待用户按键空格翻页、q 退出当终端消失后fd3 的 poll 不再返回事件也不报错只是没事件fd0 的 pipe 已被 psql 关闭写端返回 POLLHUPmore对 POLLHUP 的处理有缺陷它没有将此视为输入结束应退出而是继续等待 fd3 的用户输入结果poll 不断返回read 返回 0more不退出 → CPU 100% 自旋一句话总结终端断开只杀掉了 bash而su -已经通过setsid()脱离了 bash 的 sessionSIGHUP 传不到它。more又有 POLLHUP 处理缺陷不退出于是整条链su → psql → more全部成为孤儿进程持续运行。3.8 术语说明此处的僵尸并非 Z 状态进程原案例标题中的僵尸进程是口语化表述需要与内核语义区分僵尸进程Zombie状态Z子进程已退出但父进程未调用wait()回收其退出状态。此类进程只占一个 task_struct不消耗 CPU也不会出现在top的 CPU 排行前列。本例的真实情况more进程仍然存活且在运行top中显示 %CPU 48.1、TIME 持续增长属于孤儿进程ppid1 自旋死循环。因此排查高负载时不能用有没有 Z 状态进程来判断问题——真正的 CPU 消耗者一定处于 R/S 状态。本案例中 3 个more的 %CPU 接近单核满载这一点本身就是非僵尸的直接证据。3.9 次因r8169 网卡中断不均网络包到达 ↓ 网卡 IRQ 140 → CPU1硬件中断%irq 26% ↓ CPU1 的 NET_RX softirq → CPU1软中断%soft 51% ↓ RPS 将后续的包处理分发到 CPU0/2/3软中断层 ↓ 但硬中断仍然只在 CPU1 → CPU1 成为瓶颈根本原因r8169驱动只注册了 1 个 MSI-X 中断向量无法像ixgbe/i40e那样利用多队列 RSS 将不同 flow 的包分发到不同 CPU 的中断。这与more自旋是两个独立问题共同抬高了 load。3.10 因果链小结运维在 SSH 终端执行 su - postgres -c psql -c SELECT... ↓ postgres 用户未设置 PAGER → psql 启用默认分页器 more ↓ psql 执行完毕关闭管道写端more 等待用户按键 ↓ SSH 会话断开pts 被销毁fd 标记 deleted ↓ su - 已 setsid()收不到 bash 的 SIGHUP → 整条链成为孤儿 ↓ more 对 POLLHUP 处理缺陷 → poll 死循环自旋 ↓ 3 个 more 各占 1 核4 核机器 load 飙至 66%idle 0 ↓ 叠加 r8169 单中断向量瓶颈 → CPU1 中断负载集中 ↓ 业务响应严重变慢四、解决方案4.1 应急处置终止异常进程链已执行kill300789300386300175kill103019110298191029816kill126184412613161260889验证结果ps aux | grep more | grep -v grep返回空。效果load 从 66 → 35下降 47%CPU idle 从 0% → 1.2%。说明这里按整条链终止morepsqlsu而非只杀more。只杀more理论上也能让psql感知子进程退出但直接终止整条链更彻底可避免残留等待中的父进程。4.2 根治措施全局设置 PAGERcat已执行踩坑记录postgres用户的HOME目录被错误设置为/根目录导致~/.bashrc无法写入。# 直接写 /var/lib/pgsql/.bashrc 失败目录不存在# su - postgres -c echo ... ~/.bashrc 失败HOME/无权限写根目录最终生效方案写入/etc/profile.d/pager.sh全局生效echoexport PAGERcat/etc/profile.d/pager.shchmodx /etc/profile.d/pager.sh验证su - postgres -c echo PAGER$PAGER→PAGERcat为什么不修改 postgres 的 HOME 目录postgres用户当前有活跃进程200 PG 后端连接usermod -d要求先停止所有进程风险过大。全局/etc/profile.d/方案零风险且立即生效。4.3 附加修复网络中断不均部分修复eth0 使用r8169驱动只有 1 个硬件中断向量IRQ 140无法利用多队列 RSS 分散中断。# 启用 RPS软件层分散网络包处理到所有 CPUechof/sys/class/net/eth0/queues/rx-0/rps_cpus# 永久生效echoecho f /sys/class/net/eth0/queues/rx-0/rps_cpus/etc/rc.d/rc.local局限性RPS 只能分散软中断%soft处理硬中断%irq仍然集中在 CPU1。因为r8169驱动只注册了 1 个 MSI-X 向量/proc/irq/140/smp_affinity虽然可以改但irqbalance守护进程会持续将其重置回单核绑定。根本解决需要更换支持多队列的网卡如 Intel i40e/ixgbe或使用支持 RSS 的驱动。4.4 预防措施4.4.1 psql 分页器自旋预防编号措施级别详细说明1全局设置PAGERcat已执行/etc/profile.d/pager.sh所有用户登录自动生效2运维规范执行 psql 必须带 PAGER规范PAGERcat psql -c SELECT...或psql -t -A -c SELECT... /tmp/result.txt3cron 脚本审计已确认当前 crontab 中的 psql 命令均已使用/dev/null 21无风险4禁止su - postgres -c psql -c ...模式规范改用su - postgres -c PAGERcat psql -t -A -c ...或PGPASSWORDxxx psql -U postgres -c ...4.4.2 僵尸/孤儿进程监控# 方案 1crontab 定时检测轻量*/5 * * * * rootZOMBIES$(psaux|awk$8~/Z/|wc-l);ORPHANS$(ps-eoppid,pid|awk$11 $2100 {print}|wc-l);[$ZOMBIES-gt0]||[$ORPHANS-gt50]echoALERT: zombies$ZOMBIESorphans$ORPHANSon$(hostname)/var/log/process_alert.log# 方案 2process-exporter 告警已有# 当前已部署 /home/collect/process-exporter/可在 Prometheus 规则中增加# - alert: ZombieProcessDetected# expr: process_zombies 0# for: 5m提示结合 3.8 节若关注的是自旋型孤儿进程仅监控process_zombies无法覆盖建议同时监控长期高 CPU 且 ppid1 的进程。4.4.3 SSH 会话管理# /etc/ssh/sshd_config 中添加ClientAliveInterval300# 5分钟无操作发送心跳ClientAliveCountMax3# 3次无响应断开# 防止 SSH 断开后子进程成为孤儿4.4.4 网卡中断均衡长期当前状态问题建议r8169 驱动1 个中断向量硬中断无法分散更换支持多队列 RSS 的网卡Intel i40e/ixgbeRPS 已启用软中断可分散但硬中断仍集中 CPU1短期已是最优方案irqbalance 运行中会覆盖手动 smp_affinity 设置无需特殊处理4.5 修复前后对比指标修复前修复后改善load average66.2034.99下降 47%%idle0%1.2%出现空闲more 进程数30已清除CPU1 %soft49.5%51%RPS 对硬中断无效r8169 限制注意load 仍为 35 而非 4主要原因是业务本身负载高213 个 PG 后端进程 Java cdvserver FuseVM非异常。五、经验总结5.1 关键教训su - user -c psql -c ...是危险模式psql 默认启用分页器在终端断开场景下会产生自旋的孤儿进程。终端断开 ≠ 进程退出SIGHUP 只发给 session leadersu因setsid()脱离了 bash 的 session成为孤儿后被 init 收养more继续运行。PAGER环境变量必须在执行 psql 前设置这是防止此类问题最简单有效的方法且对-c非交互模式同样生效\pset pager off则无效。4 核机器 load 60 ≈ 3 个核被自旋进程占满 1 个核处理中断排查高负载时先看top中 CPU 占用最高的进程。/etc/profile.d/是设置全局环境变量的最佳位置比修改用户.bashrc更可靠尤其当用户 HOME 目录配置异常时。5.2 方法论沉淀从谁在吃 CPU出发而不是从谁在吃内存本例top直接给出 3 个more方向明确无需复杂分析。进程环境变量是追溯操作来源的黄金证据/proc/PID/environ中的SSH_TTY、SSH_CONNECTION能直接还原谁、从哪、在哪个终端执行的这是区分人工操作与 cron/systemd 的关键手段。(deleted)是终端/文件已消失的可靠信号结合ls -la /proc/PID/fd/可判断进程是否已失去 I/O 对象。strace用于区分卡在 I/O与自旋poll反复返回 read返回 0 却不退出是典型的用户态自旋特征此时%CPU高但wchan无阻塞。杀掉进程只是止血必须落实同类场景不再产生本例的根治点是PAGER的全局默认值而非一次性kill。不要把两个独立问题混为一谈more自旋与r8169中断瓶颈都是 load 高的成因需分别定性、分别处置。5.3 遗留问题与待办序号遗留问题说明后续动作1无法追溯操作人last/wtmp 中 Aug27 以前记录已轮转调整日志轮转策略延长 wtmp 保留期或接入 SSH 操作审计2postgres用户 HOME 被设为/导致无法写入~/.bashrc/~/.psqlrc在允许停机维护时用usermod -d修正3网卡硬中断无法分散r8169仅 1 个 MSI-X 向量RPS 只能缓解软中断长期更换支持多队列 RSS 的网卡4load 仍为 35属业务本身负载213 个 PG 后端 Java cdvserver FuseVM单独评估业务容量与资源配比5自旋类孤儿进程缺乏监控现有僵尸监控只看 Z 状态覆盖不到本例场景增加ppid1 且长期高 CPU的告警规则6预防措施尚未验证闭环PAGERcat已生效但缺少长时间观察观察一周确认无新增more进程附录排查命令速查# 查看进程环境变量追溯 SSH 来源、终端cat/proc/pid/environ|tr\0\ncat/proc/pid/environ|tr\0\n|grep-ESSH_TTY|SSH_CONNECTION# 查看进程打开的文件描述符判断终端是否已消失ls-la/proc/pid/fd/# 查看进程系统调用判断是阻塞 I/O 还是自旋strace-ppid-etraceall# 查看进程树、等待的内核函数、状态pstree-spalpidcat/proc/pid/wchancat/proc/pid/status# 查看僵尸进程与孤儿进程psaux|awk$8 ~ /Z/ps-eoppid,pid,comm|awk$1 1# 定位高 CPU 进程top-b-n1-o%CPU|head-20# 查看网络中断分布cat/proc/interrupts|grepeth0cat/sys/class/net/eth0/queues/rx-0/rps_cpus