ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从双机容错到数据库备份:企业恢复链设计与演练

从双机容错到数据库备份:企业恢复链设计与演练

简介:企业数据备份方案文档面向企业网管、IT运维与信息化建设负责人,聚焦业务网络长期稳定运行中的备份与恢复问题,系统梳理网络备份含义、双机容错、数据库备份、网络故障与灾难恢复、备份任务管理等核心环节。内容结合局域网向广域网扩展后易受攻击、硬件故障和操作差异影响的实际背景,讨论在线容错式备份与后备式备份的配合方式,并提及Standby Server、LifeKeeper、NTCluster、Octopus等实时容错方案,便于理解高可用架构的选型与应急恢复思路。压缩包内含1个doc文件,约240KB,篇幅紧凑,可作为方案撰写、内部培训或备份体系自查的参考底稿。已有313人学习,读者可从中获取服务器备份分类方法、数据库与系统文件恢复要点以及备份任务自动化管理的落地框架。

1. 备份的价值不在“备”,在“恢”

企业数据备份方案这六个字,很多人第一反应是“定期把数据拷一份”。某企业机房做过一次演练:备份任务的日志连续三年是绿色的,磁带规规矩矩每周换一盘,真到存储阵列全挂那天,运维把最近一盘磁带塞回去,系统盘起来了,业务服务起不来——目录服务数据库没备,系统配置是旧的,文件权限全乱。数据都在,环境没了。

这就是值得把机制拆开看的原因:备份系统交付的不是一份副本,而是一条能走通的恢复路径,拷贝只是这条路径上的一环。路径上任何一环缺失,副本就是一堆躺着的字节。后面几章拆的正是这条路径上的四个节点:接管、链长、一致性、可验证。

2. 双机容错:主服务器倒下时,客户端为什么只“停顿”一下

容错方案的产品说明里常见一句话叫“对客户端无感知”。这句话在工程上并不成立,准确说法是:客户端会停顿几秒到几十秒,停顿多久取决于三个独立动作配合得怎么样——故障判定、名称接管、连接重建。这三件事任何一件卡住,用户看到的就是“卡死”而不是“停顿”。

先把这套机制的骨架说清楚。文件服务器上跑着目录服务、文件共享、数据库实例,主机对外提供服务的身份是两个东西:一个 IP 地址和一个主机名。备机在待命状态下不挂载业务卷、不启动业务服务,只做一件事——持续确认主机还活着。一旦判定主机死亡,备机接手 IP、接手名字、启动服务,客户端重连时解析到的还是同一个名字,所以配置不用改。这是设计上最漂亮的一点:所有切换动作都藏在服务端,客户端对此毫不知情。

2.1 心跳与故障判定:为什么“三次超时”是个危险参数

主备之间维持一条独立链路传心跳,常见是交叉网线或者串口直连,不走业务交换机,避免业务网络拥塞把心跳挤掉。心跳间隔通常取 0.5 到 1 秒,连续丢失 N 个包判定主机故障。这套参数看起来简单,实际上是一道两难:N 取小了,交换机抖一下、内核调度卡一下,备机就误判;N 取大了,真故障时切换要等十几秒,RTO 直接超标。

比误判更麻烦的是脑裂。主机其实没死,只是心跳链路断了,备机接管了名字和 IP 开始对外服务;过一会儿主机网络恢复,它以为自己还是主节点,继续接业务。两台机器用着同一个名字往两个方向写数据,分叉就此产生,事后合并的代价往往比停机大得多。所以成熟方案在接管前一定会做一件事,叫隔离(fence):确认主机对共享存储的访问已被切断,或者干脆把主机下电。隔离失败就宁可不切换,让服务停着,也不让两边同时写。

# 心跳判定与隔离的核心状态机,参数决定误切率 HEARTBEAT_INTERVAL = 1.0 # 秒,主备心跳发包间隔 FAIL_THRESHOLD = 3 # 连续丢失多少个包进入"疑似故障" GRACE_AFTER_FAIL = 2.0 # 判定疑似后继续观察的宽限时间,等迟到的心跳 def watch_dog(last_seen, now, state): elapsed = now - last_seen if elapsed < HEARTBEAT_INTERVAL * FAIL_THRESHOLD: return state # 容忍窗口内,什么都不做 # 进入疑似故障,此时绝不能直接接管 if elapsed < HEARTBEAT_INTERVAL * FAIL_THRESHOLD + GRACE_AFTER_FAIL: return "SUSPECT" # 先探测主机的管理口、存储链路 # 接管的前置条件:隔离必须成功 if not fence_primary(): # 切断主机存储访问或强制下电 return "FENCE_FAILED" # 隔离失败,宁可停着也不切换 return "TAKEOVER"

这段状态机里最关键的不是超时数字,而是FENCE_FAILED这个分支。它表达的取舍是“宁停不分叉”。实际部署时,隔离手段通常是存储侧的端口下线、SCSI 持久预留锁、或者带外管理口强制关机,三者至少要有一种,纯靠软件互相喊话的方案在极端场景下并不可靠。

2.2 名称接管:客户端不用改配置的底层

备机接管要完成三件事,缺一不可。

第一件是抢 IP。备机在同一个二层网络里发免费 ARP 宣告,告诉网关和交换机“这个地址现在在我这”,交换机的 MAC 地址表随之刷新。这一步在网络正常时是毫秒级的,但如果客户端和服务器跨了三层设备,中间设备的 ARP 缓存没刷新,流量还是会往旧机器打。

第二件是改名字。主机名在目录服务数据库里有记录,集群软件接管时会修改对应条目,把主机名指向备机。如果企业内部还跑着独立的域名解析,还得同步更新记录,并把 TTL 调到 60 秒以内,否则客户端会长时间解析到已经下线的地址。

第三件是起服务。这一步在真实故障里失败率最高:数据库实例启动时发现数据文件被残留锁文件占着,或者日志目录权限在备机上对不上,服务起不来,但 IP 已经切过去了,监控显示“切换成功”,业务实际全断。

至于客户端感受到的“短暂停顿”,来源也不在服务端,而在 TCP 连接本身。主机断电时不会给对端发 RST,客户端的内核还认为连接处于 ESTABLISHED,往一个不存在的对端发数据,走重传退避。默认的tcp_retries2是 15 次,按退避曲线算下来要十三到十五分钟才报错。用户看到的“卡死”,八成是这个参数在起作用。解决办法在应用侧:连接串设 connect timeout,开启 TCP keepalive 并把探测间隔压到几十秒,连接池做失效连接剔除。这三条不做,再好的容错软件也救不回体感。

2.3 两条硬件路线:镜像双写与共享盘预留

双机容错的底层存储有两条完全不同的实现路线,选错会直接影响数据库的提交延迟。

路线一是纯软件镜像。备机用自己的本地盘,主机的每一次写操作都通过镜像链路同步到备机,备机确认后才算写完。好处是不需要共享盘柜,备机可以用退役的旧设备,投资省。代价是写放大:数据库一次事务提交本来只等本地盘的 fsync,现在要等一次跨机架的网络往返加远端落盘,提交延迟直接翻倍,对写入密集的业务影响很明显。另一层风险在镜像链路本身,链路一断,镜像关系失效,等链路恢复要做全量重同步,重同步期间备机数据是不可信的。

路线二是共享磁盘阵列加集群软件。两台机器都连同一个盘柜,但同一时刻只有一个节点能挂载文件系统,靠 SCSI 持久预留锁保证互斥。备机平时不挂载,切换时去抢那把锁,抢到了才挂载、才起服务。好处是没有同步开销,数据只有一份,不存在分叉。代价是盘柜本身成了新的单点,必须配双控制器、双电源、RAID,而这部分成本往往比两台服务器高得多。

两条路线对数据新鲜度的影响也不一样。同步复制下,备机的数据和主机完全一致,切换后不丢事务;异步复制下,主机突然断电时,最后若干个还没传到备机的事务就丢了,切换后要靠数据库自身的日志做一致性检查。这个尾差能不能接受,取决于业务对 RPO 的要求,而不是取决于软件宣传的“实时”。

3. 备份策略:全量、增量、差异不是风格问题,是恢复链长度问题

备份策略的选择经常被简化成一句“增量省空间”。这句话只说了成本的一半,另一半是恢复时要走多长的链。三个概念本身不难,难的是把“备份窗口”和“恢复时间”这两笔账一起算。

全量备份把选定的数据集整体复制一遍,恢复时只需要一盘磁带,链长为一。 增量备份只复制“上次备份之后新增或变化”的部分,恢复时必须先还原最近一次全量,再按时间顺序把中间每一次增量依次重放,链长随天数线性增长。 差异备份复制“上次全量之后所有变化”的部分,恢复时是一盘全量加一盘差异,链长恒定为一加一。

3.1 恢复链越长,失败面越大

链长不只是时间长,更是失败概率的乘积。假设每一盘磁带在恢复时读取成功的概率是 99%,这已经是很乐观的数字。链长为一时,恢复成功率是 99%;链长为七时,成功率掉到 93%;链长到十四,只剩 87%。磁带越多,其中某一盘老化、受潮、磁场干扰的概率就越高,而增量链是串联结构,中间断一环,后面的全部作废。

差异备份的设计动机就在这里。它用“每次备份都比上次大一点”换取“恢复链恒定”,代价是备份窗口随时间递增:周一备一天的变化,周二备两天,周日备六天。企业里常见的一种折中是“每周一次全量加每日差异,月末再做一次全量归档”,既控制了链长,也控制了最大窗口。

下表是同一种数据在三种策略下,恢复“周三下班时”的状态分别需要哪些介质:

策略周一周二周三恢复周三状态所需介质链长
全量全量全量全量周三那盘1
增量全量增量增量周一全量 + 周二增量 + 周三增量3
差异全量差异差异周一全量 + 周三差异2

3.2 窗口预算:先把三个速度算清楚再选策略

备份窗口跑不完,绝大多数不是软件慢,是没算过账。一条备份链路上有三个速度,实际吞吐取三者最小值:源端读取速度(磁盘阵列的随机读能力)、网络带宽、写入端速度(磁带机或备份存储的持续写)。很多机房是磁盘阵列能给到 300MB/s,万兆网络能跑 800MB/s,磁带机标称 120MB/s 实际只有 60MB/s,那整条链就被磁带机卡住,扩带宽毫无意义。

下面这段代码是把窗口算清楚的脚手架,参数换成自己环境的实测值即可:

# 备份窗口与恢复链的估算:先算清楚,再决定策略 FULL_HOURS = 6.0 # 一次全量实测耗时(小时) DAILY_DELTA = 0.5 # 一天变化量对应的增量耗时(小时) DAYS = 7 # 一个备份周期的天数 def window_incremental(days=DAYS): """每周一次全量 + 其余每天一次增量""" return FULL_HOURS + DAILY_DELTA * (days - 1) def window_differential(days=DAYS): """每周一次全量 + 其余每天一次差异,差异耗时随天数线性增长""" return FULL_HOURS + sum(DAILY_DELTA * k for k in range(1, days)) def restore_chain(strategy, day_index): """恢复到第 day_index 天所需的介质份数""" if strategy == "full": return 1 if strategy == "incremental": return day_index + 1 # 全量 + 期间每一次增量 return 2 # 全量 + 当天差异 print(window_incremental()) # 9.0 小时 print(window_differential()) # 16.5 小时,但链长恒为 2

算出来的结论很直白:增量把窗口压到最短,代价是恢复那天要按顺序重放整条链,且这条链上任何一盘介质出问题就全废;差异的窗口随周期增长,但恢复路径永远只有两步。核心业务通常选“全量加日志”走另一条路(下一章展开),次核心选“全量加差异”,海量非结构化数据才用“全量加增量”,因为它对恢复时间的要求本来就不高。

3.3 打开的文件与跨平台元数据:最容易翻车的两处

文件在被进程占用时,备份软件读取会失败,或者读到一半内容被改掉。针对这种情况有两类做法:一是卷快照,在文件系统层瞬间建立一个写时复制快照,备份从快照读,源卷继续正常读写;二是备份代理,把读取请求挂进文件系统或数据库的读接口,让系统自己保证一致性。快照这条路的坑在于快照空间是预分配的,备份跑得太久、源端写入太猛,快照空间被写满后系统会自动丢弃快照,备份任务读到一半源就没了,日志里只留一句语焉不详的读取错误。

跨平台是另一个隐蔽的坑。异构环境里同时跑着目录服务型网络操作系统、企业级服务器操作系统和类 Unix 工作站,三套系统的权限模型完全不同:有的用 ACL 加属主,有的用目录服务里的对象权限,有的用 POSIX 的属主属组加粘滞位。备份软件如果只存文件内容不存元数据,恢复出来的目录权限全是默认值,业务账号访问不了自己的文件,排查起来极其费时。所以选型时必须确认它能不能完整保留 ACL、属主、稀疏文件、硬链接和扩展属性,这一条比压缩率重要得多。

4. 数据库备份:为什么直接拷贝数据库文件是一场赌博

文件服务器可以靠快照备份,数据库不行,因为数据库文件在被持续修改,而操作系统拷贝文件不是原子操作。这一章拆的是“从数据库里把数据安全取出来”这件事的底层约束。

4.1 页撕裂与日志不同步:拷文件不可靠的两个根因

数据库的数据文件由固定大小的页组成,常见是 8KB 一页。操作系统读文件按块走,一次读 4KB 或 64KB,并不关心页的边界。假设数据库正在写某一页,前 4KB 已经落盘、后 4KB 还在内存里,此时备份任务正好把这个页读走,拿到的就是半个新页加半个旧页。这个页在恢复时校验和对不上,数据库启动直接报页损坏。这个现象叫页撕裂,它是“在线拷文件”这种做法的第一个死穴。

第二个死穴是日志与数据文件的时间点不一致。数据文件被拷走的时候是 T1 时刻的,而事务日志可能已经推进到 T2,恢复时拿 T1 的文件去重放 T2 的日志,页号和序列号对不上,重放直接中断。更隐蔽的是内存里的脏页:已提交但还没落盘的事务只存在于缓冲池中,拷文件的时候根本没有被带走,恢复后这些事务凭空消失,业务方却发现账目对不上。

所以“停库拷贝”是安全的,因为库停了之后文件不再变化,所有脏页都已落盘;但停库意味着业务中断,除了极小的系统,没人愿意接受。

4.2 在线备份的三个动作:检查点、冻结、日志区间

真正安全的在线备份,机制上都绕不开三步:先打检查点把内存脏页刷盘并记录当前的日志序列号作为起点;再把数据文件在某个瞬间固定住,要么冻结写入,要么切到存储快照;拷完之后解冻,并把备份期间产生的日志一并归档。第三步最容易被忽略,却是决定恢复精度的关键。

# 数据库在线备份的典型时序,重点是日志区间不能漏 def online_backup(db, dest): lsn_begin = db.checkpoint() # 1. 刷脏页,拿到备份起点序列号 db.freeze_io() # 2. 冻结写入,或在此刻建存储快照 try: copy_datafiles(db.datafiles, dest) # 此时数据文件是静止的,不会有页撕裂 finally: db.unfreeze_io() # 3. 解冻,业务写入恢复 lsn_end = db.current_lsn() archive_logs(lsn_begin, lsn_end, dest) # 4. 备份期间的日志必须一起带走 write_manifest(dest, lsn_begin, lsn_end) def restore(dest): restore_datafiles(dest) # 还原到 lsn_begin 的一致状态 replay_logs(dest.lsn_begin, dest.lsn_end) # 重放到 lsn_end crash_recovery(dest) # 回滚未提交事务,前滚已提交事务

这段时序的每一行都对应一个失败模式。少了checkpoint,拷走的数据文件里缺已提交事务;少了freeze_io,出现页撕裂;少了archive_logs,数据只能恢复到冻结那一刻,RPO 直接等于整个备份时长。这也是为什么核心业务会采用“定期全量物理备份加持续日志归档”的组合:全量负责给一个一致的起点,日志负责把这个起点推进到故障前最后一秒,时间点恢复能力就是这样拼出来的。

另一条路是逻辑导出,把表导成文本或 SQL 脚本。它的优势是能跨版本、能只恢复某一张表、能人工审阅;劣势是大库导出极慢,一致性靠一个长事务快照维持,导出期间日志不能截断,磁盘空间会被撑爆。

4.3 系统状态:那些不占空间却决定能否开机的数据

回到开头那个演练场景。数据全都恢复了,服务起不来,缺的就是系统状态数据。

这类数据包括目录服务数据库里的用户、组、权限和登录脚本,系统配置库里的服务启动项、驱动加载顺序和网络参数,还有共享定义、安全策略、证书和密钥。它们在磁盘上占的空间往往不到一个 GB,但对恢复是决定性的:缺了用户对象,数据库实例的服务账户不存在,实例起不来;缺了驱动加载顺序,恢复出来的机器蓝屏;缺了证书,对外服务全部握手失败。

常见的做法是每次全量备份都带上系统状态,并单独保留最近若干份,不随数据备份一起轮转。恢复顺序必须固定:装操作系统、还原系统状态、还原数据库、启动服务并验证。顺序颠倒的典型后果是账户标识对不上,服务以错误身份启动,日志里报的是权限错误,很容易被误判成数据损坏。

5. 常见问题与排查:四类让方案在恢复日失效的坑

备份系统的特点是平时看不出问题,出问题的时候已经没有第二次机会。下面四条是从实际故障里归纳出来的,每一条都按现象、原因、处理来写。

5.1 切换显示成功,业务连不上

现象:监控告警显示备机已接管虚拟地址,从运维机 ping 得通,但业务系统报连接超时或者连接被拒绝,重启应用也没用。

原因通常有三层。最外层是名字没接管:应用连的是主机名而不是地址,客户端的解析缓存里还是旧记录,TTL 设成了几小时,流量继续往已经下线的机器打。中间层是连接池:客户端内核还认为旧连接是 ESTABLISHED,数据发出去等重传退避,要十几分钟才报错,这期间应用线程全部阻塞在连接池上。最内层是服务没起:集群软件把虚拟地址当成资源接管了,但数据库实例启动失败,原因可能是备机上残留着锁文件,或者归档目录的属主不对。

处理办法是把“端口探测”做成接管流程的一部分。虚拟地址切换完成后,必须连一次业务端口、跑一条最简单的查询,通过才算切换完成,不通过就触发回滚或者升级告警。同时把应用侧的 connect timeout 压到秒级,开启 keepalive,解析记录的 TTL 调到一分钟以内。这三条都属于应用和网络配置,光靠容错软件解决不了。

5.2 恢复时才发现增量链断了一环

现象:要恢复到上周三的状态,还原了周一全量和周二增量,到周三那盘时报“找不到基准”或“序列号不连续”,整条链作废,只能退回到周一的状态,丢掉两天数据。

原因多半出在保留策略上。很多方案按“保留最近 N 份”来回收介质,第 N+1 份生成时覆盖最早那份——而最早那份恰好是这条链的全量基准。另一种情况是中间某天的任务失败了,但失败通知只发到一封没人看的邮件里,任务在第二天照常跑增量,链中间就出现了一个空洞。增量备份的链是串联结构,它不会因为中间少一环而自动降级,只会静默地变成一条废链。

处理办法是把保留单位从“份数”改成“链”:整条链过期之前,全量基准不允许回收。每次备份完成后做一次链完整性校验,确认上一份介质的序列号和本次能接上。监控不要看任务状态,要看“最近一个可恢复时间点距今多久”,这个指标才能真正反映保护能力。

5.3 备份窗口永远跑不完

现象:任务晚上十点启动,第二天上午九点还没结束,上班后磁盘 IO 被打满,业务系统跟着变慢,运维只能手动杀掉任务,第二天重复同样的过程。

原因通常是三个叠加。一是全量放在了工作日,六小时的全量跟业务高峰重叠;二是压缩和去重没开,源端读出来多少就写多少,磁带机的实际持续写入速度只有标称的一半;三是多路复用开得太大,把十几个客户端的数据交织写进同一条带,写入确实变快了,但恢复时没法顺序读,要在磁带上来回定位,恢复时间反而翻几倍。

处理办法是先测三个速度:源端随机读、网络有效带宽、写入端持续写,取最小值作为规划依据,据此排窗口。全量放周末,差异放工作日,增量按小时切。多路复用只对“大量小文件、单文件很小”的场景开启,数据库这种大块连续 IO 不要开。

5.4 日志全是成功,恢复时校验失败

现象:备份报告的成功率长期是百分之百,真做恢复演练时第一盘就报校验错误,或者读到一半卡住不动。

原因是校验只做了写入侧。带写后即读通道的驱动器会在写完的一瞬间回读比对,这一步能挡住磁头故障和明显的介质缺陷,但挡不住介质随时间劣化。磁带存放在温湿度不达标、附近有强磁场的机柜里,几个月后磁记录强度衰减,当时能读的数据就读不出来了。磁头脏了也是同理,写的时候信号弱,当时勉强能读,放一段时间就彻底失败。

处理办法是把“抽样校验”升级成“抽样恢复”。每月挑一盘介质,在隔离环境里完整读一遍并做校验和比对,而不是只看备份软件的报告。介质按批次轮换退役,同一盘不要用满五年。存放环境控制温湿度,远离配电柜和音箱这类强磁源。这一步做完,才算真正知道自己的备份是活的。

6. 分级与演练:备份方案的成本到底花在哪一刀上

备份方案的本质是一笔成本与恢复时间的交易。把所有业务按同一个标准保护,结果只有两种:要么预算高到没人批,要么关键业务被一堆无关数据拖累,真出事时抢不回来。

合理的做法是先把业务分级,再给每一级配不同的手段。核心交易类业务要求分钟级恢复、接近零的数据丢失,只能靠双机热备加持续日志归档,这条路的钱花在共享存储和集群软件上;重要业务能容忍几小时中断,用每日全量加高频日志备份就够,钱花在备份软件和介质上;一般业务和归档数据对恢复时间不敏感,用差异备份加离线磁带,钱花在流程和人工上。分级定完,预算自然就落到了该落的地方。

业务等级目标恢复时间目标数据丢失主要手段
核心交易5 分钟以内接近 0双机热备 + 日志实时归档
重要业务4 小时以内15 分钟以内每日全量 + 高频日志备份
一般业务24 小时以内24 小时每周全量 + 每日差异
归档数据3 天以内不要求离线介质,异地存放

真正决定这套方案能不能用的,不是上面这张表,而是一次从裸机开始的恢复演练。演练的做法很朴素:找一台空机器,断掉它和生产网络的一切连接,只给一盘介质、一张恢复引导盘和一份操作手册,让一个不熟悉这套系统的人按手册操作,全程计时。演练中暴露的问题几乎永远不在数据本身,而在引导盘过期、驱动缺失、手册里的步骤顺序和实际版本对不上、恢复后忘了改回网络配置导致地址冲突。

我自己的习惯是每季度做一次,而且故意不让当初搭建这套系统的人上手,让另一组人按文档走。第一次这么干的时候,恢复卡在了引导盘上——那张盘是两年前做的,里面的存储控制器驱动早就跟新换的硬件不匹配,手册里写着“按提示操作”,实际屏幕上是一片找不到设备的提示符。从那以后我每次交付备份方案,都会把“引导介质随硬件变更同步重建”和“每季度裸机恢复演练”写进验收清单,演练不通过就不算交付。备份这件事没有后悔药,能提前吃的只有演练这一颗。

希望这套拆解帮你在下一次设计或者接手备份方案时,先问一句“这条恢复路径走通了吗”,而不是先看备份日志有多绿。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进