简介:一份面向Linux运维人员及初学者的常用自动化脚本合集,围绕服务器日常管理中常见的效率痛点,给出可直接参考的Shell实现。脚本覆盖MySQL数据库备份与主从同步状态监控、批量创建用户、批量远程执行命令、网站异常检测、LNMP一键部署、Java项目自动发布、Dos攻击自动屏蔽IP、nginx日志按天切割与分析、服务器资源利用率查看等高频场景。资源包共20个文件,包含19个Shell脚本与1个nginx.conf配置文件,整体仅12KB,轻量实用,便于按需挑选或二次改造。目前已有82人学习下载。这些脚本不是零散命令的堆砌,而是具有明确运维意图的实用工具集,既能帮助新手理解自动化运维的思路,也能让有经验的工程师直接复用,提升日常巡检、备份与故障处理效率。
1. Linux运维常用脚本:先弄清楚这堆脚本到底在解决什么问题
Linux运维常用脚本,听起来像是一堆现成命令的堆叠,实际上它是把运维里最频繁、最容易出错的重复动作,固化成可复用、可审计、可交接的代码。比如凌晨两点磁盘告警、新机器上线要装一堆基础组件、几十台机器批量改配置,这些场景靠人工敲命令不是不行,但会累、会漏、会因人而异,脚本就是把这些动作从「靠记忆」变成「靠代码」。
这篇文章不谈花哨的框架,只讲一线运维真正每天在用的那几类脚本:环境判断、危险命令保护、监控告警、日志清理、批量执行,以及每个脚本里最容易踩的坑。适合刚接手服务器、还没建立自己脚本库的新手,也适合写了几年脚本但偶尔被 rm 和 find 坑一把的熟手。看完你能直接照着抄,也能知道自己以前为什么翻车。
2. 动手前的三条铁规矩:环境判断、rm 后悔药、远程执行护栏
写运维脚本和写业务代码最大的区别是:业务代码跑挂了可以回滚,运维脚本跑挂了可能直接让服务起不来。所以动手写任何功能之前,先把护栏做好。我一般会先做三件事:判断当前机器身份、给危险的删除命令加后悔药、给远程执行加超时和回显。
2.1 环境判断脚本:动任何命令之前先搞清楚「我是谁」
运维脚本经常要在不同发行版、不同用户身份的机器上跑。同一个脚本,root 下能删的文件,普通用户下可能直接 Permission denied;CentOS 的 yum 和 Ubuntu 的 apt 命令完全不一样。所以脚本开头第一件事,不是写功能,而是写「我是谁、我在哪」。
#!/bin/bash # env_check.sh:执行正式操作前先输出环境信息 set -uo pipefail # 判断当前用户是否为 root,1 表示是,0 表示否 if [ "$(id -u)" -eq 0 ]; then IS_ROOT=1 else IS_ROOT=0 fi # 读取发行版信息,兼容 CentOS / Ubuntu / Debian 等 if [ -f /etc/os-release ]; then . /etc/os-release OS_FAMILY="$ID" else OS_FAMILY="unknown" fi # 输出内核版本和关键架构信息 KERNEL=$(uname -r) ARCH=$(uname -m) echo "uid=$(id -u) root=${IS_ROOT} 发行版=${OS_FAMILY} 内核=${KERNEL} 架构=${ARCH}" # 检查关键目录是否存在,不存在则提前告警 for d in /etc /var/log /opt; do [ -d "$d" ] || echo "警告:目录 $d 不存在" done这段脚本里有几个点需要说明。id -u拿到的用户 ID,root 固定是 0,这是判断权限最可靠的方式,比whoami匹配字符串更稳。/etc/os-release是现代发行版都带的标准文件,source 之后直接就能拿到ID这个变量,比去解析/etc/redhat-release兼容性好得多。开头我用set -uo pipefail而不是set -e,因为这是检查脚本,某个检查项失败不应该让整个脚本退出,而是要跑完所有检查、把问题全部暴露出来。uname -r和uname -m在某些脚本里判断是否需要装特定内核模块时非常有用,提前打印出来能省掉后面排查的时间。
实际使用中,这个脚本通常会作为其他脚本的「前置片段」被 source 进去,而不是单独运行。常见做法是把它抽成一个lib_env.sh,其他脚本开头写source /opt/scripts/lib_env.sh,这样所有脚本对环境的判断逻辑只有一个地方维护,不会出现 A 脚本认为自己是 CentOS、B 脚本认为自己是 Ubuntu 的割裂情况。
2.2 给 rm 加一道后悔药:回收站脚本与自动清理
rm -rf是运维界最贵的命令,没有之一。我见过不止一次有人把变量写错,rm -rf ${DIR}/变成rm -rf /,或者清理脚本里路径变量没赋值,直接变成rm -rf。人肉防线不可靠,得靠机制。最实用的做法是给 rm 建一个回收站,删除变成移动,后悔了还能捞回来。
#!/bin/bash # install_trash.sh:给当前用户安装回收站机制 TRASH_DIR="${HOME}/.trash" mkdir -p "$TRASH_DIR" # 向 ~/.bashrc 追加回收站函数和别名 cat >> ~/.bashrc <<'EOF' # 回收站函数:把文件移动到 ~/.trash,保留时间戳后缀 trash() { local ts ts=$(date +%Y%m%d_%H%M%S) for f in "$@"; do [ -e "$f" ] || [ -L "$f" ] && mv "$f" "${HOME}/.trash/${f##*/}_${ts}" done } # 用 trash 覆盖 rm,避免手滑 alias rm=trash # 给习惯了 del 的人一个入口 alias del=trash EOF echo "回收站已安装到 ${TRASH_DIR}" echo "执行 source ~/.bashrc 后生效"这里有个细节值得注意:函数里用了[ -e "$f" ] || [ -L "$f" ],因为-e对损坏的符号链接会返回假,但符号链接本身也是要删的对象,加上-L判断才完整。${f##*/}是 bash 参数扩展,取路径的最后一段,防止/var/log/a.log被移动后名字变成/var/log/a.log_时间戳这种带斜杠的怪名字。
光有回收站不够,还得有自动清理策略。不然.trash目录会无限膨胀。我一般会在 crontab 里加一行:
# 每天凌晨 3 点清理回收站里超过 7 天的文件 0 3 * * * find "${HOME}/.trash" -type f -mtime +7 -delete这套方案的成本很低,但它能救命的场景非常多。注意它是用户级的,root 的~/.bashrc也要装一份,因为 root 的 rm 手滑代价最大。装完之后建议先source ~/.bashrc再手动rm一个测试文件验证一下,别等真的删错了才发现别名没生效。
2.3 远程执行护栏:超时、回显、退出码三板斧
运维脚本里大量动作都是通过 ssh 在远程机器上执行的。ssh 有个让人头疼的问题:网络抖动或远端卡死时,命令可能挂在那里几分钟不动,看起来像脚本在正常执行,实际上早就卡死了。所以远程执行必须带超时、带退出码检查。
#!/bin/bash # run_remote.sh:封装安全的远程执行函数 SSH_TIMEOUT="${SSH_TIMEOUT:-15}" run_remote() { local host="$1" shift local cmd="$*" echo "[$(date '+%H:%M:%S')] 执行: ${host} -> ${cmd}" # 用 timeout 包裹 ssh,超时返回 124 timeout "$SSH_TIMEOUT" ssh -o ConnectTimeout=5 \ -o StrictHostKeyChecking=accept-new \ -o BatchMode=yes \ "$host" "$cmd" local rc=$? if [ "$rc" -eq 124 ]; then echo "[${host}] 命令执行超时(${SSH_TIMEOUT}s),请检查远端状态" >&2 elif [ "$rc" -ne 0 ]; then echo "[${host}] 命令失败,退出码 ${rc}" >&2 fi return "$rc" } # 用法示例: # run_remote web-01 "systemctl restart nginx" # run_remote db-01 "df -h /data"timeout是外部命令,它发送的是 SIGTERM,对 ssh 进程有效,但要注意远端命令不会自动被杀掉,这是它的一个局限。StrictHostKeyChecking=accept-new是为了在首次连接新机器时不弹交互确认,避免脚本卡在 yes/no 上,但如果你对安全要求极高、密钥指纹有强制校验需求,这个选项要改回yes。BatchMode=yes的意思是禁止交互式密码输入,没有配置密钥就直接失败,防止脚本挂在那里等密码。
这段函数的真正价值在于:它强制了「每次远程执行都有回显、有超时、有退出码」。脚本里如果有一百台机器要逐个执行,某台失败时你能立刻知道是哪台、为什么,而不是整批跑完才发现少了一台。我自己的脚本库里,所有远程操作都会走到这层封装上,包括后续的监控检查和批量分发。
3. 监控告警脚本:从采集到群通知的最小闭环
很多团队一开始没有正式监控系统,或者监控系统覆盖不全,临时盯服务器只能靠手动 ssh 上去看。这时候一组好用的巡检脚本比什么都强。我常用的做法是把检查项、采集逻辑、通知动作拆开,各管一段,最后用 crontab 串起来。
3.1 健康检查脚本:进程、端口、负载、磁盘一次性查完
巡检脚本的核心不是把指标打印得好看,而是用退出码和输出约定好「到底有没有问题」。这样后续不管是接入告警群还是写进自动化平台,都能直接消费结果。我建议把检查结果分两种:有问题的信息打到 stdout 或 stderr,退出码非 0;全部正常则只退出 0,不输出任何噪音。
#!/bin/bash # health_check.sh:最小健康检查,异常时输出信息并返回非 0 set -uo pipefail HOST=$(hostname) DATE=$(date '+%Y-%m-%d %H:%M:%S') PROBLEM=0 check_port() { local host="$1" local port="$2" # 用 /dev/tcp 做 TCP 连接探测,3 秒超时 if ! timeout 3 bash -c "echo > /dev/tcp/${host}/${port}" 2>/dev/null; then echo "${DATE} ${HOST} 端口 ${host}:${port} 连接失败" PROBLEM=1 fi } check_proc() { local name="$1" # 精确匹配进程名 if ! pgrep -x "$name" >/dev/null; then echo "${DATE} ${HOST} 进程 ${name} 未在运行" PROBLEM=1 fi } check_load() { local warn="$1" local load load=$(awk '{print $1}' /proc/loadavg) # 用 bc 做浮点比较 if [ "$(echo "${load} > ${warn}" | bc)" = "1" ]; then echo "${DATE} ${HOST} 负载过高 当前=${load} 阈值=${warn}" PROBLEM=1 fi } check_port 127.0.0.1 80 check_port 127.0.0.1 3306 check_proc nginx check_load 4.0 exit "$PROBLEM"/dev/tcp是 bash 内置的 TCP 能力,不需要装 nc 或 telnet,适合最小环境。注意它只能在 bash 里用,脚本声明必须是#!/bin/bash,用#!/bin/sh会直接报错。pgrep -x nginx的-x是精确匹配,防止nginx匹配到nginx-worker之类带前缀的进程,但要注意 Linux 进程名长度上限是 15 个字符,如果某个进程名太长被内核截断,-x就永远匹配不上,这时候反而要改用pgrep -f做全命令行匹配。
bc做浮点比较在大多数机器上都有,但如果你的最小化系统没装,脚本会直接挂。替代方案是用awk直接比,输出 1 或 0,后面我会在清理脚本里用到类似写法。这套检查脚本的产出不是给人看的,是给调度用的:crontab 里跑它,失败了才走通知逻辑。
3.2 通知脚本:把告警消息推到群机器人 Webhook
有了检查结果,下一步是让负责人知道。短信网关和邮件在大多数中小团队里申请周期长,最实用的通道是群机器人 Webhook:只要一个 URL,curl 就能发,手机端能实时收到。
#!/bin/bash # notify.sh:通用 Webhook 通知脚本,发送文本消息 send_notify() { local webhook="$1" local content="$2" # 构造通用的 JSON 消息体,适配常见群机器人协议 local body body=$(printf '{"msgtype":"text","text":{"content":"%s"}}' "$content") curl -s -m 10 \ -H 'Content-Type: application/json' \ -d "$body" \ "$webhook" echo " [通知已发送]" } # 用法示例: # send_notify "https://hook.example.com/xxxx" "web-01 端口 80 连接失败,请检查"这段脚本刻意把 URL 和内容都做成参数,不写死。实际项目里最常见的错误是通知脚本里写死某个测试 URL,后面接生产环境时忘了换,导致所有告警都发到测试群里没人看。我的习惯是通知 URL 通过环境变量注入。curl -m 10是给通知请求自身加超时,告警系统不能在发送通知这一步把自己也卡死。消息内容的格式可以根据你用的群机器人协议调整,但消息体必须是 JSON 这一点是通用的。
在检查脚本和通知脚本的配合上,我一般会把健康检查的输出直接透传给通知脚本:
# 告警合并发送:只有检查失败才发通知 OUTPUT=$(bash health_check.sh 2>&1) if [ $? -ne 0 ]; then send_notify "${WEBHOOK_URL}" "${OUTPUT}" fi这里有个容易踩的坑:OUTPUT=$(...)会吞掉所有输出,如果检查脚本本身没输出、只有退出码异常,你收到的消息就是空的。所以健康检查脚本里每一处异常都必须显式输出内容,这也是前面我用echo输出每条异常的根本原因。
3.3 调度与告警去重:crontab 写法和静默期设计
巡检脚本放在 crontab 里跑,频率根据业务重要性定。一分钟一次过于频繁,容易把自己告警告到麻木;五分钟一次是比较常见的折中。但光有 crontab 还不够,必须考虑告警风暴的问题——同一台机器挂了,每分钟都在推送,手机一晚上响几十次,最后的结果一定是被静音。
我的做法是加一个简单的静默期锁:
#!/bin/bash # alert_once.sh:同一告警源在静默期内只发一次 ALERT_LOCK="/tmp/alert_${HOSTNAME}.lock" SILENCE_MIN=10 LOCK_TIME=60 # 0 表示不存在锁,允许发通知;1 表示仍在静默期 if [ -f "$ALERT_LOCK" ]; then AGE=$(( $(date +%s) - $(stat -c %Y "$ALERT_LOCK") )) if [ "$AGE" -lt $((SILENCE_MIN * 60)) ]; then exit 0 fi fi # 发送通知并创建锁文件 bash notify_logic.sh touch "$ALERT_LOCK"脚本里的stat -c %Y取的是文件最后修改时间,这里用文件存在性加时间差完成静默,逻辑简单但有效。crontab 配合方式是这样:
# 每 5 分钟检查一次,静默期 10 分钟,避免告警轰炸 */5 * * * * bash /opt/scripts/health_check.sh || bash /opt/scripts/alert_once.sh注意 crontab 的行尾不要带多余空格,环境变量和 PATH 是 crontab 里两个老大难问题,后面避坑章节会专门展开。
4. 磁盘与日志清理脚本:保留期、白名单与 dry-run 设计
服务器磁盘被写满,是运维里最常见的故障之一,日志文件又是最大的磁盘杀手。很多团队会用 logrotate 管理日志轮转,但总有一些日志是程序自己写的、不走 logrotate,还有一些是临时文件、备份包、旧安装包堆积。这时候需要自己写清理脚本,而清理脚本的核心是:宁可少删,不能多删。所以 dry-run 和保留期参数设计是灵魂。
4.1 先分清哪些事该交给 logrotate,哪些才需要自写脚本
logrotate 是系统自带的日志轮转工具,按大小或时间切割日志,支撑保留份数配置,对绝大多数标准日志场景已经足够。它的典型配置长这样:
/var/log/app/*.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate }这里daily是按天轮转,rotate 7保留 7 份,copytruncate是复制日志后清空原文件,适合那些不重新打开日志文件的程序。但 logrotate 解决不了的问题也很明确:程序日志名不是规范的.log结尾、日志目录下有一大堆中间件产生的临时文件、业务备份包按日期堆积。这些场景 logrotate 管不了,只能写脚本。
我给团队定的分工是:凡是程序自己会持续写入、文件名规则固定的,一律走 logrotate;凡是按天生成独立文件、需要按保留天数清理的,走自写脚本;临时文件和大文件定位则单独用小工具处理。这样不会出现 logrotate 和脚本抢文件的情况。
4.2 一个保守的清理脚本:find 的 mtime、白名单与 dry-run
清理脚本的第一个版本必须是只读的,也就是 dry-run 模式,先看它会删什么,再决定要不要真删。我把这个机制直接做进脚本参数里,而不是靠注释提醒。
#!/bin/bash # clean_old_files.sh:按保留天数清理旧文件,默认 dry-run set -uo pipefail # 环境变量控制:DRY_RUN=1 只打印不删除,0 才真正执行 DRY_RUN="${DRY_RUN:-1}" KEEP_DAYS="${KEEP_DAYS:-30}" TARGET_DIR="${1:-/var/log/app}" # 目录不存在或为空时直接退出,避免误删 if [ ! -d "$TARGET_DIR" ]; then echo "错误:目录 ${TARGET_DIR} 不存在" >&2 exit 1 fi # 白名单:这些目录/文件即使超期也不清理 WHITELIST=".git .snapshot lost+found" # 找出超过保留期的普通文件(非目录、非链接) FILES=$(find "$TARGET_DIR" -type f -mtime +"$KEEP_DAYS") for f in $FILES; do # 跳过白名单路径 skip=0 for w in $WHITELIST; do case "$f" in *"$w"*) skip=1 ;; esac done [ "$skip" -eq 1 ] && continue if [ "$DRY_RUN" = "1" ]; then echo "[dry-run] 将删除: $f" else rm -f "$f" echo "[已删除] $f" fi done # 处理空目录:超过保留期的空目录也一并清理 find "$TARGET_DIR" -type d -empty -mtime +"$KEEP_DAYS" -delete echo "清理完成,保留天数=${KEEP_DAYS} dry-run=${DRY_RUN}"这段脚本的三个关键点都值得展开。第一个是DRY_RUN设计成环境变量,意味着执行时用DRY_RUN=0 bash clean_old_files.sh /var/log/app就能真正清理,不需要改脚本文件,减少误操作的可能。第二个是白名单机制,.snapshot是快照目录、lost+found是文件系统修复目录、.git是版本库,这些目录里的文件即使看起来旧也不能碰。第三个是空目录清理放在最后,避免出现「文件删了,目录还在,后面还以为有内容」的假象。
一个常见争议是find -mtime +30是不是严格等于 30 天前。-mtime +30在 find 里的语义是「文件内容最后修改时间距今超过 24*30 小时」,不是自然日。如果业务方要求「保留最近 30 个自然日的日志」,应该用-mtime +30配合-daystart,或者直接按文件名里的日期判断。我在生产环境碰到过因为没搞清这个差别,把当天凌晨的文件提前清理掉的情况,这就是边界理解问题。
4.3 大文件定位与交互确认:清理前的最后一道确认
清理脚本负责的是「批量按规则删」,但运维里还有一种高频场景是「磁盘满了,但不知道是谁占的」。这时候先别急着写删除脚本,先找出大文件,跟业务方确认,再动手。我常用的定位命令组合:
# 查看目录下各子目录占用,从大到小 du -h --max-depth=1 /var/log 2>/dev/null | sort -hr | head -20 # 查找指定目录下超过 500M 的文件 find /var/log -type f -size +500M -exec ls -lh {} \; 2>/dev/null # 查被删除但仍占用空间的文件(经典磁盘满但 du 对不上) lsof +L1 2>/dev/null | awk '$7 > 0'lsof +L1是找「文件已被删除,但进程还持有文件句柄」的场景,这是磁盘明明满了、du却算不出占用的大坑。原因是 Unix 下进程打开的文件被 rm 后,空间不会释放,直到进程关闭文件。遇到这种情况清理脚本没用,得找到对应进程重启或让它重新打开日志文件,这也是copytruncate为什么存在的原因。
大文件定位之后,我坚持做人工确认而不是直接写进清理脚本。方法是把 find 结果输出到一个临时清单,人眼扫一遍,再对清单做删除。这个确认步骤看起来多此一举,但它能挡住绝大多数「删错目录」的事故。脚本跑得再快,方向错了就是灾难。
5. 避坑与排查:运维脚本最容易翻车的五个现场
脚本写多了之后你会发现,真正让人头疼的往往不是业务逻辑,而是那些看起来细枝末节的东西——换个行符、多一个空格、少一个环境变量。这里把我踩过的、也帮别人排查过的五类高频问题列出来,每条都按「现象 → 原因 → 解决」讲清楚。
5.1 脚本在本地好好的,传到服务器就报$'\r': command not found
现象:脚本在 Windows 上编辑过或者从某个平台复制出来,传到 Linux 执行时第一行就报错,提示$'\r': command not found,用vi打开能看到每行结尾有个^M。
原因:Windows 文本文件用的是 CRLF(\r\n)换行,Linux 只认 LF(\n)。\r在 bash 眼里是普通字符,会被当成命令的一部分,脚本自然跑不起来。常见的坑还包括#!/bin/bash\r,shebang 本身都错。
解决:
# 方法一:dos2unix 转换 dos2unix script.sh # 方法二:sed 直接去掉行尾的 \r sed -i 's/\r$//' script.sh这个问题的隐蔽之处在于,部分工具会自动帮你转换,部分不会。Git 的core.autocrlf设置可能让 clone 下来的文件悄悄变成 CRLF。我的处理习惯是:脚本文件统一在 Linux 上创建和编辑,需要从外部复制时,先跑一次file script.sh看输出里有没有CRLF字样,再上线。
5.2set -e的陷阱:grep -q匹配不到就让整个脚本退出
现象:脚本里写了set -e,某行执行echo "$x" | grep -q "keyword",当匹配不到关键字时脚本直接退出,后面的清理和收尾动作全都没执行。
原因:set -e的语义是「任何命令返回非 0 就退出」。「匹配不到」对grep -q来说就是返回 1,恰好触发了退出。还有同理的:grep -q放进if条件里没事,因为 if 会把命令的返回值当作判断条件,但裸写在脚本中间就会中招。
解决:要么去掉set -e改用显式错误处理,要么在可能返回非 0 的命令后加|| true,要么把它改成条件判断:
if echo "$x" | grep -q "keyword"; then echo "找到关键字" else echo "没找到,继续执行" fi我的建议是:运维脚本不要一律set -e,尤其是含多条命令、每条都可能失败的长脚本,set -e会让失败点变得不可预知。比较稳的是set -uo pipefail,错误处理全部显式写出来,每条关键命令后检查退出码,这样能精确定位是哪一步挂了。
5.3find -exec和xargs的差异:文件名带空格直接被拆开
现象:清理脚本里find ... | xargs rm -f,遇到文件名里带空格或中文的日志文件,报No such file or directory,空格前半截被当成一个文件。
原因:xargs默认按空白字符切分输入,文件名里的空格会被当作分隔符,导致一个文件名被拆成两三个。find -exec则不会拆,它会完整地把每个文件名传给命令。
解决:使用-print0和xargs -0配对,或者干脆用find -delete:
# 安全的配对方式:null 分隔 find "$DIR" -type f -mtime +7 -print0 | xargs -0 rm -f # 更简单的方式:find 自带删除能力 find "$DIR" -type f -mtime +7 -delete这里突出一个原则:能用find -delete就不要走管道。凡是要把 find 结果交给别的命令处理的场景,务必-print0和xargs -0一起用,这个习惯能让你少踩很多文件名异常的坑。我自己写的清理脚本里,上面那版用for循环处理find输出其实也有这个风险,所以在白名单版本里我特意没有用管道,但如果你要处理任意文件名,请直接按这节的做法改。
5.4 变量赋值等号两边多打空格,命令直接变笑话
现象:脚本里写KEEP_DAYS = 30,执行后$KEEP_DAYS是空值,清理脚本把所有新旧文件全删了,或者if [ $KEEP_DAYS -gt 0 ]报参数太多。
原因:bash 里变量赋值KEEP_DAYS=30是一个整体,等号两边不能有空格。一旦写成KEEP_DAYS = 30,bash 会把KEEP_DAYS当命令名去执行,报command not found,变量根本没赋值。
解决:无话可说,只能靠习惯。赋值语句永远紧贴等号,不要为了对齐好看加空格。另外,引用变量时一定要加双引号,[ "$KEEP_DAYS" -gt 0 ]和[ $KEEP_DAYS -gt 0 ]在变量为空时完全是两个结果。这个坑死过无数人,几乎每个脚本事故报告里都有它的身影。
5.5 crontab 里脚本跑不起来,但手动执行一切正常
现象:脚本手动执行没问题,放进 crontab 后要么不执行,要么执行后报command not found,要么脚本里依赖的环境变量是空的。
原因:crontab 的环境和登录 shell 完全不同。PATH通常是精简过的/usr/bin:/bin,很多自装软件在/usr/local/bin下直接找不到;JAVA_HOME、PYTHON_PATH这类变量根本没有;cron 执行时也不是登录 shell,~/.bashrc里的配置不会生效。
解决:脚本开头显式导出环境变量,命令尽量用绝对路径:
#!/bin/bash export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" export LANG=en_US.UTF-8 # 命令用绝对路径或确保在 PATH 内 /usr/local/bin/python3 /opt/scripts/clean_logs.py调试 crontab 问题的固定套路是:重定向输出到日志文件,等下一次执行后看内容。crontab 里加>> /tmp/cron_debug.log 2>&1,看是没执行还是执行报错。如果日志里什么都不出现,优先怀疑 crontab 服务没启动或时间格式写错;如果出现command not found,基本就是 PATH 问题。这套排查顺序能解决九成 crontab 相关故障。
6. 上线前的验证闭环:让脚本不变成新的故障源
脚本写完能跑,和脚本可以放心上生产,是两回事。我见过太多人写完脚本直接在线上执行,结果脚本本身的 bug 比它要解决的问题还大。这里分享一套我固定走的验证流程,每一步都花不了多少时间,但能把事故概率压到最低。
第一步是语法检查。bash -n能查出纯语法错误,比如 if 没写 fi、括号不匹配。更严格的是用 ShellCheck 做静态分析,它能指出变量未加引号、误用eval、set -e导致的隐患等,建议脚本库里的每个脚本上线前都过一遍,能减少七八成低级错误。
bash -n clean_old_files.sh shellcheck clean_old_files.sh第二步是 dry-run 验证。所有涉及删除、覆盖、批量修改的脚本,第一次运行必须带只读参数或环境变量。就像前面清理脚本里的DRY_RUN=1,先让它把「将要做什么」完整打印出来,人眼确认一遍再真正执行。我见过最快的翻车方式是:清理脚本写得太顺手,-delete一加直接跑,然后发现保留期设错了。dry-run 就是上生产前的后悔药。
第三步是灰度执行。先把脚本放到一台非核心机器上,用真实数据跑一遍,观察退出码和输出。确认没问题后再扩展到同一批次的其他机器。批量执行脚本尤其要这样做,几十台机器同时跑一个不成熟的脚本,出了问题就是几十倍的放大。我的习惯是先拿一台机跑,日志翻一遍没异常,再组织批量。
最后是验证结果本身。脚本跑完不等于任务完成,要确认它是否达到了预期效果,以及是否产生了副作用。比如清理脚本跑完,磁盘空间到底释放了没有,不能看日志说「删了 100 个文件」就放心,要看df -h的实际数字。我给自己定的一个规矩是:脚本改动后,至少连续观察三天运行日志,第三天还正常,才算真正稳定。
我最早自己写清理脚本时,直接在测试环境跑了一遍-delete,当时感觉一切正常,后来发现它把一个备份目录里「按时间戳命名但没有访问时间」的旧归档全部清掉了,备份链断了一环。从那之后,所有脚本都默认先 dry-run、再单机、再扩大范围执行,这条流程执行到现在,几乎没有再因为脚本自身的问题出过线上事故。这套验证闭环不一定最华丽,但足够稳,希望帮到你。
本文还有配套的精品资源,点击获取