星辰算力平台基于深入优化云原生统一接入和多云调度,加固容器运行态隔离,挖掘技术增量价值,平台承载了腾讯内部的CPU和异构算力服务,是腾讯内部大规模离线作业、资源统一调度平台。
背景
在大规模 Kubernetes 集群中,集群瞬息万变,每时每刻可能都有相关用户、集群组件、运维人员对集群进行操作。根据大规模集群的注意事项,Kubernetes v1.26 单个集群支持的最大节点数为 5000。更具体地说,Kubernetes 旨在适应满足以下所有标准的配置:
- 每个节点的 Pod 数量不超过 110
- 节点数不超过 5000
- Pod 总数不超过 150000
- 容器总数不超过 300000
在这样大规模的集群下,通常我们需要压测各类组件来保障集群在突发状况(如高峰时间段)下的性能和可靠性。对于 apiserver、etcd 这类基础组件,我们只要将服务启动后,可以非常容易地进行压测,如通过 clusterloader2 并发创建大量的请求等方式。但针对调度器,我们却需要一个含有大量节点的集群进行模拟测试,但通常情况下很难短时准备如此多的空闲节点,且测试时对节点资源也是一种浪费。
万幸的是,调度器是负责将 Pod 调度到合适的 Node 上,并不关心后续 Pod 的生产过程。如果能够在集群中虚拟出大量的 Node,就可以完成大规模集群的模拟环境搭建。碰巧,Kubernetes 社区开源的新项目 KWOK(Kubernetes WithOut Kubelet) 为我们带来了解决方案。本文将阐述如何快速模拟大规模测试环境(你甚至可以在自己的 minikube 上搭建),并简要给出调度器的压测结果。同时,由于我们在生产环境中大量使用拓扑感知调度功能并已经贡献至 Crane 开源社区,本文中的压测环境也是基于带有拓扑感知调度增强功能的 crane-scheduler。
- 大规模集群的注意事项:https://kubernetes.io/zh-cn/docs/setup/best-practices/cluster-large/
- clusterloader2:https://github.com/kubernetes/perf-tests/blob/master/clusterloader2/docs/design.md
- KWOK:https://github.com/kubernetes-sigs/kwok
- Crane 开源社区:https://gocrane.io/
- crane-scheduler:https://github.com/gocrane/crane-scheduler
环境准备
虚拟节点
环境信息:
- 测试环境:含有300个真实节点的Kubernetes v1.22集群
- kwok 版本:v0.0.1(这个版本用于调度测试可能有点小问题,见文章末尾)
部署 kwok:
- 按照此文档进行部署(https://kwok-demo.netlify.app/docs/user/kwok-in-cluster/)
- 查看 kwok-controller 已被正确部署:
kubectl get pod -n kube-system app=kwok-controller
创建节点,按照生产环境下的 CVM 机型进行配置:
代码语言:javascript复制cat << EOF > /tmp/node.yaml
apiVersion: v1
kind: Node
metadata:
annotations:
node.alpha.kubernetes.io/ttl: "0"
kwok.x-k8s.io/node: fake
labels:
beta.kubernetes.io/arch: amd64
beta.kubernetes.io/os: linux
kubernetes.io/arch: amd64
kubernetes.io/hostname: {NODE_NAME}
kubernetes.io/os: linux
kubernetes.io/role: agent
node-role.kubernetes.io/agent: ""
type: kwok
name: {NODE_NAME}
spec:
taints: # Avoid scheduling actual running pods to fake Node
- effect: NoSchedule
key: kwok.x-k8s.io/node
value: fake
status:
allocatable:
cpu: 179800m
ephemeral-storage: "289839513121"
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 64836160Ki
pods: "64"
capacity:
cpu: "180"
ephemeral-storage: 307125Mi
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 65655360Ki
pods: "64"
nodeInfo:
architecture: amd64
bootID: ""
containerRuntimeVersion: ""
kernelVersion: ""
kubeProxyVersion: fake
kubeletVersion: fake
machineID: ""
operatingSystem: linux
osImage: ""
systemUUID: ""
phase: Running
EOF
创建100个节点:
代码语言:javascript复制for i in {0..99}; do sed "s/{NODE_NAME}/kwok-node-$i/g" /tmp/node.yaml | kubectl apply -f -; done
可以看到100个节点均已 running:
代码语言:javascript复制$ kubectl get node
NAME STATUS ROLES AGE VERSION
9.134.230.65 Ready control-plane,master 122d v1.23.3
kwok-node-0 Ready agent 70m fake
kwok-node-1 Ready agent 71m fake
...
kwok-node-99 Ready agent 70m fake
部署Pod
在集群中按照文档创建 Pod 进行调度器测试。由于 kwok 的节点含有特定的标签和污点,所以 Pod 最好带有特定的 nodeAffinity
和 tolerations
,这样可以确保 Pod 被调度到虚拟的节点上。
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: fake-pod
namespace: default
spec:
replicas: 10
selector:
matchLabels:
app: fake-pod
template:
metadata:
labels:
app: fake-pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: type
operator: In
values:
- kwok
# A taints was added to an automatically created Node.
# You can remove taints of Node or add this tolerations.
tolerations:
- key: "kwok.x-k8s.io/node"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: fake-container
image: fake-image
EOF
部署NRT对象(可选)
针对带有拓扑感知插件的 crane-scheduler,需要创建 NRT 对象。
代码语言:javascript复制cat << EOF > /tmp/nrt.yaml
apiVersion: topology.crane.io/v1alpha1
kind: NodeResourceTopology
metadata:
name: {NODE_NAME}
ownerReferences:
- apiVersion: v1
blockOwnerDeletion: true
controller: true
kind: Node
name: {NODE_NAME}
uid: {NODE_UID}
craneManagerPolicy:
cpuManagerPolicy: Static
topologyManagerPolicy: SingleNUMANodePodLevel
reserved:
cpu: 200m
memory: 700Mi
zones:
- costs:
- name: node0
value: 10
- name: node1
value: 20
name: node0
resources:
allocatable:
cpu: 89800m
memory: 31932648Ki
capacity:
cpu: "90"
memory: 32649448Ki
reservedCPUNums: 1
type: Node
- costs:
- name: node0
value: 20
- name: node1
value: 10
name: node1
resources:
allocatable:
cpu: "90"
memory: 33005912Ki
capacity:
cpu: "90"
memory: 33005912Ki
type: Node
EOF
创建对应的 NRT 对象:
代码语言:javascript复制for i in {0..99}; do uid=$(kubectl get node kwok-node-$i -ojsonpath='{.metadata.uid}'); sed "s/{NODE_NAME}/kwok-node-$i/g;s/{NODE_UID}/$uid/g" /tmp/nrt.yaml | kubectl apply -f -; done
删除虚拟节点
测试完成后如果想删除虚拟节点,可以直接删除掉所有的 kwok 节点。
代码语言:javascript复制kubectl delete node -l type=kwok
测试程序与误差分析
这里简单准备了一个测试程序(https://github.com/Garrybest/k8s-example)进行调度测试,它会统计某一个 namespace 下所有 Pod 的调度时间,通过 Pod 的 Condition 和 CreationTimestamp 进行判断,最后进行加和以及求平均的工作,这种方式是相对更加精确的。但由于 metav1.Time
结构在传输时采用 RFC3339 进行编码,只能精确到秒,因此会损失部分精度,在大规模测试中可以忽略不计。
当然,你也可以通过暴露调度器的 metrics 来做这项工作,但是我十分不建议在大规模压测时通过 grafana 看板进行调度时间的统计。因为调度器暴露的调度时间指标是通过 histogram 直方图的方式,而 histogram 是假定位于每个 bucket 的样本在该 bucket 内满足均匀分布。当压测进行时,短时间大量创建 Pod,必定有部分 Pod 的调度时长达到分钟级别,此时其所属的 bucket 范围更广,均匀分布的条件就越不可能成立,从 metrics 这统计的调度时间会产生很大的误差(https://hulining.gitbook.io/prometheus/practices/histograms#errors-of-quantile-estimation)。
测试结果
下表展示了最终测试结果。在我的环境下并没有进行调度器调优,也没有使用 clusterloader2 进行更加复杂的测试。读者可根据自己的需求选择不同的压测程序和工具。测试时设置调度器的QPS=200,Burst=400。
测试用例 | 节点数 | 并发创建Pod数 | 总调度时间(s) | 平均调度时间(s) | 每秒调度Pod数 | CPU峰值 | 内存峰值(GiB) | goroutines峰值 |
---|---|---|---|---|---|---|---|---|
1 | 5000 | 1000 | 8 | 3.849 | 125 | 0.29 | 1.93 | 502 |
2 | 5000 | 2000 | 17 | 8.917 | 117.6470588 | 0.578 | 3.32 | 1090 |
3 | 5000 | 5000 | 45 | 25.7236 | 111.1111111 | 1.39 | 6.09 | 1900 |
4 | 5000 | 10000 | 92 | 53.2814 | 108.6956522 | 3.15 | 10.3 | 3300 |
可以明显看出,每秒调度Pod数大概是在110左右达到极限,这对于大部分场景来说已经够了。我们发现耗时较多的阶段主要是Bind,通过调大QPS和Burst参数还可以将每秒调度Pod数继续提升至160左右。同时短时间创建大量的 Pod 会导致调度器内存飙升,这可能与调度器内部的 cache 有关。
后记
测试过程中顺手修了两个 kwok 的 Bug:
- #141:kwok 原本把 PodScheduled 的 Condition 覆盖了,导致测试程序获取不到调度时长。
- #142:kwok 在某些特殊情况下无法正常删除虚拟的节点。
这在 kwok-v0.0.1 的镜像里面是没有包含的,读者可自行从 master 分支编译一个镜像或等待下一个 release 版本发布。