服务器运维的脚本化并不是把几行Shell命令存成文件那么简单。真正落地的脚本开发,核心在于解决三个问题:如何让脚本在无人值守时绝对可靠、如何让团队其他人能快速接手、以及如何避免脚本本身成为生产环境的故障源。很多人写的脚本在测试环境跑得顺畅,一到生产环境就挂,原因往往不是逻辑错误,而是忽略了运行环境、权限、依赖和异常处理这些细节。
脚本语言的选择不纠结,看场景Shell脚本依然是Linux服务器运维的第一选择,因为它的最大优势是零依赖,只要系统能启动到基本运行级别,Shell脚本就能执行。对于批量管理文件、启动停止服务、检查系统状态这类操作,Shell是最直接的。但Shell的弱点也很明显:复杂的数据结构处理困难,字符串操作容易出错,调试手段有限。当运维任务涉及API调用、数据库操作、复杂的逻辑判断时,Python是更好的选择。Python的subprocess模块可以调用系统命令,paramiko库能处理SSH连接,requests库能对接各种接口。一个务实的做法是:用Shell做系统层面的基础操作,用Python做需要复杂逻辑的上层调度,两者通过命令行参数或标准输入输出传递数据。
脚本结构必须标准化一个能直接上生产的运维脚本,结构至少要包含这几个部分:头部注释区、变量定义区、函数定义区、主逻辑区。头部注释不是走形式,要写清楚脚本用途、作者、创建日期、修改记录、依赖的外部命令或文件、预期的执行用户和权限。变量定义区要把所有可能变化的参数提取出来,包括路径、IP地址、端口、阈值、日志保留天数等,集中放在脚本开头,用大写字母命名以示区分。函数定义区把每个独立功能封装成函数,函数名用动词开头,比如check_disk_usage、clean_old_logs、restart_service。主逻辑区只做一件事:按顺序调用函数,并根据返回值决定后续流程。
#!/bin/bash
#===================================
# 脚本名称: system_health_check.sh
# 功能: 检查磁盘使用率、内存使用率、关键服务状态
# 作者: ops-team
# 创建日期: 2025-01-15
# 修改记录:
# 2025-02-10 增加nginx服务检查
# 依赖命令: df, free, systemctl, awk
# 执行用户: root
# 预期环境: CentOS 7/8, Ubuntu 20.04+
#===================================
# ---------- 变量定义 ----------
DISK_THRESHOLD=80
MEM_THRESHOLD=90
LOG_DIR="/var/log/health_check"
LOG_FILE="${LOG_DIR}/check_$(date +%Y%m%d).log"
SERVICES=("nginx" "mysql" "redis" "sshd")
# ---------- 函数定义 ----------
check_disk_usage() {
local threshold=$1
df -h | awk 'NR>1 {print $5 " " $6}' | while read usage mount; do
local percent=$(echo $usage | tr -d '%')
if [ $percent -gt $threshold ]; then
echo "[WARN] 磁盘 $mount 使用率 ${percent}% 超过阈值 ${threshold}%"
fi
done
}
check_memory_usage() {
local threshold=$1
local mem_used_percent=$(free | awk '/Mem:/ {printf "%.0f", $3/$2*100}')
if [ $mem_used_percent -gt $threshold ]; then
echo "[WARN] 内存使用率 ${mem_used_percent}% 超过阈值 ${threshold}%"
else
echo "[INFO] 内存使用率 ${mem_used_percent}%,正常"
fi
}
check_services() {
for svc in "${SERVICES[@]}"; do
if systemctl is-active --quiet $svc; then
echo "[INFO] 服务 $svc 运行正常"
else
echo "[ERROR] 服务 $svc 未运行"
fi
done
}
# ---------- 主逻辑 ----------
mkdir -p $LOG_DIR
{
echo "========== 检查时间: $(date) =========="
check_disk_usage $DISK_THRESHOLD
check_memory_usage $MEM_THRESHOLD
check_services
echo "========== 检查结束 =========="
} >> $LOG_FILE 2>&1
异常处理是脚本的灵魂
运维脚本和普通程序最大的区别在于,它运行的环境随时可能出问题。磁盘满了、网络断了、依赖的服务没启动、权限被改了,这些情况脚本都必须能处理。每条可能失败的命令都要检查返回值,Shell中用$?判断上一条命令的退出码,0表示成功,非0表示失败。对于关键操作,失败后要有明确的错误信息输出,并且脚本的退出码也要非0,这样上层调度系统才能感知到异常。set -e可以让脚本在任何命令失败时立即退出,但不要滥用,有些命令失败是可以接受的,比如清理一个可能不存在的临时文件,这时要用|| true来容错。set -u可以在使用未定义变量时报错,避免因为变量名拼写错误导致不可预知的行为。set -o pipefail让管道中任意一个命令失败都导致整个管道失败,这对日志处理场景很重要。
#!/bin/bash
set -euo pipefail
cleanup_temp() {
rm -f /tmp/myapp_*.tmp || true
}
restart_service_safely() {
local svc=$1
if ! systemctl restart $svc; then
echo "[FATAL] 重启 $svc 失败,尝试回滚"
systemctl start $svc || echo "[FATAL] 回滚也失败了,请人工介入"
return 1
fi
return 0
}
日志和告警要可追溯
脚本在后台自动运行,没人盯着屏幕看输出,所以日志机制直接决定了问题排查效率。日志至少要包含时间戳、日志级别、具体信息三要素。INFO级别记录正常流程节点,WARN级别记录需要关注但不影响运行的情况,ERROR级别记录导致任务失败的问题。日志文件按天切割,保留最近30天,避免单个日志文件过大。对于关键任务的脚本,日志不仅要写文件,还要通过企业微信、钉钉或邮件发送告警。告警不要每条都发,同一个问题在短时间内只发一次,用状态文件记录上次告警时间,避免告警风暴。
send_alert() {
local message=$1
local alert_lock="/tmp/alert_disk.lock"
local now=$(date +%s)
local last_alert=0
if [ -f $alert_lock ]; then
last_alert=$(cat $alert_lock)
fi
# 30分钟内不重复告警
if [ $((now - last_alert)) -gt 1800 ]; then
curl -s -X POST "https://your-webhook-url" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"$message\"}}"
echo $now > $alert_lock
fi
}
参数化与配置分离
把配置硬编码在脚本里是大忌。环境不同、服务器不同,阈值、路径、端口都可能不一样。每改一次配置就要改脚本代码,既容易出错,也不利于版本管理。正确的做法是把配置项放到独立的配置文件中,脚本启动时读取。配置文件用ini格式或简单的key=value格式都行,关键是团队成员都能看懂。更进一步的,把敏感信息如密码、密钥放到环境变量或专用的密钥管理服务中,脚本里绝不出现明文密码。
# config.env 配置文件 DISK_THRESHOLD=85 MEM_THRESHOLD=90 LOG_RETENTION_DAYS=30 DB_HOST=10.0.1.50 DB_PORT=3306 ALERT_WEBHOOK=https://hooks.example.com/alert
#!/bin/bash
# 加载配置文件
CONFIG_FILE="/etc/ops/config.env"
if [ -f $CONFIG_FILE ]; then
source $CONFIG_FILE
else
echo "[ERROR] 配置文件 $CONFIG_FILE 不存在"
exit 1
fi
# 使用配置变量
echo "磁盘阈值: $DISK_THRESHOLD"
echo "日志保留: $LOG_RETENTION_DAYS 天"
幂等性让脚本可以反复执行
运维脚本最怕的是重复执行导致副作用。比如一个创建用户的脚本,第一次执行创建了用户,第二次执行因为用户已存在而报错退出,这就不具备幂等性。幂等的意思是,脚本执行一次和执行多次,结果是一样的,不会产生额外的影响。实现幂等性的方法是在操作前先检查当前状态:创建目录前先判断目录是否存在,添加防火墙规则前先检查规则是否已存在,写入文件前先判断内容是否已经是目标值。这样脚本就可以安全地反复执行,也方便放在crontab里定期运行。
ensure_user_exists() {
local username=$1
if id "$username" &>/dev/null; then
echo "[INFO] 用户 $username 已存在,跳过创建"
else
useradd -m -s /bin/bash $username
echo "[INFO] 用户 $username 创建成功"
fi
}
ensure_line_in_file() {
local line=$1
local file=$2
if grep -qF "$line" $file; then
echo "[INFO] 配置项已存在于 $file,跳过"
else
echo "$line" >> $file
echo "[INFO] 配置项已追加到 $file"
fi
}
版本管理与灰度发布
运维脚本也是代码,同样需要版本管理。把脚本放到Git仓库里,每次修改都有commit记录,出问题时可以快速回滚。脚本的部署不要一次性全量推送到所有服务器,先在几台非核心服务器上验证,确认无问题后再逐步推广。可以用ansible、saltstack这类配置管理工具来分发脚本,它们本身就支持分组执行和灰度策略。如果团队规模小,手动scp也行,但要建立一个中心化的脚本仓库目录,比如/opt/scripts/,所有服务器从这个目录执行脚本,并且这个目录本身受版本控制。
定时任务的正确使用姿势crontab是运维脚本最常用的调度工具,但很多人用错了。crontab里直接写复杂的命令管道,时间长了谁也看不懂。正确做法是crontab里只写脚本路径和简单参数,所有逻辑都在脚本里。crontab的执行环境和登录Shell不同,PATH变量很精简,所以脚本里所有命令都要用绝对路径,或者在脚本开头显式设置PATH。脚本的输出crontab默认会发邮件给root,如果不想收到这些邮件,把输出重定向到日志文件。另外要注意crontab的时间重叠问题,如果一个任务还没跑完,下一个周期又启动了,可能导致资源争抢或数据错乱,脚本里要用锁文件或flock来防止并发执行。
# crontab 示例 # 每天凌晨2点执行健康检查 0 2 * * * /opt/scripts/system_health_check.sh >> /var/log/cron_health.log 2>&1 # 每小时清理一次临时文件 0 * * * * /opt/scripts/clean_temp.sh
#!/bin/bash
# 使用flock防止脚本并发执行
LOCK_FILE="/var/run/my_script.lock"
exec 200>$LOCK_FILE
if ! flock -n 200; then
echo "[WARN] 脚本已在运行中,退出"
exit 1
fi
# 脚本主体逻辑
echo "开始执行任务..."
sleep 60
echo "任务完成"
# 脚本结束时自动释放锁
flock -u 200
测试是上线前的最后一道关
运维脚本的测试经常被忽略,因为大家觉得脚本简单,跑一遍看看没报错就算通过了。这远远不够。测试要覆盖正常路径和异常路径:磁盘空间正常时脚本怎么表现,磁盘满了时怎么表现;目标服务运行时怎么表现,服务停了怎么表现;配置文件存在时怎么表现,配置文件缺失时怎么表现。可以用Docker快速搭建一个和目标环境相似的测试容器,在容器里模拟各种故障场景。脚本里加上dry-run模式,用--dry-run参数让脚本只打印要执行的操作而不真正执行,这样可以在生产环境先做一次预演。
DRY_RUN=false
if [ "${1:-}" == "--dry-run" ]; then
DRY_RUN=true
echo "[DRY-RUN] 仅模拟执行,不做实际操作"
fi
delete_old_logs() {
local dir=$1
local days=$2
if $DRY_RUN; then
echo "[DRY-RUN] 将删除 $dir 中 $days 天前的日志文件"
else
find $dir -name "*.log" -mtime +$days -delete
echo "[INFO] 已删除 $dir 中 $days 天前的日志文件"
fi
}
文档和知识传递
脚本写得再好,如果只有你自己会用,那它的价值就大打折扣。每个脚本都应该有一份对应的文档,说明脚本做什么、怎么用、参数是什么、配置文件在哪、常见问题怎么处理。文档不用写成长篇大论,放在脚本仓库的README里或者内部的Wiki上就行。更重要的是,脚本本身的注释要足够清晰,别人打开脚本看一遍注释就能理解整体逻辑。团队里定期做脚本评审,和代码评审一样,让其他人看看脚本有没有潜在问题,同时也是知识传递的过程。当有人休假或者离职时,他写的脚本别人能无缝接手,这才是真正落地的自动化运维。
运维脚本从开发到落地,本质上是一个工程化的过程。它不要求你写出多么精巧的算法,但要求你在可靠性、可维护性、可扩展性上下足功夫。把上面这些原则融入到日常的脚本开发习惯中,你的脚本就能从“能用”变成“可靠”,从个人工具变成团队资产。
