作为一个常年跟服务器故障打交道的运维我手机里存得最多的不是聊天记录而是各种排障脚本。今天聊的这件事几乎每个运维都躲不掉半夜收到告警几十台服务器同时报异常你一个人一台台登上去敲命令敲到天亮可能还没排查完。Shell 批量排查脚本就是干这个用的——把原本要重复几十遍的操作压缩成一条命令的事。这篇实战笔记我会从场景拆解、脚本设计讲到完整实现和踩坑实录适合刚入门 Shell 的新手也适合已经在写脚本但想优化执行效率的兄弟。1. 场景拆解为什么批量排查脚本是运维的刚需1.1 故障现场的真实痛点先说一个我印象特别深的真实场景。某次业务大促前夜监控平台突然飘红一批应用服务器负载全线飙升。当时团队里能上手的人不多我挨个 ssh 登上去先看 uptime再看 top然后 df -h、free -m一台机器轮询下来少说五分钟二十台机器就是将近两个小时。等我把所有机器看完最早异常的那台早被业务方重启过了现场全没了。这个经历给我一个特别深的教训故障排查最怕的不是问题难而是排查速度跟不上故障扩散的速度。手动逐台登录有几大天然缺陷——操作不统一不同人敲的命令不一样采集到的指标口径对不上速度慢一台台串行执行时间成本随机器数量线性增长无法留存输出散落在各个终端窗口里复盘时根本拼不出一张完整的故障时间线。批量排查脚本解决的正是这三件事统一采集口径、并行提升效率、落盘留存现场。它不替代你的分析判断但它能保证你在最短时间内用一致的方式把所有机器的关键状态拉到眼前。1.2 脚本化排查相对手动操作的核心优势如果要给脚本化排查一个定位我的理解是它是运维的第一现场固化器。在你还没想清楚故障根因之前脚本先帮你把现场完整保留下来。这个价值在故障复盘时尤其明显——很多故障事后查不清楚就是因为当时手忙脚乱没留下足够多的原始数据。具体来说四个优势是实打实的。第一是速度把串行登录改成批量并行执行二十台机器从两小时压缩到两分钟量级差异非常直观。第二是一致性脚本里写死要采集哪些指标、用什么命令、输出什么格式所有机器执行的是同一套逻辑不会出现A机器看了磁盘、B机器忘了看这种低级遗漏。第三是可重复脚本跑完自动生成带时间戳的结果文件同一批机器可以间隔几分钟再跑一次两次结果一对比趋势就出来了。第四是沉淀脚本本身就是团队的排障知识库新人拿到脚本就知道老手在故障时先看什么比读文档直观得多。这些优势不是我凭空总结的。我自己的习惯是每解决一次典型故障就把采集命令固化进脚本库下次遇到类似问题直接复用。日积月累下来这套脚本库基本覆盖了日常能遇到的绝大多数故障形态。2. 核心设计思路与脚本框架2.1 脚本整体架构采集、执行、汇总三段式批量排查脚本我做了很多版最后稳定下来的是一个三段式架构采集、执行、汇总。这三段各管一件事职责清晰也方便单独扩展。采集段的核心是确定要查什么。我的默认清单包括基础系统状态负载、运行时长、CPU 占用 TOP 进程、内存使用情况、磁盘空间占用、inode 使用率、系统日志错误、关键服务状态、TCP 连接数统计等。这份清单不是拍脑袋定的几乎所有服务器故障最终都会在这几个指标上留下痕迹负载高往往对应 CPU 或磁盘 IO 异常连接数异常膨胀往往是流量异常或半连接攻击磁盘满则直接导致服务写日志失败。把清单固定下来就等于给每台机器做了一次标准化体检。执行段负责把采集命令送到每台目标机器上跑。这里有两种做法一是通过 ssh 远程执行单条命令或脚本片段二是先把脚本分发到目标机再本地执行。日常排查我几乎只用前者省去分发环节一条命令搞定如果涉及复杂脚本也可以先把脚本用 scp 分发过去再执行后者适合需要多阶段采集的场景。实际经验是90% 的排障场景用远程执行就足够了。汇总段则是把各台机器返回的结果统一收集、整理、落盘。最简单的做法是每台机器一个输出文件文件名带上主机名和时间戳进阶做法是统一汇总成一个结果文件按主机分区展示方便横向对比。我在生产环境用的是两种都留单机明细文件用于深挖单点汇总文件用于快速扫一眼集群整体状态。2.2 关键参数设计主机清单、并发度与超时控制架构确定之后设计阶段最见功力的其实是三个参数主机清单、并发度、超时时间。这三个参数直接决定了脚本在真实故障场景下能不能活着跑完。主机清单我推荐独立成一个文件一行一个 IP 或主机名脚本里用 while read 循环读进来。为什么不用数组硬写在脚本里因为故障时你根本不知道具体哪几台有问题很可能是临时拼一个可疑机器列表独立文件可以随时改不用动脚本主体。另外清单文件天然支持注释掉某台机器故障处理过程中想把某台摘出去单独观察注释一行就行。并发度是批量脚本的命门。并发太高控制机自身先被压垮ssh 进程一多 CPU 和文件描述符都会吃紧并发太低又回到串行的老路上。我常用的保守值是 5-10几十台机器分成几批跑完。机器数量上百时可以考虑分片执行一次只处理一个分组。记住一个原则批量排查脚本的目标是稳定拿到结果而不是把控制机自己也跑挂。超时控制更是血的教训换来的。ssh 在目标机无响应或网络异常时可能一直挂着如果脚本不做超时限制一个坏节点就能拖死整批任务。我所有脚本里的 ssh 和远程命令都会套上 timeout一般给 10-15 秒超过就直接判定该节点异常记录下来继续跑后面的机器。提示超时时间别设太短目标机在高负载时命令本身执行就会变慢10 秒以下容易误杀正常节点。给出 10-15 秒是折中后的经验值。3. 实战脚本批量排查的核心实现3.1 单机健康检查脚本先打好地基批量排查的前提是先有一个靠谱的单机采集脚本。我日常用的健康检查脚本长这样核心是采集刚才说的那套标准清单#!/bin/bash # desc: 单机健康状态采集脚本 # usage: bash health_check.sh echo 基本信息 echo 主机名: $(hostname) echo IP 地址: $(hostname -I | awk {print $1}) echo 当前时间: $(date %Y-%m-%d %H:%M:%S) echo 开机时长: $(uptime -p) echo echo 负载情况 uptime echo echo CPU 占用 TOP 8 ps -eo pid,ppid,user,pcpu,pmem,comm --sort-pcpu | head -9 echo echo 内存使用 free -h echo echo 磁盘空间 df -h | grep -vE ^tmpfs|^devtmpfs|^overlay echo echo inode 使用 df -i | grep -vE ^tmpfs|^devtmpfs|^overlay echo echo TCP 连接状态统计 ss -ant | awk NR1 {print $1} | sort | uniq -c | sort -rn echo echo 系统错误日志最近 10 条 if command -v journalctl /dev/null 21; then journalctl -p err --no-pager -n 10 2/dev/null || echo journalctl 不可用 else grep -iE error|fail /var/log/messages 2/dev/null | tail -10 || echo 无系统日志或不可读 fi这个脚本的设计逻辑值得展开说。负载情况用 uptime 一眼看到 1/5/15 分钟三个值能快速判断负载是刚起来还是持续已久这是排障最关键的时序判断依据。CPU TOP 进程用 ps 按 CPU 降序排直接暴露是谁在吃 CPU。内存用 free -h 看总量和可用量特别要注意 available 这一列它比 free 更能反映实际可用内存。磁盘检查我特意过滤掉了 tmpfs、devtmpfs 这类虚拟文件系统因为它们在 df 输出里会抢占视觉注意力而真正要盯的是业务数据盘。inode 检查很多人会忽略但 inode 耗尽时即使磁盘还有空间也写不进文件症状和磁盘满了一模一样别漏掉。TCP 连接状态统计用 ss 取代了老旧的 netstat它是 iproute2 套件自带的几乎所有现代 Linux 发行版都有按状态分组后能快速看到 TIME_WAIT、SYN_SENT 等异常堆积。这个脚本默认在控制机本地跑。验证时随便找一台测试机执行bash health_check.sh输出内容一目了然。建议第一次使用时逐行对照确认每段输出都能正常显示尤其是 journalctl 那段不同发行版行为差异较大。3.2 批量执行串行保底并行提效有了单机采集脚本接下来要做的就是把它投递到所有目标机器。这里我给两个版本串行版保底用并行版日常用。串行版逻辑最简单适合机器数量少、或首次在陌生环境跑时用来验证连通性#!/bin/bash # desc: 串行批量执行健康检查 # usage: bash batch_check_serial.sh host_list.txt HOST_LIST${1:-host_list.txt} SSH_USER${SSH_USER:-root} OUTPUT_DIRcheck_result_$(date %Y%m%d_%H%M%S) mkdir -p ${OUTPUT_DIR} while read -r host; do [ -z ${host} ] continue [[ ${host} ~ ^# ]] continue echo 正在检查: ${host} timeout 15 ssh -o ConnectTimeout5 -o StrictHostKeyCheckingno \ ${SSH_USER}${host} \ bash -s health_check.sh ${OUTPUT_DIR}/${host}.txt 21 \ || echo [错误] ${host} 执行失败或超时 | tee -a ${OUTPUT_DIR}/error.log done ${HOST_LIST} echo 检查完成结果目录: ${OUTPUT_DIR}这里有几个关键细节。timeout 15是整个 ssh 会话的兜底配合ssh内部的ConnectTimeout5两层超时覆盖了连接阶段和执行阶段两种卡死可能。StrictHostKeyCheckingno是为了避免首次连接时的交互确认自动化场景下这个是必需品但要注意它降低了安全性所以这个脚本只建议在受控内网环境使用。bash -s health_check.sh是远程执行本地脚本的经典手法本质是把本地脚本内容通过标准输入传过去目标机用 bash 逐行解释执行免去了先 scp 再执行的繁琐。[[ ${host} ~ ^# ]] continue这行别小看它让主机清单支持注释行你可以随时把某台摘出去又不删掉记录。tee -a把错误信息同时打到终端和错误日志文件故障时你既要在屏幕上看到进度也要留档备查。串行版能跑通后升级到并行版就顺手了。并行版我用的是 xargs 配合-P参数指定并发数这是最简洁的并行方案#!/bin/bash # desc: 并行批量执行健康检查 # usage: bash batch_check_parallel.sh host_list.txt [并发数] HOST_LIST${1:-host_list.txt} SSH_USER${SSH_USER:-root} PARALLEL${2:-10} OUTPUT_DIRcheck_result_$(date %Y%m%d_%H%M%S) mkdir -p ${OUTPUT_DIR} cat ${HOST_LIST} | grep -vE ^\s*#|^\s*$ | xargs -P ${PARALLEL} -I{} \ bash -c host$1 echo 正在检查: ${host} timeout 15 ssh -o ConnectTimeout5 -o StrictHostKeyCheckingno \ ${SSH_USER}${host} \ bash -s health_check.sh ${OUTPUT_DIR}/${host}.txt 21 \ || echo [错误] ${host} 执行失败或超时 | tee -a ${OUTPUT_DIR}/error.log _ {} echo 检查完成结果目录: ${OUTPUT_DIR}这个脚本之所以用 xargs 而不是 for 循环加后台执行是因为 xargs 天然管理并发进程数-P 10就是最多同时跑 10 个 ssh既不会串行浪费时间也不会瞬间拉起几十个进程把控制机拖垮。-I{}是替换占位符xargs 每拿到一行主机名就替换到{}位置执行一次。写这个脚本时最容易翻车的是变量的传递。bash -c会创建一个新 shell外层变量传不进去所以我在脚本内部用${SSH_USER}${host}这种方式做了拼接把外层变量直接展开进字符串里。整个过程比较绕但理解了外层变量要展开后传给内层 shell这个原理就不会再被坑。跑起来后你会在终端看到多台机器的检查进度同时刷新十几台机器基本一两分钟内全部出结果。我在一次真实的 CPU 飙升故障里用这个脚本 3 分钟拉完了 32 台机器的健康数据定位到其中的 3 台异常机器这个效率手动操作完全做不到。3.3 定向排查脚本针对典型故障场景快速定位健康检查解决的是有没有问题的问题但故障往往更具体比如磁盘突然写满、某个服务异常退出、连接数飙升。针对这些高频场景我额外维护了几个定向排查片段它们可以独立执行也可以嵌到健康检查脚本里做条件触发。磁盘写满是最常遇到的故障之一而且磁盘满之前通常有征兆空间缓慢增长、日志文件异常膨胀、临时文件堆积。定向排查脚本的逻辑是先看整体水位再找大文件最后看哪些目录增长最快echo 磁盘空间 TOP 10 分区 df -h | grep -vE ^tmpfs|^devtmpfs|^overlay | sort -k5 -hr | head -10 echo echo 根目录下各一级目录占用 du -x --max-depth1 -h / 2/dev/null | sort -hr | head -20 echo echo 最近 24 小时修改的大文件超过 500M find / -xdev -type f -size 500M -mtime -1 -exec ls -lh {} \; 2/dev/null | head -20这套组合拳的执行顺序是有讲究的。先看分区水位是定位哪块盘满了然后看一级目录占用是定位空间被谁吃了最后找大文件是落到具体是哪个文件。一层层缩小范围比直接满盘 find 效率高得多——全部 / 路径扫大文件非常慢但从前两级结果里你基本已经锁定目录了。服务异常排查也是高频场景。我的做法是先看服务进程在不在、再看监听端口通不通、最后翻日志找原因SERVICE_NAME${1:-nginx} echo 服务进程检查 pgrep -a ${SERVICE_NAME} || echo 未找到 ${SERVICE_NAME} 进程 echo echo 监听端口检查 ss -tlnp | grep -iE ${SERVICE_NAME}|:80|:443 || echo 未发现相关监听端口 echo echo 服务状态systemd systemctl status ${SERVICE_NAME} --no-pager 2/dev/null || echo 非 systemd 管理或服务不存在 echo echo 最近 30 条日志 journalctl -u ${SERVICE_NAME} --no-pager -n 30 2/dev/null || tail -30 /var/log/${SERVICE_NAME}/error.log 2/dev/null顺序逻辑同样很关键进程在、端口在大概率服务是活的接下来就看日志里的异常进程已经不在了日志里的最后几行往往直接告诉你崩溃原因。这个脚本可以接收服务名参数批量执行时把服务名传进去就能对一批机器同时排查同一个服务的问题。连接数异常排查在流量异常时会用到。常见的判断维度是 ESTABLISHED 连接数是否远超基线、SYN_RECV 是否堆积可能被 SYN 攻击、TIME_WAIT 是否异常多短连接频繁建立导致的echo 全端口连接数统计 ss -ant | awk NR1 {print $5} | sed s/:[0-9]*$// | sort | uniq -c | sort -rn | head -15 echo echo 指定端口实时连接数 ss -ant | awk {print $4} | grep -E :80$ | wc -l echo echo 各种连接状态数量 ss -ant | awk NR1 {print $1} | sort | uniq -c | sort -rn这些片段单独跑只能看一台机器搭配前面批量执行框架就是同样逻辑的横向对比。比如全端口连接数统计批量跑完每台机器的 TOP 连接地址出来一眼就能看出流量是否集中在某台机器或某个来源 IP。4. 常见问题与踩坑实录4.1 环境差异引发的执行异常批量脚本最大的敌人不是逻辑写错而是目标机器环境不统一。我在生产环境踩过的坑至少一半都跟环境差异有关。第一个坑是脚本文件带了 Windows 换行符。如果你在 Windows 上编辑过脚本再传到 Linux 上跑\r会被 shell 当成命令的一部分最常见的报错是$\r: command not found。排查方法很直接——cat -v script.sh | head如果看到行尾有^M基本就是它了。处理用sed -i s/\r$// script.sh一行搞定。现在我写脚本都在服务器上直接编辑从源头上避开这个坑。第二个坑是 shebang 和解释器的差异。脚本第一行如果写#!/bin/bash目标机又只有 dash 这种精简 shell某些特性行为就不一样。更隐蔽的是ssh 远程执行bash -s时默认可能进入 sh 兼容模式如果脚本里用了 bash 特有的数组、[[ ]]等语法就会报错。我的规避方案是执行时显式带bash -s并且在脚本开头显式声明#!/bin/bash不给解释器歧义留空间。第三个坑是环境变量不一致。ssh 登录拿到的往往是非交互式、非登录 shellPATH 很精简/usr/local/bin里的命令可能直接找不到。比如有些机器把命令装在/usr/local/bin而默认 PATH 里没有脚本里直接写命令名就挂了。解决方法是脚本里尽量使用绝对路径定期用command -v验证关键命令位置。第四个坑是系统发行版差异。CentOS 7 的/var/log/messages日志、Ubuntu 的 journald命令和路径都不一样。我在健康检查脚本里做了兼容处理先判断 journalctl 是否存在不存在再回退到/var/log/messages。这类兼容逻辑要多写几个判断分支但换来的是脚本在异构机房也能一次跑通。4.2 批量执行中的超时与假死批量执行时最让人崩溃的是整个脚本卡住不动多看一眼发现是某台机器的 ssh 会话挂死了。这里我把踩过的坑和最终方案都讲透。第一个坑是密码交互导致卡死。如果目标机没有配置 SSH 免密登录ssh 会停下来等密码输入而脚本是无人值守的这个等待就是无限期。我的解决方式是双保险批量执行统一用预先配置好的 SSH 密钥同时在 ssh 参数里加-o BatchModeyes。BatchMode 下 ssh 不会交互式询问密码无法认证就直接失败退出绝不挂起。第二个坑是首次连接的 known_hosts 确认。新机器第一次 ssh 会提示确认指纹脚本同样会卡住。StrictHostKeyCheckingno配合UserKnownHostsFile/dev/null可以彻底跳过这个步骤。再次强调这只建议在受控内网使用公网环境不要这么干。第三个坑是指令本身很慢。比如磁盘 IO 已经很高时df -h本身可能卡住网络异常时远端命令要等 TCP 超时。这就是我给所有 ssh 和远程命令都套timeout 15的原因。另外给 ssh 单独加了ConnectTimeout5把连接阶段和执行阶段分开控制。实际效果是连接不上的机器 5 秒内就报错连上但执行慢的机器最多等 15 秒整个批量流程永远不会被单台机器拖死。第四个坑是并发数设置不当把控制机压垮。ssh 进程虽然不重但并发 50 个时控制机 CPU 和文件句柄会明显紧张。我一般控制在 5-15具体看控制机配置和机器数量。另外建议在 xargs 版本里先小并发跑一遍比如-P 5确认稳定后再适当调高别一上来就追求极限。4.3 排查结果的解读与闭环脚本跑完只是第一步结果的解读和闭环才是排障的真正价值所在。这块我总结了一套自己的处理方式。输出目录里每台机器一个结果文件我先做的是横向快速扫一遍。用grep -l 错误这类命令找出状态异常的机器或者用脚本提取关键指标做对比。比如这次排查的目的就是看哪些机器负载高直接批量提取 uptime 输出的 1 分钟负载值排序后异常机器自然浮出水面。我经常用一条命令做这个事for f in check_result_*/**.txt; do echo $f: $(grep load average $f | awk -Faverage: {print $2}) done | sort -t: -k2 -rn | head -20结果对比的价值在于趋势判断。间隔几分钟跑两次同样脚本对比两次输出的负载、内存、连接数差异就能判断异常是持续恶化还是在恢复。我通常会在初步定位后立刻再跑一次做对比这个习惯帮我区分过很多次正在崩溃和已经崩溃完的现场。闭环是整个流程里很多人忽略的一步——排查完把结论回写。我会在结果目录里放一个summary.md简单记录每台机器的结论和处置动作。这不仅是给自己留档更是给后来接手的同事看的。脚本负责采集人负责分析和沉淀二者配合才算一套完整的排障流程。注意故障处置完成后记得重新检查 SSH 免密配置的权限设置。批量脚本依赖密钥认证如果密钥文件权限不对比如密钥权限过大ssh 会直接拒绝使用下次故障时脚本就罢工了。5. 让脚本沉淀成团队资产脚本写出来能用只是及格线真正有价值的是把它沉淀成团队可复用的排障工具库。这部分的经验我想单独分享一下因为它决定了你手头这套脚本能走多远。我在团队里做的事情其实很简单把脚本从个人目录挪到 git 仓库按故障类型分目录管理每条脚本的注释里写上适用场景和使用示例。这样任何同事接手故障时都能按图索骥找到对应的脚本而不是依赖某个人记在脑子里的经验。长期维护下来这个仓库就成了团队活的排障手册新人培养的效率也高了很多。配套的还有主机清单的分层管理。我把机器按业务线分成不同文件比如 web.txt、db.txt、cache.txt排障时按业务维度批量排查比混在一个大清单里更精准。脚本本身不关心清单里是什么机器你传入哪个文件它就查哪个复用性因此强了很多。另外强调一点脚本仓库里要保留每次重要故障的排查记录。我会在每次大故障后花十分钟把最终定位的指标组合、关键命令、判断依据追加到对应脚本的注释里。比如某次发现 TPS 异常伴随 TIME_WAIT 大量堆积我就把这个关联写进了连接数排查脚本的注释。下次再遇到类似现象后人直接照着注释的判断逻辑就能少走弯路。我自己从这套实践里收获最大的体会是排障脚本不是一朝写就的静态文件它应该跟着每一次故障一起进化。你处置过的故障越多脚本里的场景覆盖就越全下次遇到问题的定位速度就越快。这其实是一个典型的正反馈循环。说到最后我还是想强调最开始那个观点脚本替代不了你的判断但它能把你的判断力发挥到最快。故障发生时你需要的不是一个绞尽脑汁回忆命令的自己而是一套早就准备好的、能瞬间铺开到几十台机器上的标准动作。把这些动作提前固化下来就是批量排查脚本最大的意义。你现在就可以从最小版单机脚本开始搭跑通了加批量稳定了再沉淀进团队仓库一步步来这套东西会成为你运维生涯里最值得的投资。
阅读完成 · 觉得有帮助?