读书:深入刨析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限定的资源、文件、设备、网络等等资源。而对于宿主机以及其他不相干的程序完全看不到。
总之,容器只是一个特殊一点的进程而已。
.assets/image-20220121170136227.png)
进一步理解,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 | # docker run -it -d busybox |
进入容器查看ps,看到当前主进程号是1。隔离。
使用docker inpect查看下在宿主机上真正的PID。 “Pid”: 32694
1 | # docker inspect 68f11c135b |
在宿主机上查看
1 | # ps -ef | grep 32694 |
查看32694进程的namespace资源
1 | # ls -l /proc/32694/ns |
Cgroup
Cgroup的全称是Control Group,作用是限制一个进程组能够使用的资源上限,比如CPU,内存,磁盘,网络带宽等
linux的实现方式是在一个特定的目录下有特定的配置文件。/sys/fs/cgroup/
1 | # ls -l /sys/fs/cgroup/ |
这一个个的文件夹就是cgroup可以限制的资源类型。比如cpu
1 | # ls -l /sys/fs/cgroup/cpu/ |
比如cfs_period_us和cfs_quota_us参数就是,限制进程在长度为 cfs_period 的一段时间内,只 能被分配到总量为 cfs_quota 的 CPU 时间。
.assets/image-20220121171625696.png)
注意圈出的docker文件夹。
1 | # ls -l /sys/fs/cgroup/cpu/docker |
还记得我们之前建的busybox容器吗?68f11c135bcdf9d5f793a6f12e90e37b5bca07735b9d715c2a02a163d3715c77
对这个容器的资源限制就在这里
1 | # cat cpu.cfs_quota_us |
创建的时候我们没有限制资源,所以这里显式是-1,即不限制。
我们再运行一个Ubuntu容器
1 | # docker run -it -d --cpu-period=100000 --cpu-quota=20000 busybox |
period=100000 配合 cpu-quota=20000的含义是:在每 100 ms 的时间里,被该控制组 限制的进程只能使用 20 ms 的 CPU 时间,即限制只能使用20%的cpu算力。
再去查看period和quota
1 | # cat cpu.cfs_period_us |
容器是一个单进程模型。一个容器的本质是一个进程。用户的应用进程实际上就是容器里 PID=1 的进程,也是其他后续创建的所有进程的父进程。
这就意味着,在一个容器中,你没办法同时运行两个不同的应 用。这是因为容器本身的设计,就是希望容器和应用能够同生命周期,这个概念对后续的容器编排非常重要。否则,一旦出现类似于“容器是正常运行的,但是里面的应用早已经挂了”的情况,编排系统处理起来就非常麻烦了
对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
启用 Linux Namespace 配置;
设置指定的 Cgroups 参数;
切换进程的根目录(Change Root)。
需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。所以说,rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。**实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。**这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身
Kubernetes架构
.assets/image-20210811154142473.png)
两类节点: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进行机器学习等要关注这个组件。
.assets/image-20210811172904096.png)
Service 服务声明的 IP 地址等信息是“终生不变”的。这个Service 服务的主要作用,就是作为 Pod 的代理入口(Portal),从而代替 Pod 对外暴露一个固定的网络地址。Service 后端真正代理的 Pod 的 IP 地址、端口等信息的自动更新、维护,则是 Kubernetes 项目的职责。
master组件的部署
kubelet是在服务器上直接部署的,其他的组件在容器中启动。通过yaml部署的。
master组件的yaml放在 /etc/kubernetes/manifests
.assets/image-20210812093719676.png)
这些 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 容器关联在一起。
.assets/image-20210812142046890.png)
Infra 容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。
在 Infra 容器“Hold 住”Network Namespace 后,用户容器就可以加入到 Infra 容器的Network Namespace 当中了。
所以Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关
编排
控制器模式
编排的过程,
- 比如Deployment控制器从etcd中获取带某个标签的pod的数量,这就是实际状态
- 和yaml中的replicas字段就是期望状态
- 根据期望状态和实际状态的比较,确定下一步的动作
被控制对象的定义,则来自于一个模板。比如yaml中的template,称为pod模板
.assets/image-20210813105711300.png)
Deployment 控制器实际操纵的,是 ReplicaSet 对象,而不是 Pod 对象。
.assets/image-20210813110718323.png)
在用户提交了一个 Deployment 对象后,Deployment Controller 就会立即创建一个 Pod 副本个数为 3 的 ReplicaSet。这个 ReplicaSet 的名字,则是由 Deployment 的名字和一个随机字符串共同组成 `
滚动升级的流程
.assets/image-20210813134916854.png)
滚动升级时,是新起一个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的特征
这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
每个节点上只有一个这样的 Pod 实例;
当有新的节点加入 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 对象。