大规模 Kubernetes 集群调优设置
0. 前置条件
0.1 网段划分
首先集群网段划分,支持5000+ Node, 10000+ Service 和 100000+ Pod, 因此需要:
1个B段IP 用于Node划分 3个B段IP 用于Pod Service划分
为了支持1w以上Pod,避免iptable 规则指数级增长炸裂,必须使用 ipvs 模式
0.2 单机Node Pod支持
其次,为了 kubemock 1w+ Service与 Pod ,Kubelet 单机Pod上线更改
  --node-status-update-frequency=3s #Node状态上报周期。默认10s
  --max-pods=500 #Node启动Pod个数。默认110
0.3 其他组件参数调优
kube-controller-manager配置调优:
  --node-monitor-period=2s #检查 kubelet 的状态时间间隔
  --node-monitor-grace-period=20s #检查 notready node 时间间隔
  --pod-eviction-timeout=30s # pod 绑定失败后重新调度时间间隔
  --concurrent-deployment-syncs=50 # deployment并发度
  --concurrent-endpoint-syncs=50 # endpoint并发度
  --concurrent-job-syncs-50 # job并发度
  --concurrent-namespace-syncs=100 # namespace并发度
  --concurrent-replicaset-syncs=50 # replicaset并发度
  --concurrent-service-syncs=100 # service并发度
  --kube-api-qps=500 # 与apiserver 限流 500qps
  --kube-api-burst=100 # 与apiserver 限并发 100连接并发
1. etcd调优
1.1 etcd 请求时间调优
heartbeat-interval就是所谓的心跳间隔,即主节点通知从节点它还是领导者的频率。实践数据表明,该参数应该设置成节点之间 RTT 的时间。Etcd 的心跳间隔默认是 100 毫秒。
election-timeout是选举超时时间,即从节点等待多久没收到主节点的心跳就尝试去竞选领导者。Etcd 的选举超时时间默认是 1000 毫秒。
    # RTT时间是 60ms
    --heartbeat-interval=100 #心跳间隔 100ms
    --election-timeout=500 #选举超时时间 500ms
1.2 快照调优
存储创建快照的代价是很高的,所以只用当参数累积到一定的数量时,Etcd 才会创建快照文件。 默认值是 10000
在超大集群中,Etcd 的内存使用和磁盘使用过高,那么应该尝试调低快照触发的阈值
   --snapshot-count=5000 #数量达到 5000 时才会建立快照
1.3 磁盘 IO 调优
etcd 的存储目录分为 snapshot 和 wal,他们写入的方式是不同的,snapshot 是内存直接 dump file,而 wal 是顺序追加写。因此可以将 snap 与 wal 进行分盘,放在两块 SSD 盘上,提高整体的 IO 效率,这种方式可以提升 etcd 20%左右的性能。
Linux 中 etcd 的磁盘优先级可以使用 ionice 配置:
$ ionice -c2 -n0 -p `pgrep etcd`
1.4 CPU 优先级调整
$ renice -n -20 -P $(pgrep etcd)
其中 nice 值可以由用户指定,默认值为 0,root 用户的取值范围是[-20, 19],普通用户的值取值范围是[0, 19],数字越小,CPU 执行优先级越高。
1.5 数据规模和自动整理
etcd 的硬盘存储上限(默认是 2GB),当 etcd 数据量超过默认 quota 值后便不再接受写请求,可以通过设置 --quota-backend-bytes 参数来增加存储大小, quota-backend-bytes 默认值 2GB,上限值为 8 GB,  3.4版本支持100GB
  --quota-backend-bytes=8589934592  # 后端存储 8G
  --auto-compaction-mode=revision
  --auto-compaction-retention=1000  # 开启每5分钟就自动压缩,并保留lastet 1000个revision
1.6 TC网络优化
master节点上,通过TC流量控制机制对对等流量进行优先级排序
$ NETWORK_INTERFACE=eth0
# 针对 2379、2380 端口放行
$ tc qdisc add dev ${NETWORK_INTERFACE} root handle 1: prio bands 3
$ tc filter add dev ${NETWORK_INTERFACE} parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1
$ tc filter add dev ${NETWORK_INTERFACE} parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1
$ tc filter add dev ${NETWORK_INTERFACE} parent 1: protocol ip prio 2 u32 match ip sport 2739 0xffff flowid 1:1
$ tc filter add dev ${NETWORK_INTERFACE} parent 1: protocol ip prio 2 u32 match ip dport 2739 0xffff flowid 1:1
# 查看现有的队列
$ tc -s qdisc ls dev enp0s8
1.7 K8s events 拆到单独的 etcd 集群
apiserver是通过event驱动的服务,因此,将apiserver中不同类型的数据存储到不同类型的etcd集群中。 从 etcd 内部看,也就对应了不同的数据目录,通过将不同目录的数据路由到不同的后端 etcd 中,从而降低了单个 etcd 集群中存储的数据总量,提高了扩展性。
简单计算下: 如果有5000个Pod, 每次kubelet 上报信息是15Kb大小; 10000个Pod 事件变更信息,每次变更4Kb
etcd 接受Node信息: 15KB * (60s/3s) * 5000 = 150000Kb = 1465Mb/min
etcd 接受Pod event信息:10000 * 4Kb * 30% = 12Mb/min
这些更新将产生近 1.2GB/min 的 transaction logs(etcd 会记录变更历史)
拆解原则:
pod etcd
lease etcd
event etcd
其他etcd (node、job、deployment等等)
apiserver events拆解:
  --etcd-servers="http://etcd1:2379,http://etcd2:2379,http://etcd3:2379" \
  --etcd-servers-overrides="/events#http://etcd4:2379,http://etcd5:2379,http://etcd6:2379"
  --etcd-servers-overrides="coordination.k8s.io/leases#http://etcd7:2379,http://etcd8:2379,http://etcd9:2379"
  --etcd-servers-overrides="/pods#http://etcd10:2379,http://etcd11:2379,http://etcd12:2379"
1.8 性能对比验证:etcd benchmark (基于移动云KCS etcd性能验证)
条件:
kubernetes 1.21.5, 3 master * 4Core 16G, 2 work * 4Core 8G
kubemark mock 1700 个 Node; 部署了 6400个Pod(包括calico 和 csi)
工具与被测对象:
[root@kcs-dongjiang--m-4w48b /]#  crictl images | grep etcd
cis-hub-huadong-4.cmecloud.cn/ecloud/etcd                               3.4.13-0            0369cf4303ffd       86.7MB
cis-hub-huadong-4.cmecloud.cn/testdj/etcd-tools-benchmark               3.4.4               76e8f59bb5a90       193MB
其中etcd Etcd DBSize 数据 1.56GB
写入测试
// leader
$ benchmark --endpoints="http://10.179.0.13:2379" --target-leader --conns=1 --clients=1 put --key-size=8 --sequential-keys --total=10000 --val-size=256
$ benchmark --endpoints="http://10.179.0.13:2379" --target-leader --conns=100 --clients=1000 put --key-size=8 --sequential-keys --total=100000 --val-size=256
// 所有 members
$ benchmark --endpoints="http://10.179.0.13:2379,http://10.179.0.2:2379,http://10.179.0.6:2379" --target-leader --conns=1 --clients=1 put --key-size=8 --sequential-keys --total=10000 --val-size=256
$ benchmark --endpoints=""http://10.179.0.13:2379,http://10.179.0.2:2379,http://10.179.0.6:2379"  --target-leader --conns=100 --clients=1000 put --key-size=8 --sequential-keys --total=100000 --val-size=256
| key 数量 | Key 大小 | Value的大小 | 连接数量 | 客户端数量 | 目标 etcd 服务器 | 平均写入 QPS | 每请求平均延迟 | IO优先级 & 网络带宽调优 | 
|---|---|---|---|---|---|---|---|---|
| 10,000 | 8 | 256 | 1 | 1 | 只有主 | 75 | 50.0ms | 否 | 
| 10,000 | 8 | 256 | 1 | 1 | 只有主 | 322 | 13.2ms | 是 | 
| 100,000 | 8 | 256 | 100 | 1000 | 只有主 | 1871 | 1207.7ms | 否 | 
| 100,000 | 8 | 256 | 100 | 1000 | 只有主 | 2239 | 992.4ms | 是 | 
| 10,000 | 8 | 256 | 1 | 1 | 所有 members | 326 | 13.4ms | 否 | 
| 100,000 | 8 | 256 | 1 | 1 | 所有 members | 352 | 12.6ms | 是 | 
| 100,000 | 8 | 256 | 100 | 1000 | 所有 members | 1132 | 1649.1ms | 否 | 
| 100,000 | 8 | 256 | 100 | 1000 | 所有 members | 1198 | 1536.8ms | 是 | 
读取测试
$ benchmark --endpoints="http://10.179.0.13:2379,http://10.179.0.2:2379,http://10.179.0.6:2379"  --conns=1 --clients=1  range foo --consistency=l --total=10000
$ benchmark --endpoints="http://10.179.0.13:2379,http://10.179.0.2:2379,http://10.179.0.6:2379"  --conns=1 --clients=1  range foo --consistency=s --total=10000
$ benchmark --endpoints="http://10.179.0.13:2379,http://10.179.0.2:2379,http://10.179.0.6:2379"  --conns=100 --clients=1000  range foo --consistency=l --total=100000
$ benchmark --endpoints="http://10.179.0.13:2379,http://10.179.0.2:2379,http://10.179.0.6:2379"  --conns=100 --clients=1000  range foo --consistency=s --total=100000
| key 数量 | Key 大小 | Value的大小 | 连接数量 | 客户端数量 | 一致性(线性化/串行化) | 每请求平均延迟(99%) | 平均读取 QPS | 
|---|---|---|---|---|---|---|---|
| 10,000 | 8 | 256 | 1 | 1 | Linearizable | 36.2ms | 319 | 
| 10,000 | 8 | 256 | 1 | 1 | Serializable | 34.4ms | 916 | 
| 100,000 | 8 | 256 | 100 | 1000 | Linearizable | 1302.7ms | 1680 | 
| 100,000 | 8 | 256 | 100 | 1000 | Serializable | 1097.6ms | 2401 | 
2. APIServer 调优
2.1 APIServer load balancing
apiserver 添加以下服务goaway 优雅重连机制
--goaway-chance=0.001  # 1/1000的连接 请求后,断开connection
2.2 APIServer Local Cache
本地APIServer Local Cache调整
--max-mutating-requests-inflight=3000 #在给定时间内的最⼤ mutating 请求数,调整 apiserver 的流控 qos,可以调整⾄ 3000 ,默认为 200
--max-requests-inflight=1000  #在给定时间内的最⼤ non-mutating 请求数,默认400 ,可以调整⾄ 1000
--watch-cache=true
--watch-cache-sizes=node#5000,pod#10000,event#1000,namespace#500,service#1000  #调⼤ resources 的 watch size,默认为 100,当集群中 node、lease、service、namespace 以及pod 数量⾮常多时可以稍微调⼤,⽐如: --watch-cache-sizes=node#5000,pod#10000,event#1000,namespace#500
2.3 APIServer bookmark 和 reflector 机制
bookmark 和 reflector 机制,都是对自定义的controller、operator、kubelet等请求方依赖client-go 进行优化实现
确保使用规范,尽量使用watch方式获得obeject变更;List & Watch 某些资源对象,并将此“增量”事件 push 入一个 DeltaFIFO 队列。
bookmark demo例子:
func (c *Cacher) GetList(ctx , key string, opts storage.ListOptions, listObj runtime.Object) error {
    // 情况一:ListOption 要求必须从 etcd 读
    ...
    // 情况二:apiserver 缓存未建好,只能从 etcd 读
    ...
    // 情况三:apiserver 缓存正常,从缓存读:保证返回的 objects 版本不低于 `listRV`
    listPtr := meta.GetItemsPtr(listObj) // List elements with at least 'listRV' from cache.
    listVal := conversion.EnforcePtr(listPtr)
    filter  := filterWithAttrsFunction(key, pred) // 最终的过滤器
    objs, readResourceVersion, indexUsed := c.listItems(listRV, key, pred, ...) // 根据 index 预筛,性能优化
    for _, obj := range objs {
        elem := obj.(*storeElement)
        if filter(elem.Key, elem.Labels, elem.Fields)   {                        // 真正的过滤
            listVal.Set(reflect.Append(listVal, reflect.ValueOf(elem))
        }
    }
    if c.versioner != nil
        c.versioner.UpdateList(listObj, readResourceVersion, "", nil)
    return nil
}
reflector demo例子:
func (r *Reflector) Run(stopCh <-chan struct{}) {
    klog.V(2).Infof("Starting reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name)
    wait.BackoffUntil(func() {
        if err := r.ListAndWatch(stopCh); err != nil {
            utilruntime.HandleError(err)
        }
    }, r.backoffManager, true, stopCh)
    klog.V(2).Infof("Stopping reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name)
}
3. kubelet 调优
3.1 node lease信息上报调优
  --node-status-update-frequency=3s #Node状态上报周期。默认10s
  --kube-api-qps=50 #node lease信息上报qps
  --kube-api-burst=100 #node lease信息上报并发
  --event-qps=100 #pod event信息上报 qps
  --event-burst=100 #pod event信息上报并发
注意
- 集群中
Node数据越少, kubelet的event/api qps与burst数越大,单机器上的产生的event数和apiserver交互的请求数就越大; - 集群数据
Node数据越大,apiserver和etcd的个数也需要相应按比例增加 
初略计算方式:  qps值 = apiserver的qps数 / node个数 
比如(之前华为云CCE上线自定义版本配置)
| Node数据最大 | 控制面个数 | 
|---|---|
| 200 | 3 master节点 | 
| 1000 | 5 master节点 | 
| 2500 | 7 master节点 | 
| 4000 | 9 master节点 | 
| 5000 | 11 master节点 | 
3.2 cpu 预留, 确保整个node资源可控
--enforce-node-allocatable=pods,kube-reserved,system-reserved # 对pods、kube-reserved 和 system-reserved预留
--kube-reserved-cgroup=/system.slice/kubelet.service  # 确保kubelet管理的进程可用
--system-reserved-cgroup=/system.slice
--kube-reserved=cpu=200m,memory=2Gi,ephemeral-storage=1Gi
--system-reserved=cpu=500m,memory=1Gi,ephemeral-storage=1Gi
--eviction-hard=memory.available<1Gi,nodefs.available<5%  #node资源低于这个值,驱逐现有pod
提前,创建好kubelet.service 和 system.slice cgroup目录 mkdir -p /sys/fs/cgroup/cpuset/system.slice/kubelet.service mkdir -p /sys/fs/cgroup/hugetlb/system.slice/kubelet.service
3.3 kubelet 测试
测试 kubelet部署Pod速度 和 kubelet 缩容后 event上报速度 两个关键指标
kubelet部署Pod速度
亲和到单机,pod 1个数 扩容到 200个, 用时 78,123s 下降到了 24.769s
[root@kcs-dj-test-m-4vdkh /]#  time kubectl rollout status deployment/nginx-deployment
Waiting for deployment "nginx-deployment" rollout to finish: 1 of 200 updated replicas are available...
Waiting for deployment "nginx-deployment" rollout to finish: 2 of 200 updated replicas are available...
Waiting for deployment "nginx-deployment" rollout to finish: 3 of 200 updated replicas are available...
Waiting for deployment "nginx-deployment" rollout to finish: 4 of 200 updated replicas are available...
...
Waiting for deployment "nginx-deployment" rollout to finish: 197 of 200 updated replicas are available...
Waiting for deployment "nginx-deployment" rollout to finish: 198 of 200 updated replicas are available...
Waiting for deployment "nginx-deployment" rollout to finish: 199 of 200 updated replicas are available...
deployment "nginx-deployment" successfully rolled out
real    0m24.769s
user    0m0.075s
sys     0m0.012s
kubelet 缩容 event上报速度
亲和到单机,pod 200个数 缩容到 1个, 释放 199 个pod 时间, 从 32s 下降到 9.05s
[root@kcs-dj-test-m-4vdkh /]#  watch time kubectl get rs  
Every 2.0s: time kubectl get rs                                                                                       Thu Sep 15 15:16:33 2022
NAME                         DESIRED   CURRENT   READY   AGE
nginx-deployment-b56784d9b   1         1         1       28h
real    0m9.058s
user    0m0.058s
sys     0m0.015s
4. Kube-Proxy 调优
4.1 ipvs vs iptable
kube-proxy分配的是ClusterIP, calico/flannel分配的是Pod IP
通过Service 访问具体实例的Endpoint, 核心是在kube-proxy
| Service基础 | 1 | 5000 | 20000 | 
|---|---|---|---|
| rules基数 | 8 | 40000 | 160000 | 
| 增加1条 iptable 规则 | 50us | 11min | 5h | 
| 增加1条 ipvs 规则 | 30us | 50us | 75us | 
必须 ipvs
因此 kube-proxy 更改
--ipvs-min-sync-period=1s #默认:最小刷新间隔1s
--ipvs-sync-period=5s  # 默认:5s周期性刷新
--cleanup=false
--ipvs-min-sync-period=0s # 发生事件实时刷新
--ipvs-sync-period=30s # 30s周期性刷新, 刷新一次节点 iptables 规则
--cleanup=true (清理 iptables 和 ipvs 规则并退出)
对于数据面高并发场景,服务服务配置
--ipvs-scheduler=lc #最小连接,默认是rr round-robin
IPVS 内置在 Kernel 中, Kernel 的版本对 IPVS 还是有很大影响
4.2 conntracker设置
无论是iptable 或者 ipvs 底层都会走conntracker
$ sysctl -w net.netfilter.nf_conntrack_max=2310720
kube-proxy 启动的时候会重新设置,
因此配置参数可以按node CPU 个数进行调整
--conntrack-max-per-core=144420  # 2310720/cpu个数。 2310720/16核 = 144420
--conntrack-min=2310720 # 设置为nf_conntrack_max 
5. kube-controller-manager
5.1 controller manager 检查 kubelet 周期
   --node-monitor-period=2s #检查 kubelet 的状态时间间隔 默认:5s
   --node-monitor-grace-period=20s #检查 notready node 时间间隔, 默认: 40s
   --pod-eviction-timeout=30s # pod 绑定失败后重新调度时间间隔, 默认 5min
controller manager  通过node controller watch了 node状态,并进行bind score计分;
5.2 调整 controller manager 并发度
  --concurrent-deployment-syncs=50 #并发创建deployment的并发数
  --concurrent-endpoint-syncs=50 #并发创建endpoint的并发数
  --concurrent-namespace-syncs=100 #并发创建namespace的并发数
  --concurrent-replicaset-syncs=50 #并发创建replicaset的并发数
  --concurrent-service-syncs=100 #并发创建service-的并发数
  --kube-api-qps=100 
  --kube-api-burst=500
6. kube-scheduler
6.1 调整 scheduler 并发度
  --kube-api-qps=500 # 默认值500
  --kube-api-burst=1000 # 默认值100
7. 整体测试
7.1 集群环境:
5个 Master 节点: 192Core、384G机器 50个 Node 节点:32Core 64G机器 CNI:calico IPIP 网络; CRI:containerd; 网段: 4个B段
7.2 验证部署方式:
- 使用45台机器 
kubermark在kubermarknamespace下 mock 出来5000个 node - 使用e2e工具 创建出来50个
namespace, 1000个service, 40000个pod。 - 其中中1/4的
namespace是大规模1000+pod,1/4是中型规模400+pod; 1/2的namespace是小型50+pod; 按namespace下的configmap、serviceaccount、secrets等,按pod的个数等4:1比例随机创建 
7.3 结果
本结果统计apiserver metrics聚合结果 (不包括网络传输请求时延)
apiserver get/list P99请求响应:

整体满足, 有部分 list cluster pods list接口 P99响应超过一秒, 后续继续优化
「如果这篇文章对你有用,请随意打赏」
如果这篇文章对你有用,请随意打赏
使用微信扫描二维码完成支付