Skip to content

Kubernetes编排和调度能力 #52

@QingyaFan

Description

@QingyaFan

Kubernetes是一个容器编排系统,编排的一个主要方面是 部署管理,其中部署流程的管控是其依赖的主要能力,这部分能力主要体现在对Pod和容器(container)的生命周期管理能力,但也有一些 crd 实现了暂停点这样的编排能力。

能力

一组Pod的编排

暂停点

社区 openkruise 的cloneset实现了暂停点功能:更新过程中可暂停更新,可以利用这项能力实现灰度发布这样的发布策略。其实现原理是在 Reconcile 过程中检查 cs.Spec.Paused 的值,如果是 true 则表示启用暂停,Reconcile 不做任何操作直接返回。

优先选择符合条件的 Pod 执行某操作

场景:例如横向缩容时优先选择状态异常的 Pod

在cloneset中,spec.scaleStrategy.podsToDelete记录了要删除的Pod,我们可以有一个定时程序定时更新cloneset中异常状态pod,当缩容时优先delete标记为异常的 Pods。

Pod 级别

Pod生命周期过程中的编排控制能力有创建(调度)、删除(优雅退出)等

调度

调度过程决定了 Pod 创建到哪个Node,用到的策略主要有 Pod 和 Node 亲和性、Pod 间亲和性、反亲和性:

Pod 和 Node 亲和性,包括标签(Labels)、污点(Taints)、容忍度(Tolerations)

-- 标签,Node的特征标记,可以把Pod定向调度到满足资源需求的Node节点上,例如 需要 ssd磁盘、A30 GPU等硬件资源
-- 污点,排斥一类 pod
-- 容忍度,允许调度器调度带有对应污点的Pod,容忍度并不保证调度到带有污点的机器,所以往往结合 label 和 affinity 使用

例子:圈一批Node供给某业务,不允许其它业务使用(i.e. 实现资源隔离)

假设有一个集群3个node,给自动驾驶大模型业务使用一台,节点名为 kind-worker2,我们需要:

  1. 给Node打标签,kubectl label nodes kind-worker2 business=self-drive-models
image
  1. 给Node打污点,防止其它业务被调度过来,kubectl taint nodes kind-worker2 business=self-drive-models:NoSchedule
image
  1. 为允许的业务Pod添加容忍度和节点亲和性,容忍度保证Pod可以被调度到带有该污点的节点上,亲和性保证Pod只会调度到带有特定标签的节点上,否则也可能调度到其它机器上
apiVersion: v1
kind: Pod
metadata:
  name: self-drive-models-ins-01
spec:
  tolerations:
  - key: "business"
    operator: "Equal"
    value: "self-drive-models"
    effect: "NoSchedule"
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: "business"
            operator: "In"
            values:
            - "self-drive-models"
  containers:
  - name: main
    image: nginx:1.25.5-alpine

部署业务Pod,我们可以看到确实调度到了指定的node

image

如果我们写错亲和性标签到一个不存在的node,那么调度器就会报错:

image

从报错信息中我们也可以看到,control-plane 节点,也就是 master 节点也是通过 taint 防止业务pod调度的。

这也反映出 NodeAffinity 是强制的,没有满足的,Pod 就会 Pending 等待有满足条件的 Node 出现。

Pod间亲和性与反亲和性

与 Node 亲和性不同的是,Pod间亲和性与反亲和性是基于在节点上运行的Pod标签来约束Pod可以调度的节点,而不是基于节点上的标签。

主要效果是体现应用间或同一应用的不同实例的拓扑关系。例如如果两个应用间有直接访问关系,部署到一台机器上可能是更好的选择;如一个应用是重IO操作,最好避免多个实例同时部署到一个机器,否则磁盘IO会限制应用的性能;

例子:某使用GPU的模型预估服务,有很高的网络吞吐,需要打散防止Node网卡成为瓶颈

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: gpu-app
  template:
    metadata:
      labels:
        app: gpu-app
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - gpu-app
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: gpu-container
        image: nginx:1.25.5-alpine

创建deployment,查看调度结果:

image

会发现有一个在等待调度(Pending),这是因为 worker2 被打上了NoSchedule,且我们指定的是 硬反亲和性,其实还有 软反亲和性(preferredDuringSchedulingIgnoredDuringExecution) 来解决这种问题(集群node不够):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: gpu-app
  template:
    metadata:
      labels:
        app: gpu-app
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - gpu-app
              topologyKey: "kubernetes.io/hostname"
      topologySpreadConstraints:
      - maxSkew: 2
        topologyKey: "kubernetes.io/hostname"
        whenUnsatisfiable: ScheduleAnyway
        labelSelector:
          matchLabels:
            app: gpu-app
      containers:
      - name: gpu-container
        image: nginx:1.25.5-alpine

创建deployment后,都调度上了:

image

注意我们用了 topologySpreadConstraints 保证每个 Node 同一应用的副本不会超过 2 个。

优雅退出

terminationGracePeriodSeconds,当执行 kubectl delete pod/xxx 时,pod不会马上被delete,而是等待 terminationGracePeriodSeconds 指定的时间后,再退出。

容器级别

主要是容器生命周期钩子,包括 postStartpreStop

  • postStart,在容器创建之后立即执行,但不能保证回调会在容器入口点(ENTRYPOINT)之前执行。这是因为 容器启动过程包括创建和启动两个阶段,ENTRYPOINT 或 CMD 在启动阶段,所以 postStart 不一定在启动完成后,可能的原因是脚本启动进程前做了其它初始化工作,例如加载数据,或者启动时间长,而k8s不是容器运行引擎,控制粒度没有这么细致,所以不做这个保证;
  • preStop,指容器收到终止信号(SIGTERM)时和在真正退出前这段时间

例子

应用进程停止之前需要做一些清理工作,是比较常见的诉求,这个场景下如何实现业务进程优雅退出?

  1. 如果业务进程实现了优雅退出逻辑(监听 SIGTERM信号并执行优雅退出动作),那么我们只需要设置容器的 terminationGracePeriodSeconds 即可,它会在容器收到 SIGTERM 之后等待设置的时长。如果在指定的时间后,k8s会发出 SIGKILL信号强制容器退出,与之对应的,应用提前完成工作,容器提前退出。
  2. 如果业务进程没实现优雅退出逻辑,而是把退出前的工作放在了容器内某个脚本中,则需要结合容器生命周期钩子和优雅退出等待两个能力。如下 example.yaml:
apiVersion: v1
kind: Pod
metadata:
  name: graceful-shutdown-example
spec:
  terminationGracePeriodSeconds: 60
  containers:
  - name: my-app
    image: redis:7.2.5
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "echo 'Received SIGTERM, shutting down...'; sleep 5"]
  1. kubectl apply -f example.yaml 会创建 pod/graceful-shutdown-example
  2. kubectl logs -f pod/graceful-shutdown-example 监视容器输出
  3. kubectl delete pod/graceful-shutdown-example 向容器发送 SIGTERM 信号

我们会看到控制台 5 秒后输出 Received SIGTERM, shutting down...后退出

image

接下来把

terminationGracePeriodSeconds: 60
...
          command: ["/bin/sh", "-c", "echo 'Received SIGTERM, shutting down...'; sleep 5"]

改成

terminationGracePeriodSeconds: 10
          command: ["/bin/sh", "-c", "echo 'Received SIGTERM, shutting down...'; sleep 20"]

即模拟pod等待时间小于容器做清理任务的时间,这时 10秒后就会输出然后退出。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions