☰
Shell脚本自动化运维:从重复命令到无人值守
2026/10/10 21:32:40 网站建设 项目流程

凌晨一点半,手机在床头柜上震动起来。我眯着眼看了一眼,是某个项目服务器的磁盘告警通知。爬起来打开电脑,SSH到机器上敲了两行命令,确认是日志文件把分区撑满了,清理完,再回到床上,睡意已经被折腾掉大半。那一刻我就在心里想:这种活,真的不该再靠人的手去做了。

这其实是我做自动化与脚本这件事的起点。今天想聊的,不是某个高大上的自动化平台,也不是K8s、Ansible、GitOps那套重型体系,而是一个更朴实的命题:作为一名日常守着多台服务器、跑着多个服务的工程师,如何用Shell脚本把这摊重复得让人麻木的事情,一点一点交给机器去做。

1. 为什么说"自动化"首先是思维方式,然后才是脚本

很多人一提到自动化,第一反应是"用Python写个脚本"。这个说法不能说错,但它把问题框小了。以我自己的经验来看,自动化本质上是把"人要做的一系列决策和动作"翻译成"机器可执行的确定步骤"——脚本只是这一步的载体。

1.1 被反复执行的检查,正是自动化的最佳切入点

就拿凌晨那次告警来说,事后我复盘了整个处理过程。看起来只是"清理日志文件"一步,但拆开来看,包含了好几个动作:检查哪个分区使用率超标、定位日志目录里最大的文件、确认这个文件是否还被进程占用、清理后验证空间是否释放。这些动作我用Shell脚本实现时,每一步都有明确的逻辑判断,机器完全可以替代我执行判断和动作。

1.2 自动化项目从"记录重复动作"开始

我在某次带项目前端联调时,发现身边不少同事也有类似的困扰,他们的桌面理解是把重复战斗全部写成笔记。于是我把自己的做法整理成一套流程:先把每天手动执行的命令按"检查类""处理类""汇报类"分门别类记录下来,再逐条分析哪些是可以直接交给机器的。

比如说,每天早上第一件事是登录跳板机看各台服务器的负载、磁盘、内存占用。这类工作就是典型的"检查类",非常适合脚本化。而像"某台机器上服务异常,需要看了日志才能确定怎么处理"这种,就需要人的判断,初期不建议自动化。先易后难,从一个能稳定跑通的自动检查工具入手,这远比一开始就想着写一套全自动运维平台要靠谱得多。

2. 我的自动化骨架:一套服务于日常运维的Shell脚本体系

有了思路,接下来就得考虑怎么落地。我踩过不少坑,也看到过很多同事的脚本散落在各种目录里,最后连自己都找不到。所以我决定从一开始就让这套东西像一个小型项目一样组织起来,而不是一堆孤零零的脚本文件。

2.1 清晰的目录结构,脚本也要"分门别类"

我的脚本目录结构大致是这样:

~/automation/ ├── bin/ # 核心脚本存放处,均已加可执行权限 │ ├── health_check.sh │ ├── log_cleaner.sh │ ├── domain_expire_check.sh │ └── billing_report.sh ├── etc/ # 配置文件、白名单、阈值定义 │ ├── host_list.conf │ ├── threshold.conf │ └── cron.conf ├── logs/ # 脚本运行日志、历史输出 └── README.md # 每个脚本的说明文档

别小看这个目录结构,它带来的好处是长期的。脚本和配置分离,意味着我改阈值的时候不用去翻脚本正文;脚本和日志分离,意味着排查问题时不会在脚本堆里找输出文件。这套目录设计借鉴了Linux系统本身的理念——配置放/etc,可执行文件放/bin,日志放/var/log,跟系统打交道多了,你会发现这种约定俗成的划分非常符合直觉。

2.2 统一入口:一键触发全部脚本

我始终觉得,自动化的体验必须让人"愿意用"。如果每次要分别手动跑五个脚本,那跟手动敲命令也没太大区别。所以我在bin目录下加了一个统一的入口脚本run_all.sh,它的作用不只是挨个调用其他脚本,更重要的是把输出结果汇总到一份简报里,然后推送到钉钉机器人的 Webhook。

#!/usr/bin/env bash # 统一调度入口:按顺序执行各检查脚本,汇总输出 set -uo pipefail BASE_DIR="$HOME/automation" LOG_DIR="$BASE_DIR/logs" DATE_TAG="$(date +%Y%m%d-%H%M)" # 各脚本执行函数,保证单个脚本失败不会中断整体流程 run_script() { local name="$1" local script="$BASE_DIR/bin/$name" if [[ -x "$script" ]]; then echo "[RUN] $name" "$script" >> "$LOG_DIR/${name}_$DATE_TAG.log" 2>&1 || echo "[FAILED] $name" else echo "[SKIP] $name 不存在或不可执行" fi } run_script health_check.sh run_script log_cleaner.sh run_script domain_expire_check.sh run_script billing_report.sh echo "[DONE] 调度结束,时间:$DATE_TAG"

注意:这里我用set -uo pipefail,但故意不加-e。原因是,在调度多个独立检查脚本时,某个脚本"非零退出"不应该中断整个调度流程。-e在这种场景下往往带来副作用,这是我在实际使用中反复调整后形成的习惯。

3. 四个支撑日常运维的核心脚本,从需求到实现

这套骨架里最重要的,当然是那几个实际干活的脚本。我把它们拆开来讲,每个脚本都会先说清楚它解决什么问题、为什么这样设计,再贴关键代码片段。

3.1 健康检查脚本:把"肉眼盯监控"变成"机器抓异常"

health_check.sh承担的任务是:批量登录多台服务器,检查负载、内存、磁盘、以及关键进程状态。

#!/usr/bin/env bash # 批量服务器健康检查 BASE_DIR="$HOME/automation" HOST_FILE="$BASE_DIR/etc/host_list.conf" THRESHOLD_FILE="$BASE_DIR/etc/threshold.conf" source "$THRESHOLD_FILE" while read -r host; do [[ -z "$host" || "$host" == \#* ]] && continue echo "========== 检查主机:$host ==========" # 磁盘使用率检查 ssh "$host" "df -P / /data 2>/dev/null" | awk 'NR>1 {print $6, $5}' | while read -r mount_point usage; do usage_num=${usage%\%} if [[ "$usage_num" -gt "$DISK_THRESHOLD" ]]; then echo "[告警] $mount_point 使用率已达 $usage_num%(阈值${DISK_THRESHOLD}%)" else echo "[正常] $mount_point 使用率 $usage_num%" fi done # 系统负载检查:取1分钟负载值做判断 ssh "$host" "uptime" | sed 's/.*load average: //' | awk -F', ' '{print $1}' | \ while read -r load1; do if (( $(echo "$load1 > $LOAD_THRESHOLD" | bc) )); then echo "[告警] 1分钟负载 $load1 超出阈值 $LOAD_THRESHOLD" else echo "[正常] 1分钟负载 $load1" fi done # 进程状态检查:以Nginx和MySQL为例 ssh "$host" "pgrep -f nginx > /dev/null && echo 'nginx:运行中' || echo 'nginx:未运行'" ssh "$host" "pgrep -f mysqld > /dev/null && echo 'mysql:运行中' || echo 'mysql:未运行'" done < "$HOST_FILE"

这段代码的核心思路是"远程命令 + 本地解析"。我刻意没有用专业的监控agent,因为这套方案要部署到多个项目环境里,部分环境甚至不允许额外安装agent。用SSH批量执行命令,只需要在本地保留一台控制机,目标机器无需任何改动,落地成本几乎是零。

阈值配置文件threshold.conf长这样:

DISK_THRESHOLD=85 LOAD_THRESHOLD=4.0 MEMORY_THRESHOLD=90

放在etc目录下的好处是,将来机器配置提升了、告警阈值要调高,只需要改一个文件。我经常看到有人把阈值硬编码在脚本里,临时调个值还要打开脚本从头到尾找一遍,确实很不方便。

3.2 日志清理脚本:告别"凌晨被磁盘告警吵醒"

回到开头那个让我凌晨爬起来的场景。log_cleaner.sh就是专门对付它的。

#!/usr/bin/env bash # 日志轮转与过期日志清理 LOG_DIRS="/var/log/nginx /var/log/mysql /home/app/logs" RETENTION_DAYS=7 DRY_RUN=false [[ "$1" == "--dry-run" ]] && DRY_RUN=true for dir in $LOG_DIRS; do if [[ -d "$dir" ]]; then echo "处理目录:$dir" if $DRY_RUN; then # 预演模式:只列出将删除的文件 find "$dir" -type f -mtime +$RETENTION_DAYS -name "*.log" -print else # 实际删除:先压缩再清理,保留最近7天 find "$dir" -type f -mtime +$RETENTION_DAYS -name "*.log" -exec gzip {} \; find "$dir" -type f -mtime +$((RETENTION_DAYS + 7)) -name "*.log.gz" -delete fi fi done

为什么要先压缩再删除?因为运维规范通常不允许直接删除日志,尤其在排查历史问题时。"先gzip,再等超过保留期限后删除压缩包",既兼顾了合规审计,又实际释放了磁盘空间。--dry-run这个参数是我强烈建议保留的,自动清理涉及删除数据,必须允许人工先跑一遍预演看清楚会删什么,不然哪天误删了关键日志,哭都来不及。

3.3 域名和证书到期检查:把"突然发现过期"变成"提前30天接通知"

域名续费和SSL证书到期,是所有人都知道重要、但总有人忘掉的事情。我见过一个项目因为证书过期导致线上App无法通信,排查了半天才发现问题根源,整个过程非常被动。于是我把这两个检查合并成一个脚本domain_expire_check.sh。

#!/usr/bin/env bash # 证书和域名到期检查 DOMAINS="example.com api.example.com static.example.com" CERT_ALERT_DAYS=30 DOMAIN_ALERT_DAYS=30 for domain in $DOMAINS; do # 检查HTTPS证书剩余天数 cert_output=$(echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null | \ openssl x509 -noout -enddate 2>/dev/null) if [[ -n "$cert_output" ]]; then end_date=$(echo "$cert_output" | cut -d= -f2) end_ts=$(date -d "$end_date" +%s) now_ts=$(date +%s) remain_days=$(( (end_ts - now_ts) / 86400 )) if [[ "$remain_days" -le "$CERT_ALERT_DAYS" ]]; then echo "[告警] $domain 证书剩余 ${remain_days} 天,请尽快续期" else echo "[正常] $domain 证书剩余 $remain_days 天" fi else echo "[异常] $domain 无法获取证书信息" fi # 检查域名到期时间 expire_info=$(whois "$domain" 2>/dev/null | grep -i 'Expiry Date\|Expiration Date' | head -1) # ... 解析出剩余天数,逻辑同上,不再赘述 done

这个脚本的安全价值不可低估。证书和域名到期这类事情,有很强的"低频突发"属性——平时想不起来,一过期就是事故。脚本化之后,它每天自动跑一遍,到期前30天开始提醒,相当于给承包方上了个横跨数月的闹钟。

3.4 账单与成本趋势统计:让花出去的钱不再是一笔糊涂账

我负责的项目里,有几台云服务器和对象存储是分开计费的,每个月的账单能拉出一大堆明细。手工统计既费时又容易错,所以我写了个billing_report.sh,把各家Cloud服务商导出的CSV账单下载后,用脚本汇总出各项目的月度消费趋势。

#!/usr/bin/env bash # 账单汇总统计:合并多份CSV,按项目和月份聚合 CSV_DIR="$HOME/automation/data/billing" REPORT_FILE="$HOME/automation/logs/billing_report.txt" # 用awk合并所有CSV中"项目"维度的费用数据 awk -F',' 'NR>1 { # 假设CSV列: 月份,项目,费用 key=$1"-"$2; sum[key]+=$3; } END { for (k in sum) { printf "%s %.2f\n", k, sum[k]; } }' "$CSV_DIR"/*.csv | sort | tee "$REPORT_FILE"

这个脚本跑通后,我每月的成本汇报从"对着Excel手动加半天"变成了"直接贴出自动化汇总结果"。对运维和研发负责人来说,这种脚本带来的节省不是一次性的,它每个月都在省时间。

4. 踩过的坑与总结出的经验:从脚本"能跑"到"跑得稳"

脚本写出来能跑是一回事,跑得稳、扛得住半年不维护还不出问题,是另一回事。这部分我积累的经验最多,每个坑都付出过实际的代价。

4.1 环境变量与登录环境的坑:定时任务的PATH不是你想的PATH

有段时间我用crontab定时跑健康检查,脚本直接SSH到目标机器执行命令,结果报了command not found。排查到最后,发现问题不在目标机器,而在本地调度机上——cron执行脚本时的PATH环境变量,只有最基本的一小段路径,习惯了交互式Shell的人根本不会意识到这点差异。

处理方式有两个,一是在脚本开头显式声明PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",二是在cron里就用绝对路径调用脚本。我这里选择的是第一种,更灵活。而且,不只是cron,很多脚本在手动跑和定时跑时的环境并不一致,统一在脚本开头设置好环境变量,能避免一大半莫名其妙的问题。

4.2 中文注释与编码的坑:脚本里别乱用中文注释

有人可能会觉得我这话说得夸张,但在我负责的某台CentOS机器上部署脚本时,由于系统语言环境和文件编码设置不一致,脚本里的中文注释直接导致了奇怪行为。这类问题很隐蔽——终端看起来一切正常,脚本似乎能跑,但在某些locale环境下管道处理出现偏差,最终结果里多出一些莫名其妙的内容。

我的建议是,脚本内统一用英文注释,至少在生产环境脚本里这样处理。不是中文注释本身有什么问题,而是脚本运行在什么环境下是不可控的,没必要为了一处注释引入一个不确定因素。

4.3 用shellcheck做静态检查:别等上线了才发现低级错误

有个阶段我写脚本的速度很快,但错误率也很高,尤其是变量引号问题,给后续排查带来了不少麻烦。后来我养成了一个习惯:写完脚本后,先跑一遍shellcheck。这款工具能识别出未加引号的变量、误用的test语法、不明确的grep正则等常见问题。这个习惯极大提升了我的脚本质量,而且越早使用越划算——实际运行前的静态检查成本最低。

4.4 安全性与权限:脚本里不应该出现密码

我在早期写的脚本里,有一段用来做远程数据库备份,直接把root密码以明文形式写在脚本中。后来一位前辈看到我的代码,当场提醒我安全隐患很大。虽然他当时没有说得太严重,但我回去后认真想了想,确实如此——脚本时常会在自动执行时被各种工具读取,硬编码密码无异于把自己的钥匙挂在门口。

后来我改成了本地密钥认证SSH,数据库备份则使用更安全的临时授权方式。对于需要root权限的操作,也在配置上限制到最小必要范围。这个改变让我睡得踏实了很多,而这份踏实,对于一个常年和线上服务打交道的人来说,是用多少钱都换不来的。

4.5 输出与日志:脚本不是跑完就消失的

刚开始写自动检查脚本的时候,我把所有的输出都打到终端上,跑完就结束了。后来有一次需要回溯两周前某天的磁盘使用情况,才发现什么都没有——脚本根本没有保存历史。从那以后,我给所有关键脚本都加了统一的输出日志机制。

# 所有脚本统一把输出写入带日期标签的日志文件 LOG_FILE="$HOME/automation/logs/$(basename "$0")_$(date +%Y%m%d).log" exec >> "$LOG_FILE" 2>&1

加上这一行之后,脚本的历史输出全部留存,配合grep随时能查。这个习惯的成本几乎为零,但价值会随着时间积累越来越大。很多时候排查问题,历史日志比实时监控更有用——它会告诉你"当时的现场"是什么样。

5. 定时任务调度:让自动化真正"自动"起来

脚本写好只是第一步,让它们按照合适的时间节奏自动运行,才是完整的关键环节。我用的是crontab,简单可靠,没有额外依赖。

5.1 调度周期设计:不同任务要分频次

我根据任务的重要程度和变更频率,把调度分成几个档位:

任务调度周期理由
健康检查每5分钟线上故障需要快速感知
日志清理每天凌晨3点业务低峰期,避免影响正常请求
证书与域名检查每天上午9点每天确认一次即可,告警有至少30天提前量
账单汇总每周一早上按周汇总,方便上一周成本趋势追踪

定时任务的落地方式,是在crontab文件里直接指定执行脚本的路径和频率。用绝对路径的原因前面已经讲过:cron环境变量不可控,配置短路径很容易出问题。

*/5 * * * * /home/user/automation/bin/health_check.sh 0 3 * * * /home/user/automation/bin/log_cleaner.sh 0 9 * * * /home/user/automation/bin/domain_expire_check.sh 0 8 * * 1 /home/user/automation/bin/billing_report.sh

5.2 告警去重:别让同一条告警刷屏

脚本自动跑起来之后,新的麻烦出现了:如果某台服务器持续处于告警状态,那么每5分钟就会产生一条告警消息。半夜被吵醒一次还能忍,被同一条消息吵醒五次就真的让人暴躁了。

我的解决方案是加了一层"告警去重与抑制"机制:脚本在发出告警前,先在本地写一个状态标记文件。如果上次告警已经发过,且当前依然处于告警状态,则只在日志中记录,不重复推送;只有状态从"正常"变为"异常",或者从"异常"恢复为"正常"时,才真正推送消息。

# 告警去重核心逻辑 ALERT_FLAG="/tmp/alert_$domain.flag" if [[ "$remain_days" -le "$CERT_ALERT_DAYS" ]]; then if [[ ! -f "$ALERT_FLAG" ]]; then echo "[告警] $domain 证书剩余 ${remain_days} 天" touch "$ALERT_FLAG" else echo "[持续告警,跳过推送] $domain" fi else # 恢复正常时清理标记 if [[ -f "$ALERT_FLAG" ]]; then echo "[恢复] $domain 证书剩余天数已回到安全范围" rm -f "$ALERT_FLAG" fi fi

这个改进之后,告警噪音立刻大幅下降。自动化的目标本来是解放人的注意力,如果脚本反而变成了一种全天候骚扰源,方向就完全跑偏了。

6. 从一个脚本到一套体系:哪些该自动化,哪些不该自动化

做了半年多的脚本自动化之后,我慢慢体会到,自动化这件事最重要的能力不是写代码,而是判断"哪些事情值得自动化"。

6.1 值得自动化的三要素:高频、稳定、可判断

如果一件事情同时满足"高频重复""步骤稳定""结果可以用规则判断"这三点,那它就几乎一定值得自动化。健康检查、日志清理、证书到期提醒,全都符合这三个特征。而像"排查服务突然不可用的根因"这类工作,尽管也常发生,但判断路径不固定,自动化它的成本会极高,不如保留人的灵活性。

6.2 自动化不是一次性投入,要当成长期项目维护

很多人以为脚本写完、cron配上、能够自动跑了,这事就完结了。我自己的体会是,一套自动化体系真正成熟,至少需要两三个月的持续打磨。比如告警去重、阈值调整、新增主机、格式优化,都是在实际使用中一点点迭代出来的。如果把它当成一次性交付物,很快就会发现它和实际需求脱节,最后沦为一套跑着但没人看的僵尸脚本。

我现在的习惯是,每隔两周抽半小时,把整套脚本的输出日志浏览一遍,看看有没有异常的沉默(说明任务可能挂了),有没有重复的告警(说明阈值需要调整),有没有持续出现的隐患提示(说明该处理根源问题了)。这套例行"体检"机制,让我的自动化工具始终处于健康状态。

6.3 扩展方向:当Shell脚本不够用的时候

Shell脚本的优势是轻量、无依赖、任何Linux环境都能跑。但它也有边界,比如复杂的数据处理、需要维护状态机的多步骤任务、或者要对接API和数据库的场景,用Shell写起来会很别扭。我在跑通基础的日常运维之后,就对特定模块做了升级:把账单统计换成了Python脚本,把健康检查的数据统一写入本地SQLite,甚至开始用简单的Web页面把巡检结果呈现出来。

这一步升级的前提是Shell版本的脚本已经稳定跑了一段时间,逻辑被验证过。自动化的演进路径应该是"先用最朴素的工具跑通流程,再在确有必要时引入更重的方案",而不是一开始就想着上框架、上平台。流程没跑通之前上框架,本质上是在为复杂度工作,而不是为稳定性工作。

7. 最后分享两个小技巧

第一,所有脚本的配置项尽量外置。阈值、主机列表、目录路径这些,统一放进etc目录下的配置文件里,脚本里用source引入。这样做的好处是改配置不用动脚本逻辑,尤其是当你需要把脚本交付给其他人使用时,这个习惯会大大降低沟通成本。

第二,善于使用bash -x来调试。如果你遇到的脚本行为不符合预期,直接跑一遍bash -x your_script.sh,它会一步步把执行过程展开,让你看到每个变量的真实值、每一条判断都走了哪个分支。这比你在代码里到处加echo要高效得多。

踩过凌晨被磁盘告警吵醒的坑之后,我才真正明白这件事:所谓自动化,核心要义从来不是"写出多么聪明的脚本",而是"让我可以安心把时间花在机器解决不了的事情上"。写这套脚本的成本,换算成时间,大概不过一两个完整的周末,但它每天为我省下的精力和安抚的睡眠,远超这一点投入。如果你也在日复一日地重复敲着同样的命令,不妨从最小的一条健康检查脚本开始,把第一个"自动执行"的按钮按下去。

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

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

立即咨询