深入剖析 Kubernetes-3 容器编排与Kubernetes作业管理

1 为什么我们需要Pod

Pod,是 Kubernetes 项目的原子调度单位。

Docker容器的本质:“Namespace 做隔离,Cgroups 做限制,rootfs 做文件系统。

1.1 Pod为调度原子单位

以Pod为原子单位调度,使得要求紧密联系的容器部署在同一个节点的诉求迎刃而解。

这些具有“超亲密关系”容器的典型特征包括但不限于:互相之间会发生直接的文件交换、使用 localhost 或者 Socket 文件进行本地通信、会发生非常频繁的远程调用、需要共享某些 Linux Namespace(比如,一个容器要加入另一个容器的 Network Namespace)等等。

1.2 Pod 的实现原理

Pod 最重要的一个事实是:它只是一个逻辑概念,其实是一组共享了某些资源的容器。Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。

在K8S项目里,Pod的实现需要使用一个中间容器,这个容器叫Infra容器,作为第一个被创建的容器,而其他用户定义的容器,则通过加入网络命名空间的方式与Infra容器关联在一起。

image-20221012223928192

Infra容器占用的资源极少,使用一个非常特殊的镜像,叫做k8s.gcr.io/pause,此镜像是一个永远处于“暂停”状态的容器。Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关

将来如果你要为 Kubernetes 开发一个网络插件时,应该重点考虑的是如何配置这个 Pod 的 Network Namespace,而不是每一个用户容器如何使用你的网络配置,这是没有意义的。

Pod,实际上是在扮演传统基础设施里“虚拟机”的角色;而容器,则是这 个虚拟机里运行的用户程序

1.3 容器设计模式

典型案例有:

  • WAR 包与 Web 服务器

    WAR包容器作为初始化容器在Web容器启动前,将war包拷贝到/app 目录下,然后退出,扮演了一个sidecar的角色。

  • 容器的日志收集

    应用容器不断地把日志文件输出到容器的/var/log目录中,再把一个 Pod 里的 Volume 挂载到应用容器的 /var/log 目录上,同时运行一个 sidecar 容器,它也声明挂载同一个 Volume 到自己的 /var/log 目录上,这样sidecar 容器就只需要做一件事儿,那就是不断地从自己的 /var/log 目录里读取日志文件,转发到 MongoDB 或者 Elasticsearch 中存储起来。

2 深入解析Pod对象

凡是调度、网络、存储,以及安全相关的属性,基本上是 Pod 级别的。凡是跟容器的 Linux Namespace
相关的属性,也一定是 Pod 级别的。凡是 Pod 中的容器要共享宿主机的 Namespace,也一定是 Pod 级别的定义

2.1 Projected Volume

在 Kubernetes 中,有几种特殊的 Volume,它们存在的意义不是为了存放容器里的数 据,也不是用来进行容器和宿主机之间的数据交换。这些特殊 Volume 的作用,是为容器 提供预先定义好的数据。所以,从容器的角度来看,这些 Volume 里的信息就是仿佛是被 Kubernetes“投射”(Project)进入容器当中的。这正是 Projected Volume 的含义。

Projected Volume来源共有四种:

  • Secret
  • ConfigMap
  • Downward API(让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息)
  • ServiceAccountToken(只是一种特殊的Secret而已)

任意一个运行在 Kubernetes 集群里的 Pod,就会发现,每一个 Pod,都已经自动声明一个类型是 Secret、名为 default-token-xxxx 的 Volume,然后 自动挂载在每个容器的一个固定目录上,这个 Secret 类型的 Volume,正是默认 Service Account 对应的ServiceAccountToken。,Kubernetes 其实在每个 Pod 创建的时候,自动在它的
spec.volumes 部分添加上了默认 ServiceAccountToken 的定义,然后自动给每个容器加上了对应的 volumeMounts 字段。这个过程对于用户来说是完全透明的。一旦 Pod 创建完成,容器里的应用就可以直接从这个默认 ServiceAccountToken的挂载目录里访问到授权信息和文件。这个容器内的路径在 Kubernetes 里是固定的,即:/var/run/secrets/kubernetes.io/serviceaccount。

2.2 Pod 恢复机制

Pod 恢复机制,也叫 restartPolicy。一定要强调的是,Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。

  • Always

    在任何情况下,只要容器不在运行状态,就自动重启容器;

  • OnFailure

    只在容器异常时才自动重启容器;

  • Never

    从来不重启容器

Pod容器状态与Pod状态对应关系:

  • 只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod 就会保持 Running 状态,并进行容器重启。否则,Pod 就会进入 Failed 状态 。
  • 对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入Failed 状态。在此之前,Pod 都是 Running 状态。此时,Pod 的 READY 字段会显示正常容器的个数。

2.3 PodPreset

创建Pod时,K8S会自动给Pod填充对应字段,对 Pod 进行批量化、自动化修改。

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
 name: allow-database
spec:
 selector:
 matchLabels:
 role: frontend
 env:
 - name: DB_PORT
 value: "6379"
 volumeMounts:
 - mountPath: /cache
 name: cache-volume
 volumes:
 - name: cache-volume
 emptyDir: {}

在这个 PodPreset 的定义中,首先是一个 selector。这就意味着后面这些追加的定义,只会作用于 selector 所定义的、带有“role: frontend”标签的 Pod 对象,这就可以防止“误伤”。

注意,PodPreset 里定义的内容,只会在 Pod API 对象被创建之前追加在这个对象本身上,而不会影响任何 Pod 的控制器的定义。

3. ”控制器“模型

Pod这个看似复杂的API对象,实际上就是对容器的进一步抽象和封装而已

3.1 调谐(Reconcile)

以 Deployment 为例,简单描述一下它对控制器模型的实现:

  1. Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统
    计它们的数量,这就是实际状态;
  2. Deployment 对象的 Replicas 字段的值就是期望状态;
  3. Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删
    除已有的 Pod

这个操作,通常被叫作调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile
Loop”(调谐循环)或者“Sync Loop”(同步循环)。

image-20221017230420370

类似 Deployment 这样的一个控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的。

4. 作业副本与水平扩展

Deployment实现了Pod的水平扩展/收缩(horizontal scaling out/in)

4.1 Deployment与Replicaset、Pod关系

image-20221017231344259

4.2 滚动更新

将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。

Deployment 实际上是一个两层控制器。首先,它通过ReplicaSet 的个数来描述应用的版本;然后,它再通过ReplicaSet 的属性(比如 replicas的值),来保证 Pod 的副本数量。

image-20221017231639199

5 深入理解StatefulSet

5.1 拓扑状态

StatefulSet的设计抽象为两种情况:

  • 拓扑状态

    这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动。并且,如果Pod重建,它的网络标识必须和原来的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新Pod。

  • 存储状态

    应用的多个实例分别绑定了不同的存储数据,对于这些示例来说,Pod访问的数据应该是同一份,即便在此期间Pod被重新创建过。况最典型的例子,就是一个数据库应用的多个存储实例。

StatefulSet的核心功能,就是通过某种方式记录这些状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态

5.1.1 Headless Service

Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。

Service如何被访问:

  • Normal Service,以 Service 的 VIP(Virtual IP,即:虚拟 IP)方式

    例如,当我访问10.0.23.1 这个 Service 的 IP 地址时,10.0.23.1 其实就是一个 VIP,它会把请求转发到该Service 所代理的某一个 Pod 上。

  • Headless Service,以 Service 的 DNS 方式
    例如,只要我访问“my-svc.mynamespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod。

Kubernetes 将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录(例如web-0-my-svc.mynamespace.svc.cluster.local)。但是Pod的IP地址不是固定的,这就意味着,对于“有状态应用”实例的访问,你必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。

5.1.2 拓扑状态

StatefulSet 这个控制器的主要作用之一,就是使用 Pod 模板创建 Pod 的时候,对它们进行编号,并且按照编号顺序逐一完成创建工作。而当StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod编号的顺序,逐一完成这些操作

此同时,通过 Headless Service 的方式,StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录,来作为它的访问入口。部署“有状态应用”的时候,应用的每个实例拥有唯一并且稳定的“网络标识”

5.2 存储状态

5.2.1 PVC

Kubernetes 项目引入了一组叫作 Persistent Volume Claim(PVC)和 Persistent Volume(PV)的 API 对象,大大降低了用户声明和使用,持久化 Volume 的门槛。

StatefulSet中额外添加了一个volumeClaimTemplates字段,凡是被这个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC,而这个 PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段。更重要的是,这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。

即便Pod重建,Kubernetes也会为新的 Pod 找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一
起的 PV。这样,新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume,并且获取到保存在 Volume 里的数据。

通过这种方式,Kubernetes 的 StatefulSet 就实现了对应用存储状态的管理。

5.2.2 总结

  • 首先,StatefulSet 的控制器直接管理的是 Pod。每个 Pod的 hostname、名字等都是不同的、携带了编号。

    StatefulSet 区分这些实例的方式,就是通过在 Pod 的名字里加上事先约定好的编号。

  • 其次,Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有同样编号的 DNS 记录。

    只要 StatefulSet 能够保证这些 Pod 名字里的编号不变,那么 Service 里类似于 web-0.nginx.default.svc.cluster.local 这样的 DNS 记录也就不会变,而这条记录解析出来的 Pod 的 IP 地址,则会随着后端 Pod 的删除和再创建而自动更新。

  • 最后,StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC。

    Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有一个独立的 Volume。

6 容器化守护进程DaemonSet

DaemonSet 的主要作用,是让你在 Kubernetes 集群里,运行一个 DaemonPod。

这个Pod有如下三个特征:

  • 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
  • 每个节点上只有一个这样的 Pod 实例;
  • 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。

6.1 每个节点只有1个Pod

相比于 Deployment,DaemonSet 只管理 Pod 对象,然后通过 nodeAffinity 和Toleration 这两个调度器的小功能,保证了每个节点上有且只有一个 Pod。

6.2 版本控制ControllerRevision

DaemonSet 使用 ControllerRevision,来保存和管理自己对应的“版本”。

在 Kubernetes 项目里,ControllerRevision 其实是一个通用的版本管理对象。这样,Kubernetes 项目就巧妙地避免了每种控制器都要维护一套冗余的代码和逻辑的问题。

7 Job与CronJob

7.1 编排对象类型

  • 在线业务,即Long Running Task(长作业),一旦运行起来,除非出错或停止,容器进程会一直保持在Running状态

  • 离线业务,即 Batch Job(计算业务),业务在计算完成后直接推出,如果使用Deployment来管理这种业务的话,Pod在计算结束后推出,会被Deployment Controller不断地重启,可以使用Job

7.2 使用Job对象的常用方法

  • 外部管理器 +Job 模板

    把 Job 的 YAML 文件定义为一个“模板”,然后用一个外部工具控制这些“模板”来生成 Job。

  • 拥有固定任务数目的并行 Job

    这种模式下,我只关心最后是否有指定数目(spec.completions)个任务成功退出。至于执行时的并行度是多少,我并不关心。

  • 指定并行度(parallelism),但不设置固定的completions 的值

    此时,你就必须自己想办法,来决定什么时候启动新 Pod,什么时候 Job 才算执行完成。在这种情况下,任务的总数是未知的,所以你不仅需要一个工作队列来负责任务分发,还需要能够判断工作队列已经为空(即:所有的工作已经结束了)。

7.3 CronJob

CronJob 与 Job 的关系,正如同 Deployment 与 Pod 的关系一样。CronJob 是一个专门用来管理 Job 对象的控制器。只不过,它创建和删除 Job 的依据,是 schedule 字段定义的、一个标准的Unix Cron格式的表达式。

Cron 表达式中的五个部分分别代表:分钟、小时、日、月、星期。

比如,"*/1 * * * *"。这个 Cron 表达式里 */1 中的 * 表示从 0 开始,/ 表示“每”,1 表示偏移量。所以,它的
意思就是:从 0 开始,每 1 个时间单位执行一次。

7.3.1 并发策略

需要注意的是,由于定时任务的特殊性,很可能某个 Job 还没有执行完,另外一个新 Job就产生了。这时候,你可以通过 spec.concurrencyPolicy 字段来定义具体的处理策略。

  • concurrencyPolicy=Allow,这也是默认情况,这意味着这些 Job 可以同时存在
  • concurrencyPolicy=Forbid,这意味着不会创建新的 Pod,该创建周期被跳过
  • concurrencyPolicy=Replace,这意味着新产生的 Job 会替换旧的、没有执行完的Job

7.3.2 miss标记

如果某一次 Job 创建失败,这次创建就会被标记为“miss”。当在指定的时间窗口内,miss 的数目达到 100 时,那么 CronJob 会停止再创建这个 Job。

这个时间窗口,可以由 spec.startingDeadlineSeconds 字段指定。比如startingDeadlineSeconds=200,意味着在过去 200 s 里,如果 miss 的数目达到了 100次,那么这个 Job 就不会被创建执行了。

8 声明式API与Kubernetes编程范式

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

8.1 Istio项目

基于 Kubernetes 项目的微服务治理框架

image-20221023225601010

Istio 最根本的组件,是运行在每一个应用 Pod 里的 Envoy 容器(一个高性能 C++ 网络代理)。

Istio 项目,则把这个代理服务以 sidecar 容器的方式,运行在了每一个被治理的应用Pod 中。我们知道,Pod 里的所有容器都共享同一个 Network Namespace。所以,Envoy 容器就能够通过配置 Pod 里的 iptables 规则,把整个 Pod 的进出流量接管下来。

Istio 的控制层(Control Plane)里的 Pilot 组件,就能够通过调用每个 Envoy容器的 API,对这个 Envoy 代理进行配置,从而实现微服务治理。

Istio 项目使用的,是 Kubernetes 中的一个非常重要的功能,叫作 Dynamic Admission Control

Kubernetes 项目为我们额外提供了一种“热插拔”式的 Admission 机制,它就是Dynamic Admission Control,也叫作:Initializer。

Istio 要做的,就是编写一个用来为 Pod“自动注入”Envoy 容器的 Initializer。

首先,Istio 会将这个 Envoy 容器本身的定义,以 ConfigMap 的方式保存在Kubernetes 当中。

接下来,Istio 将一个编写好的 Initializer,作为一个 Pod 部署在 Kubernetes 中。

Initializer 的代码通过调用K8S提供的TwoWayMergePatch函数将ConfigMap记录的Envoy 容器信息与用户创建的容器信息合并,然后使用这个 patch 的数据,调用 Kubernetes 的 Client,发起一个 PATCH 请求,最终使得每个Pod都具备一个Envoy的Side Car容器。

Istio项目的核心,就是由无数个运行在应用 Pod 中的 Envoy 容器组成的服务代理网格

8.2 声明式API

首先,所谓“声明式”,指的就是我只需要提交一个定义好的 API 对象来“声明”,我所期望的状态是什么样子。

其次,“声明式 API”允许有多个 API 写端,以 PATCH 的方式对 API 对象进行修改,而无需关心本地原始 YAML 文件的内容。

最后,也是最重要的,有了上述两个能力,Kubernetes 项目才可以基于对 API 对象的增、删、改、查,在完全无需外界干预的情况下,完成对“实际状态”和“期望状态”的调谐(Reconcile)过程。

声明式 API,才是 Kubernetes 项目编排能力“赖以生存”的核心所在

使用 Initializer 的流程中,最核心的步骤,莫过于 Initializer“自定义控制器”的编写过程。它遵循的,正是标准的“Kubernetes 编程范式”,即:

如何使用控制器模式,同 Kubernetes 里 API 对象的“增、删、改、查”进行协作,进而完成用户业务逻辑的编写过程。

9 基于角色的权限控制:RBAC

9.1 基本概念

  • Role:角色,它其实是一组规则,定义了一组对 Kubernetes API 对象的操作权限。

  • Subject:被作用者,既可以是“人”,也可以是“机器”,也可以使你在 Kubernetes里定义的“用户”。

  • RoleBinding:定义了“被作用者”和“角色”的绑定关系。

Role 和 RoleBinding 对象都是 Namespaced 对象(Namespaced Object),它们对权限的限制规则仅在它们自己的 Namespace 内有效,roleRef 也只能引用当前 Namespace 里的 Role 对象。

对于非 Namespaced(Non-namespaced)对象(比如:Node),或者,某一个 Role 想要作用于所有的 Namespace 的时候,应该使用ClusterRole 和 ClusterRoleBinding。

Role示例:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""] # "" 标明 core API 组
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

RoleBinding示例:

apiVersion: rbac.authorization.k8s.io/v1
# 此角色绑定允许 "jane" 读取 "default" 名字空间中的 Pod
# 你需要在该命名空间中有一个名为 “pod-reader” 的 Role
kind: RoleBinding
metadata:
  name: read-pods
  namespace: default
subjects:
# 你可以指定不止一个“subject(主体)”
- kind: User
  name: jane # "name" 是区分大小写的
  apiGroup: rbac.authorization.k8s.io
roleRef:
  # "roleRef" 指定与某 Role 或 ClusterRole 的绑定关系
  kind: Role        # 此字段必须是 Role 或 ClusterRole
  name: pod-reader  # 此字段必须与你要绑定的 Role 或 ClusterRole 的名称匹配
  apiGroup: rbac.authorization.k8s.io

9.2 用户、组

RoleBinding 对象里定义了一个 subjects 字段,即“被作用者”。它的类型包括User、Group、ServiceAccount。

实际上,Kubernetes 里的“User”,也就是“用户”,只是一个授权系统里的逻辑概念。它需要通过外部认证服务,比如 Keystone来提供。或者,你也可以直接给 APIServer 指定一个用户名、密码文件。那么 Kubernetes 的授权系统,就能够从这个文件里找到对应的“用户”了。在大多数私有的使用场景下,我们经常使用K8S提供的内置用户ServiceAccount或通过Webhook回调外部系统进行认证返回定义的用户和组。

在 Kubernetes 中已经内置了很多个为系统保留的 ClusterRole,它们的名字都以 system: 开头,一般来说,这些系统 ClusterRole,是绑定给 Kubernetes 系统组件对应的ServiceAccount 使用的。

其中cluster-admin角色,对应的是整个K8S项目中的最高权限(verbs=*)。

ServiceAccount,在 Kubernetes 里对应的“用户”的名字是: system:serviceaccount:<ServiceAccount 名字 >,对应的内置”用户组“的名字是:system:serviceaccounts:<Namespace 名字 >,对应集群级别的“用户组”名字是:system:serviceaccounts

9.3 Pod使用ServiceAccount应用

1、首先,定义一个ServiceAccount,然后通过们通过编写 RoleBinding 的 YAML 文件,来为这个ServiceAccount 分配权限。Kubernetes 会为一个 ServiceAccount 自动创建并分配一个 Secret 对象,这个 Secret,就是这个 ServiceAccount 对应的、用来跟 APIServer 进行交互的授权文件,我们一般称它为:Token。Token 文件的内容一般是证书或者密码,它以一个 Secret对象的方式保存在 Etcd 当中。

2、创建Pod是,通过spec.serviceAccountName声明使用这个ServiceAccount,等Pod运行起来之后,该 ServiceAccount 的 token,也就是一个Secret 对象,被 Kubernetes 自动挂载到了容器的/var/run/secrets/kubernetes.io/serviceaccount 目录下,包括ca.crt、namespace、token等文件。最后,容器里的应用,就可以使用这个 ca.crt 来访问 APIServer 了,另外由于ServiceAccount权限已经被绑定了Role做了限制,因此只能访问运行的资源。

3、如果一个 Pod 没有声明 serviceAccountName,Kubernetes 会自动在它的 Namespace 下创建
一个名叫 default 的默认 ServiceAccount,然后分配给这个 Pod。但在这种情况下,这个默认 ServiceAccount 并没有关联任何 Role。也就是说,此时它有访问 APIServer 的绝大多数权限。当然,这个访问所需要的 Token,还是默认ServiceAccount 对应的 Secret 对象为它提供的。

posted @ 2022-10-12 23:35  hunter-w  阅读(107)  评论(0编辑  收藏  举报