给数据库备份"减负":从本地落盘到流式直传对象存储的踩坑之路

发布于 阅读 6未分类

给数据库备份"减负":从本地落盘到流式直传对象存储的踩坑之路

0. 引子:半夜的磁盘告警

这事儿得从一次半夜的硬盘空间告警说起。

监控在某台主机上疯狂弹"磁盘使用率 > 90%",等我 SSH 上去看的时候,已经 100% 了。业务还在跑,但 /var 下眼看着就要写爆。du 一排,罪魁一目了然:数据库备份的本地文件。

那一刻我意识到,一个"每天备份、备份完上传对象存储"看起来人畜无害的流程,在库变大的时候,会反过来咬运维一口。更何况,这次出事的虽然只是业务并不使用的从机,但备份事故绝不算小事——备份文件一旦损坏,影响的就不只是一份副本,还会直接导致主从复制失败,轻则复制中断告警,重则把隐患带进生产链路。所以从机上的备份翻车,同样要认真对待。


1. 起点:备份完,再上传

我们最初的设计很朴素,也是大多数人的第一版:

  1. xtrabackup(MySQL 5.7 对应 innobackupex)对实例做物理备份;
  2. 备份产物先落本地临时目录;
  3. 备份完成后,把产物打包/压缩,上传到一套 S3 接口兼容的对象存储(内部自建的,走 rclone / aws s3 那一套语义);
  4. 上传成功,本地清理。

目标本来是想得很清楚:备份文件的最终归宿是对象存储,本地只是"过路"。但"过路"这两个字,在库小的时候没问题,在库大到几百 GB 甚至上 TB 的时候,就成了灾难。


2. 第一次事故:本地盘被备份吃满

第一台机器告警之后,我查了下根因:

  • 备份是全量物理备份,产物大小和库本身基本一个量级;
  • 临时目录就挂在系统盘上;
  • 备份跑的时候,本地临时目录先被写满,还没来得及上传清理,磁盘就红了。

一句话总结:本地临时空间 < 一次全量备份的体积,于是必然满。


3. 第一次解法:跑前先查空间,超阈值直接退出 + 告警

治标先治标。我在脚本最前面加了一道空间闸门:每次真正开始备份之前,先做一次容量预判——把数据目录的实际体积,和临时目录的总容量比一比。如果数据目录的体积已经超过临时目录总容量的 80%,就判定"这次备份大概率塞不进临时目录",于是直接放弃本次备份并退出脚本,同时向告警平台的 webhook 发一条请求,把主机名和放弃原因带上去。

这个逻辑的本质是:宁可这次先不备份、让告警来催我处理,也绝不让备份把本地盘写爆、把业务拖死。这套东西上线后,确实再没出现过"备份把盘写爆还没人知道"的情况。


4. 第二次演进:80% 阈值被普遍突破,挂 NAS 顶上

问题来了:这套闸门是按"单台机器数据目录 vs 它自己的临时目录"算的。

更要命的是现状——现在很多数据库的数据目录容量已经超过了 1TB,而在不打算(或没法)给机器加本地硬盘的情况下,本地盘根本塞不下一次全量备份,也就没法在本地做备份了。结果过了一阵子,发现大部分主机的数据目录容量都超过了它们临时目录容量的 80%。也就是说,按我定的规则,这些机器全部都会触发"放弃备份"——等于备份集体停摆。这显然比磁盘告警更可怕。

治本的办法是让临时目录"足够大"。于是我们给备份机挂了一块 NAS 存储,专门当临时目录

  • 优点:容量管够,随便用,再大的库也能先落下来;
  • 缺点:慢。NAS 走网络,随机 I/O 比本地盘差一个数量级,xtrabackup 在 NAS 上跑备份明显更慢。

慢就慢吧,至少能跑,至少盘不会爆。那段时间,临时目录从本地盘切到了 NAS,世界暂时清净。


5. 第二次事故:备份集中 + 海量小文件,NAS 写崩、进程卡死

清净没持续多久。

我们的备份策略是集中在同一个时间窗跑的(都是为了避开业务高峰,结果全挤一块了)。xtrabackup 在备份时,除了最终产物,还会往临时目录同时写入大量中间文件——每个库的 xtrabackup 过程,少说也要产生几千个文件,尤其是表特别多的实例,.ibd、元数据、压缩临时块一堆一堆地往外冒。

在 NAS 这种"慢但大"的盘上,叠加集中爆发的并发写入,就出事了:

  • NAS 写入出现错误(ENOSPC / 网络抖动 / 文件系统层报错);
  • xtrabackup 卡在写临时文件的步骤,备份进程直接卡死,不报错也不退出;
  • 卡死的进程占着资源,下一份备份的闸门又把它拦住,连锁反应。

到这里我彻底明白:只要备份还"先落本地(哪怕是 NAS)再上传",临时目录就是一个永远的包袱——它要么不够大(本地盘),要么够大但慢且脆(NAS),而且表越多越容易在临时目录上翻车。


6. 终极解法:备份过程直接上传,不落本地

结论很直接:备份流一边产生、一边传,本地不要留文件。 也就是——流式备份(streaming backup)

xtrabackup 本身支持 --stream=xbstream,把备份输出成而不是目录。但"流"出来之后得有人接住、传走。查了一圈,这事儿得靠一个第三方工具:rclone

rclone 这位老朋友了。我之前写非结构化数据备份脚本的时候用的就是它——S3 / 对象存储的通用搬运工,配置一个 remote 就能 rcat 边读边传。

6.1 核心思路

做法的核心是"边产生边上传":让 innobackupex 以流(xbstream)模式把备份输出成连续的字节流,再由 rclone 的 rcat 子命令从这个流里读取、按分块边读边上传到对象存储。整条链路里,本地磁盘只承载数据库本身,临时目录几乎不再被占用。

当然要把话说准:流式不等于本地零占用。即便走 xbstream,xtrabackup 仍会把排序缓冲、并行线程的中间文件写进 --tmpdir,表特别多、--parallel 开得高时这部分占用依旧可观。所以"边产生边上传"真正省掉的是"整份备份副本"那份大头,而 --tmpdir 该预留的空间一点不能省——只是它不再需要等于整个库的大小。

其中 rclone 侧要事先配好一个指向内部对象存储的 S3 兼容 remote(我们起名叫 backup);xbstream 流模式保证 innobackupex 吐出的是连续字节流而不是一堆目录文件;rcat 则负责把这个流按设定的分块大小和上传并发数,流式写进对象存储的对应路径。

6.2 我们实际是怎么做的

我们的做法不是把"生成备份"和"传到对象存储"两步硬绑成一条命令,而是让它们拆成两段独立的过程,中间用一个会实时统计已传字节的环节把两者接起来。这样备份一边产生、对象存储那边一边接收,本地全程几乎不落盘。整条链路大致是这样的:

xbstream 流模式 innobackupex

把两个工具拆开、用代码桥接而不是一条管道,主要为了两件事:

  • 进度可见:那个统计环节会实时累加已传输的字节数,外层每隔几秒就能把"已上传多少"推到前端界面,备份进度不再是黑盒,而是一片真实的进度。
  • 可控可观测:两个子进程都纳入同一个进程组,主流程被信号终止时会一并清理,避免 innobackupex 或 rclone 变成孤儿进程挂在机器上。

rclone 的几个关键参数来自一份独立配置:分块大小默认 64M,上传并发数默认 8。这两个值直接决定流式上传的吞吐——分块越大、并发越高,带宽吃得越满,但也要和对象存储端的限制、本地内存一起权衡。

6.3 两个必须踩过才记得住的坑

代码注释里记了两个真实现场,不写等于白踩:

  • rclone 的 rcat 必须主动关闭输入才会正常退出。流传输完之后如果不关闭它的标准输入,rclone 会一直等 EOF 不结束,备份主流程就卡在收尾步骤。
  • 警惕"假成功":rcat 返回成功,并不代表对象存储上真的有完整文件。我们最后加了一道完整性校验——用 rclone size 比对对象存储上的字节数和本地的计数,对不上就重试(最多 3 次,退避 10s / 30s / 60s),重试仍失败才判定为失败并清理那个半截文件。否则就会出现"显示成功、对象存储里却是空的"这种幻觉。

7. 收尾:四套方案的演进对比

阶段方案本地占用优点
初期本地备份 + 上传对象存储全量(系统盘)简单磁盘被吃满
空间闸门跑前查:数据目录 > 临时目录 80% 就放弃 + webhook防满盘普遍超阈值,备份停摆
NAS 临时目录挂大容量 NAS 当临时盘全量(NAS)容量足,随便用慢;集中写 + 多表 → 写错卡死
流式直传xtrabackup 流式输出 + rclone 直传大幅下降(tmpdir 仍需预留)不占整份副本、不依赖大临时盘实现复杂度高,要处理假成功/卡死

四套方案的演进对比

一点感悟

备份这种活儿,最容易被轻视,因为它"平时不出声"。但恰恰是这种后台流程,在规模上来之后会第一个反噬运维:

  • "临时目录"不是免费的——它要么不够大,要么够大但慢且脆;
  • "先落本地再上传"的模型,先天带着一份副本的包袱,库越大包袱越重;
  • 真正省心的解法,往往是让数据在产生的那一刻就流向终点,而不是先找个地方堆着。

rclone 这位老朋友,从非结构化数据备份一路陪到数据库流式备份,证明确实是"通用搬运工"里最顺手的一把。

评论 (0)
0 / 500
暂无评论,快来抢沙发~