开门见山说个事儿:我在Kubernetes上做性能测试做了快三年,最深的感触是——过去那套"开台虚拟机、装上压测工具、跑个脚本出报告"的玩法,在容器化环境里基本行不通了。不是因为工具不好用,而是被测对象变了。传统性能测试面对的是一个相对静态的系统,而Kubernetes环境下,你测的是一个会自愈、会扩缩容、会重新调度的"活系统"。你压测时Pod被重新调度了,应用重启了,网络路径变了,这些在传统环境里都是事故,在K8s里却是日常。所以我们需要一套新的范式:既能测出系统性能上限,又能搞清楚K8s本身的调度、资源限制、网络转发对性能的影响。
这篇文章我用自己的实战经验,把Kubernetes环境下的性能测试从环境搭建、工具选型、指标采集到瓶颈定位完整拆一遍。适合正在做容器化改造、准备把性能测试搬上K8s的运维和测试同学。文章里不会讲太多抽象理论,更多的是我在项目中踩过的坑、验证过的方法、以及可以直接抄走的配置和步骤。
1. 为什么Kubernetes让性能测试"变了玩法"
1.1 传统性能测试的三个假设,在K8s里全都不成立
以前做性能测试,我们默认三个前提:第一,被测系统的部署方式是固定的,IP不变、端口不变、拓扑不变;第二,资源是独占的,这台机器上的CPU、内存就是给这个应用用的;第三,故障是例外,正常情况下系统不会自己重启、不会自己挪位置。
这三个假设在Kubernetes里全部失效。K8s里的Pod IP是临时的,Deployment滚动更新一次IP就全变了;同一个Node上可能跑着十几个Pod,CPU争抢是常态而不是例外;应用崩溃后Kubelet会自动重启容器,这本来是K8s的亮点,但对性能测试来说,这意味着你看到的性能数据可能是"容器重启后"的数据,而非稳态数据。
我见过一个团队,用传统方式给K8s里的服务做压测,测出来的TPS一直不稳定,忽高忽低。排查了三天,最后发现是HPA(水平Pod自动伸缩)在压测过程中自动扩容了,后端实例从3个变成了8个,TPS当然会变化。传统性能测试里不存在"被测系统自己变胖"这种事情,但在K8s里这就是默认行为。所以Kubernetes下的性能测试,第一步不是选工具,而是意识到你面对的是一个动态系统,相应的测试设计、结果分析都得变。
1.2 Kubernetes性能测试到底在测什么
把这个问题拆开来看,K8s环境下的性能测试实际上包含三个层面:
- 应用层性能:代码本身有没有性能问题,接口响应时间、吞吐量、并发处理能力,这部分和传统性能测试一样。
- 平台层性能:K8s集群本身的性能,包括API Server的处理能力、etcd的读写延迟、kubelet的调度效率、Service/Ingress的转发性能。很多团队忽略这一层,结果出了问题到处找原因,最后发现是集群自身扛不住了。
- 协同层性能:应用和平台的交互表现。比如Pod启动速度、滚动更新耗时、HPA扩容响应时间、故障恢复时间。这些指标在传统性能测试里根本没有,但在K8s里它们直接影响用户体验和SLA。
我自己的经验是,一次完整的K8s性能测试,至少需要覆盖应用层和平台层,协同层的指标可以在专项测试里做。上次我们给一个核心交易系统做压测,QPS压到2000的时候业务响应正常,但API Server的CPU飙到85%,etcd的fsync延迟超过了100ms。这说明瓶颈根本不在应用,而在控制面。这种结论,传统性能测试是给不出来的。
2. 测试环境搭建:不踩这些坑,集群白建
2.1 测试集群和生产集群必须分开
这是我吃过亏才悟出来的事儿。K8s集群的资源隔离如果不做,性能测试数据就废了。前期为了图省事,直接在已有的开发集群上做压测,结果压测流量把开发同事的业务全打挂了,而且因为共享etcd,我们自己测出来的数据也严重失真,API Server响应时间忽高忽低。
正确的做法是:测试环境单独建集群,至少保证以下三点。
- Worker节点和应用节点物理隔离,标签加硬亲和性,确保压测应用不会被调度到系统组件所在的节点。
- 如果条件允许,压测施压机单独用一台物理机或独立虚拟机,不要和被测应用混部。
- 保证测试集群的版本和生产集群一致。K8s版本差异可能导致调度策略、网络组件行为不同,性能表现天差地别。
2.2 用kubeadm还是二进制部署测试集群
我见过不少用minikube跑性能测试的,怎么说呢,当demo可以,出报告不行。minikube是单节点,很多调度行为、网络行为和真实多节点集群完全不同。用kubeadm搭一个三节点集群是最低配置,我之前常用的规格是:3台4核8G的机器做Master+Worker混合节点,跑小型压测足够;如果是正式的性能基准测试,建议至少5台:3台Master、2~3台Worker。
不过有一点要注意,测试集群的规格不要比生产环境差太多。我见过团队拿3台低配机器搭测试集群,压测时应用还没到瓶颈,集群先崩了,最后分不清到底测的是应用还是集群。如果你测的是应用瓶颈,集群资源至少要比生产环境宽裕20%~30%,这样排除了平台瓶颈,应用问题才好暴露出来。
2.3 监控链路一定要在压测前搭好
性能测试的本质是"给系统加压并观测系统行为",观测不到,压测就白做。K8s环境下的监控,不能再像传统环境那样装个Zabbix看CPU内存就完事,至少要覆盖四个维度:
- 节点维度:CPU、内存、磁盘、网络,用node-exporter采集。
- 容器维度:每个Pod的CPU、内存、网络IO、磁盘IO,用cAdvisor采集,它默认集成在kubelet里。
- K8s对象维度:Pod状态变化、Deployment副本数、HPA伸缩事件、调度事件,用kube-state-metrics采集。
- 控制面维度:API Server的请求延迟和错误率、etcd的读写延迟、kubelet的PLEG重试次数,这些需要单独配置。
我常用的组合是Prometheus + Grafana + Alertmanager,配合kube-prometheus-stack这个Helm Chart一套装完。不过要注意,Prometheus抓取数据本身也会消耗集群资源,测试前要给Prometheus单独预留资源,别让监控成为压测的瓶颈。
2.4 etcd性能检查是容易被忽视的一步
这里多说一句etcd。学K8s的时候老师讲过,K8s所有的状态都存在etcd里,但很多人做性能测试时根本没想过etcd会不会成为瓶颈。我踩过一个大坑:压测过程中频繁创建和销毁Job对象,结果etcd写入压力太大,整个集群的响应都变慢了,连kubectl get pods都要卡好几秒。
后来我养成了一个习惯,正式压测前先做一次etcd基准测试,看看磁盘IOPS和延迟是否正常。etcd对磁盘延迟极其敏感,普通机械盘根本扛不住,SSD是标配,有条件上NVMe。另外,etcd的磁盘空间也要监控,etcd的compact和defrag操作在高负载下会引发性能抖动,这个后面在排查那块会细讲。
3. 压测工具选型与部署:容器化施压的正确姿势
3.1 主流工具横评:JMeter、Locust、k6、Vegeta怎么选
先说结论:K8s环境下的压测工具,首选是k6,其次JMeter,Locust看场景。
为什么k6适合K8s?第一,k6是Go写的,单机并发能力非常强,资源占用低,跑在容器里很轻量;第二,k6支持通过Kubernetes Operator做分布式压测,可以动态创建Pod来扩展施压能力;第三,k6的脚本是JavaScript写的,生态丰富,断言和指标系统都很完善。
JMeter虽然老牌,但它是Java写的,吃内存,单机并发到2000就需要相当大的堆内存。在K8s里跑JMeter不是不行,就是要小心资源限制,之前在K8s里部署JMeter InfluxDB模式,踩了好几次堆内存溢出的坑。不过JMeter胜在生态,插件多,做复杂的业务流比k6方便,所以团队里如果测试同学只会JMeter,用它也没问题,就是对资源要求高一些。
Locust是Python写的,脚本灵活,但性能是真的一般,单机并发几千就吃力了。适合做小规模的压力测试和场景复杂的测试,不适合高并发场景。
3.2 基于JMeter的K8s分布式压测部署方案
虽然我推荐k6,但考虑到JMeter在很多团队里的存量资产比较多,我把自己验证过的JMeter部署方案分享出来。控制端和施压端分离,控制端跑在单独的Pod里,施压端通过Deployment动态调整副本数。
我的JMeter镜像Dockerfile是这样的:
FROM openjdk:11-jre-slim ARG JMETER_VERSION=5.6.3 RUN apt-get update && apt-get install -y wget unzip && \ wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz && \ tar -xzf apache-jmeter-${JMETER_VERSION}.tgz -C /opt && \ rm apache-jmeter-${JMETER_VERSION}.tgz ENV JMETER_HOME=/opt/apache-jmeter-${JMETER_VERSION} ENV PATH=${JMETER_HOME}/bin:${PATH}Deployment的资源配置要特别注意。JMeter是CPU密集型的,施压端Pod的CPU要保证充足,内存也要给足。我踩过的坑是没给JMeter设堆内存限制,JVM默认只用了物理内存的1/4,导致压测时大量GC,测试数据严重失真。解决办法是设置JVM参数:
JVM_ARGS="-Xms1g -Xmx2g -XX:+UseG1GC"这个尺寸根据你需要的并发量调整。我一般给JMeter施压Pod分配2核4G,单Pod可以跑到1000~1500并发,具体取决于脚本复杂度。
施压端的Deployment大概长这样:
apiVersion: apps/v1 kind: Deployment metadata: name: jmeter-slave spec: replicas: 3 selector: matchLabels: app: jmeter-slave template: metadata: labels: app: jmeter-slave spec: containers: - name: jmeter-slave image: myrepo/jmeter:5.6.3 args: ["-s", "-Jserver.rmi.localport=50000"] resources: requests: cpu: "2" memory: "4Gi" limits: cpu: "2" memory: "4Gi"控制端Pod执行压测时,通过Service发现施压端。这里有个关键点,JMeter的RMI通信在K8s里配置比较麻烦,需要处理好网络策略和端口映射,不然施压端收不到控制端的指令。
3.3 施压端在K8s里跑的几个关键注意事项
第一,网络模式。施压Pod默认通过ClusterIP访问被测服务,但如果你想压测Ingress或NodePort层,施压流量要经过额外的转发路径,这个瓶颈要提前想清楚。我一般会区分两种情况:测应用性能就走ClusterIP,测集群入口性能就压Ingress地址。
第二,施压端和被压端不要放在同一个Node上。如果施压端和被压端抢同一个Node的CPU,结果就是谁也测不准。解决方法是给施压端和被压端分别打不同标签,用PodAntiAffinity强制分开调度。
第三,避免压测工具自身成为瓶颈。我遇到过压测端Pod CPU打满,导致发压数据不准确的情况。压测之前先只跑少量请求,确认压测端的CPU和内存消耗在合理范围内,再逐渐加压。我一直坚持的做法是:施压端CPU使用率不要超过70%,超过就加副本,不然生成的压力曲线是"畸形"的。
4. 指标采集与分析:K8s环境下性能测试的魂
4.1 从传统三大指标到黄金信号
传统性能测试盯三个指标:TPS、响应时间、错误率。这三个指标在K8s里依然重要,但不够了。Google SRE提出的四大黄金信号——延迟、流量、错误、饱和度——更贴合K8s动态环境的分析逻辑。
- 延迟:服务的响应时间,特别注意区分"正常延迟"和"排队延迟"。K8s环境里,一个Pod CPU限流了,应用的响应时间会呈指数级上升,这就是饱和度影响延迟的典型例子。
- 流量:QPS、并发数、网络吞吐。在K8s里,流量还要细化到Service级别的流量、Ingress流量、Pod间的东西向流量。
- 错误:HTTP错误码、应用异常、Pod重启次数。K8s里有个特殊指标是"重启次数",如果压测过程中Pod频繁重启,说明应用有严重问题,比如内存泄漏导致OOMKilled。
- 饱和度:CPU使用率、内存使用率、线程池利用率、连接池利用率。K8s里尤其要看CPU限流(throttling),因为limit设置不当会导致容器CPU被内核节流,应用表面上看CPU不高,实际性能已经受损。
我强烈建议大家把这四个维度的指标全部接入Grafana,做一个性能测试专用Dashboard。压测过程中实时盯着,比事后分析日志高效太多。
4.2 K8s特有的关键指标:这些数据传统环境看不到
K8s环境下有几个指标,性能测试报告里如果缺了它们,报告是不完整的:
CPU Throttling(CPU节流)。这个是K8s性能测试最容易踩的暗坑。CPU limits设置后,内核用CFS配额来限制CPU使用,容器超过配额就会被节流。问题是,CFS的统计窗口是100ms,如果应用在窗口内瞬时CPU冲高,就会被节流,即使平均CPU使用率很低。如何看节流情况:
# 进入容器后执行 cat /sys/fs/cgroup/cpu/cpu.stat看到nr_throttled数字持续增长,说明容器被节流了,应用的延迟可能会飙升。这个指标在传统环境里根本不存在,但在K8s里必须检查。
Pod调度延迟。从Pod创建到Pod运行的时间。这个指标在做容量评估和HPA测试时很关键。HPA扩容触发后,新增Pod要经过调度、拉镜像、启动、就绪探测等多个环节,这个延迟通常需要5~30秒不等。
容器重启次数与原因。压测过程中如果容器被OOMKilled,数据要重新测,因为系统状态已经变了。通过kubectl describe pod可以查看Last State和Exit Code,Exit Code 137表示被SIGKILL杀死,通常就是OOM。
节点资源水位。不仅是单个Pod的CPU内存,还要看整个Node的Allocatable和已分配量。K8s调度器是根据requests来调度Pod的,如果一个Node上所有Pod的requests已经把CPU占满,即使实际使用率很低,新Pod也无法调度到这个Node。
4.3 性能测试指标收集的几个实用技巧
Prometheus采集指标时,几个查询建议直接存下来:
- 应用QPS和延迟:如果是Java应用,接Micrometer + Prometheus Registry,可以按接口维度统计;如果是Go应用,用官方prometheus client库。
- Pod CPU使用率:
rate(container_cpu_usage_seconds_total{container!="POD"}[1m]),注意排除POD这个pause容器。 - Pod内存使用量:
container_memory_working_set_bytes,这个比usage更接近实际占用。 - CPU节流:
rate(container_cpu_cfs_throttled_seconds_total[1m])。 - API Server延迟:
apiserver_request_duration_seconds的histogram。 - etcd磁盘延迟:
etcd_disk_wal_fsync_duration_seconds。
我建议把上面这些指标固化到Grafana Dashboard里,压测时切到对应页面,比每次临时写PromQL高效太多。
4.4 资源画像:压测完最重要的产出物
传统性能测试产出的是"系统能扛多少QPS"的结论。K8s环境下的性能测试应该更进一步,给出每个服务的资源画像:在某个QPS下,单个Pod实际消耗多少CPU、多少内存,延迟是多少,副本数应该设置多少。
这个资源画像直接指导容量规划。比如压测发现某个服务在1000 QPS下,单Pod需要2核CPU和1.5G内存,响应时间20ms,那么根据业务预估的峰值流量,可以算出需要几个副本,以及每个Pod的requests和limits怎么设置。我习惯于生成一张资源画像表,比单纯写一份报告有用得多。
5. 瓶颈定位方法论:从现象到根因的排查路径
5.1 一个典型的K8s性能瓶颈分析案例
拿一个真实案例来拆解。上次压测一个电商系统的订单服务,现象是:并发到500时,响应时间从50ms飙升到2秒,错误率从0涨到5%。按传统思路,第一反应是查应用代码、查数据库慢查询。
但换了K8s视角的排查路径完全不同。我先查了Pod的CPU Throttling,发现nr_throttled在持续增长,说明容器被CPU限制卡住了。再看Node上的其他Pod,发现同Node上有一个日志采集的DaemonSet Pod,CPU占用高达80%,和业务Pod抢CPU。
根因就是这个日志采集Pod的CPU没有设置limits,一直无限扩张。修复方式是给日志采集Pod加上CPU limits,同时给业务Pod设置合理的CPU request,确保优先调度。一次简单的资源配额调整,性能就恢复到了正常水平。
这类问题传统环境根本不会遇到,但在K8s里非常常见。所以我把这个案例放在最前面,想强调:K8s性能瓶颈排查,先看平台层,再看应用层,千万不要一上来就查代码。
5.2 五步定位法:我排查性能瓶颈的标准路径
第一步,看Pod状态。压测过程中持续观察kubectl get pods -o wide,看有没有Pending、CrashLoopBackOff、OOMKilled状态的Pod。有就直接查事件:kubectl describe pod xxx。
第二步,看节点状态。kubectl top nodes看整体资源水位,如果某个Node的CPU或内存已经打满,看是哪些Pod吃掉资源,用kubectl top pods --sort-by=cpu按使用量排序。
第三步,看网络层。Pod之间通信走的是CNI插件,可能是Calico、Flannel或者Cilium。网络问题经常表现为:应用间调用延迟高、丢包、连接超时。常用排查工具是kubectl exec进Pod里用ping、telnet、curl测试连通性,必要时抓包。K8s里还有一个常见网络问题:DNS解析失败导致连接超时,尤其是在Pod频繁重建时,CoreDNS的缓存失效会引发大量DNS查询。
第四步,看控制面。如果应用层和节点层都正常,就要怀疑集群控制面了。查API Server的指标和日志,查etcd的延迟。我遇到过的最奇葩问题:etcd的磁盘使用超过80%,虽然还没满,但因为碎片太多导致延迟飙升,影响了整个集群的调度,连带着性能测试数据全部异常。
第五步,看应用本身。前面四步都排除了,才回头查应用代码。数据库慢查询、连接池耗尽、代码阻塞、锁竞争,这些传统性能测试的手段这时候才派上用场。
这个排查路径保证了我95%以上的性能问题都能在半小时内定位到方向,而不是像以前一样盲目地查代码。
5.3 网络组件对性能的影响,比你想象的大
K8s环境的网络转发路径比传统虚拟机环境长得多。一个Service请求从Pod A到Pod B,要经过:Pod A的veth → Node A的cbr0/eth0 → CNI插件封装 → 实际网络 → Node B的cbr0/eth0 → Pod B的veth。如果用的是Calico并且开启了IPIP模式,数据包还会多一层隧道封装,性能损耗更大。
我自己实测的数据:在同一个集群里,直接访问Pod IP比通过Service访问快10%~20%;跨Node访问比同Node访问慢10%~15%;如果开启了Calico的IPIP模式,延时会额外增加0.2~0.5ms。网络层已经是K8s性能测试不能忽视的因素。
所以在性能测试报告中,一定要注明网络拓扑和CNI插件类型,否则数据没法横向对比。我在多个团队都遇到过"换了CNI组件后性能大幅下降/上升"的情况,最后发现是网络路径发生了变化,应用本身没有改动。
6. 从containerd到K8s性能测试:运行时层面的几个隐藏因素
6.1 K8s调用containerd的机制,对性能测试意味着什么
搜索热词里有个问题是"Kubernetes是如何调用containerd的"。这个问题放在性能测试场景下来回答,会更有实际意义。K8s本身并不会直接操作容器,它是通过**CRI(Container Runtime Interface)**来调用容器运行时的。当前版本K8s默认用的运行时是containerd,它们的调用链路是这样的:
Kubelet通过CRI的gRPC接口调用containerd,containerd再通过containerd-shim进程启动和管理容器实例,每个容器对应一个shim进程,shim最终调用runc来真正创建和运行容器。
这条链路对性能测试的影响有两个层面。第一,Pod启动速度:如果做了HPA扩容测试或滚动更新测试,Pod启动的耗时包含了这个链路中的每一步——镜像拉取、容器创建、进程启动、就绪探测,任何一个环节慢都会拖累扩容效率。我实测过,对于一个小型Java应用,从创建Pod到应用就绪通常需要15~45秒,其中镜像拉取占了大头。所以在容量设计时,HPA扩容的响应时间不能只算应用启动时间。第二,如果运行时出现了异常,比如containerd的日志量暴涨、shim进程异常退出,直接会导致Pod重启或卡在ContainerCreating状态,这类故障在压测过程中偶尔会发生,测试时要留意监控告警,否则数据又白测了。
6.2 容器运行时层面的性能测试优化点
再往下说一点运行时优化。既然底层是通过runc创建容器的,那么容器本身的OCI配置就会影响性能。我踩过的坑是:镜像里没有设置--oom-score-adj,导致在内存压力下,业务进程比shim进程更容易被OOM Killer杀死。
另一个值得关注的是容器镜像大小。镜像越大,Pod启动越慢,这在性能测试里直接体现在扩容效率和故障恢复时间上。建议对镜像做瘦身,能用distroless的别用带完整系统的镜像,能合并层的就合并。不过要记住,镜像层在多个Pod同时启动时,如果没有开启P2P镜像分发,Registry会成为瓶颈,某次HPA扩容时镜像拉取直接把内部Registry打挂了,那场景真的酸爽。
6.3 性能测试中容器运行时的监控要点
监控运行时层面,几个命令和指标很有用:
# 查看容器运行时信息 kubectl get nodes -o wide # 进入容器查看cgroup信息 kubectl exec -it pod-name -c container-name -- cat /proc/1/cgroup # 查看containerd状态 systemctl status containerd核心指标方面,我关注两个:一个是etcd的磁盘延迟,虽然它是控制面组件,但和运行时协作者都是底层基础设施,磁盘性能差会让所有操作都变慢;另一个是容器进程的CPU调度延迟,如果Node上跑的Pod太多,CPU排队会导致应用性能下降,即使单看CPU使用率并不高。这些都是传统性能测试完全不会涉及的维度。
7. 常见问题和排查技巧实录
7.1 问题速查表
这部分我把实际项目中遇到过的问题整理成表,方便大家对照排查。
| 现象 | 排查方向 | 解决方案 |
|---|---|---|
| 压测时TPS波动剧烈 | 先看是否触发了HPA扩容 | 压测前临时关闭HPA,或固定副本数 |
| Pod频繁重启 | 查看Exit Code;137=OOM | 调大内存limit,排查内存泄漏 |
| CPU使用率不高但延迟高 | 检查CPU Throttling | 调大CPU limit,或调整CFS参数 |
| 压测流量上不去 | 检查施压端CPU和网络 | 增加施压端副本,或拆分为多个Deployment |
| 接口偶发超时 | 检查CoreDNS和Service DNS解析 | 排查DNS缓存策略,检查CoreDNS副本数 |
| 集群API响应变慢 | 查etcd磁盘延迟和空间 | 清理历史数据,执行defrag,迁移SSD |
| 扩容后性能反而下降 | 检查新Pod所在节点的资源水位 | 调整调度策略,或增加节点资源 |
| 压测影响同集群其他业务 | 未做资源隔离 | 用Namespace和ResourceQuota做隔离 |
| 网络延迟忽高忽低 | 检查CNI插件类型和模式 | 切换网络模式,或优化网络策略 |
7.2 独家经验:压测时关掉那些自动化的"黑魔法"
这里要特别提醒一下:K8s里很多自动化机制在常规运行中是"保命符",但在性能测试中就是"捣蛋鬼"。我自己踩过最痛的一次:压测订单服务,QPS接近预期峰值时,HPA触发扩容,新Pod启动后因为镜像拉取太慢,迟迟不就绪,老Pod被压得喘不过气,结果响应时间急剧飙升,测试直接失败。
后续我总结了一套压测前的"排除干扰"清单:
- HPA全部暂停,固定副本数,除非你专门做扩容性能测试。
- 关闭Cluster Autoscaler,避免压测时云厂商自动加节点,导致计费和测试数据双重失控。
- 如果有PodDisruptionBudget,压测时不影响;但如果有Node级维护任务(比如云厂商自动升级内核),提前安排好时间窗口。
- 把日志采集的级别调到WARN或ERROR,减少日志采集对CPU的占用。我记得有个项目,压测时业务Pod的CPU有20%都被日志采集和fluentd转发吃掉了,数据根本没法看。
7.3 压测过程中Pod状态异常怎么办
压测中间遇到Pod状态异常,一定不要慌,按照状态分类处理:
CrashLoopBackOff:说明应用启动失败,查看日志:kubectl logs pod-name --previous。常见原因是配置错误、端口冲突、数据库连接不上,先修应用再继续压测。
Pending:Pod调度不上去,kubectl describe pod看Event,常见原因是资源不足、亲和性冲突、PV挂载失败。临时扩容节点或者调整资源配额。
OOMKilled:内存超限被杀死,kubectl describe pod看Last State的Exit Code和kubectl logs看是否有OutOfMemory错误。调整内存limit,但要区分是代码内存泄漏还是limit设置过小。
ImagePullBackOff:镜像拉取失败,检查镜像仓库地址和认证信息。这个在压测扩容时最容易出现,因为新Pod要拉镜像,Registry突然被大量请求冲击。
7.4 一个真实事故复盘:etcd性能恶化引发的"假"性能瓶颈
这个案例很有代表性。当时在压测一个消息推送服务,QPS加到5000以后,集群所有服务的响应时间集体飙到3秒以上。按常规思路查了业务代码、数据库、Redis,全部正常。最后通过监控看到etcd的fsync延迟达到了300ms,正常应该在2ms以内。
在排查中发现,etcd所在节点的磁盘是共享的云盘,同一时间另一个项目组在做大量的数据导入,磁盘IO被吃满。etcd的每次写入都要等fsync完成,整个集群控制面的操作都变慢了,业务Pod的健康检查超时,被反复重启,性能自然崩了。后来我们把etcd迁移到了独立的SSD云盘上,问题彻底解决。
这个案例里最重要的是结论:在K8s里做性能测试,控制面是不可忽略的组成部分。一个健康的集群是性能测试的前提,压测前先确认集群控制面各项指标正常,比什么工具选型都重要。
8. 性能测试新范式的另外两个关键动作
8.1 混沌工程和性能测试的融合
K8s环境的动态性决定了,只做"理想状态下的性能测试"是不够的。我在团队里推过一个实践:性能测试和混沌工程结合,在压测的同时注入故障,比如随机杀掉Pod、给网络加延迟、让节点短暂不可用,观察系统在性能压力和故障压力双重作用下的表现。
具体执行时,我推荐LitmusChaos这个工具,它的Pod Chaos实验可以直接在K8s里注入不同类型的故障。一个典型的场景:在QPS压到80%水位时,杀一个Pod,记录服务的错误率和恢复时间。这个"故障注入+性能压测"的组合,比单独做性能测试或单独做故障演练,能发现更多真实问题。
8.2 持续性能测试:把压测集成进CI/CD
性能测试不应该只在发版前做一次,更应该在每次代码变更后做冒烟级的性能验证。我们团队的做法是在GitLab CI里加一个性能测试阶段,用k6的Operator在测试集群里跑一个短暂的压测任务,验证5分钟内的请求延迟和错误率是否在阈值内。
这里给个建议:持续性能测试的压测时间不用长,5分钟足够;并发量不用大,日常正常流量的1.5倍就够了;关键是建立一个基线库,每次性能数据和历史基线做对比,一旦延迟劣化超过20%,流水线就会失败报警。这套机制跑下来,很多性能回归在开发阶段就暴露了,根本等不到大版本压测。
配置方面,k6 Operator的CRD大概是这样:
apiVersion: k6.io/v1alpha1 kind: K6 metadata: name: k6-smoke-test spec: parallelism: 4 script: configMap: name: k6-script runner: image: loadimpact/k6:latest arguments: --vus 50 --duration 5m多副本并行施压,能撑到几千VU级别。关键操作是把测试脚本和压测配置都做成版本化的,跟着代码库走,而不是停留在测试同学的笔记本里。
9. 最后的几点实操体会
写了这么多,最后再分享几点我个人的体会,也是我在多个项目里反复验证过的结论。
第一,K8s性能测试的本质是"测试平台与应用协同工作的性能",而不是简单地在容器里跑传统性能测试。忽视平台因素,你的测试数据再漂亮,也代表不了生产环境的真实性能。
第二,监控体系一定要在压测前建设完毕。我从来没有见过一个监控不完善的团队能在K8s环境下顺利定位性能问题。宁可多花两天搭监控,也不要为了赶进度跳过这一步。
第三,压测数据的可复现性和可对比性非常关键。你需要在报告里明确写出:K8s版本、CNI插件及模式、运行时版本、节点规格、Pod资源Limit、副本数、HPA策略等。我见过太多团队,压测数据保存了,但环境信息不全,过两个月再回头根本没法用。
第四,别迷信"高并发"这个说法。K8s里的性能问题往往不是并发量不够大,而是系统在动态变化过程中的稳定性不足。与其追求极限QPS数字,不如多做几轮"稳定水位"的测试,找到系统能长期稳定运行的QPS区间,这才是生产真正需要的数据。
第五,也是我想强调的一点:多和其他角色协作沟通。性能测试工程师一定要拉上运维和开发一起看数据,K8s的问题往往横跨多个层面,只靠一个人的经验很难完整定位。我自己的经验是,每次压测后拉个简短复盘会,把Grafana截图、压测日志、K8s事件放在一起过一遍,很多问题当场就能找到根因。
如果这篇内容对你有帮助,建议你直接在测试集群里动手搭一遍,从环境搭建到压测执行再到监控分析,跑通一轮之后,你对K8s性能测试的理解会比看十篇文章都有用。