MySQL与PostgreSQL统一Shell自动化运维:备份、恢复、监控实战
数据库管理员的日常里备份、恢复、监控这三件事最磨人。手动执行命令久了早晚会出错要么漏备一个库要么恢复的时候发现备份文件损坏。我之前维护的一套环境里同时跑着 MySQL 和 PostgreSQL两边命令写法不一样参数差异也大干脆自己写了一套统一的 Shell 脚本体系把备份、恢复、监控全部自动化。这套东西跑了大半年帮我在凌晨的故障窗口里抢回过几次数据也把日常巡检的工作量降到了最低。这篇文章就把整套脚本的设计思路、核心实现、踩坑记录完整拆出来适合正在搭数据库自动化运维体系的同行参考。1. 脚本整体设计一句话讲清楚要做什么1.1 核心需求拆解动手写代码之前先把需求拆清楚。我给自己定的目标是一套脚本两种数据库三个能力。一套脚本指的是目录结构、配置文件、日志规范完全统一切换数据库类型只是改配置文件的事。两种数据库就是 MySQL 和 PostgreSQL这套环境里业务系统一半用 MySQL一半用 PostgreSQL两个都得管。三个能力就是备份、恢复、监控这三个功能互相独立又共用一套基础设施比如配置文件解析、日志输出、时间戳处理。备份的要求很直接每天凌晨全量备份备份文件压缩后保留 7 天超出自动清理。恢复的要求是能列出可用的备份列表能指定某个备份文件执行恢复恢复之前先把目标库清空。监控的要求是每 5 分钟采集一次关键状态指标异常时输出告警信息并记录日志。这样拆解完之后脚本的模块边界就很清晰了配置解析一个文件公共函数一个文件备份、恢复、监控各自独立成文件主入口负责调用。1.2 为什么用 Shell 而不是 Python这个问题我被人问过很多次。数据库备份恢复这种运维场景Shell 有几个天然优势。第一数据库原生命令就是最好的工具mysqldump、pg_dump 本身已经封装了太多底层细节我要做的只是把参数组合好、把流程控制好Shell 做流程控制非常顺手。第二Shell 脚本不依赖运行时环境只要目标机器上有 bash、有数据库客户端脚本就能跑不需要装 Python 依赖包。第三排查问题的时候Shell 脚本的每一行都是独立命令出错之后单独执行那条命令就能复现问题调试路径极短。那 Python 完全没有用吗也不是。如果要做复杂的解析、多维度的监控数据聚合、自动生成报表Python 确实更合适。但备份恢复监控这种场景Shell 完全够用维护成本也更低。别为了显得高级去引入不必要的技术栈这是我在生产环境里悟出来的道理。再说一句题外话实际上我见过不少团队的自动化脚本是从简单的 Shell 开始后来需求复杂了逐步迁移到 Python。如果你的环境一开始就有复杂的监控报表需求直接用 Python 起步也完全可以。关键的判断标准是脚本的核心工作到底是流程控制还是数据处理。1.3 目录结构与配置文件设计脚本要落地先把目录结构和配置文件设计好。dbadmin/ ├── bin/ │ ├── backup.sh # 备份主脚本 │ ├── restore.sh # 恢复主脚本 │ ├── monitor.sh # 监控主脚本 │ └── common.sh # 公共函数库 ├── conf/ │ └── dbadmin.conf # 全局配置文件 ├── logs/ │ ├── backup.log │ ├── restore.log │ └── monitor.log └── backup/ ├── mysql/ │ ├── daily/ │ └── archive/ └── postgresql/ ├── daily/ └── archive/配置文件的写法是按数据库实例分 section类似 INI 风格# 全局配置 BACKUP_BASE/data/dbadmin/backup LOG_BASE/data/dbadmin/logs RETENTION_DAYS7 # MySQL 实例 DB_TYPEmysql MYSQL_HOST127.0.0.1 MYSQL_PORT3306 MYSQL_USERbackup_user MYSQL_PASSWORDencrypted_password_placeholder MYSQL_CHARSETutf8mb4 # PostgreSQL 实例 PG_HOST127.0.0.1 PG_PORT5432 PG_USERbackup_user PG_DATABASEmydb配置文件单独拿出来有几个好处。一是数据库连接信息和脚本逻辑分离换库、换密码不用改脚本。二是方便多实例扩展同一个配置文件里可以定义多组实例信息脚本遍历执行。三是权限管理简单配置文件权限设为 600只允许管理员读写避免数据库密码泄露。1.4 公共函数库的实现common.sh 是整个脚本体系的底座里面放了日志输出、时间戳处理、备份文件命名、配置解析、数据库连接测试这些通用功能。日志输出函数讲究一个规范我统一用级别 [时间] 消息的格式方便后续用 grep 过滤log_info() { echo $(date %Y-%m-%d %H:%M:%S) [INFO] $* ${LOG_FILE} } log_error() { echo $(date %Y-%m-%d %H:%M:%S) [ERROR] $* ${LOG_FILE} }这里的 LOG_FILE 是全局变量在进入每个脚本的时候根据脚本名设置比如 backup.sh 写 backup.logmonitor.sh 写 monitor.log。我见过不少脚本把日志写屏幕不落盘出问题的时候连排查依据都没有这属于基本习惯问题。还有一个非常实用的函数是检查命令是否存在check_command() { local cmd$1 if ! command -v ${cmd} /dev/null; then log_error command not found: ${cmd} exit 1 fi }备份脚本跑在凌晨如果 mysqldump 或者 pg_dump 因为 PATH 环境变量不一致找不到整个备份就静默失败了这个检查函数能提前暴露问题。2. 备份模块两种数据库的统一与差异化处理2.1 备份策略的选择与原因备份策略是整套脚本的核心决策。我最终采用的是每日全量备份 短期保留的方案没有做增量备份。为什么不做增量MySQL 要讲增量就得配合 binlogPostgreSQL 要讲增量就得做 WAL 归档两种数据库的增量机制完全不同一套脚本统一处理的复杂度会直线上升。而业务库的数据量在可预见的范围内不会爆炸到全量备份扛不住全量备份是恢复路径最简单、最不容易出错的方式。如果你的库真的很大比如单库几百 GB每日全量确实不现实那就需要在脚本层面对 MySQL 和 PostgreSQL 分别扩展增量能力。MySQL 可以通过记录 binlog 位点信息配合增量 binlog 备份实现PostgreSQL 可以通过配置 WAL 归档目录实现。这个扩展方向我在后面恢复部分会提到。备份保留天数定为 7 天这是从两个维度算出来的。第一恢复需求的时间窗口。业务方的要求是“至少能恢复到一周前的任意一天”7 天正好覆盖。第二磁盘占用。备份文件压缩后平均每个库 2-4GB7 天就是 30GB 左右当前磁盘容量完全放得下。保留策略在配置文件里控制调整只需要改一个变量。2.2 MySQL 备份脚本的关键参数MySQL 备份用 mysqldump核心命令长这样backup_mysql() { local db_list$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} -N -e SHOW DATABASES; | grep -Ev information_schema|performance_schema|sys) local backup_date$(date %Y%m%d_%H%M%S) for db in ${db_list}; do mysqldump -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} \ --single-transaction \ --routines \ --triggers \ --events \ --set-gtid-purgedOFF \ --databases ${db} 2/dev/null \ | gzip ${BACKUP_BASE}/mysql/daily/${db}_${backup_date}.sql.gz done }这里面的参数个个都有讲究。--single-transaction是 InnoDB 引擎做在线热备的关键它通过启动事务时创建一致性快照的方式不锁表完成备份业务不需要停机。--routines --triggers --events确保存储过程、触发器、定时事件这些对象不会丢很多新手备份只导数据不带这些恢复出来的库缺胳膊少腿。--set-gtid-purgedOFF是给开启了 GTID 的环境准备的如果不加这个参数备份文件里会写入 GTID 信息恢复到目标库时如果目标库已经执行过这些事务会直接报错。还有个细节--databases参数让备份文件里带上 CREATE DATABASE 语句恢复的时候不用手工建库。如果是导出单库不带这个参数恢复的时候就得先 CREATE DATABASE 再导入多一步操作就多一个坑。2.3 PostgreSQL 备份脚本的关键参数PostgreSQL 的逻辑备份用 pg_dump核心命令是backup_postgresql() { local backup_date$(date %Y%m%d_%H%M%S) pg_dump -h${PG_HOST} -p${PG_PORT} -U${PG_USER} -d${PG_DATABASE} \ --formatcustom \ --compress9 \ --no-owner \ --no-privileges \ -f ${BACKUP_BASE}/postgresql/daily/${PG_DATABASE}_${backup_date}.dump }PG 的备份参数选型和 MySQL 差异很大。--formatcustom是 PostgreSQL 特有的自定义格式它不是纯 SQL 文本而是一个内部结构文件。好处是支持pg_restore做选择性恢复比如只恢复某张表支持并行恢复压缩率也比文本格式好。如果你只是粗粒度整库备份也可以用--formatplain输出 SQL 文本灵活性差一些但是文件可以直接看排查问题方便。--no-owner --no-privileges这两个参数一定要加。备份文件里不包含对象所属用户的信息恢复到新环境的时候如果新环境里的用户和原环境不一致不会因为找不到用户而失败。权限信息不导出恢复完再统一授权避免权限错乱。PG 备份这里有个容易踩的坑pg_dump 的版本必须和数据库服务端版本匹配。比如服务端是 PostgreSQL 15客户端工具最好是 15.x 版本跨大版本恢复容易失败或者出现诡异的数据问题。老版本 pg_dump 备份新版本库可能直接报错不兼容新版本备份老版本库也可能出现 SQL 语法不支持的问题。2.4 备份文件的校验与清理备份完成不代表文件一定能用。我加了两个校验步骤。第一是大小校验备份之后立即检查文件大小verify_backup_file() { local file$1 local min_size$2 if [ ! -s ${file} ]; then log_error backup file is empty: ${file} return 1 fi local actual_size$(stat -c%s ${file}) if [ ${actual_size} -lt ${min_size} ]; then log_error backup file too small: ${file} (${actual_size} bytes) return 1 fi return 0 }MySQL 备份对文件做 gzip 完整性校验其实这个可以交给 gzip -t 做脚本里加一条check_gzip_integrity() { if ! gzip -t ${file}; then log_error gzip integrity check failed: ${file} return 1 fi }第二是备份文件保留清理用 find 按 mtime 删除cleanup_old_backups() { find ${BACKUP_BASE}/mysql/daily -name *.sql.gz -mtime ${RETENTION_DAYS} -delete find ${BACKUP_BASE}/postgresql/daily -name *.dump -mtime ${RETENTION_DAYS} -delete }清理逻辑放在备份成功后执行原理是只有当前这轮备份成功生成的备份文件才有资格触发清理如果备份失败了旧文件还留着不会出现“备份失败还把老备份清了”的恶性事故。3. 恢复模块备份的意义在于能恢复3.1 恢复流程设计原则备份做得再勤快恢复不了都是零。我一直强调一个观点数据库备份方案的价值是用恢复的成功率来衡量的。所以恢复模块的设计第一条原则是操作可逆、有确认步骤第二条原则是所有的恢复操作都记录审计日志。恢复模块的执行流程是这样的检查恢复环境 - 列出备份文件 - 选择备份文件 - 确认目标库 - 执行恢复 - 验证恢复结果这里每一步都设计了确认机制避免误操作。恢复之前必须输入目标数据库名称确认再输入备份文件路径确认最后还要敲一个yes才能继续执行最大程度防止恢复Switch里手滑把生产库给清了。3.2 MySQL 恢复实现MySQL 的恢复核心逻辑restore_mysql() { local backup_file$1 local target_db$2 if [ ! -f ${backup_file} ]; then log_error backup file not found: ${backup_file} exit 1 fi # 解压并检查内容 gunzip -t ${backup_file} || { log_error backup file is corrupted: ${backup_file} exit 1 } echo Restoring MySQL database: ${target_db} read -p Type yes to confirm: confirm if [ ${confirm} ! yes ]; then echo Restore cancelled. exit 1 fi # 清空目标库并重新导入 mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_ROOT_USER} -p${MYSQL_ROOT_PASSWORD} \ -e DROP DATABASE IF EXISTS ${target_db}; CREATE DATABASE ${target_db} CHARACTER SET utf8mb4; gunzip -c ${backup_file} | mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_ROOT_USER} -p${MYSQL_ROOT_PASSWORD} ${target_db} if [ $? -eq 0 ]; then log_info restore completed: ${backup_file} - ${target_db} else log_error restore failed: ${backup_file} fi }恢复操作用 root 账号而不是备份账号因为 DROP DATABASE 和 CREATE DATABASE 需要更高权限。整个流程中最危险的是清空目标库的步骤所以我才会要求二次确认。字符集固定用 utf8mb4和备份脚本的 MYSQL_CHARSET 配置保持对应。具体操作中我自己做恢复的时候很少用--databases选项的备份去做单库恢复因为这种情况下备份文件自带 CREATE DATABASE 语句清库建库反而容易冲突。更安全的做法是用不带--databases的单库导出文件恢复命令只需mysql target_db backup.sql。如果你拿不准备份文件的格式先解压看头几行再决定恢复方式。3.3 PostgreSQL 恢复实现PostgreSQL 的恢复用 pg_restorerestore_postgresql() { local backup_file$1 local target_db$2 if [ ! -f ${backup_file} ]; then log_error backup file not found: ${backup_file} exit 1 fi echo Restoring PostgreSQL database: ${target_db} read -p Type yes to confirm: confirm if [ ${confirm} ! yes ]; then echo Restore cancelled. exit 1 fi # 先断开目标库的连接 psql -h${PG_HOST} -p${PG_PORT} -U${PG_SUPERUSER} -d postgres \ -c SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname${target_db} AND pid pg_backend_pid(); # 删除并重建目标库 dropdb -h${PG_HOST} -p${PG_PORT} -U${PG_SUPERUSER} ${target_db} createdb -h${PG_HOST} -p${PG_PORT} -U${PG_SUPERUSER} -T template0 ${target_db} pg_restore -h${PG_HOST} -p${PG_PORT} -U${PG_SUPERUSER} \ --dbname${target_db} \ --no-owner \ --no-privileges \ --verbose ${backup_file} if [ $? -eq 0 ]; then log_info restore completed: ${backup_file} - ${target_db} else log_error restore failed: ${backup_file} fi }这里的几个细节非常关键。第一恢复前用pg_terminate_backend强制断开目标库的活跃连接否则 DROP DATABASE 会因为会话占用而失败这个是 PostgreSQL 特有的问题MySQL 那边没有对应操作。第二createdb 用--templatetemplate0而不是默认的 template1是为了避免从 template1 继承不必要的自定义对象保证新库干净。第三恢复到新库之后如果原库有序列、外键等依赖关系需要注意恢复顺序--verbose参数能看到每个对象的恢复状态建议首次恢复时加上。遇到同一份备份要恢复到多个环境的情况比如从生产恢复到测试记得修改序列的当前值pg_restore 恢复的序列值是从备份时刻的当前值开始的如果已经有测试数据写入容易出现主键冲突这种情况得手动调整序列。3.4 恢复演练的必要性这部分经验是从一次真实的教训里得来的。有一次同事在凌晨执行恢复操作备份文件解压正常大小正常但导入到一半报错主键冲突。原因是备份脚本通过 crontab 执行时数据一直在写入备份时刻拿到的事务快照里数据和真实数据不完全一致恢复的时候把重复数据导进去了。后来我给恢复模块加了一步校验逻辑恢复完成后对关键表做 count 校验对比备份执行时刻的统计信息和恢复后的统计信息偏差超过阈值就输出告警。更重要的改进是每个月做一次完整的恢复演练恢复到独立的临时库验证备份文件、恢复流程、校验逻辑整个链路都是通的。恢复演练这个习惯救过我的命。某个线上事故排查到最后发现某个旧备份文件早就损坏了但因为每个月都在演练及时发现并重新做了完整备份没有造成数据灾难。不要等到真出事了才测恢复流程那是拿生产数据做赌注。4. 监控模块提前发现比事后处理更重要4.1 监控维度与指标体系备份脚本管的是数据安全监控脚本管的是运行状态。我把监控指标分成三层存活层数据库进程在不在端口通不通资源层连接数、慢查询数、锁等待数量、WAL 文件增长速度容量层数据目录磁盘使用率、数据库大小每一层对应不同的告警阈值。存活层异常直接严重告警资源层和容量层使用百分比阈值分级告警。监控循环逻辑简单直接检查连接 - 采集指标 - 比对阈值 - 输出结果。4.2 MySQL 监控状态采集MySQL 的关键指标采集用 SQL 查询-- 当前活跃连接数 SELECT COUNT(*) FROM information_schema.processlist WHERE command ! Sleep; -- 慢查询数量基于全局状态变量 SHOW GLOBAL STATUS LIKE Slow_queries; -- 当前锁等待 SELECT COUNT(*) FROM information_schema.processlist WHERE state Waiting for table metadata lock; -- 磁盘容量基于外部 df df -h /data | tail -1 | awk {print $5}脚本里对状态信息的处理和阈值比对monitor_mysql() { local thread_count$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} -N -e SELECT COUNT(*) FROM information_schema.processlist WHERE command ! Sleep;) local slow_queries$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} -N -e SHOW GLOBAL STATUS LIKE Slow_queries; | awk {print $2}) local lock_wait$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} -N -e SELECT COUNT(*) FROM information_schema.processlist WHERE state Waiting for table metadata lock;) echo ${thread_count} ${slow_queries} ${lock_wait} ${MONITOR_DATA_FILE} }慢查询的计数是累计值需要和上一次采样值做差值才能得到时间窗口内的慢查询增量。差值计算放到了监控脚本的数据处理函数里这样每个采样周期的慢查询数才是有效的。4.3 PostgreSQL 监控状态采集PostgreSQL 的关键指标采集用系统视图-- 活跃连接数 SELECT COUNT(*) FROM pg_stat_activity WHERE state active; -- 等待事件锁竞争 SELECT COUNT(*) FROM pg_stat_activity WHERE wait_event_type Lock; -- 事务回滚率反映应用健康度 SELECT 100.0 * xact_rollback / (xact_commit xact_rollback) AS rollback_ratio FROM pg_stat_database WHERE datname current_database(); -- WAL 生成速率基于文件变化 SELECT pg_walfile_name(pg_current_wal_lsn());PostgreSQL 的监控脚本还需要关注异常查询我最常用的一个查询是抓执行时间超过阈值的慢查询SELECT pid, now() - xact_start AS duration, query FROM pg_stat_activity WHERE state active AND now() - xact_start interval 5 minutes;这个查询可以用来定位长期跑着的异常查询也可以作为续写长事务告警的基础。长事务是 PostgreSQL 的一大杀手它会导致 VACUUM 无法回收死元组进而导致表膨胀、数据库越来越大。数据目录膨胀的监控我用了 PostgreSQL 查询加外部文件系统检查组合方式。外部文件系统检查用 df 命令看磁盘剩余量数据膨胀从数据库内部看 pg_database_size 的变化趋势两者结合就能判断是数据库本身膨胀还是其他文件占满了磁盘。4.4 告警逻辑与通知方式监控模块的告警逻辑用最朴素的阈值判断threshold_check() { local current$1 local warn$2 local crit$3 local metric_name$4 if [ ${current} -ge ${crit} ]; then log_error CRITICAL: ${metric_name} ${current}, threshold ${crit} send_alert CRITICAL: ${metric_name} is ${current} elif [ ${current} -ge ${warn} ]; then log_warn WARNING: ${metric_name} ${current}, threshold ${warn} else log_info OK: ${metric_name} ${current} fi }告警通知方式我用的是 Shell 脚本直接调用 webhook 的方式不需要额外安装监控组件。比如调用企业内部的消息通知接口把告警内容通过 HTTP POST 发送出去。考虑到通用性和安全性具体通知渠道不在本文展开核心原则是告警必须在第一时间到达值班人员的手机端。通知频率要防抖动同一个指标连续触发告警时只发一次等恢复正常后再发恢复通知。不然半夜一个锁等待问题能刷上百条告警值班人员的手机直接被打爆关键告警被淹没在噪音里反而会失效。5. 调度、日志与自保护机制5.1 定时任务调度方案自动化必须配合调度器。我的调度方案是 crontab 设置三个时间点# 每天凌晨 1 点执行全量备份 0 1 * * * /data/dbadmin/bin/backup.sh /data/dbadmin/logs/cron.log 21 # 每 5 分钟执行一次监控采集 */5 * * * * /data/dbadmin/bin/monitor.sh /data/dbadmin/logs/monitor_cron.log 21 # 每天上午 10 点执行备份完整性检查 0 10 * * * /data/dbadmin/bin/backup_check.sh /data/dbadmin/logs/check_cron.log 21crontab 里有一个常年坑人的细节crond 执行脚本的环境变量非常精简PATH 很可能没有包含数据库客户端命令的目录。所以脚本开头统一 export PATH具体的环境变量在代码里显式声明。否则你会看到 cron 任务白跑了一晚上log 里全是 command not found。5.2 日志规范与退出码约定日志是运维排障的眼睛。脚本里每个关键动作都输出日志格式统一为级别 时间 消息。同时脚本的退出码也有约定0 表示成功1 表示失败2 表示参数错误这样调度器可以根据退出码判断是否需要重试或触发告警。# 备份脚本结尾统一处理退出码 if [ ${FAIL_COUNT} -gt 0 ]; then log_error backup finished with ${FAIL_COUNT} failure(s) exit 1 else log_info backup finished successfully exit 0 fi日志文件再配一个 logrotate 策略按天轮转保留 30 天。不然跑上几个月磁盘被日志塞满监控自己先挂了那就闹笑话了。5.3 脚本自锁与防重入凌晨备份如果前一个任务还没结束后一个任务又启动了两个备份同时写文件大概率会出问题。解决办法是加一个文件锁。lock_script() { local lock_file/tmp/dbadmin_$(basename ${0}).lock exec 9${lock_file} if ! flock -n 9; then log_error another instance is running, exit exit 1 fi }这里用 flock 做锁exec 9打开一个文件描述符flock -n 9尝试获取锁失败就退出。这个锁的粒度是整个脚本不管里面跑多少个库同一时间只允许一个备份任务在跑。如果备份全库需要的时间超过调度周期宁可跳过这次的备份也不要让两个任务叠加。监控脚本也会有类似问题。如果监控查询本身因为数据库锁等待卡住了下一轮监控任务又启动会堆积一堆查询连接。我同样给监控脚本加了锁采集间隔也做了超时保护单次监控执行超过 60 秒就强制结束。6. 常见问题与排查实录6.1 备份脚本的经典故障场景问题一备份文件为空但是脚本显示成功这个坑我在 MySQL 备份上遇到过。原因是 mysqldump 执行时数据库权限不足mysqldump 把错误信息输出到了 stderr我在脚本里把 stderr 重定向到了 /dev/null结果 mysqldump 退出码虽然是非 0但外层管道 gzip 的退出码是 0管道整体被判定为成功。后来改成使用set -o pipefail选项让管道的退出码以最右侧非 0 命令为准再配合文件大小校验这个坑才彻底堵上。set -o pipefail mysqldump ... | gzip backup.sql.gz没有 pipefailShell 管道只看最后一个命令的返回值。mysqldump 在管道中间挂了只要 gzip 成功读出空输入并正常退出整个备份就被误判为成功。这类故障如果没有文件大小校验可以潜伏很久不被发现等需要数据的时候才发现备份全是空文件后果不堪设想。问题二cron 环境变量导致命令找不到之前说过 crond 的环境变量精简我再补充一个更隐蔽的版本。有些数据库安装的时候会把 mysqldump 放到/usr/local/mysql/bin/而这个路径不在 crond 的 PATH 里。手动执行脚本没问题cron 执行就报mysqldump: command not found。解决办法就是在脚本开头显式把所有需要的二进制文件放到 PATH 里或者干脆用绝对路径调用。 。问题三PostgreSQL 备份时间过长pg_dump 的--formatcustom备份大库的时候单线程压缩会比较慢。解决方法是开启并行压缩--compression9配合--jobs4这个选项取决于版本老版本不支持并行压缩。另一个方案是用pigz替代 gzip 做管道压缩多核并行压缩速度能提升好几倍。不过不建议在备份高峰期做并行压缩CPU 占用高容易影响在线业务。6.2 恢复过程的风险点问题一目标库还有活跃连接DROP DATABASE 失败这个问题 PostgreSQL 尤其常见。可用pg_terminate_backend强制断开具体代码在 3.3 里已经写过了。注意先断开连接再执行 DROP顺序不能反。MySQL 这边清库用的是 DROP DATABASE 后重建如果连接池里有活跃连接通常 DROP 会直接成功或者等待但建议恢复前先通知应用停服避免数据写了一半被清掉。问题二字符集不一致导致恢复乱码MySQL 备份的时候用了--default-character-setutf8mb4恢复的时候没带这个参数默认字符集可能是 latin1中文字符全部变成乱码。恢复命令显式加上字符集参数和备份时保持一致。PostgreSQL 的乱码问题主要出现在库的编码和客户端编码不一致恢复前用SET client_encoding UTF8;固定客户端编码。问题三恢复了旧数据新数据丢了这种情况常见于“用昨天的备份恢复到生产库但是今天到事故点之前的数据在 binlog 里”。MySQL 的恢复流程要配合 binlog 做时间点恢复PITR备份binlog 才能恢复到故障发生前的最后一致状态。PostgreSQL 对应的是 WAL 日志回放。我在这整套脚本里暂时没有把 PITR 实现成完全自动化因为涉及 binlog/WAL 的连续性、时间点选择等复杂逻辑但建议对数据敏感度高的业务尽快规划 PITR 方案。我之前踩过的一个教训是备份脚本每天跑但 binlog 保留时间只设置了一天等需要恢复到“昨天下午 4 点”状态的时候发现 binlog 已经被清理了。所以如果你决定做 PITRbinlog 的保留周期至少要比全量备份周期长交叉覆盖才有意义。6.3 监控脚本误报与漏报的处理监控误报有两个常见原因一是阈值设置不合理二是采集数据本身的问题。阈值设置这块连接数的告警阈值不能是固定值。如果一个库高峰期 200 连接是常态你设 100 就整天报警。需要观察一段时间取正常运行的基线数据在基线上上浮一定比例设告警。我刚上线监控脚本的时候告警频繁值班同事都麻木了后来花了两个星期观察基线、调整阈值告警才变得有意义。采集数据的问题主要集中在慢查询计数上。慢查询状态变量是累计值不是周期值直接和阈值对比一定会误报。处理方法就是每次采集后用变量保存上一次采样的值做差值比较。这个逻辑要知道不然监控数据永远是花的。PostgreSQL 的回滚率指标同样有类似问题重启数据库后统计信息清零需要重新积累基线。漏报的问题大多数出现在“采集成功后写数据失败但脚本没退出”。监控脚本每一步都要判断返回值写数据文件失败、解析指标失败、计算差值失败任何一个环节都可能导致监控数据缺失而这种静默失败最危险因为看起来脚本在跑实际上指标已经断了。我给监控脚本加了一个更完善的方案每次采集都输出一个带时间戳的心跳文件另有一个检查脚本负责确认心跳是否过期过期就告警。这样监控自身的健康也纳入了监控范围。这套脚本之外我最后想说的话这套脚本体系看着不复杂但每行命令都是从实际运维的坑里爬出来的。比如 pipefail 的教训、cron 环境变量的问题、pg_dump 版本匹配的限制这些都是测试环境里跑一百遍都发现不了、生产环境一遇故障就坑人的细节。如果你要自己搭一套我的建议是别一上来就追求功能完整先把备份模块跑通加上文件大小校验然后做一次恢复演练确认链路是通的再迭代加监控、加告警。备份能恢复是底线监控是锦上添花。脚本本身不需要多么花哨关键是在你需要它的时候它能稳稳地把事办好。这套脚本结构也预留了扩展能力。哪天业务量大到需要增量备份可以单独加增量模块哪天要接入统一的运维平台把日志和告警输出改成平台兼容的格式就行。自动化运维的道路上备份、恢复、监控永远是地基把这三件事做扎实了后面才有底气去折腾更复杂的架构。