云原生

【整理总结自 CNCF × Alibaba 云原生技术公开课】

【非全部内容】

【图片看不到,可以去gitee上看】

文章目录

  • 云原生
    • 相关定义和概念
      • 技术范畴
      • 两个理论
      • 云原生关键技术点
    • 容器
      • 进程间隔离
      • 镜像
      • 容器生命周期
      • 容器项目架构
        • moby 容器引擎架构
        • containerd 容器架构
        • 容器流程
      • 容器和虚拟机
    • Kubernetes 概念
        • 核心功能
        • 架构
        • 核心概念
        • API相关
      • Pod
        • 具体概念
        • 实现机制
        • sidecar容器设计模式
      • 编排和管理
        • 资源元信息
        • 控制器模式
      • Deployment
        • 作用
        • 语法定义
        • 状态转变
        • 结构设计
        • Sepc字段解析
      • Job CronJobs DaemonSet
        • job:管理任务的控制器
        • CronJob:定时运行 Job
        • job 架构设计
        • DaemonSet:守护进程控制器
        • DaemonSet架构设计
      • 应用配置管理
        • 背景
        • 配置管理
        • ConfigMap
        • Secret
        • ServiceAccount
        • Resource
        • SecurityContext
        • InitContainer
      • Volumes
        • 背景和类型
        • PV 与 PVC
          • 引入原因
          • PVC设计
          • PV的产生
          • PV PVC StorageClass使用
          • PV状态流转
          • 架构设计
      • 存储拓扑调度
        • 拓扑
        • 拓扑调度
      • 可观测性
        • Liveness 指针和 Readiness 指针
        • 常见故障和排查
      • Service
        • 背景
        • Service:Kubernetes 中的服务返现与负载均衡
        • 架构设计
      • 资源调度
        • 调度过程
        • 基础调度
          • Pod资源需求
          • 亲和性/反亲和性调度
          • 小结
        • 调度流程
          • 整体调度框架
          • 具体流程
          • 算法实现
            • 过滤器 Predicates
            • 打分器 Priorities
          • 优先级(Priority )和抢占(Preemption)机制
            • 优先级
            • 抢占机制
          • 调度器语法
          • 调度器框架
          • 调度器框架

相关定义和概念

云原生为用户指定了一条低心智负担的、敏捷的、能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径。云原生其实是一套指导进行软件架构设计的思想。按照这样的思想而设计出来的软件:首先,天然就“生在云上,长在云上”;其次,能够最大化地发挥云的能力,使得我们开发的软件和“云”能够天然地集成在一起,发挥出“云”的最大价值。

技术范畴

  1. 第一部分是云应用定义与开发流程。这包括应用定义与镜像制作、配置 CI/CD、消息和 Streaming 以及数据库等。
  2. 第二部分是云应用的编排与管理流程。这也是 Kubernetes 比较关注的一部分,包括了应用编排与调度、服务发现治理、远程调用、API 网关以及 Service Mesh
  3. 第三部分是监控与可观测性。这部分所强调的是云上应用如何进行监控、日志收集、Tracing 以及在云上如何实现破坏性测试,也就是混沌工程的概念。
  4. 第四部分就是云原生的底层技术,比如容器运行时、云原生存储技术、云原生网络技术等。
  5. 第五部分是云原生工具集,在前面的这些核心技术点之上,还有很多配套的生态或者周边的工具需要使用,比如流程自动化与配置管理、容器镜像仓库、云原生安全技术以及云端密码管理等。
  6. 最后则是 Serverless。Serverless 是一种 PaaS 的特殊形态,它定义了一种更为“极端抽象”的应用编写方式,包含了 FaaS 和 BaaS 这样的概念。而无论是 FaaS 还是 BaaS,其最为典型的特点就是按实际使用计费(Pay as you go),因此 Serverless 计费也是重要的知识和概念

两个理论

  • 第一个理论基础是:不可变基础设施。这一点目前是通过容器镜像来实现的,其含义就是应用的基础设施应该是不可变的,是一个自包含、自描述可以完全在不同环境中迁移的东西;
  • 第二个理论基础就是:云应用编排理论。当前的实现方式就是 Google 所提出来的“容器设计模式”,这也是本系列课程中的 Kubernetes 部分所需主要讲解的内容。

云原生关键技术点

  1. 如何构建自包含、可定制的应用镜像
  2. 能不能实现应用快速部署与隔离能力;
  3. 应用基础设施创建和销毁的自动化管理
  4. 可复制的管控系统和支撑组件

容器

进程间隔离

操作系统中进程有自己的特点:

  • 进程可以相互看到并通信–》高级权限的进程可以攻击其他进程
  • 进程使用同一个文件系统–》高级权限进程对其他进程数据进行增删改查,破环其正常运行;此外进程与进程之间依赖容易产生冲突
  • 进程会使用相同的资源–》进程与进程之间会存在资源抢占的问题

解决办法:提供一个独立的运行环境

  • chroot:将子目录变成根目录,达到视图级别的隔离;进程在 chroot 的帮助下可以具有独立的文件系统,对于这样的文件系统进行增删改查不会影响到其他进程
  • namespace:实现进程在资源的视图上进行隔离,不让进程相互看见
  • Cgroup :来限制其资源使用率,设置其能够使用的 CPU 以及内存量

namespace

  • mout namespace。mout namespace 就是保证容器看到的文件系统的视图,是容器镜像提供的一个文件系统,也就是说它看不见宿主机上的其他文件,除了通过 -v 参数 bound 的那种模式,是可以把宿主机上面的一些目录和文件,让它在容器里面可见的。

  • uts namespace,这个 namespace 主要是隔离了 hostname 和 domain

  • pid namespace,这个 namespace 是保证了容器的 init 进程是以 1 号进程来启动的。

  • network namespace,除了容器用 host 网络这种模式之外,其他所有的网络模式都有一个自己的 network namespace 的文件。

  • user namespace,这个 namespace 是控制用户 UID 和 GID 在容器内部和宿主机上的一个映射,不过这个 namespace 用的比较少。

  • IPC namespace,这个 namespace 是控制了进程间通信的一些东西,比方说信号量。

  • cgroup namespace,用 cgroup namespace 带来的一个好处是容器中看到的 cgroup 视图是以根的形式来呈现的,这样的话就和宿主机上面进程看到的 cgroup namespace 的一个视图方式是相同的。另外一个好处是让容器内部使用 cgroup 会变得更安全。

Cgroup(以docker为例)

  • cgroup 主要是做资源限制的,docker 容器有两种 cgroup 驱动:一种是 systemd 的,另外一种是 cgroupfs 的。

    • cgroupfs 比较好理解。比如说要限制内存是多少,要用 CPU share 为多少,其实直接把 pid 写入对应的一个 cgroup 文件,然后把对应需要限制的资源也写入相应的 memory cgroup 文件和 CPU 的 cgroup 文件就可以了。
    • 因为 systemd 本身可以提供一个 cgroup 管理方式,写 cgroup 操作都必须通过 systemd 的接口来完成,不能手动更改 cgroup 的文件
  • docker 常用cgroup
    • CPU,CPU 一般会去设置 cpu share 和 cupset,控制 CPU 的使用率
    • memory,是控制进程内存的使用量。
    • device ,device 控制了你可以在容器中看到的 device 设备。
    • freezer。它和第三个 cgroup(device)都是为了安全的。当你停止容器的时候,freezer 会把当前的进程全部都写入 cgroup,然后把所有的进程都冻结掉,这样做的目的是,防止你在停止的时候,有进程会去做 fork,防止进程逃逸到宿主机上面去,是为安全考虑。
    • blkio,blkio 主要是限制容器用到的磁盘的一些 IOPS(每秒读操作次数) 还有 bps(每秒读取块设备的数据量) 的速率限制。因为 cgroup 不唯一的话,blkio 只能限制同步 io,docker io 是没办法限制的
    • pid, 限制的是容器里面可以用到的最大进程数量

镜像

容器具有一个独立的文件系统,因为使用的是系统的资源,所以在独立的文件系统内不需要具备内核相关的代码或者工具,我们只需要提供容器所需的二进制文件、配置文件以及依赖即可,这些容器运行时所需要的所有的文件集合称之为容器镜像。

构建容器镜像

通常情况下,我们会采用 Dockerfile 来构建镜像,这是因为 Dockerfile 提供了非常便利的语法糖,能够帮助我们很好地描述构建的每个步骤,每个构建步骤都会对已有的文件系统进行操作,这样就会带来文件系统内容的变化,我们将这些变化称之为 changeset。

changeset 的分层以及复用特点能够带来几点优势:

  • 第一,能够提高分发效率,对于大的镜像而言,如果将其拆分成各个小块就能够提高镜像的分发效率,这是因为镜像拆分之后就可以并行下载这些数据;

  • 第二,因为这些数据是相互共享的,也就意味着当本地存储上包含了一些数据的时候,只需要下载本地没有的数据即可,举个简单的例子就是 golang 镜像是基于 alpine 镜像进行构建的,当本地已经具有了 alpine 镜像之后,在下载 golang 镜像的时候只需要下载本地 alpine 镜像中没有的部分即可;

  • 第三,因为镜像数据是共享的,因此可以节约大量的磁盘空间,简单设想一下,当本地存储具有了 alpine 镜像和 golang 镜像,在没有复用的能力之前,alpine 镜像具有 5M 大小,golang 镜像有 300M 大小,因此就会占用 305M 空间;而当具有了复用能力之后,只需要 300M 空间即可。

如下图所示的 Dockerfile 适用于描述如何构建 golang 应用的。

如图所示:

  1. FROM 行表示以下的构建步骤基于什么镜像进行构建,正如前面所提到的,镜像是可以复用的;
  2. WORKDIR 行表示会把接下来的构建步骤都在哪一个相应的具体目录下进行,其起到的作用类似于 Shell 里面的 cd;
  3. COPY 行表示的是可以将宿主机上的文件拷贝到容器镜像内;
  4. RUN 行表示在具体的文件系统内执行相应的动作。当我们运行完毕之后就可以得到一个应用了;
  5. CMD 行表示使用镜像时的默认程序名字

docker Images

  • docker 镜像是基于联合文件系统的。联合文件系统:它允许文件是存放在不同的层级上面的,但是最终是可以通过一个统一的视图,看到这些层级上面的所有文件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lH6CZJlG-1623207610981)(云原生.assets/image-20210607112239877.png)]

  • docker 镜像的存储,它的底层是基于不同的文件系统的,所以它的存储驱动也是针对不同的文件系统作为定制的,比如 AUFS、btrfs、devicemapper 还有 overlay。docker 对这些文件系统做了一些相对应的一个 graph driver 的驱动,也就是通过这些驱动把镜像存在磁盘上面。
  • 存储流程 overlay为例
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C88oZo3R-1623207610982)(云原生.assets/image-20210607112403069.png)]
    • lower 层,也就是镜像层,它是一个只读层
    • upper 层,容器的读写层,采用了写时复制的机制,也就是说只有对某些文件需要进行修改的时候才会从 lower 层把这个文件拷贝上来,之后所有的修改操作都会对 upper 层的副本进行修改
    • workdir,它的作用是充当一个中间层的作用。当对 upper 层里面的副本进行修改时,会先放到 workdir,然后再从 workdir 移到 upper 里面去。
    • mergedir,是一个统一视图层。从 mergedir 里面可以看到 upper 和 lower 中所有数据的整合,然后我们 docker exec 到容器里面,看到一个文件系统其实就是 mergedir 统一视图层
  • 文件操作 overlay为例
    • 读,如果 upper 层没有副本,数据都是从 lower 层上读取
    • 写,容器刚创建时,upper 层为控,只有对文件进行写操作时,才会从 lower 层拷贝文件上来,对副本进行操作
    • 删,overlay 里面其实是没有真正的删除操作的。它所谓的删除其实是通过对文件进行标记,然后从最上层的统一视图层去看,看到这个文件如果做标记,认为这个文件是被删掉的,就不会让这个文件显示出来。

容器生命周期

容器的生命周期和 initial 进程的生命周期是一致的, initial 进程退出的时候,所有的子进程也会随之退出

这样会导致有状态数据不能持久化,因此需要有数据卷的存在

数据卷的生命周期是独立于容器的生命周期的,其管理主要有两种方式:

  • 第一种是通过 bind 的方式,直接将宿主机的目录直接挂载到容器内;这种方式比较简单,但是会带来运维成本,因为其依赖于宿主机的目录,需要对于所有的宿主机进行统一管理。
  • 第二种是将目录管理交给运行引擎。

容器项目架构

moby 容器引擎架构

moby 是目前最流行的容器管理引擎,moby daemon 会对上提供有关于容器、镜像、网络以及 Volume的管理。moby daemon 所依赖的最重要的组件就是 containerd,containerd 是一个容器运行时管理引擎,其独立于 moby daemon ,可以对上提供容器、镜像的相关管理。

containerd 底层有 containerd shim 模块,其类似于一个守护进程,这样设计的原因有几点:

  • 首先,containerd 需要管理容器生命周期,而容器可能是由不同的容器运行时所创建出来的,因此需要提供一个灵活的插件化管理。而 shim 就是针对于不同的容器运行时所开发的,这样就能够从 containerd 中脱离出来,通过插件的形式进行管理。
  • 其次,因为 shim 插件化的实现,使其能够被 containerd 动态接管。如果不具备这样的能力,当 moby daemon 或者 containerd daemon 意外退出的时候,容器就没人管理了,那么它也会随之消失、退出,这样就会影响到应用的运行。
  • 最后,因为随时可能会对 moby 或者 containerd 进行升级,如果不提供 shim 机制,那么就无法做到原地升级,也无法做到不影响业务的升级,因此 containerd shim 非常重要,它实现了动态接管的能力。

containerd 容器架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GrEpiS5l-1623207610984)(云原生.assets/image-20210607112937753.png)]

两大功能

  1. 对于容器生命周期的管理(左边)
  2. 镜像存储的管理(右边)

一个镜像存储的管理

  • 第一层

    • GRPC,containerd 对于上层来说是通过 GRPC server 的形式来对上层提供服务的
    • Metrics ,这个部分主要是提供 cgroup Metrics 的一些内容
  • 第二层
    • 左边是容器镜像的一个存储
    • 中线 images、containers 下面是 Metadata,这部分 Matadata 是通过 bootfs 存储在磁盘上面的。
    • 右边的 Tasks 是管理容器的容器结构Events 是对容器的一些操作都会有一个 Event 向上层发出,然后上层可以去订阅这个 Event,由此知道容器状态发生什么变化。
  • 第三层
    • Runtimes 层,这个 Runtimes 可以从类型区分,比如说 runC 或者是安全容器之类的

容器流程

start流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9E11ZndY-1623207610985)(云原生.assets/image-20210607113809704.png)]

组成部分:

  • 第一个部分是容器引擎部分,容器引擎可以是 docker,也可以是其它的。
  • 两个虚线框框起来的 containerd 和 containerd-shim,它们两个是属于 containerd 架构的部分。
  • 最下面就是 container 的部分,这个部分是通过一个 runtime 去拉起的,可以认为是 shim 去操作 runC 命令创建的一个容器

流程:

首先它会去创建一个 matadata,然后会去发请求给 task service 说要去创建容器。通过中间一系列的组件,最终把请求下发到一个 shim。containerd 和 shim 的交互其实也是通过 GRPC 来做交互的,containerd 把创建请求发给 shim 之后,shim 会去调用 runtime 创建一个容器出来

exec 流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oo0fZKhy-1623207610986)(云原生.assets/image-20210607113756658.png)]

exec 的操作还是发给 containerd-shim 的。对容器来说,去 start 一个容器和去 exec 一个容器,其实并没有本质的区别。

start 和 exec 区别:

  • exec 的时候,需要把这个进程加入到一个已有的 namespace 里面;
  • start 的时候,容器进程的 namespace 是需要去专门创建。

容器和虚拟机

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N1iSaZ0J-1623207610986)(云原生.assets/image-20210601143638048.png)]

VM 利用 Hypervisor 虚拟化技术来模拟 CPU、内存等硬件资源,这样就可以在宿主机上建立一个 Guest OS,这是常说的安装一个虚拟机。

  • 容器是一个进程集合,具有自己独特的视图视角;
  • 镜像是容器所需要的所有文件集合,其具备一次构建、到处运行的特点;
  • 容器的生命周期和 initial 进程的生命周期是一样的;
  • 容器和 VM 相比,各有优劣,容器技术在向着强隔离方向发展。

Kubernetes 概念

核心功能

  • 服务的发现与负载的均衡

  • 容器的自动装箱,我们也会把它叫做 scheduling,就是“调度”,把一个容器放到一个集群的某一个机器上,Kubernetes 会帮助我们去做存储的编排,让存储的声明周期与容器的生命周期能有一个连接;

  • Kubernetes 会帮助我们去做自动化的容器的恢复。在一个集群中,经常会出现宿主机的问题或者说是 OS 的问题,导致容器本身的不可用,Kubernetes 会自动地对这些不可用的容器进行恢复;

  • Kubernetes 会帮助我们去做应用的自动发布与应用的回滚,以及与应用相关的配置密文的管理;

  • 对于 job 类型任务,Kubernetes 可以去做批量的执行

  • 为了让这个集群、这个应用更富有弹性,Kubernetes 也支持水平的伸缩

架构

Kubernetes 架构是一个比较典型的二层架构和 server-client 架构。Master 作为中央的管控节点,会去与 Node 进行一个连接。

所有 UI 的、clients、这些 user 侧的组件,只会和 Master 进行连接,把希望的状态或者想执行的命令下发给 Master,Master 会把这些命令或者状态下发给相应的节点,进行最终的执行。

  • **API Server:**顾名思义是用来处理 API 操作的,Kubernetes 中所有的组件都会和 API Server 进行连接,组件与组件之间一般不进行独立的连接,都依赖于 API Server 进行消息的传送;

  • **Controller:**是控制器,它用来完成对集群状态的一些管理。比如刚刚我们提到的两个例子之中,第一个自动对容器进行修复、第二个自动进行水平扩张,都是由 Kubernetes 中的 Controller 来进行完成的;

  • **Scheduler:**是调度器,“调度器”顾名思义就是完成调度的操作,就是我们刚才介绍的第一个例子中,把一个用户提交的 Container,依据它对 CPU、对 memory 请求大小,找一台合适的节点,进行放置;

  • **etcd:**是一个分布式的一个存储系统,API Server 中所需要的这些原信息都被放置在 etcd 中,etcd 本身是一个高可用系统,通过 etcd 保证整个 Kubernetes 的 Master 组件的高可用性。

Kubernetes 的 **Node 是真正运行业务负载的,每个业务负载会以 Pod 的形式运行。等一下我会介绍一下 Pod 的概念。**一个 Pod 中运行的一个或者多个容器,真正去运行这些 Pod 的组件的是叫做 kubelet,也就是 Node 上最为关键的组件,它通过 API Server 接收到所需要 Pod 运行的状态,然后提交到我们下面画的这个 Container Runtime 组件中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y4Z1ygJG-1623207610987)(云原生.assets/image-20210601205016872.png)]

在 OS 上去创建容器所需要运行的环境,最终把容器或者 Pod 运行起来,也需要对存储跟网络进行管理。Kubernetes 并不会直接进行网络存储的操作,他们会靠 Storage Plugin 或者是网络的 Plugin 来进行操作。用户自己或者云厂商都会去写相应的 Storage Plugin 或者 Network Plugin,去完成存储操作或网络操作。

在 Kubernetes 自己的环境中,也会有 Kubernetes 的 Network,它是为了提供 Service network 来进行搭网组网的。(等一下我们也会去介绍“service”这个概念。)真正完成 service 组网的组件的是 Kube-proxy,它是利用了 iptable 的能力来进行组建 Kubernetes 的 Network,就是 cluster network,以上就是 Node 上面的四个组件。

Kubernetes 的 Node 并不会直接和 user 进行 interaction,它的 interaction 只会通过 Master。而 User 是通过 Master 向节点下发这些信息的。Kubernetes 每个 Node 上,都会运行我们刚才提到的这几个组件

核心概念

Pod

Pod 是 Kubernetes 的一个。用户可以通过 Kubernetes 的 Pod API 生产一个 Pod,让 Kubernetes 对这个 Pod 进行调度,也就是把它放在某一个 Kubernetes 管理的节点上运行起来。一个 Pod 简单来说是对一组容器的抽象,它里面会包含。

  • 最小调度以及资源单元
  • 一个或多个容器组成
  • 定义容器运行的方式(Command、环境变量)
  • 提供给容器共享的运行环境(网络、进程空间)

Volume

  • 声明Pod中容器可访问的文件目录
  • 可以被挂载在Pod中一个(或者多个)容器的指定路径下
  • 支持多种后端存储抽象:分布式、本地、云存储

Deployment

  • 定义一组Pod的副本数目、版本等
  • 通过控制器(Controller)维持Pod的数目
    • 自动恢复失败的Pod
  • 通过控制器以指定的策略控制版本
    • 滚动升级、重新生成、回滚等

Service

  • Service 提供了一个或者多个 Pod 实例的稳定访问地址

  • 实现 Service 有多种方式,

    • Kubernetes 支持 Cluster IP,上面我们讲过的 kuber-proxy 的组网,
    • 它也支持 nodePort、 LoadBalancer 等其他的一些访问的能力

Namespace

  • 一个集群内部的逻辑隔离的,它包括鉴权、资源管理等
  • Kubernetes 的每个资源,比如刚才讲的 Pod、Deployment、Service 都属于一个 Namespace,
  • 同一个 Namespace 中的资源需要命名的唯一性
  • 不同的 Namespace 中的资源可以重名。

API相关

结构

  • 从 high-level 上看,Kubernetes API 是由 HTTP+JSON 组成的:用户访问的方式是 HTTP,访问的 API 中 content 的内容是 JSON 格式的
  • 可以通过 kubectl、 UI、curl访问

对于这个 Pod 类型的资源,它的 HTTP 访问的路径,就是 API,然后是 apiVesion: V1, 之后是相应的 Namespaces,以及 Pods 资源,最终是 Podname,也就是 Pod 的名字

Label

这个 label 可以是一组 KeyValuePair

  • 可以被selector查询
  • 资源集合的默认表达形式

Pod

具体概念

在 kubernetes 里面,Pod 实际上正是 kubernetes 项目为你抽象出来的一个可以类比为进程组的概念

进程组

  • 一个应用一般由多个进程组成,进程之间会共享一些资源和文件。

  • 由于容器实际上是一个“单进程”模型,所以如果你在容器里启动多个进程,只有一个可以作为 PID=1 的进程,而这时候,如果这个 PID=1 的进程挂了,或者说失败退出了,那么其他三个进程就会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资源,这是一个非常不好的情况

  • 容器的设计本身是一种“单进程”模型,不是说容器里只能起一个进程,由于容器的应用等于进程,所以只能去管理 PID=1 的这个进程,其他再起来的进程其实是一个托管状态。

    • 除非,服务应用进程本身就具有“进程管理”的能力
    • 或者使用systemd,用它来管理其他所有的进程,但会导致我们实际上没办法直接管理我的应用了,因为我的应用被 systemd 给接管了,那么这个时候应用状态的生命周期就不等于容器生命周期

pod在k8s

  • Pod 在 Kubernetes 里面只有一个逻辑单位,没有一个真实的东西对应说这个就是 Pod
  • 真正起来在物理上存在的东西,就是四个容器。这四个容器,或者说是多个容器的组合就叫做 Pod
  • Pod 是 Kubernetes 分配资源的一个单位,因为里面的容器要共享某些资源,所以 Pod 也是 Kubernetes 的原子调度单位。
  • 组合资源需求避免应用调度失败。Task co-scheduling 问题

超亲密关系

亲密关系是一定可以通过调度来解决的,超亲密关系来说,有一个问题,即它必须通过 Pod 来解决。

超亲密关系大概分为以下几类:

  • 比如说两个进程之间会发生文件交换,前面提到的例子就是这样,一个写日志,一个读日志;
  • 两个进程之间需要通过 localhost 或者说是本地的 Socket 去进行通信,这种本地通信也是超亲密关系;
  • 这两个容器或者是微服务之间,需要发生非常频繁的 RPC 调用,出于性能的考虑,也希望它们是超亲密关系;
  • 两个容器或者是应用,它们需要共享某些 Linux Namespace。最简单常见的一个例子,就是我有一个容器需要加入另一个容器的 Network Namespace。这样我就能看到另一个容器的网络设备,和它的网络信息。

实现机制

Pod 要解决这个问题,核心就在于如何让一个 Pod 里的多个容器之间最高效的共享某些资源和数据。

具体的解法分为两个部分:网络和存储

共享网络

  • 会在每个 Pod 里,额外起一个 Infra container 小容器来共享整个 Pod 的 Network Namespace。
  • Infra container 是一个非常小的镜像,大概 100~200KB 左右,是一个汇编语言写的、永远处于“暂停”状态的容器。由于有了这样一个 Infra container 之后,其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中
  • 一个 Pod 里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于 Pod 第一次创建的这个 Infra container。也就是所有容器共享这个 infra 的 IP 地址。
  • 整个 Pod 里面,必然是 Infra container 第一个启动。并且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。所以 k8s 可以单独更新一个 Pod 里的一个镜像。

共享存储

通过yaml文件声明共享存储名字以及对应mountPath就行。

sidecar容器设计模式

在 Pod 里面,可以定义一些专门的容器,来执行主业务容器所需要的一些辅助工作

其它有哪些操作呢?比如说:

  • 原本需要在容器里面执行 SSH 需要干的一些事情,可以写脚本、一些前置的条件,其实都可以通过像 Init Container 或者另外像 Sidecar 的方式去解决;

  • 当然还有一个典型例子就是我的日志收集,日志收集本身是一个进程,是一个小容器,那么就可以把它打包进 Pod 里面去做这个收集工作;

  • 还有一个非常重要的东西就是 Debug 应用,实际上现在 Debug 整个应用都可以在应用 Pod 里面再次定义一个额外的小的 Container,它可以去 exec 应用 pod 的 namespace;

  • 查看其他容器的工作状态,这也是它可以做的事情。不再需要去 SSH 登陆到容器里去看,只要把监控组件装到额外的小容器里面就可以了,然后把它作为一个 Sidecar 启动起来,跟主业务容器进行协作,所以同样业务监控也都可以通过 Sidecar 方式来去做。

辅助功能从我的业务容器解耦了,所以我就能够独立发布 Sidecar 容器,并且更重要的是这个能力是可以重用的,即同样的一个监控 Sidecar 或者日志 Sidecar,可以被全公司的人共用的

应用场景

  1. tomcat + war 部署

  2. 应用与日志收集

    业务容器将日志写在一个 Volume 里面,而由于 Volume 在 Pod 里面是被共享的,所以日志容器 —— 即 Sidecar 容器一定可以通过共享该 Volume,直接把日志文件读出来,然后存到远程存储里面,或者转发到另外一个例子

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6301Im5k-1623207610988)(云原生.assets/image-20210602110338980.png)]

  3. 代理容器

    • 单独写一个这么小的 Proxy,用来处理对接外部的服务集群,它对外暴露出来只有一个 IP 地址就可以了。所以接下来,业务容器主要访问 Proxy,然后由 Proxy 去连接这些服务集群,这里的关键在于 Pod 里面多个容器是通过 localhost 直接通信的,因为它们同属于一个 network Namespace,网络视图都一样,所以它们俩通信 localhost,并没有性能损耗

    • 代理容器除了做了解耦之外,并不会降低性能,更重要的是,像这样一个代理容器的代码就又可以被全公司重用了

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bKmyZGyJ-1623207610988)(云原生.assets/image-20210602110513271.png)]

  4. 适配器容器

    • 业务容器暴露出来的监控接口是 /metrics,访问这个这个容器的 metrics 的这个 URL 就可以拿到了。可是现在,这个监控系统升级了,它访问的 URL 是 /health,我只认得暴露出 health 健康检查的 URL,才能去做监控,metrics 不认识
    • 额外写一个 Adapter,用来把所有对 health 的这个请求转发给 metrics 就可以了,所以这个 Adapter 对外暴露的是 health 这样一个监控的 URL

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oE27RfzX-1623207610988)(云原生.assets/image-20210602110657160.png)]

编排和管理

资源元信息

Labels

  • 资源标签是一种具有标识型的 Key:Value 元数据
  • 作用
    • 用于筛选资源
    • 唯一的组合资源方法
  • 可以使用selector来查询

Selector

  • 类型

    • 相等型 Selector,可以包括多个相等条件,多个相等条件之间是逻辑”与“的关系
    • 集合型 Selector
      • in 和 notin,集合内筛选条件为“或”关系

Annotations

  • 标识型的 Key:Value 元数据
  • 作用
    • 存储资源的非标示性信息
    • 扩展资源的 spec/status 的描述
  • 特点
    • 一般比label更大
    • 可以包含特殊字符
    • 可以结构化也可以非结构化

Ownereference

  • 所有者,一般就是指集合类的资源

    • Pod 集合,就有 replicaset、statefulset
  • 集合类资源的控制器会创建对应的归属资源

    • replicaset 控制器在操作中会创建 Pod,被创建 Pod 的 Ownereference 就指向了创建 Pod 的 replicaset
  • 作用

    • 使得用户可以方便地查找一个创建资源的对象

    • 还可以用来实现级联删除的效果

控制器模式

控制循环

控制型模式最核心的就是控制循环的概念。在控制循环中包括了控制器,被控制的系统,以及能够观测系统的传感器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OrIfh5tb-1623207610989)(云原生.assets/image-20210602113539906.png)]

各个组件将都会是独立自主地运行,不断使系统向 spec 表示终态趋近

Sensor 传感器

控制循环中逻辑的传感器主要由 Reflector、Informer、Indexer 三个组件构成

  • Reflector 通过 List 和 Watch K8s server 来获取资源的数据。

    • List 用来在 Controller 重启以及 Watch 中断的情况下,进行系统资源的全量更新
    • 而 Watch 则在多次 List 之间进行增量的资源更新
  • Reflector 在获取新的资源数据后,会在 Delta 队列中塞入一个包括资源对象信息本身以及资源对象事件类型的 Delta 记录
    • Delta 队列中可以保证同一个对象在队列中仅有一条记录,从而避免 Reflector 重新 List 和 Watch 的时候产生重复的记录
  • Informer 组件不断地从 Delta 队列中弹出 delta 记录,然后把资源对象交给 indexer,让 indexer 把资源记录在一个缓存中,缓存在默认设置下是用资源的命名空间来做索引的,并且可以被 Controller Manager 或多个 Controller 所共享
  • 之后,再把这个事件交给事件的回调函数

控制器

  • 控制循环中的控制器组件主要由事件处理函数以及 worker 组成,事件处理函数之间会相互关注资源的新增、更新、删除的事件,并根据控制器的逻辑去决定是否需要处理。
  • 对需要处理的事件,会把事件关联资源的命名空间以及名字塞入一个工作队列中,并且由后续的 worker 池中的一个 Worker 来处理
    • 工作队列会对存储的对象进行去重,从而避免多个 Woker 处理同一个资源的情况。
    • Worker 在处理资源对象时,一般需要用资源的名字来重新获得最新的资源数据,用来创建或者更新资源对象,或者调用其他的外部服务
    • Worker 如果处理失败的时候,一般情况下会把资源的名字重新加入到工作队列中,从而方便之后进行重试。

总结

  • Kubernetes 控制器模式依赖声明式的 API,而不是命令式API

    • 命令式 API 就是通过向系统发出明确的操作来执行的

      • 最大问题:错误处理无法很好得到解决

      • 在大规模的分布式系统中,错误是无处不在的。一旦发出的命令没有响应,调用方只能通过反复重试的方式来试图恢复错误,然而盲目的重试可能会带来更大的问题

      • 命令式 API 在处理多并发访问时,也很容易出现问题

      • 假如有多方并发的对一个资源请求进行操作,并且一旦其中有操作出现了错误,就需要重试。那么最后哪一个操作生效了,就很难确认,也无法保证。

    • 声明式交互方式,就是老板对自己员工的交流方式。老板通过给员工设置可量化的业务目标的方式,来发挥员工自身的主观能动性。指出目标,交给下层处理完成

      • 声明式 API 系统里天然地记录了系统现在和最终的状态

      • 不需要额外的操作数据。另外因为状态的幂等性,可以在任意时刻反复操作。在声明式系统运行的方式里,正常的操作实际上就是对资源状态的巡检,不需要额外开发巡检系统,系统的运行逻辑也能够在日常的运行中得到测试和锤炼,因此整个操作的稳定性能够得到保证。

      • 最后,因为资源的最终状态是明确的,我们可以合并多次对状态的修改。可以不需要加锁,就支持多方的并发访问

  1. Kubernetes 所采用的控制器模式,是由声明式 API 驱动的。确切来说,是基于对 Kubernetes 资源对象的修改来驱动的;
  2. Kubernetes 资源之后,是关注该资源的控制器。这些控制器将异步的控制系统向设置的终态驱近
  3. 这些控制器是自主运行的,使得系统的自动化和无人值守成为可能;
  4. 因为 Kubernetes 的控制器和资源都是可以自定义的,因此可以方便的扩展控制器模式。特别是对于有状态应用,我们往往通过自定义资源和控制器的方式,来自动化运维操作。这个也就是后续会介绍的 operator 的场景

Deployment

作用

  • Deployment 定义了一种 Pod 期望数量,controller 就会持续维持 Pod 数量为期望的数量。
  • 配置 Pod 发布方式,也就是说 controller 会按照用户给定的策略来更新 Pod,而且更新过程中,也可以设定不可用 Pod 数量在多少范围内;
  • 如果更新过程中发生问题的话,可以“一键”回滚。

语法定义

“apiVersion:apps/v1”,也就是说 Deployment 当前所属的组是 apps,版本是 v1。“metadata”是我们看到的 Deployment 元信息,也就是往期回顾中的 Labels、Selector、Pod.image,这些都是在往期中提到的知识点。

Deployment 作为一个 K8s 资源,它有自己的 metadata 元信息,这里我们定义的 Deployment.name 是 nginx.DeploymentDeployment.spec 中首先要有一个核心的字段,即 replicas,这里定义期望的 Pod 数量为三个selector 其实是 Pod 选择器,那么所有扩容出来的 Pod,它的 Labels 必须匹配 selector 层上的 image.labels,也就是 app.nginx。

就如上面的 Pod 模板 template 中所述,这个 template 它其实包含了两部分内容:

  • 一部分是我们期望 Pod 的 metadata,其中包含了 labels,即跟 selector.matchLabels 相匹配的一个 Labels;

  • 第二部分是 template 包含的一个 **Pod.spec。**这里 Pod.spec 其实是 Deployment 最终创建出来 Pod 的时候,它所用的 Pod.spec,这里定义了一个 container.nginx,它的镜像版本是 nginx:1.7.9。

下面是遇到的新知识点:

  • 第一个是 replicas,就是 Deployment 中期望的或者终态数量;
  • 第二个是 template,也就是 Pod 相关的一个模板。

状态转变

  • Processing 指的是 Deployment 正在处于扩容和发布中。比如说 Processing 状态的 deployment,它所有的 replicas 及 Pod 副本全部达到最新版本,而且是 available,这样的话,就可以进入 complete 状态。而 complete 状态如果发生了一些扩缩容的话,也会进入 processing 这个处理工作状态。
  • 比如说拉镜像失败了,或者说 readiness probe 检查失败了,就会进入 failed 状态;如果在运行过程中即 complete 状态,中间运行时发生了一些 pod readiness probe 检查失败,这个时候 deployment 也会进入 failed 状态。进入 failed 状态之后,除非所有点 replicas 均变成 available,而且是 updated 最新版本,deployment 才会重新进入 complete 状态。

PS:

  • LivenessProbe:用于判断容器是否存活(running状态),如果LivenessProbe探针探测到容器不健康,则kubelet杀掉该容器,并根据容器的重启策略做相应的处理。如果一个容器不包含LivenessProbe探针,则kubelet认为该容器的LivenessProbe探针返回的值永远是“Success”。

  • LivenessProbe三种实现方式:

    1. HTTP GET探针对容器的ip地址(指定端口和路径)执行HTTP GET请求。响应状态码是2xx或3xx则探测成功。

    2. TCP套接字探针尝试建立TCP连接,成功建立则成功。

    3. Exec探针,在容器内执行任意命令,检测命令的退出状态码,是0则成功,其他失败。

  • ReadinessProbe:用于判断容器是否启动完成(ready状态),可以接收请求。如果ReadinessProbe探针检测到失败,则Pod的状态被修改。Endpoint Controller将从Service的Endpoint中删除包含该容器所在Pod的Endpoint。

结构设计

  • Deployment 只负责管理不同版本的 ReplicaSet,由 ReplicaSet 来管理具体的 Pod 副本数,每个 ReplicaSet 对应 Deployment template 的一个版本。在上文的例子中可以看到,每一次修改 template,都会生成一个新的 ReplicaSet,这个 ReplicaSet 底下的 Pod 其实都是相同的版本。

  • 控制器都是通过 Informer 中的 Event 做一些 Handler 和 Watch。 Deployment 控制器,其实是关注 Deployment 和 ReplicaSet 中的 event,收到事件后会加入到队列中。
  • 而 Deployment controller 从队列中取出来之后,它的逻辑会判断 Check Paused,这个 Paused 其实是 Deployment 是否需要新的发布,如果 Paused 设置为 true 的话,就表示这个 Deployment 只会做一个数量上的维持,不会做新的发布
  • 如果 Check paused 为 Yes 也就是 true 的话,那么只会做 Sync replicas。也就是说把 replicas sync 同步到对应的 ReplicaSet 中,最后再 Update Deployment status,那么 controller 这一次的 ReplicaSet 就结束了。
  • 那么如果 paused 为 false 的话,它就会做 Rollout,也就是通过 Create 或者是 Rolling 的方式来做更新,更新的方式其实也是通过 Create/Update/Delete 这种 ReplicaSet 来做实现的

  • Deployment 分配 ReplicaSet 之后,ReplicaSet 控制器本身也是从 Informer 中 watch 一些事件,这些事件包含了 ReplicaSet 和 Pod 的事件。从队列中取出之后,ReplicaSet controller 的逻辑很简单,就只管理副本数。也就是说如果 controller 发现 replicas 比 Pod 数量大的话,就会扩容,而如果发现实际数量超过期望数量的话,就会删除 Pod。

Deployment 控制器的图中可以看到,Deployment 控制器其实是一个上层管理,做更复杂的事情,包含了版本管理,而它把每一个版本下的数量维持工作交给 ReplicaSet 来做。(AC: 很符合那个声明式API做的事情,只做抽象式管理,而不是具体做啥)

deployment rollout是创建新的pod,使用以前的replicasets的hash,但不是使用以前旧的容器

Sepc字段解析

  • MinReadySeconds:Deployment 会根据 Pod ready 来看 Pod 是否可用,但是如果我们设置了 MinReadySeconds 之后,比如设置为 30 秒,那 Deployment 就一定会等到 Pod ready 超过 30 秒之后才认为 Pod 是 available 的。Pod available 的前提条件是 Pod ready,但是 ready 的 Pod 不一定是 available 的,它一定要超过 MinReadySeconds 之后,才会判断为 available;

  • revisionHistoryLimit:保留历史 revision,即保留历史 ReplicaSet 的数量,默认值为 10 个。这里可以设置为一个或两个,如果回滚可能性比较大的话,可以设置数量超过 10;

  • paused:paused 是标识,Deployment 只做数量维持,不做新的发布,这里在 Debug 场景可能会用到;

  • progressDeadlineSeconds:前面提到当 Deployment 处于扩容或者发布状态时,它的 condition 会处于一个 processing 的状态,processing 可以设置一个超时时间。如果超过超时时间还处于 processing,那么 controller 将认为这个 Pod 会进入 failed 的状态。

  • MaxUnavailable:滚动过程中最多有多少个 Pod 不可用;

  • MaxSurge:滚动过程中最多存在多少个 Pod 超过预期 replicas 数量

    • 默认25%,Deployment 在发布的过程中,可能有 25% 的 replica 是不可用的,也可能超过 replica 数量 25% 是可用的,最高可以达到 125% 的 replica 数量
    • 当用户的资源足够,且更注重发布过程中的可用性,可设置 MaxUnavailable 较小、MaxSurge 较大。
    • 用户的资源比较紧张,可以设置 MaxSurge 较小,甚至设置为 0
    • MaxSurge 和 MaxUnavailable 不能同时为 0。当 MaxSurge 为 0 的时候,必须要删除 Pod,才能扩容 Pod;两者同时为 0 的话,MaxSurge 保证不能新扩 Pod,而 MaxUnavailable 不能保证 ReplicaSet 中有 Pod 是 available 的。

总结

  • Deployment 是 Kubernetes 中常见的一种 Workload,支持部署管理多版本的 Pod;
  • Deployment 管理多版本的方式,是针对每个版本的 template 创建一个 ReplicaSet,由 ReplicaSet 维护一定数量的 Pod 副本,而 Deployment 只需要关心不同版本的 ReplicaSet 里要指定多少数量的 Pod;
  • 因此,Deployment 发布部署的根本原理,就是 Deployment 调整不同版本 ReplicaSet 里的终态副本数,以此来达到多版本 Pod 的升级和回滚。

Job CronJobs DaemonSet

job:管理任务的控制器

job 功能

  • 首先 kubernetes 的 Job 是一个管理任务的控制器,它可以创建一个或多个 Pod 来指定 Pod 的数量,并可以监控它是否成功地运行或终止;(AC : 管理的还是pod)
  • 我们可以根据 Pod 的状态来给 Job 设置重置的方式及重试的次数;(AC: 保证进程运行失败后重试)
  • 我们还可以根据依赖关系,保证上一个任务运行完成之后再运行下一个任务;(AC: 管理多个有依赖性任务)
  • 同时还可以控制任务的并行度,根据并行度来确保 Pod 运行过程中的并行次数和总体完成大小。(AC: 控制并行并管理任务队列大小)

job 语法

  • restartPolicy,在 Job 里面我们可以设置 Never、OnFailure、Always 这三种重试策略。在希望 Job 不需要重新运行的时候,我们可以用 Never;希望在失败的时候再运行,再重试可以用 OnFailure;或者不论什么情况下都重新运行时 Alway;
  • backoffLimit 就是来保证一个 Job 到底能重试多少次。

job 状态

kubectl get jobs
  • AGE 的含义是指这个 Pod 从当前时间算起,减去它当时创建的时间。这个时长主要用来告诉你 Pod 的历史、Pod 距今创建了多长时间
  • DURATION 主要来看我们 Job 里面的实际业务到底运行了多长时间,当我们的性能调优的时候,这个参数会非常的有用。
  • COMPLETIONS 主要来看我们任务里面这个 Pod 一共有几个,然后它其中完成了多少个状态,会在这个字段里面做显示。

并行运行 Job

我们有时候有些需求:希望 Job 运行的时候可以最大化的并行,并行出 n 个 Pod 去快速地执行。同时,由于我们的节点数有限制,可能也不希望同时并行的 Pod 数过多,有那么一个管道的概念,我们可以希望最大的并行度是多少,Job 控制器都可以帮我们来做到。

  • completions。指定本 Pod 队列执行次数,也就是任务运行的总数
  • parallelism。代表这个并行执行的个数,其实就是一个管道或者缓冲器中缓冲队列的大小。

CronJob:定时运行 Job

CronJob 语法

  • schedule:schedule 这个字段主要是设置时间格式,它的时间格式和 Linux 的 crontime 是一样的,所以直接根据 Linux 的 crontime 书写格式来书写就可以了。
  • **startingDeadlineSeconds:**即:每次运行 Job 的时候,它最长可以等多长时间,有时这个 Job 可能运行很长时间也不会启动。所以这时,如果超过较长时间的话,CronJob 就会停止这个 Job;
  • concurrencyPolicy:就是说是否允许并行运行。所谓的并行运行就是,比如说我每分钟执行一次,但是这个 Job 可能运行的时间特别长,假如两分钟才能运行成功,也就是第二个 Job 要到时间需要去运行的时候,上一个 Job 还没完成。如果这个 policy 设置为 true 的话,那么不管你前面的 Job 是否运行完成,每分钟都会去执行;如果是 false,它就会等上一个 Job 运行完成之后才会运行下一个;
  • JobsHistoryLimit:这个就是每一次 CronJob 运行完之后,它都会遗留上一个 Job 的运行历史、查看时间。当然这个额不能是无限的,所以需要设置一下历史存留数,一般可以设置默认 10 个或 100 个都可以,这主要取决于每个人集群不同,然后根据每个人的集群数来确定这个时间。

job 架构设计

  • Job Controller 其实还是主要去创建相对应的 pod,然后 Job Controller 会去跟踪 Job 的状态,及时地根据我们提交的一些配置重试或者继续创建。同时我们刚刚也提到,每个 pod 会有它对应的 label,来跟踪它所属的 Job Controller,并且还去配置并行的创建, 并行或者串行地去创建 pod。(AC:直接作用在Pod上,而不是像Deployment管理deplicasets)

  • 所有的 job 都是一个 controller,它会 watch 这个 API Server,我们每次提交一个 Job 的 yaml 都会经过 api-server 传到 ETCD 里面去,然后 Job Controller 会注册几个 Handler,每当有添加、更新、删除等操作的时候,它会通过一个内存级的消息队列,发到 controller 里面
  • 通过 Job Controller 检查当前是否有运行的 pod,如果没有的话,通过 Scale up 把这个 pod 创建出来;如果有的话,或者如果大于这个数,对它进行 Scale down,如果这时 pod 发生了变化,需要及时 Update 它的状态。
  • 同时要去检查它是否是并行的 job,或者是串行的 job,根据设置的配置并行度、串行度,及时地把 pod 的数量给创建出来。最后,它会把 job 的整个的状态更新到 API Server 里面去,这样我们就能看到呈现出来的最终效果了。

(AC: 所有的操作都是通过API Server去进行完成的,API Server 去控制kubelet,scheduler去确定Pod的创建和位置)

DaemonSet:守护进程控制器

DaemonSet 也是 Kubernetes 提供的一个 default controller,它实际是做一个守护进程的控制器,它能帮我们做到以下几件事情:

  • 首先能保证集群内的每一个节点都运行一组相同的 pod
  • 同时还能根据节点的状态保证新加入的节点自动创建对应的 pod;(新节点感知并初始化)
  • 移除节点的时候,能删除对应的 pod;(节点推出,对应Pod删除)
  • 而且它会跟踪每个 pod 的状态,当这个 pod 出现异常、Crash 掉了,会及时地去 recovery 这个状态(Pod异常监控)

DaemonSet 语法

DaemonSet 作用

  • 首先是存储,GlusterFS 或者 Ceph 之类的东西,需要每台节点上都运行一个类似于 Agent 的东西,DaemonSet 就能很好地满足这个诉求;

  • 另外,对于日志收集,比如说 logstash 或者 fluentd,这些都是同样的需求,需要每台节点都运行一个 Agent,这样的话,我们可以很容易搜集到它的状态,把各个节点里面的信息及时地汇报到上面

  • 还有一个就是,需要每个节点去运行一些监控的事情,也需要每个节点去运行同样的事情,比如说 Promethues 这些东西,也需要 DaemonSet 的支持。

管理的还是Pod

DaemonSet 状态

需要的 pod 个数、当前已经创建的 **pod 个数、就绪的个数,以及所有可用的、通过健康检查的 pod;**还有 NODE SELECTOR,因为 NODE SELECTOR 在 DaemonSet 里面非常有用。有时候我们可能希望只有部分节点去运行这个 pod 而不是所有的节点,所以有些节点上被打了标的话,DaemonSet 就只运行在这些节点上。

DaemonSet 和 deployment 特别像,它也有两种更新策略:一个是 RollingUpdate,另一个是 OnDelete

  • RollingUpdate 其实比较好理解,就是会一个一个的更新。先更新第一个 pod,然后老的 pod 被移除,通过健康检查之后再去见第二个 pod,这样对于业务上来说会比较平滑地升级,不会中断;

  • OnDelete 其实也是一个很好的更新策略,就是模板更新之后,pod 不会有任何变化,需要我们手动控制手动去删除某一个节点对应的 pod,它才会重建,不删除的话它就不会重建,这样的话对于一些我们需要手动控制的特殊需求也会有特别好的作用。

DaemonSet架构设计

DaemonSet 还是一个 controller,它最后真正的业务单元也是 Pod,DaemonSet 其实和 Job controller 特别相似,它也是通过 controller 去 watch API Server 的状态,然后及时地添加 pod。唯一不同的是,它会监控节点的状态,节点新加入或者消失的时候会在节点上创建对应的 pod,然后同时根据你配置的一些 affinity 或者 label 去选择对应的节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uLdp8Tpt-1623207610990)(云原生.assets/image-20210603101439801.png)]

  • DaemonSet 其实和 Job controller 做的差不多:两者都需要根据 watch 这个 API Server 的状态。现在 DaemonSet 和 Job controller 唯一的不同点在于,DaemonsetSet Controller需要去 watch node 的状态,但其实这个 node 的状态还是通过 API Server 传递到 ETCD 上。
  • 当有 node 状态节点发生变化时,它会通过一个内存消息队列发进来,然后DaemonSet controller 会去 watch 这个状态,看一下各个节点上是都有对应的 Pod,如果没有的话就去创建。如果有的话,它会比较一下版本,是否去做 RollingUpdate?Ondelete 删除 pod 的时候也会去做 check 它做一遍检查,是否去更新,或者去创建对应的 pod。

PS

  • DaemonSet的restartPolicy 只有 Always(也是默认重启策略), Job 只有 never 和 Onfailure,因为DaemonSet需要一直运行下去,不能停止的。
  • 而Job则是有一些离线计算的pod,完成后不需要重启,或者只有失败才重启,要不然重复计算没有意义。 Onfailure 下 重启次数可以通过spec.backoffLimit 字段控制,而spec.activeDeadlineSeconds 字段可以设置最长运行时间,避免jod一直不肯结束。

应用配置管理

背景

启动容器需要解决的一些配套问题:

  1. 可变配置问题。不可能把可变的配置写到镜像中,如果发生变化,肯定不能重新编译镜像;
  2. 敏感信息的存储和使用;
  3. 容器访问集群本身,需要的一个身份认证问题;
  4. 容器再节点上运行之后的资源需求;
  5. 容器的安全管控
  6. 容器启动前的额一个前置条件的检验。

配置管理

为解决上面的问题,对Pod的我们有对应的配置管理。

  • 可变配置就用 ConfigMap;
  • 敏感信息是用 Secret;
  • 身份认证是用 ServiceAccount 这几个独立的资源来实现的;
  • 资源配置是用 Resources;
  • 安全管控是用 SecurityContext;
  • 前置校验是用 InitContainers 这几个在 spec 里面加的字段,来实现的这些配置管理。

ConfigMap

定义

  • 可以让一些可变配置和容器镜像进行解耦,这样也保证了容器的可移植性

  • 主要是管理一些可变配置信息,比如说应用的一些配置文件,或者说它里面的一些环境变量,或者一些命令行参数

  • ConfigMap 定义包括两个部分:一个是 ConfigMap 元信息,我们关注 name 和 namespace 这两个信息;另外一个是data信息,管理了配置文件,key:value形式存储,key 是一个文件名,value 是这个文件的内容

创建

kubectl 这个命令来创建,它带的参数主要有两个:一个是指定 name,第二个是 DATA。其中 DATA 可以通过指定文件或者指定目录,以及直接指定键值对。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ubyobxg8-1623207610991)(云原生.assets/image-20210603230823061.png)]

使用

  • 第一种是环境变量。环境变量的话通过 valueFrom,然后 ConfigMapKeyRef 这个字段,下面的 name 是指定 ConfigMap 名,key 是 ConfigMap.data 里面的 key。这样的话,在 busybox 容器启动后容器中执行 env 将看到一个 SPECIAL_LEVEL_KEY 环境变量;
  • 第二个是命令行参数。命令行参数其实是第一行的环境变量直接拿到 cmd 这个字段里面来用;
  • 最后一个是通过 volume 挂载的方式直接挂到容器的某一个目录下面去。上面的例子是把 special-config 这个 ConfigMap 里面的内容挂到容器里面的 /etc/config 目录下,这个也是使用的一种方式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vk12eQEi-1623207610991)(云原生.assets/image-20210603231021675.png)]

注意点

  1. ConfigMap 文件的大小。虽然说 ConfigMap 文件没有大小限制,但是在 ETCD 里面,数据的写入是有大小限制的,现在是限制在 1MB 以内;
  2. 第二个注意点是 pod 引入 ConfigMap 的时候,必须是相同的 Namespace 中的 ConfigMap,前面其实可以看到,ConfigMap.metadata 里面是有 namespace 字段的;
  3. 第三个是 pod 引用的 ConfigMap。假如这个 ConfigMap 不存在,那么这个 pod 是无法创建成功的,其实这也表示在创建 pod 前,必须先把要引用的 ConfigMap 创建好
  4. 第四点就是使用 envFrom 的方式。把 ConfigMap 里面所有的信息导入成环境变量时,如果 ConfigMap 里有些 key 是无效的,比如 key 的名字里面带有数字,那么这个环境变量其实是不会注入容器的,它会被忽略。但是这个 pod 本身是可以创建的。这个和第三点是不一样的方式,是 ConfigMap 文件存在基础上,整体导入成环境变量的一种形式;
  5. 最后一点是:什么样的 pod 才能使用 ConfigMap?这里只有通过 K8s api 创建的 pod 才能使用 ConfigMap,比如说通过用命令行 kubectl 来创建的 pod,肯定是可以使用 ConfigMap 的,但其他方式创建的 pod,比如说 kubelet 通过 manifest 创建的 static pod,它是不能使用 ConfigMap 的。

PS:大小限制,namespace限制,存在限制,envFrom key的限制,k8s创建pod使用限制

Secret

定义

Secret 是一个主要用来存储密码 token 等一些敏感信息的资源对象。其中,敏感信息是采用 base-64 编码保存起来的,我们来看下图中 Secret 数据的定义。

  • 元数据的话,里面主要是 name、namespace 两个字段;接下来是 type,它是非常重要的一个字段,是指 Secret 的一个类型。
  • Secret 类型种类比较多,下面列了常用的四种类型:
    • 第一种是 Opaque,它是普通的 Secret 文件
    • 第二种是 service-account-token,是用于 service-account 身份认证用的 Secret
    • 第三种是 dockerconfigjson,这是拉取私有仓库镜像的用的一种 Secret;
    • 第四种是 bootstrap.token,是用于节点接入集群校验用的 Secret。
  • data,是存储的 Secret 的数据,它也是 key-value 的形式存储的。

创建

两种创建方式:

  1. 系统创建:比如 K8s 为每一个 namespace 的默认用户(default ServiceAccount)创建 Secret
  2. 用户手动创建:手动创建命令,推荐 kubectl 这个命令行工具,它相对 ConfigMap 会多一个 type 参数。其中 data 也是一样,它也是可以指定文件和键值对的。type 的话,要是你不指定的话,默认是 Opaque 类型

使用

  • 挂载到用户指定目录

    • 第一种方式:如上图左侧所示,用户直接指定,把 mysecret 挂载到容器 /etc/foo 目录下面;
    • 第二种方式:如上图右侧所示,系统自动生成,把 serviceaccount-secret 自动挂载到容器 /var/run/secrets/kubernetes.io/serviceaccount 目录下,它会生成两个文件,一个是 ca.crt,一个是 token。这是两个保存了认证信息的证书文件

  • 使用私有镜像库

  • 第一种方式:如上图左侧所示,直接在 pod 里面,通过 imagePullSecrets 字段来配置;

  • 第二种方式是自动注入。用户提前在 pod 会使用的 serviceaccount 里配置 imagePullSecrets,Pod创建时系统自动注入这个 imagePullSecrets。

注意点

  1. 第一个是 Secret 的文件大小限制。这个跟 ConfigMap 一样,也是 1MB

  2. 第二个是 Secret 采用了 base-64 编码,但是它跟明文也没有太大区别。所以说,如果有一些机密信息要用 Secret 来存储的话,还是要很慎重考虑。也就是说谁会来访问你这个集群,谁会来用你这个 Secret,还是要慎重考虑,因为它如果能够访问这个集群,就能拿到这个 Secret。

    如果是对 Secret 敏感信息要求很高,对加密这块有很强的需求,推荐可以使用 Kubernetes 和开源的 vault做一个解决方案,来解决敏感信息的加密和权限管理。

  3. 第三个就是 Secret 读取的最佳实践,建议不要用 list/watch,如果用 list/watch 操作的话,会把 namespace 下的所有 Secret 全部拉取下来,这样其实暴露了更多的信息。推荐使用 GET 的方法,这样只获取你自己需要的那个 Secret

ServiceAccount

定义

ServiceAccount 首先是用于解决 pod 在集群里面的身份认证问题,身份认证信息是存在于 Secret 里面,所以说是Secret的进一步包装?

  • 左边有一个 Secret 字段,它指定 ServiceAccount 用哪一个 Secret,这个是 K8s 自动为 ServiceAccount 加上的。
  • 右边中的 Secret 的 data 里有两块数据,一个是 ca.crt,一个是 token。ca.crt 用于对服务端的校验,token 用于 Pod 的身份认证,它们都是用 base64 编码过的。
  • metadata有关联 ServiceAccount 信息的(这个 secret 被哪个 ServiceAccount 使用)。
  • type,这个就是 service-account-token 这种类型。

创建

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ou2c2G4p-1623207610992)(云原生.assets/image-20210603233335550.png)]

Resource

定义

  • 容器的一个资源配置管理,包含CPU、内存,以及临时存储三种。此外,用户还可以自定义externResource,但配置时,指定的数量必须为整数。
  • 目前资源配置主要分成 request 和 limit 两种类型,一个是需要的数量,一个是资源的界限。CPU、内存以及临时存储都是在 container 下的 Resource 字段里进行一个声明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PqMTcYpK-1623207610992)(云原生.assets/image-20210603233555655.png)]

Pod 服务质量 (QoS) 配置

根据 CPU 对容器内存资源的需求,我们对 pod 的服务质量进行一个分类,分别是 Guaranteed、Burstable 和 BestEffort。

  • Guaranteed :pod 里面每个容器都必须有内存和 CPU 的 request 以及 limit 的一个声明,且 request 和 limit 必须是一样的,这就是 Guaranteed;
  • Burstable:Burstable 至少有一个容器存在内存和 CPU 的一个 request
  • BestEffort:只要不是 Guaranteed 和 Burstable,那就是 BestEffort。

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 状态)

SecurityContext

定义

用于限制容器的一个行为,它能保证系统和其他容器的安全

这一块的能力不是 Kubernetes 或者容器 runtime 本身的能力,而是 Kubernetes 和 runtime 通过用户的配置,最后下传到内核里,再通过内核的机制让 SecurityContext 来生效

SecurityContext 主要分为三个级别:

  • 第一个是容器级别,仅对容器生效;
  • 第二个是 pod 级别,对 pod 里所有容器生效;
  • 第三个是集群级别,就是 PSP,对集群内所有 pod 生效。

权限和访问控制设置项,现在一共列有七项(这个数量后续可能会变化):

  • 第一个就是通过用户 ID 和组 ID 来控制文件访问权限;
  • 第二个是 SELinux,它是通过策略配置来控制用户或者进程对文件的访问控制;
  • 第三个是特权容器
  • 第四个是 Capabilities,它也是给特定进程来配置一个 privileged 能力;
  • 第五个是 AppArmor,它也是通过一些配置文件来控制可执行文件的一个访问控制权限,比如说一些端口的读写;
  • 第六个是一个对系统调用的控制;
  • 第七个是对子进程能否获取比父亲更多的权限的一个限制。

InitContainer

InitContainer 和普通 container 的区别,有以下三点内容:

  1. InitContainer 首先会比普通 container 先启动,并且直到所有的 InitContainer 执行成功后,普通 container 才会被启动;(优先启动性
  2. InitContainer 之间是按定义的次序去启动执行的,执行成功一个之后再执行第二个,而普通的 container 是并发启动的;(有序性
  3. InitContainer 执行成功后就结束退出,而普通容器可能会一直在执行。它可能是一个 longtime 的,或者说失败了会重启,这个也是 InitContainer 和普通 container 不同的地方。(一次性
  4. Pod重启后,InitContainer 也会再次启动执行

作用

为普通 container 服务,比如说它可以为普通 container 启动之前做一个初始化,或者为它准备一些配置文件, 配置文件可能是一些变化的东西。再比如做一些前置条件的校验,如网络是否联通

Volumes

背景和类型

应用场景

  • 场景一:如果 pod 中的某一个容器在运行时异常退出,被 kubelet 重新拉起之后,如何保证之前容器产生的重要数据没有丢失?

  • 场景二:如果同一个 pod 中的多个容器想要共享数据,应该如何去做?

解决办法:

  • Volumes的应用

Volumes类型有

  1. 本地存储,常用的有 emptydir/hostpath
  2. 网络存储:网络存储当前的实现方式有两种,
    • 一种是 in-tree,它的实现的代码是放在 K8s 代码仓库中的,随着k8s对存储类型支持的增多,这种方式会给k8s本身的维护和发展带来很大的负担;
    • 而第二种实现方式是 out-of-tree,它的实现其实是给 K8s 本身解耦的,通过抽象接口将不同存储的driver实现从k8s代码仓库中剥离,因此out-of-tree 是后面社区主推的一种实现网络存储插件的方式;
  3. Projected Volumes:它其实是将一些配置信息,如 secret/configmap 用卷的形式挂载在容器中,让容器中的程序可以通过POSIX接口(可移植操作系统接口)来访问配置数据;
  4. PV (Persistent Volumes)与 PVC(Persistent Volumes Claims)。

PV 与 PVC

引入原因
  • Pod Volumes 很难准确地表达它的复用/共享语义,对它的扩展也比较困难,在以下场景可以体现:
  • Pod 重建销毁,如用 Deployment 管理的 pod,在做镜像升级的过程中,会产生新的 pod并且删除旧的 pod ,那新旧 pod 之间如何复用数据
  • 宿主机宕机的时候,要把上面的 pod 迁移,这个时候 StatefulSet 管理的 pod,其实已经实现了带卷迁移的语义。这时通过 Pod Volumes 显然是做不到的
  • 多个Pod共享同一个数据volume情况
  • 对数据卷进行一些功能性的拓展实现,如snapshot、resieze等功能情况
  • K8s 中又引入了 Persistent Volumes 概念,它可以将存储和计算分离,通过不同的组件来管理存储资源和计算资源,然后解耦 pod 和 Volume 之间生命周期的关联
PVC设计
  • PVC的设计主要是为了简化K8s用户对存储的使用方式,做到职责分离。通常用户在使用存储的时候,只用声明所需的存储大小以及访问模式
  • 访问模式主要指:使用的存储是可以被多个node共享还是只能单node独占访问(注意是node level而不是pod level)?只读还是读写访问?用户只用关心这些东西,与存储相关的实现细节是不需要关心的。(AC:一种声明式指导思想吧)
  • PVC 和 PV 的概念,将用户需求和实现细节解耦开,用户只用通过 PVC 声明自己的存储需求PV是有集群管理员和存储相关团队来统一运维和管控,这样的话,就简化了用户使用存储的方式。可以看到,PV 和 PVC 的设计其实有点像面向对象的接口与实现的关系。用户在使用功能时,只需关心用户接口,不需关心它内部复杂的实现细节。
PV的产生

Static Volume Provisioning

  • 静态 Provisioning:由集群管理员事先去规划这个集群中的用户会怎样使用存储,它会先预分配一些存储,也就是预先创建一些 PV;然后用户在提交自己的存储需求(也就是 PVC)的时候,K8s 内部相关组件会帮助它把 PVC 和 PV 做绑定;之后用户再通过 pod 去使用存储的时候,就可以通过 PVC 找到相应的 PV,它就可以使用了
  • 不足之处:预分配其实是很难预测用户真实需求的

Dynamic Volume Provisioning

  • 集群管理员不预分配 PV,他写了一个模板文件,这个模板文件是用来表示创建某一类型存储(块存储,文件存储等)所需的一些参数,这些参数是用户不关心的,给存储本身实现有关的参数。用户只需要提交自身的存储需求,也就是PVC文件,并在 PVC 中指定使用的存储模板(StorageClass
  • K8s 集群中的管控组件,会结合 PVC 和 StorageClass 的信息动态,生成用户所需要的存储(PV),将 PVC 和 PV 进行绑定后,pod 就可以使用 PV 了。通过 StorageClass 配置生成存储所需要的存储模板,再结合用户的需求动态创建 PV 对象,做到按需分配,在没有增加用户使用难度的同时也解放了集群管理员的运维工作。
PV PVC StorageClass使用

静态

动态

第一个是 provisioner,provisioner 是什么?它其实就是说我当时创建 PV 和对应的存储的时候,应该用哪个存储插件来去创建。

这些参数是通过k8s创建存储的时候,需要指定的一些细节参数。对于这些参数,用户是不需要关心的,像这里 regionld、zoneld、fsType 和它的类型。ReclaimPolicy跟我们刚才讲解的 PV 里的意思是一样的,就是说动态创建出来的这块 PV,当使用方使用结束、Pod 及 PVC 被删除后,这块 PV 应该怎么处理,我们这个地方写的是 delete,意思就是说当使用方 pod 和 PVC 被删除之后,这个 PV 也会被删除掉。

VC 的文件里存储的大小、访问模式是不变的。现在需要新加一个字段,叫 StorageClassName,它的意思是指定动态创建PV的模板文件的名字,这里StorageClassName填的就是上面声明的csi-disk。

在提交完 PVC之后,K8s 集群中的相关组件就会根据 PVC 以及对应的 StorageClass 动态生成这块 PV 给这个 PVC 做一个绑定,之后用户在提交自己的 yaml 时,用法和接下来的流程和前面的静态使用方式是一样的,通过 PVC 找到我们动态创建的 PV,然后把它挂载到相应的容器中就可以使用了

PV Spec 重要字段

  • Capacity:这个很好理解,就是存储对象的大小;

  • **AccessModes:**也是用户需要关心的,就是说我使用这个 PV 的方式。它有三种使用方式。

    • ReadWriteOnce: 单 node 读写访问;
    • ReadOnlyMany : 多个 node 只读访问,是常见的一种数据的共享方式;
    • WriteOnlyMany: 是多个 node 上读写访问。
  • 这两个字段 —— Capacity 和 AccessMode决定在提交 PVC 后,k8s 集群中的相关组件是如何去找到合适的 PV 。

    • 首先它是通过为 PV 建立的 AccessModes 索引找到所有能够满足用户的 PVC 里面的 AccessModes 要求的 PV list
    • 然后根据PVC的 CapacityStorageClassName, Label Selector 进一步筛选 PV,
    • 如果满足条件的 PV 有多个,选择 PV 的 size 最小的,accessmodes 列表最短的 PV,也即最小适合原则
  • ReclaimPolicy:这个就是刚才提到的,我的用户方 PV 的 PVC 在删除之后,我的 PV 应该做如何处理?常见的有三种方式。

    • Recycle: 第一种方式我们就不说了,现在 K8s 中已经不推荐使用了;
    • Delete: 也就是说 PVC 被删除之后,PV 也会被删除;
    • Retain: 就是保留,保留之后,后面这个 PV 需要管理员来手动处理。
  • StorageClassName:PVC可以通过这个字段找到相同值的PV(静态Provisioning), 也可以通过这个字段对应的storageClass动态Provisioning新的PV对象

  • NodeAffinity:限制可以访问该volume的nodes,影响调度

PV状态流转

  • 首先在创建 PV 对象后,它会处在短暂的pending 状态;等真正的 PV 创建好之后,它就处在 available 状态。
  • available 状态意思就是可以使用的状态,用户在提交 PVC 之后,被 K8s 相关组件做完 bound(即:找到相应的 PV),这个时候 PV 和 PVC 就结合到一起了,此时两者都处在 bound 状态。
  • 当用户在使用完 PVC,将其删除后,这个 PV 就处在 released 状态,根据ReclaimPolicy来删除还是被保留
  • 特别地,当 PV 已经处在 released 状态下,它是没有办法直接回到 available 状态,也就是说接下来无法被一个新的 PVC 去做绑定。如果我们想把已经 released 的 PV 复用,可以用以下方法:
    1. 我们可以新建一个 PV 对象,然后把之前的 released 的 PV 的相关字段的信息填到新的 PV 对象里面,这样的话,这个 PV 就可以结合新的 PVC 了
    2. 我们在删除 pod 之后,不要去删除 PVC 对象,这样给 PV 绑定的 PVC 还是存在的,下次 pod 使用的时候,就可以直接通过 PVC 去复用。K8s中的 StatefulSet 管理的 Pod 带存储的迁移就是通过这种方式。
架构设计

csi 的全称是 container storage interface,它是K8s社区后面对存储插件实现(out of tree)的官方推荐方式。csi 的实现大体可以分为两部分:

  • 第一部分是由k8s社区驱动实现的通用的部分,像我们这张图中的 csi-provisioner和 csi-attacher controller;
  • 另外一种是由云存储厂商实践的,对接云存储厂商的 OpenApi,主要是实现真正的 create/delete/mount/unmount 存储的相关操作,对应到上图中的csi-controller-server和csi-node-server。

流程图:

  • PV对象创建:用户提交 PVCyaml ,首先会在集群中生成一个 PVC 对象,然后 PVC 对象会被 csi-provisioner controller watch到,csi-provisioner 会结合 PVC 对象以及 PVC 对象中声明的 storageClass,通过 GRPC 调用 csi-controller-server,然后,到云存储服务这边去创建真正的存储,并最终创建出来 PV 对象。
  • Pod创建:用户在提交 pod 之后,首先会被调度器调度选中某一个合适的node,之后该 node 上面的 kubelet 在创建 pod 流程中会通过首先 csi-node-server 将我们之前创建的 PV 挂载到我们 pod 可以使用的路径,然后 kubelet 开始 create && start pod 中的所有 container。

使用流程图,主要分为三个阶段:

  • Create阶段: 用户提交完 PVC,由 csi-provisioner 创建存储,并生成 PV 对象,之后 PV controller 将 PVC 及生成的 PV 对象做 bound,bound 之后,create 阶段就完成了;
  • attach阶段: 之后用户在提交 pod yaml 的时候,首先会被调度选中某一个 合适的node,等 pod 的运行 node 被选出来之后,会被 AD Controller watch 到 pod 选中的 node,它会去查找 pod 中使用了哪些 PV。然后它会生成一个内部的对象叫 VolumeAttachment 对象,从而去触发 csi-attacher去调用csi-controller-server 去做真正的 attache 操作,attach操作调到云存储厂商OpenAPI。这个 attach 操作就是将存储 attach到 pod 将会运行的 node 上面。
  • mount 阶段: 第三个阶段 发生在kubelet 创建 pod的过程中,它在创建 pod 的过程中,首先要去做一个 mount,这里的 mount 操作是为了将已经attach到这个 node 上面那块盘,进一步 mount 到 pod 可以使用的一个具体路径,之后 kubelet 才开始创建并启动容器。

存储拓扑调度

拓扑

拓扑是 K8s 集群中为管理的 nodes 划分的一种“位置”关系,意思为:可以通过在 node 的 labels 信息里面填写某一个 node 属于某一个拓扑

  • 第一种,在使用云存储服务的时候,经常会遇到 region,也就是地区的概念,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/region 来标识。这个是为了标识单个 K8s 集群管理的跨 region 的 nodes 到底属于哪个地区;

  • 第二种,比较常用的是可用区,也就是 available zone,在 K8s 中常通过 label failure-domain.beta.kubernetes.io/zone 来标识。这个是为了标识单个 K8s 集群管理的跨 zone 的 nodes 到底属于哪个可用区;

  • 第三种,是 **hostname,**就是单机维度,是拓扑域为 node 范围,在 K8s 中常通过 label kubernetes.io/hostname 来标识,这个在文章的最后讲 local pv 的时候,会再详细描述

  • 拓扑其实是可以自己定义的。可以定义一个字符串来表示一个拓扑域,这个 key 所对应的值其实就是拓扑域下不同的拓扑位置

拓扑调度

K8s 中通过 PV 的 PVC 体系将存储资源和计算资源分开管理了。如果创建出来的 PV有"访问位置"的限制,也就是说,它通过 nodeAffinity 来指定哪些 node 可以访问这个 PV。但是这种情况下就会出现一种访问位置的限制。这种限制主要有两种:

  • Local PV 的限制。Local PV 是将一个 node 上的本地存储封装为 PV,通过使用 PV 的方式来访问本地存储,其提出就是为了提升 IO 速度,但是如果PV创建在另外一个Node上,而Pod调度在这个Node上,这样子的话Po的启动就会失败
  • Zone可用区的限制,比如 PV 只能在同一个可用区中使用,这样子 Pod 和 PV 分离也会导致Pod调度失败。

K8s的解决办法:

延迟PV:在 K8s 中将 PV 和 PVC 的 binding 操作和动态创建 PV 的操作做了 delay,delay 到 pod 调度结果出来之后,再去做这两个操作

好处有:

  • 首先,如果要是所要使用的 PV 是预分配的,如 Local PV,其实使用这块 PV 的 pod 它对应的 PVC 其实还没有做绑定,就可以通过调度器在调度的过程中,结合 pod 的计算资源需求(如 cpu/mem) 以及 pod 的 PVC 需求,选择的 node 既要满足计算资源的需求又要 pod 使用的 pvc 要能 binding 的 pv 的 nodeaffinity 限制;(AC:就是在调度的时候需要考虑一个满足这个nodeaffinity的node出来直接调度,然后再创建对应的PV出来)
  • 其次对动态生成 PV 的场景其实就相当于是如果知道 pod 运行的 node 之后,就可以根据 node 上记录的拓扑信息来动态的创建这个 PV,也就是保证新创建出来的 PV 的拓扑位置与运行的 node 所在的拓扑位置是一致的,如上面所述的阿里云云盘的例子,既然知道 pod 要运行到可用区 1,那之后创建存储时指定在可用区 1 创建即可。

延迟绑定和延迟创建 PV,需要在 K8s 中的改动涉及到的相关组件有三个:

  • PV Controller 也就是 persistent volume controller,需要支持延迟 Binding 这个操作。
  • 动态生成 PV 的组件,如果 pod 调度结果出来之后,它要根据 pod 的拓扑信息来去动态的创建 PV。(AC:也就是创建PV的时候需要考虑当前的结果)
  • 第三组件,最重要的一个改动点就是 kube-scheduler。在为 pod 选择 node 节点的时候,它不仅要考虑 pod 对 CPU/MEM 的计算资源的需求,它还要考虑这个 pod 对存储的需求,也就是根据它的 PVC,它要先去看一下当前要选择的 node,能否满足能和这个 PVC 能匹配的 PV 的 nodeAffinity;或者是动态生成 PV 的过程,它要根据 StorageClass 中指定的拓扑限制来 check 当前的 node 是不是满足这个拓扑限制,这样就能保证调度器最终选择出来的 node 就能满足存储本身对拓扑的限制。

示例

  • Local PV 大部分使用的时候都是通过静态创建的方式,也就是要先去声明 PV 对象,既然 Local PV 只能是本地访问,就需要在声明 PV 对象的,在 PV 对象中通过 nodeAffinity 来限制我这个 PV 只能在单 node 上访问,也就是给这个 PV 加上拓扑限制。如上图拓扑的 key 用 kubernetes.io/hostname 来做标记,也就是只能在 node1 访问。如果想用这个 PV,你的 pod 必须要调度到 node1 上。
  • 既然是静态创建 PV 的方式,这里为什么还需要 storageClassname 呢?前面也说了,在 Local PV 中,如果要想让它正常工作,需要用到延迟绑定特性才行,那既然是延迟绑定,当用户在写完 PVC 提交之后,即使集群中有相关的 PV 能跟它匹配,它也暂时不能做匹配,也就是说 PV controller 不能马上去做 binding,这个时候你就要通过一种手段来告诉 PV controller,什么情况下是不能立即做 binding。这里的 storageClass 就是为了起到这个副作用,我们可以看到 storageClass 里面的 provisioner 指定的是 no-provisioner,其实就是相当于告诉 K8s 它不会去动态创建 PV,它主要用到 storageclass 的 VolumeBindingMode 字段,叫 WaitForFirstConsumer,可以先简单地认为它是延迟绑定。
  • 当用户开始提交 PVC 的时候,pv controller 在看到这个 pvc 的时候,它会找到相应的 storageClass,发现这个 BindingMode 是延迟绑定,它就不会做任何事情。
  • 之后当真正使用这个 pvc 的 pod,在调度的时候,当它恰好调度在符合 pv nodeaffinity 的 node 的上面后,这个 pod 里面所使用的 PVC 才会真正地与 PV 做绑定,这样就保证我 pod 调度到这台 node 上之后,这个 PVC 才与这个 PV 绑定,最终保证的是创建出来的 pod 能访问这块 Local PV,也就是静态 Provisioning 场景下怎么去满足 PV 的拓扑限制。

动态就是指动态创建 PV 就有拓扑位置的限制,那怎么去指定?

  • 首先在 storageclass 还是需要指定 **BindingMode,就是 WaitForFirstConsumer,**就是延迟绑定。
  • 其次特别重要的一个字段就是 allowedTopologies,限制就在这个地方。上图中可以看到拓扑限制是可用区的级别,这里其实有两层意思:
    1. 第一层意思就是说我在动态创建 PV 的时候,创建出来的 PV 必须是在这个可用区可以访问的;
    2. 第二层含义是因为声明的是延迟绑定,那调度器在发现使用它的 PVC 正好对应的是该 storageclass 的时候,调度 pod 就要选择位于该可用区的 nodes。
  • 总之,就是要从两方面保证,一是动态创建出来的存储时要能被这个可用区访问的,二是我调度器在选择 node 的时候,要落在这个可用区内的,这样的话就保证我的存储和我要使用存储的这个 pod 它所对应的 node,它们之间的拓扑域是在同一个拓扑域,用户在写 PVC 文件的时候,写法是跟以前的写法是一样的,主要是在 storageclass 中要做一些拓扑限制。

调度流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I4f6bE4j-1623207610993)(云原生.assets/image-20210605161103605.png)]

上图中红色部分就是调度器新加的存储拓扑调度逻辑,我们先来看一下不加红色部分时调度器的为一个 pod 选择 node 时,它的大概流程:

  • 首先用户提交完 pod 之后,会被调度器 watch 到,它就会去首先做预选,预选就是说它会将集群中的所有 node 都来与这个 pod 它需要的资源做匹配;
  • 如果匹配上,就相当于这个 node 可以使用,当然可能不止一个 node 可以使用,最终会选出来一批 node;
  • 然后再经过第二个阶段优选,优选就相当于我要对这些 node 做一个打分的过程,通过打分找到最匹配的一个 node;
  • 之后调度器将调度结果写到 pod 里面的 spec.nodeName 字段里面,然后会被相应的 node 上面的 kubelet watch 到,最后就开始创建 pod 的整个流程。

那现在看一下加上卷相关的调度的时候,筛选 node(第二个步骤)又是怎么做的?

  • 先就要找到 pod 中使用的所有 PVC,找到已经 bound 的 PVC,以及需要延迟绑定的这些 PVC
  • 对于已经 bound 的 PVC,要 check 一下它对应的 PV 里面的 nodeAffinity 与当前 node 的拓扑是否匹配 。如果不匹配, 就说明这个 node 不能被调度。如果匹配,继续往下走,就要去看一下需要延迟绑定的 PVC
  • 对于需要延迟绑定的 PVC。先去获取集群中存量的 PV,满足 PVC 需求的,先把它全部捞出来,然后再将它们一一与当前的 node labels 上的拓扑做匹配,如果它们(存量的 PV)都不匹配,那就说明当前的存量的 PV 不能满足需求,就要进一步去看一下如果要动态创建 PV 当前 node 是否满足拓扑限制,也就是还要进一步去 check StorageClass 中的拓扑限制,如果 StorageClass 中声明的拓扑限制与当前的 node 上面已经有的 labels 里面的拓扑是相匹配的,那其实这个 node 就可以使用,如果不匹配,说明该 node 就不能被调度。

经过这上面步骤之后,就找到了所有即满足 pod 计算资源需求又满足 pod 存储资源需求的所有 nodes。

当 node 选出来之后,第三个步骤就是调度器内部做的一个优化。这里简单过一下,就是更新经过预选和优选之后,pod 的 node 信息,以及 PV 和 PVC 在 scheduler 中做的一些 cache 信息。

第四个步骤也是重要的一步,已经选择出来 node 的 Pod,不管其使用的 PVC 是要 binding 已经存在的 PV,还是要做动态创建 PV,这时就可以开始做。由调度器来触发,调度器它就会去更新 PVC 对象和 PV 对象里面的相关信息,然后去触发 PV controller 去做 binding 操作,或者是由 csi-provisioner 去做动态创建流程。

可观测性

Liveness 指针和 Readiness 指针

Liveness 指针和 Readiness 指针支持三种不同的探测方式

  1. httpGet。它是通过发送 http Get 请求来进行判断的,当返回码是 200-399 之间的状态码时,标识这个应用是健康的;
  2. Exec。它是通过执行容器中的一个命令来判断当前的服务是否是正常的,当命令行的返回结果是 0,则标识容器是健康的;
  3. tcpSocket。它是通过探测容器的 IP 和 Port 进行 TCP 健康检查,如果这个 TCP 的链接能够正常被建立,那么标识当前这个容器是健康的。

从探测结果来讲主要分为三种:

  • success,当状态是 success 的时候,表示 container 通过了健康检查,也就是 Liveness probe 或 Readiness probe 是正常的一个状态;
  • Failure,Failure 表示的是这个 container 没有通过健康检查,如果没有通过健康检查的话,那么此时就会进行相应的一个处理,那在 Readiness 处理的一个方式就是通过 service。service 层将没有通过 Readiness 的 pod 进行摘除,而 Liveness 就是将这个 pod 进行重新拉起,或者是删除。
  • Unknown,Unknown 是表示说当前的执行的机制没有进行完整的一个执行,可能是因为类似像超时或者像一些脚本没有及时返回,那么此时 Readiness-probe 或 Liveness-probe 会不做任何的一个操作,会等待下一次的机制来进行检验。

主要参数

  • initialDelaySeconds,它表示的是说这个 pod 启动延迟多久进行一次检查,比如说现在有一个 Java 的应用,它启动的时间可能会比较长,因为涉及到 jvm 的启动,包括 Java 自身 jar 的加载。所以前期,可能有一段时间是没有办法被检测的,而这个时间又是可预期的,那这时可能要设置一下 initialDelaySeconds;

  • periodSeconds,它表示的是检测的时间间隔,正常默认的这个值是 10 秒;

  • timeoutSeconds,它表示的是检测的超时时间,当超时时间之内没有检测成功,那它会认为是失败的一个状态;

  • successThreshold,它表示的是:当这个 pod 从探测失败到再一次判断探测成功,所需要的阈值次数,默认情况下是 1 次,表示原本是失败的,那接下来探测这一次成功了,就会认为这个 pod 是处在一个探针状态正常的一个状态;

  • failureThreshold,它表示的是探测失败的重试次数,默认值是 3,表示的是当从一个健康的状态连续探测 3 次失败,那此时会判断当前这个pod的状态处在一个失败的状态。

区别

Liveness Readines
概念 Liveness 指针是存活指针,它用来判断容器是否存活、判断 pod 是否 running。 Readiness 指针用来判断这个容器是否启动完成,即 pod 的 condition 是否 ready
检测失败 kubelet 杀掉相应的 pod,并根据重启策略来判断是否重启这个容器 将 pod 从 Endpoint 上移除,也就是说从接入层上面会把前一个 pod 进行摘除,直到下一次判断成功,这个 pod 才会再次挂到相应的 endpoint 之上。
适用场景 Liveness 指针适用场景是支持那些可以重新拉起的应用 Readiness 指针主要应对的是启动之后无法立即对外提供服务的这些应用

注意事项

  1. 调大超时的阈值,因为在容器里面执行一个 shell 脚本,它的执行时长是非常长的,平时在一台 ecs 或者在一台 vm 上执行,可能 3 秒钟返回的一个脚本在容器里面需要 30 秒钟。所以这个时间是需要在容器里面事先进行一个判断的,那如果可以调大超时阈值的方式,来防止由于容器压力比较大的时候出现偶发的超时;
  2. 调整判断失败的一个次数,3 次的默认值其实在比较短周期的判断周期之下,不一定是最佳实践,适当调整一下判断的次数也是一个比较好的方式;
  3. exec,如果是使用 shell 脚本的这个判断,调用时间会比较长,比较建议大家可以使用类似像一些编译性的脚本 Golang 或者一些 C 语言、C++ 编译出来的这个二进制的 binary 进行判断,那这种通常会比 shell 脚本的执行效率高 30% 到 50%;
  4. 使用 tcpSocket 方式进行判断的时候,如果遇到了 TLS 的服务,那可能会造成后边 TLS 里面有很多这种未健全的 tcp connection,那这个时候需要自己对业务场景上来判断,这种的链接是否会对业务造成影响。

常见故障和排查

  • Pod 停留在 Pending。pending 表示调度器没有进行介入。此时可以通过 kubectl describe pod 来查看相应的事件。
  • Pod 停留在 waiting。pod 的 states 处在 waiting 的时候,通常表示说这个 pod 的镜像没有正常拉取。
  • Pod 不断被拉取并且可以看到 crashing。。这个通常表示说 pod 已经被调度完成了,但是启动失败,那这个时候通常要关注的应该是这个应用自身的一个状态,并不是说配置是否正确、权限是否正确,此时需要查看的应该是 pod 的具体日志。
  • Pod 处在 Runing 但是没有正常工作。那此时比较常见的一个点就可能是由于一些非常细碎的配置,类似像有一些字段可能拼写错误等,可以通过 apply-validate-f pod.yaml 的方式来进行判断当前 yaml 是否是正常的。如果 yaml 没有问题,那么接下来可能要诊断配置的端口是否是正常的,以及 Liveness 或 Readiness 是否已经配置正确。
  • Service 无法正常的工作。那比较常见的 service 出现问题的时候,是自己的使用上面出现了问题。

Service

背景

k8s应用之间相互调用需求:

  • Pod生命周期短暂,IP地址随时变化
  • Deployment等Pod组需要统一的访问入口和负载均衡
  • 应用间再不同环境部署时需要保持同样的部署拓扑和访问方式

k8s应用如何暴露到外部访问和负载均衡

Service:Kubernetes 中的服务返现与负载均衡

语法

主要注意的是targetPort,效果是访问到这个 service 80 端口会被路由到后端的 targetPort,就是只要访问到这个 service 80 端口的都会负载均衡到后端 app:MyApp 这种 label 的 pod 的 9376 端口

集群内访问Service

  • 通过 service 的虚拟 IP 去访问,通过 kubectl get svc 或者 kubectl discribe service 都可以看到service 的虚拟 IP 地址以及 80,然后就可以通过这个虚拟 IP 及端口在 pod 里面直接访问到这个 service 的地址。

  • 直接访问服务名,依靠 DNS 解析,就是同一个 namespace 里 pod 可以直接通过 service 的名字去访问到刚才所声明的这个 service。不同的 namespace 里面,我们可以通过 service 名字加“.”,然后加 service 所在的哪个 namespace 去访问这个 service,例如我们直接用 curl 去访问,就是 my-service:80 就可以访问到这个 service。

  • 通过环境变量访问,在同一个 namespace 里的 pod 启动时,K8s 会把 service 的一些 IP 地址、端口,以及一些简单的配置,通过环境变量的方式放到 K8s 的 pod 里面。在 K8s pod 的容器启动之后,通过读取系统的环境变量比读取到 namespace 里面其他 service 配置的一个地址,或者是它的端口号等等。比如在集群的某一个 pod 里面,可以直接通过 curl $ 取到一个环境变量的值,比如取到 MY_SERVICE_SERVICE_HOST 就是它的一个 IP 地址,MY_SERVICE 就是刚才我们声明的 MY_SERVICE,SERVICE_PORT 就是它的端口号,这样也可以请求到集群里面的 MY_SERVICE 这个 service

[Headless Service](Container & Kubernetes.md#StatefulSet)

  • service 有一个特别的形态就是 Headless Service。service 创建的时候可以指定 clusterIP:None,告诉 K8s 说我不需要 clusterIP(就是刚才所说的集群里面的一个虚拟 IP)
  • 没有虚拟IP,pod 可以直接通过 service_name 用 DNS 的方式解析到所有后端 pod 的 IP 地址,通过 DNS 的 A 记录的方式会解析到所有后端的 Pod 的地址,由客户端选择一个后端的 IP 地址,这个 A 记录会随着 pod 的生命周期变化,返回的 A 记录列表也发生变化,这样就要求客户端应用要从 A 记录把所有 DNS 返回到 A 记录的列表里面 IP 地址中,客户端自己去选择一个合适的地址去访问 pod

架构设计

三个关键组件

  • 一个是 Cloud Controller Manager,负责去配置 LoadBalancer 的一个负载均衡器给外部去访问;
  • 另外一个就是 Coredns,就是通过 Coredns 去观测 APIServer 里面的 service 后端 pod 的一个变化,去配置 service 的 DNS 解析,实现可以通过 service 的名字直接访问到 service 的虚拟 IP,或者是 Headless 类型的 Service 中的 IP 列表的解析
  • 然后在每个 node 里面会有 kube-proxy 这个组件,它通过监听 service 以及 pod 变化,然后实际去配置集群里面的 node pod 或者是虚拟 IP 地址的一个访问。

访问路径

  • 内部访问

    • Client Pod3 首先通过 Coredns 这里去解析出 ServiceIP,Coredns 会返回给它 ServiceName 所对应的 service IP 是什么
    • 这个 Client Pod3 就会拿这个 Service IP 去做请求,它的请求到宿主机的网络之后,就会被 kube-proxy 所配置的 iptables 或者 IPVS 去做一层拦截处理之后去负载均衡到每一个实际的后端 pod 上面去,这样就实现了一个负载均衡以及服务发现
  • 外部访问
    • 外部的一个负载均衡器 Cloud Controller Manager 去监听 service 的变化之后,去配置的一个负载均衡器,然后转发到节点上的一个 NodePort 上面去
    • NodePort 也会经过 kube-proxy 的一个配置的一个 iptables,把 NodePort 的流量转换成 ClusterIP,紧接着转换成后端的一个 pod 的 IP 地址,去做负载均衡以及服务发现

资源调度

调度过程

  • 提交 yaml 文件到 kube-apiserver, ApiServer 会先把这个待创建的请求路由给我们的 webhooks 的 Controllers 进行校验

  • 通过校验之后,ApiServer 会在集群里面生成一个 pod,但此时生成的 pod,它的 nodeName 是空的,并且它的 phase 是 Pending 状态

  • kube-Scheduler 以及 kubelet 都能 watch 到这个 pod 的生成事件,kube-Scheduler 发现这个 pod 的 nodeName 是空的之后,会认为这个 pod 是处于未调度状态,它会把这个 pod 拿到自己里面进行调度,通过一系列的调度算法,包括一系列的过滤和打分的算法后,Schedule 会选出一台最合适的节点,并且把这一台节点的名称绑定在这个 pod 的 spec 上(更新spec.nodeName),完成一次调度的过程。

    合适在于

    • 首先要满足 pod 的资源要求;
    • 其次要满足 pod 的一些特殊关系的要求;
    • 再次要满足 node 的一些限制条件的要求;
    • 最后还要做到整个集群资源的合理利用。
  • 在 Node1 上的这台 kubelet 会 watch 到这个 pod 是属于自己节点上的一个 pod,然后它会把这个 pod 拿到节点上进行操作,包括创建一些容器 storage 以及 network,最后等所有的资源都准备完成,kubelet 会把状态更新为 Running,这样一个完整的调度过程就结束了

基础调度

Pod资源需求

资源类型主要包含

  • cpu: 2000m 或者 整数; cpu可压缩资源,只会饥饿不会杀死pod

  • memory: 1024Mi 或者 1Gi; mem不可压缩资源,会因为OOM(out-of-memory)被杀死

  • ephemeral-storage

  • extended-resource:gpu等,扩展资源必须是整数的

Qos( Quality of Service ):

  • requests 和 limits

    • kube-schedueler会按照requests进行调度,但是真正设置cGroups限制则是按照limits进行设置(kubelet负责)
  • Quality of Service 三种类型:

  • Guaranteed:同时设置了limits和requests,并且他们的值都相同(CPU/Memory); 高的 Qos Class,一般用 Guaranteed 来为一些需要资源保障能力的 pod 进行配置

  • Burstable:Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests, limits 和 requests 不相等(CPU/Memory);中等的一个 Qos label,一般会为一些希望有弹性能力的 pod 来配置 Burstable

  • BestEffort: Pod 既没有设置 requests,也没有设置 limits; 尽力而为式的服务质量

  • Quality of Service 区别

  • 调度表现:

    • 调度器根据requests进行调度,整数Guaranteed会进行绑核处理;非整数的 Guaranteed/Burstable/BestEffort,它们的 CPU 会放在一块,组成一个 CPU share pool

  • memory 上也会按照不同的 Qos 进行划分:OOMScore。OOMScore 得分越高的话,在物理机出现 OOM 的时候会优先被 kill 掉

    • Guaranteed,它会配置默认的 -998 的 OOMScore;
    • Burstable 的话,它会根据内存设计的大小和节点的关系来分配 2-999 的 OOMScore。
    • BestEffort 会固定分配 1000 的 OOMScore;
  • 在节点上的 eviction 动作:

    • eviction 的时候,会优先考虑驱逐 BestEffort 的 pod

Resource Quota

限制 namespace 资源用量

  • spec 包括了一个 hard 和 scopeSelector
  • hard 内容其实和 Resourcelist 很像,这里可以填一些基础的资源。但是它比 ResourceList 更丰富一点,它还可以填写一些 Pod,这样可以限制 Pod 数量能力。
  • scopeSelector 为这个 Resource 方法定义更丰富的索引能力:NotBestEffort,Terminating/Not Terminating,BestEffort/NotBestEffort,PriorityClass
  • Resource Quota 作用于集群,如果用户真的用超了资源,表现的行为是:它在提交 Pod spec 时,会收到一个 forbidden 的 403 错误,提示 exceeded quota。这样用户就无法再提交 cpu 或者是 memory,或者是 Pod 数量的资源。假如再提交一个没有包含在这个 ResourceQuota 方案里面的资源,还是能成功的
亲和性/反亲和性调度

Pod之间关系

PodAffinity

  • requiredDuringSCchedulingIgnoredDuringExecution:必需和某些Pod调度在一起
  • preferredDuringSCchedulingIgnoredDuringExecution:优先和某些Pod调度在一起

PodAntiAffinity

  • requiredDuringSCchedulingIgnoredDuringExecution:禁止和某些Pod调度在一起
  • preferredDuringSCchedulingIgnoredDuringExecution:优先不和某些Pod调度在一起

Operator

  • In/ NotIn/ Exisits/ DoesNotExisits
  • Exists 范围可能会比 In 范围更大,当 Operator 填了 Exists,就不需要再填写 values

Node之间关系

NodeSelector

  • 必须调度到带了某些标签的Node
  • Map[string]string

NodeAffinity

  • requiredDuringSCchedulingIgnoredDuringExecution:必需调度到某些Node上
  • preferredDuringSCchedulingIgnoredDuringExecution:优先调度到某些Node上

Operator

  • In/ NotIn/ Exisits/ DoesNotExisits/ Gt/ Lt
  • 增加了 Gt 和 Lt,数值比较的玩法。当使用 Gt 的时候,values 只能填写数字

Node标记(Taints)/容忍(Tolerations)

标记(Taints)

限制 Pod 调度到某些 Node 上

taints 内容包括 key、value、effect:

  • key 就是配置的键值
  • value 就是内容
  • effect 是标记了这个 taints 行为是什么,有以下三种:
    • NoSchedule 禁止新的 Pod 调度上来;
    • PreferNoSchedul 尽量不调度到这台;
    • NoExecute 会 evict 没有对应 toleration 的 Pods,并且也不会调度新的上来。这个策略是非常严格的,大家在使用的时候要小心一点。

容忍(Tolerations)

  • 将Pod 是调度到这些标记节点,在 Pod 的 spec 中填写一个 Tolerations,它里面也包含了 key、value、effect,这三个值和 taint 的值是完全对应的,taint 里面的 key,value,effect 是什么内容,Tolerations 里面也要填写相同的内容。
  • Tolerations 还多了一个选项 Operator,Operator 有两个 value:Exists/Equal。Equal 的概念是必须要填写 value,而 Exists 就跟上文说的 NodeAffinity 一样,不需要填写 value,只要 key 值对上了,就认为它跟 taints 是匹配的。
小结

首先假如有需求是处理 Pod 与 Pod 的时候,比如 Pod 和另一个 Pod 有亲和的关系或者是互斥的关系,可以给它们配置下面的参数:

  • PodAffinity
  • PodAntiAffinity

假如存在 Pod 和 Node 有亲和关系,可以配置下面的参数:

  • NodeSelector
  • NodeAffinity

假如有些 Node 是限制某些 Pod 调度的,比如说一些故障的 Node,或者说是一些特殊业务的 Node,可以配置下面的参数:

  • Node – Taints
  • Pod – Tolerations

调度流程

整体调度框架

  • 调度器启动时会通过配置文件 File,或者是命令行参数,或者是配置好的 ConfigMap,来指定调度策略。指定要用哪些过滤器 (Predicates)打分器 (Priorities) 以及要外挂哪些外部扩展的调度器 (Extenders),和要使用的哪些 Schedule 的扩展点 (Plugins)
  • 启动的时候会通过 kube-apiserver 去 watch 相关的数据,通过 Informer 机制获取调度需要的数据 :Pod 数据、Node 数据、存储相关的数据,以及在抢占流程中需要的 PDB 数据,和打散算法需要的 Controller-Workload 数据。
  • 调度流水线 (Schedule Pipeline) 。通过 Informer 去 watch 到需要等待的 Pod 数据,放到队列里面,通过调度算法流程里面,会一直循环从队列里面拿数据,然后经过调度流水线进行调度
    • 在整个循环过程中,会有一个串行化的过程:从调度队列里面拿到一个 Pod 进入到 Schedule Theread 流程中,通过 Pre Filter–Filter–Post Filter–Score(打分)-Reserve,最后 Reserve 对账本做预占用
    • 基本调度流程结束后,会把这个任务提交给 Wait Thread 以及 Bind Thread,然后 ScheduleTheread 继续执行流程,会从调度队列中拿到下一个 Pod 进行调度。
    • 调度完成后,会去更新调度缓存 (Schedule Cache),如更新 Pod 数据的缓存,也会更新 Node 数据

性能提升三设计

  • Node状态信息Cache化,Kubernetes 调度器性能得以提升的一个关键演化

  • 乐观绑定 Assume bind

    • 在 Bind 阶段,**只会更新 Scheduler Cache 里的 Pod 和 Node 的信息。**之后在创建goroutine进行调度
  • 无锁化

    • Predicates阶段:调度器会启动多个 Goroutine 以节点为粒度并发执行 Predicates 算法,从而提高这一阶段的执行效率。Priorities 算法也会以 MapReduce 的方式并行计算然后再进行汇总
    • 在这些所有需要并发的路径上,调度器会避免设置任何全局的竞争资源,从而免去了使用锁进行同步带来的巨大的性能损耗。
    • Kubernetes 调度器只有对调度队列和 Scheduler Cache 进行操作时,才需要加锁。而这两部分操作,都不在 Scheduling Path 的算法执行路径上。
具体流程
  • informer path 循环:启动一系列的informer,监听Etcd中Pod、Node、Service等与调度相关的API对象的变化

    • 需要调度的对象会被放进一个调度队列(具有一定优先级设计的队列,比如说,FIFO或者其他自定义优先级队列)
    • 调度器缓存更新,Kubernetes 调度部分进行性能优化的一个最根本原则,就是尽最大可能将集群信息Cache 化,以便从根本上提高 Predicate 和 Priority 调度算法的执行效率。

  • 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 端的二次确认。)
  • 调度队列设计:

    • activeQ:需要调度的Pod存储队列
    • backoffQ:在一个调度周期里面,Cache 发生了变化,会把 Pod 放到 backoffQ 里面。在 backoffQ 里面等待的时间会比在 unschedulableQ 里面时间更短,backoffQ 里有一个降级策略,是 2 的指数次幂降级。假设重试第一次为 1s,那第二次就是 2s,第三次就是 4s,第四次就是 8s,最大到 10s。
    • unschedulableQ:Pod如果 Bind 失败,会把 Pod 重新丢回到 unschedulableQ 队列里面。在这个队列里面的 Pod 如果一分钟没调度过,到一分钟的时候,它会把这个 Pod 重新丢回 activeQ,而正常情况下,它的轮询周期是 30s。
  • NodeCache算法设计:可以不用过滤所有节点,取到最优节点

    • 可通过调度器提供的取样能力,通过配置这个比例来拿到部分节点进行过滤及打分,然后选中节点进行 bind 流程。
    • 提供这种能力需要在 NodeCache 里面注意一点,NodeCache 选中的节点需要足够分散,也就意味着容灾能力的增强。
    • 在 NodeCache 中,Node 是按照 zone 进行分堆。在 **filter 阶段的时候,为会 NodeCache 维护一个 zondeIndex,每 Pop 一个 Node 进行过滤,zoneIndex 往后挪一个位置,然后从该 zone 的 node 列表中取一个 node 出来。**可以看到上图纵轴有一个 nodeIndex,每次也会自增。如果当前 zone 的节点无数据,那就会从下一个 zone 中拿数据。大概的流程就是 zoneIndex 从左向右,nodeIndex 从上到下,从而保证拿到的 Node 节点是按照 zone 打散,从而保证了在优化开启之后的容灾
    • 过滤中的 Filter 和 Score 之间的 isEnough,就是刚才说过的取样比例。如果取样的规模已经达到了我们设置的取样比例,那 Filter 就会结束,不会再去过滤下一个节点。 然后过滤到的节点会经过打分器,打完分后会选择最优节点作为 pod 的分配位置。按照默认值来说,默认的是在 [5-50] 之间,公式为 Max (5,50 - 集群的 node 数 / 125)
算法实现
过滤器 Predicates

GeneralPredicates

一组过滤规则,负责的是最基础的调度策略

  • PodFitsHost, 检查宿主机的名字是否跟 Pod 的 spec.nodeName 一致。
  • PodFitsHostPorts,检查 Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被使用的端口有冲突。
  • PodMatchNodeSelector,检查,Pod 的 nodeSelector 或者 nodeAffinity 指定的节点,是否与待考察节点匹配

存储相关

  1. NoVolumeZoneConfict,校验 pvc 上要求的 zone 是否和 Node 的 zone 匹配;
  2. MaxCSIVolumeCountPred,由于服务提供方对每个节点的单机最大挂载磁盘数是有限制的,所有这个是用来校验 pvc 上指定的 Provision 在 CSI plugin 上报的单机最大挂盘数;一个节点上某种类型的持久化 Volume是不是已经超过了一定数目,如果是的话,那么声明使用该类型持久化 Volume 的 Pod 就不能再调度到这个节点了
  3. CheckVolumeBindingPred,在 pvc 和 pv 的 binding 过程中对其进行逻辑校验;(Pod 对应的PV 的 nodeAffinity 字段,是否跟某个节点的标签相匹配)
  4. NoDiskConfict,SCSI 存储不会被重复的 volume。多个 Pod 声明挂载的持久化 Volume 是否有冲突

Pod 和 Node 匹配相关

  1. CheckNodeCondition,在 node 节点上有一个 Condition 的 type 值是不是 true,如果是 true 这个节点才会允许被调度;
  2. CheckNodeUnschedulable,在 node 节点上有一个 NodeUnschedulable 的标记,我们可以通过 kube-controller 对这个节点直接标记为不可调度,那这个节点就不会被调度了。在 1.16 的版本里,这个 Unschedulable 已经变成了一个 Taints。也就是说需要校验一下 Pod 上打上的 Tolerates 是不是可以容忍这个 Taints;
  3. PodToleratesNodeTaints,就是 Pod Tolerates 和 Node Taints 是否匹配;
  4. PodFitsHost,其实就是 Host 校验;
  5. NodeMemoryPressurePredicate,检查的是当前节点的内存是不是已经不够充足,如果是的话,那么待调度 Pod 就不能被调度到该节点上
  6. MatchNodeSelector

Pod 和 Pod 匹配相关

MatchinterPodAffinity:主要是 PodAffinity 和 PodAntiAffinity 的校验逻辑;检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系。指定作用域(topologyKey

Pod 打散相关

  1. EvenPodsSpread;

    • 描述符合条件的一组 Pod 在指定 TopologyKey 上的打散要求
    • 描述一组 Pod 的方式是可以通过 matchLabelsmatchExpressions 来进行描述是否符合条件
    • topologyKey : zone / node / region 等
    • maxSkew:最大允许不均衡的数量;与ActualSkew = count[topo] - min(count[topo]) 进行比较,看看能不能放到这个node上
  2. CheckServiceAffinity

具体执行过程

  • 开始调度一个 Pod 时,Kubernetes 调度器会同时启动 16 个Goroutine,来并发地为集群里的所有 Node 计算 Predicates,最后返回可以运行这个 Pod的宿主机列表。
  • 在为每个 Node 执行 Predicates 时,调度器会按照固定的顺序来进行检查。这个顺序,是按照 Predicates 本身的含义来确定
打分器 Priorities

打分算法主要解决的问题就是集群的碎片、容灾、水位、亲和、反亲和等

资源水位

概念:

  • Request:Node 已经分配的资源
  • Allocatable:Node 的可调度的资源

算法:

  • LeastRequestedPriorit 优先打散

    • Pod 分到可用资源最大比例的节点上
    • (Allocatable - Request) / Allocatable * Score
  • MostRequestedPriorit 优先堆叠
    • Pod 分配到 Request 的节点上,使用的资源比例越大,它应该越优先,从而达到优先堆叠。 相当于best fit
    • Request / Allocatable * Score
  • BalancedResourceAllocation 碎片均衡
    • 优先从备选节点列表中选择各项资源使用率最均衡的节点
    • { 1 - Abs[CPU(Request / Allocatable) - Mem(Request / Allocatable)] } * Score
  • RequestedToCapacityRatioPriority 指定比率分数
    • 当资源使用的比率达到某个值时,用户指定配置参数可以指定不同比率的分数,从而达到控制集群上每个节点 node 的分布
    • Request / Allocatable * MaxUtilization

Pod 打散

  • SelectorSpreadPriority

    • 为了满足 Pod 所属的 Controller 上所有的 Pod 在 Node 上打散的要求
    • 它会依据待分配的 Pod 所属的 controller,计算该 controller 下的所有 Pod,假设总数为 T,对这些 Pod 按照所在的 Node 分组统计;假设为 N (表示为某个 Node 上的统计值)
    • 那么对 Node上的分数统计为 (T-N)/T 的分数,值越大表示这个节点的 pod 部署的越少,分数越高,从而达到 workload 的 pod 打散需求。
  • ServiceSpreadingPriority
    • 大概率会用来替换 SelectorSpreadPriority
    • Service 代表一组服务,我们只要能做到服务的打散分配就足够了
  • EvenPodsSpreadPriority
    • 指定一组符合条件的 Pod 在某个拓扑结构上的打散需求,这样是比较灵活、比较定制化的一种方式,使用起来也是比较复杂的一种方式
    • 在满足topologyKey上的Node中进行调度
    • NodeTopoPods,按照Node级别累计TOpoPods
    • MaxDif = Max(NodeTopoPods) - Min(NodeTopoPods)
    • (NodeTopoPods - MaxDif ) / MaxDif

Node 亲和 & 反亲和

  • NodeAffinityPriority,这个是为了满足 Pod 和 Node 的亲和 & 反亲和

  • ServiceAntiAffinity,是为了支持 Service 下的 Pod 的分布要按照 Node 的某个 label 的值进行均衡。比如:集群的节点有云上也有云下两组节点,我们要求服务在云上云下均衡去分布,假设 Node 上有某个 label,那我们就可以用这个 ServiceAntiAffinity 进行打散分布;

  • NodeLabelPrioritizer,主要是为了实现对某些特定 label 的 Node 优先分配,算法很简单,启动时候依据调度策略 (SchedulerPolicy)配置的 label 值,判断 Node 上是否满足这个label条件,如果满足条件的节点优先分配;

  • ImageLocalityPriority节点亲和主要考虑的是镜像下载的速度。如果节点里面存在镜像的话,优先把 Pod 调度到这个节点上,这里还会去考虑镜像的大小,比如这个 Pod 有好几个镜像,镜像越大下载速度越慢,它会按照节点上已经存在的镜像大小优先级亲和

Pod 亲和&反亲和

  • InterPodAffinityPriority

    • 第一个例子,比如说应用 A 提供数据,应用 B 提供服务**,A 和 B 部署在一起可以走本地网络**,优化网络传输;
    • 第二个例子,如果应用 A 和应用 B 之间都是 CPU 密集型应用,而且证明它们之间是会互相干扰的,那么可以通过这个规则设置尽量让它们不在一个节点上。
  • NodePreferAvoidPodsPriority

    • 用于实现某些 controller 尽量不分配到某些节点上的能力;通过在 node 上加 annotation 声明哪些 controller 不要分配到 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 上。

调度器语法

常用字段及其意义

  • 第一个 algorithmSource 是算法提供者,目前提供三种方式:Provider、file、configMap,后面会介绍这块;
  • 第二个 percentageOfNodesToscore,就是调度器提供的一个扩展能力,能够减少 Node 节点的取样规模;
  • 第三个 SchedulerName 是用来表示调度器启动的时候,负责哪些 Pod 的调度;如果没有指定的话,默认名称就是 default-scheduler;
  • 第四个 bindTimeoutSeconds,是用来指定 bind 阶段的操作时间,单位是秒;
  • 第五个 ClientConnection,是用来配置跟 kube-apiserver 交互的一些参数配置。比如 contentType,是用来跟 kube-apiserver 交互的序列化协议,这里指定为 protobuf;
  • 第六个 disablePreemption,关闭抢占协议;
  • 第七个 hardPodAffinitySymnetricweight,配置 PodAffinity 和 NodeAffinity 的权重是多少。

algorithmSource

  • Provider:

    • DefaultPrivider,优先打散
    • ClusterAutoscalerProvider,优先堆叠;节点开启了自动扩容,尽量使用 ClusterAutoscalerProvider

Scheduler Extender

  • Polic 文件中 extender 的配置,包括 extender 服务的 URL 地址、是否 https 服务

  • NodeCache。如果有 NodeCache,那调度器只会传给 nodenames 列表。如果没有开启,那调度器会把所有 nodeinfo 完整结构都传递过来

  • ignorable 这个参数表示调度器在网络不可达或者是服务报错,是否可以忽略扩展调度器。

  • managedResources,官方调度器在遇到这个 Resource 时会用扩展调度器,如果不指定表示所有的都会使用扩展调度器。

调度器框架

扩展点

  • QueueSort:用来支持自定义 Pod 的排序。如果指定 QueueSort 的排序算法,在调度队列里面就会按照指定的排序算法来进行排序;
  • Prefilter:对 Pod 的请求做预处理,比如 Pod 的缓存,可以在这个阶段设置;
  • Filter:就是对 Filter 做扩展,可以加一些自己想要的 Filter,比如说刚才提到的 gpu-shared 可以在这里面实现;
  • PostFilter:可以用于 logs/metircs,或者是对 Score 之前做数据预处理。比如说自定义的缓存插件,可以在这里面做;
  • Score:就是打分插件,通过这个接口来实现增强;
  • Reserve:对有状态的 plugin 可以对资源做内存记账;
  • Permit:wait、deny、approve,可以作为 gang 的插入点。这个可以对每个 pod 做等待,等所有 Pod 都调度成功、都达到可用状态时再去做通行,假如一个 pod 失败了,这里可以 deny 掉;
  • PreBind:在真正 bind node 之前,执行一些操作,例如:云盘挂载盘到 Node 上;
  • Bind:一个 Pod 只会被一个 BindPlugin 处理;
  • PostBind:bind 成功之后执行的逻辑,比如可以用于 logs/metircs;
  • Unreserve:在 Permit 到 Bind 这几个阶段只要报错就回退。比如说在前面的阶段 Permit 失败、PreBind 失败, 都会去做资源回退。

并发模型

ntageOfNodesToscore,就是调度器提供的一个扩展能力,能够减少 Node 节点的取样规模;

  • 第三个 SchedulerName 是用来表示调度器启动的时候,负责哪些 Pod 的调度;如果没有指定的话,默认名称就是 default-scheduler;
  • 第四个 bindTimeoutSeconds,是用来指定 bind 阶段的操作时间,单位是秒;
  • 第五个 ClientConnection,是用来配置跟 kube-apiserver 交互的一些参数配置。比如 contentType,是用来跟 kube-apiserver 交互的序列化协议,这里指定为 protobuf;
  • 第六个 disablePreemption,关闭抢占协议;
  • 第七个 hardPodAffinitySymnetricweight,配置 PodAffinity 和 NodeAffinity 的权重是多少。

algorithmSource

  • Provider:

    • DefaultPrivider,优先打散
    • ClusterAutoscalerProvider,优先堆叠;节点开启了自动扩容,尽量使用 ClusterAutoscalerProvider

Scheduler Extender

  • Polic 文件中 extender 的配置,包括 extender 服务的 URL 地址、是否 https 服务

  • NodeCache。如果有 NodeCache,那调度器只会传给 nodenames 列表。如果没有开启,那调度器会把所有 nodeinfo 完整结构都传递过来

  • ignorable 这个参数表示调度器在网络不可达或者是服务报错,是否可以忽略扩展调度器。

  • managedResources,官方调度器在遇到这个 Resource 时会用扩展调度器,如果不指定表示所有的都会使用扩展调度器。

调度器框架

扩展点

  • QueueSort:用来支持自定义 Pod 的排序。如果指定 QueueSort 的排序算法,在调度队列里面就会按照指定的排序算法来进行排序;
  • Prefilter:对 Pod 的请求做预处理,比如 Pod 的缓存,可以在这个阶段设置;
  • Filter:就是对 Filter 做扩展,可以加一些自己想要的 Filter,比如说刚才提到的 gpu-shared 可以在这里面实现;
  • PostFilter:可以用于 logs/metircs,或者是对 Score 之前做数据预处理。比如说自定义的缓存插件,可以在这里面做;
  • Score:就是打分插件,通过这个接口来实现增强;
  • Reserve:对有状态的 plugin 可以对资源做内存记账;
  • Permit:wait、deny、approve,可以作为 gang 的插入点。这个可以对每个 pod 做等待,等所有 Pod 都调度成功、都达到可用状态时再去做通行,假如一个 pod 失败了,这里可以 deny 掉;
  • PreBind:在真正 bind node 之前,执行一些操作,例如:云盘挂载盘到 Node 上;
  • Bind:一个 Pod 只会被一个 BindPlugin 处理;
  • PostBind:bind 成功之后执行的逻辑,比如可以用于 logs/metircs;
  • Unreserve:在 Permit 到 Bind 这几个阶段只要报错就回退。比如说在前面的阶段 Permit 失败、PreBind 失败, 都会去做资源回退。

并发模型

并发模型意思是主调度流程是在 Pre Filter 到 Reserve,如上图浅蓝色部分所示。从 Queue 拿到一个 Pod 调度完到 Reserve 就结束了,接着会把这个 Pod 异步交给 Wait Thread,Wait Thread 如果等待成功了,就会交给 Bind Thread,就是这样一个线程模型。

kubernetes k8s 云原生技术相关推荐

  1. 邀您参加 | K8S云原生技术开放日-北京站

    K8S与云原生技术开放日北京站来了! K8S与云原生毋庸置疑已成云计算主流开源技术,2019年K8S,Serverless,微服务,DevOps将是所有企业和开发者人群持续践行及密切关注的技术领域,也 ...

  2. 重磅发布 | 30+ 阿里巴巴云原生「顶流」,给你一堂《云原生技术实践公开课》

    以"云"为核心的软件研发思想,正逐步成为所有开发者的默认选项.像 Kubernetes 等云原生技术正在成为技术人员的必修课,大量的工作岗位正在涌现出来.2020 年,云原生技术大 ...

  3. 2020 云原生技术 7 大领域趋势全预测

    文章联合撰稿人(排名不分先后) 叔同.谷朴.不瞋.育睿.许晓斌.至简.典违.鲁直.改之.小剑.汤志敏.白慕.循环.文卿,喽哥.水鸟.神秀. 在筹备阿里云首届云原生实践峰会的过程中,我们展开了对云原生技 ...

  4. “云原生”技术公开课第1章:第一堂“云原生”课

    摘要:欢迎大家来到阿里云与 CNCF 共同推出的"云原生"技术公开课.本文整理自"云原生"技术公开课的开篇:第一堂"云原生"课.在本文中,阿 ...

  5. 阿里巴巴云原生技术公开课文案:课时 1:第一堂“云原生”课(转载只为自学)

    课时 1:第一堂"云原生"课 本节课程要点 云原生技术发展历程(为什么要学习这门课) 课程简介与预备知识(这门课到底教什么) 云原生的定义与技术要点(本节正式内容) 为什么要开设云 ...

  6. CNCF X 阿里巴巴云原生技术公开课

    课程内容 课程介绍 为什么要开设云原生技术公开课? 在 2019 年,阿里巴巴宣布要全面上云,而且"上云就要上云原生".我们还可以看到,以"云"为核心的软件研发 ...

  7. 智能扩展:成功使用云原生技术扩展基础架构的4个关键技巧

    智能扩展:成功使用云原生技术扩展基础架构的4个关键技巧 作者:Reda Benzair 今天的帖子来自CNCF大使兼Streamroot工程副总裁Reda Benzair.文章最初在Streamroo ...

  8. 基于Kubernetes物联网平台Shifu开源,云原生技术助力万物互联

    物联网应用开发及管理平台Shifu正式开源,进入开源协同迭代新阶段.Shifu为客户全场景设备托管,一体化软件开发提供了透明框架.开发者登录GitHub搜索"Shifu",点击&q ...

  9. 无缝融入 Kubernetes 生态 | 云原生网关支持 Ingress 资源

    Kubernetes Ingress 介绍 通常情况下,Kubernetes 集群内的网络环境与外部是隔离的,也就是说 Kubernetes 集群外部的客户端无法直接访问到集群内部的服务,这属于不同网 ...

最新文章

  1. nodejs-7. mongoDB数据库
  2. Erlang 数据类型。。
  3. 第三章 Java Servlet基础
  4. CodeForces-985C Liebig's Barrels
  5. rocketmq发送顺序消息(四)
  6. instanceof关键字
  7. MFC无边框对话框实现拖动
  8. Spring MVC 中 HandlerInterceptorAdapter的使用
  9. 复习Django项目二——创建app应用程序
  10. Python 2 结束支持,Ubuntu 20.04 LTS 继续进行将其移除的工作
  11. 即时通讯学习笔记007---在windows下安装openfire_并且使用自定义的数据库这里用mysql
  12. oracle regexp
  13. 用ps换证件照照片底色
  14. 《天才在左 疯子在右》读书记
  15. vivado中fifo ipcore的empty和dout输出特征
  16. html5 js获取设备信息,js怎么获取电脑硬件信息
  17. DOSBox常用快捷键
  18. SpringCloudRPC远程调用核心原理:Feign弹性RPC客户端的重要组件
  19. Excel AES加密
  20. GE指数和Gini系数(补充)在经济空间差异中的作用

热门文章

  1. 【AutoMapper官方文档】DTO与Domin Model相互转换(中)
  2. 构建git+gerrit+repo的Android代码服务器
  3. 二次安检!自带文具!多地要求提前一小时到考场!
  4. Monaco Editor教程(四):设置或获取内容,并监听内容的改变
  5. SpringBoot之支付宝企业账户转账到个人账户【最新证书版】
  6. FPGA知识点笔记(一)
  7. 项目文档之项目可行性研究
  8. 低预算选购中高配车 海马M3 1.5L一个不错的选择
  9. 制作下拉菜单(二级菜单)
  10. 【2021 第一期】日常开发 26 个常见的 JavaScript 代码优化方案