简介:面向 Linux 运维工程师和初中级运维学习者的常用脚本合集,聚焦日常巡检、批量管理、网站部署、安全防护等高频场景,可在真实服务器环境中直接参考改造。压缩包共 20 个文件,以 19 个 shell 脚本为主,附带 1 个 nginx.conf 配置示例,整体 12KB,轻量实用,便于下载后逐行阅读与测试。包内覆盖 MySQL 备份、批量创建用户、远程批量执行命令、网站异常检测、LNMP/Java 项目发布、DoS 攻击自动屏蔽、日志切割与分析、主从同步监控、服务器资源利用率统计、目录变更同步等常见运维任务,适合用来构建自己的运维工具箱。已有 82 人学习,对有脚本编写基础或希望提升自动化运维效率的读者较有参考价值。
1. 运维脚本解决的不是“会不会命令”,是“半夜能不能睡”
半夜被磁盘报警吵醒,爬起来要查是哪个目录把空间写满,再手动清理、重启服务——这个流程我经历过很多次。后来把高频操作焊成一批 Linux 运维常用脚本,报警来了先让脚本把 CPU、内存、磁盘、进程状态一次性捞出来,我只需要对着输出做判断。写脚本不是为了展示语法,是把巡检、批量执行、进程守护、故障定位变成可以重复使用的东西。适合刚接手服务器、还没有监控体系的运维,也适合开发兼运维的小团队。
2. 巡检脚本:把常用命令拼成一个 5 分钟就出体检报告的模板
2.1 为什么第一个脚本选巡检
Linux 运维里最重复的事,就是每天或每周登录服务器跑同一串命令。今天uptime,明天free -h,后天df -h,看起来很轻松,但机器数量一多,漏一台、看错一行、忘记看 inode 都是常事。与其背 linux 常用命令大全,不如把常用命令组合放进脚本里,输出固定格式,扫一眼就能判断有没有异常。而且巡检脚本不修改系统状态,出错也只是报错退出来,风险最低,适合刚学 shell 脚本的人拿它入门 shell 脚本。
巡检脚本要解决的第一个问题不是“跑得多快”,而是“看全”。磁盘满了、inode 满了、负载高但 CPU 不忙、内存被 cache 吃掉,这几种情况对应的命令完全不同,手动操作容易漏。脚本固定把这些指标放在一块,就是给自己上保险。桌面运维和服务器运维在这一点上思路完全一致:先有固定检查项,再谈自动化处理。
2.2 最小体检脚本:CPU、内存、磁盘、inode 一次看全
下面这个脚本可以整体复制到/usr/local/bin/check_host.sh,它只做读取操作,不修改系统,适合第一次上手:
#!/bin/bash # 最小巡检脚本:只读命令,不修改系统,适合第一次上手 echo "==== 时间与主机 ====" date '+%F %T' echo "==== 负载与核数 ====" uptime echo "逻辑核数:$(nproc)" echo "==== 内存(free 的 available 列才是真实可用内存)====" free -h | awk 'NR==2{printf "total=%s used=%s available=%s\n", $2, $3, $7}' echo "==== 磁盘(排除 tmpfs 等内存盘)====" df -h | grep -vE '^(tmpfs|devtmpfs|overlay)' | awk '{print $6, $5, "总量:" $2}' echo "==== inode 使用率(根分区需要单独关注)====" df -i | grep -vE '^(tmpfs|devtmpfs|overlay)' | awk '{print $6, $5}' echo "==== 占用内存前 5 进程 ====" ps aux --sort=-%mem | head -n 6代码里的awk负责把命令输出整理成整洁的行:free -h那行NR==2表示取第二行的 Mem 数据,$2/$3/$7对应 total、used、available;df -h的$6/$5对应挂载点和使用率,inode 输出结构类似。grep -vE排除 tmpfs、devtmpfs 和 overlay 这类和磁盘容量无关的内存文件系统,不然 overlay 那行很容易吓人一跳。ps aux --sort=-%mem | head -n 6取内存占用前 5 个进程并保留一行表头,配合 free 能看到是谁把内存吃了。
这个脚本跑完大概一两秒,输出可以直接贴到群里给同事看。要注意free -h的 available 列比 used 列更能代表真实剩余,因为 Linux 会把空闲内存拿去做 cache,手动看很容易把 cache 当成“被占用”。
2.3 阈值怎么设:不天天“狼来了”,也不当深夜传呼机
光看输出还不够,更顺手的是让脚本自己判断是否超过阈值。常见做法是把阈值定义到文件头部,方便不同团队按业务改:
#!/bin/bash # check_threshold.sh:在巡检脚本基础上增加分级提示 # 顶部集中定义阈值,改数字不用翻下面的命令 DISK_WARN=90 # 磁盘使用率超过 90% 提示 INODE_WARN=80 # inode 使用率超过 80% 提示 LOAD_WARN_BASE=1.0 # 负载超过核数*1.0 提示 CORE_NUM=$(nproc) LOAD_NOW=$(uptime | awk -F'load average:' '{print $2}' | cut -d, -f1 | tr -d ' ') # 负载比较:awk 支持浮点数,exit !(condition) 让 if 直接吃到结果 if awk -v now="$LOAD_NOW" -v core="$CORE_NUM" -v base="$LOAD_WARN_BASE" \ 'BEGIN{exit !(now > core*base)}'; then echo "[WARN] 负载 $LOAD_NOW 超过告警线 $CORE_NUM*$LOAD_WARN_BASE" fi这个片段把阈值放脚本顶部,改数不用翻命令。awk 的exit !(condition)用来做比较:条件成立时exit 0,if进入告警分支。写顺了之后,一般会把这段和 2.2 的脚本合并,再把告警送到钉钉或 webhook,或者写到单独日志文件里等定时任务来扫。
阈值不能拍脑袋定。磁盘 90% 是比较通用的做法,但 MySQL 数据盘和日志盘建议分开看,日志盘往往 80% 就该动手;inode 80% 相对保守,因为 inode 突发上涨经常伴随小文件风暴,等 90% 再处理可能会遇到删除都要慢慢数文件的情况。负载倍数要看业务类型,纯计算任务到 1.5 倍核数仍然能扛,IO 密集任务 0.8 倍就可能开始抖动。我的经验是:先按保守值跑两周,把统计里常见数字记下来,再按“平时值加 50% 冗余”来定告警线,而不是照着别人的模板抄一份就完。
3. 批量执行:让一台机器的操作,在 50 台上只跑一次
3.1 手敲循环的问题:漏机器、漏结果、事后无记录
机器少的时候,一台一台登录敲命令看不出问题,三五十台就不一样了:中途中控断开要重来、哪台命令执行失败没细看、等跑完自己也没法证明每台都执行到位。有人喜欢直接在命令行写 for 循环,但命令行里的循环没有日志、没有超时、没有并行控制,跑挂了只能靠肉眼找原因。所以我把“主机列表 + ssh + 日志”封装成脚本,当作这一类批量操作的基础件,它也算 it 运维效率工具里投入产出比最高的一种。
批量脚本的适用面很广:升级部署、改配置、收集某条命令输出都行。我一般把机器列表放在一个hosts.txt里,格式是“主机 用户”,每一台占一行。脚本只做一件事:逐台连接、把命令跑完、把输出按主机名落盘。
3.2 批量执行脚本:主机列表、超时参数和日志落盘
#!/bin/bash # batch_exec.sh:批量在 hosts.txt 的机器上执行一条命令 # 用法:./batch_exec.sh "uptime && df -h" # hosts.txt 每行格式:主机IP 用户名(用户名为空时用当前用户) CMD="$*" HOSTS_FILE="hosts.txt" LOG_ROOT="./exec_logs/$(date +%Y%m%d_%H%M%S)" mkdir -p "$LOG_ROOT" while read -r host user; do [ -z "$host" ] && continue if [ -z "$user" ]; then SSH_DEST="$host" else SSH_DEST="$user@$host" fi echo "== $host: 开始 ==" ssh \ -o ConnectTimeout=5 \ -o ServerAliveInterval=10 \ -o StrictHostKeyChecking=accept-new \ "$SSH_DEST" "$CMD" >"$LOG_ROOT/$host.log" 2>&1 echo "== $host: 退出码 $? ==" done < "$HOSTS_FILE" echo "执行记录在 $LOG_ROOT"关键参数在 ssh 的-o里:ConnectTimeout=5让建立 TCP 连接最多等 5 秒,避免连不上的机器把整批拖死;ServerAliveInterval=10每 10 秒发一个保活包,长任务期间不会被网关断开;StrictHostKeyChecking=accept-new对首次连接的机器自动接受主机指纹,省去手动输 yes,内网巡检常见,公网环境要谨慎。退出码$?必须立刻记录,因为下一条命令会把旧值覆盖掉。
主循环把每台机器的输出分别存成一个日志文件,文件名就是主机名。这个细节很重要:失败时能直接按机器名去翻日志,而不是在一大坨输出里 grep。循环读 hosts.txt 用的while read -r host user,支持注释行和空格分隔;如果主机列表里还有端口号,再多加一列在 ssh 命令里拼-p "$port"就行。
3.3 想提速就并行:给并发数做上限
串行跑 50 台机器,每台 2 秒也要一分多钟。想更快就把循环里的 ssh 放到后台执行,但要有并发数上限:
#!/bin/bash # batch_exec_parallel.sh:并行版本,用 wait 控制收尾 # 用法:./batch_exec_parallel.sh "待执行命令" 10 hosts.txt CMD="$1" MAX_PARALLEL="${2:-10}" # 并发数默认 10 HOSTS_FILE="${3:-hosts.txt}" LOG_ROOT="./exec_logs/$(date +%Y%m%d_%H%M%S)" mkdir -p "$LOG_ROOT" pids=() while read -r host user; do [ -z "$host" ] && continue dest="${user:+$user@}$host" ssh -o ConnectTimeout=5 -o ServerAliveInterval=10 "$dest" "$CMD" \ >"$LOG_ROOT/$host.log" 2>&1 & pids+=("$!") # 记录每个后台任务的 PID if [ "${#pids[@]}" -ge "$MAX_PARALLEL" ]; then wait "${pids[@]}" # 攒够一批后统一等完 pids=() fi done < "$HOSTS_FILE" wait # 收尾剩余的 echo "完成,日志在 $LOG_ROOT"这个版本用数组保存后台 PID,每攒够${#pids[@]}个就wait一次。控制并发数是因为 ssh 本身开销不大,但太多并发会打爆中控机器的连接数,也会让目标机器的 sshd 负载异常,10 到 20 是比较安全的区间。并发之后输出顺序和主机列表顺序不再一致,排障时以日志文件为准,不能看终端打印顺序。
提示:并发数超过 30 时,先确认中控机的 MaxStartups 和系统连接数上限,否则批量连接本身就是新的故障源。
另外要留意命令里的特殊字符。CMD里有$时,本地 shell 和远程 shell 会各展开一次,引号层级很容易搞错;带复杂引号的命令,建议先本地试跑后再写成脚本文件分发,别直接往命令行里塞。
4. 故障自愈:进程守护脚本与现场采集的命令清单
4.1 进程守护脚本:拉起、计数、防抖
业务进程挂了谁能第一个知道?监控不完善的小团队,很多时候是用户先发现。进程守护脚本是性价比最高的临时方案:每隔一段时间检查进程在不在,不在就拉起,连拉几次都起不来就告警退出。这个思路和 systemd 的 Restart 策略一样,适合还没有把服务纳管到 systemd、或者进程由别人脚本启动的场景。
#!/bin/bash # keepalive.sh:检测 pidfile 对应的进程,不存在就拉起 # 用法:./keepalive.sh "启动命令" "/var/run/xxx.pid" > /var/log/keepalive_xxx.log 2>&1 START_CMD="$1" PID_FILE="$2" CHECK_INTERVAL=10 MAX_RESTART=3 restart_count=0 check_alive() { [ -f "$PID_FILE" ] || return 1 pid=$(cat "$PID_FILE" 2>/dev/null) [ -n "$pid" ] || return 1 kill -0 "$pid" 2>/dev/null # kill -0 不杀进程,只探测是否存活 } while true; do if check_alive; then restart_count=0 sleep "$CHECK_INTERVAL" continue fi if [ "$restart_count" -ge "$MAX_RESTART" ]; then echo "$(date '+%F %T') 连续重启 $MAX_RESTART 次仍未成功,退出,请人工介入" exit 1 fi echo "$(date '+%F %T') 进程不在,尝试拉起" nohup $START_CMD >> /var/log/app_output.log 2>&1 & echo $! > "$PID_FILE" restart_count=$((restart_count + 1)) sleep 5 done这段脚本关键在kill -0:它不发送任何信号,只检查 PID 对应的进程是否存在,是 shell 里最稳的存活探测方式。restart_count是防抖的关键,避免进程因为配置错误反复重启把系统打崩;连续 3 次失败就退出,等于把“人工介入”这个问题抛出来。拉起用nohup ... &,让进程脱离当前终端,stdout 和 stderr 都重定向到日志,避免挂在 SSH 会话里。
要说明边界:如果启动命令是python app.py --port 8000,$START_CMD被无引号展开后执行没问题;如果程序路径或参数里有空格,建议把整条命令写成数组,或者在脚本外面做成一个start.sh再由守护脚本调用,避免引号解析把自己绕晕。更正式的环境应当用 systemd 的Restart=on-failure接管,这条脚本更适合过渡期和临时任务。
4.2 故障现场采集:一条命令把现场打包带走
进程挂了之后,最重要的不是立刻重启,而是先留下现场。很多故障重启之后就再也复现不了,日志却散落在 journald、/var/log和应用目录里。很多 linux 运维故障案例复盘到最后,都是“现场信息没留下”。我写过一条采集脚本,把核心信息一次性落盘再打包:
#!/bin/bash # collect_fault.sh:故障现场采集,输出 /tmp/fault_时间戳.tar.gz OUT_DIR="/tmp/fault_$(date +%Y%m%d_%H%M%S)" mkdir -p "$OUT_DIR" { echo "===== 时间/负载 =====" date '+%F %T' uptime echo "===== 内存 =====" free -h echo "===== 磁盘 =====" df -h df -i echo "===== CPU/内存占用前10进程 =====" ps aux --sort=-%cpu | head -n 11 ps aux --sort=-%mem | head -n 11 echo "===== 内核日志 tail =====" dmesg | tail -n 30 echo "===== systemd 日志 tail =====" journalctl -n 100 --no-pager 2>/dev/null || true echo "===== 监听端口 =====" ss -tlnp 2>/dev/null } > "$OUT_DIR/info.txt" tar czf "$OUT_DIR.tar.gz" -C /tmp "$(basename "$OUT_DIR")" echo "现场包:$OUT_DIR.tar.gz"dmesg 里经常有数据库进程被 OOM killer 干掉、磁盘 IO 错误这类容易被日志掩盖的信息;systemd journal 记录服务重启时间和最近错误;ss -tlnp看端口对应关系,排查“进程还在但连不上”很有用。把这些收集到一个文件里再打包,比故障时现场想起一条命令敲一条可靠得多。等收集完再重启,哪怕问题只能复现一次也不怕。
4.3 用 cron 定时跑:和手动执行的三个不同
脚本写好了,大多要通过 cron 定时跑。新手最常踩的三个不同点:第一,cron 默认 shell 是/bin/sh,bash 的高阶语法比如数组直接报语法错,所以脚本第一行要写#!/bin/bash并给足可执行权限;第二,cron 的环境变量和登录 shell 不同,PATH非常短,/usr/local/bin下的命令经常找不到;第三,cron 的输出如果不重定向,会通过邮件发到本地,多半不会有人看。定时任务里统一写成绝对路径加日志重定向:
# crontab 示例:每 5 分钟跑一次巡检,日志只追加不覆盖 */5 * * * * /usr/local/bin/check_host.sh >> /var/log/host_check.log 2>&1路径全部写绝对路径,重定向必须有,这是 cron 和命令行执行最大的差异。脚本里的日志路径也要设计好,留够轮转空间,不然巡检脚本自己会变成告警源。
5. 运维脚本避坑指南:5 个让我半夜爬起来改脚本的教训
5.1 set -e 遇到 ssh 失败,整个脚本直接退出
现象:批量脚本里加了一行set -e,跑的时候某台机器 ssh 返回退出码 255,整个脚本当场终止,剩下 40 台机器一台没跑。我看日志只看到一半,还以为脚本执行到一半被系统杀了。
原因:set -e的意思是“任何命令返回非零就退出脚本”,而批量和巡检场景恰恰充满“单台失败但整体流程要继续”的情况。ssh、grep、awk 都可能因为自然原因返回非零。
解决:不要把set -e当成万能保险。批量循环里对可能失败的命令用if ! ssh ...; then echo "失败"; fi显式捕获,或者局部set +e关掉再恢复。想保留“变量未定义就报错”的保护时,单独用set -u就够了。
5.2 cron 里找不到命令、日志乱码:PATH 和 LANG 的问题
现象:同一个脚本手动执行一切正常,放进 crontab 后第一行就报somecmd: command not found,或者中文日志变成一坨看不懂的编码。
原因:cron 的 PATH 默认很短,很多自定义安装的工具在/usr/local/bin下它根本找不到;LANG 没有从登录环境继承,中文输出会被搞乱。
解决:脚本开头固定两行:
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export LANG=C.UTF-8 export LC_ALL=C.UTF-8把这个习惯带入所有要进 cron 的脚本,能省掉大量“手动好、定时坏”的排查时间。这种问题最容易被当成玄学,其实原因往往就是这么简单。
5.3 Windows 里改过的脚本,在 Linux 上提示$'\r': command not found
现象:脚本在 Windows 的文本编辑器里改完传到 Linux 执行,第一行就报$'\r': command not found,或者脚本执行到一半行为怪异。
原因:Windows 行尾是回车加换行,Linux 只认换行,多出来的\r被当成命令的一部分。bash 解释到行尾时看到一个不认识的字符,就抛错。
解决:在 Linux 一侧转换一次:
sed -i 's/\r$//' /usr/local/bin/check_host.sh或者装dos2unix直接执行。预防的办法是编辑器里把行尾设为 LF,文件传输时保持二进制,别用自动转行的方案。
5.4 批量 ssh 不加超时,一台不通全体陪等
现象:批量重启脚本跑到第三个 IP,那台机器网络不通,ssh 默认会等很长时间才返回,整个任务看着像卡死。
原因:TCP 连接对不可达 IP 的失败探测依赖系统超时,可能超过两分钟;批量场景一台卡住,后面的全部排队。
解决:前面批量脚本里的-o ConnectTimeout=5就是干这个的;更保险的做法是整条命令也套一个 timeout:
timeout 15 ssh -o ConnectTimeout=5 "$host" "$CMD"timeout 15表示 15 秒还没跑完就强制杀掉 ssh 进程并返回非零退出码。长任务场景要按业务情况调大,而不是直接去掉。
5.5 守护脚本没管日志,磁盘被自己写满
现象:keepalive 脚本每 5 秒检测一次,挂掉的进程每次重启都追加日志,第二天再看,磁盘告警了,重启日志文件已经好几个 G。
原因:nohup $START_CMD >> /var/log/app_output.log 2>&1是无限追加,进程挂得越频繁增长越快;守护脚本自己的输出如果没轮转,同样会变成另一个日志炸弹。
解决:正式环境用 logrotate 管轮转;临时过渡可以用最简单的方式,每次启动前把应用日志截为最近 200 行:
tail -n 200 /var/log/app_output.log > /tmp/app_log_tail 2>/dev/null mv /tmp/app_log_tail /var/log/app_output.log这个“先截断再写入”的做法虽然糙,但在没有 logrotate 的环境里非常实用。日志不是越多越好,运维脚本自己也要有“后悔药”。
6. 让脚本“敢用”:从语法检查到干跑模式的三个习惯
6.1 上线前:bash -n、shellcheck 和测试目录干跑
脚本在交给别人或交给定时任务之前,至少做三道自检:bash -n查语法,shellcheck查常见错误,然后在测试目录里实际跑一遍。shellcheck 是免费的静态检查工具,对未加引号、判断符号写错、废弃语法这类问题非常敏感。命令很简单:
bash -n /usr/local/bin/check_host.sh shellcheck /usr/local/bin/check_host.sh在测试目录干跑时要换掉所有写死的路径,或者用一个空目录验证脚本不会误删系统文件。这一步是给自己买后悔药,等脚本进了生产再翻车,代价得乘以机器数量。
6.2 给脚本加一个“干跑模式”,重构时才敢下手
运维脚本改多了以后,最怕的是不知道这次改动会不会把生产搞炸。我习惯给关键脚本开头统一预留一个--check参数,把真正执行的命令换成打印出来:
CHECK_MODE=0 [ "${1:-}" = "--check" ] && CHECK_MODE=1 run() { if [ "$CHECK_MODE" -eq 1 ]; then echo "[CHECK] $*" else "$@" fi } # 原来直接执行的位置,统一改成 run run rm -f /var/run/app.pid在测试环境先加--check跑一遍,确认命令集合等于自己预期的操作清单,再摘掉参数上生产。配合静态检查,新脚本上线和旧脚本重构都敢动手。另一个习惯是把所有绝对路径定义成变量放在脚本开头,排障时改一处就能全局生效;这样做久了,一个团队维护下来的脚本风格会越来越接近,互相 review 时一眼能看懂。
我现在接手一台新机器或一个新环境,第一件事就是把这套脚本按它的目录结构过一遍,确认路径和 cron 行为符合预期,再交给同事。脚本这东西,写出来只是起点,敢放心地让它替你守着才叫落地。希望帮到你。
本文还有配套的精品资源,点击获取