幂等性、重试与日志规范
High Contrast
Dark Mode
Light Mode
Sepia
Forest
3 min read661 words

幂等性、重试与日志规范

好的运维脚本不是"跑一次成功",而是"多跑几次也不会把系统搞乱"。

三个核心原则

原则 定义 违反的后果
幂等性 重复执行 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,关键操作必须失败传播

本节执行清单

下一章CI/CD 与发布流水线——把脚本接进真正的发布流程。