图片不能显示时,请查看原文:https://lessisbetter.site/2019/11/21/fabric-orderer-architecture/
排序服务由一组排序节点组成,它接收客户端提交的交易,把交易打包成区块,确保排序节点间达成一致的区块内容和顺序,提供区块链的一致性服务。
图片源自《区块链原理、设计与应用》,当时Fabric还不支持raft
排序服务所提供的一致性,依赖确定性的共识算法,而非比特币、以太坊等公有链,所采用的概率性共识算法。确定性的共识算法是区块上链,即不可修改。Fabric所采用的共识算法有Solo、Kafka、EtcdRaft。
客户端通过Broadcast接口向Orderer提交背书过的交易,客户端(此处广义指用户客户端和Peer节点)通过Deliver接口订阅区块事件,从Orderer获取区块。
更多的排序服务介绍请参考这篇官方文档排序服务。
本图依赖 Fabric 1.4 源码分析而得
Orderer由:多通道、共识插件、消息处理器、本地配置、区块元数据、gRPC服务端、账本等组成,其中gRPC中的Deliver、Ledger是通用的(Peer也有),其余都是Orderer独有的。
Fabric 支持多通道特性,而Orderer是多通道的核心组成部分。多通道由Registrar、ChainSupport、BlockWriter等一些重要部件组成。
Registrar是所有通道资源的汇总,访问每一条通道,都要经由Registrar,更多信息请看Registrar。
ChainSupport代表了每一条通道,它融合了一条通道所有的资源,更多信息请看ChainSupport。
BlockWriter 是区块达成共识后,Orderer写入区块到账本需要使用的接口。
Fabric的共识是插件化的,抽象出了Orderer所使用的共识接口,任何一种共识插件,只要满足给定的接口,就可以配合Fabric Orderer使用。
当前共识有3种插件:Solo、Kafka、EtcdRaft。Solo用于实验环境,Kafka和EtcdRaft用于生产环境,Kafka和EtcdRaft都是CFT算法,但EtcdRaft比Kafka更易配置。
EtcdRaft实在Fabric 1.4开始引入的,如果之前的生产环境使用Kafka作为共识,可以遵循Fabric给的指导,把Kafka共识,迁移到Raft共识。
Orderer只有2个gRPC接口:
其中,Broadcast是Orderer独有的,而Devliver是通用的,因为客户端也可以利用Deliver接口从Peer节点获取区块、交易等。
关于Broadcast和Orderer更多介绍可以参考杨保华的2篇笔记:
用来解析orderer节点的配置文件: orderer.yaml
,并保存入内存。
该配置文件中的配置,是节点本地的配置,不需要Orderer节点间统一的配置,因此不需要上链,相关配置有:
而上链的配置,被称为通道配置,需要使用配置交易进行更新,这部分配置,写在configtx.yaml
中,和Orderer相关的有:
区块中有4个元数据:
区块Header中记录了Data.Hash(),Data是所有交易后序列化的结果,但不包含区块元数据,所以区块元数据是可以在产生区块后修改的。即,即使元数据上链了,但这数据是可以修改的,只不过修改也没有什么意义。
orderer收到交易后需要对交易进行多项检查,不同的通道可以设置不同的MsgProcessor,也就可以进行不同的检查。
当前Processor分2个:
StandardChannel会对交易进行以下检查:
SystemChannel只比StandardChannel多一项:系统配置检查,用来检查以下交易中包含的配置,配置项是否有缺失,或者此项配置是否允许更新等。
BlockCutter用来把收到的交易分成多个组,每组交易会打包到一个区块中。而分组的过程,就是切块,每组交易被称为一个Batch,它有一个缓冲区用来存放待切块交易。
切块有3个可配置条件:
切块有1个不可配置条件:
超多刚接触Fabric的人有这样一个疑问:排序节点是按什么规则对交易排序的?
按什么顺序对交易排序并不重要,只要交易在区块内的顺序是一致的,然后所有记账节点,按交易在区块内的顺序,处理交易,最后得到的状态必然是一致的,这也是区块链保持一致性的原理。
再回过头来说一下实现是什么顺序:哪个交易先写入BlockCutter的缓冲区,哪个交易就在前面,仅此而已。
Orderer的BlockWriter是基于common/ledger实现的,它用来保存区块文件,不包含状态数据库等其他数据库,其中有3类区块文件:ram,json和file,file是Orderer和Peer都可使用的,另外2个只能Orderer使用。
BlockWriter用来向Peer的账本追加区块,但追加区块之前,还需要做另外1件事情,设置区块的元数据。
区块元数据包含:
但此时只设置其中的3个:区块签名、配置区块高度、orderer相关的元数据。因为交易的有效性在记账节点检查后才能设置。
为何不在创建区块的时候就设置这些元数据信息,而是在区块经过共识之后?
共识的过程会传播区块,只让区块包含必要的信息,可以减少区块大小,降低通信量。但元数据占用大小非常小,所以这未必是真实原因。
BlockWriter还有另外一个功能:根据一个Batch创建下一个高度的区块。一个区块包含了:
Header只记录Data的哈希值,不包含Metadata哈希值,这样的目的是,在区块创建之后,仍能修改区块。
根据Fabric 1.4源码梳理Orderer启动步骤:
启动流程图可请参考杨宝华的笔记Orderer 节点启动过程,笔记可能是老版本的Fabric,但依然有参考价值。
交易是区块链的核心,交易在Orderer中的流程分3阶段:
上面提到Orderer和共识实例分别会对交易进行2次检查,这些检查是相同的,为何要进行两次检查呢?
代码如下:ProcessMessage 会调用ProcessNormalMsg
,对交易进行第一次检查,如果有错误,会向客户端返回错误响应。 SomeConsensurFunc 是一个假的函数名称,但3种共识插件实现,都包含相同的代码片,当消息中 configSeq < seq 时,再次对交易进行检查,如果错误,则丢次此条交易。configSeq是Order函数传入的,即第一次检查交易时的配置号,seq为共识当前运行时的配置号。
func (bh *Handler) ProcessMessage(msg *cb.Envelope, addr string) (resp *ab.BroadcastResponse) {
// ...
configSeq, err := processor.ProcessNormalMsg(msg)
if err != nil {
logger.Warningf("[channel: %s] Rejecting broadcast of normal message from %s because of error: %s", chdr.ChannelId, addr, err)
return &ab.BroadcastResponse{Status: ClassifyError(err), Info: err.Error()}
}
// ...
err = processor.Order(msg, configSeq)
// ...
}
func SomeConsensurFunc() {
// ...
if msg.configSeq < seq {
_, err = ch.support.ProcessNormalMsg(msg.normalMsg)
if err != nil {
logger.Warningf("Discarding bad normal message: %s", err)
continue
}
}
// ...
}
我认为如此设计的原因,考量如下:
共识插件应当尽量高效,orderer尽量把能做的做掉,把不能做的交给共识插件,而交易检查就是orderer能做的。共识插件只有在排序服务配置更新后,才需要重新检查交易,以判断是否依然满足规则。排序服务的配置通常是比较稳定的,更新频率很低,所以进行2次校验的频率也是非常低。这种方式,比只在共识插件校验,会拥有更高的整体性能。
配置交易可以用来创建通道、更新通道配置,与普通交易的处理流程总体是相似的,只不过多了一些地方或者使用不同的函数,比如:
上面2中流程都是与具体共识算法无关的,这里补充一个Raft共识的。
使用Raft共识的链处理交易包含了上图中的4步:
如果把图中提到的:转发和Raft去掉,就是以Solo为共识的链的过程。
下图是更加细化一层的,如果看不懂,建议先读下Etcd Raft架构设计和源码剖析2:数据流这篇文章。
红色圈出来的是etcd/raft的实现,蓝色圈出来的是Fabric使用raft为共识的部分,外面的Broadcast、Deliver是属于Orderer但不属于某条链。
这张图和etcd与raft交互没有太多不同,只有2个地方:
Orderer的代码位于fabric/orderer
,其目录结构如下,标注了每个目录结构的功能:
➜ fabric git:(readCode) ✗ tree -L 2 orderer
orderer
├── README.md
├── common
│ ├── blockcutter 缓存待打包的交易,切块
│ ├── bootstrap 启动时替换通道创世块
│ ├── broadcast orderer的Broadcast接口
│ ├── cluster (Raft)集群服务
│ ├── localconfig 解析orderer配置文件orderer.yaml
│ ├── metadata 区块元数据填写
│ ├── msgprocessor 交易检查
│ ├── multichannel 多通道支持:Registrar、chainSupport、写区块
│ └── server Orderer节点的服务端程序
├── consensus 共识插件
│ ├── consensus.go 共识插件需要实现的接口等定义
│ ├── etcdraft raft共识插件
│ ├── inactive 未激活时的raft
│ ├── kafka kafka共识插件
│ ├── mocks 测试用的共识插件
│ └── solo solo共识插件
├── main.go orderer程序入口
├── mocks
│ ├── common
│ └── util
└── sample_clients orderer的客户端程序样例
├── broadcast_config
├── broadcast_msg
└── deliver_stdout
23 directories, 3 files
阅读Orderer源码,深入学习Orderer的时候,建议以下顺序:
本文从宏观的角度介绍了Orderer的功能、核心组成,以及交易在Orderer中的流程,Peer如何从Orderer获取区块。