文章开头的架构图有提到 KeeWiDB 集群由多个分片构成,每个分片内部有多个节点,这些节点共同组成一主多从的高可用架构。和 Redis 类似,用户的请求会根据 Key 被路由到对应分片的主节点,主节点执行完后再将请求转化为 Binlog Record 写入本地的日志文件并转发给从节点,从节点通过应用日志文件完成数据的复制。
在 Redis 的 Replication 实现中,从节点每接收一个请求都立即执行,然后再继续处理下一个请求,如此往复。依赖其全内存的实现,单个请求的执行耗时非常短,从节点的回放相当于是单连接的 pipeline 写入,其回放速度足以跟上主节点的执行速度。但这种方式却不适合 KeeWiDB 这样的存储型数据库,主要原因如下:
- 存储型数据库的请求执行过程中涉及到磁盘 IO,单个请求的执行耗时本身就比较长;
- 主节点同时服务多个客户端连接,不同连接的请求并发执行,发挥了协程异步 IO 的优势,节点整体 QPS 有保障;
- 主从同步只有一个连接,由于从库顺序回放请求,无法并发,回放的 QPS 远远跟不上主节点处理用户请求的 QPS;
为了提升从节点回放的速度,避免在主库高负载写入场景下,出现从库追不上主库的问题,KeeWiDB 的 Replication 机制做了以下两点改进:
- 在从节点增加 RelayLog 作为中继,将从节点的命令接收和回放两个过程拆开,避免回放过程拖慢命令的接收速度;
- 在主节点记录 Binlog 的时候增加逻辑时钟信息,回放的时候根据逻辑时钟确定依赖关系,将互相之间没有依赖的命令一起放进回放的协程池,并发完成这批命令的回放,提升从节点整体的回放 QPS;
图:从库并发回放
所谓的逻辑时钟,对应到 KeeWiDB 的具体实现里,就是我们在每一条 BinLog record 中添加了 seqnum 和 parent 两个字段:
- seqnum 是主节点事务 commit 的序列号,每次有新的事务 commit,当前 seqnum 赋给当前事务,全局 seqnum 自增 1;
- parent 由主节点在每个事务开始执行前的 prepare 阶段获取,记录此时已经 commit 的最大 seqnum,记为 max,说明当前事务是在 max 对应事务 commit 之后才开始执行,二者在主节点端有逻辑上的先后关系;
从节点回放 RelayLog 中的 Binlog Record 时,我们只需要简单地将它的 parent 和 seqnum 看作一个区间,简记为 (P,S),如果它的 (P,S) 区间和当前正在回放的其它 Record 的 (P,S) 区间有交集,说明他们在主节点端 Prepare 阶段没有冲突,可以把这条 Record 放进去一块并发地回放,反之,则这条 Record 需要阻塞等待,等待当前正在回放的这批 Binlog Record 全部结束后再继续。
通过在 Binlog 中添加 seqnum 和 parent 两个字段,我们在保证数据正确性的前提下实现了从库的并发回放,确保了主库在高负载写入场景下,从库依旧可以轻松的追上主库,为我们整个系统的高可用提供了保障。
本篇文章先从整体架构介绍了 KeeWiDB 的各个组件,然后深入 Server 内部分析了在线程模型选择时的一些思考以及面临的挑战,最后介绍了存储引擎层面的数据文件以及相关日志在不同存储介质上的分布情况,以及 KeeWiDB 是如何解决从库回放 Binlog 低效的问题。通过本文,相信不少读者对 KeeWiDB 又有了进一步的了解。那么,在接下来的文章中我们还会深入到 KeeWiDB 自研存储引擎内部,向读者介绍 KeeWiDB 在存储引擎层面如何实现高效的数据存储和索引,敬请期待。
目前,KeeWiDB 正在公测阶段(链接:https://cloud.tencent.com/product/keewidb ),现已在内外部已经接下了不少业务,其中不乏有一些超大规模以及百万 QPS 级的业务,线上服务均稳定运行中。
后台回复 “KeeWiDB”,试试看,有惊喜。