读书:深入刨析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限定的资源、文件、设备、网络等等资源。而对于宿主机以及其他不相干的程序完全看不到。
总之,容器只是一个特殊一点的进程而已。
进一步理解,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 时间。
注意圈出的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 项目来说,它最核心的原理实际上就是为待创建的用户进程:
启用 Linux Namespace 配置;
设置指定的 Cgroups 参数;
切换进程的根目录(Change Root)。
需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。所以说,rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。**实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。**这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身
Kubernetes架构
两类节点: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进行机器学习等要关注这个组件。
Service 服务声明的 IP 地址等信息是“终生不变”的。这个Service 服务的主要作用,就是作为 Pod 的代理入口(Portal),从而代替 Pod 对外暴露一个固定的网络地址。Service 后端真正代理的 Pod 的 IP 地址、端口等信息的自动更新、维护,则是 Kubernetes 项目的职责。
master组件的部署 kubelet是在服务器上直接部署的,其他的组件在容器中启动。通过yaml部署的。
master组件的yaml放在 /etc/kubernetes/manifests
这些 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 容器关联在一起。
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模板
Deployment 控制器实际操纵的,是 ReplicaSet 对象,而不是 Pod 对象。
在用户提交了一个 Deployment 对象后,Deployment Controller 就会立即创建一个 Pod 副本个数为 3 的 ReplicaSet。这个 ReplicaSet 的名字,则是由 Deployment 的名字和一个随机字符串共同组成 `
滚动升级的流程
滚动升级时,是新起一个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 对象。