作者 青鸟

最近的业务中遇到了代理TCP业务流量的需求. 在实际方案调研中,有同学提出了是否可以通过sidecar去代理这部分流量,这只需要我们向业务的pod中注入sidecar即可,对实际的cpu和mem需求并不大,并且相应的改造方案较多.但是在结合实际情况分析后,这需要我们对sidecar的版本进行管理.在生产环境中,假如我们对pod的管理使用的是deployment,那么每一次的更新都会重建这个pod,而不是重建这个sidecar,这对我们的产品迭代不是个好事情,尤其是在初期迭代频繁的时候,c端用户会对流量的稳定有着敏感的反应.

那么我们可不可以让这个pod保留着所有的配置信息,只重建变更的container相关的配置呢.这就是pod的原地重建.

pod的原地重建也面临着一些挑战,举个简单的例子,原地重建也就意味着pod是不会从serivce的endpoint摘除,但是在升级过程中业务不应该提供服务,这会造成业务流量的损失,这也一个挑战.

在方案的实际调研中,Open Kruise这个项目中实现container级别的重建,当然container级别的重建需要满足一定的条件,这个后文再提.里面的实现方式也是蛮值得研究的,于是乎就顺便水一篇总结.

遇到的问题

在k8s的pod的controller中,当yaml发生变更时,都会选择pod重建,当然这么设计也是合理的,可以简单的实现pod的变更功能. 下面给出实验的yaml和对应的spec

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

看一下新建后的pod的配置信息,这里就给出一些关键的信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kubectl get po nginx-deployment-57d84f57dc-tgnk5 -oyaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2024-12-04T15:02:23Z"
  generateName: nginx-deployment-57d84f57dc-
  labels:
    app: nginx
    pod-template-hash: 57d84f57dc
  name: nginx-deployment-57d84f57dc-tgnk5
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: ReplicaSet
    name: nginx-deployment-57d84f57dc
    uid: 74cd1bc7-475e-444a-9bfd-784ed6b63b9f
  resourceVersion: "12102691"
  uid: 2a7a4a08-bd10-4aba-9d0f-5c0d53602057

  hostIP: 192.168.0.26
  phase: Running
  podIP: 172.16.2.38
  podIPs:
  - ip: 172.16.2.38
  qosClass: BestEffort
  startTime: "2024-12-04T15:02:23Z"

下面更新镜像为ubuntu,可以看出pod已经更新完毕了,并且相关的配置都已经变更了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: ubuntu:latest
        ports:
        - containerPort: 80
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kubectl get po nginx-deployment-57d84f57dc-djvq2 -oyaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2024-12-04T15:49:12Z"
  generateName: nginx-deployment-57d84f57dc-
  labels:
    app: nginx
    pod-template-hash: 57d84f57dc
  name: nginx-deployment-57d84f57dc-djvq2
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: ReplicaSet
    name: nginx-deployment-57d84f57dc
    uid: 0739a771-dedf-4456-b3f7-67ca18ccb645
  resourceVersion: "12130996"
  uid: ab98720f-4188-479b-bff2-db84d59d7106

  hostIP: 192.168.0.26
  phase: Running
  podIP: 172.16.2.25
  podIPs:
  - ip: 172.16.2.25
  qosClass: BestEffort
  startTime: "2024-12-04T15:49:12Z"

Open Kruise 的解决思路

单pod原地重建

其实单个的pod是可以原地升级的,在原生 kube-apiserver 中,对 Pod 对象的更新请求有严格的 validation 校验逻辑:

1
2
3
4
// validate updateable fields:
// 1.  spec.containers[*].image
// 2.  spec.initContainers[*].image
// 3.  spec.activeDeadlineSeconds

只修改这三个字段的话是不会对pod重建的,重建的只是container.简单来说,对于一个已经创建出来的 Pod,在 Pod Spec 中只允许修改 containers/initContainers 中的 image 字段,以及 activeDeadlineSeconds 字段。对 Pod Spec 中所有其他字段的更新,都会被 kube-apiserver 拒绝。

如何保证原地升级过程中流量无损

这一部分在阅读源码的时候是理解的难点.下面给出相关的理解

在 Kubernetes 1.12 版本之前,一个 Pod 是否处于 Ready 状态只是由 kubelet 根据容器状态来判定:如果 Pod 中容器全部 ready,那么 Pod 就处于 Ready 状态。

但事实上,很多时候上层 operator 或用户都需要能控制 Pod 是否 Ready 的能力。因此,Kubernetes 1.12 版本之后提供了一个 readinessGates 功能来满足这个场景。如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Pod
spec:
  readinessGates:
  - conditionType: MyDemo
status:
  conditions:
  - type: MyDemo
    status: "True"
  - type: ContainersReady
    status: "True"
  - type: Ready
    status: "True"

目前 kubelet 判定一个 Pod 是否 Ready 的两个前提条件:

Pod 中容器全部 Ready(其实对应了 ContainersReady condition 为 True); 如果 pod.spec.readinessGates 中定义了一个或多个 conditionType,那么需要这些 conditionType 在 pod.status.conditions 中都有对应的 status: “true” 的状态。 只有满足上述两个前提,kubelet 才会上报 Ready condition 为 True。

在 Kubernetes 中,一个 Pod 是否 Ready 就代表了它是否可以提供服务。因此,像 Service 这类的流量入口都会通过判断 Pod Ready 来选择是否能将这个 Pod 加入 endpoints 端点中。

从 Kubernetes 1.12+ 之后,operator/controller 这些组件也可以通过设置 readinessGates 和更新 pod.status.conditions 中的自定义 type 状态,来控制 Pod 是否可用。

因此,得出个实现原理:可以在 pod.spec.readinessGates 中定义一个叫 InPlaceUpdateReady 的 conditionType。

在原地升级时:

先将 pod.status.conditions 中的 InPlaceUpdateReady condition 设为 “False”,这样就会触发 kubelet 将 Pod 上报为 NotReady,从而使流量组件(如 endpoint controller)将这个 Pod 从服务端点摘除; 再更新 pod spec 中的 image 触发原地升级。 原地升级结束后,再将 InPlaceUpdateReady condition 设为 “True”,使 Pod 重新回到 Ready 状态。

另外在原地升级的两个步骤中,第一步将 Pod 改为 NotReady 后,流量组件异步 watch 到变化并摘除端点可能是需要一定时间的。因此我们也提供优雅原地升级的能力,即通过 gracePeriodSeconds 配置在修改 NotReady 状态和真正更新 image 触发原地升级两个步骤之间的静默期时间。

总结

在使用流程中,先将Pod的readinessGates 设置为false,将流量从endpoint中摘除,之后再实现通过单个pod的重建来完成pod的原地升级.Open Kruise的实现结合了原来的k8d的特性,完成了pod的原地重建.阅读源代码也是非常有趣的,是值得一看的增强k8s workload开源项目.在实际的开发中,可以通过镜像预热+原地重建大幅度增加pod的弹性特性,将pod升级的开销大幅度降低.

参考文章