0%

测试环境版本

测试机采用的Ubuntu 16.04 与 Linux 4.4.0 内核版本:

1
2
3
4
5
[~]$ cat /etc/issue
Ubuntu 16.04.4 LTS \n \l
[~]$
[~]$ cat /proc/version
Linux version 4.4.0-117-generic (buildd@lgw01-amd64-057) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9) ) #141-Ubuntu SMP Tue Mar 13 12:01:47 UTC 2018

提醒:Linux内核版本至少要大于 4.3 这样cgroup的功能才是全的,否则Linux内核版本过低,由于功能不全可能无法运行提供的Demo,目前已知无法运行的内核版本有:Linux version 3.10.0

Cgroup memory子系统介绍

cgroup的memory子系统全称为 Memory Resource Controller ,它能够限制cgroup中所有任务的使用的内存和交换内存进行限制,并且采取control措施:当OOM时,是否要kill进程。

memroy包含了很多设置指标和统计指标:

1
2
3
4
5
6
7
8
[/sys/fs/cgroup/memory/system.slice/docker-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f.scope]$ ls memory.*
memory.failcnt memory.kmem.tcp.limit_in_bytes memory.memsw.limit_in_bytes memory.soft_limit_in_bytes
memory.force_empty memory.kmem.tcp.max_usage_in_bytes memory.memsw.max_usage_in_bytes memory.stat
memory.kmem.failcnt memory.kmem.tcp.usage_in_bytes memory.memsw.usage_in_bytes memory.swappiness
memory.kmem.limit_in_bytes memory.kmem.usage_in_bytes memory.move_charge_at_immigrate memory.usage_in_bytes
memory.kmem.max_usage_in_bytes memory.limit_in_bytes memory.numa_stat memory.use_hierarchy
memory.kmem.slabinfo memory.max_usage_in_bytes memory.oom_control
memory.kmem.tcp.failcnt memory.memsw.failcnt memory.pressure_level

下图进行了汇总,虚线所圈出的指标为常用指标,每个指标的含义也如图所标注:

cgroup memory subsystem

所有指标的含义可以参考Linux Kernel关于cgroup memory的介绍。

利用Docker演示Cgroup内存限制

  1. 创建一个容器,限制为内存为128MB
1
2
[~]$ docker run --rm -itd -m 128m stress:16.04
fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f
  1. 容器内利用stress使用100MB内存
1
2
3
[~]$ docker exec -it fda7bbf29 bash
root@fda7bbf297d9:/# stress --vm-bytes 100m --vm-keep -m 1
stress: info: [23739] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
  1. 在memory子系统目录下,利用容器id找到与当前容器相关的cgroup目录
1
2
3
[/sys/fs/cgroup/memory/system.slice]$ find . -name "*fda7bbf29*" -print
./var-lib-docker-containers-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f-shm.mount
./docker-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f.scope

./docker-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f.scope 目录为当前容器的内存cgroup节点。

  1. 查看该容器的内存使用量、限制,以及统计信息
1
2
3
4
5
6
7
8
9
10
11
[/sys/fs/cgroup/memory/system.slice/docker-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f.scope]$ cat memory.usage_in_bytes memory.limit_in_bytes memory.stat
106049536 // memory.usage_in_bytes
134217728 // memory.limit_in_bytes
cache 0 // 以下为memory.stat
rss 105943040
swap 0
...
hierarchical_memory_limit 134217728
hierarchical_memsw_limit 268435456
...
[/sys/fs/cgroup/memory/system.slice/docker-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f.scope]$
  • 使用量为 : 106049536 / 1024 / 1024 = 101.14 MB
  • 限制为 : 134217728 / 1024 / 1024 = 128MB

stat文件:

  • rss :105943040 / 1024 / 1024 = 101.03 MB
  • hierarchical_memory_limit : 134217728 / 1024 / 1024 = 128MB

stat中rss的值与 usage_in_bytes 有稍微的出入,原因是 usage_in_bytes 的值为近视值,而之所以近似,是因为内核采用的是异步统计,造成统计值和当下的值存在误差。

该cgroup中所有tasks所占用的真实内存可以使用:stat.rss + stat.cache + stat.swap ,在上面的例子中 cache 和 swap 都为0,所以 rss 的值就是真实的内存使用量。

之所以存在 usage_in_bytes , 这样做的目的是通过一个值可以快速获取内存的使用量,而无需进行计算。

利用docker.stats 查看内存占用情况:

1
2
3
[/sys/fs/cgroup/memory/system.slice/docker-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f.scope]$ docker stats
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
fda7bbf297d9 99.50% 101.1 MiB / 128 MiB 79.01% 648 B / 648 B 0 B / 0 B 4

可以看到usage和limit分别为101.1MB和128MB,usage与cgroup中 usage_in_bytes 是一致的,limit与容器启动时的配置一致。

top命令查看进程占用内存情况:

1
2
3
4
5
[/sys/fs/cgroup/memory/system.slice/docker-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f.scope]$ top
....

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
13025 root 20 0 109872 102336 36 R 93.8 1.3 6:39.09 stress

可以看到RES为 102336 KB,即 99.9 MB,小于cgroup中统计的内存使用量,原因是因为cgroup中除了stress还有其他任务,比如docker中运行的ssh。

可以查看该group的进程:

1
2
3
4
5
6
[/sys/fs/cgroup/memory/system.slice/docker-fda7bbf297d9300894c10c5514c32c70a50987ae99cad5731234058d9f6e2b7f.scope]$ cat cgroup.procs
13780
13792
13793
21124
21221

pstree -p 可以查看整个进程树:

利用Go演示Cgroup内存限制

测试源码

cgroup的演示源码 ,关于源码中的/proc/self/exe补充小知识

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
61
62
63
64
65
66
67
package main

// 参考《自动动手写Docker》

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
"syscall"
)

const CgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"

func main() {
if os.Args[0] == "/proc/self/exe" {
fmt.Println("---------- 2 ------------")
fmt.Printf("Current pid: %d\n", syscall.Getpid())

// 创建stress子进程,施加内存压力
allocMemSize := "99m" // 另外1项测试为99m
fmt.Printf("allocMemSize: %v\n", allocMemSize)
stressCmd := fmt.Sprintf("stress --vm-bytes %s --vm-keep -m 1", allocMemSize)
cmd := exec.Command("sh", "-c", stressCmd)
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
fmt.Printf("stress run error: %v", err)
os.Exit(-1)
}
}

fmt.Println("---------- 1 ------------")
cmd := exec.Command("/proc/self/exe")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS | syscall.CLONE_NEWPID,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

// 启动子进程
if err := cmd.Start(); err != nil {
fmt.Printf("/proc/self/exe start error: %v", err)
os.Exit(-1)
}

cmdPid := cmd.Process.Pid
fmt.Printf("cmdPid: %d\n", cmdPid)

// 创建子cgroup
memoryGroup := path.Join(CgroupMemoryHierarchyMount, "test_memory_limit")
os.Mkdir(memoryGroup, 0755)
// 设定内存限制
ioutil.WriteFile(path.Join(memoryGroup, "memory.limit_in_bytes"),
[]byte("100m"), 0644)
// 将进程加入cgroup
ioutil.WriteFile(path.Join(memoryGroup, "tasks"),
[]byte(strconv.Itoa(cmdPid)), 0644)

cmd.Process.Wait()
}

源码运行解读:

  1. 使用go run运行程序,或build后运行程序时,程序的名字是02.1.cgroup,所以不满足os.Args[0] == "/proc/self/exe"会被跳过。
  2. 然后使用"/proc/self/exe"新建了子进程,子进程此时叫:"/proc/self/exe"
  3. 创建cgroup test_memory_limit,然后设置内存限制为100MB
  4. 把子进程加入到cgroup test_memory_limit
  5. 等待子进程结束
  6. 子进程干了啥呢?子进程其实还是当前程序,只不过它的名字是"/proc/self/exe",符合最初的if语句,之后它会创建stress子进程,然后运行stress,可以修改allocMemSize设置stress所要占用的内存

不超越内存限制情况

源码默认在启动stress时,stress占用99m内存,cgroup限制最多使用100m内存。

1
2
3
4
5
6
7
[~/workspace/notes/docker/codes]$ go run 02.1.cgroup.go
---------- 1 ------------
cmdPid: 2533
---------- 2 ------------
Current pid: 1
allocMemSize: 99m
stress: info: [6] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd

可以看到,子进程"/proc/self/exe"运行后取得的pid为 2533 ,在新的Namespace中,子进程"/proc/self/exe"的pid已经变成1,然后利用stress打了99M内存。

使用top查看资源使用情况,stress进程内存RES大约为99M,pid 为 2539

1
2
 PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
2539 root 20 0 103940 101680 284 R 93.8 9.9 0:06.09 stress
1
2
3
4
5
6
7
8
9
10
11
12
13
[/sys/fs/cgroup/memory/test_memory_limit]$ cat memory.limit_in_bytes
104857600
[/sys/fs/cgroup/memory/test_memory_limit]$ # 104857600 刚好为100MB
[/sys/fs/cgroup/memory/test_memory_limit]$ cat memory.usage_in_bytes
2617344
[/sys/fs/cgroup/memory/test_memory_limit]$ cat tasks
2533 <--- /prof/self/exe进程
2534
2535
2536
2537
2538
2539 <--- stress进程

tasks下都是在cgroup test_memory_limit 中的进程,这些是Host中真实的进程号,通过pstree -p查看进程树,看看这些都是哪些进程:

Cgroup限制内存的进程树

进程树佐证了前面的代码执行流程分析大致是对的,只不过这其中还涉及一些创建子进程的具体手段,比如stress是通过sh命令创建出来的。

内存超过限制被Kill情况

内存超过cgroup限制的内存会怎么样?会OOM吗?

如果将stress内存提高到占用101MB,大于cgroup中内存的限制100M时,整个group中的进程就会被Kill。

修改代码,将 allocMemSize 设置为 101m ,然后重新运行程序。

1
2
3
4
5
6
7
8
9
10
11
12
[~/notes/docker/codes]$ go run 02.1.cgroup.go                                                                        *[master]
---------- 1 ------------
cmdPid: 21492
---------- 2 ------------
Current pid: 1
allocMemSize: 101m
stress: info: [6] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [6] (415) <-- worker 7 got signal 9
stress: WARN: [6] (417) now reaping child worker processes
stress: FAIL: [6] (421) kill error: No such process
stress: FAIL: [6] (451) failed run completed in 0s
2020/08/27 17:38:52 exit status 1

stress: FAIL: [6] (415) <-- worker 7 got signal 9 说明收到了信号9,即SIGKILL 。

补充小知识

在演示源码中,使用到"/proc/self/exe",它在Linux是一个特殊的软链接,它指向当前正在运行的程序,比如执行ll查看该文件时,它就执行了/usr/bin/ls,因为当前的程序是ls

1
2
[~]$ ll /proc/self/exe
lrwxrwxrwx 1 centos centos 0 8月 27 12:44 /proc/self/exe -> /usr/bin/ls

演示代码中的技巧就是通过"/proc/self/exe"重新启动一个子进程,只不过进程名称叫"/proc/self/exe"而已。如果代码中没有那句if判断,又会执行到创建子进程,最终会导致递归溢出。

总结

memory是cgroup的一个子系统,主要用来控制一组进程的内存资源,对最大使用量进行限制和控制。

参考资料

  1. Linux Kernel关于cgroup memory
  2. 阿里同学的书《自己动手写Docker》

什么是Cgroup

Cgroup 是 Control Group 的缩写,提供对一组进程,及未来子进程的资源限制、控制、统计能力,包括CPU、内存、磁盘、网络。

  • 限制:限制的资源最大使用量阈值。比如不能超过128MB内存,CPU使用率不得超过50%,或者只能是否CPU的某哪几个核。
  • 控制:超过资源使用最大阈值时,进程会被控制,不任由它发展。比如cgroup内所有tasks的内存使用量超过阈值的结果就是被KILL,CPU使用率不得超过设定值。
  • 统计:统计资源的使用情况等指标。比如cgroup内tasks的内存使用量,占用CPU的时间。

Cgroup 包含3个组件:

  • cgroup :一组进程,可以加上subsystem
  • subsystem :一组资源控制模块,CPU、内存…
  • hierarchy : 把一组cgroup串成树状结构,这样就能实现cgroup的继承。为什么要继承呢?就如同docker镜像的继承,站在前人的基础之上,免去重复的配置

为什么需要Cgroup

为什么需要Cgroup的问题等价于:为什么需要限制一组进程的资源?

有多种原因,比如:

  1. Linux是一个可以多用户登录的系统,如何限制不同的用户使用不同量的系统资源呢?
  2. 某个系统有64核,由于局部性原理,如果一组进程在64个核上调度,效率比较低,但把这些进程只允许在某几个核上调度,就有较好的局部性,提高效率。这类似与在分布式系统中,某个有状态的请求,最好能分配到上一次处理该请求的机器上一样的道理。

cgroup的文档中还提到一个思路:实现资源限制的技术有多种,为什么使用cgroup?

cgroup是内核实现的,它更轻量、更高效、对内核的热点路径影响最小。

你的Linux支持哪些Cgroup subsystem

查看当前系统支持的subsystem,共12个子系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/sys/fs/cgroup]$ cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 8 4 1
cpu 2 74 1
cpuacct 2 74 1
memory 11 74 1
devices 6 69 1
freezer 10 4 1
net_cls 4 4 1
blkio 9 69 1
perf_event 5 4 1
hugetlb 7 4 1
pids 3 69 1
net_prio 4 4 1

从左到右字段的含义分别是:

  1. subsys_name: subsystem的名字
  2. hierarchy: subsystem所关联到的cgroup树的ID,如果多个subsystem关联到同一颗cgroup树,那么他们的这个字段将一样,比如这里的cpu和cpuacct就一样,表示他们绑定到了同一颗树。如果出现下面的情况,这个字段将为0:
    • 当前subsystem没有和任何cgroup树绑定
    • 当前subsystem已经和cgroup v2的树绑定
    • 当前subsystem没有被内核开启
  3. num_cgroups: subsystem所关联的cgroup树中进程组的个数,也即树上节点的个数
  4. enabled: 1表示开启,0表示没有被开启(可以通过设置内核的启动参数“cgroup_disable”来控制subsystem的开启).

Cgroup的内核文档对各 cgroup 和 subsystem 有详细的介绍,以下是每个 subsystem 功能简记:

  1. cpu :用来限制cgroup的CPU使用率
  2. cpuacct :用来统计cgroup的CPU的使用率
  3. cpuset : 用来绑定cgroup到指定CPU哪个核上和NUMA节点
  4. memory :限制和统计cgroup的内存的使用率,包括process memory, kernel memory, 和swap
  5. devices : 限制cgroup创建(mknod)和访问设备的权限
  6. freezer : suspend和restore一个cgroup中的所有进程
  7. net_cls : 将一个cgroup中进程创建的所有网络包加上一个classid标记,用于tc和iptables。 只对发出去的网络包生效,对收到的网络包不起作用
  8. blkio : 限制cgroup访问块设备的IO速度
  9. perf_event : 对cgroup进行性能监控
  10. net_prio : 针对每个网络接口设置cgroup的访问优先级
  11. hugetlb : 限制cgroup的huge pages的使用量
  12. pids :限制一个cgroup及其子孙cgroup中的总进程数

这些子系统的排列顺序,就是引入Linux内核顺序,最早的是cpu subsystem ,引入自Linux 2.6.24,最晚的是pid subsystem ,引入自 Linux 4.3。

查看子系统和cgroup的挂载

cgroup是通过文件系统实现的,每个目录都是一个cgroup节点,目录中的子目录都是子cgroup节点,这样就形成了 cgroup的 hierarchy 特性。

cgroup会挂载到 /sys/fs/cgroup/目录,该目录下的目录基本都是subsystem,systemd目录除外(它是 systemd 自建在cgroup下的目录,但不是子系统):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[/sys/fs/cgroup]$ ll
total 0
dr-xr-xr-x 6 root root 0 Aug 30 09:30 blkio
lrwxrwxrwx 1 root root 11 Aug 30 09:30 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Aug 30 09:30 cpuacct -> cpu,cpuacct
dr-xr-xr-x 7 root root 0 Aug 30 09:30 cpu,cpuacct
dr-xr-xr-x 3 root root 0 Aug 30 09:30 cpuset
dr-xr-xr-x 6 root root 0 Aug 30 09:30 devices
dr-xr-xr-x 3 root root 0 Aug 30 09:30 freezer
dr-xr-xr-x 3 root root 0 Aug 30 09:30 hugetlb
dr-xr-xr-x 6 root root 0 Aug 30 09:30 memory
lrwxrwxrwx 1 root root 16 Aug 30 09:30 net_cls -> net_cls,net_prio
dr-xr-xr-x 3 root root 0 Aug 30 09:30 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Aug 30 09:30 net_prio -> net_cls,net_prio
dr-xr-xr-x 3 root root 0 Aug 30 09:30 perf_event
dr-xr-xr-x 6 root root 0 Aug 30 09:30 pids
dr-xr-xr-x 6 root root 0 Aug 30 09:30 systemd

发现cpu、cpuacct都指向了 cpu,cpuacct 目录,把它们合成了1个cgroup节点。另外 net_cls 和 net_prio 也都合到了 net_cls,net_prio 节点,也就形成了下面这幅图的样子,并把资源控制分成了5个类别:CPU、内存、网络、进程控制、设备,另外的perf_event是cgroup对自身的监控,不归于资源控制。

子系统挂载到cgroup的虚拟文件系统是通过mount命令实现的,系统启动时自动挂载subsystem到cgroup,查看已经挂载的Cgroup:

1
2
3
4
5
6
7
8
9
10
11
12
[~]$ mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)

查看某个进程所属的cgroup:

1
2
3
4
5
6
7
8
9
10
11
12
13
[/sys/fs/cgroup]$ # $$代表当前进程
[/sys/fs/cgroup]$ cat /proc/$$/cgroup
11:memory:/user.slice/user-1000.slice/session-269.scope
10:freezer:/
9:blkio:/user.slice
8:cpuset:/
7:hugetlb:/
6:devices:/user.slice
5:perf_event:/
4:net_prio,net_cls:/
3:pids:/user.slice
2:cpuacct,cpu:/user.slice/user-1000.slice/session-269.scope
1:name=systemd:/user.slice/user-1000.slice/session-269.scope

每一行从左到右,用:分割依次是:

  • 11: cgroup继承树的节点的ID
  • memory: 当前节点上挂载的子系统
  • /user.slice/user-1000.slice/session-269.scope: cgroup节点相对于cgroup根目录下子系统的相对路径,转换成绝对路径就是:/sys/fs/cgroup/memory/user.slice/user-1000.slice/session-269.scope

再聊cgroup hierarchy

在 cpu,cpuacct 子系统下创建一个测试cgroup节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[/sys/fs/cgroup/cpu,cpuacct]$ sudo mkdir dabin_test_cpu_cgroup
[/sys/fs/cgroup/cpu,cpuacct]$ cd dabin_test_cpu_cgroup
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$ ls
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$ ls cgroup.*
cgroup.clone_children cgroup.event_control cgroup.procs
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$ cat cgroup.clone_children
0
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$ cat cgroup.procs
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$ ls notify_on_release tasks
notify_on_release tasks
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$ cat tasks
[/sys/fs/cgroup/cpu,cpuacct/dabin_test_cpu_cgroup]$ cat notify_on_release
0

cgroup hierarchy (继承树)结构,每个cgroup节点都包含以下几个文件:

  • cgroup.clone_children : 被cpuset控制器使用,值为1时子cgroup初始化时拷贝父cgroup的配置
  • cgroup.procs : cgroup中的线程组id
  • tasks : 当前cgroup包含的进程列表
  • notify_on_release : 值为0或1,1代表当cgroup中的最后1个task退出,并且子cgroup移除时,内核会在继承树根目录运行release_agent文件

总结

cgroup对一组进程的资源进行控制,包括但不限于CPU、内存、网络、磁盘等资源,共12种资源,通过12个subsystem去进行限制、控制。

cgroup由内核使用文件系统实现,文件系统的层级结构实现了cgroup的层级结构,它默认挂载到 /sys/fs/cgroup 目录。

参考资料

  1. Linux Kernel Cgroup的文档
  2. 阿里同学的书《自己动手写Docker》

minikube很好,但某些原因造成国内用起来比较慢,要各种挂代理、Docker镜像加速。

minikube原理

kubectl和kube-apiserver是CS架构,kubectl是操作k8s集群的客户端,kube-apiserver是服务端。

minikube是创建了一个虚拟机minikube vm,然后在虚拟机里创建了1个单机的k8s集群,并把集群部署信息写到~/.kube/config文件,它是kubectl默认使用的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[~]$ ls ~/.kube/config
/Users/shitaibin/.kube/config
[~]$ cat ~/.kube/config
apiVersion: v1
clusters:
- cluster:
certificate-authority: /Users/shitaibin/.minikube/ca.crt
server: https://192.168.99.103:8443
name: minikube
contexts:
- context:
cluster: minikube
user: minikube
name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube
user:
client-certificate: /Users/shitaibin/.minikube/profiles/minikube/client.crt
client-key: /Users/shitaibin/.minikube/profiles/minikube/client.key

文件内容也可以使用 kubectl config view 命令查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[~]$ kubectl config view
apiVersion: v1
clusters:
- cluster:
certificate-authority: /Users/shitaibin/.minikube/ca.crt
server: https://192.168.99.103:8443
name: minikube
contexts:
- context:
cluster: minikube
user: minikube
name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube
user:
client-certificate: /Users/shitaibin/.minikube/profiles/minikube/client.crt
client-key: /Users/shitaibin/.minikube/profiles/minikube/client.key
[~]$

安装软件

  1. 安装minikube,1分钟,如果提供的命令行下载不下来,就浏览器下载下来,放到增加可执行,然后放到bin目录即可:
    https://yq.aliyun.com/articles/691500

  2. centos安装virtualbox,2分钟安装完成:
    https://wiki.centos.org/zh/HowTos/Virtualization/VirtualBox

  3. 安装kubectl:
    https://blog.csdn.net/yuanjunlai141/article/details/79469071

首次启动

启动命令

1
2
3
4
5
minikube start --image-mirror-country cn \
--iso-url=https://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/iso/minikube-v1.7.3.iso \
--registry-mirror="https://a90tkz28.mirror.aliyuncs.com" \
--image-repository="registry.cn-hangzhou.aliyuncs.com/google_containers" \
--kubernetes-version=v1.18.3

使用minikube可以查看帮助flag帮助信息:

  • --image-mirror-country: 需要使用的镜像镜像的国家/地区代码。留空以使用全球代码。对于中国大陆用户,请将其设置为
    cn
  • --registry-mirror: 传递给 Docker 守护进程的注册表镜像。效果最好的镜像加速器:--registry-mirror="https://a90tkz28.mirror.aliyuncs.com" 。使用加速器的原理是,docker deamon会先去加速器寻找镜像,如果找不到才从docker官方仓库拉镜像。如果指定拉某个镜像仓库的镜像,镜像加速器是用不上的。
  • --image-repository : 如果不能从gcr.io拉镜像,配置minikube中docker拉镜像的地方
  • --kubernetes-version: 指定要部署的k8s版本,可以省略

minikube内拉不到镜像的报错:

1
2
3
4
$ kubectl describe pod
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Failed 2m59s (x4 over 4m36s) kubelet, minikube Failed to pull image "kubeguide/redis-master": rpc error: code = Unknown desc = Error response from daemon: Get https://registry-1.docker.io/v2/: proxyconnect tcp: dial tcp 192.168.0.104:1087: connect: connection refused

启动日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ minikube start --image-mirror-country cn \
--iso-url=https://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/iso/minikube-v1.7.3.iso \
--registry-mirror="https://a90tkz28.mirror.aliyuncs.com" \
--image-repository="registry.cn-hangzhou.aliyuncs.com/google_containers"
😄 Darwin 10.15.3 上的 minikube v1.12.3
✨ 根据用户配置使用 virtualbox 驱动程序
✅ 正在使用镜像存储库 registry.cn-hangzhou.aliyuncs.com/google_containers
👍 Starting control plane node minikube in cluster minikube
🔥 Creating virtualbox VM (CPUs=2, Memory=4000MB, Disk=20000MB) ...
💡 Existing disk is missing new features (lz4). To upgrade, run 'minikube delete'
🐳 正在 Docker 19.03.6 中准备 Kubernetes v1.18.3…
🔎 Verifying Kubernetes components...
🌟 Enabled addons: default-storageclass, storage-provisioner
🏄 完成!kubectl 已经配置至 "minikube"

做哪些事?

  1. 创建虚拟机”minikube”
  2. 生成kubectl使用的配置文件,使用该配置连接集群:~/.kube/config
  3. 在虚拟机里的容器上启动k8s
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
61
62
63
64
$ minikube ssh
_ _
_ _ ( ) ( )
___ ___ (_) ___ (_)| |/') _ _ | |_ __
/' _ ` _ `\| |/' _ `\| || , < ( ) ( )| '_`\ /'__`\
| ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )( ___/
(_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____)

$
$ docker info
Client:
Debug Mode: false

Server:
Containers: 18
Running: 15
Paused: 0
Stopped: 3
Images: 11
Server Version: 19.03.6
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 35bd7a5f69c13e1563af8a93431411cd9ecf5021
runc version: dc9208a3303feef5b3839f4323d9beb36df0a9dd
init version: fec3683
Security Options:
seccomp
Profile: default
Kernel Version: 4.19.94
Operating System: Buildroot 2019.02.9
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 3.754GiB
Name: minikube
ID: 6GOT:L6SH:NPBW:ZM44:PVKY:LSEZ:MXW7:LWOB:GB4N:CNXU:S6NJ:KASG
Docker Root Dir: /var/lib/docker
Debug Mode: false
Registry: https://index.docker.io/v1/
Labels:
provider=virtualbox
Experimental: false
Insecure Registries:
10.96.0.0/12
127.0.0.0/8
Registry Mirrors:
https://a90tkz28.mirror.aliyuncs.com/
Live Restore Enabled: false
Product License: Community Engine

$ exit
logout

Registry Mirrors对应的是阿里云镜像加速,HTTP proxy也配置上了,如果启动后,发现没有改变,需要删除过去创建的minikube,全部清理一遍。

minikube常用命令

  • 集群状态: minikube status
  • 暂停和恢复集群,不用的时候把它暂停掉,节约主机的CPU和内存: minikube pause, minikube unpause
  • 停止集群: minikube stop
  • 删除集群,遇到问题时,清理一波数据: minikube delete
  • 查看集群IP,kubectl就是连这个IP: minikube ip
  • 进入minikube虚拟机,整个k8s集群跑在这里面: minikube ssh

kubectl自动补全

zsh在配置文件 ~/.zshrc 中增加:

1
2
source <(kubectl completion zsh)  # 在 zsh 中设置当前 shell 的自动补全
echo "if [ $commands[kubectl] ]; then source <(kubectl completion zsh); fi" >> ~/.zshrc # 在您的 zsh shell 中永久的添加自动补全

bash 在 ~/.bashrc 中增加:

1
2
source <(kubectl completion bash) # 在 bash 中设置当前 shell 的自动补全,要先安装 bash-completion 包。
echo "source <(kubectl completion bash)" >> ~/.bashrc # 在您的 bash shell 中永久的添加自动补全

VSCode已经支持远程开发,可以把代码自动从本地和服务器进行同步。

为了某些实验搞了一条Ubuntu 14.04的服务器,结果VSCode说远程服务器不支持,就只能另谋它路了,利用SFTP实现本地和服务器端的代码同步。

步骤

  1. VSCode应用市场安装SFTP插件
  2. 在项目目录下建立SFTP的配置文件:.vscode/sftp.json,内容如下
1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "root",
"host": "192.168.9.xxx",
"protocol": "sftp",
"port": 22,
"username": "centos",
"privateKeyPath": "/Users/shitaibin/.ssh/id_xxx",
"remotePath": "/home/centos/workspace/docker/notes",
"uploadOnSave": true,
"ignore": [".vscode", ".git", ".DS_Store", "node_modules", "vendor"],
"localPath":"."
}

登录服务器可以使用密码或者私钥,上面文件的示例使用私钥,如果使用密码,增加一项password即可。

uploadOnSave配置项设置为true,能够确保文件保存时,自动上传到服务器,无需手动上传。

  1. 初次上传到服务器

    a. Ctrl + Shift + P,输入SFTP,选择Sync Local -> Remote即可
    b. VSCode底部状态栏,会显示SFTP,如果在动态变化,说明在上传文件

字符串格式日期、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响应发送给客户端
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响应发送给客户端

昵称

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响应发送给客户端
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响应发送给客户端

线条和箭头

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

笔记

1
2
3
sequenceDiagram
Note left of Client: 创建请求
Note right of Gateway: 接收请求
1
2
3
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
1
2
3
4
5
6
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: 发送响应
1
2
3
4
5
6
7
8
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
1
2
3
4
5
6
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
1
2
3
4
5
6
sequenceDiagram
Client ->> Server: 发送请求
activate Server
Server ->> Server: 处理请求
Server ->> Client: 发送响应
deactivate Server

饼图

1
2
3
4
pie
title 硬币正反面的概率
"正面": 0.5
"反面": 0.5
1
2
3
4
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不可或缺的一部分。