盲猜google的Alloydb的实现
9月最后一天,快放假了,所以就无力再做工作的事情;所以就在下午的时候从自己的收藏夹(raindrop)找了几篇关于AlloyDB的文章,因为之前都是大概看一下,也没有特别思考各种细节,所以这次有点时间我就想搞搞清楚这个数据库的整体的实现细节;当然因为没有源码提供,所以很多的细节都是根据我这几年的经验来猜测的,所以不要当真,只看逻辑的是否走的通;
1. 相关资料
主要是几篇相关的文章:
- 从AlloyDb的架构能学到些什么
- 深度解析Google云新数据库产品AlloyDB
- Deep Dive into Google’s AlloyDB Architecture for PostgreSQL 可能需要翻墙才能看到,这是多么的尴尬,这文章的细节会更多一些
2. 对于文章的总结
聊技术细节之前,我们思考一下这个数据库的定位是怎么样呢?
我自己的理解: 定位应该是单机数据库的高级版本,主要在一定程度上解决数据库的容量问题的,当然也有一部分的性能问题;主打的市场是那种想无缝从单机的pg或者mysql迁移过来的用户;因为这个产品本身是100%兼容pg的,不需要任何修改的情况下就可以提供高可用和更大的容量;对应的竞争对手有:
因为没有仔细看过上面两个产品的论文,但是基本上在产品定位上是应该是一样的;对于此类产品的话,都是shared-disk这个阵营的;还有另外一个阵营就是shared-nothing,对应的产品国内有pingcap的tidb,google的spanner等等;主要的区别可以阅读这个文章,引用它的一个它的图:

按照产品的优缺点来说:
- aurora、alloydb、polardb:都是100%兼容各自的数据库,比如mysql或者pg之类的;用户可以原则上不费力的转移到这些平台上来的;缺点:有单机瓶颈;因为整体的逻辑依然是单master-多slave的架构,所以所有的写都会集中在master,那么就一定会存在上限,虽然目前磁盘理论上可以无限扩容,但是本身单机的master计算能力是有上限的,达到一定程度之后就遇到了瓶颈;
- tidb, spanner等这类数据库,相对于上面的,好处就是无限扩容的能力,通过分片的技术,将整个数据库的流量分散到各个分片上,这样的好处就是理论上的无限扩容的能力,没有所谓的瓶颈;坏处是:做不到所谓的100%无缝迁移,也就说业务方需要评估迁移成本,这个是很多用户上这个产品的忧虑点,有的时候还不一定能在开始的时候评估出来;还有一个就是分布式事务的性能问题,这个也是需要考虑的;
大概介绍了历史,下面我们就开始介绍关于Alloydb的一些细节问题;大部分介绍内容是来源于上面提到的几篇文章,我自己的主要的是对一些细节点的分析;
2.1 主体架构

这幅图来源于Deep Dive into Google's AlloyDB Architecture for PostgreSQL 后面的很多内容都可能与这个文章很类似,大家可以先看看这个原文,再继续读;这介绍alloydb算是很清晰的文章,推荐推荐;
主要的几个组成部分:
Low-latency regional log stroage: 低延迟日志存储系统;个人对这个理解应该是有一个能保证有序的持久化消息队列,存储的是wal的日志,那基本上必须要保证顺序性且不能丢数据;Log Processing Service: 日志处理服务,后面简称:LPS,主要的功能是消费Log storage存储的wal日志,然后wal日志apply之后更新底层的shared regional block storage; 当然在描述中它还会更新本地的pg的存储,也就是说LPS本身也支持pg类似的查询接口;Shared Regionl Block Storage: 看名字应该是一个对象存储服务;最终的数据会持久化到这一层,落到这层之后就保证不会被丢失;
几个数据流:
- 写流程包含了两个数据流:
primary --> replica传输wal日志,这个过程的主要的作用是管理在replica这段的缓存数据,比如有的数据如果修改了就进行cache失效,下次访问重新从底层获得;还有就是获得当前primary的已经被应用的LSN, 也就是当前primary已经被apply以后的最新的序列号的,这样下次查询的时候才能得到正确的数据;- 问题1: wal传输是否一定要成功;猜测大概率是异步的过程,可能会要求最终一定要成功,会有重试逻辑来保证,但是
replica是有可能接受不到最新的数据,那么这个时候replica如何在后期能知道自己的数据不够新呢? 这个时候获得的数据就stale;当然你读replica应该本身就会有这个预期,但是就看它什么时候能恢复回来;有两种方式:1. wal传输恢复,2. 当读replica的时候穿透cache之后达到LPS之后发现目前的replica的数据可能比较stale,然后有序的进行更新?这边就会遇到另外一个问题就是哪些数据需要更新哪些不需要更新?出现这个情况的原因是存在两个更新源,底层肯定更加准的,但是数据会慢,不知道它是如何克服这个问题;
- 问题1: wal传输是否一定要成功;猜测大概率是异步的过程,可能会要求最终一定要成功,会有重试逻辑来保证,但是
primary --> wal --> log storage: wal写入log storage之后,primary如何确定写入成功呢?当wal写入log storage之后,primary就可以回复client说写入成功,因为可以保证数据不会丢失了;log storage --> lps --> block service: lps会消费wal然后更新本地的数据,并且将数据块更新到远程对象存储;这边一定要注意的是:LPS只是缓存而已,并不会把所有的数据都存储起来的,主要是为了加速;
- 读流程:
- 每一个replica包含多个缓存,buffer cache + ultra-fast-cache, 如果查询能命中就不需要往底层访问了;这两个缓存可以认为类似于CPU L1/L2缓存;写流程中的直接从primary发送的wal的数据流本质上也是为了保证replica的缓存的有效性,如果primary更新的数据在cache,那么就让cache时效并且从底层数据中去获得的;这个部分是猜测的;
- 如果replica从L1/L2 cache中没有找到,就会到lps中来查询数据;每一个lps其实类似于一个PG的存储,提供pg的查询接口,这样通过lps就可以获得数据,当然如果lps也没有的话,lps会从底层的block storage中获得数据返回给replica,这个基本上是读的过程;
3. 问题和猜想
3.1 low latency regional log storage
首先google这个产品是跨多个az来保证高可用,
关于这个产品是如何做到在保证数据不丢的情况下保证低延迟的;思考为了保证功能基本上一定要做到
majority写,当然也可以是quorum;但是涉及到多az之间的数据通信,其实理论上不大可能做到低延迟;所以这块还是很好奇;因为这个组件本身的定位就是本地的wal,如果慢的话会导致性能变差的;关于这块我经验有限,猜不出来会怎么做?多az的
LPS消费log-storage会跨az吗?其实这个问题本质上还是在于log-storage的技术架构模型;猜测:目前看来log-storage本身是单写多读模型;而且消费过程对latency的要求没那么高,所以读可能读副本,也就是本az的副本,如何保证最终一致性呢,这个可能和细节有关,不确定;如果通过读副本来进行的话,那就表示副本可以延迟,但不能有错,不然lps恢复数据就有问题;
3.2 LPS
按照文章描述,数据底层存储是分片的;所谓分片将数据按照某一个规则,进行分割,从而达到高可用和高性能的要求;大部分数据库产品分片原则是range分片,比如按照primary-key的字典序列进行分割; 每一个分片我们后面就叫做shard, 一个shard只会有一个LPS来负责,LPS通常会负责多个shard的数据;LPS与shard之间的对应关系是动态的,通过一定方式来进行动态映射;即然有这层关系,如何避免多个LPS写同一个shard呢?
猜测:映射关系的维护估计会存在一个叫做meta-service服务, 用来管理这种映射关系; 当LPS因为假死或者其他的情况出现异常之后,如何保证不会出现多个
LPS写同一个shard的问题;
- 对于这个问题,首先我思考wal的写入应该是幂等的,即使出现了所谓的多次apply wal,我猜应该也没什么问题;
- 如何管理lps的生命周期呢?这个估计是通过租期的方式来进行管理,如果没有在一定时间内没有得到下一个租期就会自动退位;而针对在LPS期间sync数据到block-service,这个过程其实到没那么要求;因为都是通过wal数据来进行数据重建,所以理论上最终数据一定是一致的;
LPS通过不断的消费wal来重建数据,重建的数据可能会定期的同步到底层块存储; 而且LPS本身提供类似于PG的查询接口,但是LPS并不会把所有的数据存储在本地,它只是作为cache使用,如果什么都走block-service,那么速度上就比较缓慢;AlloyDB有很多层缓存,类似于CPU的cache一样;buffer-cache = L1, ultra-fast-cache = L2, LPS memory + 本地盘 = L3,而block-storage = 无限磁盘,通过这种方式来保证整体的高效和可扩展;
这边有一个细节问题是,LPS与block-service的数据同步是否会导致数据丢失的问题? 因为LPS的数据重建和同步block-service不可能是实时同步,及时在单机操作系统中,写ssd也不可能实时同步,因为latency和性能都会有影响;那就要面对如果LPS异常重启导致没有同步到底层block-service这种情况;
猜测: 这个问题的解决方法在于wal日志何时被删除的;记得每一个wal都会有一个LSN的序列号,通过这个序列号我猜可以定位到log-storage的wal日志;当wal被lps消费apply到本地存储之后,应该会保存自己消费记录,这个消费记录会保存在lps的服务中;但是当被flush到block-storage的时候,就会同步给meta-service说目前LSN之前的数据我已经同步到block-service,LSN之前的数据wal你可以删除了;当然删不删除再说;这个时候LPS宕机之后,后面新的LPS会接受这个shard,并且通过在meta-serive里面的LSN和底层block-service的数据,然后从LSN开始进行数据恢复,这样就保证了数据不丢失,但也不需要实时刷block-storage;
4. 其他
LPS和底层的block存储之间做了存储和计算分离;这种好处就是通过扩容LPS来获得更多的计算性能、内存缓存、本地高速盘之类的优势,而且LPS的切换几乎是很小成本的,恢复过程也非常的快,直接从block-service获得数据,然后从wal开始恢复数据,只需要很少操作就可以恢复;
面对负载不均衡的场景,比如单热点的话,可以通过在此分片+单独的LPS来负责单个shard的方式来增加性能,但是最终如果操作了一个LPS的极限的话,那该限流依然还是要限流的;
5. 最后
以上是我对AlloyDB的大概理解;目前我对云原生的数据库的实现的核心在于弹性伸缩上,目前的基本思路是:尽可能的所有功能都服务化,多机化;比如之前在操作系统中磁盘、内存、cpu是混合在一起的,在云原生中这些都是被分割的,每个功能都可以通过➕机器的方式来解决,比如alloydb的磁盘就是对象存储,计算就是LPS和上层的各个primary和replica,缓存可以是buffer-cache ultra-fast-cache和LPS内存、磁盘;这样可以避免相互影响,也可以单独扩容不会被各自限制;之前看到过一个云原生数据引擎的设置,里面有一点就特别吸引我,就是将compaction和真正做查询计算的节点进行分离,这样compaction就不会影响类似于cpu和io什么的,而且底层依赖对象存储,并行能力也很强,通过增加compaction节点,也就是更多的cpu资源可能达到之前在单机不可能达到的效果;这可能就是我目前理解的云原生;