本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
引言
这篇文章是对OSDI 2020 Virtual Consensus in Delos 的论文演讲以及论文的学习与思考,这篇文章中提出一种称为虚拟共识分层的思想。
Delos
首先我们来了解下Delos出现的背景。
Facebook的软件栈分为数据平面与控制平面,控制平面需要处理大量的数据,对于控制平面来说,存储系统需要满足以下需求:
- 从接口的角度来说需要支持事务,二级索引,范围查询
- 从容错的角度来说需要零依赖,可持久化,高可用
但是Facebook已有的系统并不能解决以上的问题:
- MySQL:API可以满足已有需求,但是不支持容错。
- ZooKeeper:零依赖,容错,但是API表达能力较弱。
想要去修改这两个东西的源码使得其支持另外一个特性显得不太现实,因为这两个组件可以说已经非常成熟了,再重新开发成本过于高,而且项目面临的deadline非常紧,以上问题的解决方案就是Delos。
它可以解决几个重要的问题:
- 对应用程序透明,应用程序应该是未经修改的,并且不考虑日志的虚拟化性质。这使得已有代码不需要修改。
- 应该允许底层日志简单多样,降低新日志实现的障碍。这使得项目可以迅速上线,后面再迭代日志的实现。
- 允许在实现之间进行迁移,而不需要停机。这使得我们可以不停机替换/升级底层实现。
- 抽象出共享日志层,使得对于上层来说表达能力较强,实现不同的API只需要一层简单的包装。
细节
首先基本的介绍内容可以参考[1][3],我在开始时也是看这两个链接学习的,但是对于其中有些细节不太理解,遂翻阅了论文,接下来的内容希望能以我的理解走一遍整个系统
首先我们来看看整个Delos的架构图:
在每个Delos服务器上逻辑分为三层:
- 顶部是一个特定于API的包装器。
- 中间Delos runtime是一个SMR(state machine replication)接口,其在运行时与VirtualLog交互。
- VirtualLog下的单个loglet。负责实际的物理日志。
这种分层设计提供了二维的可扩展性。首先,Delos可以在VirtualLog下支持多个Loglet;其次,Delos可以在单个平台上支持多个面向应用程序的API,每个API Wrapper都是一层很少的代码,它与Delos运行时交互,并提供针对RocksDB的序列化逻辑。
事实上基于日志提供多个API接口并不少见,大多数状态机复制库都将应用程序视为一个黑盒。所以基于此我们可以提供很丰富的API接口。而且Delos还在公共核心中提供了更强大的应用程序无关功能子集,包括本地持久存储(RocksDB)、备份和新服务器加入时的状态传输(MetaStore)。
现在我们大概知道了Delos每层到底做了什么,让我们再细化一点去看这个过程,LogLet和VirtualLog分别提供了不同的接口:
ILoglet:
append
:追加一个条目,返回在日志中的位置。checkTail
:得到所有LogLet中seal状态以及当前的tail。readNext
:为来自同一客户端的上一个checkTail调用返回值之前的日志位置定义。实现相对简单,首先检查本地并置的LogServer以查找条目。如果在本地找不到条目,它将向其他日志服务器发出读取。读取不需要法定人数,因为我们已经知道条目已提交,我们只需要找到一份副本。prefixTrim
:指示应该删除哪些数据。seal
:密封一个LogLet,可以保证后面的对于此logLet的append操作失效。
IVirtualLog:
reconfigExtend
:更改VirtualLog的active segment,以便将新的append定向到不同的Loglet。reconfigTruncate
:用于从链中移除第一个seal段,当VirtualLog的地址空间被修改时调用。reconfigModify
:修改seal段,比如替换密封日志中的故障服务器。
这些接口的功能从上面的架构图看的话分别由LogLet和core层提供,显然我们实现的LogLet需要实现前五个接口,从论文中我们可以看到目前LogLet已有的实现如下:
这里需要提一下,看论文之前我一直在思考的问题是ZK,Multi-Paxos这样的日志实体如何做到把容错和append分开,也就是使用MetaStore来负责容错以及使用LogLet来实现append呢,从论文中才看到其实ZK,Backup这样的LogLet其实不支持converged模式且支持容错,也就是其实这样自带一致性协议的系统可以直接使用其自带的容错功能,MetaStore只负责LogLet的迁移。
可以看出其实基本的append已有包含容错的系统已经实现,所以对于这些只需要其实现LogLet剩下几个接口就可以了,MetaStore只负责LogLet的迁移。但是如果要实现类似于NativeLoglet
这样本身不支持容错的系统,除去基本操作的实现,我们则可以利用MetaStore来实现容错,
但是必须根据某些策略来调用它。reconfiguration有三个主要驱动因素:
- 重新配置,比如升级到更快的Loglet,这由操作员通过命令行工具驱动。
- VirtualLog在为prefixTrim提供服务时会修剪其第一个密封Loglet的全部内容,其在自身上调用reconfigTruncate。例如,如果应用程序调用链[0->A->100->B->♾️ ]上的prefixTrim(100);VirtualLog修剪所有A,然后重新配置为链[0->B->♾️ ]。
- 不实现自己的领导人选举或重新配置的单个loglet负责检测故障并在VirtualLog上请求reconfigExtend。以NativeLoglet举例(可以看看[3]),当sequencer或其中一个日志服务器出现故障时,NativeLoglet负责检测此故障并调用VirtualLog上的reconfiguration(这反过来会将其seal并切换到新的NativeLoglet)。在我们的实现中,我们结合使用
in-band detection
(例如,sequencer检测到它已重新启动,或其他服务器持续停机)和out-of-band signals
(通过基于gossip-based
的故障检测器以及来自container管理器的信息)来触发reconfiguration(MetaStore中数据的改变)。换句话说,VirtualLog提供了reconfiguration/领导人选举机制,而NativeLoglet通过选择新NativeLoglet实例的LogServers和sequencer来处理这个操作。
一般切换LogLet的流程如下:
- 把当前的LogLet seal掉。
- 然后把最新的chain写入MetaData中。
- 然后从MetaStore中拿到最新的chain结构。(第二步可能并发进行,有些client可能会失败)。
对于LogLet至此我们已经简单的理解了。LogLet全部都需要支持一般的接口,如果没有自己的容错机制,就可以依赖与MetaData去实现一个reconfiguration。如果有的话只需要提供正常的接口来支持配置改变了。
接下来我们看看VirtualLog的抽象是怎么做的。VirtualLog面临的主要技术挑战是为客户端提供一个共享的、高度一致的、高可用的虚拟地址空间。在正常情况下,当链保持不变时,这很简单:客户端层可以使用其本地缓存的链副本来路由操作。但是,可以通过前面展示的VirtualLog reconfigExtend API更改该链,这也意味着在VirtualLog这一层我们需要做容错。
当我们实现了容错的时候,使用seal链对VirtualLog进行的append将被路由到其最后一个Loglet,该Loglet现在在新链中。因此,在旧链中发出appends的客户机将获得一个错误代码,指示Loglet已seal;然后,它将从MetaStore中获取最新的链并重试。这其实也是操作实现容错的机制。
其实还有一个问题值得思考,就是为什么Delos要抽象出一个MetaStore来存储版本已实现容错呢?原因在论文中给出了答案:
Why does the VirtualLog require its own MetaStore? Existing reconfigurable systems often store similar information (e.g., the set of servers in the next configuration) inline with the same total order as other commands (within the last configuration [34] or a combination of the old and new configurations [39]). In this case, the steps of sealing the old chain and writing the membership of the new chain can be done in a single combined operation. In the VirtualLog, this would be equivalent to storing the identity of the next Loglet within the current active Loglet while sealing it. However, such a design requires the Loglet itself to be highly available for writes (i.e., implement fault-tolerant consensus), since reconfiguring to a new Loglet would require a new entry to be written to the current Loglet. With a separate MetaStore, we eliminate the requirement of fault-tolerant consensus for each Loglet. Since one of our design goals is to make Loglets simple and diverse, we choose to use a separate MetaStore.
为什么VirtualLog需要自己的MetaStore?现有的reconfigurable系统通常以与其他命令相同的总顺序(在上一次配置中或新旧配置的组合)存储类似的信息(下一次配置中的服务器集合)。在这种情况下,密封旧链和写入新链成员的步骤可以在单个组合操作中完成。在VirtualLog中,这相当于在当前的active Loglet中存储下一个Loglet的标识,同时将其密封。然而,这样的设计要求Loglet本身具有很高的写入可用性(实现容错一致性),因为重新配置到新的Loglet将需要向当前Loglet写入一个新条目。通过一个单独的元存储,我们消除了每个Loglet的容错一致性要求。由于我们的设计目标之一是使loglet简单多样,因此我们选择使用单独的元存储。(只能说TMD巧妙,而且也不是强依赖,LogLet本身实现了容错的话也可以使用自己的,这其实更高效)
总结
好了,简单的阐述完成了,细节还是看论文吧。简单来回顾下如果实现一个Delos我们拥有了什么,我们可以在不改变应用接口的情况下拥有一个统一的存储平台,其可以不停机更换底层一致性协议(已适应不同的负载);且用户不需要修改代码;基于Share Log的设计也使得API表达能力非常强大。
后面设计系统时是否可以考虑引入这样的中间层呢,其实我个人认为可以,可以在提前可预知的高负载场景是替换为更快的LogLet,比如ZK;在低负载时改变为HDFS这样的系统,以降低成本;甚至于后面可以把已有的一致性协议进行替换,比如现在用Raft,后面替换为E-Paxos,从协议的角度来提升性能,且对于整个系统而言开发工作很少。
其次我认为,后面Delos可能会出现基于负载自动调节不同的LogLet这样的技术出现,那个时候可能又是一个技术的浪潮吧。
最后,分层细粒度管理的思路这两年还真是火啊。
参考: