0%

配置文件

选择oh my zsh的原因在于它提供了很多插件和主题。

zsh的配置文件为:~/.zshrc,oh my zsh的许多配置也在此添加。

主题

习惯使用gallois主题,但发现它有一个现在无法忍受的缺点,如果当前目录是git仓库,它会在右边显示分支名称和clean状态。当从终端复制文本出来时,分支名称和左边命令的空白,全是空格填充,复制出来就得手动删除。

oh my zsh的所有主题配置都在.oh-my-zsh/themes/目录,文件名称同主题名称,可以对这些主题的一些配置进行修改。

注释掉配置文件中关于git的设置,打开新终端后,就可以不显示git分支信息了。

从此复制出的代码,在也没有多余文本。

插件

插件目录在:~/.oh-my-zsh/plugins,从中可以浏览自己需要的插件。

比如我常用的插件为:plugins=(git autojump docker kubectl extract),添加到~/.zshrc即可。

kustomize简介

kustomize是一个自定义管理原始的YAML模板资源文件的工具,同时无需修改原始的YAML文件。

对于kustomize的理解是,它借助了docker镜像的类似概念:可以一层层的进行覆盖。

Kustmoize有Base和Overlay 2个概念,被依赖的层成为base,当前进行覆盖操作的层成为overlay。所以1个overlay,也可以是另外overlay的base。

Kustomize base和overlay

在kubectl v1.14之后,其中融合了kustomize,也就说如果安装了kubectl无需安装kustomize,即可使用kustomize。

把kustmoize的资源文件部署到集群有2个办法:

  1. 通过kubectl内置的kustomize:kubectl apply -k $KUSTOMIZE_DIR 应用到集群。
  2. 集合kustomize和kubectl: kustomize build $KUSTOMIZE_DIR | kubectl apply -f -应用到集群。

base

nginx-deployment.yaml:

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:1.9.1
name: nginx
ports:
- containerPort: 80
name: http
volumeMounts:
- mountPath: /user/share/nginx/html
name: data
restartPolicy: Always
volumes:
- name: data
emptyDir: {}

kustomization.yaml :

1
2
3
4
5
commonLabels:
app: kustomize-nginx

resources:
- nginx-deployment.yaml

build的效果就是,把列出来的resources中的label,都换成app: kustomize-nginx:

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
$ diff <(kustomize build .) nginx-deployment.yaml
4,5d3
< labels:
< app: kustomize-nginx
6a5,6
> labels:
> app: nginx
10c10
< app: kustomize-nginx
---
> app: nginx
14c14
< app: kustomize-nginx
---
> app: nginx
17,24c17,24
< - image: nginx:1.9.1
< name: nginx
< ports:
< - containerPort: 80
< name: http
< volumeMounts:
< - mountPath: /user/share/nginx/html
< name: data
---
> - image: nginx:1.9.1
> name: nginx
> ports:
> - containerPort: 80
> name: http
> volumeMounts:
> - mountPath: /user/share/nginx/html
> name: data
27,28c27,28
< - emptyDir: {}
< name: data
---
> - name: data
> emptyDir: {}

应用到k8s,并且查看label。

1
2
3
4
5
[~/workspace/notes/kubernetes/examples/kustomize/base]$ kubectl apply -k .
deployment.apps/nginx created
[~/workspace/notes/kubernetes/examples/kustomize/base]$ kubectl get deploy -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
nginx 1/1 1 1 7s nginx nginx:1.9.1 app=kustomize-nginx

overlay

修改副本数量

kustomization.yaml:

1
2
3
4
5
6
7
8
9
10
11
namePrefix: testing-
commonLabels:
app: kustomize-nginx
variant: testing
group: test

bases:
- ../../base

patchesStrategicMerge:
- nginx-deployment.yaml
  • namePrefix: 资源前缀,比如deployment名称以namePrefix: testing-开头。

  • commonLabels:会使用的标签

  • bases:所基于的base文件
  • patchesStrategicMerge:用合并的方式做path,列出涉及的文件。

nginx-deployment.yaml 为patch的内容:

1
2
3
4
5
6
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 2

名称metadata.name应当与base中的名称相同,因为使用名称做匹配,所做的patch是当前overlay,名为nginx的deployment的副本数量为2。

1
2
3
4
5
6
7
$ kubectl apply -k .
deployment.apps/testing-nginx created
[~/workspace/notes/kubernetes/examples/kustomize/overlay/testing]$
[~/workspace/notes/kubernetes/examples/kustomize/overlay/testing]$ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 1/1 1 1 19m
testing-nginx 2/2 2 2 8s

覆盖镜像

kustomization.yaml:

  • 前缀设置为develop,更新相应tag
  • 通过images更新镜像
  • Nginx deployment的副本数量修改为5,这种方式与上一个修改副本数量相比更简洁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namePrefix: dev-
commonLabels:
app: dev-nginx
variant: dev
group: develop

bases:
- ../../base
images:
- name: nginx
newTag: 1.19.0
replicas:
- name: nginx
count: 5

部署到集群:发现kubectl内置的kustomize的replicas并没有得到支持,可能是内置的kustomize版本较老,使用kustomize + kubectl的方式可以部署到集群。

1
2
3
4
5
6
7
8
9
10
[~/workspace/notes/kubernetes/examples/kustomize/overlay]$ kubectl delete -k develop
error: json: unknown field "replicas"
[~/workspace/notes/kubernetes/examples/kustomize/overlay]$ kustomize build develop | kubectl apply -f -
deployment.apps/dev-nginx created
[~/workspace/notes/kubernetes/examples/kustomize/overlay]$
[~/workspace/notes/kubernetes/examples/kustomize/overlay]$ kubectl get deploy -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
dev-nginx 5/5 5 5 17s nginx nginx:1.19.0 app=dev-nginx,group=develop,variant=dev
nginx 1/1 1 1 10h nginx nginx:1.9.1 app=kustomize-nginx
testing-nginx 2/2 2 2 10h nginx nginx:1.9.1 app=kustomize-nginx,group=test,variant=testing

https://kubectl.docs.kubernetes.io/references/kustomize/images/

更多功能

参考kustomize reference

kustomize常用命令

build

使用配置文件生成资源的YAML文件,并打印到标准输出。配合kubectl命令可以把资源部署到集群。

edit

通过命令行修改kustomization.yaml文件。这种办法可以方便的放到脚本中去修改kustomization.yaml,而不是手动去修改。比如operator-sdk的样例,通过edit命令指定资源所要使用的镜像。

edit的set命令可以做以下几样事:

1
2
3
4
5
6
Available Commands:
image Sets images and their new names, new tags or digests in the kustomization file
nameprefix Sets the value of the namePrefix field in the kustomization file.
namespace Sets the value of the namespace field in the kustomization file
namesuffix Sets the value of the nameSuffix field in the kustomization file.
replicas Sets replicas count for resources in the kustomization file

通过命令行修改前缀和镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[~/workspace/notes/kubernetes/examples/kustomize/overlay]$ cp -r develop develop-dabin
[~/workspace/notes/kubernetes/examples/kustomize/overlay/develop-dabin]$ kustomize edit set nameprefix "develop-dabin-"
[~/workspace/notes/kubernetes/examples/kustomize/overlay/develop-dabin]$ kustomize edit set image "nginx:1.18.0"
[~/workspace/notes/kubernetes/examples/kustomize/overlay/develop-dabin]$ head kustomization.yaml
namePrefix: develop-dabin-
commonLabels:
app: dev-nginx
group: develop
variant: dev

images:
- name: nginx
newTag: 1.18.0
replicas:

edit命令除了set外,还有add、remove、fix,能够更加完整的通过命令编辑kustomization.yaml文件。

本文样例:examples/kustomize

参考资料

JSONPath基础

XML有一个非常强大的解析工具是XPath,用于提取XML中的内容。之后也出现了一种高效提取JSON内容的工具,它被称为JSONPath。

JSONPath现在有很多不同的实现,不同的实现支持的提取语法略有不同,比如Goessner的JSONPath如下:

goessner jsonpath

fastjson的JSONPath支持的更加丰富。

示例JSON内容:

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
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}

以例子讲解几个最常用的语法:

语法 语法含义 例子 例子含义
$ JSON内容的根对象,所有的JSONPath都是以$为开头。 $ JSON内容本身
. 后面跟子对象。 $.expensive 提取根对象的expensive字段的值
.. 递归扫描子对象。 $..price 提取对象中所有price字段的值,结果会包含所有book和bicycle中的价格
[num] 以下标访问数组。语法类似Python,num为负数时,代表倒数。 $.store.book[0] 获取第一本书
[num1, num2,num3] 以下标获取数据中多个数据。 $.store.book[0,2] 获取第1、3本书
[start:end] 获取数组区间[start, end)的数据。 $.store.book[0:2] 获取前2本书
[start:end:step] 获取数组区间[start, end)的数据,但以step为步长提取数据。但不是所有JSONPath实现都支持 $.store.book[0:3:2] 获取第1~3本书,以步长为2提取,也即是第1、3本书。
[*] 通配符,匹配所有子对象。 $.store.*.price 匹配store子对象中的价格,因为book的价格,是更下一级,所以只能匹配到bicycle的价格
?() 过滤符,可以理解成SQL的Where。 $.store.[?(@.category=="fiction")].author 获取类别为fiction的书籍作者列表.author])。
@ 当前对象,配合?()很好用。

k8s使用jsonpath

kubectl没有提供查看Pod内容器的名称,怎么办呢?可以利用jsonpath或者go template实现。

json格式输出结果通常是嵌套多层,使用jsonpath可以忽略中间层次,而go template不行,这是jsonpath比go template好用的地方。

看一个略微复杂k8s使用jsonpath列出所有pod的所有容器名称和镜像的样例:

1
kubectl get pods --all-namespaces -o=jsonpath='{range .items[*]}{"pod: "}{.metadata.name} {"\n"}{range .spec.containers[*]}{"\tcontainer: "}{.name}{"\n\timage: "}{.image}{"\n"}{end}{end}'

发现jsonpath=''与标准的jsonpath并不一样:

  • 没有$
  • 一堆{}
  • 还有range, {"\n"}

那是因为k8s对jsonpath的支持有以下特性:

  1. 在jsonpath中使用""包含文本,这样在输出的结果可以显示自定义的字符串,还能进行换行、Tab等。
  2. 支持使用range .. end迭代数组,原生的jsonpath没有办法提取数组元素中的多个子对象,使用range达成效果,比如想获得容器的名称镜像。
  3. 支持-num获取数组的倒数位置的元素
  4. 可以省略$,太好了
  5. 每一段jsonpath使用{}连接

刚开始使用jsonpath时,有种眼花缭乱的感觉,我们就拆解下上面的样例jsonpath。

1
kubectl get pods --all-namespaces -o=jsonpath='{.items[*].metadata.name}'

先提取每个pod的名称,这个还和原生的jsonpath一样。

1
kubectl get pods --all-namespaces -o=jsonpath='{range .items[*]}{"pod: "}{.metadata.name}{"\n"}{end}'

因为每个pod还要取容器名称和镜像,所以最好每个pod占一行,我们需要使用range .. end处理每一个pod,列pod所含的容器。

1
kubectl get pods --all-namespaces -o=jsonpath='{range .items[*]}{"pod: "}{.metadata.name}{"\n"}{"\tcontainer: "}{.spec.containers[*].name}, {.spec.containers[*].image}{"\n"}{end}'

可以看到每个pod内可能包含多个容器,所以也得用range .. end去处理pod的每一个container。

1
kubectl get pods --all-namespaces -o=jsonpath='{range .items[*]}{"pod: "}{.metadata.name} {"\n"}{range .spec.containers[*]}{"\tcontainer: "}{.name}{"\n\timage: "}{.image}{"\n"}{end}{end}'

上面提到使用jsonpath可以简化层级,因为containers这个名词在层级中是独有的,不像name可能是存在于多个层级,所以可以使用..简化:

1
kubectl get pods --all-namespaces -o=jsonpath='{range .items[*]}{"pod: "}{.metadata.name} {"\n"}{range ..containers[*]}{"\tcontainer: "}{.name}{"\n\timage: "}{.image}{"\n"}{end}{end}'

最后看一下过滤的使用,只想列出weave的pod的容器和镜像:

1
kubectl get pods --all-namespaces -o=jsonpath='{range .items[?(@.metadata.name=="weave-net-sqjzh")]}{"pod: "}{.metadata.name} {"\n"}{range ..containers[*]}{"\tcontainer: "}{.name}{"\n\timage: "}{.image}{"\n"}{end}{end}'

练习

使用JSONPath获取:

  1. Pod的名称和IP
  2. Pod退出原因

参考资料

模板

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
# build时设置版本 --build-arg GO_VERSION=1.13,默认为go1.15
ARG GO_VERSION=1.15
FROM golang:${GO_VERSION} AS builder
ENV GOPROXY="https://goproxy.cn"

ENV APP_PATH="/app/goapp"
WORKDIR "/app"

# 拷贝构建文件
COPY . .

# 编译
RUN go mod download
RUN CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -a -o ${APP_PATH} .
RUN ls


# 构建运行镜像
FROM alpine:3.10 AS final

ENV APP_PATH="/app/goapp"
WORKDIR "/app"

# 拷贝程序,如有必要另外拷贝其他文件
COPY --from=builder ${APP_PATH} ${APP_PATH}

# 运行程序
ENTRYPOINT ["/app/goapp"]

构建命令:

1
docker build -t app:1.0 --build-arg GO_VERSION=1.13 .

模板说明:

  1. 构建命令不指定--build-arg GO_VERSION=1.13时,默认使用go1.15进行编译。
  2. 使用alpine作为运行基础镜像,减小镜像大小。
  3. Dockerfile文件放到main.go所在目录。
  4. goapp替换成真正的程序名称。

Docker的文档关于ARG和FROM指令配合使用做了详细说明:

ARG用于传入外部参数,定义在FROM指令前,FROM后的其他指令无法使用ARG定义的环境变量,如果FROM指令后的指令要使用ARG定义的值,需要在FROM后再次定义。如果FROM不使用定义的ARG,可以直接定义到FROM后。

传递参数

定义在FROM前

1
2
3
ARG UBUNTU_VERSION=16.04
FROM ubuntu:${UBUNTU_VERSION}
RUN env | grep UBUNTU_VERSION

从结果可以看到FROM后的指令无法访问ARG定义的UBUNTU_VERSION

1
2
3
4
5
6
7
8
$ docker build -t test:afterfrom .
Sending build context to Docker daemon 37.38kB
Step 1/3 : ARG UBUNTU_VERSION=16.04
Step 2/3 : FROM ubuntu:${UBUNTU_VERSION}
---> 4b22027ede29
Step 3/3 : RUN env | grep UBUNTU_VERSION
---> Running in 8e93ca5e376b
The command '/bin/sh -c env | grep UBUNTU_VERSION' returned a non-zero code: 1

定义在FROM后

1
2
3
4
ARG UBUNTU_VERSION=16.04
FROM ubuntu:${UBUNTU_VERSION}
ARG UBUNTU_VERSION
RUN env | grep UBUNTU_VERSION

从结果可以看到FROM后的指令可以访问ARG定义的UBUNTU_VERSION

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker build -t test:afterfrom .
Sending build context to Docker daemon 37.38kB
Step 1/4 : ARG UBUNTU_VERSION=16.04
Step 2/4 : FROM ubuntu:${UBUNTU_VERSION}
---> 4b22027ede29
Step 3/4 : ARG UBUNTU_VERSION
---> Using cache
---> e3bae0875e66
Step 4/4 : RUN env | grep UBUNTU_VERSION
---> Running in 02438aff1f75
UBUNTU_VERSION=16.04
Removing intermediate container 02438aff1f75
---> 646eec496165
Successfully built 646eec496165
Successfully tagged test:afterfrom

传递多个ARG参数

1
2
3
4
5
6
7
8
ARG UBUNTU_VERSION=16.04
ARG FILE_NAME=test
FROM ubuntu:${UBUNTU_VERSION}

ARG FILE_NAME
WORKDIR /test/
RUN touch ${FILE_NAME}
RUN ls ${FILE_NAME}

传递多个参数需要多次使用--build-arg,可以看到传递的FILE_NAME=app生效了,而不是默认值。

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
$ docker build -t test:twoargs --build-arg UBUNTU_VERSION=18.04 --build-arg FILE_NAME=app .
Sending build context to Docker daemon 37.38kB
Step 1/7 : ARG UBUNTU_VERSION=16.04
Step 2/7 : ARG FILE_NAME=test
Step 3/7 : FROM ubuntu:${UBUNTU_VERSION}
18.04: Pulling from library/ubuntu
171857c49d0f: Pull complete
419640447d26: Pull complete
61e52f862619: Pull complete
Digest: sha256:646942475da61b4ce9cc5b3fadb42642ea90e5d0de46111458e100ff2c7031e6
Status: Downloaded newer image for ubuntu:18.04
---> 56def654ec22
Step 4/7 : ARG FILE_NAME
---> Running in bee623328f35
Removing intermediate container bee623328f35
---> 52f803da2959
Step 5/7 : WORKDIR /test/
---> Running in fa4542584af1
Removing intermediate container fa4542584af1
---> 5ebd782db9b8
Step 6/7 : RUN touch ${FILE_NAME}
---> Running in 0cd5723b744d
Removing intermediate container 0cd5723b744d
---> 920c6bd75bab
Step 7/7 : RUN ls ${FILE_NAME}
---> Running in b94a33093ddf
app
Removing intermediate container b94a33093ddf
---> 804bf831059a
Successfully built 804bf831059a
Successfully tagged test:twoargs

有一个cpu指标叫iowait或者wa,在top、iostat、vmstat命令中都可以看到这一项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[~]$ top
top - 08:58:06 up 26 days, 23:20, 1 user, load average: 0.07, 0.23, 0.26
Tasks: 164 total, 1 running, 111 sleeping, 0 stopped, 0 zombie
%Cpu(s): 2.5 us, 1.2 sy, 0.0 ni, 96.2 id, 0.1 wa, 0.0 hi, 0.1 si, 0.0 st
KiB Mem : 8167548 total, 698220 free, 996640 used, 6472688 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 7061988 avail Mem

[~]$ iostat
Linux 4.15.0-112-generic (shitaibin-x) 09/28/20 _x86_64_ (4 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
1.02 0.00 0.55 0.86 0.00 97.56

Device tps kB_read/s kB_wrtn/s kB_read kB_wrtn
loop0 0.00 0.00 0.00 5 0
vda 13.32 2.65 84.15 6182326 196098973

[~]$
[~]$
[~]$ vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 685616 177356 6306652 0 0 1 21 5 3 1 1 98 1 0

这个指标的字面含义是等待IO的时间(百分比),很多人会认为这个指标暗示这IO瓶颈,然而这是有一定无解的,iowait高不一定有IO瓶颈。

1
iowait = CPU空闲时间 / CPU总时间 ,前提CPU在等待至少一项IO操作完成

所以它真正的含义是有未完成的IO操作时,CPU空闲的时间。

这2个资料都把iowait讲解的很清晰,并且举例iowait和IO瓶颈无关的例子。

例子:

  • 低iowait,高IO的例子:IO高同时CPU计算也高,这样CPU的空闲时间少,造成iowait比较低,CPU密集掩盖了IO密集。
  • 高iowait,低IO的例子:CPU计算很少,CPU基本空闲,但也有1个进程在IO,所以iowait高,但实际IO根本没任何瓶颈。

在公司都是用现成的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.cn-hangzhou.aliyuncs.com/google_containers \
--pod-network-cidr=10.24.0.0/16 \
--ignore-preflight-errors=Swap
  • --image-repository : 由于k8s.gcr.io由于网络原因无法访问,使用阿里云提供的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

k8s主节点部署后的情况

k8s本身不负责容器之间的通信,集群启动后,集群的Pod直接还不能通信,需要安装网络插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master NotReady master 5m8s v1.19.0
$ kubectl get pod -n kube-system -owide

NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-6c76c8bb89-9wnfb 0/1 Pending 0 56s <none> <none> <none> <none>
coredns-6c76c8bb89-glkdx 0/1 Pending 0 56s <none> <none> <none> <none>
etcd-shitaibin-x 0/1 Running 0 70s 10.0.0.3 shitaibin-x <none> <none>
kube-apiserver-shitaibin-x 1/1 Running 0 70s 10.0.0.3 shitaibin-x <none> <none>
kube-controller-manager-shitaibin-x 1/1 Running 0 70s 10.0.0.3 shitaibin-x <none> <none>
kube-proxy-7gpjx 1/1 Running 0 56s 10.0.0.3 shitaibin-x <none> <none>
kube-scheduler-shitaibin-x 0/1 Running 0 70s 10.0.0.3 shitaibin-x <none> <none>

从上面可以看到master节点为 NotReady 状态,coredns 服务也没有分配ip。

从下面的Condition和Events可以看到节点会进行4项检测:

  1. 节点内存是否充足
  2. 节点磁盘是否有压力
  3. 节点Pid是否充足
  4. kubelet是否就绪

从Events可以kubelet启动了2次,而内存、磁盘压力、pid条件检查进行了4次。

从Condition的kubelet消息中看到CNI网络插件还未就绪,导致kubelet并没有ready。

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
$ kubectl describe nodes shitaibin-x
Name: shitaibin-x
Roles: master
...
Conditions:
Type Status LastHeartbeatTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
MemoryPressure False Wed, 28 Oct 2020 08:40:18 +0000 Wed, 28 Oct 2020 08:40:07 +0000 KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure False Wed, 28 Oct 2020 08:40:18 +0000 Wed, 28 Oct 2020 08:40:07 +0000 KubeletHasNoDiskPressure kubelet has no disk pressure
PIDPressure False Wed, 28 Oct 2020 08:40:18 +0000 Wed, 28 Oct 2020 08:40:07 +0000 KubeletHasSufficientPID kubelet has sufficient PID available
Ready False Wed, 28 Oct 2020 08:40:18 +0000 Wed, 28 Oct 2020 08:40:07 +0000 KubeletNotReady runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
....
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Starting 111s kubelet Starting kubelet.
Normal NodeHasSufficientMemory 110s (x3 over 110s) kubelet Node shitaibin-x status is now: NodeHasSufficientMemory
Normal NodeHasNoDiskPressure 110s (x3 over 110s) kubelet Node shitaibin-x status is now: NodeHasNoDiskPressure
Normal NodeHasSufficientPID 110s (x3 over 110s) kubelet Node shitaibin-x status is now: NodeHasSufficientPID
Normal NodeAllocatableEnforced 110s kubelet Updated Node Allocatable limit across pods
Normal Starting 88s kubelet Starting kubelet.
Normal NodeHasSufficientMemory 88s kubelet Node shitaibin-x status is now: NodeHasSufficientMemory
Normal NodeHasNoDiskPressure 88s kubelet Node shitaibin-x status is now: NodeHasNoDiskPressure
Normal NodeHasSufficientPID 88s kubelet Node shitaibin-x status is now: NodeHasSufficientPID
Normal NodeAllocatableEnforced 87s kubelet Updated Node Allocatable limit across pods
Normal Starting 66s kube-proxy Starting kube-proxy.

查看kubelet进程启动配置:

1
2
$ ps -ef | grep kubelet
root 5549 1 2 08:40 ? 00:00:07 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --network-plugin=cni --pod-infra-container-image=registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.2

通过--network-plugin=cni可以看到默认使用CNI作为网络插件。

主机/etc/cni/net.d目录保存CNI的配置,发现目前为空。/opt/cni/bin目录为CNI插件程序所在的位置,可以看到已经有flannel等插件。

1
2
3
4
$ ls /etc/cni/net.d
ls: cannot access '/etc/cni/net.d': No such file or directory
$ ls /opt/cni/bin
bandwidth bridge dhcp firewall flannel host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan

接下来安装CNI网络插件。

安装CNI网络插件

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

较为简便的weave,它提供overlay network:

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

flannel,也是overlay network模型:

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

查看weave的配置和可执行程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ ls /etc/cni/net.d
10-weave.conflist
$ ls /opt/cni/bin
bandwidth dhcp flannel host-local loopback portmap sbr tuning weave-ipam weave-plugin-2.7.0
bridge firewall host-device ipvlan macvlan ptp static vlan weave-net
$
$ cat /etc/cni/net.d/10-weave.conflist
{
"cniVersion": "0.3.0",
"name": "weave",
"plugins": [
{
"name": "weave",
"type": "weave-net",
"hairpinMode": true
},
{
"type": "portmap",
"capabilities": {"portMappings": true},
"snat": true
}
]
}

安装之后节点变为Ready,coredns也拥有了ip:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-master Ready master 10m v1.19.0

$ kubectl get pod -n kube-system -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-6c76c8bb89-9wnfb 1/1 Running 0 19m 10.32.0.9 shitaibin-x <none> <none>
coredns-6c76c8bb89-glkdx 1/1 Running 0 19m 10.32.0.8 shitaibin-x <none> <none>
etcd-shitaibin-x 1/1 Running 0 20m 10.0.0.3 shitaibin-x <none> <none>
kube-apiserver-shitaibin-x 1/1 Running 0 20m 10.0.0.3 shitaibin-x <none> <none>
kube-controller-manager-shitaibin-x 1/1 Running 0 20m 10.0.0.3 shitaibin-x <none> <none>
kube-proxy-7gpjx 1/1 Running 0 19m 10.0.0.3 shitaibin-x <none> <none>
kube-scheduler-shitaibin-x 1/1 Running 0 20m 10.0.0.3 shitaibin-x <none> <none>
weave-net-72p6p 2/2 Running 0 2m36s 10.0.0.3 shitaibin-x <none> <none>

开启master调度

master节点默认是不可被调度的,不可在master上部署任务,在单节点下,只有master一个节点,部署资源后会出现以下错误:

1
2
3
4
5
$ kubectl describe pod mysql
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 22s (x2 over 22s) default-scheduler 0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.

Event的告警信息提示,当前节点存在一个污点node-role.kubernetes.io/master:,而pod却不容忍这个污点。

查看master节点的信息,确实可以看到一个不允许调度的污点。

1
2
3
4
5
6
7
8
9
$ kubectl get nodes shitaibin-x -o yaml | grep -10 taint
...
spec:
podCIDR: 10.24.0.0/24
podCIDRs:
- 10.24.0.0/24
taints:
- effect: NoSchedule
key: node-role.kubernetes.io/master

有2个办法解决这个问题,让资源可以调度到master节点上:

  1. 所有的资源声明文件中,都设置容忍这个污点,
  2. master节点上删除这个污点

我们是一个测试环境,采取第2种办法更简单:

1
2
3
# --all为所有节点上的污点
# 最后的-代表移除污点
kubectl taint nodes --all node-role.kubernetes.io/master-

移除污点记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[~]$ kubectl taint nodes shitaibin-x node-role.kubernetes.io/master-
node/shitaibin-x untainted
[~]$
[~]$ kubectl get nodes shitaibin-x -o yaml | grep -10 taint
[~]$
[~]$ kubectl describe pod | grep -10 Events
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 27s (x7 over 7m38s) default-scheduler 0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.
Normal Scheduled 17s default-scheduler Successfully assigned default/mysql-0 to shitaibin-x
Normal Pulled 15s kubelet Container image "mysql:5.6" already present on machine
Normal Created 14s kubelet Created container mysql
Normal Started 14s kubelet Started container mysql

测试集群

部署一个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

清空k8s环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// remove_k8s.sh
# 重置k8s
sudo kubeadm reset -f
# 删除kubectl配置文件
sudo rm -rf ~/.kube
# 卸载和清理程序配置文件
sudo apt-get -y purge kubeadm kubectl kubelet kubernetes-cni
# 卸载自安装依赖
sudo apt-get -y autoremove

# 删除遗留的文件
sudo rm -rf ~/.kube/
sudo rm -rf /etc/kubernetes/
sudo rm -rf /etc/systemd/system/kubelet.service.d
sudo rm -rf /etc/systemd/system/kubelet.service
sudo rm -rf /etc/cni
sudo rm -rf /opt/cni
sudo rm -rf /var/lib/etcd
sudo rm -rf /var/etcd

更快的部署方法?

利用kind,使用Docker快速部署一个本地测试、开发k8s环境。

1
GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0 && kind create cluster

资料

  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

为Docker Daemon设置网络代理(不推荐)

我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

关于Docker代理的另外一件事:容器内的代理(几乎不用)

默认情况~/.docker/config.json是docker客户端的配置文件,其中可以配置Http和Https代理,这些环境变量会通过--build-arg的方式,在执行docker build时传递到镜像中。

容器内的服务通常不需要代理,所以这个无需设置

看个测试demo,config.json内容如下:

1
2
3
4
5
6
7
8
9
10
11
{
"proxies":
{
"default":
{
"httpProxy": "http://127.0.0.1:3001",
"httpsProxy": "http://127.0.0.1:3001",
"noProxy": "*.test.example.com,.example2.com"
}
}
}

Dockerfile如下:

1
2
FROM busybox
RUN env

构建过程如下,可以看到设置了http等环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ docker build .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM busybox
---> f0b02e9d092d
Step 2/2 : RUN env
---> Running in 2a8fe4fad631
HTTPS_PROXY=http://127.0.0.1:3001
no_proxy=*.test.example.com,.example2.com
HOSTNAME=2a8fe4fad631
SHLVL=1
HOME=/root
NO_PROXY=*.test.example.com,.example2.com
https_proxy=http://127.0.0.1:3001
http_proxy=http://127.0.0.1:3001
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
HTTP_PROXY=http://127.0.0.1:3001
Removing intermediate container 2a8fe4fad631
---> 89c86d136d8d
Successfully built 89c86d136d8d

启动容器后http环境变量依然生效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[~/test]$ docker run -it --rm 89c86d136d8d sh
/ # env
HTTPS_PROXY=http://127.0.0.1:3001
no_proxy=*.test.example.com,.example2.com
HOSTNAME=e9c131f55062
SHLVL=1
HOME=/root
NO_PROXY=*.test.example.com,.example2.com
https_proxy=http://127.0.0.1:3001
http_proxy=http://127.0.0.1:3001
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
HTTP_PROXY=http://127.0.0.1:3001

测试环境

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》