大规模集群仿真模拟与调度器压测方法

2023-01-18 12:11:50 浏览数 (1)

星辰算力平台基于深入优化云原生统一接入和多云调度,加固容器运行态隔离,挖掘技术增量价值,平台承载了腾讯内部的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 被调度到虚拟的节点上。

代码语言:javascript复制
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 版本发布。

0 人点赞