如何兼顾性能与可靠性?一文解析 YashanDB 主备高可用技术
![](https://static001.geekbang.org/infoq/6f/6f878d0e93a0b600c97944ea2e2110cc.png)
数据库高可用(High Availability,HA)是指在系统遇到故障或异常情况时,能够自动快速地恢复并保持服务可用性的能力。如果数据库只有一个实例,该实例所在的服务器一旦发生故障,那就很难在短时间内恢复服务。长时间的服务中断会造成很大的损失,因此数据库高可用一般通过多实例副本冗余实现,如果一个实例发生故障,则可以将业务转移到另一个实例,快速恢复服务。
常见的高可用架构有两种,一种是主备架构,另一个是多活架构。
主备架构中,有一个实例是主库,其他实例是备库。主库发送日志给备库,备库通过回放日志来同步主库的数据,一旦主库发生故障,可以选择其中一个备库升主,接管业务。主备架构根据日志类型分两种,物理主备和逻辑主备。
物理主备:传输物理日志,备库只读,不可执行写业务,比如 Oracle Dataguard;
逻辑主备:传输逻辑日志,备库可以执行写业务,比如 MySQL。多活架构中,每个实例都可以执行读写业务,一个实例宕机后,业务可以转移到其他实例。
多活架构有基于共享存储的共享集群,比如 Oracle RAC,也有基于逻辑日志的多活架构,比如 MySQL 的双主多活。
YashanDB 目前支持物理主备和共享集群两种高可用方案,本文将聚焦于物理主备的高可用介绍。
![](https://static001.geekbang.org/infoq/ff/fff7ed33a683755bc75963d686a9c32a.png)
图 1 两种常见的高可用架构
主备高可用挑战
主备高可用方案的挑战通常有以下几点:
性能开销:主备高可用方案会对主数据库的性能产生一定的开销,包括增加网络传输、归档 IO 等资源消耗等。在部署主备环境时,需要权衡可用性和性能之间的平衡,还需要考虑部署成本。
2.同步延迟:主备数据库之间的数据同步存在一定的延迟,主要由于网络延迟和日志应用的处理时间所导致。延迟会影响备库只读性能,在主库发生故障时,备库中的数据也可能不是最新的。
3.故障切换:在主备高可用环境中,当主库发生故障时,需要手动或自动进行故障切换,将备库切换为主库。故障切换过程中,需要尽量减少服务中断时间,还要确保数据的完整性和一致性。
4.数据一致性:主备同步至少要保证最终一致性,如果备库需要支持只读,还要保证备库查询的瞬时一致性。
YashanDB 主备针对上述挑战,做了深度优化,使得主备同步性能和自动切换能力达到极致。下面将介绍 YashanDB 高性能主备复制,以及自动故障转移的技术解析。
YashanDB 高性能主备复制
整体架构
![](https://static001.geekbang.org/infoq/af/af1db33802999e8d6035346bbef24f6d.png)
图 2 YashanDB 物理主备架构图
YashanDB 的物理主备架构如上图所示,左边是主库的组件,右边是备库的组件。
各组件解释如下:
1.Log Cache:日志缓存。主库上 Redo 日志会首先写入日志缓存,备库收到 Redo 日志后也会写入日志缓存。
2.logFlush:日志刷盘线程。将日志缓存里的 Redo 日志持久化到磁盘。
3.logSend:日志发送线程。依次从日志缓存,Redo 文件,归档日志里读取 Redo 日志,发送给备库。
4.logRecv:日志接收线程。接收来自主库的 Redo 日志,写入日志缓存。
5.archProc:归档线程。在 Redo 文件被复用前,将其拷贝为归档日志文件。
6.stbyRcy:备库日志回放线程。依次从日志缓存,Redo 文件,归档日志里读取 Redo 日志,回放 Redo 日志,更新备库数据。
7.archSvr:归档发送线程。当备库请求归档日志时,主库将归档日志发送到对端。
8.archCli:归档接收线程。当备库出现归档 GAP 时,系那个主库发起请求,并接收来自主库的归档日志文件。
主备部署
YashanDB 支持一主多备部署,最多可达 32 个备库。备库可以在线添加,并且一次可以并行添加多个备库。在并行创建备库的过程中,主库只从磁盘读取一次数据,然后将数据转发给多个备库。
YashanDB 还支持级联备库,即备库可以从另一个备库接收 Redo 日志,无需和主库建立连接。级联备可以减少主库的压力,可用于异地容灾。YashanDB 的每个备库可以连接多个级联备库。
![](https://static001.geekbang.org/infoq/5b/5b0792b2834569dfecf304ad4d8480a2.png)
图 3 级联备库
保护模式
YashanDB 主备对数据的保护是基于事务级别的,一个事务提交前,如果这个事务的日志已经在备库落盘,那么该事务就不会在主库故障后丢失。
为方便用户对数据的保护程度和可用性进行权衡配置,YashanDB 提供 3 种不同的保护模式:
1.最大性能:主机事务提交无需等待备机收到 Redo,备机对主机性能影响最小。如果主机故障,备机升主后可能丢失数据。
2.最大保护:主机事务提交前,至少有一个备机将日志刷盘。这种模式能保证数据不丢失,但是对主机性能有影响,并且所有备机都断连后,主机事务将阻塞。
3.最大可用:在正常情况下,最大可用和最大保护是一样的。如果所有备机断连,会降级为最大性能模式,不阻塞主机事务。
日志发送和接收
YashanDB 在日志发送方面,做了很多优化,来提高日志发送的性能:
1.优先从日志缓存读取 Redo 日志,不产生磁盘读 IO。主备同步正常的情况下,待发送的 Redo 基本都在日志缓存里,因此不会对 Redo 磁盘产生 IO 争抢。只有当日志缓存里的 Redo 被淘汰时,才会从 Redo 文件或归档日志里读取 Redo。
2.日志批量发送,多条 Redo 一次发送,只回一次 ACK(Acknowledgment)。一段时间内产生的 Redo 日志批量发送,这一批 Redo 只需要一个 ACK,这样可以减少网络交互次数,提高吞吐量。
3.异步接收备库的 ACK。每执行一次 Redo 日志发送,并不会等待这批日志的 ACK,而是异步的接收备库的 ACK,不阻塞下一次发送。
4.Redo 缓存则发送,不等主库落盘。数据库产生的 Redo 优先写入日志缓存,然后进行本地刷盘,在本地刷盘的同时,日志发送即可进行,减少了一次等 IO 的时间。不过这种方式会带来一个问题,就是在主库发生故障后,备库的 Redo 可能比主库多,这就需要通过一些机制来判断备库的多余 Redo,在后面的未决日志这一章节将介绍这个机制。
5.备库在日志落盘前返回 ACK。如果在备库落盘前返回 ACK,则性能可进一步提升,但是在主备同时故障的情况下,可能有数据丢失的风险。该模式在大部分场景下能做到数据不丢失,同时性能更好,不过因为在极端场景下有丢失数据的风险,所以仅支持最大可用模式下使用。
![](https://static001.geekbang.org/infoq/b0/b0b5198398bf723bbb3b893c7f7fa66c.png)
图 4 日志发送与接收流程
归档 GAP
备库停机一段时间后,会落后主库较多 Redo 文件。
YashanDB 的备库重连后,直接从主库日志缓存接收最新 Redo,其余未接收到的 Redo,将通过归档日志拉取线程补齐,这部分归档称为归档 GAP。
由于最新 Redo 是通过日志缓存发送的,所以不会产生额外的 Redo 磁盘 IO,对主库性能影响较小,同时归档拉取线程逐个获取主库归档,可加速备库追赶主库。
![](https://static001.geekbang.org/infoq/91/916f73dc8cfcdf3a13aa88e00e791747.png)
图 5 归档 GAP 修复流程
未决日志
在最大保护模式下,主库的 Redo 日志没有被备库接收前,这部分日志对应的事务提交将阻塞。如果此时主库宕机,备库升主,新主库不包含这部分日志,旧主库降备后,需要将这部分日志回退,这部分日志称为未决日志。
未决日志可能会回退,其对应的事务不可提交,相应的脏页不能刷盘(如果脏页刷盘后,日志被回退了,就会违 反 WAL 原则)。
未决日志不能被备库回放,因为主库上可能没有提交对应的事务。
如果日志被主库和所有同步备持久化,则这部分日志不会被回退,这个日志点称为 commit index。主库维护 commit index 识别未决日志,同时主库会将 commit index 同步给备库。
![](https://static001.geekbang.org/infoq/e3/e35ed18a56f8b6beb2fa453add8b6f45.png)
图 6 未决日志场景 1
上图场景中,主备处于最大保护模式,备库还没有收到主库的 log 4 和 log 5 这两条日志。虽然这两条 Redo 日志在主库落盘了,但是如果主库此时发生故障,备库升主,旧主库降备后,这两条日志将会被回退。
![](https://static001.geekbang.org/infoq/ca/cab04559ec89940069e13b5825d81c48.png)
图 7 未决日志场景 2
上图场景中,主库的 log 4 这条日志已经写入主库的日志缓存,并且被发送到备库,但是主库还没有将 log4 落盘。如果主库此时发生宕机重启,主库上不会包含 log 4 这条日志,备库上这条日志将会被回退。
并行回放
YashanDB 的备库支持并行回放,来提高备库回放 Redo 的性能。
备库通过一个协调线程读取和分析 Redo 日志,然后将 Redo 按照会话 ID 哈希分配给后台回放线程,让这些后台线程并行回放 Redo。
并行回放大大提高了回放速率,使得备库 Redo 不积累,备库查询延迟低,并且故障切换的 RTO 很低。
YashanDB 的备库并行回放,复现了主库的 session 执行顺序,可以保证事务瞬时一致性,解决备库读一致性和回放性能难以兼顾的痛点问题。即使主库在高并发业务下,备库查询延迟也可以做到很低。
![](https://static001.geekbang.org/infoq/12/12fa6bab763e0967d56e764e421d3493.png)
图 8 备库并行回放
自动故障转移技术解析
当主库发生故障时,需要将某个备库切换为主库,并将业务转移到新的主库上。除了手动执行故障切换,YashanDB 还支持自选主、仲裁切换两种方式的自动故障切换。
RAFT 自动切换
YashanDB 的自选主切换是基于 Raft 算法实现的。Raft 是一种共识算法,能确保节点故障或网络分区下保持强一致性。
主备通过心跳检测对方是否存活,主库定期会给所有备库发送心跳,如果备库在超时时间内没有主库的心跳,那么备库就认为主库发生了故障。主库事务提交,要等待多数派节点将 Redo 落盘,除了主库节点,至少要有节点总数/2(下取整)个备库收到主库 Redo 。主库故障,备库心跳超时后,会发起选举投票,如果收到多数派节点的投票,则升为新的主库。投票算法可保证,新主是多数派里 Redo 最多的,而事务提交要等待多数派落盘,因此新主库一定落盘了所有已提交事务的 Redo,不会丢数据。
Raft 的每次选举都会产生一个新的任期(term),在一个任期内,最多只有一个主库。
Raft 将系统中的角色分为 Leader、Follower 和 Candidate。工程上为了提高系统稳定性,加入了 PreCandidate 角色:
1.Leader:对应于主库,向 Follower 发送 Redo 日志;当日志同步到多数派节点上后,通知 Follower 更新 commit index。
2.Follower:对应于稳态下的备库,接受并持久化 Leader 发送的日志,并回放 commit index 之前的日志。
3.Candidate:对应于选举中的备库,从 PreCandidate 到 Leader 的中间角色。
4.PreCandidate:对应于选举中的备库,从 Follower 到 Candidate 的中间角色。
![](https://static001.geekbang.org/infoq/d9/d961a38d4833a619dccb80d4a2549048.png)
图 9 Raft 节点状态转移图
如上图所示,数据库启动后都是 Follower 角色,如果没有收到 Leader 的心跳,到达超时时间后,会发起预选举。预选举成功后,会增加自己的任期然后发起正式选举,正式选举成功后,就成为 Leader,即从备库升为主库。在选举过程中,如果遇到任期比自己高的节点,或者发现当前任期的 Leader,则会中断选举,回到 Follower 角色。
仲裁切换
Raft 至少需要 3 个节点,不适合一主一备的自动切换,为此 YashanDB 提供了基于外部组件的一主一备仲裁切换功能。
yasom 是 YashanDB 的轻量级的数据库管理进程,一般部署在非数据库服务器,可对主备状态进行监控。yasom 和数据库之间有个 yasagent 进程进行代理通信,yasagent 部署在数据库所在服务器。yasom 检测到主库异常后,在满足切换条件的情况下,给备库下发 failover 命令进行切换。切换条件为 yasom 和备库都感知不到主库,如果其中任一个能感知到主库,说明主库正常。
![](https://static001.geekbang.org/infoq/bf/bf5060fcfb26f9def17e1f6d24ff7fe9.png)
图 10 OM 仲裁切换场景
如上图 3 种情况,情况 1 表示主库所在服务器故障,yasom 和备库都感知不到主库,可以切换。情况 2 表示主库进程故障宕机,yasom 和备库都感知不到主库,可以切换。情况 3 表示 yasom 和主库所在服务器网络异常,但是主库和备库之间网络正常,此时不会切换。
零丢失模式
零丢失模式下,只有确定备库不丢失数据的情况下,才可以升主,牺牲一定的可用性,但是不会丢失数据。
1.主备优先使用最大保护模式,保证数据全部同步到备库。
2.当备库异常时,yasom 将主库切换为最大可用模式,保证数据库可用,不阻塞业务。
3.主库故障时,如果处于最大保护模式,则 yasom 进行自动切换。
4.主库故障时,如果处于最大可用模式,则 yasom 不会切换,因为有数据丢失的风险,此时需要人工介入。
脑裂自动修复
非零丢失模式下,旧主库和新主库的数据可能不一致,旧主库降备后,与新主库发生脑裂。
yasom 发现备库脑裂后,将下发脑裂修复命令,修复备库不一致的数据,使主备复制恢复正常。
首先找到主备的日志分歧点,然后扫描备库 Redo 得到分歧点之后修改过的页面,将这些页面的 ID 发给主库,从主库获取对应页面并覆盖备库本地文件。再以分歧点 lsn 为基线,将主库上大于该 lsn 的数据页面发送到备库覆盖,最后将主库分歧点之后的 Redo 发送到备库,进行回放。回放完成后,主备将正常同步 Redo,脑裂修复完成。
![](https://static001.geekbang.org/infoq/06/06d39ea78b727f8628d1775cb2e5971c.png)
图 11 脑裂修复流程
总结及未来规划
在客户实际运用数据库的过程中,性能与业务连续性无疑是最为重要的两大考量因素。尽管主备高可用方案在业界极为普遍,但在真实业务场景中仍面临多重挑战。我们在架构技术与编程实现 等方面进行了深入的优化工作,以提升主备部署模式的同步性能及高可用保障。接下来,我们会继续丰富功能和性能,还将在以下几个方面继续打磨和增强:
1.逻辑主备:主要用于逻辑复制,滚动升级,HTAP 场景;
2.条件故障切换:非网络故障,比如文件损坏,服务器资源异常时自动切换。
评论