幂等性、重试与日志规范
好的运维脚本不是"跑一次成功",而是"多跑几次也不会把系统搞乱"。
三个核心原则
| 原则 | 定义 | 违反的后果 |
|---|---|---|
| 幂等性 | 重复执行 N 次,结果与执行 1 次一致 | 重跑脚本导致数据重复、配置叠加 |
| 可重试 | 遇到临时失败(网络抖动),能有限次数重试 | 一次网络超时就放弃,需要人工介入 |
| 可观察 | 日志足够详细,能从日志还原执行过程 | 出问题了不知道发生了什么 |
幂等性模式
1. 安装前检查(check-before-act)
# ✅ 幂等:安装前检查是否已存在
install_pkg() {
local pkg=$1
if dpkg -l "$pkg" &>/dev/null; then
log "$pkg 已安装,跳过"
return 0
fi
apt-get install -y "$pkg"
log "$pkg 安装完成"
}
install_pkg nginx
install_pkg postgresql-15
2. 创建用户前检查
create_user() {
local user=$1
if id "$user" &>/dev/null; then
log "用户 $user 已存在,跳过"
return 0
fi
useradd --system --shell /bin/false --create-home "$user"
log "用户 $user 创建完成"
}
3. 写入配置前检查(防止重复追加)
ensure_line() {
local line=$1 file=$2
grep -qF "$line" "$file" && { log "配置已存在: $line"; return 0; }
echo "$line" >> "$file"
log "已写入: $line -> $file"
}
ensure_line "vm.max_map_count=262144" /etc/sysctl.conf
4. 软链接与目录(天然幂等)
# ln -sfn 无论当前指向哪里,都切换到新目标(幂等)
ln -sfn /opt/myapp/releases/20260322 /opt/myapp/current
# mkdir -p 已存在时不报错(幂等)
mkdir -p /var/log/myapp /opt/myapp/releases
重试与指数退避
#!/usr/bin/env bash
# retry.sh — 通用重试函数
# 指数退避重试(适合网络请求、API 调用)
retry() {
local max_attempts=${RETRY_MAX:-3}
local delay=${RETRY_DELAY:-2}
local attempt=1
while (( attempt <= max_attempts )); do
if "$@"; then
return 0
fi
if (( attempt == max_attempts )); then
err "已重试 $max_attempts 次,最终失败: $*"
return 1
fi
warn "第 $attempt 次失败,${delay}s 后重试..."
sleep "$delay"
delay=$(( delay * 2 )) # 指数退避:2s → 4s → 8s
(( attempt++ ))
done
}
# 使用示例
retry curl -sf https://api.example.com/health
retry pg_dump -U postgres mydb -f /backup/mydb.dump
适合重试的场景:
| 场景 | 重试次数 | 退避策略 |
|---|---|---|
| HTTP API 调用(5xx 错误) | 3 次 | 指数退避 |
| 数据库连接(启动竞态) | 5 次 | 固定间隔 2s |
| 文件上传(网络抖动) | 3 次 | 指数退避 |
| 数据库迁移失败 | 不重试 | 立即中止,人工介入 |
结构化日志规范
#!/usr/bin/env bash
# 统一日志格式:[时间戳] [级别] [脚本名] 消息
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly LOG_FILE=/var/log/myapp/ops.log
_log() {
local level=$1; shift
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [$level] [$SCRIPT_NAME] $*"
echo "$msg" | tee -a "$LOG_FILE"
[[ "$level" == "ERROR" ]] && echo "$msg" >&2
}
log() { _log "INFO" "$@"; }
warn() { _log "WARN" "$@"; }
err() { _log "ERROR" "$@"; }
# 用法
log "开始执行备份,目标: $DB_NAME"
warn "磁盘使用率 ${DISK_PCT}%,接近阈值"
err "备份失败: pg_dump 退出码 $?"
日志格式说明:
| 字段 | 示例 | 作用 |
|---|---|---|
| 时间戳 | 2026-03-22 02:00:01 | 精确到秒,便于关联其他日志 |
| 级别 | INFO / WARN / ERROR | 支持 grep "\[ERROR\]" 快速过滤 |
| 脚本名 | backup.sh | 多脚本共用同一日志文件时可区分 |
| 消息 | 包含关键参数(DB 名、文件路径等) | 能从日志还原操作上下文 |
完整生产级脚本模板
#!/usr/bin/env bash
# setup_app.sh — 安装并配置 myapp(幂等,可重复执行)
# 用法:sudo ./setup_app.sh [--dry-run]
set -euo pipefail
trap 'err "脚本在第 $LINENO 行失败"' ERR
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly LOG_FILE=/var/log/myapp/setup.log
readonly DRY_RUN="${1:-}"
_log() { local l=$1; shift; echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$l] $*" | tee -a "$LOG_FILE"; }
log() { _log "INFO" "$@"; }
warn() { _log "WARN" "$@"; }
err() { _log "ERROR" "$@" >&2; }
mkdir -p "$(dirname "$LOG_FILE")"
log "=== 开始执行 $SCRIPT_NAME ==="
[[ "$DRY_RUN" == "--dry-run" ]] && { log "Dry-run 模式,不实际执行"; exit 0; }
# 检查 root 权限
(( EUID == 0 )) || { err "需要 root 权限"; exit 1; }
# 幂等安装
install_pkg() {
dpkg -l "$1" &>/dev/null && { log "$1 已安装"; return; }
apt-get install -y "$1" && log "$1 安装完成"
}
install_pkg nginx
install_pkg nodejs
log "=== $SCRIPT_NAME 执行完成 ==="
常见误区
| 误区 | 正确做法 |
|---|---|
| 重试所有错误 | 只重试可恢复的临时错误;迁移失败、权限错误不应重试 |
日志只写 echo "done" | 日志应包含时间戳、操作对象、结果,便于事后审计 |
| 脚本不检查幂等,重跑追加配置 | 写入前用 grep -qF 检查是否已存在 |
用 ||true 吞掉所有错误 | 只对预期可失败的命令用 || true,关键操作必须失败传播 |
本节执行清单
- [ ] 检查现有脚本是否可幂等重复执行(执行两次结果是否一致)
- [ ] 为网络请求等临时失败场景加入指数退避重试
- [ ] 统一日志格式(时间戳 + 级别 + 脚本名)
- [ ] 为脚本加上
trap错误钩子,确保失败时有清晰提示
下一章:CI/CD 与发布流水线——把脚本接进真正的发布流程。