【总结自张磊-深入剖析Kubernetes】
【图片看不到,可以去
**Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈看不见的墙。**这
么一折腾,进程就真的被“装”在了一个与世隔绝的房间里,而这些房间就是 PaaS 项目赖以生存的应用“沙盒”。,“Namespace 做隔离,Cgroups 做限制,rootfs 做文件系统”
参考链接
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NxjfHsEj-1623207279252)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210510230243050.png)]
Hypervisor 的软件是虚拟机最主要的部分。它通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如 CPU、内存、
I/O 设备等等。然后,它在这些虚拟的硬件上安装了一个新的操作系统,即 Guest OS。
Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的Namespace 参数。而不是使用docker engine代替hypervisor,但是多个容器之间使用的就还是同一个宿主机的操作系统内核,因此linux版本和不同系统的容器并不兼容。而虚拟机则是可以做到这种情况
在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,比如时间;此外,共享宿主机内核的事实,容器给应用暴露出来的攻击面是相当大的,应用“越狱”的难度自然也比虚拟机低得多
在生产环境中,这个问题必须进行修正,否则应用程序在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据,这会给应用的运行带来非常大的困惑和风险。这也是在企业中,容器化应用碰到的一个常见问题,也是容器相较于虚拟机另一个不尽如人意的地方
最核心的原理实际上就是为待创建的用户进程:
Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。也就是通过查看宿主机的 proc 文件,看到这个 25686 进程的所有 Namespace 对应的文件
ls -l /proc/25686/ns
一个进程的每种 Linux Namespace,都在它对应的/proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上,这也就意味着:一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。而这个操作所依赖的,乃是一个名叫 setns() 的 Linux 系统调用
具体实现:直接挂载
容器的“单进程模型”,并不是指容器里只能运行“一个”进程,而是指容器没有管理多个进程的能力,这是因为容器里 PID=1 的进程就是应用本身,其他的进程都是这个PID=1 进程的子进程。所以一般一个容器里面只有一个进程。
Pod,是 Kubernetes 项目中最小的 API 对象, 是 Kubernetes 项目的原子调度单位。
因为存在容器成组问题,这种问题在调度上比较难以来做(成组调度问题),资源囤积带来了不可避免的调度效率损失和死锁的可能性;而乐观调度的复杂程度,则不是常规技术团队所能驾驭的。而pod的引入,就是将容器打包在一起,可以不管这种成组调度问题了。
Pod,其实是一组共享了某些资源的容器,Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个Volume。
在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。对于 Pod 里的容器 A 和容器 B 来说
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bg67dpWT-1623207279254)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210514000023852.png)]
v1.11之后特性,存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换,是为容器提供预先定义好的数据,有四种:
Pod的“水平扩展 / 收缩”(horizontal scaling out/in)
ReplicaSet,由副本数目的定义和一个 Pod 模板组成的。不难发现,它的定义其实是 Deployment 的一个子集,实现pod的滚动更新。
对于一个 Deployment 所管理的 Pod,它的 ownerReference 是ReplicaSet
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sWbns9bF-1623207279257)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210518222717150.png)]
Deployment 控制 ReplicaSet(版本),ReplicaSet 控制 Pod(副本数)。
ReplicaSet 负责通过“控制器模式”,保证系统中 Pod 的个数永远等于指定的个数,–》 deployment只允许容器restartPolicy=Always
,因为只有容器保证自己是running
状态下,ReplicaSet调整pod的个数才有意义,如果pod里面容器都是never
,运行完了不就没了,pod就死掉了(Failed )
水平拓展
kubectl scale deployment nginx-deployment --replicas=4/1
滚动更新
kubectl create -f nginx-deployment.yaml --record
kubectl rollout undo 滚动回去上一个版本
deployment对象状态查看
kubectl rollout status deployment/nginx-deployment
只生成一个ReplicaSet
Deployment更新操作会生成多个ReplicaSet,因此可以使用
kubectl rollout pause deployment/nginx-deployment
暂停deployment,因此对于deployment所有修改不会触发滚动更新。
kubectl rollout resume deploy/nginx-deployment
Deployment 修改操作都完成之后,只需要再执行该指令,就可以把这个 Deployment“恢复”回来
当然,也可以使用spec.revisionHistoryLimit
来限制历史ReplicaSet数量,如果为0
则再也不能回滚操作。
为了解决Deployment认为一个应用的pod都是完全一样的, 之间没有顺序,也没有所谓运行在哪一个主机上的问题。这个问题让Deployment随意创建或者删掉一个pod。
但是,实际上很多应用,特别时分布式应用,他们之间多个实例往往存在依赖关系:主从关系和主备关系等。还有一些数据存储类应用,被杀掉后丢失实例与数据之间的对应关系。这些实例间存在不对等关系以及实例对外部数据有依赖关系的应用,被称为有状态应用
由此,StatefulSet就是为了解决这个问题。
StatefulSef设计
通过这样的状态设计,SatefulSet将真实世界里的应用状态,抽象出来,并通过某种方式来记录这些状态。
Headless Service
Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。用户通过访问Service就可以访问到具体的Pod。
访问Service方式有:
创建Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定为以下格式的 DNS 记录:
...svc.cluster.local
# 改记录k8s为 Pod 分配的唯一的“可解析身份”
**Persistent Volume Claim PVC **
解决过度暴露问题,大大降低用户声明和使用持久化Volume的门槛
StatefulSet 拓扑状态维护
StatefulSet 就是通过Headless Service维护网络拓扑结构,编号方式维护pod的顺序。
StatefulSet 存储状态维护
StatefulSet 通过PVC、PV 来管理存储状态
--< 编号 >
的方式命名,并且处于 Bound 状态。也就是说,StatefulSet通过维护一个有状态的PVC,进而找到跟这个 PVC 绑定在一起的PV。这个PVC名字是不会变的重新创建一个pod时。
总结
在 Kubernetes 集群里,运行一个 Daemon Pod,这个pod特点有:
意义
DaemonSet 其实是一个非常简单的控制器。在它的控制循环中,只需要遍历所有节点,然后根据节点上是否有被管理 Pod 的情况,来决定是否要创建或者删除一个 Pod。
版本管理
DaemonSet也存在版本号,但是和deployment控制器不一样,通过ReplicaSet来管理容器版本,DaemonSet的对象是pod,没有ReplicaSet来管理。但是k8s通过抽象对象来实现对应的功能。Kubernetes v1.7 之后添加了一个 API 对象,名叫ControllerRevision,专门用来记录某种Controller 对象的版本
万物皆对象,将DaemonSet和ReplicaSet都看抽象为ControllerRevision对象,通过这个ControllerRevision来进行管理。
特别地,如果undo一个对象,相当于进行一次 Patch
操作,也就是将新版本更新为一个旧版本,所以对应这个 旧版本
实际上是一个 新版本
,对应版本号 加一。
PS: 对于操作粒度为Pod的控制器(如StatefulSet),也是通过ControllerRevision进行版本管理的。
Job,对于离线任务的管理
这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。Job 对象本身,则被自动加上了这个 Label 对应的Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。避免了不同 Job 对象所管理的 Pod 发生重合
离线计算的 Pod 永远都不应该被重启,否则它们会再重新计算一遍
restartPolicy只有两种类型:
Never:
运行结束后pod状态变为completed,如果离线作业失败,Job Controller就会不断地尝试创建一个新 Pod,Job 对象的 spec.backoffLimit 字段
里定义了重试次数为 4, 默认值是 6
Job Controller 重新创建 Pod 的间隔是呈指数增加的,即下一次重新创建 Pod的动作会分别发生在 10 s、20 s、40 s …后
OnFailure:
离线作业失败后,Job Controller 就不会去尝试创建新的 Pod
会不断地尝试重启 Pod 里的容器
spec.activeDeadlineSeconds 字段可以设置最长运行时间,避免jod一直不肯结束。
并行控制
常用的、使用 Job 对象的方法
CronJob,定时任务
一个专门管理Job对象的控制器
创建和删除 Job 的依据,是 schedule 字段定义的、一个标准的Unix Cron格式的表达式。
"*/1 * * * *"
Cron 表达式里 */1 中的 * 表示从 0 开始,/ 表示“每”,1 表示偏移量。所以,它的意思就是:从 0 开始,每 1 个时间单位执行一次
表达式中的五个部分分别代表:分钟、小时、日、月、星期。
处理一个job未完成,新的job就被创建的情况
spec.startingDeadlineSeconds 字段指定该时间段内Job 创建失败miss数达100后,job就不会被创建执行。
apply replace create区别
更进一步地,这意味着 kube-apiserver 在响应命令式请求(比如,kubectl replace)的时候,一次只能处理一个写请求,否则会有产生冲突的可能。而对于声明式请求(比如,kubectl apply),一次能处理多个写操作,并且具备 Merge 能力。
什么是声明式
声明式API对象组成
由**Group(API 组)、Version(API 版本)和 Resource(API 资源类型)**三个部分组成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bo8niCIC-1623207279259)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210523150550090.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZAiT5hJa-1623207279260)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210523164354339.png)]
CRD(Custom Resource Definition) 和 CC(Custom Controller)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bBlPRxFo-1623207279262)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210523164304010.png)]
条件检查
PersistentVolumeController
控制绑定PV和PVC
PersistentVolumeController
属于 Volume Controller维护的多个控制循环中的一个就是为了方便用户拓展存储体系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-plbR2eqV-1623207279263)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210525160502182.png)]
在上述体系下,无论是 FlexVolume,还是 Kubernetes 内置的其他存储插件,它们实际上担任的角色,仅仅是 Volume 管理中的“Attach 阶段”和“Mount 阶段”的体执行者。
像 Dynamic Provisioning 这样的功能,就不是存储插件的责任,而是 Kubernetes 本身存储管理功能的一部分。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LJXs26Uy-1623207279264)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210525161049610.png)]
CSI插件(最右边)
需要我们自行编写,以grpc方式对外提供三个服务:
CSI Identity:CSI 插件的 CSI Identity 服务,负责对外暴露这个插件本身的信息
CSI Controller:(主机操作)
对 CSI Volume(对应 Kubernetes 里的 PV)的管理接口,比如:创建和删除 CSI Volume、对 CSI Volume 进行 Attach/Dettach(在 CSI 里,这个操作被叫作 Publish/Unpublish),以及对 CSI Volume 进行 Snapshot 等。
CSI Controller 服务里定义的这些操作有个共同特点,那就是它们都无需在宿主机上进行,而是属于 Kubernetes 里 Volume Controller 的逻辑,也就是属于 Master 节点的一部分。rpc服务完成。
CSI Controller 服务的实际调用者,并不是Kubernetes(即:通过 pkg/volume/csi 发起 CSI 请求),而是 External Provisioner 和External Attacher。这两个 external Components,分别通过监听 PVC 和VolumeAttachement 对象,来跟 Kubernetes 进行协作。
CSI Node:(宿主机操作)
CSI Volume 需要在宿主机上执行的操作,都定义在了 CSI Node 服务里面,包括mount操作
独立外部组件
**Driver Registrar:**负责将插件注册到kubelet里面;Driver Registrar 需要请求 CSI 插件的 Identity 服务来获取插件信息。
**External Provisioner:**负责的正是 Provision 阶段。在具体实现上,ExternalProvisioner 监听(Watch)了 APIServer 里的 PVC 对象。当一个 PVC 被创建时,它就会调用 CSI Controller 的 CreateVolume 方法,为你创建对应 PV。注意,CSI会自己定义一个单独的 Volume 类型
External Attacher:负责的正是“Attach 阶段”。在具体实现上,它监听了APIServer 里 VolumeAttachment 对象的变化(确认一个 Volume 可以进入“Attach 阶段”的重要标志)。一旦出现了 VolumeAttachment 对象,External Attacher 就会调用 CSI Controller 服务的ControllerPublish 方法,完成它所对应的 Volume 的 Attach 阶段。
PS:Volume 的“Mount 阶段”,并不属于 External Components 的职责。当 kubelet 的VolumeManagerReconciler 控制循环检查到它需要执行 Mount 操作的时候,会通过pkg/volume/csi 包,直接调用 CSI Node 服务完成 Volume 的“Mount 阶段”。
总得来说
第一,通过 DaemonSet 在每个节点上都启动一个 CSI 插件,来为 kubelet 提供 CSI Node 服 务。这是因为,CSI Node 服务需要被 kubelet 直接调用,所以它要和 kubelet“一对一”地部 署起来。此外,在上述 DaemonSet 的定义里面,除了 CSI 插件,我们还以 sidecar 的方式运行着 driver-registrar 这个外部组件。它的作用,是向 kubelet 注册这个 CSI 插件。这个注册过程使 用的插件信息,则通过访问同一个 Pod 里的 CSI 插件容器的 Identity 服务获取到。
在定义 DaemonSet Pod 的时候,我们需要把宿主机的 /var/lib/kubelet 以 Volume 的 方式挂载进 CSI 插件容器的同名目录下,然后设置这个 Volume 的 mountPropagation=Bidirectional,即开启双向挂载传播,从而将容器在这个目录下进行的挂 载操作“传播”给宿主机,反之亦然。
第二,通过 StatefulSet 在任意一个节点上再启动一个 CSI 插件,为 External Components 提 供 CSI Controller 服务。所以,作为 CSI Controller 服务的调用者,External Provisioner 和 External Attacher 这两个外部组件,就需要以 sidecar 的方式和这次部署的 CSI 插件定义在同 一个 Pod 里。
将 StatefulSet 的 replicas 设置为 1 的话,StatefulSet 就会确保 Pod 被删 除重建的时候,永远有且只有一个 CSI 插件的 Pod 运行在集群中。这对 CSI 插件的正确性来 说,至关重要。
一个 Network Namespace 的网络栈包括:网卡(Network Interface)、回环设备(Loopback Device)、路由表(Routing Table)和 iptables 规则。
容器之间存在namespace隔离,因为我们希望容器有自己的ip地址以及对应的端口。
因此容器之间的通信,主要靠一个叫docker0的网桥,连接在这个docker网桥上的容器,都可以通过它进行通信。
而链接到docker0上,则是通过一个叫 Veth Pair的虚拟设备::它被创建出来后,总是以两张虚拟网卡(Veth Peer)的形式成对出现的。并且,从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网 卡”上,哪怕这两个“网卡”在不同的 Network Namespace 里。(可穿透namespace传递消息)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7sdM0krM-1623207279265)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210525230433829.png)]
这个过程实际上就是:
类似地,当你在一台宿主机上,访问该宿主机上的容器的 IP 地址时,这个请求的数据包,也是先根据路由规则到达 docker0 网桥,然后被转发到对应的 Veth Pair 设备,最后出现在容器里。这个过程的示意图,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VCm9luUu-1623207279265)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210525231155755.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WWXePKGE-1623207279266)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210525231259802.png)]
你遇到容器连不通“外网”的时候,你都应该先试试 docker0 网桥能不能 ping通,然后查看一下跟 docker0 和 Veth Pair 设备相关的 iptables 规则是不是有异常
消息通过Flannel子网信息(保存在Etcd中),确定需要转发的宿主机,然后Flannel将消息封装为UDP发送过去,目的主机通过flannel发送到docker0。
Flannel UDP 模式提供的其实是一个三层的 Overlay 网络,即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。
性能限制:需要经过三次用户态与内核态之间的数据拷贝。
此外,Flannel 还要进行 UDP 封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的。
PS:进行系统级编程的时候,有一个非常重要的优化原则,就是要减少用户态到内核态 的切换次数,并且把核心的处理逻辑都放在内核态进行
VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。所以说,VXLAN 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面 相似的“隧道”机制,构建出覆盖网络(Overlay Network)。
设计思想
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LimLzlEn-1623207279267)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210526205227682.png)]
大致过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HQkXrY8o-1623207279268)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210526210656227.png)]
为什么使用UDP
这里使用UDP,不需要可靠,因为可靠性不是这一层需要考虑的事情,这一层需要考虑的事情就是把这个包发送到目标地址。如果出现了UDP丢包的情况,也完全没有关系,这里UDP包裹的是我们真正的数据包,如果我们上层使用的是TCP协议,而Flannel使用UDP包裹了我们的数据包,当出现丢包了,TCP的确认机制会发现这个丢包而重传,从而保证可靠。
感觉就是我们做的事情是网络层传输,而可靠性是属于传输层的更上一层来保证,所以没有必要用tcp协议
总结
不难看到,这两种方法有一个共性,那就是用户的容器都连接在 docker0 网桥上。而网络插件则 在宿主机上创建了一个特殊的设备(UDP 模式创建的是 TUN 设备,VXLAN 模式创建的则是 VTEP 设备),docker0 与这个设备之间,通过 IP 转发(路由表)进行协作。
维护一个路由表,确定某一个IP对应的下一跳IP地址(以及对应的MAC地址,ARP协议自己确定)
host-gw 模式的工作原理,其实就是将每个 Flannel 子网(Flannel Subnet,比 如:10.244.1.0/24)的**“下一跳”,设置成了该子网对应的宿主机的 IP 地址。**这台“主机”(Host)充当这条容器通信路径里的“网关”(Gateway)
PS: Flannel 子网和主机的信息,都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化,然后实时更新路由表即可;在 Kubernetes v1.7 之后,类似 Flannel、Calico 的 CNI 网络插件都是可 以直接连接 Kubernetes 的 APIServer 来访问 Etcd 的,无需额外部署 Etcd 给它 们使用。
Calico 项目
通过BGP协议维护路由之间的规则关系
组成
工作原理
Calico 项目实际上将集群里的所有节点,都当作是边界路由器来处理,它们一起组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则。
Calico 维护的网络在默认配置下,是一个被称为**“Node-to-Node Mesh”的 模式。这时候,每台宿主机上的 BGP Client 都需要跟其他所有节点的 BGP Client 进行通信以 便交换路由信息。只适用于少于100个节点规模**
更大规模的集群中,使用 Route Reflector 的模式。Calico 会指定一个或者几个专门的节点,来负责跟所有节点建立 BGP 连接从而 学习到全局的路由规则。而其他节点,只需要跟这几个专门的节点交换路由信息,就可以获得整 个集群的路由规则信息了。
IPIP 模式突破不同子网问题(保证三层连通性)
Kubernetes 对容器网络也类似之前的做法,但是他通过一个CNI的插件接口,维护了一个单独的网桥来代替 docker0(CNI 网桥,它在宿主机上的设备名称默认是:cni0)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R0JM6TQM-1623207279269)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210526212357934.png)]
docker0换成cni0
需要注意的是,CNI 网桥只是接管所有 CNI 插件负责的、即 Kubernetes 创建的容器 (Pod)。而此时,如果你用 docker run 单独启动一个容器,那么 Docker 项目还是会把这个 容器连接到 docker0 网桥上。所以这个容器的 IP 地址,一定是属于 docker0 网桥的 172.17.0.0/16 网段。
为何提出cni插件
cni工作原理
当 kubelet 组件需要创建 Pod 的时候,它第一个创建的一定是 Infra 容器。所以在这一步, dockershim 就会先调用 Docker API 创建并启动 Infra 容器,紧接着执行一个叫作 SetUpPod 的方法。这个方法的作用就是:为 CNI 插件准备参数,然后调用 CNI 插件为 Infra 容器配置网络。
调用 CNI 插件所需要参数
是由 dockershim 设置的一组 CNI 环境变量,最重要的环境变量参数叫作:CNI_COMMAND。它的取值只有两种:ADD 和 DEL
ADD:把容器添加到 CNI 网络里
参数有:容器里网卡的名字 eth0(CNI_IFNAME)、Pod 的 Network Namespace 文件的路径(CNI_NETNS)(/proc/< 容器进程的 PID>/ns/net
)、容器的 **ID(CNI_CONTAINERID)**等。 这些参数都属于上述环境变量里的内容。
CNI_ARGS 的参数:CRI 实现 (比如 dockershim)就可以以 Key-Value 的格式,传递自定义信息给网络插件。这是用户将 来自定义 CNI 协议的一个重要方法。
DEL:把容器从 CNI 网络里移除掉。
dockershim 从 CNI 配置文件里加载到的、默认插件的配置信息
dockershim 对 Flannel CNI 插件的调用,其实就是走了个过场。Flannel CNI 插件唯 一需要做的,就是对 dockershim 传来的 Network Configuration 进行补充。比如,将 Delegate 的 Type 字段设置为 bridge,将 Delegate 的 IPAM 字段设置为 host-local 等。也就是修改json文件,再调用/opt/cni/bin/bridge插件
CNI bridge 插件过程(代表了Flannel进行容器加入CNI网络)
PS
处理容器网络相关的逻辑并不会在 kubelet 主干代码里执 行,而是会在具体的 CRI(Container Runtime Interface,容器运行时接口)实现里完成
Kubernetes 目前不支持多个 CNI 插件混用。如果你在 CNI 配置目录 (/etc/cni/net.d)里放置了多个 CNI 配置文件的话,dockershim 只会加载按字母顺序排序的 第一个插件。
CNI 允许你在一个 CNI 配置文件里,通过 plugins 字段,定义多个插件进行协作。
总结
通过networkPolicy实现,主要就是根据networkPolicy在宿主机上生成iptables 规则(规则的名字是 KUBE-NWPLCY-CHAIN,含义是:当 IP 包的源地址是 srcIP、目的地址是 dstIP、协议是 protocol、目的端口是 port 的时候,就允许它通过(ACCEPT))
实现Pod访问请求都转发到上述 KUBE-NWPLCY-CHAIN 规则上去进行匹配。并且,如果匹配不通过,这 个请求应该被“拒绝。(设置两组iptables规则)
第一组规则,负责“拦截”对被隔离 Pod 的访问请求。拦截同一台宿主机上容器之间经过 CNI 网桥进行通信的流入数据包。其中,–physdev-is-bridged 的意思就是,这个 FORWARD 链匹配的是,通过本机上的网桥设备,发往目的地址是 podIP 的 IP 包
iptables -A FORWARD -d $podIP -m physdev --physdev-is-bridged -j KUBE-POD-SPECIFIC-FW-CHAIN
第二组规则,拦截容器跨主机通信;流入容器的数据包都是经过路由转发(FORWARD 检查点)来的。
iptables -A FORWARD -d $podIP -j KUBE-POD-SPECIFIC-FW-CHAIN
这个 KUBE-POD-SPECIFIC-FW-CHAIN 的作用,就是做出“允许”或者“拒绝”的判断。
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j KUBE-NWPLCY-CHAIN
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j REJECT --reject-with icmp-port-unreachable
Netfilter科普
iptables 只是一个操作 Linux 内核 Netfilter 子系统的“界面”。顾名思义,Netfilter 子系统的作用,就是 Linux 内核里挡在“网卡”和“用户态进程”之间的一道“防火墙”。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwIc5Xka-1623207279270)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210527104939329.png)]
IP 包“一进一出”的两条路径上,有几个关键的“检查点”,它们 正是 Netfilter 设置“防火墙”的地方。在 iptables 中,这些“检查点”被称为:链 (Chain)。这是因为这些**“检查点”对应的 iptables 规则,是按照定义顺序依次进行匹配** 的。所谓“检查点”实际上就是内核网络协议栈代码里的 Hook函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kCEXS7Ih-1623207279270)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210527105129027.png)]
检查点主要有5个,prerouting,forward, postrouting,input, output;当一个 IP 包通过网卡进入主机之后,它就进入了 Netfilter 定义的流入路径(Input Path)里。
iptables 表的作用,就是在某个具体的“检查点”(比如 Output)上,按顺序执行几个不同的检查动作(比如,先执行 nat,再执行 filter)
Service管理Pod IP以及负载均衡。实际上,Service 是由 kube-proxy 组件,加上 iptables 来共同实现的
IPVS模式
克服宿主机存在大量pod时iptables 过多且需要不断刷新的问题。(极其浪费资源)
ClusterIP 模式的 Service 为你提供的,就是一个 Pod 的稳定的 IP 地址,即 VIP。并且,这里 Pod 和 Service 的关系是可以通过 Label 确定的
Headless Service 为你提供的,则是一个 Pod 的稳定的 DNS 名字,并且,这个名字是可以 通过 Pod 名字和 Service 名字拼接出来的
资源模型
Pod资源配置:
QoS模型:
类型
QoS 划分的主要应用场景,是当宿主机资源紧张的时候,kubelet 对 Pod 进行Eviction(即资源回收)时需要用到的
cpu独占设置:
默认调度器
默认调度器的主要职责,就是为一个新创建出来的 Pod,寻找一个最合适的节点(Node)。
调度流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SVGZ3Oef-1623207279271)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210529105344404.png)]
更具体地:
informer path 循环:启动一系列的informer,监听Etcd中Pod、Node、Service等与调度相关的API对象的变化
需要调度的对象会被放进一个调度队列(具有一定优先级设计的队列,比如说,FIFO或者其他自定义优先级队列)(AC:队列的优化会可以提高一定的响应性,也就是对于响应性的高的需要优先调度完成,具体业务具体分析)
调度器缓存更新,Kubernetes 调度部分进行性能优化的一个最根本原则,就是尽最大可能将集群信息Cache 化,以便从根本上提高 Predicate 和 Priority 调度算法的执行效率。(AC:状态维护,避免每一次重新对整个集群进行一个查询,浪费时间)
Scheduling Path主循环:负责调度Pod的主循环
性能提升三设计
调度器的拓展
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aPctS1pE-1623207279273)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210529112154039.png)]
Scheduler Framework: 在调度器生命周期的各个关键点上,为用户暴露出可以进行扩展和实现的接口,从而实现由用户自定义调度器的能力。接口就是上面绿色label
上述这些可插拔式逻辑,都是标准的 Go 语言插件机制(Go plugin 机制)
Predicates
类型:
GeneralPredicates, 一组过滤规则,负责的是最基础的调度策略
Volume 相关的过滤规则,负责的是跟容器持久化 Volume 相关的调度策略
NoDiskConflict 检查的条件,是多个 Pod 声明挂载的持久化 Volume 是否有冲突
MaxPDVolumeCountPredicate 检查的条件,则是一个节点上某种类型的持久化 Volume是不是已经超过了一定数目,如果是的话,那么声明使用该类型持久化 Volume 的 Pod 就不能再调度到这个节点了。
VolumeZonePredicate,则是检查持久化 Volume 的 Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配。
VolumeBindingPredicate 的规则。它负责检查的,是该 Pod 对应的PV 的 nodeAffinity 字段,是否跟某个节点的标签相匹配
宿主机相关的过滤规则,主要考察待调度 Pod 是否满足 Node 本身的某些条件。
Pod 相关的过滤规则,跟 GeneralPredicates 大多数是重合的
topologyKey
)具体执行过程:
开始调度一个 Pod 时,Kubernetes 调度器会同时启动 16 个Goroutine,来并发地为集群里的所有 Node 计算 Predicates,最后返回可以运行这个 Pod的宿主机列表。
在为每个 Node 执行 Predicates 时,调度器会按照固定的顺序来进行检查。这个顺序,是按照 Predicates 本身的含义来确定的
Priorities
Predicates 阶段完成了节点的“过滤”之后,Priorities 阶段的工作就是为这些节点打分。这里打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。
打分规则:
解决Pod调度失败时的问题
优先级
抢占
抢占的实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APdPZmxm-1623207279274)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530102641732.png)]
实现对Externed Resource的管理
kubelet 是在每个 Node 节点上运行的主要 “节点代理”。它可以使用以下之一向 apiserver 注册: 主机名(hostname);覆盖主机名的参数;某云驱动的特定逻辑。
kubelet 是基于 PodSpec 来工作的。每个 PodSpec 是一个描述 Pod 的 YAML 或 JSON 对象。 kubelet 接受通过各种机制(主要是通过 apiserver)提供的一组 PodSpec,并确保这些 PodSpec 中描述的容器处于运行状态且运行状况良好。 kubelet 不管理不是由 Kubernetes 创建的容器
除了PodSpec方式接收容器清单(manifest)信息,还有基于file,http endpoints, http server三种形式
kubelet 以及容器运行时管理相关的内容,都属于 SIG-Node 的范畴
kubelet
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZppajLbp-1623207279275)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530103954326.png)]
kubelet 的工作核心,就是一个控制循环,即:SyncLoop(图中的大圆圈)。而驱动这个控制循环运行的事件,包括四种:
跟其他控制器类似,kubelet 启动的时候,要做的第一件事情,就是设置 Listers,也就 是注册它所关心的各种事件的 Informer。这些 Informer,就是 SyncLoop 需要处理的数据的来源。
kubelet 还负责维护着很多很多其他的子控制循环(也就是图中的小圆圈)。这些控制循 环的名字,一般被称作某某 Manager,比如 Volume Manager、Image Manager、Node Status Manager。这些控制循环的责任,就是通过控制器模式,完成 kubelet 的某项具体职责。
kubelet 也是通过 Watch 机制,监听了与自己相关的 Pod(Pod 的信息缓存在自己的内存里) 对象的变化。
Pod调度绑定Node,触发Handler(HandlePods)的ADD事件进行处理:,kubelet 会启动一个名叫 Pod Update Worker 的、单独的 Goroutine 来完成对 Pod 的处理工作,对于ADD 事件的话,kubelet 就会为这个新的 Pod 生成对应的 Pod Status,检查 Pod 所声明使用的 Volume 是不是已经准备好。然后,调用下层的容器运行时(比如 Docker),开始创建这个 Pod 所定义的容器;对于UPDATE 事件的话,kubelet 就会根据 Pod 对象具体的变更情况,调用下层容器运行 时进行容器的重建工作。
引入kubelet这样一层单独的抽象,当然是为了对 Kubernetes 屏蔽下层容器运行时的差异,比如说对docker,rkt,还有另外一种非linux容器,runV(虚拟化容器,基于虚拟化技术的强隔离容器),这些CRI就只需要自己提供一个该接口的实现,然后对 kubelet 暴露出 gRPC 服务即可。
每台宿主机上单独安装一个负责响应 CRI 的组件,这个组件, 一般被称作 CRI shim,扮演 kubelet 与容器项目之间 的“垫片”(shim),实现 CRI 规定的每个接口,然后把具体 的 CRI 请求“翻译”成对后端容器项目的请求或者操作
CRI 机制能够发挥作用的核心,就在于每一种容器项目现在都可以自己实现一个 CRI shim,自行对 CRI 请求进行处理。比如CNCF 里的 containerd 项目,就可以提供一个典型的 CRI shim 的能力,即:将 Kubernetes 发出的 CRI 请求,转换成对 containerd 的调用,然后创建出 runC 容器。而 runC 项目,才是负责执行我们前面讲解过的设置容器 Namespace、Cgroups 和 chroot 等基 础操作的组件。
CRI具体设计
调用过程实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tjp6nw4o-1623207279276)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530125947152.png)]
当我们执行 kubectl run 创建了一个名叫 foo 的、包括了 A、B 两个容器的 Pod 之后。 这个 Pod 的信息最后来到 kubelet,kubelet 就会按照图中所示的顺序来调用 CRI 接口
需要注意的是,在 RunPodSandbox 这个接口的实现中,你还需要调用 networkPlugin.SetUpPod(…) 来为这个 Sandbox 设置网络。这个 SetUpPod(…) 方法,实际 上就在执行 CNI 插件里的 add(…) 方法,也就是利用 CNI 插件为 Pod 创建 网络,并且把 Infra 容器加入到网络中的操作。
具体的 CRI shim 中,这些接口的实现是可以完全不同的。
除了上述对容器生命周期的实现之外,CRI shim 还有一个重要的工作,就是如何实现 exec、 logs 等接口。这些接口跟前面的操作有一个很大的不同,就是这些 gRPC 接口调用期间, kubelet 需要跟容器项目维护一个长连接来传输数据。这种 API,我们就称之为 Streaming API,其实现依赖于一套独立的 Streaming Server 机制:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yaHgeJIp-1623207279282)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530130732306.png)]
总结
CRI 的具体接口时,往往拥有着很高的自由度,这个自由度不仅包括了容器的生命周期管理,也包括了如何将 Pod 映射成为我自己的实现,还包括了如何调用 CNI 插件来为 Pod 设置网络的过程。
当你对容器这一层有特殊的需求时,我一定优先建议你考虑实现一个自己的 CRI shim ,而不是修改 kubelet 甚至容器项目的代码
kata Container 和 gVisor
Kata Containers 的本质 就是一个精简后的轻量级虚拟机,所以它的特点,就是“像虚拟机一样安全,像容器一样敏捷”。
gVisor 项目给容器进程配置一 个用 Go 语言实现的、运行在用户态的、极小的“独立内核”。这个内核对容器进程暴露 Linux 内核 ABI,扮演着“Guest Kernel”的角色,从而达到了将容器和宿主机隔离开的目的。
这两种容器实现的本质,都是给进程分配了一个独立的操作系统内核,从而避免了让容器共享宿主机的内核。这样,容器进程能够看到的攻击面,就从整个宿主机内核变成了一个极小的、 独立的、以容器为单位的内核,从而有效解决了容器进程发生“逃逸”或者夺取整个宿主机的控 制权的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h4LuvMlM-1623207279283)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530230502765.png)]
两者区别
Kata Containers
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5LFhJ9L1-1623207279284)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530230655711.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1kxx44AT-1623207279285)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530231112419.png)]
gVisor
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7dza5Bf-1623207279286)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530231237695.png)]
gVisor 工作的核心,在于它为应用进程、也就是用户容器,启动了一个名叫 Sentry 的进程。
而 Sentry 进程的主要职责,就是提供一个传统的操作系统内核的能力,即:运行用户程序,执 行系统调用。所以说,Sentry 并不是使用 Go 语言重新实现了一个完整的 Linux 内核,而只是一个对应用进程“冒充”内核的系统组件。
网络上,Sentry 需要自己实现一个完整的 Linux 内核网络 栈,以便处理应用进程的通信请求。然后,把封装好的二层帧直接发送给 Kubernetes 设置的 Pod 的 Network Namespace 即可。
Volume 上,Sentry 需要通过 9p 协议交给一个叫做 Gofer 的代理进程来完 成。Gofer 会代替应用进程直接操作宿主机上的文件,并依靠 seccomp 机制将自己的能力限制 在最小集,从而防止恶意应用进程通过 Gofer 来从容器中“逃逸”出去
Sentry 进程两种不同的实现方式:
第一种实现方式,是使用 Ptrace 机制来拦截用户应用的系统调用(System Call),然后把这些系统调用交给 Sentry 来进行处理。这个过程,对于应用进程来说,是完全透明的。而 Sentry 接下来,则会扮演操作系统的角色, 在用户态执行用户程序,然后仅在需要的时候,才向宿主机发起 Sentry 自己所需要执行的系统 调用。这,就是 gVisor 对用户应用进程进行强隔离的主要手段。不过, Ptrace 进行系统调用拦截的性能实在是太差,仅能供 Demo 时使用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2aMf2LRC-1623207279287)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530231737964.png)]
第二种实现方式,使用 KVM 来进行系统调用的拦截,更加具有普适性。为了能够做到这一点,Sentry 进程就必须扮演一个 Guest Kernel 的角色,负责执行用户 程序,发起系统调用。而这些系统调用被 KVM 拦截下来,还是继续交给 Sentry 进行处理。只不过在这时候,Sentry 就切换成了一个普通的宿主机进程的角色,来向宿主机发起它所需要的 系统调用。
在这种实现里,Sentry 并不会真的像虚拟机那样去虚拟出硬件设备、安装 Guest 操 作系统。它只是借助 KVM 进行系统调用的拦截,以及处理地址空间切换等细节。
gVisor 的实现依然不是非常完善,有很多 Linux 系统调用它还不支持;有很多应用,在 gVisor 里还没办法运行起来。 此外,gVisor 也暂时没有实现一个 Pod 多个容器的支持
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mz0BvmZN-1623207279287)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530232015641.png)]
总结
kube-aggregator
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qGcOeweL-1623207279288)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210531095641347.png)]
Kubernetes 的 API Server 开启了 Aggregator 模式之后,你再访问 apis/metrics.k8s.io/v1beta1 的时候,实际上访问到的是一个叫作 kube-aggregator 的代 理。而 kube-apiserver,正是这个代理的一个后端;而 Metrics Server,则是另一个后端
Prometheus
Kubernetes 项目的整套监控体系.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EIxbmvKH-1623207279288)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210531095758286.png)]
具体的监控指标规划上,遵循业界通用的 USE 原则和 RED 原则。
USE 原则指的是,按照如下三个维度来规划资源监控指标:
而 RED 原则指的是,按照如下三个维度来规划服务监控指标:
每秒请求数量(Rate);
每秒错误数量(Errors);
服务响应时间(Duration)。
Kubernetes 里面对容器日志的处理方式,都叫作 cluster-level-logging, 即:这个日志处理系统,与容器、Pod 以及 Node 的生命周期都是完全无关的
Kubernetes 本身,实际上是不会为你做容器日志收集工作的,所以为了实现上述 clusterlevel- logging,你需要在部署集群的时候,提前对具体的日志方案进行规划
三种方案:
在 Node 上部署 logging agent,将日志文件转发到后端存储里保存起来。
通过一个 sidecar 容器把这些日志文件重新输出到sidecar 的 stdout 和 stderr 上, 然后使用第一种方案,克服当容器的日志只能输出到某些文件里的时候的缺点
通过一个 sidecar 容器,直接把应用的日志文件发送到远程存储里面去
在这种方案里,你的应用还可以直接把日志输出到固定的文件里而不是 stdout,你的 logging-agent 还可以使用 fluentd,后端存储还可以是 ElasticSearch。只不过, fluentd 的输入源, 变成了应用的日志文件。