hello云胜

技术与生活

0%

读书:深入刨析Kubernetes(一)

Docker解决的核心问题是应用的打包。

容器本身没有价值,有价值的是容器编排

1
我的想法:不认可作者说的容器本身没有价值。容器标准化了应用的包。才有了后面容器编排调度的发展

Docker的底层原理

Docker的底层原理是利用了linux的Cgroups和Namespace技术。cgroups是用来制造约束的主要手段,而namespace技术是用来修改进程视图(隔离)的主要方法。

namespace

linux中船舰一个新的进程:

1
int pid = clone(main_function, stack_size, SIGCHLD, NULL);

在创建时可以传一个参数CLONE_NEWPID

1
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);

这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。

docker这个听起来很玄的技术,实际上就是在创建一个容器进程时,指定了这个进程所需要启用的一组Namespace参数。这样容器就只能看到当前namespace限定的资源、文件、设备、网络等等资源。而对于宿主机以及其他不相干的程序完全看不到。

总之,容器只是一个特殊一点的进程而已。

image-20220121170136227

进一步理解,docker容器不是一个虚拟机,并没有一个所谓的docker容器运行在宿主机上,用户进程还是那个用户进程,只不过docker帮我们加上了各种namespace参数。Docker 项目在这里扮演的角 色,更多的是旁路式的辅助和管理工作。

也可以看到docker和虚拟机相比的区别,虚拟化需要一个Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且他里面真的要运行一个操作系统。而容器本质上仍然仅仅是宿主机操作系统上的一个进程而已。

namespace参数

为了隔离不同类型的资源,Linux 内核里面实现了以下几种不同类型的 namespace。

UTS,对应的宏为 CLONE_NEWUTS,表示不同的 namespace 可以配置不同的 hostname。

User,对应的宏为 CLONE_NEWUSER,表示不同的 namespace 可以配置不同的用户 和组。

Mount,对应的宏为 CLONE_NEWNS,表示不同的 namespace 的文件系统挂载点是 隔离的

PID,对应的宏为 CLONE_NEWPID,表示不同的 namespace 有完全独立的 pid,也即 一个 namespace 的进程和另一个 namespace 的进程,pid 可以是一样的,但是代表不 同的进程。

Network,对应的宏为 CLONE_NEWNET,表示不同的 namespace 有独立的网络协议 栈

后台启动一个busybox容器

1
2
3
4
5
6
7
8
# docker run -it -d busybox
68f11c135bcdf9d5f793a6f12e90e37b5bca07735b9d715c2a02a163d3715c77
# docker exec -it 68f11c135b /bin/sh
# ps
PID USER TIME COMMAND
1 root 0:00 sh
6 root 0:00 /bin/sh
11 root 0:00 ps

进入容器查看ps,看到当前主进程号是1。隔离。

使用docker inpect查看下在宿主机上真正的PID。 “Pid”: 32694

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# docker inspect 68f11c135b
···
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 32694,
"ExitCode": 0,
"Error": "",
"StartedAt": "2022-01-21T08:48:45.55505132Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
···

在宿主机上查看

1
2
3
# ps -ef | grep 32694
root 28685 2366 0 16:52 pts/0 00:00:00 grep --color=auto 32694
root 32694 32675 0 16:48 pts/0 00:00:00 sh

查看32694进程的namespace资源

1
2
3
4
5
6
7
8
# ls -l /proc/32694/ns
总用量 0
lrwxrwxrwx 1 root root 0 1月 21 16:49 ipc -> ipc:[4026534209]
lrwxrwxrwx 1 root root 0 1月 21 16:49 mnt -> mnt:[4026534207]
lrwxrwxrwx 1 root root 0 1月 21 16:48 net -> net:[4026534212]
lrwxrwxrwx 1 root root 0 1月 21 16:49 pid -> pid:[4026534210]
lrwxrwxrwx 1 root root 0 1月 21 16:53 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 1月 21 16:49 uts -> uts:[4026534208]

Cgroup

Cgroup的全称是Control Group,作用是限制一个进程组能够使用的资源上限,比如CPU,内存,磁盘,网络带宽等

linux的实现方式是在一个特定的目录下有特定的配置文件。/sys/fs/cgroup/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ls -l /sys/fs/cgroup/
总用量 0
drwxr-xr-x 5 root root 0 7月 7 2021 blkio
lrwxrwxrwx 1 root root 11 7月 7 2021 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 7月 7 2021 cpuacct -> cpu,cpuacct
drwxr-xr-x 5 root root 0 7月 7 2021 cpu,cpuacct
drwxr-xr-x 4 root root 0 7月 7 2021 cpuset
drwxr-xr-x 5 root root 0 7月 7 2021 devices
drwxr-xr-x 4 root root 0 7月 7 2021 freezer
drwxr-xr-x 4 root root 0 7月 7 2021 hugetlb
drwxr-xr-x 5 root root 0 7月 7 2021 memory
lrwxrwxrwx 1 root root 16 7月 7 2021 net_cls -> net_cls,net_prio
drwxr-xr-x 4 root root 0 7月 7 2021 net_cls,net_prio
lrwxrwxrwx 1 root root 16 7月 7 2021 net_prio -> net_cls,net_prio
drwxr-xr-x 4 root root 0 7月 7 2021 perf_event
drwxr-xr-x 5 root root 0 7月 7 2021 pids
drwxr-xr-x 6 root root 0 7月 7 2021 systemd

这一个个的文件夹就是cgroup可以限制的资源类型。比如cpu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ls -l /sys/fs/cgroup/cpu/
总用量 0
-rw-r--r-- 1 root root 0 7月 7 2021 cgroup.clone_children
--w--w--w- 1 root root 0 7月 7 2021 cgroup.event_control
-rw-r--r-- 1 root root 0 7月 7 2021 cgroup.procs
-r--r--r-- 1 root root 0 7月 7 2021 cgroup.sane_behavior
-r--r--r-- 1 root root 0 7月 7 2021 cpuacct.stat
-rw-r--r-- 1 root root 0 7月 7 2021 cpuacct.usage
-r--r--r-- 1 root root 0 7月 7 2021 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.rt_period_us
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.shares
-r--r--r-- 1 root root 0 7月 7 2021 cpu.stat
drwxr-xr-x 5 root root 0 1月 21 16:46 docker
drwxr-xr-x 5 root root 0 7月 15 2021 kubepods
-rw-r--r-- 1 root root 0 7月 7 2021 notify_on_release
-rw-r--r-- 1 root root 0 7月 7 2021 release_agent
drwxr-xr-x 218 root root 0 1月 21 16:46 system.slice
-rw-r--r-- 1 root root 0 7月 7 2021 tasks

比如cfs_period_us和cfs_quota_us参数就是,限制进程在长度为 cfs_period 的一段时间内,只 能被分配到总量为 cfs_quota 的 CPU 时间。

image-20220121171625696

注意圈出的docker文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ls -l /sys/fs/cgroup/cpu/docker
总用量 0
drwxr-xr-x 2 root root 0 1月 21 16:41 51868296b5adcf5fa6bd0e85c72fc004ba8abf676159c6a74f06e276b7c229dd
drwxr-xr-x 2 root root 0 1月 21 16:48 68f11c135bcdf9d5f793a6f12e90e37b5bca07735b9d715c2a02a163d3715c77
drwxr-xr-x 2 root root 0 10月 12 13:26 afcc1b255416ebf7b3303904e5aee41afd281073fe00d5eb065dd9f73e31269b
-rw-r--r-- 1 root root 0 7月 19 2021 cgroup.clone_children
--w--w--w- 1 root root 0 7月 19 2021 cgroup.event_control
-rw-r--r-- 1 root root 0 7月 19 2021 cgroup.procs
-r--r--r-- 1 root root 0 7月 19 2021 cpuacct.stat
-rw-r--r-- 1 root root 0 7月 19 2021 cpuacct.usage
-r--r--r-- 1 root root 0 7月 19 2021 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.rt_period_us
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.shares
-r--r--r-- 1 root root 0 7月 19 2021 cpu.stat
-rw-r--r-- 1 root root 0 7月 19 2021 notify_on_release
-rw-r--r-- 1 root root 0 7月 19 2021 tasks

还记得我们之前建的busybox容器吗?68f11c135bcdf9d5f793a6f12e90e37b5bca07735b9d715c2a02a163d3715c77

对这个容器的资源限制就在这里

1
2
# cat cpu.cfs_quota_us
-1

创建的时候我们没有限制资源,所以这里显式是-1,即不限制。

我们再运行一个Ubuntu容器

1
2
# docker run -it -d  --cpu-period=100000 --cpu-quota=20000 busybox
cf367a47f6cd9766effe154e9d725a21657663a3d14f731077ea7e09153cc35a

period=100000 配合 cpu-quota=20000的含义是:在每 100 ms 的时间里,被该控制组 限制的进程只能使用 20 ms 的 CPU 时间,即限制只能使用20%的cpu算力。

再去查看period和quota

1
2
3
4
# cat cpu.cfs_period_us
100000
# cat cpu.cfs_quota_us
20000

容器是一个单进程模型。一个容器的本质是一个进程。用户的应用进程实际上就是容器里 PID=1 的进程,也是其他后续创建的所有进程的父进程。

这就意味着,在一个容器中,你没办法同时运行两个不同的应 用。这是因为容器本身的设计,就是希望容器和应用能够同生命周期,这个概念对后续的容器编排非常重要。否则,一旦出现类似于“容器是正常运行的,但是里面的应用早已经挂了”的情况,编排系统处理起来就非常麻烦了

对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

  1. 启用 Linux Namespace 配置;

  2. 设置指定的 Cgroups 参数;

  3. 切换进程的根目录(Change Root)。

需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。所以说,rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。**实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。**这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身

Kubernetes架构

image-20210811154142473

两类节点:master和node

master节点:负责 API 服务的 kube-apiserver、负责调度的 kube-scheduler,以及负责容器编排的 kube-controller-manager。整个集群的持久化数据,则由 kube-apiserver 处理后保存在 Ectd 中。

node节点:最核心的是kubelet组件。kubelet主要负责同容器运行时交互,例如docker。这个交互是通过cri标准接口进行的。所以容器运行时不一定非要用docker,只要遵循cri接口就可以接入k8s。而容器运行时又通过oci同底层的linux操作系统进行交互。也就是把cri的请求翻译成对linux系统的调用。

device plugin插件时k8s用来管理gpu等宿主机物理设备的组件,所以基于k8s进行机器学习等要关注这个组件。

image-20210811172904096

Service 服务声明的 IP 地址等信息是“终生不变”的。这个Service 服务的主要作用,就是作为 Pod 的代理入口(Portal),从而代替 Pod 对外暴露一个固定的网络地址。Service 后端真正代理的 Pod 的 IP 地址、端口等信息的自动更新、维护,则是 Kubernetes 项目的职责。

master组件的部署

kubelet是在服务器上直接部署的,其他的组件在容器中启动。通过yaml部署的。

master组件的yaml放在 /etc/kubernetes/manifests

image-20210812093719676

这些 YAML 文件出现在被 kubelet 监视的 /etc/kubernetes/manifests 目录下,kubelet 就会自动创建这些 YAML 文件中定义的 Pod,即 Master 组件的容器

pod实现原理

首先,关于 Pod 最重要的一个事实是:它只是一个逻辑概念。Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器Namespace 和Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。

Pod,其实是一组共享了某些资源的容器。

具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个Volume

也就是说比如pod里有容器A和B,可以使用A join进B的network 和volumn的方式实现,但是这样A和B就不是对等关系了

所以,在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。

image-20210812142046890

Infra 容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。

在 Infra 容器“Hold 住”Network Namespace 后,用户容器就可以加入到 Infra 容器的Network Namespace 当中了。

所以Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关

编排

控制器模式

编排的过程,

  1. 比如Deployment控制器从etcd中获取带某个标签的pod的数量,这就是实际状态
  2. 和yaml中的replicas字段就是期望状态
  3. 根据期望状态和实际状态的比较,确定下一步的动作

被控制对象的定义,则来自于一个模板。比如yaml中的template,称为pod模板

image-20210813105711300

Deployment 控制器实际操纵的,是 ReplicaSet 对象,而不是 Pod 对象。

image-20210813110718323

在用户提交了一个 Deployment 对象后,Deployment Controller 就会立即创建一个 Pod 副本个数为 3 的 ReplicaSet。这个 ReplicaSet 的名字,则是由 Deployment 的名字和一个随机字符串共同组成 `

滚动升级的流程

image-20210813134916854

滚动升级时,是新起一个rs,然后起pod,停掉旧rs的pod。最后停旧rs

kubectl rollout undo 命令,就能把整个 Deployment 回滚到上一个版本

StatefulSet

有状态的状态分类:拓扑状态(如主从),存储状态

拓扑状态

service是如何被访问的

第一种方式:通过service的vip

第二种方式:通过service的dns。又分两种处理方式,

  • normal service。访问服务域名dns解析到的就是这个服务的vip。后面的和第一种方式流程一样。
  • headless service。这种访问服务域名解析到的直接就是某一个pod的ip。headless service不需要分配一个vip,直接dns解析出具体的pod的ip

headless service

在yaml文件中,headless service的定义是通过clusterIP:none。这样定义出的service就是headless service。没有vip这个头。

当你按照这样的方式创建了一个 Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:

...svc.cluster.local

这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”

有了这个“可解析身份”,只要你知道了一个 Pod 的名字,以及它对应的 Service 的名字,你就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。

StatefulSet 正是使用这个 DNS 记录来维持 Pod 的拓扑状态

StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是-数字

更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。

通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性。

存储状态

PVC&PV

PVC由开发人员根据自己的需求定义

PV由运维人员提前维护

K8S会给pvc绑定pv

类似一种接口和实现的关系。pvc是接口,pv是具体的实现。

DaemonSet

DaemonSet类型pod的特征

  1. 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;

  2. 每个节点上只有一个这样的 Pod 实例;

  3. 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。

场景:网络插件的agent组件,存储插件的agent组件,监控组件,日志组件

工作原理

DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。检查node上是否有一个这个DaemonSet pod。这就是典型的控制器模型

对于创建来说,需要保证在指定的node上创建pod, DaemonSet Controller 会在创建 Pod 的时候,自动在这个 Pod 的 API 对象里,加上一个 nodeAffinity 定义。其中,需要绑定的节点名字,正是当前正在遍历的这个Node

当然,DaemonSet 并不需要修改用户提交的 YAML 文件里的 Pod 模板,而是在向Kubernetes 发起请求之前,直接修改根据模板生成的 Pod 对象。