分享人:北侠 阿里云PolarDB产品部
正文:本文介绍了低延迟流复制的思路。
一、PolarDB云原生架构 - 存储计算分离架构
PolarDB是存储计算分离的架构云原生的数据库,上图左侧是传统的计算存储一体化的数据库,它的CPU、内存、磁盘都是在一台机器上。传统的架构是通过流复制做高可用,可能会有十五、六个写节点把数据流复制出去,也可以做一些简单的读写分离,在传统环境里扩容、缩容是比较麻烦的。
上图右侧,是将存储层放到了共享存储上,共享存储呢有PolarDB自营的PolarFS的文件系统,下面可以对接任意分布式的块设备。比如在云上售卖的PolarDB的实例,底层就是为数据库自研的Polar store分布式共享的块设备。
如果用户线下想用我们的开源的数据库,他可以将PolarDB跑在自己的分布式存储,同时将PolarDB自主的搭建在云厂商提供的块设备上,只要是块设备,这个架构就能跑起来。
这个架构的优势是什么呢?一是扩展性。这个阶段分离后,当计算和存储不足的时候,可以做一个分别的扩展。二是成本。有些用户的业务可以达到一组16备,这么多备节点将数据就复制出来。共享存储是不管扩展多个点,底层的共享存储都只有一份存储,也许是一个3副本或者是1.5副本呢,不管最终扩到多少个副本数来计算层的结点数,底层的存储数是不变的。
这样也带来了易用性。比如在做扩容的时候,将三个节点迅速扩展到16个节点,新节点只要做数据的挂载,就立即进入服务的状态了。在数据库的业务上,存储层可以看作分布式的技术,存储层通过存储的池化后,可以动态地进行扩容和缩容。
在计算侧任何一个计算节点,都能看到数据库所有的数据,对于节点是单机的企业,整体上是分布式的效果。借助共享存储的高级特性,三副本的高可用,秒级备份。
架构面临的挑战是什么呢?PostgreSQL挂载到共享存储上就能跑起来吗?已经化成水了,然后我在桌上淀粉呢。充电费你不嫌脏啊,反正玩那个就不吃了,一个共享存储上,那么你就可以跑起来了。第一期讲了数据一致性的问题,第三期,讲了低延迟流复制,只有主备延迟足够的低,在做读写分离的时候,业务价值才更高,如果主备之间的延迟非常大,也没有什么业务价值。
二、PolarDB云原生架构 - 低延迟流复制
1)传统流复制
首先看一下传统数据流复制的问题,从上面这张图里面可以看到整个链路非常长,上图最左侧是一个master节点,事物在运行时接收到DML和DDL的时候要改buffer,通过buffer产生WAL日志,把日志刷到磁盘上,进程从WAL日志读上来,通过网络发送到备节点,备节点可能是15、 16个,也可能是N个。通过网络发送过去后,备节点就通过WAL Receiver进程,把WAL日志读取解析,写入到本地的磁盘里。写入到本地磁盘后,备节点startup进程是专门做日志的回放,回放进程从本地的local storage把WAL日志读上来解析,解析出来运用到自己的页面里面,解析到的WAL日志对页面做123的修改,把123页面的内容读到内存里,把WAL日志放到123页面里面。Buffer被修改之后,整个回放完成了,把buffer修改完后,在备节点上才能读取到WAL日志产生的效果。整个链路是非常长的,还涉及到主节点刷脏,备库也要刷脏,再把界面123修改完后,当内存不够用时,需要定期把parse提到盘上,这是DML的操作。
DDL也是同样的道理,回放进程在解析到DDL时,需要对表进行加锁,加锁后整个进程就被夯住了,有可能对某些页面或者表做堵,这时加锁操作就要被夯住,进程就要去等待。整个链路是非常长的,是CPU密集型和IO密集型,会涉及到大量的CPU计算和IO的操作,这就是传统流复制的问题。链路很长并且在主备节点之间,它要做很多的数据复制,比如储备之间主节点,任何的一个WAL日志都要做多份的复制,把这个数据复制成多份,发送到其他的节点。
2)优化1-只复制WAL Meta
我们的底层是共享存储,主节点在写WAL日志的时候,会将WAL日志写到共享存储上,备节点不需要去复制的时候,不用把WAL整体复制过去。所有的备节点都能从共享存储上读取到它想要的WAL日志,WAL日志对不同节点的回放速率是不一样的,需要对它做控制的。
优化方案是由原来的主备节点复制的WAL日志,改成只复制原信息。左上角虚线框里面,这个虚线框里面标记了一条WAL大概的格式,WAL Record包含Header、PageID、Payload,PageID是标记WAL日志修改了哪几个页面,在复制的时候只需要将WAL日志的前两部分,就是将Heade、Page list修改的页面,通过网络复制出去,复制后在备节点。它的整个代码路径跟之前是一样的,也是通过WAL Receiver进程收到WAL Mata信息,收到Mata信息后就不需要再写了。因为主节点已经将WAL日志写到了共享存储上,备节点要做的工作是根据WAL日志带的原信息,从共享存储上找到相应的WAL文件,读取WAL真正的内容。右上角的图片,在原生PG数据库里面,主备复制之间的数据量可以减小到原来的1%。整个数据只有原信息,计算节点之间横向的复制流数据是非常少的,这样复制量就降下去了。如果用传统模式,一份数据要做16次的复制,现在一份数据写到一个共享盘上,其它的节点按需从盘上读取WAL日志。
WAL日志如何生效呢?右侧图Replica节点里的walreceiver进程收到WAL Mata信息后,因为WAL Mata里面包含了这条WAL日志、修改的页面,这时只需做对Mata进一步的回放就可以了。
3)优化2-页面回放优化:按需回放
第二个回放的核心思想是按需回放,不同的备节点运行速度是不一样的,回放速度也是不一样的,收到一条WAL日志如何去做回放呢?这条WAL日志只有云信息。如果WAL日志对应的Page,WAL Mata里面记录了,这条WAL日志最多修改的32个页面的ID,可以在内存里面先进一次查找。如果这个页面不在内存里,这个页面在主节点上读取了,但是备节点从来没有读取过这个页面,那么这个页面就在共享存储上,它不在自己的内存里面。这时收到一条来自Master节点对这个页面的修改的WAL原信息,这时就不回放这条WAL日志。我们不需要回放不在内存中的页面,这样,就减少了WAL流量。
我们对不在内存中的页面不进行回放呢?主要是因为这个页面在主界面已经回放过了,RO只读节点回放出来也没有什么效果,它不能刷盘。第二节点回放之后,可以在内存里面等待其它进程读取,如果其它进程不读取,这个回放就是浪费的。
如果对应的Page不在内存中,仅仅记录一个Logindex。Logindex记录了这条WAL日志改了哪些页面,WAL Mata改了某些页面,我不需要做回访,但是它的WAL Mata的修改,需要把它构建出来和记录下来。这时我们就认为这次回放就做完了。
如果对应的Page在内存中, Page失效可标记为outdate,记录一下Logindex,对于某些在内存里的页面,也不需要做真正的回访,仅仅做一个标记就可以了。同理,备节点也不需要重复回放。比如一条WAL日志改了页面一,主节点通过的Mate信息把这个信息发送到备节点,备节点收到这个信息后,发现页面一已经在内存里面了,如果需要做真正的回访,这时要从存储上把这条WAL日志的内容堵上来。赌上来后的回放动作跟原生社区的回访方式是一样的。
从网络上拿到一条WAL Mata,在buffer pool里面找到这个页面,标记一下内存,在内存里标记它是outdate,再写下Logindex,最后返回。在这个过程中没有大量的RO操作,仅从网络里收到一条很短的Mata,标记为outdate。那么真正的回放是谁来完成的呢,
红色箭头的流程是在内存里面的几个操作,整个回放就完成了。跟原生的相比,进行了优化。
用户的session进程。右侧图橙色的框里,用户后台进程要读取这个页面一,它发现页面一被标记成了outdate。对用户进程来说,它是个老页面,这时可以查找Logindex结构,因为这个结构里记录了这个页面被某条WAL日志进行了修改。那么他读取页面时可以按需进行回放,可能是页面一被三条WAL日志修改,而这个进程仅需要前两个进程的修改,在进程中将标记成outdate的页面,回放了两条WAL日志,这个页面就可以被用户进程读取到需要的版本。可能其它的用户进程需要读这个页面的第三条WAL日志修改的内容,去看时发现,页面已经回放到了WAL日志二,另外一个进程,仅需要回访第三条WAL日志就完成了。
大家可以看到,真正的回放动作在session进程里面。用户的session进程是有多个的,相当于有多个进程在做真正的回访。主备节点间的延迟,影响延迟的关键路径是内存操作。因此,优化的思路将延迟又降低了。
在右侧图中,我们在线上去购买了IWS 的Aurora和PolarDB相同规格的实例,在负载比较低的情况下,在测试场景里对RO节点的流量,要把它的cpu释放出来,流量压力比较低,但主节点的压力是比较高的。写入量很大的情况下,复制延迟IWS是30毫米,PolarDB的大部分只需1毫秒就完成整个复制了。意味着在做读写分离的时候,用户将数据写入到主节点,从备节点去读,只需要1毫秒就可以读取到一个最新的数据。
4)优化3-DDL锁优化
在主节点执行DDL的时候,比如在drop table,需要在所有的节点上对表上排他锁,保证表文件不会在只读节点读取时被主结点删除。因为这个文件在共享存储上只有一份,如果主节点删掉了,备节点读取时,文件就不在了,这个问题就需要通过DDL锁来解决,所有只读节点对表上排他锁是通过WAL日志复制,复制到所有只读节点上,只读节点来回放WAL日志,回放的时候发现这条WAL日志是对某个表进行上锁,在原生的流程里面,它就会对这个表进行上锁。如果回放进程对某个表上排它锁,有可能是不成功的,因为这个表可能被其它的业务进程访问,这时回放进程就被阻塞住了,回放进程是很关键的进程,为了保证平滑的效果,我们在后台启动了一批上锁进程,上锁进程将回放进程、关键进程的上锁动作进行了托管,将上锁动作卸载到了回访进程,后台进程托管了上锁的动作,主进程仍然可以继续推送WAL日志的回放。
可以看到在长事务阻塞的DDL回放的延迟的变化,在上图右上角图中PolarDB运行的过程中发生长事务的DDL,整个运行过程是很平滑的,Aurora没有做这方面的优化,存在不确定的延迟。