CClara轻量论坛系统

数据库备份与恢复策略:全量 / 增量 / 逻辑 / 物理备份

最长的电影
最长的电影 正式会员正式会员
发布于 2026-10-25 09:58 ·3 浏览 ·12 回复
内容摘要

该内容说明逻辑备份、物理备份、全量备份、增量备份各自的区别与适用场景,演示用 mysqldump 执行全量逻辑备份的参数要点、冷备与 XtraBackup 物理备份做法,以及开启 binlog 并按时间点重放实现增量恢复,同时提醒图片附件需单独打包站点目录,最终推荐每周全量逻辑备份加 binlog 增量与站点目录打包的生产组合方案。

学完这篇,你能给自己的论坛搭一套「出事真能救回来」的 MySQL 备份方案,并知道全量、增量、逻辑、物理四种备份各自解决什么问题、什么时候用哪一种。

第一步:先分清四种备份到底是什么

  • 逻辑备份:把数据和表结构导出成 SQL 文本(mysqldump)。文件小、可读、能跨版本迁移,恢复靠重新执行 SQL。缺点是数据量大时慢。
  • 物理备份:直接复制 MySQL 的数据文件目录(或 XtraBackup)。快,但基本只能恢复到同版本同环境。
  • 全量备份:每次都备完整一份。最省心,占空间。
  • 增量备份:只备上次之后变化的部分。省空间,但恢复要「全量 + 一串增量」按顺序重放,链条断一环就废。

实际生产里不会四选一,常见组合是:每周一次全量逻辑备份 + binlog 做增量 + 站点目录单独打包。

第二步:做一次全量逻辑备份

在服务器上执行(数据库名按你安装时填的替换):

mysqldump -u root -p \
  --single-transaction --quick \
  --default-character-set=utf8mb4 \
  --routines --triggers --events \
  --databases 你的库名 | gzip > /backup/db_$(date +%F).sql.gz

几个参数别省:

  • --single-transaction:InnoDB 下不锁表导出,论坛正常访问不受影响(Clara BBS 的表默认都是 InnoDB)。
  • --default-character-set=utf8mb4:不加容易在中文字段上出乱码。
  • --routines --triggers --events:插件可能自建表、加触发器,一起带走。
  • --databases:会把 CREATE DATABASE 语句一起写进去,恢复时不用先建库。

注意:mysqldump 只备数据库。论坛的图片、附件、头像都存在服务器目录里,不在表中。备份时把整个站点目录一起打包,否则恢复完帖子还在、图全裂了。

第三步:物理备份怎么做

最稳的物理备份是冷备:停 MySQL,整目录复制,再启回来。

systemctl stop mysql
cp -a /var/lib/mysql /backup/mysql_cold_$(date +%F)
systemctl start mysql

不停机就用 XtraBackup,但对新手来说,逻辑备份 + binlog 已经够用,物理备份更适合数据库几十 G 以上的场景。如果你用的是宝塔这类面板,面板自带的数据库备份就是逻辑备份的可视化版本,可以直接用。

第四步:用 binlog 做增量恢复

增量备份的核心不是文件,而是 MySQL 的二进制日志。确认 my.cnf 里开了:

[mysqld]
log-bin=mysql-bin
binlog_format=ROW
binlog_expire_logs_seconds=604800   # 保留 7 天

备份 binlog 本身就是备份文件:

mysqlbinlog --read-from-remote-server --host=127.0.0.1 -u root -p \
  --raw --stop-never mysql-bin.000001 &

恢复时先在临时库上验证,再用 --stop-datetime 重放到误操作前一刻,避免把「误删帖」也一起重放回去。

注意:不要用论坛自带的站内计划任务去跑 mysqldump。那套 Cron::register 机制是给系统内部任务用的,靠前台访问懒触发,没流量就不跑,备份会莫名其妙断档。数据库备份一律交给系统级 crontab:

0 3 * * * /root/backup.sh >> /var/log/dbbackup.log 2>&1

第五步:恢复演练,别等出事才第一次跑

gunzip < /backup/db_2025-01-01.sql.gz | mysql -u root -p

恢复完按这个顺序自检:能登录后台 → 首页版块正常 → 打开一个带图的帖子 → 上传一张图 → 发一条回复。四项都过,才算恢复成功。

注意:如果开了 Redis 缓存,恢复数据库后旧缓存键可能残留。Redis 里存的只是缓存不是数据,清掉会按需重建,不用把 Redis 当备份对象。真正的数据只有 MySQL 库 + 站点文件目录。

小结

  • 逻辑备份用 mysqldump --single-transaction,不锁表、带 routines/triggers、显式指定 utf8mb4。
  • 物理备份快但绑定环境,冷备要停库,热备上 XtraBackup。
  • 增量靠 binlog,前提是全量备份和 binlog 都连续不断档。
  • 数据库 + 站点文件目录必须一起备份,只备库等于没备图。
  • 用系统 crontab 定时跑,别用站内懒触发的计划任务。
  • 每季度真跑一次恢复演练,备份不验证等于没有备份。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-975.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

dp32323
dp32323 正式会员正式会员 1楼 2026-10-25 10:04

这篇的思路没问题(全量逻辑 + binlog 增量 + 站点目录单独打包确实是通用解法),但第四步的 mysqlbinlog 命令发到一半断了,而且在「恢复到能用」这个环节漏了两个容易出问题的细节。

补上增量恢复的完整流程,顺序不能乱:先导入全量备份(gunzip < db_xxx.sql.gz | mysql -u root -p),再把全量时间点之后的 binlog 依次重放,最后用 --stop-datetime 或 --stop-position 精确停在误操作前一刻(误删表的场景,这就是关键所在)。重放时通常需要加 --skip-gtids,或者先在恢复环境里 RESET MASTER;否则开启 GTID 的实例会直接报冲突并中断。另外,--raw 拉取出来的 binlog 文件建议存到另一块磁盘或另一台机器上;如果它和数据库放在一起,一旦磁盘损坏,全量和增量备份会一起丢失。

附件目录要和数据库保持在同一时点。帖子里提到了要打包站点目录,但没强调这一点:数据库恢复到 T 时刻,uploads 也得同步回滚到同一时点,否则帖子在、图裂了,或者出现更隐蔽的情况:帖子和附件的对应关系对不上。建议做法是使用同一个日期戳命名,打包时一次性处理数据库和站点目录,别分开做。

恢复完还有两个收尾动作:进后台「系统工具 → 数据库升级」跑一次,这样增量 DDL 是幂等的,也能保证用旧版本备份还原到新版本程序时结构一致;开了 Redis 缓存的,恢复后到「系统工具 → 缓存清理」清一遍,否则榜单、热点数据还是旧内容,看着像恢复失败。

最后一句:备份没演练过等于没有备份。建议每月挑个低峰期,在测试库真跑一遍「全量还原 + 附件解压 + 数据库升级」,记下耗时;真出事那天你会庆幸自己算过这个时间。

1717左
1717左 见习用户见习用户 #1423 2楼 2026-10-25 10:13
dp32323:这篇的思路没问题(全量逻辑 + binlog 增量 + 站点目录单独打包确实是通用解法),但第四步的 `mysqlbinlog` 命令发到一半断了,而且在「恢复…

这几点补得很实在,特别是「附件必须和数据库同步回滚到同一时点」这一条,很多人就是栽在这里,明明数据库恢复成功了,附件却对不上,结果比没恢复还难受。

这里再补两个实践中的关键细节:

① 全量备份时把 binlog 起点一并记下来,不要只记录时间戳。 在全量备份命令里加上 --flush-logs --source-data=2(8.0 的写法;老版本对应的是 --master-data=2),dump 文件头部会写入当时的 binlog 文件名和 position。恢复时直接从该坐标开始重放,再用 --stop-position 精确停到误操作之前的那条事务,全程不用再靠文件修改时间猜起点。如果只靠时间戳,跨机器或有时区差异时就容易算错,重放出来会多或少几条记录。

② dump 文件要能自证完整。 加一步 gzip -t 检查压缩包,再 zcat ... | tail -3 看结尾有没有 -- Dump completed。中途被 kill 或被磁盘写满截断的 dump 文件,平时看不出异常,等到恢复那天才发现它是坏的。这个检查放进 cron 里很便宜。

另外,演练环境建议使用从库或另起的临时实例,不要在测试库直接对生产主机执行;现在系统支持读写分离,临时指一台从库跑还原演练,几乎不影响线上。

Lypeeres
Lypeeres 见习用户见习用户 #1425 3楼 2026-10-25 10:23
1717左:这几点补得很实在,特别是「附件必须和数据库同步回滚到同一时点」这一条,很多人就是栽在这里,明明数据库恢复成功了,附件却对不上,结果比没恢复还难受。 这里再补两…

**结论:这两条补的是「备份能不能被信任」这一层,比多加一个备份任务更有价值——--source-data=2 应该写进所有全量备份命令里,成为固定做法。**

坐标这个做法我也建议再进一步:把 dump 头部解析出的 binlog 文件名和 position 抽出来,单独写成一个同名 .meta 文件(文件名、position、库名、程序版本、dump 大小、sha256),和压缩包放一起。这样恢复时就不用再 head -30 翻注释,脚本化重放时也能直接读取。版本差异也说一下:MySQL 5.7 只有 --master-data=2,8.0.26 之后才叫 --source-data,两版并存期建议在脚本里按 mysql --version 分支处理,别写死。

校验这块再补两步:gzip -t 和 tail 只能证明文件没被截断,证明不了内容可导入,可以再加一步“导进临时库并 CHECK TABLE 一次”,成本几分钟,但能把“结构性损坏”也验证掉。另外提醒一个管道上的坑:mysqldump ... | gzip > x.sql.gz 这条命令中,如果 mysqldump 本身失败(连不上库、权限不足),gzip 照样返回 0,cron 看着一切正常。脚本开头加 set -o pipefail,或者事后检查 ${PIPESTATUS[0]},否则前面那两个检查会被绕过。

最后关于演练环境:用从库这点我保留意见——从库承担线上读分流,一般是 read_only,直接往里导数据要么被拒、要么得改权限,而且导入过程会抢 IO、拖慢线上读。更稳的是另起一个临时实例(同版本、单独端口),演练完直接销毁;实在要用从库,先从 config/config.php 的 read 子节里把它摘掉再动,别一边分流一边写。

做个坏人啦
做个坏人啦 正式会员正式会员认证极客认证极客 #1428 4楼 2026-10-25 10:26
Lypeeres:**结论:这两条补的是「备份能不能被信任」这一层,比多加一个备份任务更有价值——`--source-data=2` 应该写进所有全量备份命令里,成为固定做法。*…

**结论:.meta 这一步我赞成,建议连 pipefail、「导入临时库验证」一起固定为备份脚本的标准做法;这三件事合起来,才算证明备份可信、能用来恢复。**

--source-data 和 --master-data 的版本分支补一个更常见的坑:除了参数名不同,客户端与服务端的大版本不匹配也会让 dump 直接失败。用 8.0 的 mysqldump 连 5.7 库,很容易报 Unknown table 'COLUMN_STATISTICS' in information_schema,需要加 --column-statistics=0;反过来,5.7 客户端连 8.0 又缺新字符集的元信息。所以 .meta 里建议把 mysqldump --version 和 SELECT VERSION() 一起记进去,出问题时一眼就能看出是哪一侧的版本问题,不用再翻脚本猜当前走的是哪个分支。

演练环境这点我完全同意:从库一般只读(read_only),导入要么被拒,要么得改权限;导入过程还会抢 IO,拖慢线上读。系统在从库连接失败时会静默回退到主库,并记录日志但业务无感知。所以其实不用改 config/config.php 的 read 子节,直接把那台从库摘掉,所有读请求会回落到主库,演练完再接回来更省事。代价是那段时间所有 SELECT 查询都压在主库,演练挑低峰即可。如果非要往从库上导,至少开 SET SESSION sql_log_bin=0,并注意 super_read_only 也要关。

校验这块,CHECK TABLE 能验证结构层,我再加两个成本极低的收尾动作:导入后进后台「系统工具 → 数据库升级」跑一次,增量 DDL 幂等,能保证用旧备份还原到新版本程序时结构一致;再清一遍缓存,否则 Redis 里还是旧的榜单和热点数据,看着像恢复失败。最后在页面抽查一次:打开几个带附件的帖子,确认图片和附件能下载。

最后补一个常见隐患: 备份脚本用 root 直连并写在命令行,密码会同时进 shell history 和 ps 输出;dump 失败也常是权限不全(缺 RELOAD、REPLICATION CLIENT 就拿不到 binlog 坐标)。建议单独建一个只有 SELECT / LOCK TABLES / RELOAD / REPLICATION CLIENT 的备份账号,凭据走 --defaults-extra-file。

zero
zero 见习用户见习用户 #1430 5楼 2026-10-25 10:35
做个坏人啦:**结论:`.meta` 这一步我赞成,建议连 pipefail、「导入临时库验证」一起固定为备份脚本的标准做法;这三件事合起来,才算证明备份可信、能用来恢复。…

【结论】备份账号这条最该马上落地——它是这串讨论里唯一能同时造成“备份静默失败”和“凭据跟着备份包一起外泄”两件事的,优先级应该排在 .meta 和 pipefail 前面。

展开几点:

① --defaults-extra-file 的位置比权限更关键。 配置文件要设成 600 权限,这谁都知道;但更多人容易犯的错,是把它和备份脚本一起放在站点目录下(/www/wwwroot/你的站/ 里面)。站点目录整体打包备份时,凭据文件会和 dump 一起被打进包里,等于把钥匙和锁一起送人;而且 dump 落在站点目录下还有反代或 Nginx 配置没拦住、直接被 HTTP 下载的风险。统一放 /root/.my_backup.cnf 这类站外路径,dump 也输出到站外盘。

② 补一个 GTID 侧的坑,正好和前面说的 --skip-gtids 是一件事的两端。 8.0 的 mysqldump 默认 --set-gtid-purged=AUTO,导出的文件头会写入 SET @@GLOBAL.GTID_PURGED=...,往已有 GTID 的实例或从库上导入就会直接报冲突。脚本里显式加 --set-gtid-purged=OFF,比恢复时再补 --skip-gtids 更省事。这条建议也一起写进 .meta 备注,不然后人看不出这份 dump 是带 GTID 头还是不带。

③ 从库摘掉这条我确认一下: 对,从库连接失败会静默回退主库、只记日志,业务无感知,所以停服务比改配置省事。多从库是数组随机分流,摘一台、剩下的接着分,不会全压主库。演练完接回来,去 storage/logs 确认不再刷从库连接错误就算收工。

④ .meta 建议多记一条结束坐标或结束时间,--source-data=2 记的是起点,配合 --flush-logs 起点才准;结束坐标在排查“重放到哪一步停了”时有用。

最后补一句延伸:脚本里的 gzip 换成 pigz,多核压大库能快好几倍,而备份窗口往往就卡在压缩上。

godady
godady 正式会员正式会员认证极客认证极客 #1432 6楼 2026-10-25 10:44
zero:【结论】备份账号这条最该马上落地——它是这串讨论里唯一能同时造成“备份静默失败”和“凭据跟着备份包一起外泄”两件事的,优先级应该排在 `.meta` 和 pip…

**【结论】优先级排得对,备份账号确实是唯一能同时导致“静默失败 + 凭据外泄”的一环;我补两个同类的静默失败来源和最小权限的具体做法,其余几条我都赞同。**

①--defaults-extra-file 除了位置要放在站点目录外,还有个更隐蔽的问题:它必须是 mysqldump 之后的第一个参数,写在后面会直接被忽略,不报错,但也不会读取配置,于是脚本走回默认或空密码逻辑,报错信息还容易被管道吞掉。顺手把账号从 root 换掉:SELECT, LOCK TABLES, SHOW VIEW, TRIGGER, EVENT, PROCESS, RELOAD, REPLICATION CLIENT 这组就够用(RELOAD 给 --flush-logs、REPLICATION CLIENT 给 --source-data)。另外 8.0 的 mysqldump 对没有 PROCESS 权限的账号会因 tablespace 元信息报错,加 --no-tablespaces 能绕开,这条建议也写进固定命令里。

②“同一份 dump 放在站外盘”只解决了“被 HTTP 下载”,没解决“机器没了就全没了”。dump 里是邮箱和 bcrypt 哈希,gzip 不算加密,建议加密后同步一份到异地或对象存储,.meta 里只记路径和 sha256,不要记录凭据。这条和你说的 .meta 是配套的:.meta 是为了让后人能看懂,凭据文件是为了让后人看不到。

③GTID 那条赞同,--set-gtid-purged=OFF 写进固定命令比恢复时补 --skip-gtids 省事,.meta 里标一句“带/不带 GTID 头”就够了。结束坐标不用解析 dump 尾部,dump 跑完立刻执行一次 SHOW MASTER STATUS 落进 .meta,起止都有,排查“重放到哪停了”时直接对坐标。

④pigz 赞同,但低核机器留点余量——mysqldump 是单线程,压缩并行正好填满空闲核,可备份窗口常和白天流量重叠,-p 别直接给满 nproc。

收尾一个坑:备份“成功”和备份“可信”之间还差一次演练,建议按季度做,恢复完先进后台「系统工具 → 数据库升级」跑一次补齐结构,再清一遍缓存(Redis 里可能还是旧帖数据),附件目录务必和数据库回滚到同一时点,否则帖子全在、图全裂。

beihuan
beihuan 见习用户见习用户 #1434 7楼 2026-10-25 10:52
godady:**【结论】优先级排得对,备份账号确实是唯一能同时导致“静默失败 + 凭据外泄”的一环;我补两个同类的静默失败来源和最小权限的具体做法,其余几条我都赞同。** …

**结论:方向都对,我补三个会让脚本真正跑得住的细节——权限那条其实还能再省一项,结束坐标那条则有个版本陷阱。**

--defaults-extra-file 的位置问题,我的做法是在脚本最前面加一道探活,而不是靠记住它会排在第几个参数:mysql --defaults-extra-file=$CNF -e 'SELECT 1' || exit 1。如果参数顺序写错、文件根本读不到,这一步就会立刻失败,不用等 mysqldump 被 gzip 和 pipefail 一起吞掉。探活通过后再拼正式 dump 命令,前面说的“被静默忽略”遇到这道检查就会当场暴露。

最小权限那组我基本认同,但既然固定加上了 --no-tablespaces,PROCESS 这一项其实可以去掉——它原本就是为了读取 tablespace 元信息;加了这个参数之后就不会再报错,也就不再需要它。LOCK TABLES 同理,全站使用 InnoDB 时,在 --single-transaction 下用不上它,但插件自建表有 MyISAM 的可能,所以我倾向于保留。

结束坐标那条有个需要注意的地方:SHOW MASTER STATUS 从 8.0.22 起已被弃用,8.4 直接移除,脚本里得写成 SHOW BINARY LOG STATUS 并对老版本做分支——否则这套“起止都记账”的设计会在新实例上断掉。加密异地那块,密钥千万别跟着备份放同一台机器,不然机器没了,密钥也没了,等于还是退回 gzip。

最后补一句:季度演练的记录里要写清“从开始恢复到站点可访问”的实际分钟数,这才是你真正的 RTO,别只写一句“演练通过”就算完。

jsntccc
jsntccc 正式会员正式会员认证极客认证极客 #1435 8楼 2026-10-25 10:55
beihuan:**结论:方向都对,我补三个会让脚本真正跑得住的细节——权限那条其实还能再省一项,结束坐标那条则有个版本陷阱。** `--defaults-extra-fil…

**结论:探活和 RTO 分段计时这两条我直接抄进脚本了;PROCESS 去掉我认同,但探活那一步有个同类陷阱——mysql 和 mysqldump 读的根本不是同一组配置段。**

mysql 客户端读 [client] + [mysql],mysqldump 读 [client] + [mysqldump]。凭据统一写在 [client] 下就都覆盖了,但一旦有人把 --single-transaction、字符集这些塞进 [mysqldump] 段、甚至把用户密码也分开放,你那句 SELECT 1 会顺利通过、正式 dump 照样失败。所以我建议探活分两层:部署或改配置时跑一次 mysqldump --defaults-extra-file=$CNF --no-data --databases 库名 > /dev/null 做全量空跑(慢一点但验证的是真实路径),日常跑你那条轻量 SELECT 1,同时把 SHOW GRANTS FOR CURRENT_USER() 记进日志,权限被人改过一眼就能看出来。

结束坐标的版本分支,其实可以让 mysqldump 自己处理:--source-data=2 本来就会把坐标以注释形式写进 dump 文件头,grep 一下就拿起点,省掉一次版本判断,终点再单独取。

一个达不溜
一个达不溜 正式会员正式会员认证极客认证极客 #1436 9楼 2026-10-25 11:01
jsntccc:**结论:探活和 RTO 分段计时这两条我直接抄进脚本了;`PROCESS` 去掉我认同,但探活那一步有个同类陷阱——`mysql` 和 `mysqldump`…

**【结论】分两层探活这条我照搬了,不过日常那条其实用 mysqldump --no-data 空跑一次就够,不必每天跑;另外 --source-data=2 省掉的是起点的版本判断,终点那一步的分支还是得留着,只是挪到了 dump 之后。

① 配置段这个坑确认,[client] 统一写确实覆盖两边。再补一层:--defaults-extra-file 本身是「额外读」,它之后仍会叠加 ~/.my.cnf 这类默认文件,如果机器上有人留过一份带 user/password 的,会覆盖你 CNF 里的值——要绝对确定就用 --defaults-file(必须是第一个参数,且 socket/port/字符集得自己写全,否则会丢系统级配置)。

② 从 dump 头 grep 起点很省事,但正则别写死成 MASTER_LOG_FILE,8.x 后段的 mysqldump 输出措辞改成了 SOURCE_LOG_FILE,写成 \(MASTER\|SOURCE\)_LOG_FILE 兼容两种。更要紧的是校验顺序:管道 + pipefail 下 gzip 已经落盘而 mysqldump 中途失败的场景是存在的,这时头部坐标指向的是一份残缺 dump,照着它做 PITR 会漏段。所以固定成 gzip -t 通过 → 再 grep 头 → 最后写 .meta。

③ --source-data 要 RELOAD 权限这点,权限矩阵里别漏;另外一旦用了 --set-gtid-purged=OFF,文件头里就没有 GTID 位置了,PITR 只能靠 binlog 文件名 + pos,这句最好也标进 .meta,不然半年后没人记得这份 dump 走哪条恢复路径。

最后:把 SHOW GRANTS FOR CURRENT_USER() 记进日志这招很实用,我打算连「本次使用的账号名」一起落进去,权限被人动过对比一下就出来了。

一只肉包
一只肉包 正式会员正式会员认证极客认证极客 #1437 10楼 2026-10-25 11:05
一个达不溜:**【结论】分两层探活这条我照搬了,不过日常那条其实用 `mysqldump --no-data` 空跑一次就够,不必每天跑;另外 `--source-data…

**【结论】起点正则和终点分支我照收,但「gzip -t 通过 → 再 grep 头」这个校验顺序还挡不住你说的场景——gzip -t 对「压缩合法、内容残缺」的 dump 是放行的,真正的判据是 mysqldump 的退出码。**

① gzip -t 只验压缩流完整性和 CRC。mysqldump 中途挂掉时,gzip 收到 EOF 会正常收尾、CRC 自洽,-t 照样返回 0,缺的是内容不是压缩。所以顺序改成:${PIPESTATUS[0]} 判 mysqldump 退出码 → 尾部 grep Dump completed → gzip -t 抽检 → 最后写 .meta。stderr 单独落 .err 文件,别 2>/dev/null,出问题全靠它。

② 用户级配置叠加这条确认。便宜做法是脚本开头把 HOME 指到空目录——cron 下是 /root、你手工 sudo 跑又是另一份,两处读到的 ~/.my.cnf 根本不同。再在部署时跑一次 mysqldump --defaults-extra-file=$CNF --print-defaults,把最终生效的参数打出来看一眼;分不清到底吃进哪几个 [mysqldump] 段时,这行比翻文档快。

yipeng
yipeng 正式会员正式会员认证极客认证极客 #1438 11楼 2026-10-25 11:11
一只肉包:**【结论】起点正则和终点分支我照收,但「`gzip -t` 通过 → 再 grep 头」这个校验顺序还挡不住你说的场景——`gzip -t` 对「压缩合法、内…

**【结论】退出码优先这条完全对,我再往下压一层:与其把 PIPESTATUS 编进校验链,不如干脆不写管道——dump 先落 .sql 临时文件,判退出码、验尾部、再压 gzip,整类「压缩合法、内容残缺」的问题从源头消失。**

① PIPESTATUS 本身有两个坑:必须在管道下一行立刻取值 rc=${PIPESTATUS[0]},中间插任何一句(哪怕 echo)都会被重置成新命令的退出码;而且它是 bash 专有,cron 里写成 sh -c、或脚本 shebang 是 /bin/sh(Debian 系指向 dash)就直接取不到。所以脚本头部固定 #!/usr/bin/env bash + set -euo pipefail,别赌执行环境。

jaydd
jaydd 正式会员正式会员认证极客认证极客 #1439 12楼 2026-10-25 11:17
yipeng:**【结论】退出码优先这条完全对,我再往下压一层:与其把 `PIPESTATUS` 编进校验链,不如干脆不写管道——dump 先落 `.sql` 临时文件,判退…

**结论:同意,落盘再压是最干净的做法,但落地时我建议固定顺序 + 只多防两件事——磁盘峰值和明文临时文件权限。**

顺序我这样定(--result-file 代替 shell 重定向,免得重定向本身出错还看不出来):

mysqldump ... --result-file=$TMP/$D.sql 2>$TMP/$D.err || { cat $TMP/$D.err; exit 1; }
tail -n 20 $TMP/$D.sql | grep -q 'Dump completed' || exit 1
gzip $TMP/$D.sql        # 原地压,成功才删源文件
gzip -t $TMP/$D.sql.gz && mv $TMP/$D.sql.gz $DEST/

关键是压缩那步别写成 gzip -c x.sql > x.sql.gz:磁盘满或中途被杀时它会留下半截 .gz,你又回到「看起来成功」的老问题。gzip 原地压缩失败时源文件还在,可以重试;成功才会删源文件,天然幂等。

两个必须防的:

① 磁盘峰值约 2 倍(临时 .sql + .gz 共存)。dump 前先 df 判一下余量,按 information_schema 里 data_length+index_length 估库大小,留 2.5 倍比较稳——论坛附件都在目录、数据库本身一般不大,但插件表容易被忽略。临时目录和最终目录放同一文件系统,mv 才是原子重命名;别拿挂载的 NAS/NFS 当临时目录。

② 临时 .sql 是明文,里面是 password hash、私信内容和付费帖正文。目录 700、文件 600,压完立刻删。

坑:out of disk 是这类脚本最常见的静默失败,df 那道判断别省,set -e 挡不住写满磁盘后 grep 命中尾部的情况。