在Hyperledger Fabric最新发布的1.0版本里,分拆出来Orderer组件用于交易的排序及共识。现阶段提供solo及kafka两种方式的实现。solo模式不用多讲,即整个集群就一个Orderer节点,区块链的交易顺序即为它收到交易的顺序。而kafka模式的Orderer相对较复杂,在实现之初都有多种备选方案,但最终选择了现在大家所看到的实现方式。那么其中的选型过程是怎么样的呢?我想将开发者Kostas Christidis的设计思路给大家解析一番,既是翻译也是我自身的理解。

原文:A Kafka-based Ordering Service for Fabric

Kafka模式的Orderer服务包含Kafka集群及相关联的Zookeeper集群,以及许多OSN(ordering service node)。

osn

ordering service client可以与多个OSN连接,OSN之间并不直接通信,他们仅仅和Kafka集群通信。

OSN的主要作用:

我们都知道,Messages(Records)是被写入到Kafka的某个Topic Partition。Kafka集群可以有多个Topic,每个Topic也可以有多个Partition。每个Partition是一个排序的、持久化的Record序列,并可以持续添加。

假设每个channel有不同的Partition。那么OSNs通过client认证及transaction过滤之后,可以将发过来的transaction放到特定channel的相关Partition中。之后,OSNs就可以消费这些Partition的数据,并得到经过排序后的Transaction列表。这对所有的OSN都是通用的。

osn

在这种情况下,每一TX都是不同的Block。OSNs将每一个TX都打包成一个区块,区块的编号就是Kafka集群分配给TX的偏移编号,然后签名该区块。任意建立了Kafka消费者的Deliver RPCs都可以消费该区块。

这种解决方案是可以运行的,但是会有以下问题:

osn

osn

这就意味着Kafka的偏移编号不能和OSN的区块编号对应起来,所以也需要建立区块编号到偏移量的lookup表。

osn

如果我们选举Leader OSN,它负责将区块写入到Partition1呢?有几种方法选举Leader:可以让所有的OSNs竞争ZooKeeper的znode,或者第一个发送TTC-X消息到Partition0的OSN。另外一个有趣的方法就是让所有的OSNs都属于相同的Kafka消费者组,意味着每笔交易只会被消费一次,那么无论哪个OSN消费了该交易,都会生成相同的区块序列。

如果Leader发送区块X消息,消息还未到达Partition1时Leader崩溃了,这时候会如何呢?其他OSNs意识到Leader崩溃了,因为Leader已经不再拥有znode,这时候会选举新的Leader。这时候新的Leader发现区块X还在他这里,还没有被发送到Partition1,所以他发送区块X到Partition1。同时,旧Leader的区块X消息也发送到了Partition1,消息又冗余了。

osn

我们可以使用Kafka的日志压缩功能。

osn

如果我们启用日志压缩,我们完全可以删除所有的冗余消息。当然我们假设所有的区块X消息拥有相同的key,X不同时,key也不同。但是因为日志压缩保存的是最新版本的key,所以OSNs可能会拥有陈旧的lookup表。假设上图的中key对应的是区块。OSN收到的前两个消息在本地的lookup表中有映射关系,同时,Partition被压缩成上图下方的部分,这时候查询偏移0/1会返回错误消息。另外一个问题就是Partition1中的区块不能逆向存储,所以Deliver逻辑同样复杂。事实上,仅仅考虑到lookup表的过期问题,日志压缩就不是一个好的方案。

所以没有一个很好的解决方案解决这个问题,我们回到问题5,创建另一个Partition1,解决重复分割、签名block的问题,我们可以摒弃这种方案,让每个OSN在本地保存每个channel的区块文件。

osn

Delivery请求现在只需要顺序的读取本地ledger,没有冗余数据,没有lookup表。OSN只需要保存最后读取的偏移量,这样在重联之后就可以知道从哪里开始重新消费Kafka的消息。

一个缺点可能就是比直接通过Kafka提供服务慢,但是我们也从来不是直接从Kafka提供服务,本来就有一些操作需要OSNs本地进行,比如签名。

综上,ordering 服务使用一个单Partition(每channel)接收客户端的交易消息和TTC-X消息,在本地存储区块(每channel),这种解决方案能够在性能和复杂度之间取得较好的平衡。