简介:本资源是一份面向Linux运维工程师及Shell初学者的实用脚本速查手册,聚焦日常高频运维场景的自动化提效需求。文档以PDF格式呈现,共1个文件,体积仅100KB,轻量易携,涵盖日志过滤(如ERROR/FATAL统计)、服务健康检查(多IP批量ping)、旧文件清理(find + mtime)、目录备份压缩(tar/gzip)、远程文件传输(scp)、用户home目录校验、实时日志监控(tail + egrep)等11类典型脚本,每段均附可直接运行的代码及关键注释。内容源自一线运维实践,结构清晰、即学即用,特别适合在巡检、排障、批量操作等工作中快速调用或二次定制。目前已有1165人学习下载,是兼顾基础语法复习与实战能力提升的高性价比入门参考。
1. 这不是“脚本合集”,而是运维人每天睁眼就要跑的「生存清单」:从日志轮转、磁盘告警到服务自愈,一份真正能塞进 crontab 并扛住生产压测的 Shell 脚本实践手册
你手头那份《Linux运维必备工作常用shell脚本.pdf》——别急着打印装订。它大概率是把ls、grep、for i in $(cat list.txt)拼成的“教学体”脚本,一上线就因路径空格、权限继承、信号中断集体翻车。真实场景里,凌晨三点报警说/var/log占满 98%,你不会打开 PDF 查 for 循环语法,而是直接 ssh 进去执行一个能自动清理 N 天前日志、保留关键进程状态、且不 kill 掉正在写日志的 Java 进程的单行命令——这个命令,必须来自你亲手验证过 3 轮以上压测的脚本。本文不讲echo "Hello World",只拆解:哪些脚本真正在百万级服务器集群里每小时自动运行;它们为什么必须用find -mtime +7而不是$(date -d '7 days ago' +%F);如何让一个kill -9不误杀主进程的子线程;以及,当df -h返回多行时,怎样用纯 Shell 安全提取/dev/sda1对应的使用率数字。适合刚脱离“背命令阶段”、正被线上故障追着跑的中级运维,也适合想把自动化从 Ansible 切回 Shell 做轻量兜底的老兵——因为有些事,Shell 做得比任何工具都快、都稳、都无依赖。
2. 从“能跑”到“敢放 crontab”:5 类高频刚需脚本的选型逻辑与最小可行实现
运维脚本不是功能越多越好,而是在单个 Bash 进程内完成闭环、不依赖外部二进制、失败时有明确退出码、且能被set -euxo pipefail全局约束。下面这 5 类脚本,是我过去三年在金融、电商、IoT 三类生产环境里反复迭代、最终沉淀为基线模板的刚需项。它们共同特点是:不调用 Python/Perl,不读取配置中心,不连数据库,只靠/bin/sh兼容层和系统自带工具链。
2.1 日志轮转:为什么logrotate不是万能解,而你的脚本必须接管/var/log/app/
logrotate确实强大,但它无法解决两个硬伤:
- 当应用进程以非 root 用户启动(如
appuser),logrotate默认用 root 权限mv日志,导致新日志文件属主变成 root,应用无法继续写入; logrotate的postrotate阶段若执行kill -USR1失败,整个轮转流程静默失败,无人知晓。
真正可靠的轮转,必须由应用同用户执行,并内置状态校验:
#!/bin/bash # rotate_app_logs.sh —— 必须以 appuser 身份运行 set -euxo pipefail LOG_DIR="/var/log/app" RETAIN_DAYS=7 APP_PID_FILE="/var/run/app.pid" # 1. 校验 PID 文件存在且进程存活,避免误操作 if [[ ! -f "$APP_PID_FILE" ]] || ! kill -0 "$(cat "$APP_PID_FILE")" 2>/dev/null; then echo "ERROR: App not running or PID file missing" >&2 exit 1 fi # 2. 发送 USR1 信号触发应用自身日志切分(主流 Java/Go 应用支持) kill -USR1 "$(cat "$APP_PID_FILE")" # 3. 等待 3 秒,确保应用完成写入 sleep 3 # 4. 归档旧日志:只处理已关闭的 .log 文件(排除正在写的 current.log) find "$LOG_DIR" -maxdepth 1 -name "*.log" -type f -mtime +$RETAIN_DAYS -print0 | \ while IFS= read -r -d '' log_file; do gzip "$log_file" # 直接 gzip,不 mv 再 gzip,减少 IO echo "Rotated and compressed: $log_file.gz" done # 5. 清理超过 RETAIN_DAYS 的 .log.gz 文件 find "$LOG_DIR" -maxdepth 1 -name "*.log.gz" -type f -mtime +$((RETAIN_DAYS * 2)) -delete参数说明:
RETAIN_DAYS=7表示保留最近 7 天的日志原文;$((RETAIN_DAYS * 2))是为压缩文件设置更长保留期(避免刚压缩就被删)。-print0 | while IFS= read -r -d ''是处理含空格路径的黄金组合,比for file in $(find ...)安全 10 倍。
2.2 磁盘空间告警:df -h的坑比你想象的深,如何精准抓出/dev/sda1的 95%
df -h | awk '$5 > 90 {print $1,$5}'是新手最爱,但会翻车三次:
$5在不同 locale 下可能是Use%或Benutzt%(德语);df -h输出列数不固定(LVM 逻辑卷名可能超长,挤占列宽);/dev/mapper/vg0-lv_root这种设备名会让$1变成/dev/mapper,而非完整路径。
正确做法:强制用-P(POSIX 格式)+awk精确匹配挂载点:
#!/bin/bash set -euxo pipefail THRESHOLD=90 CRITICAL_MOUNT="/" # 使用 df -P 确保列对齐,跳过 header,用 % 字符定位使用率 df -P | awk -v threshold="$THRESHOLD" -v critical_mount="$CRITICAL_MOUNT" ' NR==1 {next} $5 ~ /%/ { # 提取纯数字(去掉 % 符号) use_pct = substr($5, 1, length($5)-1) + 0 if (use_pct > threshold && $6 == critical_mount) { print "ALERT: " $6 " usage " use_pct "% > " threshold "%" exit 1 } }'逻辑说明:
df -P强制输出固定列宽,$6永远是挂载点(/,/home等),$5永远是Use%列。substr($5,1,length($5)-1)+0将"92%"转为数值92,避免字符串比较陷阱。exit 1让脚本返回非零码,方便 crontab 配合 mailx 发邮件。
2.3 服务健康自愈:systemctl is-active不够,要能区分 “failed” 和 “activating (auto-restart)”
systemctl is-active nginx返回active时,Nginx 可能正卡在 reload 阶段,worker 进程实际已僵死。真正的健康检查必须穿透到进程层:
#!/bin/bash set -euxo pipefail SERVICE_NAME="nginx" CHECK_PORT=80 MAX_RETRY=3 # 1. 检查 systemd 状态(快速筛掉完全 dead 的) if ! systemctl is-active --quiet "$SERVICE_NAME"; then echo "$SERVICE_NAME is not active, attempting restart..." systemctl start "$SERVICE_NAME" sleep 2 fi # 2. 检查端口监听(确认 worker 进程真实存活) if ! ss -tln | grep -q ":$CHECK_PORT"; then echo "$SERVICE_NAME port $CHECK_PORT not listening" # 3. 强制 kill 僵尸进程,再重启 pkill -f "$SERVICE_NAME" 2>/dev/null || true systemctl restart "$SERVICE_NAME" sleep 3 fi # 4. 最终验证:curl 本地 127.0.0.1,超时 5 秒,失败则重试 for i in $(seq 1 $MAX_RETRY); do if curl -s --max-time 5 http://127.0.0.1/healthz >/dev/null 2>&1; then echo "$SERVICE_NAME health check passed" exit 0 fi sleep 2 done echo "$SERVICE_NAME health check failed after $MAX_RETRY attempts" exit 1关键设计:
ss -tln比netstat更快、更轻量;curl --max-time 5防止 hang 住;pkill -f确保杀死所有相关进程(包括 daemonized 子进程)。/healthz是应用暴露的轻量健康端点,比curl /更可靠。
2.4 进程内存监控:ps aux的 RSS 字段不准,要用/proc/$PID/status的VmRSS
ps aux --sort=-%mem | head -n 5显示的是“虚拟内存占比”,对 Java 应用完全失真(JVM 堆外内存不计入)。真实内存压力看VmRSS:
#!/bin/bash set -euxo pipefail MEM_THRESHOLD_KB=2097152 # 2GB TOP_N=3 # 获取所有非 kernel 进程的 VmRSS(单位 KB),排序取 Top N awk ' BEGIN { max_rss = 0 } /^(Pid|VmRSS):/ { if ($1 == "Pid:") pid = $2 else if ($1 == "VmRSS:" && pid != "") { if ($2 > ENVIRON["MEM_THRESHOLD_KB"]) { printf "%s %s KB\n", pid, $2 | "sort -k2nr | head -n '"$TOP_N"'" } pid = "" } }' /proc/[0-9]*/status 2>/dev/null | \ awk '{print "PID " $1 " uses " $2 " KB RSS memory"}'为什么不用
ps?ps的 RSS 是采样值,且对多线程进程统计不全;/proc/PID/status的VmRSS是内核实时上报的精确物理内存占用。脚本用awk直接解析/proc/*/status,避免ps的格式兼容问题。
2.5 定时任务守护:crontab -e里的@reboot不可靠,用 systemd timer 替代
@reboot依赖 cron daemon 启动时机,而某些云主机(如 AWS EC2)的 cron 可能在网络未就绪时启动,导致脚本访问不到内网 API。systemd timer 可设WantedBy=multi-user.target并After=network.target:
# /etc/systemd/system/backup.timer [Unit] Description=Daily backup timer Requires=backup.service [Timer] OnCalendar=daily Persistent=true RandomDelaySec=300 [Install] WantedBy=timers.target# /etc/systemd/system/backup.service [Unit] Description=Run daily backup script After=network.target [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh User=backupuser Restart=on-failure RestartSec=60 [Install] WantedBy=multi-user.target执行命令:
sudo systemctl daemon-reload sudo systemctl enable backup.timer sudo systemctl start backup.timer sudo systemctl list-timers --all | grep backup
Persistent=true确保机器宕机后补跑;RandomDelaySec=300避免集群内所有机器同时打存储;After=network.target保证网络就绪后再执行。
3. 避坑:Shell 脚本在生产环境翻车的 5 个血泪现场与根治方案
Shell 脚本的脆弱性不在语法,而在环境假设。以下 5 条,全部来自我亲历的 P1 故障复盘(平均每次修复耗时 4.2 小时):
3.1 现象:脚本在测试机 OK,上线后find -mtime +7什么都没删
原因:-mtime计算基于文件的mtime(修改时间),但日志轮转脚本用cp复制日志后,新文件的 mtime 是复制时刻,而非原始日志的最后写入时间。cp不保留时间戳,导致find误判“新文件才 1 天”。
解决:改用cp -p(preserve timestamp)或rsync -a:
# 错误:cp old.log new.log → new.log mtime = now # 正确:cp -p old.log new.log → new.log mtime = old.log mtime # 或更优:rsync -a --remove-source-files old.log new.log3.2 现象:df -h | awk '$5 > 90'在德国服务器上永远不告警
原因:LANG=de_DE.UTF-8下df -h输出Benutzt%列,$5变成Benutzt%文字,> 90字符串比较恒为 false。
解决:强制LC_ALL=C:
# 所有系统级脚本开头加这一行 export LC_ALL=C # 或临时生效:LC_ALL=C df -P | awk '...'3.3 现象:pkill -f "java.*app.jar"杀死了 Jenkins 的构建进程
原因:pkill -f匹配整个命令行,Jenkins 启动时也会带java -jar jenkins.war,被误杀。
解决:用pgrep精准匹配进程名 + 参数:
# 只杀当前目录下运行的 app.jar,不碰其他 java 进程 PGREP_OPTS="-f 'java.*-Dapp.home=/opt/app.*app.jar'" pkill $PGREP_OPTS 2>/dev/null || true # 更安全:先 pgrep 确认 PID,再 kill pid=$(pgrep $PGREP_OPTS) && [ -n "$pid" ] && kill "$pid"3.4 现象:for file in $(ls *.log)在日志名含空格时崩溃
原因:$(ls *.log)输出被 shell 分词,my app.log变成my和app.log两个 token。
解决:永远用glob+while read:
# 错误 for file in $(ls *.log); do ... done # 正确(推荐) while IFS= read -r file; do [ -n "$file" ] && process "$file" done < <(find . -maxdepth 1 -name "*.log" -print0 | xargs -0 -n1 basename) # 或更简洁(如果确定无空格) for file in *.log; do [ -e "$file" ] && process "$file" # 防止无匹配时循环一次 "$file" 字符串 done3.5 现象:crontab执行脚本时PATH缺失,python3命令找不到
原因:cron 的PATH默认只有/usr/bin:/bin,不包含/usr/local/bin(Python3 常在此)。
解决:在 crontab 中显式声明 PATH,或脚本内which python3:
# /etc/crontab 开头加 PATH=/usr/local/bin:/usr/bin:/bin # 或脚本内(更可靠) PYTHON_CMD=$(which python3) if [ -z "$PYTHON_CMD" ]; then echo "python3 not found" >&2 exit 1 fi "$PYTHON_CMD" /path/to/script.py4. 参数调优:让脚本从“可用”升级为“生产级”的 7 个必调开关
写完脚本能跑,只是起点。要让它在 200 台服务器上稳定运行 365 天,这 7 个参数必须逐个校准:
4.1set -euxo pipefail:不是可选项,是准入门槛
-e:任一命令失败立即退出(grep未找到时返回 1,会终止);-u:引用未定义变量时报错(避免cd $DIR因$DIR为空而 cd 到根目录);-x:打印执行命令(调试用,上线前可注释);-o pipefail:管道中任一命令失败即整体失败(cmd1 | cmd2中cmd1失败时,cmd2不再执行)。
血泪经验:某次
rsync因网络中断失败,但因没pipefail,后续rm -rf仍执行,清空了目标目录。从此所有脚本第一行必写set -euxo pipefail。
4.2IFS=$' \t\n':重置字段分隔符,防空格路径崩坏
默认IFS包含空格、制表符、换行符,但某些环境(如 Docker)会篡改。显式重置:
# 脚本开头 IFS=$' \t\n' # 或更严格(仅空格和换行) IFS=' '4.3umask 0022:确保生成文件权限可控
避免脚本创建的临时文件权限为600(仅 owner 可读),导致其他用户无法审计:
umask 0022 # 新建文件 644,目录 7554.4timeout包裹关键命令:防curl/ssh永久 hang
# 用 timeout 限制最大执行时间 if ! timeout 30s curl -s --max-time 10 http://api.internal/health; then echo "API timeout, triggering fallback" fallback_action fi4.5flock -n /tmp/script.lock:同一脚本多实例互斥
防止 crontab 间隔过短,前次脚本未结束,下次又启动:
exec 200>/tmp/rotate_logs.lock if ! flock -n 200; then echo "Another instance is running, exiting" exit 1 fi # 脚本主体... # 退出时自动释放锁(fd 200 关闭)4.6printf '%(%Y-%m-%d %H:%M:%S)T' -1:用strftime生成 ISO 时间,不依赖date
date命令在 Alpine Linux 等精简镜像中可能缺失,而printf是 bash 内置:
# 安全获取当前时间 TIMESTAMP=$(printf '%(%Y-%m-%d %H:%M:%S)T' -1) echo "[$TIMESTAMP] Starting rotation..."4.7declare -r VAR=value:声明只读变量,防误覆盖
declare -r LOG_DIR="/var/log/app" declare -r RETAIN_DAYS=7 # 若脚本中出现 LOG_DIR="/tmp",bash 直接报错5. 验证与上线:一套可落地的 4 层验证法,让脚本从开发机走向生产
写完脚本不是终点,验证才是。我坚持的 4 层验证,缺一不可:
5.1 第一层:本地沙箱验证(10 分钟)
- 创建隔离目录:
mkdir /tmp/test_env && cd /tmp/test_env - 模拟生产结构:
mkdir -p logs/{app,nginx} && touch logs/app/{access.log,error.log} - 手动执行脚本,观察输出、文件变化、退出码:
./rotate_app_logs.sh echo $? # 必须为 0 ls -l logs/app/ # 应有 access.log.gz, error.log.gz
5.2 第二层:模拟生产负载压测(30 分钟)
- 用
stress-ng模拟高 CPU/IO:stress-ng --cpu 4 --io 2 --vm 1 --vm-bytes 512M --timeout 300s & # 同时循环执行脚本 100 次 for i in $(seq 1 100); do ./rotate_app_logs.sh; done - 检查是否出现
No space left on device(脚本未清理临时文件)、Text file busy(未等进程释放文件锁)。
5.3 第三层:灰度发布(2 小时)
- 选 3 台非核心服务器,部署脚本并加入 crontab:
# 加入 crontab,每 5 分钟执行一次(非生产频率) */5 * * * * /usr/local/bin/rotate_app_logs.sh >> /var/log/rotate_debug.log 2>&1 - 监控 2 小时:
tail -f /var/log/rotate_debug.log,确认无 ERROR,df -h磁盘使用率平稳下降。
5.4 第四层:全量上线与熔断机制(持续)
- 全量部署后,必须配置熔断:
# 在脚本开头加入熔断检查 if [ -f /tmp/rotate_disabled.flag ]; then echo "Rotation disabled by flag" exit 0 fi - 当发现异常(如连续 3 次失败),手动
touch /tmp/rotate_disabled.flag,人工介入排查。 - 同时,所有脚本输出必须重定向到独立日志,并用
logrotate管理:# /etc/logrotate.d/rotate_scripts /var/log/rotate_*.log { daily missingok rotate 30 compress delaycompress notifempty create 644 root root }
我现在所有生产脚本都遵循这套验证流程。去年双十一前,一个磁盘清理脚本在灰度层发现
find -delete在 ext4 上偶发卡住(内核 bug),及时切换为find -print0 | xargs -0 rm,避免了大促当天的雪崩。Shell 脚本不是“写完就扔”,它是你运维能力的实体化延伸——每一次chmod +x,都该带着敬畏。希望帮到你。
本文还有配套的精品资源,点击获取