0%

当基于开源项目发布新的开源项目时,我们需要说明项目所使用的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

昨天睡前了看了一本收藏已久的书,是关于投资的,叫《伟大的时代-深度解读价值投资》,这是一本采访了国内价值投资者的书籍,从这些投资者的话语里,看到了一些共性的东西,寻找垄断企业持续发展的根因,也就获得了投资收益,这个果。

今天起床后,就想到了因果关系、面试、个人能力,在这些角度进行了一些思考,在此记录下思考的成果,这篇文章会介绍:

  • 因果关系应该关注因,还是关注果?
  • 如何从因果关系角度,建设个人能力?
  • 如何从因果关系角度,发现优秀的面试者?

价值投资中的因果关系

这些投资者的共性是,都提到了要寻找垄断,并且能够持续垄断的企业,并投资这些企业。

垄断是“果”,持续垄断也是过,它们需要“因”

怎么才能有垄断,并且持续垄断呢?

需要找到企业的文化、价值观、制度,这些软性的东西、虚的东西,是企业不断发展和进化的根基,这些是企业保持垄断,或成长为垄断的基石,垄断创造收益,收益是实。应了阿里一句话:把虚做实,把实做虚。

所以,企业文化、价值观和制度是“因”,垄断是“果”。

如果垄断是“因”,企业收益就是“果”。

收益的因不只有垄断,但垄断可以带来巨大收益。

关注因,还是关注果?

从企业文化、垄断和收益,这3者看,因果关系可以形成链条,组成一条因果链,一个元素即可以是因,又可以是果。

比如,垄断是企业文化的果,是收益的因。

说关注因是对的,关注果也是对的,关注因果链中,关注最根本的“因”,才是最对的

说一个开发者最容易体会的例子:解决bug,需要定位问题的“根因”,只解决中间原因,并不能真正解决bug。

如何从因果关系角度,建设个人能力?

我把中级技术开发者的能力,分成5个维度:技术深度、技术广度、商业思维能力、管理能力和演讲能力。

不同岗位、层次看到的能力维度是不一样的,比如CTO在找技术总监时的岗位时,需要有体系建设的能力。所以上面强调的是中级开发者。

这5个维度的能力是因,项目、职位、收入这些是果。

果是我们的目标,是我们要达到的地方。而因才是我们要关注的地方。

建设能力,能力就转变成了“果”,那对应的因是什么呢?

建设能力的“因”是持续学习

这几年的付费学习,可把持续学习给玩坏了,总是弥漫着一种贩卖焦虑的气息,但不可否认的是,持续学习的人,总有机会。

持续学习,有很多种通俗的说法:

  1. 活到老,学到老。
  2. 永不止步。
  3. 不给自己设限。
  4. Stay foolish, Stay hungry。——乔布斯

关于技术上的持续学习,曹大(Xargin)最近这篇文章值得一看《工程师应该怎么学习》

如何从因果关系角度,发现优秀的面试者?

我考察候选人的经历不是特别多,1年下来,简历筛选过几百份,候选人也面过几十个了,有一些体会,今天就借着因果关系,浅谈一下。

面试的本质,是挖掘面试者当前的能力和持续学习的能力

上面这句代表2个观点:

  1. 简历是“果”,能力是“因”。
  2. 招进来能持续创建价值是未来的“果”,持续学习是“因”。

阿里有句土话,用来招聘很适合:没有过程的结果是垃圾,没有结果的过程是放屁。

上半句用来筛选简历,如果简历只写自己参与、做过了什么,而没有成果,这份简历就是垃圾,而写不出好简历的面试者,能力大概率也不优秀。

下半句用来面试,面试者是怎么取得这些成果的,TA具有哪些能力才取得了这些成果?

结语

因果关系,还让我深刻的想到一句话:抓住事物的主要矛盾

咱们国家的发展,不一直就是党和政府在抓主要矛盾,解决主要矛盾的过程吗。

远见

在世界读书日那天,美团技术团队推荐了许多书,其中有一本关于职业生涯的书籍:《远见:如何规划职业生涯3大阶段》,书很薄,也很容易理解,读完之后对职业生涯的认知提升了几个Level,值得深度阅读并实践,推荐给不断让自己更优秀的你。

下面是美团点评酒旅事业群前端团队负责人郭凯的推荐语:

我们不仅要找到热爱的工作,而且要建立热爱的生活。职业生涯就像是一场至少长达45年的马拉松,这本书介绍了远见思维和三大职业生涯阶段,并介绍了如何应对职场和生活的冲突。我辈应该多行动、少忧虑,并且提前做好中长期的职业思考和职业规划。如果用“远见”的思维看待眼前的影响和困难,就根本不值得一提。用“远见”的思维,长期有耐心,每天前进30公里。

区块链最核心的是可信数据,所有的功能与设计根源都是数据。本次从数据存储的角度,看一看Peer。

账本

区块链的数据存储在账本中,账本包含:

  • 区块存储
    • 区块文件
    • 区块索引数据库
  • 世界状态数据库
  • 历史数据库
  • 私有数据数据库

关于账本以上各数据库的工具,官方文档中对区块存储和世界状态数据库介绍的比较详细了,但我们介绍下它没有提到的。

区块文件和区块索引数据库

区块是保存在文件中的,为了快速查找区块、交易,Fabric建立了索引,指明某通道某区块高度的第x个交易,是存在哪个文件,偏移量是多少。当然,索引还包含了区块高度、区块hash等,方便根据高度、hash查询区块。

上图展示了一个区块文件存储区块的情况,每个区块包含:

  • 区块长度
  • 区块头
  • 每条交易长度、交易数据

每个区块的开始位置、交易的开始位置,在写区块的时候记录下来,然后写到索引数据库(Index DB)。

整个Fabric网络只有1个区块索引数据库,也就是多通道共用一个

历史数据库

用来记录交易中每个状态数据的历史信息,直白点可以理解为链码中某个key的历史数值。它的key实际是{通道id+链码id, key, 区块高度, 交易在区块中的序号}组成的复合key,值为空,并且只包含有效的交易。

有这样一个问题:值为空,到底怎么查询到历史状态呢?

答:通过历史数据库合成复合key,但复合key中没有交易在区块中的序号,创建一个迭代器,迭代器可以获取包含key的复合key,然后从复合key中提取到交易在区块的序号,然后去区块文件中提取交易,再提取到写集的Value,就可以合成某个key的所有历史值。

因此查询历史状态,需要结合历史数据库和区块文件

各数据库实现

区块文件使用文件直接存储区块,没有使用数据库的原因是:区块是一种自然的追加操作,写入后不再修改,即不会覆盖历史区块,使用文件系统直接存储区块,可以达到区块最快落盘的目的,因为向文件写区块是顺序写,而写数据库是随机写,磁盘(包含HD、SSD)的顺序写性能要高于随机写。

世界状态数据库可以使用leveldb或者CouchDB,CouchDB支持富查询功能,当链码数据按JSON建模时,CouchDB可以提供更好的数据查询,更多CouchDB的信息见文档使用 CouchDB

其他数据库都使用leveldb作为底层存储。

提醒:Fabric支持多通道,逻辑上每个通道拥有一个账本。实现上区块文件是按通道名隔离开了,使用leveldb的各数据库,被各通道共用。

从数据看Peer功能

和账本相关的概念还有区块、交易和状态,从账本的角度看,账本向上支撑了2类功能:

  1. 数据同步:广播与同步区块
  2. 交易背书:模拟执行交易

在下图中,数据同步和交易背书分别使用蓝色和橙色的线圈出,底部剩下的2层为账本。

账本

core/ledger实现了Peer的账本功能,包含了账本中的各项数据库,它依赖common/ledger实现区块文件存储,区块文件存储包含3类:

  • File:把区块保存在文件中,生产环境使用,orderer和peer皆可使用
  • Json:把文件保证JSON格式的文件中,使用在非生产环境,仅供orderer使用
  • Ram :把区块保存在内存中,使用在非生产环境,仅供orderer使用

core/ledger中的:

  • PeerLedger接口,代表Peer账本,主要用来向账本写区块和私有数据,查询区块、交易和私有数据
  • Txsimulator接口,代表交易模拟器,用来模拟执行1条交易
  • QueryExecutor接口用来查询最新的数据
  • HistoryQueryExecutor接口用来查询历史状态

同步数据

同步数据有2种方式:

  • Deliver服务,Peer使用事件从Orderer获取区块
  • Peer向其他节点请求获取某个区间的区块

虽然Peer获取区块的方式有2种,但收到区块,处理区块的方式只有1种,所以下面分3小节介绍。

使用Deliver同步区块

Deliver用来以事件的方式获取区块,场景有2点:

  • Peer从Oderer获取区块
  • 客户端/SDK从Peer获取区块

Fabric 1.4源码解读 8:Orderer和Peer的交互中已经介绍了Peer从Orderer获取区块,这里再做一点补充。

Deliver服务是Orderer和Peer都使用的功能,但Orderer并没有core/ledger,所以从设计和实现上,common/deliver是从common/ledger中直接读区块,而不是core/ledger读区块。

Peer请求区块

每个Peer可以通过Gossip得知同通道的、所连接的Peer信息,其中一项就是对方Peer账本的高度。账本高度低的Peer可以向高度高的Peer发送StateRequest,请求获取某个连续区间的区块。

Peer上负责StateRequest的是gossip/state模块,它负责:

  • 创建StateRequest请求
  • 处理StateRequest请求,生成StateRequest响应
  • 处理StateRequest响应

创建请求:假设Peer1比Peer2少50个区块,并且配置了Peer每次最多取10个区块,Peer1会创建5个StateRequest请求,顺序的向Peer2进行请求,Peer1收到前一个请求的响应后,才发出下一个请求。

处理请求:实际是从账本读取所请求区块的过程,这个过程主要是读取区块文件,如果区块涉及私密数据,也涉及读取私密数据库,这部分功能主要由gossip/privdata完成,gossip/state把读到的区块和私密数据生成请求响应。

Peer处理收到的区块

Peer从Orderer和其他Peer哪获取的区块,最终都会进入到gossip/state,区块会被放入到一个区块缓冲区:PayloadsBuffer,默认大小为存储200个区块。

每个通道账本都有一个goroutine,从各自的PayloadsBuffer拿下一个高度的区块,交给gossip/privdata进行区块的验证和写入。

验证区块

这部分功能由core/handler/validation完成。在Fabric 1.4中,StateImpl会调用QueryExecutor查询状态,但实际StateImpl没有被调用。

验证区块主要是并发验证区块中的交易:

  • 验证交易中的字段
  • 验证是否满足背书策略
  • 验证交易是否调用最新版本的链码
  • 验证交易是否重复

交易验证的结果,即交易是否有效,并不会保存在交易中,这样区块中记录所有交易的DataHash就变化了。区块中所有交易的有效性存储在区块的元数据中,区块元数据中有一个有效性数组,依次存放了每个交易的有效性,使用数组的下标,与交易在数组中的顺序,一一对应。

交易验证后,会修改区块的元数据,把无效的交易设置为响应的无效序号。

如果缺失区块的私有数据,gossip/privdata会创建获取私有数据的请求,并获取私有数据,当区块和私有数据都准备齐全后,开始commit区块和私有数据。

区块写入账本

包含2大块:

  • 交易MVCC验证
    • Fabric要求世界状态数据库支持MVCC,即多版本并发控制,以便交易能够并发执行(背书),在真正修改状态的时候,才判断读写的数据是否冲突,冲突的交易会被标记为无效。关于MVCC我们在下文的背书部分再详细介绍。
  • 把区块写入数据库,以及修改各数据库:
    • 把区块写入到区块文件
    • 把区块、交易的索引写入到索引数据库
    • 有效交易的写集更新到世界状态
    • 提交历史数据库
    • 提交私密数据库
写区块完成后

写区块完成后,还需要做一些修剪操作:私密数据是有有效期的,比如存活100个区块时间,假如在1000高度写入了某私有数据,第1100写入账本后,私密数据就要从私密数据库被抹除。

背书

Peer除了记账的另外一个角色就是背书,背书很重要的一个环节就是模拟执行交易。

MVCC

Fabric为了提供更高的系统性能,支持并发的执行交易,交易在执行过程中会读写世界状态数据库,也就存在并发访问数据库的场景,为了安全的访问数据库数据,就需要对数据库的并发进行限制。

Draveness在浅谈数据库并发控制 - 锁和 MVCC种介绍了并发控制3种手段:悲观锁、乐观锁和MVCC。

Fabric选择了MVCC,它要求世界状态数据库支持MVCC,本质上讲任何支持MVCC的数据库,都可以用来实现状态数据库。

MVCC是多版本并发控制的缩写,它是一种思想,而不是一种具体的算法,所以不同的数据库实现的MVCC不同。

在MVCC的数据存储中,数据有版本的概念,写一个数据的值,实际上是创建了一个新的版本来保存数据。

MVCC可以实现并发读写的能力,当读数据时,先确定待读数据的版本,然后从该版本读取数据,写数据时,创建新的版本保存数据。读数据必然是已经存在的版本,而写数据是新的版本,因此读写可以并行。

Fabric对MVCC的使用

背书节点在模拟执行交易的过程中,会生成读写集,读集和写集分别是所有待写key读出来时的版本和待写入的新值

交易并发执行到写入区块的过程中存在2种读写冲突的情况:

  1. 同一个区块中的前后两笔交易,后面的交易读集包含某个key,但key在前面交易的写集:也就说后面交易读的是老版本的数据,是一种脏读的情况
  2. 区块中交易的读集的某个key,某之前区块的交易写集修改:背书跟写区块是并发执行的,背书时产生的写集,直到写区块才会更新到世界状态数据库,这里存在一段时间,即key已经有了新版本的数据,只是还没有提交到数据库。如果这期间有新的交易模拟执行,就会读到老版本数据,也是一种脏读的情况

有效交易的写集会被应用到世界状态数据库,被修改数据都会有一个新的版本,这个版本是逻辑版本,成为Hight,由{区块高度,交易在区块内的顺序}组成。

注:验证函数为 validateTx,读写集冲突错误为 TxValidationCode_MVCC_READ_CONFLICT ,另一个读写冲突错误为 TxValidationCode_PHANTOM_READ_CONFLICT, 因为执行过程中有RangeQuery,查询某个区间的Key,也需要验证这些Key是否冲突,底层本质还是读写集的验证。

总结

本文从账本的视角,介绍了Peer的账本,以及和账本打交道的功能。

真正企业级的区块链、大用户规模的区块链,必然能够支撑大量的并发交易,这对账本以及底层存储,都会提出更高的性能要求、磁盘利用率要求,所以理解和掌握账本和存储机制是非常有必要的。

参考