# 盲猜google的Alloydb的实现

9月最后一天，快放假了，所以就无力再做工作的事情；所以就在下午的时候从自己的收藏夹(raindrop)找了几篇关于AlloyDB的文章，因为之前都是大概看一下，也没有特别思考各种细节，所以这次有点时间我就想搞搞清楚这个数据库的整体的实现细节；当然因为没有源码提供，所以很多的细节都是根据我这几年的经验来猜测的，所以不要当真，只看逻辑的是否走的通；

### 1. 相关资料

主要是几篇相关的文章:

* [从AlloyDb的架构能学到些什么](https://mp.weixin.qq.com/s/z2kqY2RNYZ77bD0xJq0I_w)
* [深度解析Google云新数据库产品AlloyDB](https://mp.weixin.qq.com/s?__biz=MzkxODMzMjk1Ng==&mid=2247484198&idx=1&sn=f7f55f2e1331f7c2c0c8459ff07d7b2b&scene=58&subscene=0)
* [Deep Dive into Google’s AlloyDB Architecture for PostgreSQL](https://cyral.com/blog/deep-dive-into-googles-alloydb-architecture-for-postgresql/?utm_content=216348954&utm_medium=social&utm_source=twitter&hss_channel=tw-1017627435112644608) **可能需要翻墙才能看到,这是多么的尴尬，这文章的细节会更多一些**

### 2. 对于文章的总结

聊技术细节之前，我们思考一下这个数据库的定位是怎么样呢？

> 我自己的理解: 定位应该是单机数据库的高级版本，主要在一定程度上解决数据库的容量问题的，当然也有一部分的性能问题；主打的市场是那种想无缝从单机的pg或者mysql迁移过来的用户；因为这个产品本身是100%兼容pg的，不需要任何修改的情况下就可以提供高可用和更大的容量；对应的竞争对手有：

* [aws aurora](https://aws.amazon.com/cn/rds/aurora/)
* [aliyun polardb](https://www.aliyun.com/product/polardb)

因为没有仔细看过上面两个产品的论文，但是基本上在产品定位上是应该是一样的；对于此类产品的话，都是shared-disk这个阵营的；还有另外一个阵营就是shared-nothing，对应的产品国内有pingcap的tidb，google的spanner等等；主要的区别可以阅读这个[文章](http://www.benstopford.com/2009/11/24/understanding-the-shared-nothing-architecture/),引用它的一个它的图:
![image-20221003201902639](https://cdn.nomoshen.com/typora_img/image-20221003201902639.png)

按照产品的优缺点来说：

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

大概介绍了历史，下面我们就开始介绍关于Alloydb的一些细节问题；大部分介绍内容是来源于上面提到的几篇文章，我自己的主要的是对一些细节点的分析；

#### 2.1 主体架构

<img src="https://cdn.nomoshen.com/typora_img/image-20221003211439226.png" alt="image-20221003211439226" style="zoom:50%;" />

这幅图来源于[Deep Dive into Google's AlloyDB Architecture for PostgreSQL ](https://cyral.com/blog/deep-dive-into-googles-alloydb-architecture-for-postgresql/?utm_content=216348954&utm_medium=social&utm_source=twitter&hss_channel=tw-1017627435112644608) 后面的很多内容都可能与这个文章很类似，大家可以先看看这个原文，再继续读；这介绍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，然后有序的进行更新？这边就会遇到另外一个问题就是哪些数据需要更新哪些不需要更新？出现这个情况的原因是存在两个更新源，底层肯定更加准的，但是数据会慢，不知道它是如何克服这个问题;
    * `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资源可能达到之前在单机不可能达到的效果；这可能就是我目前理解的云原生；

