使用准备好的 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
注意
如果您更新现有的 Kueue 安装,您可能需要重新启动 kueue-controller-manager
Pod,以便 Kueue 提取更新的配置。在这种情况下,运行
kubectl delete pods --all -n kueue-system
timeout
(waitForPodsReady.timeout
)是一个可选参数,默认为 5 分钟。
当已准许的工作负载的 timeout
过期,并且工作负载的 Pod 尚未全部调度(即,工作负载条件仍然为 PodsReady=False
),则工作负载的准许将被取消,相应作业将被暂停,并且工作负载将被重新排队。
blockAdmission
(waitForPodsReady.blockAdmission
)是一个可选参数。启用后,工作负载将按顺序准许,以防止死锁情况,如下面的示例所示。
重新排队策略
注意
backoffBaseSeconds
和 backoffMaxSeconds
在 Kueue v0.7.0 及更高版本中可用requeuingStrategy
(waitForPodsReady.requeuingStrategy
)包含可选参数
timestamp
backoffLimitCount
backoffBaseSeconds
backoffMaxSeconds
timestamp
字段定义了 Kueue 用于对队列中的工作负载排序的时间戳
驱逐
(默认):工作负载中具有PodsReadyTimeout
原因的lastTransitionTime
和Evicted=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
时,如果集群有足够的资源支持并发工作负载启动,则工作负载的准入可能会因排序而被不必要地减慢。