0%

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的思维导图分享给大家。

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

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具有哪些能力才取得了这些成果?

结语

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

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

区块链最核心的是可信数据,所有的功能与设计根源都是数据。本次从数据存储的角度,看一看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的账本,以及和账本打交道的功能。

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

参考

引言

Write Ahead Logging,简称WAL,也被翻译成预写式日志,是数据库技术中实现事务日志(Transaction Journal)的一种标准方法,可以实现单机事务的原子性,同时可以提高数据库的写入效率。

思考如下场景,如何确保原子性:写操作修改数据库中a和b的值,二者是一个事务,需要把a和b的最新值持久化到磁盘,假如保存完a的值,系统宕机了,重新启动后,a的值已经写入,但b待写入的值已经丢失,如何发现事务没有完成呢?如何保证事务的原子性呢?

可以为事务加锁,也为事务增加标志位,修改完磁盘数据后,标志位设置事务为完成,事务状态保存在磁盘中,假使保存事务状态的过程中宕机了,就把事务回滚掉。实现REDO和UNDO,就能实现原子性。

数据库中针对CrashRecovery的解决方案是WAL。

原理

WAL的核心思想是先写日志再写数据文件,修改数据文件必须发生在修改操作记录在日志文件之后。

本文的日志指事务的操作日志,本文提到的日志都是指事务日志,不再特殊声明。

WAL

我们看WAL怎么解决宕机和恢复的问题

  • 写WAL前宕机了,重启后,数据处于事务未执行的状态。
  • 写WAL时宕机了,重启后,可以检查到WAL数据不正确,回滚当事务前的状态。
  • 写WAL后宕机了,重启后,把WAL中记录的操作,应用到数据库文件中,得到事务执行后的状态。

如此,保证了数据的恢复和事务的原子性。

上面提到的都是写操作,看一下使用WAL时的读操作。WAL中可能包含了未写入到数据库文件中的最新值,如果读最新值就需要从WAL中读取,如果WAL中未读到,从数据库读到的就是最新的数据。

检查点:写入到WAL文件中的操作记录并不一定会立刻应用到数据库文件上,这个过程是异步的,设计检查点来记录已经被应用到数据库文件上的操作序号,检查点后面的操作记录等待被应用到数据库文件上。

优点

WAL的作用是解决宕机和恢复的问题,同时也有其他优点:

  1. 提高写数据的性能
    1. WAL是顺序写,数据库文件是随机写,顺序写性能高于随机写
    2. 减少写磁盘次数
      1. 不直接修改数据库真实数据
      2. 合并若干小的事务,一次性commit到数据库
  2. 保证事务原子性
  3. 保证事务一致性
  4. 并发读写,比如SQLite中,读写、读读都是可以并行的,比如读时需要找到WAL某个值最后写入的位置,就可以从该位置读数据,而写操作是在WAL文件后Append,二者并行。但写写不能并行,因为2次写操作都要向WAL文件Append数据,无法同时进行。
  5. WAL文件中记录了数据的历史版本,因此可以读取历史版本的值,甚至把状态回滚到某个历史版本。

缺点

SQLite提到了WAL的几项缺点:

  1. WAL需要VFS的支持。
  2. 所有使用数据库的进程必须在同一个机器上,以为WAL是单机的。
  3. 多读少写的场景WAL比rollback-journal类型要慢1%~2%。

使用场景

WAL几乎是数据存储(数据库只是数据存储的一个类别,只不过这个类别很大)的标配:

  • Raft可以使用WAL保存log Entry以及状态
  • 数据库
    • PgSQL使用WAL实现事务日志实现事务原子性、一致性,提升性能
    • SQLite使用WAL实现原子事务和回滚
    • MySQL使用WAL保证数据不丢失的情况下提升性能
    • leveldb也使用WAL提升性能,保证操作原子性

资料

Peer与Orderer的交互主要是组织的Peer主节点从Orderer获取区块,本文就来介绍,Peer是如何从Orderer获取区块的,顺带介绍为何Peer从Orderer获取的区块“好慢”。

网络拓扑

假设存在如下的Fabric网络拓扑情况,本文使用此拓扑进行介绍Orderer到Peer的区块传播情况:

网络中存在两家组织:Org1和Org2,它们分别拥有Peer1作为主节点,连向了排序服务的Orderer1节点。

网络中存在2个应用channel:channel1和channel2,它们的账本分别是channel1 ledger和channel2 ledger,Org1和Org2都加入了这2个channel。

channel间是隔离的,所以Peer和Orderer对不同的channel都会分别处理

宏观视角

下图展示了Orderer向Peer传递区块的宏观视角,能够展示多个通道在Orderer和Peer间传递区块的情况

  1. Orderer上有2个通道的账本,每个Peer分别有2个Deliver Server对应2个通道的账本,从账本读取区块,发送给Peer。
  2. 每个Peer有2个Deliver Client,也对应2个通道,接收Orderer发来的区块,加入到缓冲区Payloads Buffer,然后再从Payloads Buffer中提取区块,验证后写入对应的通道账本。

后面,介绍区块同步某个通道区块的情况。

单通道区块同步

Peer利用Deliver从Orderer获取区块,就像SDK利用Deliver从Peer获取区块一样,Deliver服务端的处理是一样的,Deliver客户端的处理就由SDK、Peer自行处理了。

Deliver本质是一个事件订阅接口,Leading Peer启动后,会为每个通道,分别向Orderer节点注册区块事件,并且指定结束的区块高度为uint类型的最大值,这是为了不停的从orderer获取区块。

通过建立的gRPC连接,Orderer源源不断的向Peer发送区块,具体流程,如下图所示:

  1. Orderer调用deliverBlock函数,该函数是循环函数,获取区块直到指定高度。
  2. 每当有新区块产生,deliverBlock能利用NextBlock从通道账本中读到最新的区块,如果没有最新区块,NextBlock会阻塞。
  3. deliverBlock把获取的区块封装成区块事件,发送给Peer(写入到gRPC缓冲区)。
  4. Peer从gRPC读到区块事件,把区块提取出来后,加入到Payloads Buffer,Payloads Buffer默认大小为200(通过源码和日志发现,Payloads Buffer实际存储202个区块),如果Orderer想向Peer发送更多的区块,必须等Payloads Buffer被消费,有空闲的位置才可以。
  5. deliverPayloads为循环函数,不断消费Payloads Buffer中的区块,执行区块验证,添加区块剩余元数据,最后写入通道账本。
  6. 写通道账本包含区块写入区块账本,修改世界状态数据库,历史索引等。

为何Peer从Orderer获取区块慢?

在性能测试过程中,我们发现Orderer排序完成后,Peer还在不断的从Orderer获取区块,而不是所有排序后的区块都先发送给Peer,Peer缓存起来,慢慢去验证?

上面提到Orderer向Peer发送的区块,Peer收到后先存到Payloads Buffer中,Buffer有空闲位置的时候,Orderer发送的区块才能写入Buffer,deliverBlock 1次循环才能完成,才可以发送下一个区块。

但Payloads Buffer大小是有限的,当Buffer满后,Orderer发送区块的操作也会收到阻塞。

我们可以把Orderer和Peer间发送区块可以抽象一下,它们就是生产者-消费者模型,它们中间是缓冲区,Orderer是生产者,向缓冲区写数据,Peer是消费者,从缓冲区读数据,缓冲区满了会阻塞生产者写数据。

所以Orderer向Peer发送数据的快慢,取决消费者的速度,即取决于deliverPayloads处理一个区块的快慢

deliverPayloads慢在把区块写入区块账本,也就是写账本,成了整个网络的瓶颈。

为何不让Peer缓存所有未处理的区块?

从我们测试的情况看,Orderer排序的速度远快于Peer,Peer和Orderer的高度差可以达到10万+,如果让Peer来缓存这些区块,然后再做处理是需要耗费大量的空间。

在生产者-消费者模型中,只需要要消费者时刻都有数据处理即可。虽然Orderer和Peer之间是网络传输,测试网络比较可靠,传输速度远比Peer处理区块要快。

Payloads Buffer可以让网络传输区块和Peer处理区块并行,这样缩短了一个区块从Orderer中发出,到Peer写入区块到账本的总时间,提升Fabric网络整体性能。

Orderer介绍

排序服务由一组排序节点组成,它接收客户端提交的交易,把交易打包成区块,确保排序节点间达成一致的区块内容和顺序,提供区块链的一致性服务。

图片源自《区块链原理、设计与应用》,当时Fabric还不支持raft

排序服务所提供的一致性,依赖确定性的共识算法,而非比特币、以太坊等公有链,所采用的概率性共识算法。确定性的共识算法是区块上链,即不可修改。Fabric所采用的共识算法有Solo、Kafka、EtcdRaft。

客户端通过Broadcast接口向Orderer提交背书过的交易,客户端(此处广义指用户客户端和Peer节点通过Deliver接口订阅区块事件,从Orderer获取区块

更多的排序服务介绍请参考这篇官方文档排序服务

架构

Architecture of 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共识。

gRPC通信

Orderer只有2个gRPC接口:

  • Broadcast:用来接收客户端提交的待排序交易
  • Deliver:客户端(包括Peer节点)用来从Orderer节点获取已经达成一致的区块

其中,Broadcast是Orderer独有的,而Devliver是通用的,因为客户端也可以利用Deliver接口从Peer节点获取区块、交易等。

关于Broadcast和Orderer更多介绍可以参考杨保华的2篇笔记:

用来解析orderer节点的配置文件: orderer.yaml,并保存入内存。

该配置文件中的配置,是节点本地的配置,不需要Orderer节点间统一的配置,因此不需要上链,相关配置有:

  • 网络相关配置
  • 账本类型、位置
  • raft文件位置

而上链的配置,被称为通道配置,需要使用配置交易进行更新,这部分配置,写在configtx.yaml中,和Orderer相关的有:

  • 共识类型
  • 区块大小
  • 切区块的时间
  • 区块内交易数
  • 各种共识的相关配置

Metadata

区块中有4个元数据:

  • 区块签名,存放orderer对区块的SignatureHeader
  • 最新配置区块的高度,方便获取当前通道最新配置
  • 交易过滤,为数组,存放区块内所有交易的有效性,使用数字代表无效的原因,由验证交易的记账节点填写
  • orderer相关元数据,不同的共识类型,该元数据不同

区块Header中记录了Data.Hash(),Data是所有交易后序列化的结果,但不包含区块元数据,所以区块元数据是可以在产生区块后修改的。即,即使元数据上链了,但这数据是可以修改的,只不过修改也没有什么意义。

MsgProcessor

orderer收到交易后需要对交易进行多项检查,不同的通道可以设置不同的MsgProcessor,也就可以进行不同的检查。

当前Processor分2个:

  • 应用通道的叫StandardChannel
  • 系统通道的叫SystemChannel

StandardChannel会对交易进行以下检查:

  • 交易内容不能为空
  • 交易大小不能超过区块大小最大值(默认10MB)
  • 交易交易签名不符合签名策略
  • 签名者证书是否过期

SystemChannel只比StandardChannel多一项:系统配置检查,用来检查以下交易中包含的配置,配置项是否有缺失,或者此项配置是否允许更新等。

BlockCutter

BlockCutter用来把收到的交易分成多个组,每组交易会打包到一个区块中。而分组的过程,就是切块,每组交易被称为一个Batch,它有一个缓冲区用来存放待切块交易。

切块有3个可配置条件:

  • 缓冲区内交易数,达到区块包含的交易上限(默认500)
  • 缓冲区内交易总大小,达到区块大小上限(默认10MB)
  • 缓冲区存在交易,并且未出块的时间,达到切块超时时间(默认2s)

切块有1个不可配置条件:

  • 缓冲区收到配置交易,配置交易要放到单独区块,如果缓冲区有交易,缓冲区已有交易会切到1个区块

超多刚接触Fabric的人有这样一个疑问:排序节点是按什么规则对交易排序的?

按什么顺序对交易排序并不重要,只要交易在区块内的顺序是一致的,然后所有记账节点,按交易在区块内的顺序,处理交易,最后得到的状态必然是一致的,这也是区块链保持一致性的原理。

再回过头来说一下实现是什么顺序:哪个交易先写入BlockCutter的缓冲区,哪个交易就在前面,仅此而已。

BlockWriter

Orderer的BlockWriter是基于common/ledger实现的,它用来保存区块文件,不包含状态数据库等其他数据库,其中有3类区块文件:ram,json和file,file是Orderer和Peer都可使用的,另外2个只能Orderer使用。

BlockWriter用来向Peer的账本追加区块,但追加区块之前,还需要做另外1件事情,设置区块的元数据。

区块元数据包含:

  • 区块签名,存放orderer对区块的SignatureHeader
  • 最新配置区块的高度,方便获取当前通道最新配置
  • 交易过滤,为数组,存放区块内所有交易的有效性,使用数字代表无效的原因,由验证交易的记账节点填写
  • orderer相关元数据,不同的共识类型,该元数据不同

但此时只设置其中的3个:区块签名、配置区块高度、orderer相关的元数据。因为交易的有效性在记账节点检查后才能设置。

为何不在创建区块的时候就设置这些元数据信息,而是在区块经过共识之后?

共识的过程会传播区块,只让区块包含必要的信息,可以减少区块大小,降低通信量。但元数据占用大小非常小,所以这未必是真实原因。

BlockWriter还有另外一个功能:根据一个Batch创建下一个高度的区块。一个区块包含了:

  • Header:区块高度、前一个区块Hash、Data的哈希值
  • Data:被序列化的交易列表
  • Metadata:区块元数据

Header只记录Data的哈希值,不包含Metadata哈希值,这样的目的是,在区块创建之后,仍能修改区块。

Orderer节点启动

根据Fabric 1.4源码梳理Orderer启动步骤:

  • 加载配置文件
  • 设置Logger
  • 设置本地MSP
  • 核心启动部分:
    • 加载创世块
    • 创建账本工厂
    • 创建本机gRPCServer
    • 如果共识需要集群(raft),创建集群gRPCServer
    • 创建Registrar:设置好共识插件,启动各通道,如果共识是raft,还会设置集群的gRPC接口处理函数Step
    • 创建本机server:它是原子广播的处理服务,融合了Broadcast处理函数、deliver处理函数和registrar
    • 开启profile
    • 启动集群gRPC服务
    • 启动本机gRPC服务

启动流程图可请参考杨宝华的笔记Orderer 节点启动过程,笔记可能是老版本的Fabric,但依然有参考价值。

Orderer处理交易的流程

普通交易在Orderer中的流程

交易是区块链的核心,交易在Orderer中的流程分3阶段:

  1. Orderer 的 Broadcast 接口收到来自客户端提交的交易,会获取交易所在的链的资源,并进行首次检查,然后提交给该链的共识,对交易进行排序,最后向客户端发送响应,为下图蓝色部分。
  2. 共识实例是单独运行的,也就是说Orderer把交易交给共识后,共识可能还在处理交易,然而Orderer已经开始向客户端发送提交交易的响应。共识如果发现排序服务的配置如果进行了更新,会再次检查交易,然后利用把Pending的交易分割成一组,然后打包成区块,然后共识机制确保各Orderer节点对区块达成一致,最后将区块写入账本。为下图绿色部分。
  3. Peer会向Orderer订阅区块事件,每当新区块被Orderer写入账本时,Orderer会把新区块以区块事件的方式,发送给Peer。为下图换色部分。

上面提到Orderer和共识实例分别会对交易进行2次检查,这些检查是相同的,为何要进行两次检查呢?

代码如下:ProcessMessage 会调用ProcessNormalMsg,对交易进行第一次检查,如果有错误,会向客户端返回错误响应。 SomeConsensurFunc 是一个假的函数名称,但3种共识插件实现,都包含相同的代码片,当消息中 configSeq < seq 时,再次对交易进行检查,如果错误,则丢次此条交易。configSeq是Order函数传入的,即第一次检查交易时的配置号,seq为共识当前运行时的配置号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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次校验的频率也是非常低。这种方式,比只在共识插件校验,会拥有更高的整体性能。

配置交易在Orderer中的流程

配置交易可以用来创建通道、更新通道配置,与普通交易的处理流程总体是相似的,只不过多了一些地方或者使用不同的函数,比如:

  • 交易检查函数不是ProcessNormalMsg,而是ProcessConfigMsg
  • 配置交易单独打包在1个区块
  • 配置交易写入账本后,要让配置生效,即Orderer应用最新的配置

使用Raft共识,交易在Orderer中的流程

上面2中流程都是与具体共识算法无关的,这里补充一个Raft共识的。

使用Raft共识的链处理交易包含了上图中的4步:

  • 交易:处理交易
  • 区块:创建区块
  • Raft:使用Raft对区块达成共识
  • 账本:写区块元数据,把区块写入到账本

如果把图中提到的:转发和Raft去掉,就是以Solo为共识的链的过程。

下图是更加细化一层的,如果看不懂,建议先读下Etcd Raft架构设计和源码剖析2:数据流这篇文章。

红色圈出来的是etcd/raft的实现,蓝色圈出来的是Fabric使用raft为共识的部分,外面的Broadcast、Deliver是属于Orderer但不属于某条链。

这张图和etcd与raft交互没有太多不同,只有2个地方:

  1. chains要把交易转化为区块,再交给raft去共识
  2. chains的Apply并不是去修改状态机,而是把取消写到账本

源码简介

Orderer的代码位于fabric/orderer,其目录结构如下,标注了每个目录结构的功能:

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
➜  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的时候,建议以下顺序:

  • 核心的数据结构,主要在multichannel、consensus.go:Fabric 1.4源码解读 6:Orderer核心数据结构
  • Orderer的启动
  • Broadcast接口
  • msgprocessor
  • 通过Solo掌握共识插件需要做哪些工作
  • 切块:blockcutter
  • 写区块:BlockWriter、metadata

总结

本文从宏观的角度介绍了Orderer的功能、核心组成,以及交易在Orderer中的流程,Peer如何从Orderer获取区块。