0%

什么是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不可或缺的一部分。

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