4. Shell脚本驱动PostgreSQL运维
当PostgreSQL实例数量从一两台增长到十几台甚至几十台时,手工执行 pg_dump、VACUUM、日志清理等操作会迅速变得不可维护。Shell脚本是把这些重复动作固化为可复用资产的最直接手段,但一个能长期稳定运行的运维脚本,必须同时解决四件事:连接怎么建立、错误怎么捕获、过程怎么留痕、并发怎么约束。本节围绕这四点,给出可直接改造使用的备份、检查、清理脚本骨架。
连接管理:把连接参数集中收口
连接信息散落在每个脚本里是运维事故的常见来源。推荐做法是用一个共享的环境文件统一管理,脚本通过 source 引入。PostgreSQL官方客户端工具(psql、pg_dump、pg_isready等)都识别 PGHOST、PGPORT、PGUSER、PGDATABASE、PGPASSWORD 等环境变量,因此无需在每个命令后重复拼参数。
# /etc/pgops/pg.env
export PGHOST=10.20.30.11
export PGPORT=5432
export PGUSER=ops_reader
export PGDATABASE=appdb
export PGPASSWORD_FILE=/etc/pgops/.pgpass_ops
export PGCONNECT_TIMEOUT=5
export PGAPPNAME=pgops_backup
密码不建议直接写进环境变量,而是通过 PGPASSFILE 指向权限为 600 的密码文件,或使用 .pgpass 机制。连接建立后,脚本应先做一次连通性探测,而不是直接进入业务命令:
source /etc/pgops/pg.env
export PGPASSFILE="$PGPASSWORD_FILE"
if ! pg_isready -q -t 5; then
echo "[FATAL] PostgreSQL $PGHOST:$PGPORT 不可达" >&2
exit 2
fi
pg_isready 返回0表示接受连接,1表示拒绝,2表示无响应,3表示参数错误。把返回码映射为脚本退出码,可以让上层调度器(cron、systemd timer、Airflow)准确判断失败类型。
错误处理:set -euo pipefail 与 trap 的组合
Shell默认不会因为某条命令失败而中止,这在运维脚本里非常危险。标准做法是在脚本头部声明严格模式:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
-e 让命令失败即退出,-u 让引用未定义变量报错,-o pipefail 让管道中任一环节失败都算失败。但严格模式也有盲区:if 条件、|| 右侧、! 取反的命令不会触发退出。因此关键操作仍应显式判断返回码,并配合 trap 做清理与告警:
LOG=/var/log/pgops/backup_$(date +%F).log
TMPDIR=$(mktemp -d /tmp/pgbackup.XXXXXX)
cleanup() {
local rc=$?
rm -rf "$TMPDIR"
if (( rc != 0 )); then
echo "[$(date +%T)] 备份失败,退出码=$rc" >>"$LOG"
# 可在此接入告警:curl -s "https://alert/api?msg=pgbackup_fail"
fi
exit $rc
}
trap cleanup EXIT INT TERM
这里 trap ... EXIT 保证无论脚本正常结束还是中途失败,临时目录都会被清理,失败信息都会被记录。注意 cleanup 中先保存 $?,否则后续命令会覆盖真实退出码。
日志记录:结构化、可轮转、可追溯
运维脚本的日志要回答三个问题:什么时候、对哪个实例、做了什么、结果如何。建议统一日志函数,把时间戳、级别、脚本名、目标实例拼进每一行:
log() {
local level=$1; shift
printf '%s [%s] [%s@%s] %s\n' \
"$(date '+%F %T')" "$level" "$(basename "$0")" "$PGHOST" "$*" \
| tee -a "$LOG"
}
log INFO "开始备份 $PGDATABASE"
log WARN "磁盘剩余 $(df -P /backup | awk 'NR==2{print $4}') KB"
log ERROR "pg_dump 返回码 $rc"
日志文件按天命名并配合 find /var/log/pgops -name '*.log' -mtime +14 -delete 做保留策略,避免日志本身成为磁盘隐患。若接入集中日志系统,可把 tee -a 换成同时写入本地与 logger -t pgops。
并发控制:flock 防止任务重叠
备份、VACUUM FULL、索引重建这类任务一旦重叠执行,会同时占用大量I/O和连接,甚至互相阻塞。最轻量的互斥手段是 flock,它基于文件锁,进程退出自动释放,不会留下死锁:
LOCK=/var/lock/pgops_backup.lock
exec 9>"$LOCK"
if ! flock -n 9; then
log WARN "已有备份任务在运行,本次跳过"
exit 0
fi
-n 表示非阻塞,拿不到锁立即返回;若希望排队等待,可改为 flock -w 300 9 最多等300秒。锁文件放在 /var/lock 或 /run 下,重启后自动清理。对于需要限制数据库侧并发的场景,还可以在会话级设置 SET statement_timeout 和 SET lock_timeout,避免单个慢查询拖垮实例。
可复用脚本骨架:备份、检查、清理
把上述四层能力封装成公共库 pgops_lib.sh,三个业务脚本只需关注自身逻辑。以下为备份脚本核心片段(示例参数,非实测结果):
#!/usr/bin/env bash
set -euo pipefail
source /etc/pgops/pg.env
source /etc/pgops/pgops_lib.sh
BACKUP_DIR=/backup/pg
STAMP=$(date +%F_%H%M)
OUT="$BACKUP_DIR/${PGDATABASE}_${STAMP}.dump"
acquire_lock /var/lock/pgops_backup.lock || { log WARN "跳过:已有任务"; exit 0; }
check_conn || exit 2
log INFO "开始 pg_dump -Fc 到 $OUT"
if pg_dump -Fc -Z 6 -f "$OUT"; then
SIZE=$(stat -c %s "$OUT")
log INFO "备份完成,大小 ${SIZE} 字节"
else
log ERROR "pg_dump 失败"
exit 1
fi
find "$BACKUP_DIR" -name '*.dump' -mtime +7 -delete
log INFO "清理7天前备份完成"
检查脚本则侧重只读探测,输出可直接被监控采集:
psql -Atc "SELECT count(*) FROM pg_stat_activity WHERE state='active'" \
| awk '{ if ($1>50) print "WARN active=" $1; else print "OK active=" $1 }'
psql -Atc "SELECT datname, pg_database_size(datname)/1024/1024 AS mb
FROM pg_database WHERE datistemplate=false ORDER BY mb DESC LIMIT 5"
清理脚本处理的是归档日志、临时文件与过期备份,务必先 find ... -print 预览,确认无误后再改为 -delete,并同样加锁防止与备份任务冲突。
/opt/pgops/bin/,公共库放 /opt/pgops/lib/,环境文件放 /etc/pgops/,日志放 /var/log/pgops/。目录职责清晰后,cron 或 systemd timer 只需调用脚本路径,无需关心内部实现。grep 未匹配返回1会导致脚本意外退出,常见写法是 grep -q pattern file || true,或把可能失败的判断放进 if 条件中。此外,trap 中不要执行可能再次失败的命令,否则会掩盖原始退出码。判断脚本是否真正可复用,可以看三个信号:换一台实例只需改 pg.env 而不动脚本;任何一步失败都能在日志里定位到具体命令与退出码;同一任务被重复触发时不会产生第二份并发执行。做到这三点,Shell脚本才从“一次性命令集合”升级为可纳入自动化调度的运维资产。