深入剖析Kubernetes 学习笔记

Container & Kubernetes

【总结自张磊-深入剖析Kubernetes】
【图片看不到,可以去

文章目录

  • Container & Kubernetes
    • Container
        • 容器 = cgroup + namespace + rootfs + 容器引擎
        • Docker 原理
        • docker exec 是怎么做到进入容器里的呢?
        • docker volume挂载
        • 容器是“单进程模型”
    • Kubenertes
      • pod
        • 重要字段和含义
        • 为何以pod作为调度基本单位?
        • pod的意义--容器设计模式
        • projected Volume
        • 容器健康检查和恢复机制
      • 控制器模型
        • Deployment控制器
      • StatefulSet
      • DaemonSet
      • Job & CronJob
      • 声明式API容器编排
      • PV,PVC,StorageClass
        • PV与PVC绑定过程
        • 持久化宿主机目录
        • 存储插件
          • FlexVolume形式
          • CSI插件体系 容器存储接口
        • CSI插件部署(没有细看)
      • k8s的网络
        • 同一个主机上容器如何通信
        • 跨主机容器网络
          • Flannel UDP模式
          • Flannel VXLAN(也是UDP)
          • Flannel host-gw模式
        • CNI网络插件
        • Pod网络隔离实现
        • 容器服务发现
      • 资源管理和调度模型
        • 资源模型和管理
        • 调度器
          • 调度策略
          • 优先级(Priority )和抢占(Preemption)机制
          • Device Plugin
        • Kubelet
          • SIG-Node与CRI
          • CRI (容器运行时)
      • 安全容器
      • 监控体系
      • 日志收集和管理

Container

容器 = cgroup + namespace + rootfs + 容器引擎

  • Cgroup: 资源控制(限制namspace隔离进程的资源,但是Cgroups 对资源的限制能力也有很多不完善的地方, /proc 文件系统的问题,/proc 文件系统不了解 Cgroups 限制的存在)
  • namespace: 访问隔离(进程级别的隔离,看不见其他进程,但是资源都是共享的,因此存在资源被占用的情况,也存在把所有资源都吃掉的情况)
  • rootfs:文件系统隔离。镜像的本质就是一个rootfs文件
  • 容器引擎:生命周期控制

**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 核数、可用内存等信息都是宿主机上的数据,这会给应用的运行带来非常大的困惑和风险。这也是在企业中,容器化应用碰到的一个常见问题,也是容器相较于虚拟机另一个不尽如人意的地方

Docker 原理

最核心的原理实际上就是为待创建的用户进程:

  1. 启用 Linux Namespace 配置;
  2. 设置指定的 Cgroups 参数;
  3. 切换进程的根目录(Change Root)。

docker exec 是怎么做到进入容器里的呢?

Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。也就是通过查看宿主机的 proc 文件,看到这个 25686 进程的所有 Namespace 对应的文件

ls -l /proc/25686/ns

一个进程的每种 Linux Namespace,都在它对应的/proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上,这也就意味着:一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。而这个操作所依赖的,乃是一个名叫 setns() 的 Linux 系统调用

docker volume挂载

  • 允许你将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作

具体实现:直接挂载

  • 只需要在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即/var/lib/docker/aufs/mnt/[可读写层 ID]/test)上
  • 于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。
  • 使用到的挂载技术,就是 Linux 的绑定挂载(Bind Mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐
    藏起来且不受影响。(实验被挂载节点还能看到修改的东西,源挂载点应该就是这个名字,也就是/test里面东西不可见了,unmount后恢复)
  • 容器 Volume 里的信息,并不会被 docker commit 提交掉;但这个挂载点目录/test 本身,则会出现在新的镜像当中。

容器是“单进程模型”

容器的“单进程模型”,并不是指容器里只能运行“一个”进程,而是指容器没有管理多个进程的能力,这是因为容器里 PID=1 的进程就是应用本身,其他的进程都是这个PID=1 进程的子进程。所以一般一个容器里面只有一个进程。

Kubenertes

pod

Pod,是 Kubernetes 项目中最小的 API 对象, 是 Kubernetes 项目的原子调度单位。

重要字段和含义

  • NodeSelector:供用户将Pod和Node进行绑定的字段
  • NodeName:赋值意味该Pod已被调度

为何以pod作为调度基本单位?

因为存在容器成组问题,这种问题在调度上比较难以来做(成组调度问题),资源囤积带来了不可避免的调度效率损失和死锁的可能性;而乐观调度的复杂程度,则不是常规技术团队所能驾驭的。而pod的引入,就是将容器打包在一起,可以不管这种成组调度问题了。

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 来说

    • 它们可以直接使用 localhost 进行通信;(容器本地通信就好了,不需要配置内网ip)
    • 它们看到的网络设备跟 Infra 容器看到的完全一样;(方便构建网络插件
    • 一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址;
    • 当然,其他的所有网络资源,都是一个 Pod 一份,并且被该 Pod 中的所有容器共享;(比如volume)
    • Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bg67dpWT-1623207279254)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210514000023852.png)]

projected Volume

v1.11之后特性,存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换,是为容器提供预先定义好的数据,有四种:

  • Secret,
    • 把 Pod 想要访问的加密数据,存放到 Etcd 中。然后,你就可以通过在 Pod 的容器里挂载 Volume 的方式,访问到这些 Secret里保存的信息了
    • Secret 对象要求这些数据必须是经过 Base64 转码的
  • ConfigMap
    • 保存的是不需要加密的、应用所需的配置信息
  • Downward API
    • 声明了要暴露 Pod 的 一些 信息给容器,让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。
  • ServiceAccountToken
    • Service Account 对象的作用,就是 Kubernetes 系统内置的一种“服务账户”,它是 Kubernetes 进行权限分配的对象。比如,Service Account A,可以只被允许对 Kubernetes API 进行 GET 操作,而 Service Account B,则可以有 Kubernetes API 的所有操作的权限。
    • 这样的 Service Account 的授权信息和文件,实际上保存在它所绑定的一个特殊的 Secret 对象里的。这个特殊的 Secret 对象,就叫作ServiceAccountToken。任何运行在 Kubernetes 集群上的应用,都必须使用这个 ServiceAccountToken 里保存的授权信息,也就是 Token,才可以合法地访问 API Server。

容器健康检查和恢复机制

  • livenessProbe
    • 除了在容器中执行命令外,livenessProbe 也可以定义为发起 HTTP 或者 TCP 请求的方式
  • restartPolicy:pod.spec.restartPolicy,默认值是 Always
    • Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。
    • 事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点(被调度了),这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。而如果你想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理Pod,哪怕你只需要一个 Pod 副本
      • Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
      • OnFailure: 只在容器 异常时才自动重启容器;
      • Never: 从来不重启容器。
    • 只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod就会保持 Running 状态,并进行容器重启。否则,Pod 就会进入 Failed 状态。
    • 对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态

控制器模型

  • 状态信息来源
    • kubelet 通过心跳汇报的容器状态和节点状态
    • 监控系统中保存的应用监控数据
    • 控制器主动收集的它自己感兴趣的信息,这些都是常见的实际状态的来源。
  • 控制方法:循环控制control loop

Deployment控制器

  • 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则再也不能回滚操作。

StatefulSet

为了解决Deployment认为一个应用的pod都是完全一样的, 之间没有顺序,也没有所谓运行在哪一个主机上的问题。这个问题让Deployment随意创建或者删掉一个pod。

但是,实际上很多应用,特别时分布式应用,他们之间多个实例往往存在依赖关系:主从关系和主备关系等。还有一些数据存储类应用,被杀掉后丢失实例与数据之间的对应关系。这些实例间存在不对等关系以及实例对外部数据有依赖关系的应用,被称为有状态应用

由此,StatefulSet就是为了解决这个问题。

StatefulSef设计

  • 拓扑状态。应用的多个实例之间不是完全对等的关系,应用实例,必须按照某些顺序启动,并且新建的pod必需和原来的pod网络标识一样。
  • 存储状态。应用的多个实例分别绑定了不同的存储数据,应用实例,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。

通过这样的状态设计,SatefulSet将真实世界里的应用状态,抽象出来,并通过某种方式来记录这些状态。

Headless Service

Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。用户通过访问Service就可以访问到具体的Pod。

访问Service方式有:

  1. Service的VIP形式(Virtual IP)
  2. Service的DNS方式:访问“my-svc.mynamespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod
    • Normal Service:解析DNS得到对应的VIP,然后访问VIP就好了
    • Headless Service:解析DNS得到my-svc 代理的某一个 Pod 的 IP 地址,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。

创建Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定为以下格式的 DNS 记录:

...svc.cluster.local
# 改记录k8s为 Pod 分配的唯一的“可解析身份”

**Persistent Volume Claim PVC **

解决过度暴露问题,大大降低用户声明和使用持久化Volume的门槛

  1. 定义一个PVC,声明想要的Volume的属性;不需要任何关于 Volume 细节的字段,只有描述性的属性和定义。
  2. 在应用的 Pod 中,声明使用这个 PVC;只需要声明它的类型是persistentVolumeClaim,然后指定 PVC 的名字,而完全不必关心 Volume 本身的定义
  3. 创建这个 PVC 对象,Kubernetes 就会自动为它绑定一个符合条件的Volume(自于由运维人员维护的 PV(Persistent Volume)对象)。

StatefulSet 拓扑状态维护

StatefulSet 就是通过Headless Service维护网络拓扑结构,编号方式维护pod的顺序

  • 首先StatefulSet 按照 Pod 的“名字 + 编号”的方式将将 Pod 的拓扑状态固定下来。
  • 然后为每一个Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录 Headless Service
  • 这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者重新创建而失效。
  • 但是,通过Headless Service解析到的 Pod 的IP 地址,并不是固定的。因此必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址

StatefulSet 存储状态维护

StatefulSet 通过PVC、PV 来管理存储状态

  • 在StatefulSet中声明一个PVC(就来自于volumeClaimTemplates 这个模板字段),这个 PVC 的名字,会被分配一个与这个Pod 完全一致的编号。
  • 自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。PVC 与 PV 的绑定得以实现的前提是,运维人员已经在系统里创建好了符合条件的PV(比如,我们在前面用到的 pv-volume);或者,你的 Kubernetes 集群运行在公有云上,这样 Kubernetes 就会通过 Dynamic Provisioning 的方式,自动为你创建与 PVC 匹配的PV
  • PVC,都以--< 编号 >的方式命名,并且处于 Bound 状态。

也就是说,StatefulSet通过维护一个有状态的PVC,进而找到跟这个 PVC 绑定在一起的PV。这个PVC名字是不会变的重新创建一个pod时。

总结

  1. StatefulSet 的控制器直接管理的是 Pod。StatefulSet 里的不同 Pod 实例,有着不同的编号,用于维护拓扑以及唯一性
  2. Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有同样编号的 DNS 记录。StatefulSet 能够保证这些 Pod 名字里的编号不变,对应的DNS记录也不会变,也就确保网络拓扑的一致性(解析到的 Pod 的IP 地址,并不是固定的,但是对于用户来说这是透明的)
  3. StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC。这样,Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有一个独立的 Volume。PVC和PV在pod被删除后依旧被保留下来的。

DaemonSet

在 Kubernetes 集群里,运行一个 Daemon Pod,这个pod特点有:

  • 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
  • 每一个节点上只有一个这样的pod实例;
  • 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉;
  • 跟其他编排对象不一样,DaemonSet 开始运行的时机,很多时候比整个
    Kubernetes 集群出现的时机都要早
  • DaemonSet 自动地给被管理的 Pod 加上一个特殊的Toleration,即能容忍那些不被调度的节点

意义

  • 各种网络插件的 Agent 组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络;
  • 各种存储插件的 Agent 组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的 Volume 目录
  • 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集。

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 & CronJob

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一直不肯结束。

  • 并行控制

    • spec.parallelism,它定义的是一个 Job 在任意时间最多可以启动多少个 Pod 同时运行
    • spec.completions,它定义的是 Job 至少要完成的 Pod 数目,即 Job 的最小完成数
  • 常用的、使用 Job 对象的方法

    1. 外部管理器 +Job 模板。
    2. 拥有固定任务数目的并行 Job。指定需要完成的任务数量
    3. 指定并行度(parallelism),但不设置固定的completions 的值。pod内部判断是否完成对应任务。

CronJob,定时任务

一个专门管理Job对象的控制器

  • 创建和删除 Job 的依据,是 schedule 字段定义的、一个标准的Unix Cron格式的表达式。

    "*/1 * * * *"
    Cron 表达式里 */1 中的 * 表示从 0 开始,/ 表示“每”,1 表示偏移量。所以,它的意思就是:从 0 开始,每 1 个时间单位执行一次
    表达式中的五个部分分别代表:分钟、小时、日、月、星期。
    
  • 处理一个job未完成,新的job就被创建的情况

    1. concurrencyPolicy=Allow,这也是默认情况,这意味着这些 Job 可以同时存在;
    2. concurrencyPolicy=Forbid,这意味着不会创建新的 Pod,该创建周期被跳过;
    3. concurrencyPolicy=Replace,这意味着新产生的 Job 会替换旧的、没有执行完的 Job。
  • spec.startingDeadlineSeconds 字段指定该时间段内Job 创建失败miss数达100后,job就不会被创建执行。

声明式API容器编排

apply replace create区别

  • kubectl replace 的执行过程,是使用新的 YAML 文件中的 API对象,替换原有的 API 对象。kubectl set image 和 kubectl edit 也是对已有 API 对象的修改。
  • kubectl apply,则是执行了一个对原有 API 对象的 PATCH 操作。所以yaml文件的内容可以只写需要升级的属性
  • kubectl create,是先删除所有现有的东西,重新根据yaml文件生成新的。所以要求yaml文件中的配置必须是完整的

更进一步地,这意味着 kube-apiserver 在响应命令式请求(比如,kubectl replace)的时候,一次只能处理一个写请求,否则会有产生冲突的可能。而对于声明式请求(比如,kubectl apply),一次能处理多个写操作,并且具备 Merge 能力。

什么是声明式

  1. 首先,所谓“声明式”,指的就是我只需要提交一个定义好的 API 对象来“声明”,我所期望的状态是什么样子。
  2. 其次,“声明式 API”允许有多个 API 写端,以 PATCH 的方式对 API 对象进行修改,而无需关心本地原始 YAML 文件的内容。
  3. 最后,也是最重要的,有了上述两个能力,Kubernetes 项目才可以基于对 API 对象的增、删、改、查,在完全无需外界干预的情况下,完成对“实际状态”和“期望状态”的调谐(Reconcile)过程。

声明式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)]

  • 对于 Kubernetes 里的核心 API 对象,比如:Pod、Node 等,是不需要Group 的(即:它们 Group 是“”)
  • API对象会一层层找对应的对象以及对应的版本号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)]

  • API server 事件(APIServer 端有新的 Network 实例被创建、删除或者更新)触发Reflector,事件和对应的API对象(称为增量Delta),会被放置在Delta FIFO Queue里面;
  • Informer 会不断从这个Queue中读取增量Delta,并判断事件类型,同步本地缓存(没有就创建,有就更新)
  • 同时,Informer会根据这些事件的类型,触发事先注册好的ResourceEventHandler(Handler,需要在创建控制器的时候注册给它对应的Informer)
    • Informer,其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 client。它是自定义控制器跟 APIServer 进行数据同步的重
      要组件。
    • Informer 通过一种叫作 ListAndWatch 的方法,把 APIServer 中的 API 对象缓存在了本地,并负责更新和维护这个缓存。
    • 此外,在这个过程中,每经过 resyncPeriod 指定的时间,Informer 维护的本地缓存,都会使用最近一次 LIST 返回的结果强制更新一次,从而保证缓存的有效性,这个定时 resync 操作,也会触发 Informer 注册的“更新”事件。

PV,PVC,StorageClass

  • PVC 描述的,是 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等
  • PV 描述的,则是一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储服务器地址等。
  • StorageClass 的作用,则是充当 PV 的模板。并且,只有同属于一个 StorageClass 的PV 和 PVC,才可以绑定在一起。此外,StorageClass可以指定PV 的 Provisioner(存储插件)

PV与PVC绑定过程

条件检查

  • PV 和 PVC 的 spec 字段。比如,PV 的存储(storage)大小,就必须满足 PVC 的要求。
  • PV 和 PVC 的 storageClassName 字段必须一样

PersistentVolumeController 控制绑定PV和PVC

  • PersistentVolumeController 属于 Volume Controller维护的多个控制循环中的一个
  • PersistentVolumeController 会不断地查看当前每一个 PVC,是不是已经处于 Bound(已绑定)状态。如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与这个的 PVC进行绑定(将这个PV对象名字,填在PVC中的spec。volumeName)

持久化宿主机目录

  1. Attach。为宿主机挂载远程磁盘。Kubernetes 提供的可用参数是 nodeName,即宿主机的名字。也就是为主机挂载一个磁盘
    • 操是由 Volume Controller 负责维护的,控制循环的名字叫作:AttachDetachController。
    • 不断地检查每一个Pod 对应的 PV,和这个 Pod 所在宿主机之间挂载情况。从而决定,是否需要对这个 PV 进行Attach(或者 Dettach)操作。
    • AttachDetachController一定运行在master节点上
  2. Mount。磁盘设备格式化并挂载到 Volume 宿主机目录的操作。Kubernetes 提供的可用参数是 dir,即 Volume 的宿主机目录。挂载在这个主机的磁盘可以被mount在多个pod上
    • VolumeManagerReconciler,必须发生在 Pod 对应的宿主机上,所以它必须是 kubelet 组件的一部分。
    • 它运行起来之后,是一个独立于 kubelet 主循环的 Goroutine。避免对kubelet主控制循环被block

存储插件

就是为了方便用户拓展存储体系

FlexVolume形式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-plbR2eqV-1623207279263)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210525160502182.png)]

  1. 在上述体系下,无论是 FlexVolume,还是 Kubernetes 内置的其他存储插件,它们实际上担任的角色,仅仅是 Volume 管理中的“Attach 阶段”和“Mount 阶段”的体执行者。

  2. 像 Dynamic Provisioning 这样的功能,就不是存储插件的责任,而是 Kubernetes 本身存储管理功能的一部分。

CSI插件体系 容器存储接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LJXs26Uy-1623207279264)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210525161049610.png)]

  1. CSI 插件体系的设计思想,就是把这个 Provision 阶段,以及 Kubernetes 里的一部分存储管理功能,从主干代码里剥离出来,做成了几个单独的组件。
  2. 这些组件会通过 WatchAPI 监听 Kubernetes 里与存储相关的事件变化,比如 PVC 的创建,来执行具体的存储管理动作。

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 阶段”。

总得来说

  • 相比于 FlexVolume,CSI 的设计思想,把插件的职责从“两阶段处理”,扩展成了Provision、Attach 和 Mount 三个阶段。其中,Provision 等价于“创建磁盘”,Attach 等价于“挂载磁盘到虚拟机”,Mount 等价于“将该磁盘格式化后,挂载在 Volume 的宿主机目录上”。CSI将k8s的一些功能独立出来,然后进行自己的一个开发和拓展
  • 在CSI插件作用下:
    • 当 AttachDetachController 需要进行“Attach”操作时(“Attach 阶段”),它实际上会 执行到 pkg/volume/csi 目录中,创建一个 VolumeAttachment 对象,从而触发 External Attacher 调用 CSI Controller 服务的 ControllerPublishVolume 方法。
    • 当 VolumeManagerReconciler 需要进行“Mount”操作时(“Mount 阶段”),它实际 上也会执行到 pkg/volume/csi 目录中,直接向 CSI Node 服务发起调用 NodePublishVolume 方法的请求。

CSI插件部署(没有细看)

第一,通过 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 插件的正确性来 说,至关重要。

k8s的网络

一个 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)]

这个过程实际上就是:

  1. 每创建一个docker容器,在该容器中有自己的一个eth0的网卡,这个网卡属于Veth Pair设备的一端,另外一段则是体现在主机上,为一个虚拟机网卡,而这个虚拟网卡则是插在docker0网桥上。
  2. 一旦一张虚拟网卡被“插”在网桥上,它就会变成该网桥的“从设备”。从设备会被“剥夺”调 用网络协议栈处理数据包的资格,从而“降级”成为网桥上的一个端口。而这个端口唯一的作 用,就是接收流入的数据包,然后把这些数据包的“生杀大权”(比如转发或者丢弃),全部交给对应的网桥。
  3. 因此,在收到这些 ARP 请求之后,docker0 网桥就会扮演二层交换机的角色,把 ARP 广播转发到其他被“插”在 docker0 上的虚拟网卡上,然后获取对应的MAC地址进行数据转发。
  4. 需要注意的是,在实际的数据传递时,上述数据的传递过程在网络协议栈的不同层次,都有 Linux 内核 Netfilter 参与其中

类似地,当你在一台宿主机上,访问该宿主机上的容器的 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 UDP模式
  • 消息通过Flannel子网信息(保存在Etcd中),确定需要转发的宿主机,然后Flannel将消息封装为UDP发送过去,目的主机通过flannel发送到docker0。

  • Flannel UDP 模式提供的其实是一个三层的 Overlay 网络,即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。

  • 性能限制:需要经过三次用户态与内核态之间的数据拷贝。

    • 第一次:用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态;
    • 第二次:IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;
    • 第三次:flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。

    此外,Flannel 还要进行 UDP 封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的。

PS:进行系统级编程的时候,有一个非常重要的优化原则,就是要减少用户态到内核态 的切换次数,并且把核心的处理逻辑都放在内核态进行

Flannel VXLAN(也是UDP)

VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。所以说,VXLAN 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面 相似的“隧道”机制,构建出覆盖网络(Overlay Network)

  • 设计思想

    • 在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可 以)之间,可以像在同一个局域网(LAN)里那样自由通信。
    • 为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧 道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。VTEP 设备的作用,其实跟前面的 flanneld 进程非常相似。只不过,它进行封装和解封装的对 象,是二层数据帧(Ethernet frame);而且这个工作的执行流程,全部是在内核里完成的(因为VXLAN 本身就是 Linux 内核中的一个模块)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LimLzlEn-1623207279267)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210526205227682.png)]

  • 大致过程:

    • “源 VTEP 设备”收到“原始 IP 包”后,就要想办法把“原始 IP 包”加上 一个目的 MAC 地址,封装成一个二层数据帧,然后发送给“目的 VTEP 设备”
    • MAC地址ARP寻址,fanneld进程会在新节点启动时,把自己的MAC添加到旧节点ARP记录表中。(启动是自动添加,而不需要ARP广播
    • 添加MAC地址,封装成内部数据帧(因为不能在宿主机二层网络中传输)
    • Linux 内核还需要再把“内部数据帧”进一步封装成为宿主机网络里的一个普通的数据帧(“外部数据帧”),好让它“载着”“内部数据帧”,通过宿主机的 eth0 网卡进行传输,同时Linux 内核会在“内部数据帧”前面,加上一个特殊的 VXLAN 头(有一个重要的标志叫作VNI,flannel中默认值为1),用来表示这个“乘客”实际上是一个 VXLAN 要使用的数据帧
    • 然后,Linux 内核会把这个数据帧封装进一个 UDP 包里。(还是UDP想不到吧)
    • 发送UDP需要知道目标主机IP地址,这里是将flannel.1看作为一个网桥,利用flannel进程维护的FDB信息来确定其目标主机IP,封装成IP数据报(加上了Outer IP Header)
    • 最后,源主机对这个IP数据报加上MAC地址封装成真正的帧,发出去了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 转发(路由表)进行协作。

Flannel host-gw模式

维护一个路由表,确定某一个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 给它 们使用。

  • Flannel host-gw 模式必须要求集群宿主机之间是二层连通的。需要注意的是,宿主机之间二层不连通的情况也是广泛存在的。比如,宿主机分布在了不同的子 网(VLAN)里。但是,在一个 Kubernetes 集群里,宿主机之间必须可以通过 IP 地址进行通 信,也就是说至少是三层可达的。也就是,必需保证能相互访问通过IP。

Calico 项目

通过BGP协议维护路由之间的规则关系

  • 组成

    1. Calico 的 CNI 插件。这是 Calico 与 Kubernetes 对接的部分。如下所示([CNI网络插件](#### CNI网络插件))
    2. Felix。它是一个 DaemonSet,负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库),以及维护 Calico 所需的网络设备等工作。
    3. BIRD。它就是 BGP 的客户端,专门负责在集群里分发路由规则信息
  • 工作原理

    • Calico 项目与 Flannel 的 host-gw 模式的另一个不同之处,就是它不会在宿主机上创建任何网桥设备
    • 其通过route表将Veth Pair关系记录下来(在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则,用于接收传入的 IP 包。)
  • Calico 项目实际上将集群里的所有节点,都当作是边界路由器来处理,它们一起组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则。

  • Calico 维护的网络在默认配置下,是一个被称为**“Node-to-Node Mesh”的 模式。这时候,每台宿主机上的 BGP Client 都需要跟其他所有节点的 BGP Client 进行通信以 便交换路由信息。只适用于少于100个节点规模**

  • 更大规模的集群中,使用 Route Reflector 的模式。Calico 会指定一个或者几个专门的节点,来负责跟所有节点建立 BGP 连接从而 学习到全局的路由规则。而其他节点,只需要跟这几个专门的节点交换路由信息,就可以获得整 个集群的路由规则信息了。

  • IPIP 模式突破不同子网问题(保证三层连通性

    • 通过IP 隧道(IP tunnel)设备转发消息
    • IP 包进入 IP 隧道设备之后,就会被 Linux 内核的 IPIP 驱动接管。IPIP 驱动 会将这个 IP 包直接封装在一个宿主机网络的 IP 包中
    • 经过封装后的新的 IP 包的目的地址(图 5 中的 Outer IP Header 部分),正是原 IP 包 的下一跳地址,即 Node 2 的 IP 地址:192.168.2.2。而原 IP 包本身,则会被直接封装成新 IP 包的 Payload
    • 由于宿主机之间已经使用路由器配置了三层转发,也就是设置了宿主机之间的“下一跳”。所以 这个 IP 包在离开 Node 1 之后,就可以经过路由器,最终“跳”到 Node 2 上。 这时,Node 2 的网络内核栈会使用 IPIP 驱动进行解包,从而拿到原始的 IP 包。然后,原始 IP 包就会经过路由规则和 Veth Pair 设备到达目的容器内部。(会增加开销

CNI网络插件

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插件

  1. Kubernetes 项目并没有使用 Docker 的网络模型(CNM),所以它并不希望、也 不具备配置 docker0 网桥的能力;
  2. 这还与 Kubernetes 如何配置 Pod,也就是 Infra 容器的 Network Namespace 密切相关。
    • Kubernetes 创建一个 Pod 的第一步,就是创建并启动一个 Infra 容器,用 来“hold”住这个 Pod 的 Network Namespace
    • CNI 的设计思想,就是:Kubernetes 在启动 Infra 容器之后,就可以直接调用 CNI 网络插件,为这个 Infra 容器的 Network Namespace,配置符合预期的网络栈。

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网络)

    1. CNI bridge 插件会在宿主机上检查 CNI 网桥是否存在。如果没有的话,那就创建它。
    2. CNI bridge 插件会通过 Infra 容器的 Network Namespace 文件,进入到这个Network Namespace 里面,然后创建一对 Veth Pair 设备,然后它会把这个 Veth Pair 的其中一端,**“移动”**到宿主机上。(先容器后主机是因为编程方便而已,容器的 Namespace 是可以直接通过 Namespace 文件 拿到的;而 Host Namespace,则是一个隐含在上下文的参数)
    3. 将 宿主机 Veth Pair 设备连接在 CNI 网桥之后,CNI bridge 插件还会为它设置HairpinMode(发夹模式)。解除网桥设备是不允许一个数据包从一个端口进来后,再从这个端口发出去的限制。(应对场景:容器需要通过NAT(即:端口映射)的方式,“自己访问自己”的场景)
    4. CNI bridge 插件会调用 CNI ipam 插件,从 ipam.subnet 字段规定的网段里为容器分配一个可用的 IP 地址。然后,CNI bridge 插件就会把这个 IP 地址添加在容器的 eth0 网卡上,同时为容器设置默认路由
    5. CNI bridge 插件会为 CNI 网桥添加 IP 地址
    6. 执行完上述操作之后,CNI 插件会把容器的 IP 地址等信息返回给 dockershim,然后被kubelet 添加到 Pod 的 Status 字段。最终完成通信

PS

  • 处理容器网络相关的逻辑并不会在 kubelet 主干代码里执 行,而是会在具体的 CRI(Container Runtime Interface,容器运行时接口)实现里完成

  • Kubernetes 目前不支持多个 CNI 插件混用。如果你在 CNI 配置目录 (/etc/cni/net.d)里放置了多个 CNI 配置文件的话,dockershim 只会加载按字母顺序排序的 第一个插件。

  • CNI 允许你在一个 CNI 配置文件里,通过 plugins 字段,定义多个插件进行协作。

总结

  1. 所有容器都可以直接使用 IP 地址与其他容器通信,而无需使用 NAT。
  2. 所有宿主机都可以直接使用 IP 地址与所有容器通信,而无需使用 NAT。反之亦然。
  3. 容器自己“看到”的自己的 IP 地址,和别人(宿主机或者容器)看到的地址是完全一样的。

Pod网络隔离实现

  • 通过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
      
      • 首先在第一条规则里,我们会把 IP 包转交给前面定义的 KUBE-NWPLCY-CHAIN 规 则去进行匹配。按照我们之前的讲述,如果匹配成功,那么 IP 包就会被“允许通过”
      • 如果匹配失败,IP 包就会来到第二条规则上。可以看到,它是一条 REJECT 规则。通过这条 规则,不满足 NetworkPolicy 定义的请求就会被拒绝掉,从而实现了对该容器的“隔离”。

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)里。

  • prerouting:判断IP数据包下一步流向,留在本机处理还是转发到其他目的地
  • 第一种情况,IP 包将继续向上层协议栈流动。在它进入传输层之 前,Netfilter 会设置一个名叫 INPUT 的“检查点”。到这里,IP 包流入路径(Input Path) 结束。这个 IP 包通过传输层进入用户空间,交给用户进程处理。而处理完成后,用户进程会通过本机发出返回的 IP 包。这时候,这个 IP 包就进入了流出路径(Output Path)
  • IP 包首先还是会经过主机的路由表进行路由。路由结束后,Netfilter 就会设置一个名叫 OUTPUT 的“检查点”。然后,在 OUTPUT 之后,再设置一个名叫 POSTROUTING“检查 点”,POSTROUTING 的作用,其实就是上述两条路径,最终汇聚在一起的“最终检查点”。
  • 第二种情况,,这个 IP 包不会进入传输层,而是会继续在网络层流动,从而进入到转发路径 (Forward Path)。在转发路径中,Netfilter 会设置一个名叫 FORWARD 的“检查点”。FORWARD“检查点”完成后,IP 包就会来到流出路径。而转发的 IP 包由于目的地已经 确定,它就不会再经过路由,也自然不会经过 OUTPUT,而是会直接来到 POSTROUTING“检 查点

iptables 表的作用,就是在某个具体的“检查点”(比如 Output)上,按顺序执行几个不同的检查动作(比如,先执行 nat,再执行 filter)

容器服务发现

Service管理Pod IP以及负载均衡。实际上,Service 是由 kube-proxy 组件,加上 iptables 来共同实现的

  • 对于我们前面创建的名叫 hostnames 的 Service 来说,一旦它被提交给 Kubernetes,那么 kube-proxy 就可以通过 Service 的 Informer 感知到这样一个 Service 对 象的添加。而作为对这个事件的响应,它就会在宿主机上创建这样一条 iptables 规则,将外部访问消息转发到这个规则上。
  • 而这个规则则是由一组规则集合组成(一组随机模式(–mode random)的 iptables 链(DNAT 规则),对应你这组类型pod),规则被选中的概率为 1 n , 1 n − 1 , . . . , 1 \frac{1}{n}, \frac{1}{n-1},...,1 n1,n11,...,1

IPVS模式

克服宿主机存在大量pod时iptables 过多且需要不断刷新的问题。(极其浪费资源

  • IPVS 模式的工作原理,其实跟 iptables 模式类似。当我们创建了前面的 Service 之后,kubeproxy 首先会在宿主机上创建一个虚拟网卡(叫作:kube-ipvs0),并为它分配 Service VIP 作为 IP 地址
  • kube-proxy 就会通过 Linux 的 IPVS 模块,为这个 IP 地址设置三个 IPVS 虚拟主机,并设置这三个虚拟主机之间使用轮询模式 (rr) 来作为负载均衡策略
  • 相比于 iptables,IPVS 在内核中的实现其实也是基于 Netfilter 的 NAT 模式,所以在转发这 一层上,理论上 IPVS 并没有显著的性能提升。但是,IPVS 并不需要在宿主机上为每个 Pod 设 置 iptables 规则,而是把对这些“规则”的处理放到了内核态,从而极大地降低了维护这些规 则的代价

ClusterIP 模式的 Service 为你提供的,就是一个 Pod 的稳定的 IP 地址,即 VIP。并且,这里 Pod 和 Service 的关系是可以通过 Label 确定的

Headless Service 为你提供的,则是一个 Pod 的稳定的 DNS 名字,并且,这个名字是可以 通过 Pod 名字和 Service 名字拼接出来的

资源管理和调度模型

资源模型和管理

资源模型

Pod资源配置:

  • 容器的cpu和mem
    • cpu可压缩资源,只会饥饿不会杀死pod;mem不可压缩资源,会因为OOM(out-of-memory)被杀死
    • cpu单位是cpu的个数,可以是实际物理核,也可以是vcpu,也可以是一个超线程
  • 容器的limits和requests
    • kube-schedueler会按照request进行调度,但是真正设置cGroups限制则是按照limit进行设置(kubelet负责)

QoS模型:

  • 类型

    • Guaranteed:同时设置了limits和requests,并且他们的值都相同
    • Burstable:Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests
    • BestEffort: Pod 既没有设置 requests,也没有设置 limits
  • QoS 划分的主要应用场景,是当宿主机资源紧张的时候,kubelet 对 Pod 进行Eviction(即资源回收)时需要用到的

    • Eviction分为hard 和 soft两种,hard就是达到阈值立马执行,soft则是达到阈值一定时间后执行(AC: 这里是否可以设置多级阈值,常见思想)
    • Kubernetes 计算 Eviction 阈值的数据来源,主要依赖于从 Cgroups 读取到的值,以及使用 cAdvisor 监控到的数据。
    • 冲突Pod删除执行顺序:BestEffort、发生“饥饿”的资源使用量已经超出了 requests 的Burstable类型Pod,Guaranteed(当且仅当 Guaranteed 类别的 Pod的资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure 状态)

cpu独占设置:

  • cpuset,可以讲容器绑定在某一个cpu核上,而不必和其他容器共享cpu,减少上下文切换(AC:一个优化点吧,如何绑定会比较好?而且这个也是可以跟numa有很大的关系,因为非一致性时延问题)

调度器

默认调度器

默认调度器的主要职责,就是为一个新创建出来的 Pod,寻找一个最合适的节点(Node)。

  1. 从集群所有的节点中,根据调度算法挑选出所有可以运行该 Pod 的节点
  2. 从第一步的结果中,再根据调度算法挑选一个最符合条件的节点作为最终结果。

调度流程:

  1. 调用predicate算法,检查每一个Node
  2. 调用Priority算法,对第一步得到的结果中的Node进行打分
  3. 选取分数最高的那个作为放置Node(也会将该Pod的spec.nodeName 字段填上调度结果的节点名字)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SVGZ3Oef-1623207279271)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210529105344404.png)]

更具体地:

  1. informer path 循环:启动一系列的informer,监听Etcd中Pod、Node、Service等与调度相关的API对象的变化

    • 需要调度的对象会被放进一个调度队列(具有一定优先级设计的队列,比如说,FIFO或者其他自定义优先级队列)(AC:队列的优化会可以提高一定的响应性,也就是对于响应性的高的需要优先调度完成,具体业务具体分析)

    • 调度器缓存更新,Kubernetes 调度部分进行性能优化的一个最根本原则,就是尽最大可能将集群信息Cache 化,以便从根本上提高 Predicate 和 Priority 调度算法的执行效率。(AC:状态维护,避免每一次重新对整个集群进行一个查询,浪费时间)

  2. Scheduling Path主循环:负责调度Pod的主循环

    • 不断地从调度队列里出队一个 Pod。然后,调用Predicates 算法进行“过滤”。这一步“过滤”得到的一组 Node,就是所有可以运行这个Pod 的宿主机列表。当然,Predicates 算法需要的 Node 信息,都是从 Scheduler Cache 里直接拿到的(保证算法执行效率的主要手段之一)
    • 调用 Priorities 算法为上述列表里的 Node 打分,分数从 0 到 10。得分最高的 Node,就会作为这次调度的结果。
    • Bind & Assume:调度算法执行完成后,调度器就需要将 Pod 对象的 nodeName 字段的值,修改为上述 Node的名字。这个步骤在 Kubernetes 里面被称作 Bind。为了不在关键调度路径里远程访问 APIServer,Kubernetes 的默认调度器在 Bind 阶段,只会更新 Scheduler Cache 里的 Pod 和 Node 的信息。(Assume 乐观假设的API对象更新方式)
    • 异步调度:Assume 之后,调度器才会创建一个 Goroutine 来异步地向 APIServer 发起更新 Pod 的请求,来真正完成 Bind 操作。如果这次异步的 Bind 过程失败了,其实也没有太大关系,等Scheduler Cache 同步之后一切就会恢复正常。
    • Admit 操作:调度完成后,还需要对pod进行运行检查。节点上的 kubelet 还会通过一个叫作 Admit 的操作来再次验证该 Pod 是否确实能够运行在该节点上(“资源是否可用”“端口是否冲突”等再执行一遍(GeneralPredicates调度算法),作为 kubelet 端的二次确认。)

性能提升三设计

  • Node状态信息Cache化,Kubernetes 调度器性能得以提升的一个关键演化
  • 乐观绑定 Assume bind
    • 在 Bind 阶段,**只会更新 Scheduler Cache 里的 Pod 和 Node 的信息。**之后在创建goroutine进行调度
  • 无锁化
    • Predicates阶段:调度器会启动多个 Goroutine 以节点为粒度并发执行 Predicates 算法,从而提高这一阶段的执行效率。Priorities 算法也会以 MapReduce 的方式并行计算然后再进行汇总
    • 在这些所有需要并发的路径上,调度器会避免设置任何全局的竞争资源,从而免去了使用锁进行同步带来的巨大的性能损耗。
    • Kubernetes 调度器只有对调度队列和 Scheduler Cache 进行操作时,才需要加锁。而这两部分操作,都不在 Scheduling Path 的算法执行路径上。

调度器的拓展

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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, 一组过滤规则,负责的是最基础的调度策略

    • PodFitsHost, 检查宿主机的名字是否跟 Pod 的 spec.nodeName 一致。
    • PodFitsHostPorts,检查 Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被使用的端口有冲突。
    • PodMatchNodeSelector,检查,Pod 的 nodeSelector 或者 nodeAffinity 指定的节点,是否与待考察节点匹配
  • Volume 相关的过滤规则,负责的是跟容器持久化 Volume 相关的调度策略

    • NoDiskConflict 检查的条件,是多个 Pod 声明挂载的持久化 Volume 是否有冲突

    • MaxPDVolumeCountPredicate 检查的条件,则是一个节点上某种类型的持久化 Volume是不是已经超过了一定数目,如果是的话,那么声明使用该类型持久化 Volume 的 Pod 就不能再调度到这个节点了。

    • VolumeZonePredicate,则是检查持久化 Volume 的 Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配。

    • VolumeBindingPredicate 的规则。它负责检查的,是该 Pod 对应的PV 的 nodeAffinity 字段,是否跟某个节点的标签相匹配

  • 宿主机相关的过滤规则,主要考察待调度 Pod 是否满足 Node 本身的某些条件。

    • PodToleratesNodeTaints,负责检查的就是我们前面经常用到的 Node 的“污点”机制。
    • NodeMemoryPressurePredicate,检查的是当前节点的内存是不是已经不够充足,如果是的话,那么待调度 Pod 就不能被调度到该节点上。
  • Pod 相关的过滤规则,跟 GeneralPredicates 大多数是重合的

    • 比较特殊的,是PodAffinityPredicate。这个规则的作用,是检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系。指定作用域(topologyKey
    • requiredDuringSchedulingIgnoredDuringExecution:这条规则必须在 Pod 调度时进行检查(requiredDuringScheduling);但是如果是已经在运行的 Pod 发生变化,比如 Label 被修改,造成了该 Pod 不再适合运行在这个 Node 上的时候,Kubernetes 不会进行主动修正(IgnoredDuringExecution)

具体执行过程:

  • 开始调度一个 Pod 时,Kubernetes 调度器会同时启动 16 个Goroutine,来并发地为集群里的所有 Node 计算 Predicates,最后返回可以运行这个 Pod的宿主机列表。

  • 在为每个 Node 执行 Predicates 时,调度器会按照固定的顺序来进行检查。这个顺序,是按照 Predicates 本身的含义来确定

Priorities

Predicates 阶段完成了节点的“过滤”之后,Priorities 阶段的工作就是为这些节点打分。这里打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。

打分规则:

  1. LeastRequestedPriorit:选择空闲资源(CPU 和 Memory)最多的宿主机
  2. BalancedResourceAllocation: 优先从备选节点列表中选择各项资源使用率最均衡的节点
  3. CalculateNodeLabelPriority:优先选择含有指定Label的节点
  4. NodeAffinityPriority、TaintTolerationPriority 和 InterPodAffinityPriority:作为 Priority,一个 Node 满足上述规则的字段数目越多,它的得分就会越高
  5. ImageLocalityPriority:如果待调度 Pod 需要使用的镜像很大,并且已经存在于某些 Node上,那么这些 Node 的得分就会比较高。为了避免这个算法引发调度堆叠,调度器在计算得分的时候还会根据镜像的分布进行优化,即:如果大镜像分布的节点数目很少,那么这些节点的权重就会被调低,从而“对冲”掉引起调度堆叠的风险。

优先级(Priority )和抢占(Preemption)机制

解决Pod调度失败时的问题

优先级

  • 需要在 Kubernetes 里提交一个 PriorityClass 的定义
  • 优先级是一个 32 bit 的整数,最大值不超过 1000000000(10 亿,1billion),并且值越大代表优先级越高,超出 10 亿的值,其实是被 Kubernetes 保留下来分配给系统 Pod 使用的。显然,这样做的目的,就是保证系统 Pod 不会被用户抢占掉。

抢占

  • 当一个高优先级的 Pod 调度失败的时候,调度器的抢占能力就会被触发。这时,调度器就会试图从当前集群里寻找一个节点,使得当这个节点上的一个或者多个低优先级 Pod 被删除后,待调度的高优先级 Pod 就可以被调度到这个节点上
  • 抢占并不是立马执行,而是将抢占者的 spec.nominatedNodeName 字段,设置为被抢占的 Node 的名字。
  • 抢占者会重新进入下一个调度周期,然后在新的调度周期里来决定是不是要运行在被抢占的节点上。这当然也就意味着,即使在下一个调度周期,调度器也不会保证抢占者一定会运行在被抢占的节点上。
    • 因为调度器只会通过标准的 DELETE API 来删除被抢占的 Pod,所以,这些 Pod 必然是有一定的**“优雅退出”**时间(默认是 30s)的。
    • 此期间,集群环境会发生变化,比如其他的节点也是有可能变成可调度的,或者直接有新的节点被添加到这个集群中来
    • 此外,抢占者等待被调度的过程中,如果有其他更高优先级的 Pod 也要抢占同一个节点,那么调度器就会清空原抢占者的 spec.nominatedNodeName 字段,从而允许更高优先级的抢占者执行抢占,并且,这也就是得原抢占者本身,也有机会去重新抢占其他节点。
    • 因此,把抢占者交给下一个调度周期再处理,是一个非常合理的选择。

抢占的实现

  • Kubernetes 调度器实现抢占算法的一个最重要的设计,就是在调度队列的实现里,使用了两个不同的队列
    • 第一个队列,叫作 activeQ。凡是在 activeQ 里的 Pod,都是下一个调度周期需要调度的对象。所有需要调度的对象都是从这个队列里面pop出来
    • 第二个队列,叫作 unschedulableQ,专门用来存放调度失败的 Pod。当一个 unschedulableQ 里的 Pod 被更新之后,调度器会自动把这个 Pod 移动到 activeQ 里,从而给这些调度失败的 Pod “重新做人”的机会。
  • 调度失败事件会触发 调度器为抢占者寻找牺牲者的流程
    1. 调度器会检查这次失败事件的原因,来确认抢占是不是可以帮助抢占者找到一个新节点,因为有很多 Predicates 的失败是不能通过抢占来解决。
    2. 如果确定抢占可以发生,那么调度器就会把自己缓存的所有节点信息复制一份,然后使用这个副本来模拟抢占过程。
      1. 调度器会检查缓存副本里的每一个节点,然后从该节点上最低优先级的 Pod 开始,逐一“删除”这些 Pod。而每删除一个低优先级 Pod,调度器都会检查一下抢占者是否能够运行在该 Node 上。一旦可以运行,调度器就记录下这个 Node 的名字和被删除Pod 的列表,这就是一次抢占过程的结果了
      2. 遍历完所有的节点之后,调度器会在上述模拟产生的所有抢占结果里做一个选择,找出最佳结果。而这一步的判断原则,就是尽量减少抢占对整个系统的影响
    3. 根据2.2结果,开始真正抢占
      1. 调度器会检查牺牲者列表,清理这些 Pod 所携带的 nominatedNodeName 字段。
      2. 调度器会把抢占者的 nominatedNodeName,设置为被抢占的 Node 的名字。这里对抢占者 Pod 的更新操作,就会触发到我前面提到的**“重新做人”**的流程,从而让抢占者在下一个调度周期重新进入调度流程。
      3. 调度器会开启一个 Goroutine,同步地删除牺牲者。
    4. 接下来,调度器就会通过正常的调度流程把抢占者调度成功。但是当然也会有其他情况出现啦,因为时正常流程嘛。
      • 因为有上述抢占者的存在,任意一个待调度 Pod 来说的调度过程,其实是有一些特殊情况需要特殊处理的。
      • 在为某一对 Pod 和 Node 执行 Predicates 算法的时候,如果待检查的 Node 是一个即将被抢占的节点(调度队列里有 nominatedNodeName 字段值是该 Node 名字的Pod 存在),调度器就会对这个 Node ,将同样的Predicates 算法运行两遍。
      • 第一遍, 调度器会假设上述“潜在的抢占者”已经运行在这个节点上,然后执行 Predicates 算法;第二遍, 调度器会正常执行 Predicates 算法,即:不考虑任何“潜在的抢占者”。只有这两遍 Predicates 算法都能通过时,这个 Pod 和 Node 才会被认为是可以绑定(bind)的。
        • 第一遍可以保证前面的一些pod调度不会影响到这个“潜在抢占者”吧。InterPodAntiAffinity规则影响,InterPodAntiAffinity 规则关心待考察节点上所有 Pod 之间的互斥关系,所以我们在执行调度算法时必须考虑,如果抢占者已经存在于待考察 Node 上时,待调度 Pod 还能不能调度成功
        • 这也就意味着,我们在这一步只需要考虑那些优先级等于或者大于待调度 Pod 的抢占者。毕竟对于其他较低优先级 Pod 来说,待调度 Pod 总是可以通过抢占运行在待考察 Node
          上。
        • 执行第二遍 Predicates 算法的原因,则是因为“潜在的抢占者”最后不一定会运行在待考察的 Node 上。
Device Plugin

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APdPZmxm-1623207279274)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530102641732.png)]

实现对Externed Resource的管理

  • Device Plugin 会通过一个叫作 ListAndWatch 的 API,定期向 kubelet 汇报该 Node 上GPU 的列表,ListAndWatch 向上汇报的信息,只有本机上 GPU 的 ID 列表,而不会有任何关于 GPU 设备本身的信息。这也导致无法根据异构GPU的情况进行一个合理调度。
  • 调度成功后的 Pod 信息,自然就会被对应的 kubelet 拿来进行容器操作。而当 kubelet 发 现这个 Pod 的容器请求一个 GPU 的时候,kubelet 就会从自己持有的 GPU 列表里,为这个容 器分配一个 GPU。此时,kubelet 就会向本机的 Device Plugin 发起一个 Allocate() 请求。这 个请求携带的参数,正是即将分配给该容器的设备 ID 列表
  • Device Plugin 收到 Allocate 请求之后,它就会根据 kubelet 传递过来的设备 ID,从 Device Plugin 里找到这些设备对应的设备路径和驱动目录。被分配 GPU 对应的设备路径和驱动目录信息被返回给 kubelet 之后,kubelet 就完成了为一 个容器分配 GPU 的操作。接下来,kubelet 会把这些信息追加在创建该容器所对应的 CRI 请求 当中。这样,当这个 CRI 请求发给 Docker 之后,Docker 为你创建出来的容器里,就会出现这 个 GPU 设备,并把它所需要的驱动目录挂载进去。

Kubelet

kubelet 是在每个 Node 节点上运行的主要 “节点代理”。它可以使用以下之一向 apiserver 注册: 主机名(hostname);覆盖主机名的参数;某云驱动的特定逻辑。

kubelet 是基于 PodSpec 来工作的。每个 PodSpec 是一个描述 Pod 的 YAML 或 JSON 对象。 kubelet 接受通过各种机制(主要是通过 apiserver)提供的一组 PodSpec,并确保这些 PodSpec 中描述的容器处于运行状态且运行状况良好。 kubelet 不管理不是由 Kubernetes 创建的容器

除了PodSpec方式接收容器清单(manifest)信息,还有基于file,http endpoints, http server三种形式

SIG-Node与CRI

kubelet 以及容器运行时管理相关的内容,都属于 SIG-Node 的范畴

kubelet

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZppajLbp-1623207279275)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530103954326.png)]

  • kubelet 的工作核心,就是一个控制循环,即:SyncLoop(图中的大圆圈)。而驱动这个控制循环运行的事件,包括四种:

    • Pod 更新事件;
    1. Pod 生命周期变化;
    2. kubelet 本身设置的执行周期;
    3. 定时的清理事件
  • 跟其他控制器类似,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 的组件,这个组件, 一般被称作 CRI shim,扮演 kubelet 与容器项目之间 的“垫片”(shim),实现 CRI 规定的每个接口,然后把具体 的 CRI 请求“翻译”成对后端容器项目的请求或者操作

  • CRI 机制能够发挥作用的核心,就在于每一种容器项目现在都可以自己实现一个 CRI shim,自行对 CRI 请求进行处理。比如CNCF 里的 containerd 项目,就可以提供一个典型的 CRI shim 的能力,即:将 Kubernetes 发出的 CRI 请求,转换成对 containerd 的调用,然后创建出 runC 容器。而 runC 项目,才是负责执行我们前面讲解过的设置容器 Namespace、Cgroups 和 chroot 等基 础操作的组件。

CRI具体设计

  1. 第一组,是 RuntimeService。它提供的接口,主要是跟容器相关的操作。比如,创建和启动 容器、删除容器、执行 exec 命令等等。
    • CRI 设计的一个重要原则,就是确保这个接口本身,只关注容器,不关注 Pod,因为
      • Pod 是 Kubernetes 的编排概念,而不是容器运行时的概念
      • 此外,如果 CRI 里引入了关于 Pod 的概念,那么接下来只要 Pod API 对象的字段发生变化, 那么 CRI 就很有可能需要变更,Pod有时变动频繁,这样不好。
  2. 而第二组,则是 ImageService。它提供的接口,主要是容器镜像相关的操作,比如拉取镜 像、删除镜像等等。(镜像可以看作是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变

调用过程实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tjp6nw4o-1623207279276)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530125947152.png)]

  1. 当我们执行 kubectl run 创建了一个名叫 foo 的、包括了 A、B 两个容器的 Pod 之后。 这个 Pod 的信息最后来到 kubelet,kubelet 就会按照图中所示的顺序来调用 CRI 接口

  2. 需要注意的是,在 RunPodSandbox 这个接口的实现中,你还需要调用 networkPlugin.SetUpPod(…) 来为这个 Sandbox 设置网络。这个 SetUpPod(…) 方法,实际 上就在执行 CNI 插件里的 add(…) 方法,也就是利用 CNI 插件为 Pod 创建 网络,并且把 Infra 容器加入到网络中的操作。

  • 具体的 CRI shim 中,这些接口的实现是可以完全不同的。

    • 比如,如果是 Docker 项目, dockershim 就会创建出一个名叫 foo 的 Infra 容器(pause 容器),用来“hold”住整个 Pod 的 Network Namespace
    • 基于虚拟化技术的容器,比如 Kata Containers 项目,它的 CRI 实现就会直接创建出 一个轻量级虚拟机来充当 Pod
  1. kubelet 继续调用 CreateContainer 和 StartContainer 接口来创建和启动容器 A、 B。
    • 对应到 dockershim 里,就是直接启动 A,B 两个 Docker 容器。所以最后,宿主机上会出 现三个 Docker 容器组成这一个 Pod
    • 如果是 Kata Containers 的话,CreateContainer 和 StartContainer 接口的实现,就只会在 前面创建的轻量级虚拟机里创建两个 A、B 容器对应的 Mount Namespace。所以,最后在宿主机上,只会有一个叫作 foo 的轻量级虚拟机在运行。

除了上述对容器生命周期的实现之外,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)]

  1. 当我们对一个容器执行 kubectl exec 命令的时候,这个请求首先交给 API Server, 然后 API Server 就会调用 kubelet 的 Exec API。 这时,kubelet 就会调用 **CRI 的 Exec 接口,**而负责响应这个接口的,自然就是具体的 CRI shim。
  2. 但在这一步,CRI shim 并不会直接去调用后端的容器项目(比如 Docker )来进行处理,而只会返回一个 URL 给 kubelet。这个 URL,就是该 CRI shim 对应的 Streaming Server 的地址 和端口。
  3. kubelet 在拿到这个 URL 之后,就会把它以 Redirect 的方式返回给 API Server。所以这时 候,API Server 就会通过重定向来向 Streaming Server 发起真正的 /exec 请求,与它建立长连接
  4. 这个 Streaming Server 本身,是需要通过使用 SIG-Node 为你维护的 Streaming API 库来实现的。并且,Streaming Server 会在 CRI shim 启动时就一起启动。

总结

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 使用的是传统的虚拟化技术,通过虚拟硬件模拟出了一 台“小虚拟机”,然后在这个小虚拟机里安装了一个裁剪后的 Linux 内核来实现强隔离。
  • gVisor 的做法则更加激进,Google 的工程师直接用 Go 语言“模拟”出了一个运行在用户 态的操作系统内核,然后通过这个模拟的内核来代替容器进程向宿主机发起有限的、可控的系统调用。

Kata Containers

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5LFhJ9L1-1623207279284)(https://gitee.com/whoconli/k8s-learning/blob/master/Container%20&%20Kubernetes.assets/image-20210530230655711.png)]

  • 启动一个 Kata Containers,一个标准的虚拟机管理程序(Virtual Machine Manager, VMM)也会启动;这里就是Qemu
  • 使用了虚拟机作为进程的隔离环境之后,Kata Containers 原生就带有了 Pod 的概念。即: 这个 Kata Containers 启动的虚拟机,就是一个 Pod
  • 用户定义的容器,就是运行在这个轻量级虚拟机里的进程
  • Kata Containers 的虚拟机里会有一个特殊的 Init 进程负 责管理虚拟机里面的用户容器,并且只为这些容器开启 Mount Namespace。所以,这些用户容器之间,原生就是共享 Network 以及其他 Namespace 的
  • 为了跟上层编排框架比如 Kubernetes 进行对接,Kata Containers 项目会启动一系列跟用户容器对应的 shim 进程(实际上就是 Init 进程),来负责操作这些用户容器的生命周期。
  • Kata Containers 运行起来之后,虚拟机里的用户进程(容器),实际上只能看 到虚拟机里的、被裁减过的 Guest Kernel,以及通过 Hypervisor 虚拟出来的硬件设备。为了能够对这个虚拟机的 I/O 性能进行优化,Kata Containers 也会通过 vhost 技术(比 如:vhost-user)来实现 Guest 与 Host 之间的高效的网络通信,并且使用 PCI Passthrough (PCI 穿透)技术来让 Guest 里的进程直接访问到宿主机上的物理设备

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)]

总结

  1. 性能上,KataContainers 和 KVM 实现的 gVisor 基本不分伯仲,在启动速度和占用资源 上,基于用户态内核的 gVisor 还略胜一筹
  2. 系统调用上,对于系统调用密集的应用,gVisor 就会因为需要频繁拦截系统调用而出现性能急剧下降的情况。此外, gVisor 由于要自己使用 Sentry 去模拟一个 Linux 内核,所以它能支持的系统调用是有限的, 只是 Linux 系统调用的一个子集。

监控体系

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)]

  • Prometheus 项目工作的核心,是使用 Pull (也有post)的方式去搜集被监控对象的 Metrics 数据(监控指标数据),然后,再把这些数据保存在一个 TSDB (时间序列数据库,比 如 OpenTSDB、InfluxDB 等)当中,以便后续可以按照时间进行检索
  • 第一种 Metrics,是宿主机的监控数据;第二种 Metrics,是来自于 Kubernetes 的 API Server、kubelet 等组件的 /metrics API;第三种 Metrics,是 Kubernetes 相关的监控数据。

具体的监控指标规划上,遵循业界通用的 USE 原则和 RED 原则。

USE 原则指的是,按照如下三个维度来规划资源监控指标:

  1. 利用率(Utilization),资源被有效利用起来提供服务的平均时间占比;
  2. 饱和度(Saturation),资源拥挤的程度,比如工作队列的长度;
  3. 错误率(Errors),错误的数量。

而 RED 原则指的是,按照如下三个维度来规划服务监控指标:

  1. 每秒请求数量(Rate);

  2. 每秒错误数量(Errors);

  3. 服务响应时间(Duration)。

日志收集和管理

Kubernetes 里面对容器日志的处理方式,都叫作 cluster-level-logging, 即:这个日志处理系统,与容器、Pod 以及 Node 的生命周期都是完全无关的

Kubernetes 本身,实际上是不会为你做容器日志收集工作的,所以为了实现上述 clusterlevel- logging,你需要在部署集群的时候,提前对具体的日志方案进行规划

三种方案:

  1. Node 上部署 logging agent,将日志文件转发到后端存储里保存起来。

    • 这里的核心就在于 logging agent ,它一般都会以 DaemonSet 的方式运行在节点 上,然后将宿主机上的容器日志目录挂载进去,最后由 logging-agent 把日志转发出去
    • 在 Node 上部署 logging agent 最大的优点,在于一个节点只需要部署一个agent,并且不会对应用和 Pod 有任何侵入性
    • 不足之处就在于,它要求应用输出的日志,都必须是直接输出到容器的 stdout 和 stderr
  2. 通过一个 sidecar 容器把这些日志文件重新输出到sidecar 的 stdout 和 stderr 上, 然后使用第一种方案,克服当容器的日志只能输出到某些文件里的时候的缺点

    • 添加的sidecar 跟主容器之间是共享 Volume 的,所以这里的 sidecar 方案的额外性能损耗并不高,也就是多占用一点 CPU 和内存罢了,但是相当于对日志进行备份,浪费磁盘空间
  3. 通过一个 sidecar 容器,直接把应用的日志文件发送到远程存储里面去

    • 在这种方案里,你的应用还可以直接把日志输出到固定的文件里而不是 stdout,你的 logging-agent 还可以使用 fluentd,后端存储还可以是 ElasticSearch。只不过, fluentd 的输入源, 变成了应用的日志文件。

你可能感兴趣的:(k8s学习)