Linux运维shell脚本实战:从骨架搭建到自检清单
2026/9/17 6:04:00 网站建设 项目流程

简介:《Linux运维必备工作常用shell脚本》是一份面向Linux运维工程师的实用Shell脚本参考文档,适合具备一定命令行基础、希望将日常巡检与维护工作自动化的读者。文档围绕真实运维场景,系统整理了日志错误过滤与统计、服务健康检查、网络连通性测试、旧文件清理与备份压缩、循环多文件操作、scp远程传输、用户home目录核查、日志实时监控与异常捕获,以及自动化创建用户、进程检查与kill、服务器系统初始化配置等常见需求,并配有可直接参考的脚本片段和简要注释。包体为单个PDF文件,大小仅100KB,内容精炼、便于随时查阅。该资源目前已有1164人学习下载,既适合需要快速提升脚本编写效率的运维新手,也可作为中高级运维人员的日常工作速查手册。整体结构清晰,示例短小直接,便于理解后按需调整复用。

1. 为什么“Linux运维必备常用shell脚本”先要立规矩,而不是背命令

运维手里那份《Linux运维必备工作常用shell脚本.pdf》,问题从不在于“不够全”,而在于把它当成背书的材料。真正回到生产环境时,你面对的不是“一条命令能不能跑”,而是“这条命令在凌晨两点读写到日志里的内容是什么格式、退出码是多少、下一次执行时会不会污染上一轮结果”。同样一条df -h,在终端里看和写进脚本里定时跑,要求完全不一样:终端里输出错了可以重敲,脚本里错了就可能在监控页面上躺一整晚。所以这类资料的合理用法,是把它当索引,照着索引维护一套自己的脚本库,每一条都带上日志、阈值、退出码和可重入的保护。这篇文章按“骨架 → 巡检 → 批量与备份 → 排障 → 自检”这条线,把我平时怎么把这些脚本组合成一套可运维、可交接的shell脚本库,完整拆开讲。

这套东西适合两类人:一是刚切到服务器运维、手里有一堆常用命令但还没形成脚本规范的Linux运维工程师;二是做桌面运维助手或自动化运维转型的人,需要把零散命令固化成能被cron和监控系统反复调用的固定工具。下面的代码都以bash 4+为准,默认系统是RHEL系或Debian系,不需要额外安装重型依赖。

2. 脚本骨架:set -euo pipefail、日志函数和退出码是脚本库的地基

2.1 让每个脚本自动带上 set -euo pipefail,出现异常立即退出

我见过的运维脚本里,最常见的翻车现场不是逻辑写错,而是“命令失败了脚本还在继续跑”。比如cd /data/app && rm -rf logs,如果cd失败,rm 会删到当前目录。避免这类问题的第一步,就是在每个脚本头部写死三行:

#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t'
  • set -e:只要脚本里有命令返回非零状态,整体立即退出。代价是像grep没匹配到这种“正常失败”也会中断流程,所以后续要用|| trueif grep显式兜底。
  • set -u:变量未定义就报错。典型收益是防止rm -rf $DIR/DIR没赋值,最后变成rm -rf /这种事故。
  • set -o pipefail:管道中任何一段失败都会让整条管道返回非零。没有它,mysqldump | gzip里mysqldump崩了,gzip照样成功,备份却是个空壳。

IFS=$'\n\t'是把字段分隔符收缩成换行和制表符,防止文件名里的空格被 for 循环拆开。这行配合set -u是脚本库地基里的地基,建议做成模板文件,而不是每次手打。

2.2 日志函数统一输出格式,排查时不用猜

脚本多了以后,最难受的是每份脚本打印日志的格式不一样。有人用echo,有人用logger,报警的时候很难归档。我一般会抽一个公共库文件,比如在/opt/ops/lib.sh里放两个函数,所有脚本都source它:

readonly APP_LOG="/var/log/ops/$(basename "$0").log" log_info() { echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] $*" | tee -a "$APP_LOG" } log_error() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $*" >&2 | tee -a "$APP_LOG" >&2 }

tee -a把内容同时送到终端和日志文件;log_error里要先重定向到>&2,否则错误信息和正常日志混在同一个输出流,cron 里没法区分成功失败。日志文件的目录要提前建好,脚本里可以加一句mkdir -p /var/log/ops,不要等第一次报错才想起来目录不存在。

2.3 退出码约定:0是成功,其他都是有具体含义的失败

脚本被cron或监控平台调用时,最终靠退出码判断成功与否。我习惯在前半部分定义一组退出码,比裸数字好读:

readonly E_OK=0 readonly E_PARAM=2 readonly E_DISK_FULL=3 readonly E_SERVICE_DOWN=4

常见错误码对应关系可以这样沉淀成一张表:

退出码含义使用场景
0成功正常完成且数据有效
1通用错误未分类异常
2参数错误传入的参数数量或格式不对
3资源不足磁盘、内存、容量触达阈值
4服务异常端口不通、进程消失、依赖缺失

调用方只要对照表就能定位大概方向。函数里return $E_PARAM,脚本末尾exit $E_DISK_FULL,监控系统拿到的不是一个没头没尾的“1”,而是可以直接映射到告警文案的状态。骨架阶段不要追求代码量,把这三个部分统一好,后面写任何脚本都是往这个框里填肉。

3. 巡检脚本:磁盘、内存、CPU、进程存活一次跑完并输出结构化结果

3.1 磁盘巡检脚本:df加阈值,比裸跑df靠谱得多

运维巡检里最高频的是磁盘。df -h的毛病是输出给人看的,脚本没法直接吃。我会写一个check_disk.sh,接收一个阈值参数,默认写到 80% 就告警:

#!/usr/bin/env bash source /opt/ops/lib.sh THRESHOLD=${1:-80} df -P | awk -v thr="$THRESHOLD" ' NR>1 { gsub(/%/, "", $5) if ($5 >= thr) { printf "DISK_ALERT mount=%s used=%s%%\n", $6, $5 exit 4 } }'

df -P是POSIX格式,不会因为设备名过长而换行,这是给脚本用的关键参数。awk 里用-v thr="$THRESHOLD"把shell变量传进来,gsub去掉百分号后做数值比较。注意这里exit 4会直接结束awk,而set -e会把 awk 的退出码4传给脚本,刚好落到上面约定的E_DISK_FULL=4。如果你用df -h写同样逻辑,迟早会因为卷名里出现多余空格而误报。

3.2 内存和CPU统计:从 /proc 取数,不装 sysstat

有些最小化安装的机器没有free或者版本太旧,我更推荐直接读/proc/meminfo,它一定存在且格式稳定:

total=$(awk '/MemTotal/ {print $2}' /proc/meminfo) avail=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) used_percent=$(( (total - avail) * 100 / total )) if (( used_percent > 90 )); then log_error "memory used=${used_percent}%" exit 3 else log_info "memory ok used=${used_percent}%" fi

MemAvailable是内核 3.14+ 提供的字段,它估算的是“在不触发交换的前提下还可分配多少内存”,比used/total这种粗糙算法贴近业务体感。CPU 负载则读/proc/loadavg的第一列,但要记得load average包含不可中断睡眠,偶发偏高不一定代表CPU被打满。想看真实占用,用top -bn1 | head -n5做一次采样更直观,脚本里注意加-b批处理模式,否则top会用交互界面输出乱码。

3.3 服务进程与端口存活检查:内存操作中不可忽略的一步

进程检查按“端口优先于进程名”的原则设计。比如MySQL,pgrep mysqld在服务僵死时仍可能返回成功,探查TCP端口更可靠:

check_port() { local host=$1 local port=$2 if command -v nc >/dev/null 2>&1; then nc -z -w 3 "$host" "$port" >/dev/null 2>&1 else timeout 3 bash -c "echo >/dev/tcp/$host/$port" >/dev/null 2>&1 fi } check_port 127.0.0.1 3306 && log_info "mysql alive" || { log_error "mysql down"; exit 4; }

nc -z -w 3是零数据探测并等待3秒,不给服务写入任何业务数据,适合巡检场景。没有nc的系统就用bash自带的/dev/tcp设备文件做TCP连接,同样能达到目的。两者都失败时,进程名检查可以作为兜底,但不建议反过来。整个巡检脚本设计成“一个main函数串起多个check函数”,每类检查输出一行机器可读的KEY=VALUE文本,之后无论是接Prometheus的textfile collector,还是自己写解析,都省事。

4. 批量操作与备份:for循环的坑、SSH批量分发和定时任务守护

4.1 for循环的正确姿势:数组和while read让步进可控

shell脚本里for循环是高频操作,而最常见的坑是变量遍历被空格拆开。要遍历一个目录下所有文件,新手这样写:

for f in $(ls /var/log/*.log); do echo "$f" done

这个写法一旦文件名里带空格,$f就会被拆成两个。正确做法是用数组或find + while

shopt -s nullglob for f in /var/log/*.log; do [[ -f "$f" ]] || continue echo "$f" done while IFS= read -r line; do echo "$line" done < <(find /var/log -maxdepth 1 -type f -name "*.log" -print)

shopt -s nullglob保证没有匹配文件时通配符展开为空数组,而不是保留字面量导致误处理。while IFS= read -r line里的IFS=避免行尾空格被吞掉,-r防止反斜杠被解释,这两点是读取文件名时不可减的配置。用find时我发现很多人忘记-maxdepth 1,结果递归处理了子目录里的同名文件,批量操作时容易波及无关目标。

4.2 批量SSH执行:无论用expect还是ssh密钥,先管好密码与登录安全

对几十台上万台服务器批量执行命令,我先检查现有基础设施:如果已有堡垒机或配置管理平台,就优先走它们;如果必须用自己写的脚本,第一选择是SSH密钥登录,而不是expect里嵌明文密码。密钥登录的批量分发脚本骨架如下:

hosts=(172.16.1.10 172.16.1.11 172.16.1.12) cmd='uptime && df -h /' for h in "${hosts[@]}"; do ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no \ -o BatchMode=yes "opuser@$h" "$cmd" \ || log_error "node $h failed" done

BatchMode=yes可以保证当你没有配置密钥或密钥不对时,SSH不会停在那里交互式问你密码,这在无人值守执行里极其重要,避免一个卡住的输入把整批任务挂在第一台机器上。StrictHostKeyChecking=no只建议在首次批量接入时使用,并且配合known_hosts校验,否则等于自己放弃了中间人防范。如果你的执行环境实在只能密码登录,用expect也要把凭证从外部配置读取并打码,脚本本身不要出现password=字样的明文参数。

4.3 备份脚本:tar打包、按时间轮转、再同步到远程,定时任务要加锁

备份脚本看着简单,实际容易毁在“重复执行”上。上一轮还没跑完,cron又拉起新一轮,两边同时写同一个tar文件,备份文件直接损坏。我的做法是给脚本加一个简单的flock文件锁:

exec 9>/var/run/ops_backup.lock if ! flock -n 9; then log_error "backup already running, exit" exit 1 fi STAMP=$(date +%Y%m%d_%H%M%S) tar czf "/backup/app_${STAMP}.tar.gz" -C /data app/ find /backup -name "app_*.tar.gz" -mtime +7 -delete

exec 9>文件打开一个持有文件描述符9的锁文件,flock -n 9非阻塞尝试加锁,失败说明另一个实例在跑,直接退出。打好包后立即用find -mtime +7 -delete清理超过7天的旧备份,这是最便宜的轮转策略。别在tar的源路径上写/data/app,到了恢复场景还得想“当时相对路径是什么”,-C /data把归档基准目录单独指定,解包时直接落在当前目录/app下面,路径行为可预期。

如果还要推到远端,用rsync -av --partial /backup/ backup-server:/backup/--partial保证中断后保留已传部分,下次续传而不是从头来。定时任务里我一般写成:

10 2 * * * /opt/ops/backup.sh >> /var/log/ops/backup_cron.log 2>&1

这里2>&1>>缺一不可,否则cron会额外给maillog发一封只有报错内容的信,时间久了邮箱淹没,问题反而不显眼。

5. 日志与故障排查:awk、sed、grep 排障组合及命令超时防护

5.1 日志切割与清理:先归档再删除,避免磁盘被日志占满

排查故障第一步是拿到日志,但系统里堆了几GB日志时,最需要的是把“一直在写的日志”和“可以归档的旧日志”分开。常见的logrotate配置之外,手动处理时我习惯用find + tar按天归档:

find /var/log/app -name "*.log" -mtime +1 -print0 \ | xargs -0 tar czf /backup/log/app_$(date +%Y%m%d).tar.gz find /var/log/app -name "*.log" -mtime +7 -delete

-print0搭配xargs -0,让文件名里的空格、换行都不再是障碍。第二段-mtime +7 -delete删除的是归档后的源文件。想解压中文文件名乱码的问题,根源通常是zip的编码和系统locale不一致,unzip -O gbk可以指定编码,归档阶段多用一个参数比事后解乱码省心。

5.2 grep/awk/sed 在排障场景的常用组合:把日志切片而不是全文翻

线上排查日志时,我一般按“先缩小时间范围,再做字段提取,最后聚合计数”的顺序,而不是直接tail -n 1000纯靠肉眼。一段时间内某个接口的耗时分布,可以这样切:

grep "2025-06-01 10:" /var/log/app/access.log \ | grep -o 'cost=[0-9]*ms' \ | sort -t= -k2 -n \ | tail -20

grep -o只输出匹配的片段,整个管道里的数据量骤减。如果要看错误码分布,把grep -o换成sed -n 's/.*status=\([0-9]*\).*/\1/p' | sort | uniq -c,直接得到每个状态码的出现次数。awk 适合做字段抽取,比如打印第7列的耗时,配一个if ($7 > 3000) print $1, $7就完成了慢请求初筛。最后结合sed -n '/2025-06-01 10:01:00/,/2025-06-01 10:02:00/p'把窗口外数据滤掉,整轮排查就不需要翻动大文件。

5.3 给命令加超时和并发:防止脚本卡死和清空负载

脚本里最容易被忽略的坑是“命令不发返回”。磁盘有坏道、NFS挂载点无响应、DNS解析超时,都会让脚本里去不返。统一给外部命令加timeout

timeout 10 ping -c 3 "$target" >/dev/null 2>&1 || log_error "ping timeout"

timeout 10让ping最多运行10秒,超时即杀。这个习惯必须覆盖到sshcurlmysql等所有会发起外部连接的命令上,否则一个巡检脚本可能因为某个远端服务假死而卡到cron下一轮启动,最终拖垮调度。

并发批量执行时,xargs -P比各自&更可控:

cat host_list.txt | xargs -P 10 -I {} ssh -o ConnectTimeout=5 opuser@{} 'uptime'

-P 10限制同时最多10个SSH连接,-I {}把每一行内容替换到命令位置。没有并发控制时,对200台机器一次性发起SSH,源端的进程数和连接数会瞬间打满,CPU没爆、负载先爆。观察这类故障,只看ssh进程数量就能确认是并发失控。

6. 用 shellcheck 和自检清单把脚本库变成可交接的资产

脚本写完不等于能用,能跑也不等于可维护。在把新脚本放进cron以前,我固定跑两遍检查。第一遍是shellcheck,它对bash的静态检查比人眼靠谱得多:

shellcheck -s bash -S warning /opt/ops/*.sh

-S warning把告警级别设为warning以上才提示,排除一批琐碎的style建议。最常见的两个告警是:SC2086提示变量没加双引号,这对应实际运行中的空格拆分问题;SC2164提示cd后没检查是否成功——这就是开头说到的rm -rf事故源头。修完shellcheck后,用shfmt -i 4 -ci -sr统一格式,我倾向于缩进4空格,-ci让case语句对齐更清晰,-sr把重定向符号规范成> file而不是file >,风格统一对交接非常重要。

第二遍是格式自查。脚本文件必须是UTF-8无BOM,行尾必须是LF。Windows下编辑过的脚本带着CRLF,bash执行时会报$'\r': command not found之类莫名其妙的错,排查时直接浪费半小时。用file script.sh看输出,或者sed -n l查看行尾控制符,都能快速识别。

自检清单我会固定在脚本库根目录的README里:每个脚本必须有日志函数调用;外部命令必须套timeout;涉及删除和覆盖的路径必须是变量且经过存在性检查;必须定义非零退出码含义;必须用flock挡住并发执行。新脚本只有过完这五条才能提交到生产环境的/opt/ops/目录。这样一套体系下来,那份PDF里的命令仍然全都在用,但每一行都变成了有日志、有退出码、有超时保护的正式工具,换人接手时不需要对着脚本猜当初的意图。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询