一次k8s pod并发拉起慢的定位过程
发表于 2025/05/09
0
作者|廖宏科
问题背景
在k8s环境中,并发拉起100个pod,统计这100个pod全部启动完成的平均耗时,平均耗时较大。
应用信息
软件名称 | 版本 | 说明 |
k8s | 1.20.2 | k8s框架 |
openjdk | 1.8 | pod中运行openjdk的镜像 |
calico | 3.15 | k8s中使用的cni组件 |
docker | 19.03.10 | docker版本 |
测试方法
k8s的组网为一个master,两个node节点。通过脚本在master节点上并发下发创建100个pod的指令,等待这100全部为running状态之后,通过k8s日志统计每个pod从开始到完成创建的精确耗时,最终得出整体的平均耗时,一开始默认测出来为8s左右,时间偏大,需要调优。
问题定位
1)耗时拆分
结合源码,我们将整个容器部署流程进行拆分,以便分析实际延迟原因及性能瓶颈,大致步骤如下:
1) 输入部署指令:通过kubectl输入job部署指令,向kube-apiserver发送请求,apiserver收到后将job对象持久化写入etcd并响应成功。
2) 创建pod对象:job-controller通过观察apiserver发现有新的job建立后,将其从队列中拉出,根据其配置创建对应pod对象,返回apiserver并写入etcd。
3) 调度至特定工作节点上:scheduler通过观察apiserver发现有新创建且未被调度的pod,根据特定调度规则选择其运行的工作节点,将节点信息写入pod描述后,返回apiserver并写入etcd。
4) 创建前检查:特定工作节点上kubelet发现有pod属于该节点后,将其从队列中拉出,根据其描述创建容器。创建容器前需要进行相关检查,包括权限检查,数据目录创建,volume挂载等。
5) 创建沙盒容器:创建业务容器前,需要先创建沙盒容器及网络接口,用于为pod内容器提供通信服务。
6) 拉取镜像:根据pod描述拉取对应容器镜像,默认在本地有镜像缓存时直接拉取本地镜像。
7) 创建容器:镜像拉取完毕后创建业务容器。
8) 启动容器:容器创建完成后启动,可以开始执行实际业务。
为了方便问题定位,我们将整个过程跨分为三个阶段:调度阶段,创建阶段(包括创建前准备),启动阶段
K8S集群(日志时间戳分析阶段) |
时间戳阶段介绍 |
调度阶段 |
100并发容器从收到任务指令到调度到node节点为调度阶段 |
创建阶段(docker容器创建、网络创建) |
docker容器创建和k8s pod网络创建为创建阶段 |
启动阶段 |
创建完到启动过程为启动阶段 |
三个阶段的时间获取方式如下,通过调整master节点的 kube-scheduler的日志等级,可以在scheduler的日志中获取调度完成事件的精确时间;通过调整node节点的kubelet日志等级,可以在node节点的kubelet日志中获取创建完成和启动完成事件的精确时间。
在/etc/kubernetes/manifests/kube-scheduler.yaml添加--v=2,设置Scheduler 的日志等级;
在/usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf添加--v=3, 设置kubelet的日志等级。
阶段 |
事件 |
日志记录(v: 打印等级) |
调度阶段 |
Scheduled |
Scheduler (v=2):“Successfully bound pod to node ……” |
创建阶段 |
Created |
Kubelet (v=3):“Created ……” |
启动阶段 |
Started |
Kubelet (v=3):“Started ……” |
经过拆分,我们发现总耗时8s中,调度阶段的耗时占比最大:
阶段 | 耗时 |
调度阶段 | 7.7s |
创建阶段 | 0.5s |
启动阶段 | 0.1s |
2)调度阶段分析
通过分析调度阶段scheduler日志,我们提取每个pod调度完成时间,发现调度过程存在阻塞。接着,我们进一步研读日志打印的信息,发现Throttling requests异常信息。该异常指向一条scheduler向apiserver发起的http请求,因为触发限流的缘故,等待了比正常耗时远长的时间才得到响应。
耗时日志:"Throttling request took 298.07161ms, request: PUT", 表示触发了kube-apiserver请求限流。
调度阶段,job-controller和scheduler都要频繁向apiserver发送http请求来新建对象,获取状态,修改状态等,这样的限流机制显然极大地延缓了调度的进行,进而导致延迟加大。
因此,我们需要增大限流的阈值,从而提高网络资源的利用,加速调度阶段的进行,进而减少延迟。
问题解决
1)调度阶段优化
通过上面的分析,我们要增大scheduler和controller的并发限制,还要增大工作节点的kubelet的并发限制,k8s的并发参数主要有两个:
K8S参数值 |
调整前 |
调整后 |
kube-api-qps |
50 |
150 |
kube-api-burst |
100 |
200 |
k8s调整前:kube-api-qps:50(默认值),kube-api-burst: 100(默认值)
参数介绍:kube-api-qps 参数是指k8s api 的每秒请求数上限;kube-api-burst 是指 k8s api 的突发请求数。调整以上两个值,来控制每秒请求的阈值和突发请求的阈值。在100并发的测试场景中,调大该值能提升调度效率,降低调度阶段的时延。
具体调整方式如下:
1. 管理节点上Scheduler参数修改
vi /etc/kubernetes/manifests/kube-scheduler.yaml
在spec:containers:-command:中添加如下语句
- --kube-api-qps=150
- --kube-api-burst=200
2. 管理节点上Controller-manager参数修改
vi /etc/kubernetes/manifests/kube-controller-manager.yaml
在spec:containers:-command:中添加如下语句
- --kube-api-qps=150
- --kube-api-burst=200
3. 工作节点上Kubelet参数修改
vi /var/lib/kubelet/kubeadm-flags.env
在KUBELET_KUBEADM_ARGS属性中添加如下语句
--kube-api-qps=150
--kube-api-burst=200
调整完成之后重启kubelet,重新测试发现调度时间从7.7s降低到了0.5s左右,但是创建时间从0.5涨到了5.5s,整体耗时从8s下降到了6s左右。有一定优化,但创建时间大涨。这一现象说明并发调度没有瓶颈之后,并发创建存在较大瓶颈,整体是个此消彼长的态势。
2)创建阶段分析与优化
因为之前创建没有瓶颈,怀疑是之前并发程度不够没有触发创建的瓶颈,因为之前瓶颈在与调度,调度“限流”了并发之后,同一时刻进入创建阶段的pod数量不多,现在调度快速完成之后,同时进入创建阶段的pod就变多,导致创建阶段的并发调度就变得激烈。
通过对比基线环境的各项配置,我们发现存在几个操作系统内核参数的差异:
内核参数 |
调整前 |
调整后 |
CONFIG_HZ |
CONFIG_HZ=250 |
CONFIG_HZ=1000 |
CONFIG_HZ_250=y |
CONFIG_HZ_1000=y |
我们修改内核参数,重新编译操作系统内核之后并且重新测试,发现创建时间从5.5s降低到了1s左右,整体耗时来到了1.8s左右。
在Linux内核中,CONFIG_HZ 定义了系统定时器中断的频率,即每秒产生的时钟中断次数。CONFIG_HZ=250, 时间为1s/250 = 4ms,表示CPU 4ms产生一次时钟中断,CONFIG_HZ=1000 表示1ms产生一次时钟中断。
更快的产生时钟中断对于对,调度器能更频繁地调整进程的虚拟运行时间,确保多任务间的资源分配更公平。对于当前这类场景来看,更精确的时间调度,能够减少一些线程等待情况,更好的提高并发拉起性能。