使用准备好的 Pod 设置全有或全无

全有或全无调度的基于超时的实现

某些作业需要所有 Pod 同时运行才能操作;例如,需要 Pod 间通信的同步分布式训练或基于 MPI 的作业。在默认的 Kueue 配置中,如果资源的物理可用性与 Kueue 中配置的配额不匹配,则此类作业对可能会陷入死锁。如果其 Pod 按顺序调度,则同一对作业可以运行至完成。

为了满足此要求,在 0.3.0 版本中,我们引入了一种通过标志 waitForPodsReady 配置的加入机制,它提供了全有或全无调度的简单实现。启用后,工作负载将由 Kueue 监控,直到其所有 Pod 都准备就绪(即已调度、正在运行并且通过了可选的准备就绪探测)。如果工作负载的所有 Pod 未在配置的超时时间内准备就绪,则工作负载将被驱逐并重新排队。

此页面展示了如何配置 Kueue 以使用 waitForPodsReady,它是全有或全无调度的简单实现。此页面的目标受众是 批处理管理员

开始之前

确保满足以下条件

  • Kubernetes 集群正在运行。
  • kubectl 命令行工具已与集群通信。
  • Kueue 已安装,版本为 0.3.0 或更高版本。

启用 waitForPodsReady

按照 此处 所述的说明,通过使用以下字段扩展配置来安装发布版本

    waitForPodsReady:
      enable: true
      timeout: 10m
      blockAdmission: true
      requeuingStrategy:
        timestamp: Eviction | Creation
        backoffLimitCount: 5
        backoffBaseSeconds: 60
        backoffMaxSeconds: 3600

timeoutwaitForPodsReady.timeout)是一个可选参数,默认为 5 分钟。

当已准许的工作负载的 timeout 过期,并且工作负载的 Pod 尚未全部调度(即,工作负载条件仍然为 PodsReady=False),则工作负载的准许将被取消,相应作业将被暂停,并且工作负载将被重新排队。

blockAdmissionwaitForPodsReady.blockAdmission)是一个可选参数。启用后,工作负载将按顺序准许,以防止死锁情况,如下面的示例所示。

重新排队策略

功能状态自 Kueue v0.6 起稳定

requeuingStrategywaitForPodsReady.requeuingStrategy)包含可选参数

  • timestamp
  • backoffLimitCount
  • backoffBaseSeconds
  • backoffMaxSeconds

timestamp 字段定义了 Kueue 用于对队列中的工作负载排序的时间戳

  • 驱逐(默认):工作负载中具有 PodsReadyTimeout 原因的 lastTransitionTimeEvicted=true 条件。
  • 创建:工作负载中的 creationTimestamp。

如果您想将被 PodsReadyTimeout 驱逐的工作负载重新排队到队列中的原始位置,则应将时间戳设置为 创建

Kueue 会重新排队被 PodsReadyTimeout 原因驱逐的工作负载,直到重新排队的次数达到 backoffLimitCount。如果您未为 backoffLimitCount 指定任何值,则会根据 timestamp 反复且无休止地将工作负载重新排队到队列中。一旦重新排队的次数达到限制,Kueue 将停用工作负载

每次连续超时后重新排队工作负载的时间会以 2 的指数级增加。第一个延迟由 backoffBaseSeconds 参数(默认为 60)确定。您可以通过设置 backoffMaxSeconds(默认为 3600)来配置最大后退时间。使用默认值,驱逐的工作负载将在大约 60、120、240、...、3600、...、3600 秒后重新排队。即使后退时间达到 backoffMaxSeconds,Kueue 仍会继续使用 backoffMaxSeconds 重新排队驱逐的工作负载,直到重新排队的次数达到 backoffLimitCount

示例

在此示例中,我们演示了在 Kueue 中启用 waitForPodsReady 的影响。我们创建了两个作业,这两个作业都需要其所有 Pod 同时运行才能完成。该集群有足够的资源同时支持其中一个作业的运行,但无法同时支持两个作业的运行。

注意在此示例中,我们使用禁用了自动缩放的集群,以便模拟资源配置问题,以满足配置的集群配额。

1. 准备

首先,检查集群中可分配的内存量。在许多情况下,可以使用此命令完成此操作

TOTAL_ALLOCATABLE=$(kubectl get node --selector='!node-role.kubernetes.io/master,!node-role.kubernetes.io/control-plane' -o jsonpath='{range .items[*]}{.status.allocatable.memory}{"\n"}{end}' | numfmt --from=auto | awk '{s+=$1} END {print s}')
echo $TOTAL_ALLOCATABLE

在我们的案例中,此输出为 8838569984,为了示例的目的,可以近似为 8429Mi

配置 ClusterQueue 配额

我们通过将集群中可分配的总内存加倍来配置内存类型,以便模拟配置问题。

将以下集群队列配置另存为 cluster-queues.yaml

apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: "default-flavor"
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: "cluster-queue"
spec:
  namespaceSelector: {}
  resourceGroups:
  - coveredResources: ["memory"]
    flavors:
    - name: "default-flavor"
      resources:
      - name: "memory"
        nominalQuota: 16858Mi # double the value of allocatable memory in the cluster
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  namespace: "default"
  name: "user-queue"
spec:
  clusterQueue: "cluster-queue"

然后,通过以下方式应用配置

kubectl apply -f cluster-queues.yaml

准备作业模板

将以下作业模板保存在 job-template.yaml 文件中。请注意 _ID_ 占位符,它将被替换为创建两个作业的配置。此外,请注意将容器的内存字段配置为每个 Pod 的可分配总内存的 75%。在我们的案例中,这是 75%*(8429Mi/20)=316Mi。在这种情况下,没有足够的资源同时运行这两个作业的所有 Pod,从而有陷入死锁的风险。

apiVersion: v1
kind: Service
metadata:
  name: svc_ID_
spec:
  clusterIP: None
  selector:
    job-name: job_ID_
  ports:
  - name: http
    protocol: TCP
    port: 8080
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: script-code_ID_
data:
  main.py: |
    from http.server import BaseHTTPRequestHandler, HTTPServer
    from urllib.request import urlopen
    import sys, os, time, logging

    logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
    serverPort = 8080
    INDEX_COUNT = int(sys.argv[1])
    index = int(os.environ.get('JOB_COMPLETION_INDEX'))
    logger = logging.getLogger('LOG' + str(index))

    class WorkerServer(BaseHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200)
            self.end_headers()
            if "exit" in self.path:
              self.wfile.write(bytes("Exiting", "utf-8"))
              self.wfile.close()
              sys.exit(0)
            else:
              self.wfile.write(bytes("Running", "utf-8"))

    def call_until_success(url):
      while True:
        try:
          logger.info("Calling URL: " + url)
          with urlopen(url) as response:
            response_content = response.read().decode('utf-8')
            logger.info("Response content from %s: %s" % (url, response_content))
            return
        except Exception as e:
          logger.warning("Got exception when calling %s: %s" % (url, e))
        time.sleep(1)

    if __name__ == "__main__":
      if index == 0:
        for i in range(1, INDEX_COUNT):
          call_until_success("http://job_ID_-%d.svc_ID_:8080/ping" % i)
        logger.info("All workers running")

        time.sleep(10) # sleep 10s to simulate doing something

        for i in range(1, INDEX_COUNT):
          call_until_success("http://job_ID_-%d.svc_ID_:8080/exit" % i)
        logger.info("All workers stopped")
      else:
        webServer = HTTPServer(("", serverPort), WorkerServer)
        logger.info("Server started at port %s" % serverPort)
        webServer.serve_forever()    
---

apiVersion: batch/v1
kind: Job
metadata:
  name: job_ID_
  labels:
    kueue.x-k8s.io/queue-name: user-queue
spec:
  parallelism: 20
  completions: 20
  completionMode: Indexed
  suspend: true
  template:
    spec:
      subdomain: svc_ID_
      volumes:
      - name: script-volume
        configMap:
          name: script-code_ID_
      containers:
      - name: main
        image: python:bullseye
        command: ["python"]
        args:
        - /script-path/main.py
        - "20"
        ports:
        - containerPort: 8080
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            memory: "316Mi" # choose the value as 75% * (total allocatable memory / 20)
        volumeMounts:
          - mountPath: /script-path
            name: script-volume
      restartPolicy: Never
  backoffLimit: 0

附加快速作业

我们还准备了一个附加作业,以增加时间上的差异,从而使死锁更有可能发生。将以下 yaml 另存为 quick-job.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: quick-job
  annotations:
    kueue.x-k8s.io/queue-name: user-queue
spec:
  parallelism: 50
  completions: 50
  suspend: true
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: sleep
        image: bash:5
        command: ["bash"]
        args: ["-c", 'echo "Hello world"']
        resources:
          requests:
            memory: "1"
  backoffLimit: 0

2. 在默认配置下诱发死锁(可选)

运行作业

sed 's/_ID_/1/g' job-template.yaml > /tmp/job1.yaml
sed 's/_ID_/2/g' job-template.yaml > /tmp/job2.yaml
kubectl create -f quick-job.yaml
kubectl create -f /tmp/job1.yaml
kubectl create -f /tmp/job2.yaml

过一会儿,通过以下方式检查 Pod 的状态

kubectl get pods

输出如下(为简洁起见,省略了 quick-job 的 pod)

NAME            READY   STATUS      RESTARTS   AGE
job1-0-9pvs8    1/1     Running     0          28m
job1-1-w9zht    1/1     Running     0          28m
job1-10-fg99v   1/1     Running     0          28m
job1-11-4gspm   1/1     Running     0          28m
job1-12-w5jft   1/1     Running     0          28m
job1-13-8d5jk   1/1     Running     0          28m
job1-14-h5q8x   1/1     Running     0          28m
job1-15-kkv4j   0/1     Pending     0          28m
job1-16-frs8k   0/1     Pending     0          28m
job1-17-g78g8   0/1     Pending     0          28m
job1-18-2ghmt   0/1     Pending     0          28m
job1-19-4w2j5   0/1     Pending     0          28m
job1-2-9s486    1/1     Running     0          28m
job1-3-s9kh4    1/1     Running     0          28m
job1-4-52mj9    1/1     Running     0          28m
job1-5-bpjv5    1/1     Running     0          28m
job1-6-7f7tj    1/1     Running     0          28m
job1-7-pnq7w    1/1     Running     0          28m
job1-8-7s894    1/1     Running     0          28m
job1-9-kz4gt    1/1     Running     0          28m
job2-0-x6xvg    1/1     Running     0          28m
job2-1-flkpj    1/1     Running     0          28m
job2-10-vf4j9   1/1     Running     0          28m
job2-11-ktbld   0/1     Pending     0          28m
job2-12-sf4xb   1/1     Running     0          28m
job2-13-9j7lp   0/1     Pending     0          28m
job2-14-czc6l   1/1     Running     0          28m
job2-15-m77zt   0/1     Pending     0          28m
job2-16-7p7fs   0/1     Pending     0          28m
job2-17-sfdmj   0/1     Pending     0          28m
job2-18-cs4lg   0/1     Pending     0          28m
job2-19-x66dt   0/1     Pending     0          28m
job2-2-hnqjv    1/1     Running     0          28m
job2-3-pkwhw    1/1     Running     0          28m
job2-4-gdtsh    1/1     Running     0          28m
job2-5-6swdc    1/1     Running     0          28m
job2-6-qb6sp    1/1     Running     0          28m
job2-7-grcg4    0/1     Pending     0          28m
job2-8-kg568    1/1     Running     0          28m
job2-9-hvwj8    0/1     Pending     0          28m

这些作业现在处于死锁状态,无法继续执行。

清理

通过以下方式清理作业

kubectl delete -f quick-job.yaml
kubectl delete -f /tmp/job1.yaml
kubectl delete -f /tmp/job2.yaml

3. 启用 waitForPodsReady 运行

启用 waitForPodsReady

按照 此处 的说明更新 Kueue 配置。

运行作业

运行 start.sh 脚本

sed 's/_ID_/1/g' job-template.yaml > /tmp/job1.yaml
sed 's/_ID_/2/g' job-template.yaml > /tmp/job2.yaml
kubectl create -f quick-job.yaml
kubectl create -f /tmp/job1.yaml
kubectl create -f /tmp/job2.yaml

监控进度

在几秒钟的间隔内执行以下命令以监控进度

kubectl get pods

为简洁起见,我们省略了已完成的 quick 作业的 pod。

job1 启动时输出,请注意 job2 仍处于挂起状态

NAME            READY   STATUS              RESTARTS   AGE
job1-0-gc284    0/1     ContainerCreating   0          1s
job1-1-xz555    0/1     ContainerCreating   0          1s
job1-10-2ltws   0/1     Pending             0          1s
job1-11-r4778   0/1     ContainerCreating   0          1s
job1-12-xx8mn   0/1     Pending             0          1s
job1-13-glb8j   0/1     Pending             0          1s
job1-14-gnjpg   0/1     Pending             0          1s
job1-15-dzlqh   0/1     Pending             0          1s
job1-16-ljnj9   0/1     Pending             0          1s
job1-17-78tzv   0/1     Pending             0          1s
job1-18-4lhw2   0/1     Pending             0          1s
job1-19-hx6zv   0/1     Pending             0          1s
job1-2-hqlc6    0/1     ContainerCreating   0          1s
job1-3-zx55w    0/1     ContainerCreating   0          1s
job1-4-k2tb4    0/1     Pending             0          1s
job1-5-2zcw2    0/1     ContainerCreating   0          1s
job1-6-m2qzw    0/1     ContainerCreating   0          1s
job1-7-hgp9n    0/1     ContainerCreating   0          1s
job1-8-ss248    0/1     ContainerCreating   0          1s
job1-9-nwqmj    0/1     ContainerCreating   0          1s

job1 正在运行且 job2 现在已解除挂起,因为 job 已分配所有必需的资源时输出

NAME            READY   STATUS      RESTARTS   AGE
job1-0-gc284    1/1     Running     0          9s
job1-1-xz555    1/1     Running     0          9s
job1-10-2ltws   1/1     Running     0          9s
job1-11-r4778   1/1     Running     0          9s
job1-12-xx8mn   1/1     Running     0          9s
job1-13-glb8j   1/1     Running     0          9s
job1-14-gnjpg   1/1     Running     0          9s
job1-15-dzlqh   1/1     Running     0          9s
job1-16-ljnj9   1/1     Running     0          9s
job1-17-78tzv   1/1     Running     0          9s
job1-18-4lhw2   1/1     Running     0          9s
job1-19-hx6zv   1/1     Running     0          9s
job1-2-hqlc6    1/1     Running     0          9s
job1-3-zx55w    1/1     Running     0          9s
job1-4-k2tb4    1/1     Running     0          9s
job1-5-2zcw2    1/1     Running     0          9s
job1-6-m2qzw    1/1     Running     0          9s
job1-7-hgp9n    1/1     Running     0          9s
job1-8-ss248    1/1     Running     0          9s
job1-9-nwqmj    1/1     Running     0          9s
job2-0-djnjd    1/1     Running     0          3s
job2-1-trw7b    0/1     Pending     0          2s
job2-10-228cc   0/1     Pending     0          2s
job2-11-2ct8m   0/1     Pending     0          2s
job2-12-sxkqm   0/1     Pending     0          2s
job2-13-md92n   0/1     Pending     0          2s
job2-14-4v2ww   0/1     Pending     0          2s
job2-15-sph8h   0/1     Pending     0          2s
job2-16-2nvk2   0/1     Pending     0          2s
job2-17-f7g6z   0/1     Pending     0          2s
job2-18-9t9xd   0/1     Pending     0          2s
job2-19-tgf5c   0/1     Pending     0          2s
job2-2-9hcsd    0/1     Pending     0          2s
job2-3-557lt    0/1     Pending     0          2s
job2-4-k2d6b    0/1     Pending     0          2s
job2-5-nkkhx    0/1     Pending     0          2s
job2-6-5r76n    0/1     Pending     0          2s
job2-7-pmzb5    0/1     Pending     0          2s
job2-8-xdqtp    0/1     Pending     0          2s
job2-9-c4rcl    0/1     Pending     0          2s

一旦 job1 完成,它将释放 job2 运行其 pod 以取得进展所需的资源。最后,所有作业都完成。

清理

通过以下方式清理作业

kubectl delete -f quick-job.yaml
kubectl delete -f /tmp/job1.yaml
kubectl delete -f /tmp/job2.yaml

缺点

启用 waitForPodsReady 时,如果集群有足够的资源支持并发工作负载启动,则工作负载的准入可能会因排序而被不必要地减慢。