279个Shell脚本实战:从Nginx日志切割、DDoS封禁到MySQL备份的运维避坑指南

发布时间:2026/10/16 17:43:39
279个Shell脚本实战:从Nginx日志切割、DDoS封禁到MySQL备份的运维避坑指南
简介这是2024年新版推出的279个开箱即用Shell脚本合集整个资源只含一个PDF文档压缩包大小约4.69MB适合Linux/Unix运维人员、系统管理员以及希望提升自动化能力的脚本学习者。内容覆盖DDoS攻击自动屏蔽、Linux系统发送告警、MySQL数据库单循环与多循环备份、Nginx访问日志按天切割与深度分析、网卡实时流量查看等高频运维场景每个脚本都包含可复用的关键命令和注释方便直接抄录到生产环境并二次修改。文档内部按功能模块组织既是一本随身运维手册也可作为Shell编程教学案例帮助读者理解日志采集、iptables防火墙策略、mysqldump备份、cron定时任务、正则匹配和awk统计等实战方法并将复杂命令拆解为易理解的步骤。目前已有154人学习下载适合需要快速筛选可用脚本、提升服务器管理效率的开发者参考。1. 开箱即用的 279 个 Shell 脚本先去平台化再去踩坑一份「279 个开箱即用的 Shell 脚本」合集摆在面前很多人第一反应是收藏第二反应是不敢跑。毕竟脚本这东西网上下的包里有玄学变量没替换、路径不对、权限不够随便一个都能让自动化变成事故现场。这份合集覆盖了运维日常最常碰的场景Nginx 日志切割与分析、DDoS 攻击自动封禁、MySQL 数据库备份、系统初始化、批量服务器巡检、进程状态统计、企业微信告警、FTP 与 SSH 客户端封装。与其说它是一份教程不如说是一个软件/插件式的运维工具包。适合刚接手服务器的新手快速抄作业也适合熟手拿去做二次改造——但前提是你得知道每个脚本的边界在哪、哪些参数必须换、哪些逻辑本身就有问题。2. 日志与安全的四板斧Nginx 切割、访问分析和 DDoS 自动封禁怎么改参数日志处理和安全防护是运维最早碰到的需求也是这份脚本包里最具「拿来就能用」气质的部分。但这一段恰恰是最容易翻车的地方因为涉及日志轮转、信号机制、防火墙规则三条线任何一环没想清楚脚本跑起来都是表面正常、实际无效。2.1 按天切割 Nginx 日志mv 之后必须做的事Nginx 的 access.log 如果不切几个月就能把磁盘撑爆。常规做法是每天 0 点把日志挪走再让 Nginx 重新打开新文件。脚本合集里那套按天切割的写法核心逻辑如下#!/bin/bash LOG_DIR/usr/local/nginx/logs YESTERDAY_TIME$(date -d yesterday %F) LOG_MONTH_DIR$LOG_DIR/$(date %Y-%m) LOG_FILE_LISTdefault.access.log for LOG_FILE in $LOG_FILE_LIST; do [ ! -d $LOG_MONTH_DIR ] mkdir -p $LOG_MONTH_DIR mv $LOG_DIR/$LOG_FILE $LOG_MONTH_DIR/${LOG_FILE}_${YESTERDAY_TIME} done kill -USR1 $(cat /var/run/nginx.pid)这个脚本的核心不在 mv而在最后一行kill -USR1。Nginx 主进程收到 USR1 信号后会重新打开日志文件否则你 mv 走旧文件Nginx 的文件句柄还指着它日志照样往旧文件里写切割等于没切。参数上有几个点要按自己环境改LOG_DIR是 Nginx 日志根目录LOG_FILE_LIST支持多个站点日志用空格隔开想切 error.log 就加进去/var/run/nginx.pid的路径以你实际编译安装的位置为准用nginx -V或ps -ef | grep nginx都能确认。如果 Nginx 是编译安装在 /usr/local/nginx 下pid 文件一般就在/usr/local/nginx/logs/nginx.pid照抄默认路径会 kill 失败。切割频率也建议改一下脚本本身按天切但访问量大的站点建议按小时甚至按日志大小切。常见做法是配合 cron 每天 0 点执行加上日志保留策略比如只保留最近 30 天的切割文件否则切完不清理磁盘压力只是换了个地方堆积。2.2 日志分析脚本UV、PV、TOP IP 的统计口径拿到切割后的日志下一步就是分析。合集里的日志分析脚本用 awk 一把梭统计访问最多的 IP、时间段访问量、访问最多的页面、状态码分布。核心统计片段长这样# 统计访问最多的 10 个 IP同时输出 UV awk {a[$1]}END{print UV:,length(a);for(v in a)print v,a[v]} $LOG_FILE | sort -k2 -nr | head -10 # 统计时间段内访问最多的 IP awk $4[01/Dec/2018:13:20:25 $4[27/Nov/2018:16:20:49{a[$1]}END{for(v in a)print v,a[v]} $LOG_FILE | sort -k2 -nr | head -10 # 统计访问最多的 10 个页面带 PV awk {a[$7]}END{print PV:,length(a);for(v in a){if(a[v]10)print v,a[v]}} $LOG_FILE | sort -k2 -nr这里几个参数要拆开看。a[$1]的$1是日志第一列也就是$remote_addr统计 IP 必须用这个字段。sort -k2 -nr表示按第二列做数值逆序排序-n保证 100 排在 99 后面而不是按字符串排-r是从大到小。head -10控制输出条数想看 Top 20 就改成head -20。时间段过滤那行用的是$4对应当前日志格式的[$time_local]需要注意里面带方括号和冒号awk 里比较字符串时[不能转义错误否则直接匹配不到任何行。状态码统计那段a[$7 $9]把页面和状态码拼在一起做聚合目的是看哪些页面产生了大量 5xx这种问题定位比看单一指标更直接。这套 awk 统计的边界在于它只能处理标准格式的日志如果 Nginx 配置里改了 log_format字段位置全变脚本就要跟着改。想省事的话我一般会直接把它接到 cron 里每天生成一份日报再把结果路径固化下来比手工跑靠谱得多。2.3 DDoS 攻击自动封禁从日志到 iptables 的阈值与边界防护脚本的思路很直接盯着 Nginx 日志把最近一段时间内请求次数超过阈值的 IP 直接丢进 iptables INPUT 链 DROP 掉。脚本核心部分#!/bin/bash DATE$(date %d/%b/%Y:%H:%M) LOG_FILE/usr/local/nginx/logs/demo2.access.log ABNORMAL_IP$(tail -n5000 $LOG_FILE | grep $DATE | awk {a[$1]}END{for(i in a)if(a[i]10)print i}) for IP in $ABNORMAL_IP; do if [ $(iptables -vnL | grep -c $IP) -eq 0 ]; then iptables -I INPUT -s $IP -j DROP echo $(date %F_%T) $IP /tmp/drop_ip.log fi done几个参数决定这套防护的灵敏度。tail -n5000是回溯窗口只看最近 5000 行日志攻击量大的时候这 5000 行可能只覆盖几十秒阈值 10 很容易被触发。grep $DATE匹配当前分钟的时间戳保证统计颗粒度是「这一分钟内的请求」。if(a[i]10)print i里的 10 就是每分钟阈值内部业务系统可以调到 50 甚至 100对外服务的网站建议先按 30 观察再压。这段脚本有边界必须知道它只能挡应用层的小流量突刺真正的流量型 DDoS 在带宽层面就把入口打满了Nginx 日志根本写不进去脚本也跑不起来。另外iptables -vnL | grep -c $IP这种判断方式在规则条数多了以后容易误判——比如已经封了1.2.3.0/24网段再检查1.2.3.100时 grep 匹配不到就会重复插规则。更稳的做法是用ipset存黑名单判断和封禁都走集合操作脚本合集的优点是有完整逻辑可以参考缺点是没有 ipset 版本需要自己改写。配套的 80 端口请求分析脚本也是同一思路netstat -ant统计 :22 端口连接数最高的前 20 个 IP如果第 20 名的请求数还大于 500就执行sar -A导出系统活动报告到 alert.txt。这个脚本适合排查 SSH 暴力破解和端口扫描注意-F[ :]是 netstat 输出的字段分隔符写法awk 里改成别的分隔符会导致$4取不到源 IP。3. 数据备份与系统初始化MySQL 循环备份的取舍和初始化脚本的边界数据库备份和服务器初始化是这份合集里另一个高频区域。MySQL 备份脚本有两个版本单循环和多循环摆在一起看很有意思——它们解决的是不同量级的问题选错方案就是给自己挖坑。系统初始化脚本则是一把双刃剑适合新机器不适合已有业务的服务器。3.1 MySQL 单循环备份逻辑清晰但藏着三个隐患单循环脚本的逻辑是先查出所有数据库列表过滤掉系统库再逐库 mysqldump。核心代码#!/bin/bash DATE$(date %F_%H-%M-%S) HOSTlocalhost USERbackup PASS123.com BACKUP_DIR/data/db_backup DB_LIST$(mysql -h$HOST -u$USER -p$PASS -s -e show databases; 2/dev/null | egrep -v Database|information_schema|mysql|performance_schema|sys) for DB in $DB_LIST; do BACKUP_NAME$BACKUP_DIR/${DB}_${DATE}.sql if ! mysqldump -h$HOST -u$USER -p$PASS -B $DB $BACKUP_NAME 2/dev/null; then echo $BACKUP_NAME 备份失败! fi done这个脚本能用的前提是把HOST、USER、PASS、BACKUP_DIR四个变量全改掉。-B参数很关键它会在导出文件里自动带上CREATE DATABASE和USE语句恢复的时候不用手动建库。egrep -v过滤掉了 information_schema、mysql、performance_schema、sys 这几个系统库但如果你自己建过test库或者其他公共库正则里没排除照样会备份。隐患有三个。第一密码明文写在脚本里任何能读这个文件的人都能拿到数据库密码我一般建议改成从/etc/my.cnf的[client]段读取或者用MYSQL_PWD环境变量配合权限控制。第二2/dev/null把 mysqldump 的报错全吞了出问题时只会看到一个「备份失败」的提示具体是连接失败还是权限不足根本看不到排查全靠瞎猜。第三备份文件没做压缩数据量上来后磁盘占用非常快建议在重定向后加一条gzip处理或者直接用mysqldump ... | gzip ${BACKUP_NAME}.gz。3.2 多循环备份每张表一个文件恢复更灵活但文件数爆炸多循环脚本在单循环外面又套了一层先查库再进每个库查表每个表导出一个独立的 SQL 文件。核心片段for DB in $DB_LIST; do BACKUP_DB_DIR$BACKUP_DIR/${DB}_${DATE} [ ! -d $BACKUP_DB_DIR ] mkdir -p $BACKUP_DB_DIR TABLE_LIST$(mysql -h$HOST -u$USER -p$PASS -s -e use $DB;show tables; 2/dev/null) for TABLE in $TABLE_LIST; do BACKUP_NAME$BACKUP_DB_DIR/${TABLE}.sql if ! mysqldump -h$HOST -u$USER -p$PASS $DB $TABLE $BACKUP_NAME 2/dev/null; then echo $BACKUP_NAME 备份失败! fi done done多循环的价值在恢复粒度线上出了问题时如果只是某一张大表的数据被误删单循环方式要导入整个库的文件时间成本和风险都高多循环方式直接挑那一张表的 SQL 恢复就行。但代价也很明显表一多目录下文件数量爆炸inode 吃紧备份耗时也会变长。我的建议是分场景选型中小业务、表数量百张以内、单表不大用单循环加 gzip 最省事核心库有几百 GB 且需要频繁单表恢复的再上多循环。别把两种脚本都跑上那是重复浪费磁盘和 IO。另外这两种脚本都缺一个「备份完自动清理旧文件」的环节记得自己加一条find $BACKUP_DIR -type f -name *.sql -mtime 15 -delete保底。3.3 系统初始化脚本新机器一把梭但要逐行确认再跑初始化脚本是这份合集里改动风险最大的一份它会把时区、SELinux、防火墙、历史命令、SSH 配置、文件描述符、内核参数全改一遍。给新装的 CentOS 机器跑没问题给线上跑就是事故苗子。# 设置时区并同步时间 ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 历史命令显示操作时间 echo export HISTTIMEFORMAT%F %T whoami /etc/bashrc # SSH 超时时间 echo export TMOUT600 /etc/profile # 禁止 root 远程登录 sed -i s/#PermitRootLogin yes/PermitRootLogin no/ /etc/ssh/sshd_config # 设置最大打开文件数 echo * soft nofile 65535 /etc/security/limits.conf echo * hard nofile 65535 /etc/security/limits.conf # 内核参数优化 cat /etc/sysctl.conf EOF net.ipv4.tcp_syncookies 1 net.ipv4.tcp_max_tw_buckets 20480 net.ipv4.tcp_max_syn_backlog 20480 net.core.netdev_max_backlog 262144 net.ipv4.tcp_fin_timeout 20 EOF逐行过一遍就能看出问题时区那行用ln -s硬链接 /etc/localtime如果系统已经是别的时区这个软链会覆盖但不会清理旧文件TMOUT600设置全局 10 分钟超时对跑长任务的会话是噩梦建议按用户维度配置而不是写进全局 profilePermitRootLogin no改完之前必须确认已经有一个 sudo 用户能登上去否则重启后你自己也进不来这就是典型的「后悔药都来不及吃」。内核参数里tcp_syncookies 1是防 SYN 攻击的经典选项tcp_max_tw_buckets控制 TIME_WAIT 数量上限tcp_fin_timeout 20缩短 FIN 等待时间这几个值对高并发短连接场景友好对长连接为主的数据库服务器反而可能引发连接异常。脚本里还包含 ntpdate 定时同步、IPv4 转发、swappiness 调低等操作其中 swappiness 改的是/proc/sys/vm/swappiness重启就还原要持久化必须再写进 sysctl.conf。这份脚本的正确打开方式是新装完系统、没接业务之前跑一遍跑完reboot验证 SELinux 状态和 SSH 登录都符合预期。如果机器已经上了业务只挑其中无害的几项手工执行比如 HISTTIMEFORMAT、limits、sysctl防火墙和 SELinux 的改动宁可不做。4. 批量与监控场景多机磁盘巡检、并发 SSH 和进程采集脚本的取舍当服务器数量从一台变成几十台Shell 脚本的价值才真正显现。合集里这组脚本把「批量」和「并发」玩得比较典型但也有一些写法只适合小型环境量大了必须改造成可靠的方式不能无脑照抄。4.1 host.info 驱动的多机磁盘巡检清单文件比脚本本身更重要磁盘巡检脚本没有把 IP 写死在脚本里而是用一个 host.info 清单文件驱动这个设计值得单独拿出来讲。清单格式是每行三个字段IP、用户名、SSH 端口脚本靠 awk 按 IP 关联取值#!/bin/bash HOST_INFOhost.info for IP in $(awk /^[^#]/{print $1} $HOST_INFO); do USER$(awk -v ip$IP ip$1{print $2} $HOST_INFO) PORT$(awk -v ip$IP ip$1{print $3} $HOST_INFO) TMP_FILE/tmp/disk.tmp ssh -p $PORT $USER$IP df -h $TMP_FILE USE_RATE_LIST$(awk BEGIN{OFS}/^\/dev/{print $NF,int($5)} $TMP_FILE) for USE_RATE in $USE_RATE_LIST; do PART_NAME${USE_RATE%*} USE_RATE${USE_RATE#*} if [ $USE_RATE -ge 80 ]; then echo Warning: $PART_NAME Partition usage $USE_RATE%! fi done done-v ip$IP是 awk 传外部变量的标准姿势脚本里在单引号内取$1做匹配比拼接字符串安全。OFS把分区和使用率拼成分区百分比的形式后面用${USE_RATE%*}和${USE_RATE#*}拆开这种写法比cut -d -f1少一次管道纯 Shell 风格的取舍值得学。边界在这几处df -h的输出在不同系统上列数有差异/^\/dev/只匹配以 /dev 开头的行像 overlay、tmpfs 这些不会被统计阈值 80 是写死的想按环境调就改成USE_RATE -ge $ALARM_VALUE并支持外部传参。巡检日志目前只打印到标准输出没有落盘建议改成追加到/var/log/disk_check.log并加上时间戳。另外这个脚本必须配好 SSH 免密登录才能跑通否则循环卡在密码输入那一步这就是后话避坑章要展开的内容。4.2 并发 SSH 采集主机名并排序 与 wait 的粗粒度并发另一个批量脚本解决的是「同时操作多台机器」的问题对一组 IP 并发执行 SSH拿到每台机器的 hostname 和往返耗时落盘到 hostname.txt最后找出耗时最短的那台机器并拉取它的 top 输出。并发关键代码#!/bin/bash ALL_HOSTS(192.168.1.10 192.168.1.11) for host in ${ALL_HOSTS[*]}; do { start_time$(date %s) ssh $host hostname /dev/null sleep 2 stop_time$(date %s) time_consuming$((stop_time-start_time)) echo $host: $time_consuming hostname.txt } done wait host$(sort -n -k 2 hostname.txt | head -1 | awk -F: {print $1}) ssh $host top -b -n 1花括号包起来的代码块最后加一个整个块就变成后台任务for 循环一次性把全部 SSH 丢到后台wait等待所有任务结束。这是 Shell 并发最简单可靠的写法不需要 xargs -P 也不需要 GNU parallel。耗时统计里的sleep 2是人为加进去的缓冲目的是让每台机器的耗时有个可比较的基线实际用的时候按网络状况删掉或调整。需要注意echo $host: $time_consuming hostname.txt是追加写多进程并发追加会不会串行Shell 的重定向对短行写入是原子的在低并发场景下问题不大但量大了还是建议每个任务写独立临时文件最后再合并。sort -n -k 2只对空格分隔的文本有效hostname.txt 里是IP: 耗时格式awk 指定-F:取第一段就是 IP。这个脚本适合用来做「找出最空闲或网络最好的节点」这类粗粒度判断不适合做严谨的性能基准测试因为 SSH 握手开销和 DNS 解析时间都会混进耗时的统计里。4.3 /proc 进程状态统计、jstack 定时采集与 GC 日志均值这批脚本走的是「从 /proc 和日志文件里直接抠数据」的路子不依赖额外监控工具。进程状态统计脚本遍历 /proc 下所有数字目录读每个进程的 status 文件里的 State 字段ALL_PROCESS$(ls /proc/ | egrep [0-9]) for pid in ${ALL_PROCESS[*]}; do test -f /proc/$pid/status state$(egrep State /proc/$pid/status | awk {print $2}) case $state in R) running_count$((running_count1)) ;; T) stoped_count$((stoped_count1)) ;; S) sleeping_count$((sleeping_count1)) ;; Z) zombie_count$((zombie_count1)) echo $pid zombie.txt kill -9 $pid ;; esac done从/proc/$pid/status读 State 比ps aux更直接因为ps本身也读 /proc多一层解析反而可能漏掉瞬时状态。Zombie 的kill -9值得商榷——僵尸进程已经死掉kill -9杀的是它的父进程没回收正确做法是处理父进程或者等 init 回收这里直接 kill 实际效果取决于父进程行为写进脚本里当清理手段没问题别指望它根治。jstack 采集脚本的意图是每小时对指定 Java 进程打一次线程快照并控制目录内文件数量不超过 10 个超过就删最旧。但有一个明显问题CURRENT_TIME$(date %F-%H:%M:%S)写在 while 循环外面导致所有 jstack 输出全部追加到同一个文件滚动删除逻辑失效文件无限变大。修正很简单把CURRENT_TIME移进循环体内每次迭代重新生成。采集的关键字/inceptor/也要改成你自己的 Java 进程名grep inceptor | grep -v grep这种过滤方式在进程名带特殊字符时还得加引号。GC 日志统计用 awk 从 hive-server2.log 里截取第二列tr -d :去掉冒号后累加求平均和最大值。这个脚本的字段位置完全取决于你的 GC 日志格式换了 JDK 版本或换打印参数后第二列可能就不是耗时跑之前先手工head -5看一眼格式别让统计结果成为新的黑匣子。5. 落地避坑实录脚本直接照抄常见的五个翻车点279 个脚本说「开箱即用」但真正开箱即用的是整理过、验证过、参数替换过的那部分。以下五个坑是脚本合集里最容易让人栽跟头的地方按「现象、原因、解决」写清楚省得你们再踩一遍。5.1 SELinux 改完等于没改现象跑完系统初始化脚本getenforce输出还是 EnforcingSELinux 依旧开启。原因脚本里那行sed -i /SELINUX/{s/permissive/disabled/} /etc/selinux/config只把SELINUXpermissive替换成disabled如果你的配置本来就是SELINUXenforcing正则匹配后替换命令不生效等于白跑。解决把替换改成直接强制赋值sed -i s/^SELINUX.*/SELINUXdisabled/ /etc/selinux/config跑完确认一下grep ^SELINUX /etc/selinux/config看到disabled才算真的改掉。注意改 SELINUX 必须重启才生效测试机上验证完再批量。5.2 Nginx 日志切割后文件还在涨现象mv 完旧日志第二天发现 access.log 没变小的同时昨天切出去的文件却在继续增长。原因Nginx 还握着旧文件句柄你 mv 之后没有向 master 进程发送USR1信号Nginx 依旧往原 inode 上写。解决在 mv 循环结束后追加信号命令kill -USR1 $(cat /var/run/nginx.pid)如果 Nginx 是编译安装的pid 路径可能不是/var/run用ps -ef | grep nginx找到 master 进程号直接传。注意执行 kill 的用户要对 pid 文件有读权限root 跑最保险放到 cron 里时确保脚本属主是 root。5.3 FTP 上传脚本报错或传了个空文件现象用脚本包里 FTP 客户端的 upload 方法传文件抛出异常或者传上去的是 0 字节文件。原因storbinary的第二个参数需要的是一个已打开的二进制文件对象原脚本把fp open(localpath, rb)注释掉直接传了文件路径字符串进去这个 bug 在纯 Python 代码里一眼就能看到。解决改为先打开文件再传递def upload(self, localpath, remotepathNone): if not localpath: return Please select a local file. if not remotepath: remotepath path.basename(localpath) with open(localpath, rb) as fp: self._ftp.storbinary(STOR remotepath, fp)用 with 语法确保文件用完自动关闭比原脚本里手动fp.close()更稳。顺便检查 download 方法里retrbinary(RETR remotepath, fp.write)的写文件权限下载目录不存在时会抛 IOError。5.4 iptables 封禁重启即失忆现象DDoS 封禁脚本明明把攻击 IP 加进了 iptables服务器一重启防护规则全没了同样的攻击又来了。原因iptables -I INPUT -s $IP -j DROP只是内存里的规则没有持久化到磁盘重启后 iptables 服务加载的是空的规则集。解决封禁脚本跑完后执行规则保存iptables-save /etc/sysconfig/iptablesCentOS 7 及以后如果没启用 iptables-services还要先systemctl enable iptables让规则开机自动加载。更彻底的方案是把封禁逻辑改成 ipset 加 TTL 超时自动解封黑名单不存在持久化问题攻击停了 IP 自动释放这才是应对短时攻击的正道。5.5 批量 SSH 巡检卡到天荒地老现象执行 100 台服务器的磁盘巡检脚本第一个 IP 就停住不动或者整个循环跑完要一个多小时。原因目标机器的 SSH 没配免密登录脚本进入交互式密码输入状态没有输入自然卡死。就算配了密码串行循环每台机器一次 SSH 握手100 台就是 100 次秒级延迟时间全耗在等待上。解决先铺 SSH key再改脚本加上 BatchModessh-keygen -t rsa -N -f ~/.ssh/id_rsa ssh-copy-id -i ~/.ssh/id_rsa.pub root192.168.1.10脚本里加两个选项让没配 key 的机器立即失败而不是卡住ssh -o BatchModeyes -o StrictHostKeyCheckingno -p $PORT $USER$IP df -hBatchModeyes禁止交互式输入密码StrictHostKeyCheckingno跳过首次连接的 host key 确认。加上这两行以后脚本要么秒过要么快速报错不会再出现半夜跑批卡死到天亮的悲剧。6. 把 279 个脚本变成运维资产统一三件套、静态检查和灰度验证脚本攒到 279 个最大的问题已经不是「没有脚本」而是「这个脚本怎么保证是可维护的」。我拿到这类合集的第一步不是直接跑而是把里面自己需要用到的脚本全部过一遍「三件套」改造顺手再过一个 shellcheck。这套习惯花不了多少时间但能避免非常多线上事故。三件套的第一件是脚本头部加严苛参数#!/bin/bash set -euo pipefail LOG_DIR/var/log/scripts log() { echo [$(date %F %T)] $* $LOG_DIR/$(basename $0).log; } main() { local backup_dir${1:-/data/backup} # 具体逻辑 } main $set -e让脚本在第一个出错命令处退出-u对未定义变量直接报错-o pipefail让管道中任何一段失败都算整体失败。这三个参数能让几百个脚本里「mysqldump 失败但脚本继续往下走」这类静默故障直接暴露出来。配合一个 log 函数统一记录时间戳排查问题不用再去猜脚本是哪一秒跑的、第几步挂的。第二件是静态检查。Shell 脚本没有编译器shellcheck 就是 lintershellcheck -S warning backup_mysql.sh跑完会报三类问题没加引号的变量、可移植性问题、明显的逻辑错误。批量检查整个目录的话用shellcheck -S warning /opt/scripts/*.sh几百个脚本一次扫完重点看 error 级别的问题warning 级别按需处理。第三件是调用方式标准化。脚本类的资源最容易死在「手工跑没问题cron 跑就出岔子」因为 cron 环境没有你的 PATH 和变量。统一模板30 2 * * * /opt/scripts/backup_mysql.sh /var/log/scripts/cron.log 21脚本内部固定用/usr/bin/mysqldump这类绝对路径变量声明全部移到 main 函数里外部参数通过$传进去这样同一个脚本既能手工调试也能直接上 cron。灰度验证我是从一次数据恢复演练里长出来的教训有一次新写的备份脚本跑了一周都没出声等到真要恢复数据才发现 SQL 文件是坏的原因是 mysqldump 的退出码被管道吞掉了。从那以后我每次拿到新脚本都会强制走一遍三件套流程再把脚本丢到测试机上用 mock 数据跑三次确认退出码为 0、输出文件内容可解析最后才挂 cron。脚本不是写出来就行能扛住线上环境才算真的落地。这份合集里的 279 个脚本也一样挑出适合你场景的那几十个过一遍 shellcheck、改掉变量和路径跑通了再谈效率。希望帮到你。本文还有配套的精品资源点击获取