对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时候对资源的开销,让用户使用体验非常好。
文章评论