0%

在公司都是用现成的K8s集群,没自己搭过,想知道搭建集群涉及哪些组件、做了什么,于是自己搭了一下,没想象的顺利,动作做到位了,也就不会有太多问题。

许多资料都是基于Centos7的,包括《Kubernetes权威指南》,手头只有Ubuntu 16.04,刚好也是支持K8s最低Ubuntu版本,就在Ubuntu上面部署。Ubuntu与Centos部署K8s并没有太大区别,唯一区别是安装kubeadm等软件的不同,由于k8s本身也是运行在容器中,其他的过程二者都相同了,这种设计也极大的方便了k8s集群的搭建。

没有阿里云,搭建一个K8s集群还是挺费劲的

准备工作

  1. /etc/hosts中加入:
1
127.0.0.1 k8s-master
  1. 关闭防火墙:ufw status

  2. 安装Docker,并设置镜像加速器

安装软件

Ubuntu 16.04上利用阿里云安装kubeadm、kubelet、kubectl

1
2
3
4
5
6
7
8
sudo apt-get update && sudo apt-get install -y apt-transport-https curl
curl -s http://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add -
cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb http://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

centos 7上利用阿里云镜像安装kubeadm、kubelet、kubectl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

# 将 SELinux 设置为 permissive 模式(相当于将其禁用)
setenforce 0
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

systemctl enable --now kubelet

二进制程序安装位置:

1
2
3
4
[~]$ which kubectl kubeadm kubectl
/usr/bin/kubectl
/usr/bin/kubeadm
/usr/bin/kubectl

部署Master节点

1
2
3
4
5
kubeadm init \
--kubernetes-version=v1.19.0 \
--image-repository registry.aliyuncs.com/google_containers \
--pod-network-cidr=10.24.0.0/16 \
--ignore-preflight-errors=Swap
  • --image-repository : 使用阿里云提供的k8s镜像仓库,快速下载k8s相关的镜像
  • --ignore-preflight-errors : 部署时忽略swap问题
  • --pod-network-cidr :设置pod的ip区间

遇到错误需要重置集群:kubeadm reset

遇到错误参考:kubernetes安装过程报错及解决方法

拷贝kubectl配置

切回普通用户,拷贝当前集群的配置给kubectl使用:

1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

查看集群信息:

1
2
3
dabin@ubuntu:~$ kubectl cluster-info
Kubernetes master is running at https://192.168.0.103:6443
KubeDNS is running at https://192.168.0.103:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

安装CNI网络插件

集群启动后,缺少网络插件,集群的Pod直接还不能通信。

1
2
3
dabin@ubuntu:~$ kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master NotReady master 5m8s v1.19.0

k8s的文档列举了多种选择,这里提供2种:

weave:

1
kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"

flannel:

1
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

本机选择了weave:

1
2
3
4
5
6
7
dabin@ubuntu:~$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"
serviceaccount/weave-net created
clusterrole.rbac.authorization.k8s.io/weave-net created
clusterrolebinding.rbac.authorization.k8s.io/weave-net created
role.rbac.authorization.k8s.io/weave-net created
rolebinding.rbac.authorization.k8s.io/weave-net created
daemonset.apps/weave-net created

安装之后节点变为Ready:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dabin@ubuntu:~$ kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master Ready master 10m v1.19.0
dabin@ubuntu:~$
dabin@ubuntu:~$ kubectl get -n kube-system pods
NAME READY STATUS RESTARTS AGE
coredns-6d56c8448f-bdgwj 1/1 Running 0 10m
coredns-6d56c8448f-w6nnb 1/1 Running 0 10m
etcd-k8s-master 1/1 Running 0 10m
kube-apiserver-k8s-master 1/1 Running 0 10m
kube-controller-manager-k8s-master 1/1 Running 0 10m
kube-proxy-xtgwn 1/1 Running 0 10m
kube-scheduler-k8s-master 1/1 Running 0 10m
weave-net-4gtcq 2/2 Running 0 93s

开启master调度

master节点默认是不可调度的,不可在master上部署任务,在单节点下,需要开启master可被调度。

1
kubectl taint nodes --all node-role.kubernetes.io/master-

以上集群搭建完毕。

测试

部署一个Pod进行测试,Pod能Running,代表Docker、K8s的配置基本没问题了:

声明文件为twocontainers.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1 #指定当前描述文件遵循v1版本的Kubernetes API
kind: Pod #我们在描述一个pod
metadata:
name: twocontainers #指定pod的名称
namespace: default #指定当前描述的pod所在的命名空间
labels: #指定pod标签
app: twocontainers
annotations: #指定pod注释
version: v0.5.0
releasedBy: david
purpose: demo
spec:
containers:
- name: sise #容器的名称
image: quay.io/openshiftlabs/simpleservice:0.5.0 #创建容器所使用的镜像
ports:
- containerPort: 9876 #应用监听的端口
- name: shell #容器的名称
image: centos:7 #创建容器所使用的镜像
command: #容器启动命令
- "bin/bash"
- "-c"
- "sleep 10000"

部署Pod:

1
kubectl apply -f twocontainers.yaml

几分钟后可以看pod状态是否为running。

1
2
3
dabin@k8s-master:~/workspace/notes/kubernetes/examples$ kubectl get pods
NAME READY STATUS RESTARTS AGE
twocontainers 2/2 Running 2 83m

如果不是,查看Pod部署遇到的问题:

1
kubectl describe pod twocontainers

资料

  1. 人人必备的神书《Kuerbenetes权威指南》
  2. K8S中文文档
  3. kubernetes安装过程报错及解决方法

对于Docker官方镜像仓库Registry,没有仓库镜像加速,寸步难行。

对于国外非Docker官方镜像仓库,并且还被墙的仓库Registry,没有网络代理,寸步难行。

镜像仓库加速器Registry Mirrors,是国内对官方Registry的”镜像(mirror)”,当拉取image时,Docker Daemon先去 Registry Mirrors 拉去镜像,如果没找到镜像,Registry Mirrors找官方Registry拉去镜像,然后再返回给本地。

网络代理是给Docker设置http和https代理,最原始的方式,适合有代理的情况。主要用于服务器上有稳定可访问的代理或者当前主机上有稳定代理的情况。对于代理和docker不在同一台机器上时,稳定可访问就成了一个问题,比如代理在笔记本上,IP随时都可能变化,服务器连接笔记本做代理,就算法上稳定可访问,而docker也在笔记本上运行,通过环回地址就能稳定访问。

不推荐给Docker设置代理,而应当优先使用Registry Mirrors。代理也是有副作用的,你需要保证非本机能稳定连接到代理,并且能够转发数据,不然端口拒绝访问、TLS握手失败等问题,需要花费更多的时间。

可访问的Registry有:

  • quay.io : 只是访问慢一些而已,可以拉下镜像来

镜像仓库加速器(推荐)

如果指定拉某个镜像仓库的镜像,镜像加速器是用不上的。如果该仓库可以访问,非本机有代理的情况,无需配置网络代理。

看如何配置Docker镜像加速器

推荐使用阿里云、七牛、DaoCloud的镜像仓库加速器。

/etc/docker/daemon.json 配置如下:

1
2
3
4
{
"insecure-registries":["192.168.9.8:80"],
"registry-mirrors": ["https://a90tkz28.mirror.aliyuncs.com"]
}

然后冲抵daemon:

1
2
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

网络代理(不推荐)

我Mac上的http、https、socks5代理,http和https监听的是7890端口,sock5监听的是7891端口。

拉镜像时,可以看到docker连接了7890端口走http代理。

1
2
3
4
5
6
[/private/tmp]$ lsof -i:7890
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
....
com.docke 73371 shitaibin 50u IPv4 0xa8184ba4240fbe99 0t0 TCP localhost:58847->localhost:7890 (ESTABLISHED)
com.docke 73371 shitaibin 53u IPv4 0xa8184ba4035deb09 0t0 TCP localhost:58857->localhost:7890 (ESTABLISHED)
com.docke 73371 shitaibin 58u IPv4 0xa8184ba42d889861 0t0 TCP localhost:58893->localhost:7890 (ESTABLISHED)

官方设置代理教程,2选1:

  1. 给Docker客户端设置代理,拉去镜像、创建新容器时,客户端会把变量发送给Daemon。支持17.07及以上版本,这是官方推荐方式。
  2. 给Daemon设置代理,通过环境变量的方式。支持17.06及以下版本,不推荐。

给Daemon设置的代理的另外方法:

创建Daemon的代理配置文件:

1
2
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo touch /etc/systemd/system/docker.service.d/http-proxy.conf

内容为:

1
2
[Service]
Environment="HTTP_PROXY=http://proxy.server.com:913/" "HTTPS_PROXY=http://proxy.server.com:913/" "NO_PROXY=localhost,127.0.0.1,10.96.0.0/16, 10.244.0.0/16"

然后重启Daemon:

1
2
3
4
sudo systemctl daemon-reload
sudo systemctl restart docker

systemctl show --property=Environment docker

测试环境

Ubuntu 18.04,内核版本4.15,机器拥有4核。

1
2
3
4
5
6
7
8
9
[~]$ cat /proc/version
Linux version 4.15.0-112-generic (buildd@lcy01-amd64-027) (gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)) #113-Ubuntu SMP Thu Jul 9 23:41:39 UTC 2020
[~]$
[~]$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.3 LTS (Bionic Beaver)"
...
[~]$ cat /proc/cpuinfo | grep "processor" | wc -l
4

CPU相关子系统简介

有关CPU的cgroup subsystem有3个:

  • cpu : 用来限制cgroup的CPU使用率
  • cpuacct : 用来统计cgroup的CPU的使用率
  • cpuset : 用来绑定cgroup到指定CPU的哪个核上和NUMA节点

每个子系统都有多个配置项和指标文件,主要介绍下图常用的配置项:

cpu、cpuacct、cpuset的指标

cpu

cpu子系统用来限制cgroup如何使用CPU的时间,也就是调度,它提供了3种调度办法,并且这3种调度办法都可以在启动容器时进行配置,分别是:

  • share :相对权重的CPU调度
  • cfs :完全公平调度
  • rt :实时调度

share调度的配置项和原理如下:

cpu share调度

cfs 是Completely Fair Scheduler的缩写,代表完全公平调度,它利用 cpu.cfs_quota_uscpu.cfs_period_us 实现公平调度,这两个文件内容组合使用可以限制进程在长度为 cfs_period_us 的时间内,只能被分配到总量为 cfs_quota_us 的 CPU 时间。CFS的指标如下:

cpu cfs调度

注意

  1. cfs_period_us 取值范围1000~1000000:1ms ~ 1s,cfs_quota_us的最小值为1000,当设置的值不在取值范围时,会报 write xxx: invalid argument 的错误。
  2. 只有这2个参数都有意义时,才能把任务写入到 tasks 文件。

rt 是RealTime的缩写,它是实时调度,它与cfs调度的区别是cfs不会保证进程的CPU使用率一定要达到设置的比率,而rt会严格保证,让进程的占用率达到这个比率,适合实时性较强的任务,它包含 cpu.rt_period_uscpu.rt_runtime_us 2个配置项。

cpuacct

cpuacct包含非常多的统计指标,常用的有以下4个文件:

cpuacct常用指标文件

cpuset

为啥需要cpuset?

比如:

  1. 多核可以提高并发、并行,但是核太多了,会影响进程执行的局部性,降低效率。
  2. 一个服务器上部署多种应用,不同的应用不同的核。

cpuset也包含居多的配置项,主要是分为cpu和mem 2类,mem与NUMA有关,其常用的配置项如下图:

cpuset常用配置项

利用Docker演示Cgroup CPU限制

cpu

不限制cpu的情况

stress为基于ubuntu:16.04安装stress做出来的镜像,利用stress来测试cpu限制。

Dockerfile如下:

1
2
3
4
5
6
7
From ubuntu:16.04
# Using Aliyun mirror
RUN mv /etc/apt/sources.list /root/sources.list.bak
RUN sed -e s/security.ubuntu/mirrors.aliyun/ -e s/archive.ubuntu/mirrors.aliyun/ -e s/archive.canonical/mirrors.aliyun/ -e s/esm.ubuntu/mirrors.aliyun/ /root/sources.list.bak > /etc/apt/sources.list
RUN apt-get update
RUN apt-get install -y stress
WORKDIR /root

启动容器不做任何cpu限制,利用 stress -c 2 开启另外2个stress线程,共3个:

1
2
3
[/sys/fs/cgroup/cpu]$ docker run --rm -it stress:16.04
root@5fad38726740:/# stress -c 2
stress: info: [12] dispatching hogs: 2 cpu, 0 io, 1 vm, 0 hdd

cgroup/cpu,cpuacct下,找到该容器对应的目录,查看 cfs_period_uscfs_quota_us 的默认值:

1
2
3
4
[/sys/fs/cgroup/cpu,cpuacct/system.slice/docker-5fad38726740b90b93c06972fe4a9f11391a38aaeb3e922f10c3269fa32e1873.scope]$ cat cpu.cfs_period_us
100000
[/sys/fs/cgroup/cpu,cpuacct/system.slice/docker-5fad38726740b90b93c06972fe4a9f11391a38aaeb3e922f10c3269fa32e1873.scope]$ cat cpu.cfs_quota_us
-1

查看主机CPU利用率,为3个stress进程,每1个都100%,它们属于同一个cgroup:

1
2
3
4
 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
5616 root 20 0 109872 102336 36 R 100.0 1.3 0:07.46 stress
5617 root 20 0 7468 88 0 R 100.0 0.0 0:07.45 stress
5615 root 20 0 7468 88 0 R 100.0 0.0 0:07.45 stress

限制cpu的情况

--cpu-quota设置5000,开stress分配到另外2个核。

[/sys/fs/cgroup/cpu]$ docker run –rm -it –cpu-quota=5000 stress:16.04
root@7e79005d7ca1:/#
root@7e79005d7ca1:/# stress -c 2
stress: info: [13] dispatching hogs: 2 cpu, 0 io, 1 vm, 0 hdd

查看 cfs_period_uscfs_quota_us 的设置,5000/100000 = 5% , 即限制该容器的CPU使用率不得超过5%。

1
2
3
4
[/sys/fs/cgroup/cpu,cpuacct/system.slice/docker-7e79005d7ca1b338d870d3dc79af3f1cd38ace195ebd685a09575f6acee36a07.scope]$ cat cpu.cfs_quota_us
5000
[/sys/fs/cgroup/cpu,cpuacct/system.slice/docker-7e79005d7ca1b338d870d3dc79af3f1cd38ace195ebd685a09575f6acee36a07.scope]$ cat cpu.cfs_period_us
100000

利用top可以看到3个进程总cpu使用率5.1%。

1
2
3
4
 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
5411 root 20 0 7468 92 0 R 1.7 0.0 0:00.53 stress
5412 root 20 0 109872 102500 36 R 1.7 1.3 0:00.30 stress
5413 root 20 0 7468 92 0 R 1.7 0.0 0:00.35 stress

cpuacct

查看cpuacct.stat, cpuacct.usage, cpuacct.usage_percpu,一定要同时输出这几个文件,不然可能有时间差,利用python可以计算每个核上的时间之和为usage,即该容器占用的cpu总时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[/sys/fs/cgroup/cpu,cpuacct]$ cat cpuacct.*
user 20244450 // cpuacct.stat
system 52361 // cpuacct.stat
204310768947624 // cpuacct.usage
61143521333219 32616883199042 73804985004267 36745379411096 // cpuacct.usage_percpu

[/sys/fs/cgroup/cpu,cpuacct]$ python
Python 2.7.5 (default, Apr 11 2018, 07:36:10)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-28)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> sum = 61143521333219+32616883199042+73804985004267+36745379411096
>>> dist = sum - 204310768947624
>>> dist
0
>>> sum
204310768947624
>>> sum2 = 20244450+52361
>>> sum2
20296811

cpuset

启动容器,然后使用stress占用1个核:

1
2
3
4
[/sys/fs/cgroup/cpu]$ docker run --rm -it stress:16.04
root@a907df624697:~#
root@a907df624697:~# stress -c 1
stress: info: [12] dispatching hogs: 1 cpu, 0 io, 0 vm, 0 hdd

top显示占用100%CPU。

1
2
 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
6633 root 20 0 7480 92 0 R 100.0 0.0 0:12.13 stress

cpuset 能看到可使用的核为: 0~3。

1
2
[/sys/fs/cgroup/cpuset/docker/a907df624697a19631929c1e9e971d2893afddbf6befb0dd44be3cf0024a3e0d]$ cat cpuset.cpus
0-3

使用cpuacct查看CPU情况使用统计,可以看到用了4个核上的使用时间。

1
2
3
4
5
6
7
[/sys/fs/cgroup/cpu/docker/a907df624697a19631929c1e9e971d2893afddbf6befb0dd44be3cf0024a3e0d]$ cat cpuacct.usage cpuacct.usage_all
153015464879
cpu user system
0 45900415963 0
1 4675002 0
2 63537634967 0
3 43572738947 0

现在创建一个新容器,限制只能用1,3这2个核:

1
2
3
[/sys/fs/cgroup/cpu]$ docker run --rm -it --cpuset-cpus 1,3 stress:16.04
root@0ce61a38e7c9:~# stress -c 1
stress: info: [10] dispatching hogs: 1 cpu, 0 io, 0 vm, 0 hdd

查看可以使用的核:

1
2
[/sys/fs/cgroup/cpuset/docker/0ce61a38e7c9621334871ab40d5b7d287d89a1e994148833ddf3ca4941a39c89]$ cat cpuset.cpus
1,3

cpuacct.usage_all 显示只有1、3两个核的数据在使用:

1
2
3
4
5
6
[/sys/fs/cgroup/cpu/docker/0ce61a38e7c9621334871ab40d5b7d287d89a1e994148833ddf3ca4941a39c89]$ cat cpuacct.usage_all
cpu user system
0 0 0
1 37322884717 0
2 0 0
3 21332956940 0

现在切换到root账号,把 sched_load_balance 标记设置为0,不进行核间的负载均衡,然后利用 cpuacct.usage_all 查看每个核上的时间,隔几秒前后查询2次,可以发现3号核的cpu使用时间停留在21332956940,而核1的cpu使用时间从185084024837 增加到 221479683602, 说明设置之后stress线程一致在核1上运行,不再进行负载均衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[/sys/fs/cgroup/cpuset/docker/0ce61a38e7c9621334871ab40d5b7d287d89a1e994148833ddf3ca4941a39c89]$ echo 0 > cpuset.sched_load_balance

[/sys/fs/cgroup/cpu/docker/0ce61a38e7c9621334871ab40d5b7d287d89a1e994148833ddf3ca4941a39c89]$ cat cpuacct.usage_all
cpu user system
0 0 0
1 185084024837 0
2 0 0
3 21332956940 0

[/sys/fs/cgroup/cpu/docker/0ce61a38e7c9621334871ab40d5b7d287d89a1e994148833ddf3ca4941a39c89]$ cat cpuacct.usage_all
cpu user system
0 0 0
1 221479683602 0
2 0 0
3 21332956940 0

利用Go演示Cgroup CPU限制

测试程序:02.2.cgroup_cpu.go

该程序接受1个入参,代表测试类型:

  • 空或nolimit: 无限制
  • cpu : 执行cpu限制,利用cfs把cpu使用率控制在5%
  • cpuset : 限制只使用核1和核3

测试程序的执行动作如下:

  1. 程序首先在cpu和cpuset中创建2个cgroup,
  2. 按传入的参数设置限制或不设置限制
  3. 利用/proc/self/exe启动进程
  4. 把进程加入到2个cgroup的tasks,即加入cgroup
  5. 进程会创建3个goroutine不断的去消耗cpu,它们会占用3个线程

当CPU使用率不限制时,3个线程会分配到3个核上执行,所以进程的CPU使用率应当达到300%。

利用测试程序分3组实验,然后利用 topcpuacct.usage_allcpuset.cpu 3个角度查看CPU限制和使用情况。

不限制CPU

  1. 启动测试程序,进程id为4805,进入Namespace后进程id变为1,可以看到启动了3个worker协程。
1
2
3
4
5
6
7
8
9
[/home/ubuntu/workspace/notes/docker/codes]$ go run 02.2.cgroup_cpu.go
---------- 1 ------------
Test type: No limit
cmdPid: 4805
---------- 2 ------------
Current pid: 1
worker 2 start
worker 0 start
worker 1 start
  1. top查看进程的CPU占用率为300%,符合预期。
1
2
 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
4805 root 20 0 3376 1004 836 R 300.0 0.0 4:41.09 exe
  1. 利用cpuacct查看每个核上的使用时间:
1
2
3
4
5
6
[/sys/fs/cgroup/cpuacct]$ cat test_cpu_limit/cpuacct.usage_all
cpu user system
0 8046597390 0
1 34269979109 0
2 26597651949 0
3 33886705168 0
  1. 利用cpuset.cpus查看使用的cpu核:
1
2
[/sys/fs/cgroup/cpuset]$ cat test_cpuset_limit/cpuset.cpus
0-3

使用cpu限制CPU使用率

  1. 启动测试程序:
1
2
3
4
5
6
7
8
9
[/home/ubuntu/workspace/notes/docker/codes]$ go run 02.2.cgroup_cpu.go cpu
---------- 1 ------------
Test type: Cpu limit
cmdPid: 4937
---------- 2 ------------
Current pid: 1
worker 2 start
worker 1 start
worker 0 start
  1. top查看进程的CPU占用率为5.0%,符合预期。
1
2
 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
4937 root 20 0 3376 1004 836 R 5.3 0.0 0:08.40 exe
  1. 利用cpuacct查看每个核上的使用时间,由于没有限制使用的cpu核,所以每个核上都还有运行时间
1
2
3
4
5
6
[/sys/fs/cgroup/cpuacct]$ cat test_cpu_limit/cpuacct.usage_all
cpu user system
0 2036903414 0
1 44170 0
2 4428266075 0
3 4356927661 0
  1. 利用cpuset.cpus查看使用的cpu核
1
2
[/sys/fs/cgroup/cpuset]$ cat test_cpuset_limit/cpuset.cpus
0-3

使用cpuset限制CPU占用的核

  1. 启动测试程序,这次与前面的不同,看到只起来了2个worker协程在运行,因为机器上的Go版本是go1.10,还不支持抢占,当协程为for循环时,2个协程都持续运行,不让出cpu,只有2个核时,第3个协程无法运行。
1
2
3
4
5
6
7
8
[/home/ubuntu/workspace/notes/docker/codes]$ go run 02.2.cgroup_cpu.go cpuset
---------- 1 ------------
Test type: Cpuset limit
cmdPid: 5063
---------- 2 ------------
Current pid: 1
worker 2 start
worker 0 start
  1. top查看进程的CPU占用率为200%,符合只使用2个核的预期。
1
2
 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
5063 root 20 0 3376 1004 836 R 199.7 0.0 4:21.49 exe
  1. 利用cpuacct查看每个核上的使用时间,只有核1和3上有时间统计,说明只使用了核1和3
1
2
3
4
5
6
[/sys/fs/cgroup/cpuacct]$ cat test_cpu_limit/cpuacct.usage_all
cpu user system
0 0 0
1 24172994458 0
2 0 0
3 24213057511 0
  1. 利用cpuset.cpus查看使用的cpu核
1
2
[/sys/fs/cgroup/cpuset]$ cat test_cpuset_limit/cpuset.cpus
1,3

推荐资料

  1. Linux Kernel关于cgroup cpu、cpuset的文档
  2. 阿里同学的书《自己动手写Docker》
  3. 解决写 cpu.cfs_quota_us invalid argument问题
  4. 解决写 cpuset.tasks No space 问题
  5. cgroup使用踩坑

测试环境版本

测试机采用的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很好,但某些原因造成国内用起来比较慢,要各种挂代理、镜像加速。

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
6
7
8
9
minikube start --image-mirror-country cn \
--iso-url=https://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/iso/minikube-v1.7.3.iso \
--docker-env http_proxy=http://192.168.0.104:1087 \
--docker-env https_proxy=http://192.168.0.104:1087 \
--docker-env no_proxy=localhost,127.0.0.1,10.96.0.0/12,192.168.99.0/24,192.168.39.0/24 \
--registry-mirror="https://a90tkz28.mirror.aliyuncs.com" \
--image-repository="registry.cn-hangzhou.aliyuncs.com/google_containers" \
--insecure-registry=192.168.9.8 \
--kubernetes-version=v1.18.3

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

  • --image-mirror-country: 需要使用的镜像镜像的国家/地区代码。留空以使用全球代码。对于中国大陆用户,请将其设置为
    cn
  • --docker-env: 是通过环境变量向docker挂http代理,否则国内可能出现拉不到镜像的问题。挂代理还需要一个必要条件,在主机上使用SS开启代理。挂了代理可能也很难拉到,但不挂代理,几乎拉不下来镜像。就算挂了代理,也会和某些https的镜像仓库出现TLS握手失败、超时的错误,可以利用docker search去搜索镜像,也许镜像加速器已经有同名镜像了,yaml中把镜像地址设置为当前查询到的镜像即可,这种方式存在的风险是search出的镜像和指定的镜像名称版本相同,但可能并不是相同的镜像,比较存在于不同的镜像仓库中。
  • --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
15
16
17
18
19
20
21
22
23
$ minikube start --image-mirror-country cn \
--iso-url=https://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/iso/minikube-v1.7.3.iso \
--docker-env http_proxy=http://192.168.0.104:1087 \
--docker-env https_proxy=http://192.168.0.104:1087 \
--docker-env no_proxy=localhost,127.0.0.1,10.96.0.0/12,192.168.99.0/24,192.168.39.0/24 \
--registry-mirror="https://a90tkz28.mirror.aliyuncs.com" \
--image-repository="registry.cn-hangzhou.aliyuncs.com/google_containers" \
--insecure-registry=192.168.9.8
😄 Darwin 10.15.3 上的 minikube v1.12.3
✨ 根据现有的配置文件使用 virtualbox 驱动程序
👍 Starting control plane node minikube in cluster minikube
🏃 Updating the running virtualbox "minikube" VM ...
🐳 正在 Docker 19.03.6 中准备 Kubernetes v1.18.3…
▪ env http_proxy=http://192.168.0.104:1087
▪ env https_proxy=http://192.168.0.104:1087
▪ env no_proxy=localhost,127.0.0.1,10.96.0.0/12,192.168.99.0/24,192.168.39.0/24
> kubeadm.sha256: 65 B / 65 B [--------------------------] 100.00% ? p/s 0s
> kubelet.sha256: 65 B / 65 B [--------------------------] 100.00% ? p/s 0s
> kubeadm: 37.97 MiB / 37.97 MiB [--------------] 100.00% 320.45 MiB p/s 0s
> kubelet: 108.04 MiB / 108.04 MiB [---------] 100.00% 514.43 KiB p/s 3m36s
🔎 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
65
66
67
68
$ minikube ssh
_ _
_ _ ( ) ( )
___ ___ (_) ___ (_)| |/') _ _ | |_ __
/' _ ` _ `\| |/' _ `\| || , < ( ) ( )| '_`\ /'__`\
| ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )( ___/
(_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____)

$
$ docker info
Client:
Debug Mode: false

Server:
Containers: 14
Running: 14
Paused: 0
Stopped: 0
Images: 10
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: DSF4:HEQB:HTUU:OXRS:ZBWC:ESX4:WEST:UFDC:WAW5:5CDV:PITM:BEXZ
Docker Root Dir: /var/lib/docker
Debug Mode: false
HTTP Proxy: http://192.168.0.104:1087
HTTPS Proxy: http://192.168.0.104:1087
No Proxy: localhost,127.0.0.1,10.96.0.0/12,192.168.99.0/24,192.168.39.0/24
Registry: https://index.docker.io/v1/
Labels:
provider=virtualbox
Experimental: false
Insecure Registries:
192.168.9.8
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原有版权声明,但可以选择不开源。