0%

字符串格式日期、time.Time类型、整形时间戳三者之间的转换如下图:

有2点要注意:

  1. 字符串日期和时间戳之间不能直接转换,需要通过time.Time完成。
  2. 涉及字符串日期的时候,字符串日期格式一定要以Go诞生的时间为基准,而不是随意的时间,否则会导致时间转换不正确。所以,以下Demo中的日期格式是通用的。
  3. 字符串日期格式要与真实的日期格式完全匹配,否则会解析时间不正确。比如设置的格式为2006-01-02,实际日期格式为2006-1-2时会解析错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"time"
)

func Date2Time() {
fmt.Println(">> Date2Time")
defer fmt.Println("<< Date2Time")

// 一定要以Go诞生的时间为基准
// 2006年1月2号,MST时区,下午3:04分为基准
const dateFormat = "Jan 2, 2006 at 3:04pm (MST)"
t, _ := time.Parse(dateFormat, "May 20, 2020 at 0:00am (UTC)")
fmt.Println(t)

const shortForm = "2006-Jan-02"
t, _ = time.Parse(shortForm, "2020-May-20")
fmt.Println(t)

t, _ = time.Parse("01/02/2006", "05/20/2020")
fmt.Println(t)
}

func Time2Date() {
fmt.Println(">> Time2Date")
defer fmt.Println("<< Time2Date")

tm := time.Now()
fmt.Println(tm.Format("2006-01-02 03:04:05 PM"))
fmt.Println(tm.Format("2006-1-2 03:04:05 PM"))
fmt.Println(tm.Format("2006-Jan-02 03:04:05 PM"))
fmt.Println(tm.Format("02/01/2006 03:04:05 PM"))
}

func Timestamp2Time() {
fmt.Println(">> Timestamp2Time")
defer fmt.Println("<< Timestamp2Time")

ts := int64(1595900001)
tm := time.Unix(ts, 0)
fmt.Println(tm)
}

func Time2Timestamp() {
fmt.Println(">> Time2Timestamp")
defer fmt.Println("<< Time2Timestamp")

tm := time.Now()
ts := tm.Unix()
fmt.Println(ts)
}

func main() {
Date2Time()
Time2Date()
Timestamp2Time()
Time2Timestamp()
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>> Date2Time
2020-05-20 00:00:00 +0000 UTC
2020-05-20 00:00:00 +0000 UTC
2020-05-20 00:00:00 +0000 UTC
<< Date2Time
>> Time2Date
2020-07-28 09:35:46 AM
2020-7-28 09:35:46 AM
2020-Jul-28 09:35:46 AM
28/07/2020 09:35:46 AM
<< Time2Date
>> Timestamp2Time
2020-07-28 09:33:21 +0800 CST
<< Timestamp2Time
>> Time2Timestamp
1595900146
<< Time2Timestamp

生成SSH密钥

1
ssh-keygen -t rsa -f ~/.ssh/id_rsa -C "temp user" -N ""

-t:指定加密算法
-f:指定路径
-C:注释,可以填写用户名或邮箱
-N:密码

指定以上f、C、N这3个参数,可以避免交互式问答,快速生成密钥,在脚本中使用很方便。

SSH客户端配置文件

~/.ssh/config文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host 个人VM
HostName 192.168.9.137
User centos

Host 阿里云
HostName 139.224.105.10
User root


Host 腾讯云
HostName 140.143.6.185
User root
Port 22
IdentityFile ~/.ssh/id_rsa_tencent
  • Host:自定义命名
  • HostName:机器IP或者域名
  • User:登录机器的用户名
  • Port:登录机器的端口,默认为22,可省略
  • IdentityFile:登录机器时使用的私钥,默认为~/.ssh/id_rsa,可省略;当某台机器使用单独密钥时,很有用

当基于开源项目发布新的开源项目时,我们需要说明项目所使用的License,同样也需要考虑你基于开源项目所做的事情,是否满足该项目的License。

下面这2幅图摘自 开源许可证都有什么区别,一般开源项目用什么许可证? - 知乎,足以帮助我们判断:

  1. 要做的事情,是否满足开源项目的License。
  2. 开源一个项目,该如何选择License。


来源:https://www.zhihu.com/question/28292322/answer/656121132


来源:https://www.zhihu.com/question/28292322/answer/840556759
从左到右,是从宽松到严格。

举例:

  1. 以太坊采License用的是LGPLv3,修改源码后如果提供给外部使用必须开源,不要求新增代码采用相同的License,也不要求对新增代码进行文档说明,后来我们项目同样采用了LGPLv3。
  2. Fabric采用Apache 2.0,基于Fabric项目原有代码都必须放置Fabric原有版权声明,但可以选择不开源。

序言

密码学是当代数字信息化时代的基础技术,没有密码学,网络上的传输信息的可靠性就无法保证,比如你输入的密码会被窃取,你存在网络上的照片、文档如果没有加密,就有可能泄露。

密码学也是区块链的一项基础技术,使用密码学实现区块链中的:身份验证、数据可信、权限管理、零知识证明、可信计算等等。

Fabric提供了模块化的、可插拔的密码服务,该服务由bccsp模块提供,本文就谈一下BCCSP插件化设计,另外Fabric国密化也是最近2年必做的事情,所以同时介绍实现可扩展国密的思路,最后介绍一下Hyperledger社区对Fabric支持国密的开发。

BCCSP介绍

BCCSP是Block Chain Crypto Service Provider的缩写。

bccsp模块它为Fabric的上层模块提供密码学服务,它包含的具体功能有:对称加密和非对称加密的密钥生成、导如、导出,数字签名和验证,对称加密和解密、摘要计算。

bccsp模块为了密码服务的扩展性,定义了BCCSP接口,上层模块调用BCCSP接口中定义的方法,而不直接调用具体的实现函数,实现和具体密码学实现的解耦,当bccsp使用不同密码学实现时,上层模块无需修改,这种解耦是通过依赖反转实现的。

bccsp模块中当前有2种密码实现,它们都是bccsp中的密码学插件:SW和PKCS11,SW代表的是国际标准加密的软实现,SW是software的缩写,PKCS11代指硬实现。

扩展阅读:PKCS11是PKCS系列标准中的第11个,它定义了应用层和底层加密设备的交互标准,比如过去在电脑上,插入USBKey用网银转账时,就需要走USBKey中的硬件进行数字签名,这个过程就需要使用PCKS11。

密码学通常有软实现和硬实现,软实现就是常用的各种加密库,比如Go中crypto包,硬实现是使用加密机提供的一套加密服务。软实现和硬实现的重要区别是,密码算法的安全性强依赖随机数,软实现利用的是OS的伪随机数,而硬实现利用的是加密机生成的随机数,所以硬实现的安全强度要高于软实现。

让Fabric支持国密时,就需要在bccsp中新增一个国密插件GM,只在bccsp中增加GM并不是完成的Fabric国密改造,下文再详细介绍。

SW介绍

SW是国际标准加密的软实现插件,它包含了ECDSA算法、RSA算法、AES算法,以及SHA系列的摘要算法。

BCCSP接口定义了以下方法,其实对密码学中的函数进行了一个功能分类:

  • KeyGen:密钥生成,包含对称和非对称加密
  • KeyDeriv:密钥派生
  • KeyImport:密钥导入,从文件、内存、数字证书中导入
  • GetKey:获取密钥
  • Hash:计算摘要
  • GetHash:获取摘要计算实例
  • Sign:数字签名
  • Verify:签名验证
  • Encrypt:数据加密,包含对称和非对称加密
  • Decrypt:数据解密,包含对称和非对称加密

SW要做的是,把ECDSA、RSA、AES、SHA中的各种函数,对应到以上各种分类中,主要的分类如下图所示。

从上图可以看出,密钥生成、派生、导入都包含了ECDSA、RSA、AES,签名和延签包含了ECDSA和RSA,摘要计算包含了SHA系列,加密解密包含了AES,但没有包含RSA,是因为非对称加密耗时,并不常用。

可插拔国密

Fabric支持国密并非仅仅在bccsp中增加1个国密实现这么简单,还需要让数字证书支持国密,让数字证书的操作符合X.509。各语言的标准库x509都是适配标准加密的,并不能直接用来操作国密证书。

在数字证书支持国密后,还可能需要进一步考虑,是否需要TLS证书使用国密数字证书,让通信过程使用国密算法。

另外,国密的实现有很多版本,如果需要适配不同的国密实现,就需要保证国密的可插拔和可扩展。

综上情况,你需要一个中间件,中间件中包含定义好国密接口、国密数字证书接口等,用这些接口去适配Fabric,然后当采用不同国密实现时,只需要对具体实现进行封装,去适配中间件中定义好的接口。

社区对Fabric支持国密的态度

国密有很多基于Fabric的项目,金融业是区块链场景最多的行业,金融行业又必须使用国密,所以国内对Fabric国密的改造是必须的,在《金融分布式账本安全规范》发布之后,社区也计划让Fabric支持国密,但方式是不提供具体国密实现,而是定义好接口,项目方使用哪种国密实现,去适配定义好的接口即可,这样保留了好的扩展性,与可插拔国密的目的是一致的,选择权交给企业。

社区支持Fabric国密的版本,预计在2.x版本发布。

结语

密码学在区块链中的地位是相当高的,从区块链使用最基础的密码学,到现在还在不断融入同态加密、零知识证明等前言的加密技术,未来可以在区块链上保护数据隐私的情况,提供更好的服务,区块链也可以有更多的应用场景。

mermaid是一个开源项目,可以在Markdown中,使用类似编写代码的方式,制作流程图、时序图、甘特图、饼图等。使用下来,感觉可以明显提升时序图的效率。

时序图

示例

1
2
3
4
5
6
7
8
9
sequenceDiagram
%% 注释
Client ->> Gateway: 发送JSON RPC请求
Gateway ->> Gateway: JSON RPC请求转换为gRPC请求
Gateway ->> Server: 发送gRPC请求
Server ->> Server: 处理gRPC请求
Server ->> Gateway: 发送gRPC响应
Gateway ->> Gateway: gRPC响应转换为JSON RPC响应
Gateway ->> Client: 把JSON RPC响应发送给客户端
sequenceDiagram
    %% 注释
    Client ->> Gateway: 发送JSON RPC请求
    Gateway ->> Gateway: JSON RPC请求转换为gRPC请求
    Gateway ->> Server: 发送gRPC请求
    Server ->> Server: 处理gRPC请求
    Server ->> Gateway: 发送gRPC响应
    Gateway ->> Gateway: gRPC响应转换为JSON RPC响应
    Gateway ->> Client: 把JSON RPC响应发送给客户端

昵称

1
2
3
4
5
6
7
8
9
10
11
12
sequenceDiagram
participant C as Client
participant G as Gateway
participant S as Server

C ->> G: 发送JSON RPC请求
G ->> G: JSON RPC请求转换为gRPC请求
G ->> S: 发送gRPC请求
S ->> S: 处理gRPC请求
S ->> G: 发送gRPC响应
G ->> G: gRPC响应转换为JSON RPC响应
G ->> C: 把JSON RPC响应发送给客户端
sequenceDiagram
    participant C as Client
    participant G as Gateway
    participant S as Server

    C ->> G: 发送JSON RPC请求
    G ->> G: JSON RPC请求转换为gRPC请求
    G ->> S: 发送gRPC请求
    S ->> S: 处理gRPC请求
    S ->> G: 发送gRPC响应
    G ->> G: gRPC响应转换为JSON RPC响应
    G ->> C: 把JSON RPC响应发送给客户端

线条和箭头

1
2
3
4
5
sequenceDiagram
Client -> Gateway: 实线
Client --> Gateway: 虚线 --
Client ->> Gateway: 带箭头 >>
Client -x Gateway: 带叉,不使用>
sequenceDiagram
    Client -> Gateway: 实线
    Client --> Gateway: 虚线 --
    Client ->> Gateway: 带箭头 >>
    Client -x Gateway: 带叉,不使用>

笔记

1
2
3
sequenceDiagram
Note left of Client: 创建请求
Note right of Gateway: 接收请求
sequenceDiagram
    Note left of Client: 创建请求
    Note right of Gateway: 接收请求

循环

1
2
3
4
5
6
sequenceDiagram
loop Every Second
Client ->> Server: 发送请求
Server ->> Server: 处理请求
Server ->> Client: 发送响应
end
sequenceDiagram
    loop Every Second
        Client ->> Server: 发送请求
        Server ->> Server: 处理请求
        Server ->> Client: 发送响应
    end

If语句

1
2
3
4
5
6
7
8
sequenceDiagram
Client ->> Server: 查询用户
alt User not found
Server ->> Server: 创建错误响应:用户不存在
else
Server ->> Server: 使用用户信息创建响应
end
Server ->> Client: 发送响应
sequenceDiagram
    Client ->> Server: 查询用户
    alt User not found
        Server ->> Server: 创建错误响应:用户不存在
    else 
        Server ->> Server: 使用用户信息创建响应
    end
    Server ->> Client: 发送响应

背景颜色

1
2
3
4
5
6
sequenceDiagram
Client ->> Server: 发送请求
rect rgb(191,223,255)
Server ->> Server: 处理请求
Server ->> Client: 发送响应
end
sequenceDiagram
    Client ->> Server: 发送请求
    rect rgb(191,223,255)
    Server ->> Server: 处理请求
    Server ->> Client: 发送响应
    end

激活

1
2
3
4
5
6
sequenceDiagram
Client ->> Server: 发送请求
activate Server
Server ->> Server: 处理请求
Server ->> Client: 发送响应
deactivate Server
sequenceDiagram
    Client ->> Server: 发送请求
    activate Server
    Server ->> Server: 处理请求
    Server ->> Client: 发送响应
    deactivate Server

饼图

1
2
3
4
pie
title 硬币正反面的概率
"正面": 0.5
"反面": 0.5
pie
    title 硬币正反面的概率
    "正面": 0.5
    "反面": 0.5

前言

在当前的PBFT资料中,尤其是中文资料,多数都在介绍PBFT的3阶段消息过程,很少提及View Changes(视图切换),View Changes对PBFT的重要性,如同Leader Election对Raft的重要性,它是一个一致性算法中,不可或缺的部分。

作者为大家介绍下,为什么View Changes如此重要,即为什么PBFT需要View Changes,以及View Changes的原理。

为什么PBFT需要View Changes

一致性算法都要提供:

  • safety :原意指不会出现错误情况,一致性中指操作是正确的,得到相同的结果。
  • liveness :操作过程能在有限时间内完成。

一致性协议需要满足的特性

safety通常称为一致性,liveness通常称为可用性,没有liveness的一致性算法无法长期提供一致性服务,没有safety的一致性算法称不上一致性算法,所以,所有的一致性算法都在做二者之间的折中。

所以对一致性和可用性不同的要求,就出现了你常听见的ACID原理、CAP理论、BASE理论。

PBFT作为一个一致性算法,它也需要提供一致性和可用性。在为什么PBFT需要3个阶段消息中,介绍了PBFT算法的如何达成一致性,并且请求可以在有限时间内达成一致,客户端得到响应,也满足可用性。

但没有介绍,当遇到以下情况时,是否还能保住一致性和可用性呢?

  1. 主节点是拜占庭节点(宕机、拒绝响应…)
  2. 主节点不是拜占庭节点,但非拜占庭副本节点参与度不足,不足以完成3阶段消息
  3. 网络不畅,丢包严重,造成不足以完成3阶段消息

在以上场景中,新的请求无法在有限时间内达成一致,老的数据可以保持一致性,所以一致性是可以满足的,但可用性无法满足。必须寻找一个方案,恢复集群的可用性。

PBFT算法使用View Changes,让集群重新具有可用性。通过View Changes,可以选举出新的、让请求在有限时间内达成一致的主节点,向客户端响应,从而满足可用性的要求。

让集群重新恢复可用,需要做到什么呢?让至少f+1个非拜占庭节点迁移到,新的一致的状态。然后这些节点,运行3阶段消息协议,处理新的客户端请求,并达成一致。

不同版本的View Changes协议有什么不同?

PBFT算法有1999年和2001年2个版本:

PBFT-PR并非只是在PBFT上增加了PR,同时也对PBFT算法做了详细的介绍和改进,View Changes的改进就是其中一项。

PBFT中View Changes介绍比较简单,没有说明以下场景下,View Changes协议如何处理:

  • 如果下一个View的主节点宕机了怎么办
  • 如果下一个View的主节点是恶意节点,作恶怎么办
  • 如果非拜占庭恶意发起View Changes,造成主节点切换怎么办?
  • 如果参与View Changes的节点数量不足怎么办

如果,以上场景下,节点处在View Changes阶段,持续的等待下去,就无法恢复集群的可用性。

PBFT-PR中的View Changes协议进行了细化,可以解决以上问题。

2001年版本View Changes协议原理

每个主节点都拥有一个View,就如同Raft中每个leader都拥有1个term。不同点是term所属的leader是选举出来的,而View所属的主节点是计算出的: primary = v % R,R是运行PBFT协议的节点数量。

View Changes的战略是:当副本节点怀疑主节点无法让请求达成一致时,发起视图切换,新的主节点收集当前视图中已经Prepared,但未Committed的请求,传递到下一个视图中,所有非拜占庭节点基于以上请求,会达到一个新的、一致的状态。然后,正常运行3阶段消息协议。

为什么要包含已经Prepared,但未Committed的请求?如果一个请求,在副本节点i上,已经是Prepared状态,证明至少f+1的非拜占庭节点,已经拥有此请求并赞成请求req在视图v中使用序号n。如果没有问题,不发生视图切换,这些请求可以在有限的时间内达成一致,新的主节点把已经Prepared的请求,带到新的view,并证明给其他节点,请求已经Prepared,那只需1轮Commit就可以达成一致。

View Changes主要流程简介

对View Changes的流程可以分为2部分:

  • View Changes的开端,即每一次View的开始
  • View Changes的中间过程,以及View Changes的结束,切换到正常流程

这2部分分别占据了下图的左右两部分。实线代表流程线,虚线代表网络消息。蓝色代表正常操作流程(三阶段消息:Preprepare、Prepare、Commit),青色代表View Changes流程,蓝青相接就是正常流程和View Changes流程切换的地方。

View Changes的开端流程是通用的,主节点和副本节点都遵守这一流程:新视图:v=v+1,代表一个新的View开始,指向它的每一个箭头,都是视图切换的一种原因。某个副本节点,新视图的开始,还伴随着广播view-change消息,告诉其他节点,本节点开启了一个新的视图。

主节点是通过公式算出来的,其余为副本节点,在View Changes流程中,副本节点会和主节点交互,共同完成View Changes过程。副本节点会对收到的view-change消息进行检查,然后把一条对应的view-change-ack消息发送给主节点,主节点会依赖收到的view-change消息和view-change-ack消息数量和内容,产生能让所有节点移动到统一状态的new-view消息,并且对new-view消息进行3阶段共识,即对new-view消息达成一致,从而让至少f+1个非拜占庭节点达成一致。

View Changes的开端

View Change的核心因素只有一个:怀疑当前的主节点在有限的时间内,无法达成一致。

具体有4个路径:

  1. 正常阶段定时器超时,代表一定时间内无法完成Pre-prepare -> Prepare -> Commit
  2. View Changes阶段定时器超时,代表一定时间内无法完成正在进行的View Change
  3. 定时器未超时,但有效的view-change消息数量达到f+1个,代表当前已经有f+1个非拜占庭节点发起了新的视图切换,本节点为了不落后,不等待超时而进入视图切换
  4. new-view消息不合法,代表当前View Changes阶段的主节点为拜占庭节点

图中【正常阶段定时器超时】被标记为蓝色,是因为它是正常阶段进入视图切换阶段的开端,【有效的view-change消息数量达到f+1个】即有可能是正常阶段的定时器,也有可能是视图切换过程中的定时器,所以颜色没做调整。

主副节点主要交互流程

视图切换过程中有3个消息:view-change消息、view-change-ack消息和new-view消息,下文围绕这3个消息,对主副节点的交互流程做详细介绍。

view-change消息阶段

在view v时,副本节点怀疑主节点fault时,会发送view-change消息,该消息包含:

  1. h:副本i最新的稳定检查点序号
  2. C:副本i保存的h之后的(非稳定)检查点
  3. P和Q
  4. i:副本i
  5. α:副本i对本消息的数字签名

P和Q是2个集合。

P是已经Prepared消息的信息集合:消息既然已经Prepared,说明至少2f+1的节点拥有了消息,并且认可<view, n, d>,即为消息分配的view和序号,只是还差一步commit阶段就可以完成一致性确认。P中包含的就是已经达到Prepared的消息的摘要d,无需包含完整的请求消息。新的view中,这些请求会使用老的序号n,而无需分配新的序号。

Q是已经Pre-prepared消息的信息集合,主节点已经发送Pre-prepare或副本节点i为请求已经发送Prepare消息,证明该节点认可<n, d, v>

P、Q中的请求都是高低水位之间的,无View Changes时,P、Q都是空的,也就是说不包含已经committed的请求。new-view消息中的数据(View Changes的决策结果),都是基于P、Q集合计算出的。

在发送view-change消息前,副本节点会利用日志中的三阶段消息计算P、Q集合,发送view-change消息后,就删除日志中的三阶段消息。

view-change-ack消息阶段

视图v+1的主节点在收到其他节点发送的view-change消息后,并不确认view-change消息是是否拜占庭节点发出的,即不确定消息是否是正确无误的,如果基于错误的消息做决策,就会得到错误的结果,违反一致性:一切操作都是正确的。

设置view-change-ack消息的目的是,让所有副本节点对所有它收到的view-change消息进行检查和确认,只不过确认的结果都发送给新的主节点。主节点统计ack消息,可以辨别哪些view-change是正确的,哪些是拜占庭节点发出的。

副本节点会对view-change消息中的P、Q集合进行检查,要求集合中的请求消息小于等于视图v,满足则发送view-change-ack消息:

  1. v:v+1
  2. i:发送ack消息的副本序号
  3. j:副本i要确认的view-change消息的发送方
  4. d:副本i要确认的view-change消息的摘要
  5. μip:i向主节点p发送消息的通信密钥计算出的MAC,这里需要保证i和p之间通信的私密性,所以不使用数字签名

new-view消息阶段

新视图主节点p负责基于view-change消息做决策,决策放到new-view消息中。

主节点p维护了一个集合S,用来存放正确的view-change消息。只有view-change消息,以及为该消息背书的view-change-ack消息达到2f-1个时,view-change消息才能加入到集合S,但view-change-ack消息不加入集合S。

当集合S的大小达到2f+1时,证明有足够多的非拜占庭节点认为需要进行视图变更,并提供了变更的依据:2f+1个view-change消息,主节点p使用S做决策。以下便是决策逻辑

主节点p先确定h:所有view-change消息中最大的稳定检查点。h和h+L其实就是高低水位。

然后依次检查h到h+L中的每一个序号n,对序号n对于的请求进行的处理为:请求m已经Prepared并且Pre-prepared,则收集序号n对应的请求。否则,说明没有请求在序号n能达到committed,为序号n分配一个空请求,并收集起来。它们最后会被放到new-view消息的X集合中。

主节点会创建new-view消息:

  • view:当前新视图的编号
  • V:是一个集合,每个元素是一对(i, d),代表i发送的view-change消息摘要是d,每一对都与集合S中消息对应,可以使用V证明主节点是在满足条件下,创建new-view消息的,即V是新视图的证明。因为其它多数副本节点已经接收view-change消息,所以此处发送消息的摘要做对比即可。
  • X:是一个集合,包含检查点以及选定的请求
  • α:主节点p对new-view消息的数字签名

之后,主节点会把new-view消息广播给每一个副本节点。

处理new-view消息

主节点处理new-view消息

在发生View Changes时,主节点的状态可能也不是最全的,如果它没有X结合中的请求或者检查点,它可以从其他节点哪拉去。

主节点需要使用new-view消息,达到视图切换的最后一步状态:在新视图v+1中,让集合X中的请求,全部是Pre-prepared状态。为何是Pre-prepared状态呢?因为new-view消息,可以看做一次特殊的Pre-prepare消息。

为什么不直接标记为Committed呢?因为主节点也可能是拜占庭节点,副本节点需要检查new-view消息,向所有节点广播自己检查的结果,满足条件后才能达成一致性。

副本节点处理new-view消息

副本节点在视图v+1,会持续接收view-change消息和new-view消息,它会把new-view消息V集合中的view-change消息,跟它收到的消息做对比,如果它本地不存在某条view-change消息,它可以要求主节点向他提供view-change消息和view-change-ack消息集合,证明至少f+1个非拜占庭副本节点收到过此view-change消息。

副本节点拥有所有的view-change消息之后,副本节点会和主节点运行相同的决策逻辑,以校验new-view消息的正确性。

如果new-view消息是正确的,副本节点会和主节点一样移动到相同的状态,然后广播一条Prepare消息给所有节点,这样就恢复到了正常情况下的:Pre-prepare -> Prepare -> Commit 一致性逻辑。这样就完成了从View Changes到正常处理流程的迁移。

如果new-view消息是错误的,说明主节点p是拜占庭节点,副本节点会直接进入v+2,发送view-change消息,进行新的一轮视图切换。

View Changes如何提供liveness

在一轮视图切换无法完成的时候,会开启新的一轮视图切换,由于拜占庭节点的数量最多为f个,最终会在某一轮视图切换中,能够完成视图切换,所有非拜占庭节点达成一致的状态,保证liveness和safety。

本文前面列出了几种异常情况,下面就看一下View Changes是如何应对这些异常情况的,以及如何提供活性。

Q1:如果下一个View的主节点宕机了怎么办?

A1:副本节点在收集到2f+1个view-change消息后,会启动定时器,超时时间为T,新view的主节点宕机,必然会导致定时器超时时,未能完成View Changes流程,会进入新一轮视图切换。

Q2:如果下一个View的主节点是恶意节点,作恶怎么办?

A2:新view的主节点是恶意节点,如果它做恶了,生成的new-view消息不合法,副本节点可以检测出来。或者new-view消息是合法的,但它只发送给了少数副本节点,副本节点在对new-view消息进行正常的3阶段流程,参与的节点太少,在定时器超时前,不足以完成3阶段流程,副本节点会进入下一轮视图切换。

Q3:如果非拜占庭恶意发起View Changes,造成主节点切换怎么办?

A3:定时器未超时情况下,只有有效的f+1个view-change消息,才会引发其他副本节点进行主节点切换,否则无法造成主节点切换。但PBFT的前提条件是恶意节点不足f个,所以只有恶意节点发起view-change消息时,无法造成主节点切换。

Q4:如果参与View Changes的节点数量不足怎么办?

A4:这个问题可以分几种情况。

  • 发起view-change的节点数量不足f+1个,这种情况不会发生整个集群的视图切换。
  • 视图切换过程中,不满足各节点的数量要求,无法完成本轮视图切换,会进入下一轮视图切换。

结语

View Changes是PBFT中一个重要的环节,它能保证整个协议的liveness,是PBFT不可或缺的一部分。

gops简介

gops 是Go团队提供的命令行工具,它可以用来获取go进程运行时信息。

可以查看:

  • 当前有哪些go语言进程,哪些使用gops的go进程
  • 进程的概要信息
  • 进程的调用栈
  • 进程的内存使用情况
  • 构建程序的Go版本
  • 运行时统计信息

可以获取:

  • trace
  • cpu profile和memory profile

还可以:

  • 让进程进行1次GC
  • 设置GC百分比

示例代码

使用Options配置agent。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"log"
"runtime"
"time"

"github.com/google/gops/agent"
)

func main() {
if err := agent.Listen(agent.Options{
Addr: "0.0.0.0:8848",
// ConfigDir: "/home/centos/gopsconfig", // 最好使用默认
ShutdownCleanup: true}); err != nil {
log.Fatal(err)
}

// 测试代码
_ = make([]int, 1000, 1000)
runtime.GC()

_ = make([]int, 1000, 2000)
runtime.GC()

time.Sleep(time.Hour)
}

agent Option选项

agent有3个配置:

  • Addr:agent要监听的ip和端口,默认ip为环回地址,端口随机分配。
  • ConfigDir:该目录存放的不是agent的配置,而是每一个使用了agent的go进程信息,文件以pid命名,内容是该pid进程所监听的端口号,所以其中文件的目的是形成pid到端口的映射。默认值为~/.config/gops
  • ShutdownCleanup:进程退出时,是否清理ConfigDir中的文件,默认值为false,不清理

通常可以把Addr设置为要监听的IP,把ShutdownCleanup设置为ture,进程退出后,残留在ConfigDir目录的文件不再有用,最好清除掉。

ConfigDir示例:

1
2
3
4
5
// gopsconfig为设置的ConfigDir目录,2051为pid,8848为端口号。
➜ ~ cat gopsconfig/2051
8848%
➜ ~ netstat -nap | grep `pgrep gopsexample`
tcp6 0 0 :::8848 :::* LISTEN 2051/./gopsexample

gops原理

gops的原理是,代码中导入gops/agent,建立agent服务,gops命令连接agent读取进程信息。

gops

agent的实现原理可以查看agent/handle函数

使用go标准库中原生接口实现相关功能,如同你要在自己的程序中开启pprof类似,只不过这部分功能由gops/agent实现了:

  • 使用runtime.MemStats获取内存情况
  • 使用runtime/pprof获取调用栈、cpu profile和memory profile
  • 使用runtime/trace获取trace
  • 使用runtime获取stats信息
  • 使用runtime/debugGC设置和启动GC

再谈ConfigDir。从源码上看,ConfigDir对agent并没有用途,对gops有用。当gops和ConfigDir在一台机器上时,即gops查看本机的go进程信息,gops可以通过其中的文件,快速找到agent服务的端口。能够实现:gops <sub-cmd> pidgops <sub-cmd> 127.0.0.1:port的转换。

如果代码中通过ConfigDir指定了其他目录,使用gops时,需要添加环境变量GOPS_CONFIG_DIR指向ConfigDir使用的目录。

子命令介绍

gops后面可以跟子命令,然后是pid或者远端地址。

也可以直接跟pid,查看本机进程信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  ~ gops help memstats
gops is a tool to list and diagnose Go processes.

Usage:
gops <cmd> <pid|addr> ...
gops <pid> # displays process info
gops help # displays this help message

Commands:
stack Prints the stack trace.
gc Runs the garbage collector and blocks until successful.
setgc Sets the garbage collection target percentage.
memstats Prints the allocation and garbage collection stats.
version Prints the Go version used to build the program.
stats Prints runtime stats.
trace Runs the runtime tracer for 5 secs and launches "go tool trace".
pprof-heap Reads the heap profile and launches "go tool pprof".
pprof-cpu Reads the CPU profile and launches "go tool pprof".

All commands require the agent running on the Go process.
"*" indicates the process is running the agent.

查看当前机器上go程序进程信息

查看当前机器上的go进程,可以列出pid、ppid、进程名、可执行程序所使用的go版本,以及可执行程序的路径。

1
2
3
4
5
6
7
8
➜  ~ gops
67292 66333 gops * go1.13 /Users/shitaibin/Workspace/golang_step_by_step/gops/gops
67434 65931 gops go1.13 /Users/shitaibin/go/bin/gops
66551 1 gocode go1.11.2 /Users/shitaibin/go/bin/gocode
137 1 com.docker.vmnetd go1.12.7 /Library/PrivilegedHelperTools/com.docker.vmnetd
811 807 com.docker.backend go1.12.13 /Applications/Docker.app/Contents/MacOS/com.docker.backend
807 746 com.docker.supervisor go1.12.13 /Applications/Docker.app/Contents/MacOS/com.docker.supervisor
810 807 com.docker.driver.amd64-linux go1.12.13 /Applications/Docker.app/Contents/MacOS/com.docker.driver.amd64-linux

*的是程序中使用了gops/agent,不带*的是普通的go程序。

go程序进程树

查看进程树:

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  ~ gops tree
...
├── 66333
│   └── [*] 67292 (gops) {go1.13}
├── 1
│   ├── 66551 (gocode) {go1.11.2}
│   └── 137 (com.docker.vmnetd) {go1.12.7}
├── 65931
│   └── 67476 (gops) {go1.13}
└── 746
└── 807 (com.docker.supervisor) {go1.12.13}
├── 811 (com.docker.backend) {go1.12.13}
└── 810 (com.docker.driver.amd64-linux) {go1.12.13}

pid:进程概要信息

查看进程的概要信息,非gops进程也可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  ~ gops 67292
parent PID: 66333
threads: 7
memory usage: 0.018%
cpu usage: 0.000%
username: shitaibin
cmd+args: ./gops
elapsed time: 11:28
local/remote: 127.0.0.1:54753 <-> :0 (LISTEN)
➜ ~
➜ ~ gops 807
parent PID: 746
threads: 28
memory usage: 0.057%
cpu usage: 0.003%
username: shitaibin
cmd+args: /Applications/Docker.app/Contents/MacOS/com.docker.supervisor -watchdog fd:0
elapsed time: 27-23:36:35
local/remote: 127.0.0.1:54832 <-> :0 ()
local/remote: *:53849 <-> :0 ()
local/remote: 127.0.0.1:49473 <-> :0 (LISTEN)

stack:当前调用栈

查看使用gops的进程的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
➜  ~ gops stack 67292
goroutine 19 [running]:
runtime/pprof.writeGoroutineStacks(0x1197160, 0xc00009c028, 0x0, 0x0)
/Users/shitaibin/goroot/src/runtime/pprof/pprof.go:679 +0x9d
runtime/pprof.writeGoroutine(0x1197160, 0xc00009c028, 0x2, 0x0, 0x0)
/Users/shitaibin/goroot/src/runtime/pprof/pprof.go:668 +0x44
runtime/pprof.(*Profile).WriteTo(0x1275c60, 0x1197160, 0xc00009c028, 0x2, 0xc00009c028, 0x0)
/Users/shitaibin/goroot/src/runtime/pprof/pprof.go:329 +0x3da
github.com/google/gops/agent.handle(0x1665008, 0xc00009c028, 0xc000014068, 0x1, 0x1, 0x0, 0x0)
/Users/shitaibin/go/src/github.com/google/gops/agent/agent.go:185 +0x1ab
github.com/google/gops/agent.listen()
/Users/shitaibin/go/src/github.com/google/gops/agent/agent.go:133 +0x2bf
created by github.com/google/gops/agent.Listen
/Users/shitaibin/go/src/github.com/google/gops/agent/agent.go:111 +0x364

goroutine 1 [sleep]:
runtime.goparkunlock(...)
/Users/shitaibin/goroot/src/runtime/proc.go:310
time.Sleep(0x34630b8a000)
/Users/shitaibin/goroot/src/runtime/time.go:105 +0x157
main.main()
/Users/shitaibin/Workspace/golang_step_by_step/gops/example.go:15 +0xa3

goroutine 18 [syscall]:
os/signal.signal_recv(0x0)
/Users/shitaibin/goroot/src/runtime/sigqueue.go:144 +0x96
os/signal.loop()
/Users/shitaibin/goroot/src/os/signal/signal_unix.go:23 +0x22
created by os/signal.init.0
/Users/shitaibin/goroot/src/os/signal/signal_unix.go:29 +0x41

memstats: 内存使用情况

查看gops进程内存使用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
➜  ~ gops memstats 67944
alloc: 136.80KB (140088 bytes) // 当前分配出去未收回的内存总量
total-alloc: 152.08KB (155728 bytes) // 已分配出去的内存总量
sys: 67.25MB (70518784 bytes) // 当前进程从OS获取的内存总量
lookups: 0
mallocs: 418 // 分配的对象数量
frees: 82 // 释放的对象数量
heap-alloc: 136.80KB (140088 bytes) // 当前分配出去未收回的堆内存总量
heap-sys: 63.56MB (66650112 bytes) // 当前堆从OS获取的内存
heap-idle: 62.98MB (66035712 bytes) // 当前堆中空闲的内存量
heap-in-use: 600.00KB (614400 bytes) // 当前堆使用中的内存量
heap-released: 62.89MB (65945600 bytes)
heap-objects: 336 // 堆中对象数量
stack-in-use: 448.00KB (458752 bytes) // 栈使用中的内存量
stack-sys: 448.00KB (458752 bytes) // 栈从OS获取的内存总量
stack-mspan-inuse: 10.89KB (11152 bytes)
stack-mspan-sys: 16.00KB (16384 bytes)
stack-mcache-inuse: 13.56KB (13888 bytes)
stack-mcache-sys: 16.00KB (16384 bytes)
other-sys: 1.01MB (1062682 bytes)
gc-sys: 2.21MB (2312192 bytes)
next-gc: when heap-alloc >= 4.00MB (4194304 bytes) // 下次GC的条件
last-gc: 2020-03-16 10:06:26.743193 +0800 CST // 上次GC的世界
gc-pause-total: 83.84µs // GC总暂停时间
gc-pause: 44891 // 上次GC暂停时间,单位纳秒
num-gc: 2 // 已进行的GC次数
enable-gc: true // 是否开始GC
debug-gc: false

stats: 运行时信息

查看运行时统计信息:

1
2
3
4
5
➜  ~ gops stats 68125
goroutines: 3
OS threads: 12
GOMAXPROCS: 8
num CPU: 8

trace

获取当前运行5s的trace信息,会打开网页:

1
2
3
4
5
6
➜  ~ gops trace 68125
Tracing now, will take 5 secs...
Trace dump saved to: /var/folders/5g/rz16gqtx3nsdfs7k8sb80jth0000gn/T/trace116447431
2020/03/16 10:23:37 Parsing trace...
2020/03/16 10:23:37 Splitting trace...
2020/03/16 10:23:37 Opening browser. Trace viewer is listening on http://127.0.0.1:55480

cpu profile

获取cpu profile,并进入交互模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  ~ gops pprof-cpu 68125
Profiling CPU now, will take 30 secs...

Profile dump saved to: /var/folders/5g/rz16gqtx3nsdfs7k8sb80jth0000gn/T/profile431166544
Binary file saved to: /var/folders/5g/rz16gqtx3nsdfs7k8sb80jth0000gn/T/binary765361519
File: binary765361519
Type: cpu
Time: Mar 16, 2020 at 10:25am (CST)
Duration: 30s, Total samples = 0
No samples were found with the default sample value type.
Try "sample_index" command to analyze different sample values.
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
(pprof) top
Showing nodes accounting for 0, 0% of 0 total
flat flat% sum% cum cum%

memory profile

获取memory profile,并进入交互模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
➜  ~ gops pprof-heap 68125
Profile dump saved to: /var/folders/5g/rz16gqtx3nsdfs7k8sb80jth0000gn/T/profile292136242
Binary file saved to: /var/folders/5g/rz16gqtx3nsdfs7k8sb80jth0000gn/T/binary693335273
File: binary693335273
Type: inuse_space
Time: Mar 16, 2020 at 10:27am (CST)
No samples were found with the default sample value type.
Try "sample_index" command to analyze different sample values.
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
(pprof) traces
File: binary693335273
Type: inuse_space
Time: Mar 16, 2020 at 10:27am (CST)
-----------+-------------------------------------------------------
bytes: 256kB
0 compress/flate.(*compressor).init
compress/flate.NewWriter
compress/gzip.(*Writer).Write
runtime/pprof.(*profileBuilder).build
runtime/pprof.profileWriter
-----------+-------------------------------------------------------
bytes: 64kB
0 compress/flate.newDeflateFast
compress/flate.(*compressor).init
compress/flate.NewWriter
compress/gzip.(*Writer).Write
runtime/pprof.(*profileBuilder).build
runtime/pprof.profileWriter
-----------+-------------------------------------------------------

使用远程连接

agent的默认配置Option{},监听的是环回地址。

1
2
3
4
5
➜  ~ sudo netstat -nap | grep 414
➜ ~ netstat -nap | grep `pgrep gopsexample`
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:36812 0.0.0.0:* LISTEN 414/./gopsexample

修改程序,在Option中设置监听的地址和端口:

1
agent.Listen(agent.Options{Addr:"0.0.0.0:8848"})

在远程主机上重新编译、重启进程,确认进程监听的端口:

1
2
3
4
➜  ~ netstat -nap | grep `pgrep gopsexample`
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp6 0 0 :::8848 :::* LISTEN 887/./gopsexample

在本地主机上使用gops连接远端go进程,并查看数据:

1
2
3
4
5
➜  ~ gops stats 192.168.9.137:8848
goroutines: 3
OS threads: 9
GOMAXPROCS: 4
num CPU: 4

gops后面只能跟pid查看进程简要信息,不能跟ip和port查看远端go进程简要信息,这些简要信息可以通过子命令汇集起来。

1
2
3
4
5
➜  ~ gops 192.168.9.137:8848
gops: unknown subcommand
➜ ~
➜ ~ gops version 192.168.9.137:8848
go1.13

前言

在面试的时候,很多同学的简历熟悉PBFT共识算法,在现场面试的时候,却只能说个主要逻辑,离完整的算法,还差十万八千里,相似从网络上看了一些文章,就算是熟悉了。当我问“为什么PBFT需要3个阶段消息?2个阶段行不行”时,还没有人能回答出来。

回答这个问题,还要从PBFT要解决的本质问题说起,所以我打算以这样一个思路,为大家回答问题:

  • PBFT与拜占庭问题
  • 拜占庭节点在网络中的行为
  • 什么是3阶段消息
  • 3阶段消息解决什么问题
  • 为什么不能只有前2个阶段
  • 论文使用的2个不变性
  • 为什么3个阶段可以达成一致性

PBFT与拜占庭问题

莱斯利·兰波特在其论文[1]中描述了如下拜占庭问题:

一组拜占庭帝国的将军分别各率领一支军队共同围困一座城市。为了简化问题,将各支军队的行动策略限定为进攻或撤离两种。因为部分军队进攻,或部分军队撤离可能会造成灾难性后果,因此各位将军必须通过投票来达成一致策略,即所有军队一起进攻或所有军队一起撤离。因为各位将军分处城市不同方向,他们只能通过信使互相联系。在投票过程中,每位将军都将自己投票进攻还是撤退的信息,通过信使分别通知其他所有将军,这样一来每位将军根据自己的投票,和其他所有将军送来的信息,就可以知道共同的投票结果,而决定行动策略。

问题在于,将军中可能出现叛徒(坏将军),他们不仅可能向较为糟糕的策略投票,还可能选择性地发送投票信息。阻止好将军达成一致的形成策略。

摘自:维基百科:拜占庭将军问题,有删改。

很多人喜欢玩狼人杀,我也喜欢,但我玩的很菜,我用狼人杀跟拜占庭将军问题做个类比。

在狼人杀开局的时候,你是好人,并且不知道自己的队友是谁,也不知道狼人是谁,但所有的好人都有一个共同的目的:干死狼人,好人获胜。所以游戏中需要使用技巧和策略,达成目的。

拜占庭将军问题是类似的,好的将军不知道其他将军是好的,还是坏的,但所有好的将军的目的是:行动一致,共同进退。所以,它们也需要策略达成一致。

BFT是一类解决拜占庭将军问题的策略/算法:让非拜占庭节点达成一致的算法。在这类论文中,拜占庭节点指“坏”的将军,非拜占庭节点指“好”的将军。

PBFT是实用拜占庭算法(Practical Byzantine Fault Tolerance)的缩写,该论文与1999年发表,另外2001年又发表了一篇Practical Byzantine Fault Tolerance and Proactive Recovery,让PBFT拥有恢复能力。

PBFT作为解决拜占庭问题的策略:非拜占庭节点不知道哪些是拜占庭节点,哪些是非拜占庭节点,PBFT要让非拜占庭节点达成一致

拜占庭节点在网络中的行为

拜占庭问题是在分布式对等网络,对通信容错所提出来的。在真实世界中,拜占庭问题是什么样的?

通常使用拜占庭行为,描述拜占庭节点可能的行为,拜占庭行为有:

  • 任何不遵守协议的动作
  • 恶意代码、节点
  • 代码bug
  • 网络故障、数据包损坏
  • 磁盘崩掉、重复丢失
  • 无权限时加入

什么是3阶段消息

3阶段消息

3阶段消息是:Pre-prepare、Prepare和Commit。每个消息都会包含数字签名,证明消息的发送者,以及消息类型,下文中会省略。

Pre-prepare消息由主节点发出,包含:

  • 当前view:v
  • 主节点分配给请求的序号n
  • 请求的摘要d
  • 请求本身m

务必记牢,m、v、n、d,后面会使用缩写

Prepare是副本节点收到Pre-prepare消息后,做出的响应,发送给所有副本节点,包含:

  • v
  • n
  • d

Prepared状态:副本i有Pre-prepare消息,且收到2f个有效的Prepare消息。

副本i达到Prepared状态,可以发送Commit消息,Commit消息的内容和Prepare消息内容相同,但消息类型和数字签名是不同的,所以可以区分。

m可以使用d代替,所以Prepare和Commit消息使用d代替m,来节省通信量。

3阶段消息解决什么问题

前面提到,PBFT解决的是拜占庭问题的一致性,即让非拜占庭节点达成一致。更具体的说:让请求m,在view内使用序号n,并且完成执行m,向客户端发送响应

为什么不能只有前2个阶段消息

这个问题的等价问题是:为什么Pre-prepare和Prepare消息,不能让非拜占庭节点达成一致?

Pre-prepare消息的目的是,主节点为请求m,分配了视图v和序号n,让至少f+1个非拜占庭节点对这个分配组合<m, v, n>达成一致,并且不存在<m', v, n>,即不存在有2个消息使用同一个v和n的情况。

Prepared状态可以证明非拜占庭节点在只有请求m使用<v, n>上达成一致。主节点本身是认可<m, v, n>的,所以副本只需要收集2f个Prepare消息,而不是2f+1个Prepare消息,就可以计算出至少f个副本节点是非拜占庭节点,它们认可m使用<v, n>,并且没有另外1个消息可以使用<v, n>

既然1个<v, n>只能对应1个请求m了,达到Prepared状态后,副本i执行请求m,不就达成一致了么?

并不能。Prepared是一个局部视角,不是全局一致,即副本i看到了非拜占庭节点认可了<m, v, n>,但整个系统包含3f+1个节点,异步的系统中,存在丢包、延时、拜占庭节点故意向部分节点发送Prepare等拜占庭行文,副本i无法确定,其他副本也达到Prepared状态。如果少于f个副本成为Prepared状态,然后执行了请求m,系统就出现了不一致。

所以,前2个阶段的消息,并不能让非拜占庭节点达成一致。

如果你了解2PC或者Paxos,我相信可以更容易理解上面的描述。2PC或Paxos,第一步只是用来锁定资源,第2步才是真正去Do Action。把Pre-prepare和Prepare理解为第一步,资源是<v, n>,只有第一步是达不成一致性的。

2个不变性

PBFT的论文提到了2个不变性,这2个不变性,用来证明PBFT如何让非拜占庭节点达成一致性

第1个不变性,它是由Pre-prepare和Prepare消息所共同确保的不变性:非拜占庭节点在同一个view内对请求的序号达成共识。关于这个不变性,已经在为什么不能只有前2个阶段消息中论述过。

介绍第2个不变性之前,需要介绍2个定义。

  • committed-local:副本i已经是Prepared状态,并且收到了2f+1个Commit消息。
  • committed:至少f+1个非拜占庭节点已经是Prepared状态。

第2个不变性,如果副本i是committed-local,那么一定存在committed。

2f+1个Commit消息,去掉最多f个拜占庭节点伪造的消息,得出至少f+1个非拜占庭节点发送了Commit消息,即至少f+1个非拜占庭节点是Prepared状态。所以第2个不变性成立。

为什么3个阶段消息可以达成一致性

committed意味着有f+1个非拜占庭节点可以执行请求,而committed-local意味着,副本i看到了有f+1个非拜占庭节点可以执行请求,f+1个非拜占庭节点执行请求,也就达成了,让非拜占庭节点一致。

虽然我前面使用了2PC和Paxos做类比,但不意味着PBFT的Commit阶段就相当于,2PC和Paxos的第2步。因为2PC和Paxos处理的CFT场景,不存在拜占庭节点,它们的主节点充当了统计功能,统计有多少节点完成了第一步。PBFT中节点是存在拜占庭节点的,主节点并不是可靠(信)的,不能依赖主节点统计是否有f+1个非拜占庭节点达成了Prepared,而是每个节点各自统计,committed-local让节点看到了,系统一定可以达成一致,才去执行请求。

总结

本文介绍了2个阶段消息是无法达成一致的原因,而为什么3阶段消息可以。最核心的还是要理解好,PBFT解决了什么问题,以及它是如何解决的。

PBFT解决的是在拜占庭环境下,如何提供一致性,以及如何持续的提供一致性的问题。本文只介绍了如何提供一致性,没有提如何持续提供一致性,即PBFT的可用性。现在,不妨思考一下,View Change是如何保证切换时一致性的,是否也需要2个不变性的支持呢?

最近央行发布的《金融分布式账本安全规范》中提到了区块链系统要提供BFT共识,把之前整理的PBFT的思维导图分享给大家。

新标签页中打开,查看高清大图

1999年版本

2001年版本

PDF如下:

PDF不显示时,hexo安装插件:npm install --save hexo-pdf