阿里云PolarDB及其共享存储PolarFS技术实现分析(下)


上篇介绍了PolarDB数据库及其后端共享存储PolarFS系统的基本架构和组成模块,是最基础的部分。本篇重点分析PolarFS的数据IO流程,元数据更新流程,以及PolarDB数据库节点如何适配PolarFS这样的共享存储系统。html

PolarFS的数据IO操做node

写操做mysql

通常状况下,写操做不会涉及到卷上文件系统的元数据更新,由于在写以前就已经经过libpfs的pfs_posix_fallocate()这个API将Block预分配给文件,这就避免在读写IO路径上出现代价较高的文件系统元数据同步过程。上图是PolarFS的写操做流程图,每步操做解释以下:算法

  1. POLARDB经过libpfs发送一个写请求Request1,经由ring buffer发送到PolarSwitch;sql

  2. PolarSwitch根据本地缓存的元数据,将Request1发送至对应Chunk的Leader节点(ChunkServer1);数据库

  3. Request1到达ChunkServer1后,节点上的RDMA NIC将Request1放到一个预分配好的内存buffer中,基于Request1构造一个请求对象,并将该对象加到请求队列中。一个IO轮询线程不断轮询这个请求队列,一旦发现有新请求则当即开始处理;后端

  4. IO处理线程经过异步调用将Request1经过SPDK写到Chunk对应的WAL日志块上,同时将请求经过RDMA异步发向给Chunk的Follower节点(ChunkServer二、ChunkServer3)。因为都是异步调用,因此数据传输是并发进行的;缓存

  5. 当Request1请求到达ChunkServer二、ChunkServer3后,一样经过RDMA NIC将其放到预分配好的内存buffer并加入到复制队列中;服务器

  6. Follower节点上的IO轮询线程被触发,Request1经过SPDK异步地写入该节点的Chunk副本对应的WAL日志块上;网络

  7. 当Follower节点的写请求成功后,会在回调函数中经过RDMA向Leader节点发送一个应答响应;

  8. Leader节点收到ChunkServer二、ChunkServer3任一节点成功的应答后,即造成Raft组的majority。主节点经过SPDK将Request1写到请求中指定的数据块上;

  9. 随后,Leader节点经过RDMA NIC向PolarSwitch返回请求处理结果;

  10. PolarSwitch标记请求成功并通知上层的POLARDB。

读请求无需这么复杂的步骤,lipfs发起的读请求直接经过PolarSwitch路由到数据对应Chunk的Leader节点(ChunkServer1),从其中读取对应的数据返回便可。须要说明的是,在ChunkServer上有个子模块叫IoScheduler,用于保证发生并发读写访问时,读操做可以读到最新的已提交数据。

基于用户态的网络和IO路径

在本地IO处理上,PolarFS基于预分配的内存buffer来处理请求,将buffer中的内容直接使用SPDK写入WAL日志和数据块中。PolarFS读写数据基于SPDK套件直接经过DMA操做硬件设备(SSD卡)而不是操做系统内核IO协议栈,解决了内核IO协议栈慢的问题;经过轮询的方式监听硬件设备IO完成事件,消除了上下文切换和中断的开销。还能够将IO处理线程和CPU进行一一映射,每一个IO处理线程独占CPU,相互之间处理不一样的IO请求,绑定不一样的IO设备硬件队列,一个IO请求生命周期从头至尾都在一个线程一颗CPU上处理,不须要锁进行互斥。这种技术实现最大化的和高速设备进行性能交互,实现一颗CPU达每秒约20万次IO处理的能力,而且保持线性的扩展能力,也就意味着4颗CPU能够达到每秒80万次IO处理的能力,在性能和经济型上远高于内核。

网络也是相似的状况。过去传统的以太网,网卡发一个报文到另外一台机器,中间经过一跳交换机,大概须要一百到两百微秒。POLARDB支持ROCE以太网,经过RDMA网络,直接将本机的内存写入另外一台机器的内存地址,或者从另外一台机器的内存读一块数据到本机,中间的通信协议编解码、重传机制都由RDMA网卡来完成,不须要CPU参与,使性能得到极大提高,传输一个4K大小报文只须要六、7微秒的时间。

如同内核的IO协议栈跟不上高速存储设备能力,内核的TCP/IP协议栈跟不上高速网络设备能力,也被POLARDB的用户态网络协议栈代替。这样就解决了HDFS和Ceph等目前的分布式文件系统存在的性能差、延迟大的问题。

基于ParallelRaft的数据可靠性保证

在PolarFS中,位于不一样ChunkServer上的3个Chunk数据副本使用改进型Raft协议ParallelRaft来保障可靠性,经过快速主从切换和majority机制确保可以容忍少部分Chunk副本离线时仍可以持续提供在线读写服务,即数据的高可用。

在标准的Raft协议中,raft日志是按序被Follower节点确认,按序被Leader节点提交的。这是由于Raft协议不容许出现空洞,一条raft日志被提交,意味着它以前的全部raft日志都已经被提交。在数据库系统中,对不一样数据的并发更新是常态,也正由于这点,才有了事务的组提交技术,但若是引入Raft协议,意味着组提交技术在PolarFS数据多副本可靠性保障这一层退化为串行提交,对于性能会产生很大影响。经过将多个事务batch成一个raft日志,经过在一个Raft Group的Leader和Follower间创建多个链接来同时处理多个raft日志这两种方式(batching&pipelining)可以缓解性能退化。但batch会致使额外延迟,batch也不能过大。pipelining因为Raft协议的束缚,仍然须要保证按序确认和提交,若是出现因为网络等缘由致使先后pipeline上的raft日志发送往follow或回复leader时乱序,那么就不可避省得出现等待。

为了进一步优化性能,PolarFS对Raft协议进行了改进。核心思想就是解除按序确认,按序提交的束缚。将其变为乱序确认,乱序提交和乱序应用。首先看看这样作的可行性,假设每一个raft日志表明一个事务,多个事务可以并行提交说明其不存在冲突,对应到存储层每每意味着没有修改相同的数据,好比事务T1修改File1的Block1,事务T2修改File1的Block2。显然,先修改Block1仍是Block2对于存储层仍是数据库层都没有影响。这真是可以乱序的基础。下图为优化先后的性能表现:

但T1和T2都修改了同一个表的数据,致使表的统计信息发生了变化,好比T1执行后表中有10条记录,T2执行后变为15条(举例而已,不必定正确)。因此,他们都须要更新存储层的相同BlockX,该更新操做就不能乱序了。

为了解决上述所说的问题,ParallelRaft协议引入look behind buffer(LBB)。每一个raft日志都有个LBB,缓存了它以前的N个raft日志所修改的LBA信息。LBA即Logical Block Address,表示该Block在Chunk中的偏移位置,从0到10GB。经过判断不一样的raft日志所包含的LBA是否有重合来决定可否进行乱序/并行应用,好比上面的例子,前后修改了BlockX的raft日志就能够经过LBB发现,若是T2对BlockX的更新先完成了确认和提交,在应用前经过LBB发现所依赖的T1对BlockX的修改尚未应用。那么就会进入pending队列,直到T1对BlockX完成应用。

另外,乱序意味着日志会有空洞。所以,Leader选举阶段额外引入了一个Merge阶段,填补Leader中raft日志的空洞,可以有效保障协议的Leader日志的完整性。

PolarFS元数据管理与更新

PolarFS各节点元数据维护

libpfs仅维护文件块(块在文件中的偏移位置)到卷块(块在卷中的偏移位置)的映射关系,并未涉及到卷中Chunk跟ChunkServer间的关系(Chunk的物理位置信息),这样libpfs就跟存储层解耦,为Chunk分配实际物理空间时无需更新libpfs层的元数据。而Chunk到ChunkServer的映射关系,也就是物理存储空间到卷的分配行为由PolarCtrl组件负责,PolarCtrl完成分配后会更新PolarSwitch上的缓存,确保libpfs到ChunkServer的IO路径是正确的。

Chunk中Block的LBA到Block真实物理地址的映射表,以及每块SSD盘的空闲块位图均所有缓存在ChunkServer的内存中,使得用户数据IO访问可以全速推动。

PolarFS元数据更新流程

前面咱们介绍过,PolarDB为每一个数据库实例建立了一个volume/卷,它是一个文件系统,建立时生成了对应的元数据信息。因为PolarFS是个可多点挂载的共享访问分布式文件系统,须要确保一个挂载点更新的元数据可以及时同步到其余挂载点上。好比一个节点增长/删除了文件,或者文件的大小发生了变化,这些都须要持久化到PolarFS的元数据上并让其余节点感知到。下面咱们来讨论PolarFS如何更新元数据并进行同步。

PolarFS的每一个卷/文件系统实例都有相应的Journal文件和与之对应的Paxos文件。Journal文件记录了文件系统元数据的修改历史,是该卷各个挂载点之间元数据同步的中心。Journal文件逻辑上是一个固定大小的循环buffer,PolarFS会根据水位来回收Journal。若是一个节点但愿在Journal文件中追加项,其必需使用DiskPaxos算法来获取Journal文件控制权。

正常状况下,为了确保文件系统元数据和数据的一致性,PolarFS上的一个卷仅设置一个计算节点进行读写模式挂载,其余计算节点以只读形式挂载文件系统,读写节点锁会在元数据记录持久化后立刻释放锁。可是若是该读写节点crash了,该锁就不会被释放,为此加在Journal文件上的锁会有过时时间,在过时后,其余节点能够经过执行DiskPaxos来从新竞争对Journal文件的控制权。当PolarFS的一个挂载节点开始同步其余节点修改的元数据时,它从上次扫描的位置扫描到Journal末尾,将新entry更新到节点的本地缓存中。PolarFS同时使用push和pull方式来进行节点间的元数据同步。

下图展现了文件系统元数据更新和同步的过程:

  1. Node 1是读写挂载点,其在pfs_fallocate()调用中将卷的第201个block分配给FileID为316的文件后,经过Paxos文件请求互斥锁,并顺利得到锁。

  2. Node 1开始记录事务至journal中。最后写入项标记为pending tail。当全部的项记录以后,pending tail变成journal的有效tail。

  3. Node1更新superblock,记录修改的元数据。与此同时,node2尝试获取访问互斥锁,因为此时node1拥有的互斥锁,Node2会失败重试。

  4. Node2在Node1释放lock后(多是锁的租约到期所致)拿到锁,但journal中node1追加的新项决定了node2的本地元数据是过期的。

  5. Node2扫描新项后释放lock。而后node2回滚未记录的事务并更新本地metadata。最后Node2进行事务重试。

  6. Node3开始自动同步元数据,它只须要load增量项并在它本地重放便可。

PolarFS的元速度更新机制很是适合PolarDB一写多读的典型应用扩展模式。正常状况下一写多读模式没有锁争用开销,只读实例能够经过原子IO无锁获取Journal信息,从而使得PolarDB能够提供近线性的QPS性能扩展。

数据库如何适配PolarFS

你们可能认为,若是读写实例和只读实例共享了底层的数据和日志,只要把只读数据库配置文件中的数据目录换成读写实例的目录,貌似就能够直接工做了。可是这样会遇到不少问题,MySQL适配PolarFS有不少细节问题须要处理,有些问题只有在真正作适配的时候还能想到,下面介绍已知存在的问题并分析数据库层是如何解决的。

数据缓存和数据一致性

从数据库到硬件,存在不少层缓存,对基于共享存储的数据库方案有影响的缓存层包括数据库缓存,文件系统缓存。

数据库缓存主要是InnoDB的Buffer Pool(BP),存在2个问题:

  1. 读写节点的数据更改会缓存在bp上,只有完成刷脏页操做后polarfs才能感知,因此若是在刷脏以前只读节点发起读数据操做,读到的数据是旧的;

  2. 就算PolarFS感知到了,只读节点的已经在BP中的数据仍是旧的。因此须要解决不一样节点间的缓存一致性问题。

PolarDB采用的方法是基于redolog复制的节点间数据同步。可能咱们会想到Primary节点经过网络将redo日志发送给ReadOnly/Replica节点,但其实并非,如今采用的方案是redo采用非ring buffer模式,每一个文件固定大小,大小达到后Rotate到新的文件,在写模式上走Direct IO模式,确保磁盘上的redo数据是最新的,在此基础上,Primary节点经过网络通知其余节点能够读取的redo文件及偏移位置,让这些节点自主到共享存储上读取所需的redo信息,并进行回放。流程以下图所示:

因为StandBy节点与读写节点不共享底层存储,因此须要走网络发送redo的内容。节点在回放redo时需区分是ReadOnly节点仍是StandBy节点,对于ReadOnly节点,其仅回放对应的Page页已在BP中的redo,未在BP中的page不会主动从共享存储上读取,且BP中Apply过的Page也不会回刷到共享存储。但对于StandBy节点,须要全量回放并回刷到底层存储上。

文件系统缓存主要是元数据缓存问题。文件系统缓存包括Page Cache,Inode/Dentry Cache等,对于Page Cache,能够经过Direct IO绕过。但对于VFS(Virtual File System)层的Inode Cache,没法经过Direct IO模式而需采用o_sync的访问模式,但这样致使性能严重降低,没有实际意义。vfs层cache没法经过direct io模式绕过是个很严重的问题,这就意味着读写节点建立的文件,只读节点没法感知,那么针对这个新文件的后续IO操做,只读节点就会报错,若是采用内核文件系统,很差进行改造。

PolarDB经过元数据同步来解决该问题,它是个用户态文件系统,数据的IO流程不走内核态的Page Cache,也不走VFS的Inode/Dentry Cache,彻底本身掌控。共享存储上的文件系统元数据经过前述的更新流程实现便可。经过这种方式,解决了最基本的节点间数据同步问题。

事务的数据可见性问题

1、MySQL/InnoDB经过Undo日志来实现事务的MVCC,因为只读节点跟读写节点属于不一样的mysqld进程,读写节点在进行Undo日志Purge的时候并不会考虑此时在只读节点上是否还有事务要访问即将被删除的Undo Page,这就会致使记录旧版本被删除后,只读节点上事务读取到的数据是错误的。

针对该问题,PolarDB提供两种解决方式:

  • 全部ReadOnly按期向Primary汇报本身的最大能删除的Undo数据页,Primary节点统筹安排;

  • 当Primary节点删除Undo数据页时候,ReadOnly接收到日志后,判断即将被删除的Page是否还在被使用,若是在使用则等待,超过一个时间后还未有结束则直接给客户端报错。

2、还有个问题,因为InnoDB BP刷脏页有多种方式,其并非严格按照oldest modification来的,这就会致使有些事务未提交的页已经写入共享存储,只读节点读到该页后须要经过Undo Page来重建可见的版本,但可能此时Undo Page还未刷盘,这就会出现只读上事务读取数据的另外一种错误。

针对该问题,PolarDB解决方法是:

  1. 限制读写节点刷脏页机制,若是脏页的redo尚未被只读节点回放,那么该页不能被刷回到存储上。这就确保只读节点读取到的数据,它以前的数据链是完整的,或者说只读节点已经知道其以前的全部redo日志。这样即便该数据的记录版本当前的事务不可见,也能够经过undo构造出来。即便undo对应的page是旧的,能够经过redo构造出所需的undo page。

  2. replica须要缓存全部未刷盘的数据变动(即RedoLog),只有primary节点把脏页刷入盘后,replica缓存的日志才能被释放。这是由于,若是数据未刷盘,那么只读读到的数据就多是旧的,须要经过redo来重建出来,参考第一点。另外,虽然buffer pool中可能已经缓存了未刷盘的page的数据,但该page可能会被LRU替换出去,当其再次载入因此只读节点必须缓存这些redo。

DDL问题

若是读写节点把一个表删了,反映到存储上就是把文件删了。对于mysqld进程来讲,它会确保删除期间和删除后再也不有事务访问该表。可是在只读节点上,可能此时还有事务在访问,PolarFS在完成文件系统元数据同步后,就会致使只读节点的事务访问存储出错。

PolarDB目前的解决办法是:若是主库对一个表进行了表结构变动操做(须要拷表),在操做返回成功前,必须通知到全部的ReadOnly节点(有一个最大的超时时间),告诉他们,这个表已经被删除了,后续的请求都失败。固然这种强同步操做会给性能带来极大的影响,有进一步的优化的空间。

Change Buffer问题

Change Buffer本质上是为了减小二级索引带来的IO开销而产生的一种特殊缓存机制。当对应的二级索引页没有被读入内存时,暂时缓存起来,当数据页后续被读进内存时,再进行应用,这个特性也带来的一些问题,该问题仅存在于StandBy中。例如Primary节点可能由于数据页还未读入内存,相应的操做还缓存在Change Buffer中,可是StandBy节点则由于不一样的查询请求致使这个数据页已经读入内存,能够直接将二级索引修改合并到数据页上,无需通过Change Buffer了。但因为复制的是Primary节点的redo,且须要保证StandBy和Primary在存储层的一致性,因此StandBy节点仍是会有Change Buffer的数据页和其对应的redo日志,若是该脏页回刷到存储上,就会致使数据不一致。

为了解决这个问题,PolarDB引入shadow page的概念,把未修改的数据页保存到其中,将cChange Buffer记录合并到原来的数据页上,同时关闭该Mtr的redo,这样修改后的Page就不会放到Flush List上。也就是StandBy实例的存储层数据跟Primary节点保持一致。

性能测试

性能评估不是本文重点,官方的性能结果也不必定是靠谱的,只有真实测试过了才算数。在此仅简单列举阿里云本身的性能测试结果,权当一个参考。

PolarFS性能

不一样块大小的IO延迟

4KB大小的不一样请求类型

PolarDB总体性能

使用不一样底层存储时性能表现

对外展现的性能表现

与Aurora简单对比

阿里云的PolarDB和AWS Aurora虽然同为基于MySQL和共享存储的Cloud-Native Database(云原生数据库)方案,不少原理是相同的,包括基于redo的物理复制和计算节点间状态同步。但在实现上也存在很大的不一样,Aurora在存储层采用日志即数据的机制,计算节点无需再将脏页写入到存储节点,大大减小了网络IO量,但这样的机制须要对InnoDB存储引擎层作很大的修改,难度极大。而PolarDB基本上听从了原有的MySQL IO路径,经过优化网络和IO路径来提升网络和IO能力,相对来讲在数据库层面并未有框架性的改动,相对容易些。我的认为Aurora在数据库技术创新上更胜一筹,但PolarDB在数据库系统级架构优化上作得更好,以尽量小的代价得到了足够好的收益。

另附PolarFS的架构师曹伟在知乎上对PolarDB和Aurora所作的对比:

在设计方法上,阿里云的PolarDB和Aurora走了不同的路,归根结底是咱们的出发点不一样。AWS的RDS一开始就是架设在它的虚拟机产品EC2之上的,使用的存储是云盘EBS。EC2和EBS之间经过网络通信,所以AWS的团队认为“网络成为数据库的瓶颈”,在Aurora的论文中,他们开篇就提出“Instead, the bottleneck moves to the network between the database tier requesting I/Os and the storage tier that performs these I/Os.” Aurora设计于12到13年之际,当时网络主流是万兆网络,确实容易成为瓶颈。而PolarDB是从15年开始研发的,咱们见证了IDC从万兆到25Gb RDMA网络的飞跃。所以咱们很是大胆的判断,将来几年主机经过高速网络互联,其传输速率会和本地PCIe总线存储设备带宽打平,网络不管在延迟仍是带宽上都会接近总线,所以再也不成为高性能服务器的瓶颈。而偏偏是软件,过去基于内核提供的syscall开发的软件代码,才是拖慢系统的一环。Bottleneck resides in the software.

在架构上Aurora和PolarDB各有特点。我认为PolarDB的架构和技术更胜一筹。

1)现代云计算机型的演进和分化,计算机型向高主频,多CPU,大内存的方向演进;存储机型向高密度,低功耗方向发展。机型的分化能够大大提升机器资源的使用率,下降TCO。

所以PolarStore中大量采用OS-bypass和zero-copy的技术来节约CPU,下降处理单位I/O吞吐须要消耗的CPU资源,确保存储节点处理I/O请求的效率。而Aurora的存储节点须要大量CPU作redolog到innodb page的转换,存储节点的效率远不如PolarStore。

2)Aurora架构的最大亮点是,存储节点具备将redolog转换为innodb page的能力,这个改进看着很吸引眼球,事实上这个优化对关系数据库的性能提高颇有限,性能瓶颈真的不在这里:),反而会拖慢关键路径redolog落地的性能。btw,在PolarDB架构下,redolog离线转换为innodb page的能力不难实现,但咱们目前不认为这是高优先级要作的。

3)Aurora的存储多副本是经过quorum机制来实现的,Aurora是六副本,也就是说,须要计算节点向六个存储节点分别写六次,这里其实计算节点的网络开销又上去了,并且是发生在写redolog这种关键路径上。而PolarDB是采用基于RDMA实现的ParallelRaft技术来复制数据,计算节点只要写一次I/O请求到PolarStore的Leader节点,由Leader节点保证quorum写入其余节点,至关于多副本replication被offload到存储节点上。

此外,在最终一致性上Aurora是用gossip协议来兜底的,在完备程度上没有PolarDB使用的ParallelRaft算法有保证。

4)Aurora的改动手术切口太大,使得它很难后面持续跟进社区的新版本。这也是AWS几个数据库产品线的通病,例如Redshift,如何吸取PostgrelSQL 10的变动是他们的开发团队很头疼的问题。对新版本作到与时俱进是云数据库的一个朴素需求。怎么设计这个刀口,达到effect和cost之间的平衡,是对架构师的考验。

总得来讲,PolarDB将数据库拆分为计算节点与存储节点2个独立的部分,计算节点在已有的MySQL数据库基础上进行修改,而存储节点基于全新的PolarFS共享存储。PolarDB经过计算和存储分离的方式实现提供了即时生效的可扩展能力和运维能力,同时采用RDMA和SPDK等最新的硬件来优化传统的过期的网络和IO协议栈,极大提高了数据库性能,基本上解决了使用MySQL是会遇到的各类问题,除此以外本文并未展开介绍PolarDB的ParallelRaft,其依托上层数据库逻辑实现IO乱序提交,大大提升多个Chunk数据副本达成一致性的性能。以上这些创新和优化,都成为了将来数据库的发展方向。

参数资料:


本文来自网易云社区 ,经做者温正湖受权发布。

网易云免费体验馆,0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区


相关文章:
【推荐】 从加班论客户端开发中的建模
【推荐】 GDB抓虫之旅(上篇)
【推荐】 谈谈验证码的工做原理

相关文章
相关标签/搜索