Author: 慕星
PolarDB采用物理复制的方式来实现朱从节点间的数据同步,区别于Mysql官方的binlog复制,PolarDB在主从节点间通过传输Redo Log,并在从节点上对Redo Log进行Replay,从而完成用户在主节点上写入或更新的数据,在从节点上能被完整的访问到。但物理复制的架构本身也会带来一些新的挑战和约束,具体笔者将在本篇文章详细道来。
物理复制是PolarDB的核心技术之一,结合底层分布式文件系统PolarStore,构建起了物理复制+共享存储的新一代云原生数据库架构。
在PolarDB中,定义了三种不同的节点,Primary,Replica,Standby,各自负责不同的职责,具体如下:
Primary:负责接收读写请求,又称为读写节点,可以理解为传统数据库中的主节点,一般只有1个。
Replica:负责接收读请求,又称为只读节点,可以理解传统数据库的从节点,支持有多个,可以随意扩展。
Standby:不提供读服务,主要用来备份,异地保证实例高可用,避免单点,一般只有1个,也可多个。
具体的架构如下:
如架构图所示,Primary和Replica节点共享同一个PFS(PolarStore File System),复用数据文件和日志文件,RO节点直接读取PFS上的Redo Log,并进行解析,并将其修改应用到自己Buffer Pool中的Page上,这样当用户的请求到达Replica节点后,就可以访问到最新的数据了。同时Replica和Primary节点间也会保持RPC通信,用于同步Replica当前日志的Apply位点,以及ReadView等信息。
Standby节点部署在其他Region中,拥有独立的PFS集群,拥有独立的数据和日志文件,Standby会向Primary节点建立连接,用于读取Primary节点上的Redo Log,并回发到Standby节点,Standby节点会将Redo Log保存自己本地,并解析这些Redo Log,将其完全在自己的Buffer Pool中进行回放,并通过周期性的刷脏操作将数据持久化到磁盘,最终实现数据同步。
我们今天讨论的主要问题也是基于Primary和Replica节点之间,在共享存储的架构下,相比原有的InnoDB能够在不增加磁盘存储的情况下,实现更好的扩展读请求负载,能够快速的增加和删除Replia节点,并且能在Replica节点和Primary节点进行实时HA切换,大大提升了实例的可用性,天然契合云原生架构。在原有的InnoDB架构中,数据的持久化是由Page Cleaner线程周期的对脏页进行落盘来完成的,以避免用户线程同步刷脏而影响性能。在PolarDB的架构中,为了保证用户线程的读请求在Replica节点上访问Page时给用户返回的数据的一致性,Primary节点在对脏页进行刷脏操作时,需要保证该脏页的最新修改的LSN不能超过所有Replica节点的最小Redo log Apply的 LSN位点,以避免用户在Replica节点上访问到过新的数据,以及正在做SMO的数据。因此为了保证磁盘数据始终保持在连续一致的状态,Primary节点在刷脏时必须要考虑Replica节点的Apply LSN的位点,并且受Replica节点的Apply LSN位点的约束来完成数据落盘。
我们把所有Replica节点上的最小Apply LSN定义为Safe LSN,Primary节点在进行Flush Page时,一定要保证该Page的最新修改的LSN(new_modification_lsn)要小于Safe LSN,不然就不能对这个Page进行刷脏落盘,因此在某些情况下可能会导致Primary节点上的脏页无法得到及时刷脏,并且无法推进最老的Flush LSN(oldest_flush_lsn)。
而在Replica节点上,我们为了加速物理复制的同步效率,新增了运行时应用(Runtime Apply)的机制,Runtime Apply是指在物理复制中Apply Redo时,如果Page不在Buffer Pool中,将不会对这个Page进行Apply,避免了Replica节点上后台Apply线程频繁从共享存储读取Page,但需要把解析好的Redo缓存起来,保存在Parse Buffer中,以便后续用户的读请求到达时读取共享存储上的Page,并通过Runtime Apply应用Parse Buffer中缓存的针对这个Page的修改的所有Redo,最终返回最新的Page。在Parse Buffer中缓存的Redo log必须要等到Primary节点的oldest_flush_lsn推进之后才能进行清理,即意味着这段Redo修改对应的脏页已经在Primary节点上落盘,那这段Redo在Replica上就可以丢掉了。
在这种约束下,倘若出现热点Page的更新(即new_modification_lsn不停地在更新),或者Primary节点刷脏过慢,就会导致Replica节点的Parse Buffer中堆积大量的解析好的Redo,同时会影响Replica节点的Parse和Apply性能,导致Replica节点的Apply LSN推进过慢,反向又会导致Primary节点更加无法刷脏,最终影响用户线程的写入操作。如果Replica节点应用日志的速度慢到一定程度,会导致应用日志的速度和写节点产生的日志差距会越拉越大,最终使得复制延迟会持续增大。
为了解决在以上约束下产生的各种问题,PolarDB针对Primary节点的Buffer Pool进行了一些优化,具体如下:
通过这三个策略,我们对这种热点页带来的影响基本降到了最低,使得Primary和Replica节点之间的物理复制更加丝滑。
Copy Page在热点页场景中的解决了热点页无法刷脏的问题,但同时也解决了在频繁修改热点页和刷脏之间对Page X锁和SX锁的争抢,在InnoDB的原生逻辑中,刷脏是需要长时间(整个IO期间)持有Page的SX锁的,但Copy Page只需要很少时间持有SX锁,从而大大降低了刷脏对写入请求的影响,这个议题是我们针对写入场景下的优化,也称之为Copy Page2.0,鉴于篇幅有限,后续会再写一个专题来介绍。
在PolarDB的物理复制+共享存储架构下,我们遇到了很多的挑战,在解决这些问题的过程中,也将PolarDB打磨的越来越好,相信在不远的将来,PolarDB也将会承载更多客户的信任和依托。