More than code

More Than Code
The efficiency of your iteration of reading, practicing and thinking decides your understanding of the world.
  1. 首页
  2. 未分类
  3. 正文

Amazon MemoryDB Notes

2024年10月2日 326点热度 2人点赞 0条评论

对MemoryDB的paper做一些总结

要了解做MemoryDB的目的,需要先明白原本的redis有哪些问题,那么首先要了解下redis

摘自原文中的redis的一些特点:
* redis支持200+ commands,10种数据结构,包括hash table, sorted sets, stream, hyperloglogs等
* command可以被组合起来成为group,并有原子执行的能力
* redis支持水平拓展,通过crc16把key space编码成16384个slot。并把这些slot分布到若干个shard中
* 支持slot-level migration
* 主从复制的方法:mutating command会先更新primary上的数据结构,然后异步复制到replica中。(应该是)串行复制,所以replica中的数据是具有consistent view的,但是可能是旧的点
* (这个是我补充的):redis的ckpt机制是:全量+增量,收到写入请求后,command会被持久化到一个append only file中,作为log存在,recover的时候会读。全量的ckpt则是叫做snapshot,是用COW机制fork一个子进程出来,然后在子进程中把数据库的所有状态持久化下来,作为快照保存,这样就可以释放snapshot之前的日志了。

然后看一下redis的问题:
* 在主从复制的场景,redis用了quorom based协议来做故障探测和选主。但是因为主从复制是异步的,所以redis的选主不保证新的主还有之前的所有数据,也就是说发生切主的时候可能数据会出现丢失。
* 另一个问题是,选主的时候没有机制保证选出来的主是具有最新数据的主。最差情况下,redis可能选出来的主上面没有任何的数据
* redis支持WAIT command,可以阻塞用户,直到之前所有的mutating command都复制到了指定个数的replica上。这里的问题是虽然保证了写者可以读到最新的数据。但是其他并发的读者可能已经读到了从节点上的新写入的数据,然而这个新写入的数据不一定可以在failure之后还存在(也就是可能出现读摇摆问题)

MemoryDB这里要解决的问题就是,让redis成为一个primary database(解决数据丢失以及不一致的问题),同时具有multi-AZ durability


MemoryDB的架构如图所示:
* 解决consistency/durability的核心思路是用一个跨AZ的,强一致的transaction log service来解耦redis的内存层和持久化相关的事情。
* 一个特点是,MemoryDB的开销主要来自于数据库的大小,因为需要全放入到内存中,所以需要比较多的钱花在memory中。那么负责持久化的transaction log吃的磁盘开销占比也就少了很多了。

看到这里的时候我想吐槽下,paper里说他们决策使用write-ahead logging或者write-behind logging,选择了WBL,是因为WBL是在执行op后才产生的,更加契合redis原本的流程。
* WAL说的只是page写入前,对应page上的redo log要下刷,和这里完全不是一回事。
* 执行op后产生的log可以更好的支持non-deterministic command。原文中叫:replicating the effect of command instead of the original command

写入的流程上,因为是先改了primary的结构,再写的transaction log service,所以加了一层reply tracker,用来阻塞client直到transaction log service的回复写入成功,才能回复给client。
* 这里带来一个细节上的设计问题,transaction log service如果回复写入失败怎么办?因为内存已经改了。对应其实AWS Aurora也有类似的问题。
* 个人感觉这里的做法就是无限重试,直到写入成功。或者就是降级primary,清理内存状态,然后在一个新的地方恢复数据库。
* 这种里的client tracker还有另一个问题,是如果读请求读到了新写入的数据,但是还没有提交的话,也需要block在这里。
* 所以个人感觉,这里是一个key级别的tracker,当txn log service回复后,把对应key上的人唤醒。
* 估计还需要处理写写冲突,所以要唤醒哪些读者也需要比较细节的处理。key上做排队什么的。
* memorydb的paper中说他们是强一致的,这里还有一个小细节没有提,就是下层的txn log service只知道log什么时候复制成功了,但是不清楚replica什么时候读到了日志,以及把mutation apply到了内存中。
* 所以要做到真的强一致,还需要replica上做一些ack的操作。
* 再考虑多一点,还需要做follower上的lease维护什么的,来保证切主的时候的强一致。
* 这么看感觉就是把raft相关的一堆东西拿过来了。或者在txn log service上注册一堆callback做?
* 再想一下,这种基于shared storage的系统做强一致的话,应该要么是类似polardb那种,RO去拉最新的数据,要么是延迟等待RW的写入,这个应该是主动再redis中引入一个WAIT command来做

基本架构差不多就是这样,通过txn log service和reply tracker保证了强一致以及数据的持久性,剩下的一个问题就是leader election要保证选出拥有最新数据的主了:
* txn log service支持conditional append,也就是支持类似cas的语意 ,指定前一个的log,然后append后一个。那么当一个旧的replica没有消费最新数据的话,他所指定的前一个log就不是日志流中最新的那个日志,此时的append就会被txn log service拒绝。只有看到所有日志的replica,才能append成功。
* 这种选主机制的另一个好处是每一个replica只需要和txn log service交互就可以选主,不需要和其他的replica交互。可用性上会更好一点。个人感觉也更简单
* 有了这个选主机制,还需要一个故障探测的机制来确定什么时候leader跪了。这个是通过leader周期性的写一条lease的log来做的,follower看到了这个lease就会reset timer,等着下一次的抢主。


然后看recover的流程:
* memorydb提到了一个特别的点是,redis本身的snapshot机制会影响primary的性能,因为要涉及到fork子进程,极端情况下可能让内存占用double一份。如果为了这个snapshot机制来让用户预留双倍的资源的话,资源利用率就太低了。
* 利用上述强一致的性质,我们可以拉起一个不丢数据的follower节点,让他去做snapshot,然后把snapshot上传到S3中
* recover的时候,就可以先从S3中下载最新的数据,然后从txn log service中把增量的数据追上即可。为了避免recover过久,memorydb会限制txn log的长度,不过具体的细节就没有提了。


最后是一些运维管控相关的事情:
* 多个memorydb会共享一个控制面,用来做升级/扩容。recovery也是通过控制面来控制的。所以这里的控制面有点集群的metaserver的感觉,不过也会协调升级。
* 升级/实例规格变更都是通过N+1 滚动升级做的,先升级一台新的,然后再去降级旧的节点,保证任何时候都有N台机器服务。
* memorydb会控制让rw最后升级,防止写入旧版本无法识别的日志。
* 这里的一个兜底手段是在日志流中增加version,当follower读到了version更新的数据的时候,会停止消费日志。
* slot migration应该没有什么特殊的,会先类似replication,启动一个特殊的单个slot级别的复制流,让目标的shard去追数据。在追成功后,会卡住primary的写入,然后把目标的shard追到最新,并写入一个slot ownership transfer log。日志写入后,然后新的shard就可以接收写请求了。

最后面memorydb还提到了他们是怎么做正确性验证的:
* 引入了特殊的语言去做,类似TLA+,他们这个叫P。
* 还有一个consistency testing框架,叫做porcupine,是一个linearizability checker,给定一个client command的序列,输出这个序列是不是linearizable的。
* 另外还有一个我比较关注的,应该是用来处理data corruption的:
* 因为每次写snapshot都是全量的数据,一旦出现了内存错误,snapshot写坏了,就会导致整个数据库不可用。
* memorydb的snapshot中会维护data自身的checksum,以及snapshot对应的txn log的checksum。txn log自身也会带checksum。restore的时候,会先校验snapshot自身的数据有没有坏,然后用snapshot上记录的txn log的checksum作为基础,在tail log的时候计算新的checksum,并与txn log中记录的checksum进行校验。
* 这里比较奇怪的一点是,这套机制貌似只能校验txn log的正确性,感觉不是非常的够用。
* 个人感觉做的比较好一点是:
* 如果可以的话,动态的维护数据库的校验码:
* 如果可以把数据库看成一堆kv的话,数据库的校验码可以是这些kv的checksum的xor值。那么delete就是去xor一个要删掉的kv,insert也是xor要insert的kv。
* 当然如果对于比较复杂的场景自然就不是这样了。
* 写snapshot的时候,重新计算snapshot的校验码,和动态维护的做校验,保证内存状态正常。序列化后的snapshot可以再次计算做校验,保证snapshot创建的过程是没问题的。这样就可以保证snapshot数据一定正确。
* txn log的话,单独维护每一条log的checksum应该就够了。如果做的更好一点,可以再动态维护校验码,并和存储在txn log中的校验码去比对。
* 所以我感觉这里的主要问题是,memorydb搞了两个checksum,校验的能力就没有那么强了。

最后简单说两句吧:
* 可以看出来memorydb对于redis本身的内核改造是相对比较少的(除了我不太了解redis的源码,不清楚他们那个reply tracker改的怎么样)。这里面向的场景也就是全内存的场景,没有考虑做非全内存的redis。然后把主要的精力放到了一致性/持久性上,而没有为了支持非全内存去修改redis的代码。个人感觉是一个非常值得借鉴的做法。
* 还投入的精力去做数据的校验,代码的校验,可以看出来AWS打磨产品还是非常用心的。说出去的强一致就会用形式化工具去验证。
* off-box snapshot的设计让人感觉眼前一亮,打破了一个以往大家都会觉得checkpoint这种东西只能是leader做,follower不能做的认识。第二点是考虑到了用户在使用snapshot时候对资源的开销,让用户使用体验非常好。

标签: 暂无
最后更新:2024年10月2日

sheep

think again

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 heavensheep.xyz. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS