ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

K8s Ingress延迟优化实战:从2秒到100毫秒的配置调优

K8s Ingress延迟优化实战:从2秒到100毫秒的配置调优

线上有个业务一直反馈页面打开要2秒左右,一开始大家都以为是后端某接口慢,前后端吵了好几轮也没结论。后来我拉上运维把链路从头到尾捋了一遍,才发现问题根本不在业务代码,而是卡在K8s Ingress网关这层。那次优化做完,同样的接口从2秒落到了100毫秒上下,前后端都愣了。这篇文章就是那次“救赎之路”的完整复盘:怎么定位瓶颈、怎么压测验证、动了哪些参数、踩了哪些坑,给正在被Ingress延迟折磨的同行一个可复现的排查框架。

顺便说一句,整个过程不涉及换网关、不涉及改业务代码,纯粹是把K8s Ingress的流量链路和连接模型吃透,然后把配置“拧”到正确的位置。不管你是刚接触K8s的中级工程师,还是被线上延迟问题搞得焦头烂额的集群维护者,按这个思路走一遍,大概率也能找到属于自己的那100毫秒。

1. 2秒到底卡在哪:先画请求链路,再谈性能优化

1.1 一次普通请求的真实路径

在动任何参数之前,我先做了一件事:把一条请求从客户端到后端Pod之间经过的所有环节全部列出来。K8s Ingress网关的性能问题,绝大多数不是“某个组件慢”,而是“请求路径上每个环节都在叠加延迟”,最终量变引起质变。

以最常见的Nginx Ingress为例,一次请求大致经历这样一条链路:

  • 客户端发起请求,首先做DNS解析,拿到入口IP
  • 请求到达云负载均衡器或自建LB,再转发到NodePort
  • kube-proxy把流量转发到Ingress Controller所在的Pod
  • Ingress Controller收到请求,依据Ingress规则匹配后端Service
  • Ingress Controller与后端Pod建立连接并转发
  • 后端Pod处理完业务逻辑后原路返回响应

看上去就六步,每一步看似都很快,但问题在于:如果连接没有复用,每一步都可能在做重复的“握手”动作。DNS解析几十毫秒,TCP握手一个RTT,TLS握手两三个RTT,到后端又是新的TCP连接,加在一起就很可观了。尤其是公网场景,客户端和服务器之间RTT可能20到80毫秒,几次握手下来,延迟直接奔着秒级去。

1.2 先确认现象:延迟到底分布在哪个环节

我的习惯是,先不猜,用最土的办法看一遍数据。在客户端机器上直接跑curl,把请求过程分段计时:

curl -o /dev/null -s -w \ 'DNS解析: %{time_namelookup}s\nTCP连接: %{time_connect}s\nTLS握手: %{time_appconnect}s\n首字节时间: %{time_starttransfer}s\n总耗时: %{time_total}s\n' \ https://your-ingress-domain/api/health

那次在某个业务环境的实测数据大概是这样的:

  • DNS解析:310ms(走了公共DNS,缓存失效)
  • TCP连接:45ms
  • TLS握手:560ms(证书链很长,且TLS 1.2需要两个RTT)
  • 首字节时间:1.6s
  • 总耗时:2.1s

看到这个数据,问题方向已经很明确了。DNS解析吃掉300多毫秒,TLS握手吃掉500多毫秒,剩下的时间大部分也不是后端业务本身慢,而是“从Nginx到后端Pod的连接”又在重复建连。也就是说,这条链路上的每个握手环节都慢,单独看都不算离谱,叠到一起就成了2秒。

1.3 为什么“短连接加旧架构”会慢成这样

很多人对“握手”没有直观概念。打个比方:TCP连接就像打电话前先拨号,TLS握手就是拨号之后还要对暗号。如果每次请求都要重新拨号、重新对暗号,那任何操作都逃不掉三个RTT的固定开销。假设公网RTT是50ms,三个RTT就是150ms,加上证书链如果很大,TLS握手在慢启动阶段还要多传几个往返,光握手就奔着半秒去了。

更隐蔽的是Ingress到后端Pod这一段。Nginx反向代理默认不会把客户端的长连接“继承”给上游,也就是说,即使客户端和Nginx之间用了Keepalive,Nginx到后端Pod还是“一个请求一条连接”。后端Pod如果频繁扩缩容、IP变更,连接缓存失效,情况会更严重。新版本的ingress-nginx虽然默认开启了一些连接池能力,但生产环境依然需要根据实际并发重新设置,不能直接用默认值。

所以那次我初步判断:2秒不是后端代码的问题,而是“链路握手叠加”的结果。优化的核心就是两条主线——减少握手次数、让连接尽量复用。后面的所有操作都围绕这两条主线展开。

2. 量化手段:用什么工具和指标才能看清问题

2.1 压测工具的选择:wrk、vegeta、k6怎么配合

手头没有压测数据就去优化配置,等于闭着眼开车。但压测工具选错了同样会翻车。我常用的组合是:wrk做快速验证,vegeta做固定QPS灌压,k6跑复杂场景。

wrk类工具最大的问题是默认使用长连接压测,这和我们线上“真实用户反复建连”的模型不完全一致。所以我压测时会刻意区分两种场景:

# 长连接场景:压的是复用后的真实吞吐 wrk -t8 -c200 -d60s --latency https://your-ingress-domain/api/health # 短连接场景:模拟每次新建连接的极端情况 # 可以在脚本里用HTTP/1.0的方式,或者每请求新建socket

vegeta更适合按固定QPS持续施压,看系统在稳定流量下的延迟表现:

echo "GET https://your-ingress-domain/api/health" | \ vegeta attack -rate=500 -duration=30s | \ vegeta report --type=histogram

k6则是写脚本做多阶段压测,比如前30秒低流量、中间30秒突增、最后30秒降流量,用来观察Ingress在突发流量下的表现。那次我压测时用的就是k6,因为要观察连接池在突发情况下的重建行为和P99波动。

2.2 监控指标:不要只看平均延迟

Ingress Controller本身就暴露Prometheus指标,关键是知道看哪几个。我重点盯的是:

  • nginx_ingress_controller_request_duration_seconds:请求总延迟
  • nginx_ingress_controller_upstream_response_duration_seconds:后端响应耗时
  • nginx_ingress_controller_upstream_connect_time:Nginx到后端的连接建立耗时

判断口径很简单:如果请求总延迟很高,但upstream_response_duration_seconds很低,说明瓶颈不在后端,而在Ingress本身或连接建立过程。如果upstream_connect_time始终大于1ms,说明后端连接没有复用,每次都在重新建连。

除了Prometheus,tcpdump抓包也是排查握手的利器。在Ingress Pod上抓一段流量,用Wireshark看TCP握手、TLS ClientHello和ServerHello之间的时间差,能直接看到TLS握手是不是在“裸奔”。

kubectl -n kube-system exec -it <ingress-nginx-pod> -- \ tcpdump -i eth0 -w /tmp/ingress.pcap port 443

2.3 分阶段定位:把链路拆开测

优化前我还做了一个小实验,用来把链路拆成三段:客户端到Ingress、Ingress到后端Pod、后端Pod本身。方法是临时创建一个返回“ok”的测试后端,然后通过Ingress访问它。如果延时依然很高,基本可以断定问题集中在Ingress配置层,而不是业务代码。

那次的实验结果证实了我的判断:测试后端走Ingress访问,P99依然接近1.8秒,而测试后端本身处理请求只要5毫秒。这说明2秒的锅,Ingress网关至少背了90%以上。有了这个结论,后面的优化才敢放开手做。

3. 救赎之路:把每毫秒从手缝里抠出来

3.1 第一刀:让连接真正“复用”起来

性能优化收益最大的永远是从“不可复用”到“可复用”的转变。这一刀砍下去,效果立竿见影。

在ingress-nginx的ConfigMap里,有几个关键参数直接决定连接复用行为:

apiVersion: v1 kind: ConfigMap metadata: name: ingress-nginx-controller namespace: ingress-nginx data: keep-alive: "900" # 客户端到Ingress的空闲连接保持时间 keep-alive-requests: "10000" # 单条连接最大请求数,默认100太小了 upstream-keepalive-connections: "256" # 后端连接池大小,老版本默认0相当于不启用

为什么这些参数重要?引用Nginx官方文档的说法,反向代理默认不会复用上游连接,除非显式配置keepalive。ingress-nginx从0.26版本开始把upstream-keepalive-connections默认值从0调到32,但这只适合低并发场景。像我们那会儿并发一上来,连接池立刻被打满,多余的请求全部回到“每次新建连接”的老路,延时自然下不去。

调整完这三个参数之后,我又看了一眼后端Pod的配置。这里有个容易忽略的点:连接能否成功复用,不只是Ingress单方面说了算,后端服务本身也要支持Keepalive。比如某些PHP-FPM、老旧的Java应用容器,默认可能关闭了Keepalive,或者keepalive超时太短。这时候就算Ingress连接池配得再大,后端也会主动断开连接。

这一刀的效果非常明显:P99从1.8秒降到了450毫秒左右。中间的1.3秒主要就是省下的握手和连接建立时间。说实话,看到数据那一刻我反而有点后怕——这么简单的配置,之前居然一直用的默认值。

3.2 第二刀:把TLS握手时间打下来

连接复用解决的是“存量连接”的问题,但总有首次连接、连接池打满、请求超时断开等场景需要重新握手。TLS握手本身也必须优化。那次的TLS握手耗时560毫秒,仔细拆开来,原因有三个:证书链太长、TLS版本还是1.2没有开启1.3、Session复用配置缺失。

先看证书链。我们把证书链从5层精简到2层,去掉了中间多余的交叉证书,同时开启了OCSP Stapling,让Nginx在握手时直接把证书状态一次性带给客户端,避免客户端再单独查询。这一步直接省掉了一个RTT。

再看TLS版本。TLS 1.2完整握手需要两个RTT,TLS 1.3只需要一个RTT,还支持0-RTT恢复。在Nginx里开启TLS 1.3的方式很简单:

data: ssl-protocols: "TLSv1.2 TLSv1.3" ssl-session-cache: "true" ssl-session-tickets: "true"

ssl-session-cache和ssl-session-tickets是Session复用的关键。前者在Nginx进程内缓存会话,后者让客户端保存加密的Session Ticket,下次连接直接恢复会话,跳过完整的证书交换和密钥协商。有人担心TLS 1.3兼容性,实际上现代浏览器、移动端主流版本都已经支持,企业内网环境如果客户端太老,再按需降级兼容即可。

另外还做了一件容易被忽略的事:把证书格式从RSA优先切到了ECC优先。ECC证书的ServerHello消息体、密钥交换数据都比RSA小不少,在弱网环境下能明显缩短握手时间。

这一刀之后,单次握手耗时从560毫秒降到了100毫秒以内,首次访问的P99大概又掉了150毫秒。

3.3 第三刀:Ingress Controller自身参数调优

连接和TLS都处理完之后,我开始折腾Ingress Controller本身的运行参数。这部分的收益虽然没有前两刀那么夸张,但胜在积少成多,每一两项都是一些零碎但真实的延迟。

第一个是worker进程数。Nginx默认配置是自动等于CPU核心数,这个一般不用动,但要注意一下是不是被资源限制卡住了。如果Ingress Controller的Pod只分配了1个CPU,那Nginx只有一个worker在转发流量,再好的配置也扛不住并发。

第二个是开启HTTP/2。HTTP/2和HTTP/1.1最大的区别是同一个连接上可以并行发起多个请求,不用排队等一个个响应。在ConfigMap里:

data: http2: "true"

开启HTTP/2之后,浏览器并发加载页面资源的速度会有明显提升,这在整体体感上非常直观。

第三个是proxy缓冲区相关的调整。在大流量场景,Nginx默认的代理缓冲区可能会频繁触发“写入缓冲→读取缓冲”的拷贝,增加延迟。可以适当调整:

data: proxy-buffering: "off"

不过关闭proxy-buffering会降低Nginx对慢客户端的背压能力,如果后端响应体很大、客户端网络很慢,反而会把Nginx的内存打爆。我当时的场景是后端返回体普遍小于10KB,关闭缓冲收益大于风险,才做了这个调整。如果你的接口经常返回几十上百MB的文件,强烈不建议关。

还有几个超时配置的调整,比如proxy-connect-timeout默认5秒对后端在同一个集群内的情况太宽了,我把它调到了1秒,避免后端异常时请求长时间挂在Ingress上。

data: proxy-connect-timeout: "1"

最后是Pod副本数和调度。Ingress Controller默认可能只有1个副本,而它承载的是整个集群的南北流量入口。同一个Node上如果已经跑了很多高负载Pod,网络栈的竞争会很严重。我把Ingress Controller的副本扩到3个,加了Pod反亲和,让它们尽量分散在不同Node上,同时设置了HPA,按CPU和每秒请求数自动扩缩容。这一套组合拳下来,Controller本身的处理延迟又降了一小截。

3.4 第四刀:内核参数与网络栈调优

到这一步,应用层面的配置已经基本榨干了。再往下走,就是系统网络栈的调优了。注意这一层的操作影响面比较大,一定要先在小范围验证,再做集群级变更。

K8s集群里的Pod默认继承宿主机的内核参数。如果宿主机用的是默认的somaxconn(128),而Nginx的backlog配置超过这个值,高并发下连接请求会在内核的accept队列里排队,造成延迟抖动。我们当时调整的几个关键参数:

net.core.somaxconn = 65535 net.core.netdev_max_backlog = 16384 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_tw_reuse = 1 net.ipv4.ip_local_port_range = 1024 65535 fs.file-max = 1048576

在K8s环境里给Ingress Controller Pod设置内核参数,我用了initContainer的方式:

initContainers: - name: sysctl image: busybox:1.36 securityContext: privileged: true command: - /bin/sh - -c - sysctl -w net.core.somaxconn=65535 && sysctl -w net.ipv4.tcp_tw_reuse=1 && sysctl -w net.ipv4.tcp_fin_timeout=15

为什么这些参数能降低延迟?somaxconn决定内核accept队列的长度,Nginx的事件模型是异步的,但队列太短会导致内核丢弃或积压连接,客户端表现为连接被重置或非常慢。tcp_tw_reuse允许内核快速回收TIME_WAIT状态的连接,在短连接场景下效果尤其明显,否则大量连接会停留在TIME_WAIT里,占用本地端口和内存。ip_local_port_range则是扩大可用的本地端口范围,避免高并发下端口耗尽导致的建连失败。

这里必须提醒:tcp_tw_reuse在部分NAT环境下可能引发连接复用混乱,如果你们的Ingress前面还有一层云LB做地址转换,先在小流量验证一下,别直接全量开。

3.5 第五刀:要不要换网关?先说结论

在优化过程中,不止一个人建议过我“别折腾了,直接换一个更高级的Ingress网关”。我没急着换,原因是:从2秒到400毫秒那会儿,问题已经明显不在Ingress组件本身,而是配置和系统参数没对上。换网关能把几百毫秒降到几十毫秒,但同样需要重新调优,成本不低而且风险不小。

当然,如果负载已经到每秒钟几万请求,而且需要更细粒度的流量治理,比如gRPC负载均衡、熔断、限流、精确的路由匹配,那Nginx Ingress确实再能满足需求上也显得吃力了。这时候再考虑Envoy Gateway或主流云厂商的托管Ingress网关是合理的。但在那之前,至少要先把能调的参数调到位再下结论。

那次我最终没有换网关。一个稳定运行的生产组件,在没有明确瓶颈在组件本身之前,尽量不要为了“换而换”。优化到100毫秒之后,整个系统的瓶颈已经转移到了后端业务本身,这时候换网关属于过度设计。

3.6 最终效果:从2秒到100毫秒的延迟拆解

优化完成之后,我又跑了一遍压测,同时把链路各环节的耗时记了下来。以某个标准接口为例,优化前后的对比大致是这样的:

环节优化前优化后主要手段
DNS解析310ms15ms本地DNS缓存、缩短解析链
TCP+首次TLS握手560ms80msTLS1.3、ECC证书、OCSP Stapling、会话复用
客户端到Ingress转发420ms5msHTTP/2、连接复用、proxy缓冲调整
Ingress到后端建连480ms5msupstream keepalive连接池
后端业务处理200ms200ms未改业务代码
总耗时约1.97s约105ms整体压测P99

后端业务处理时间一分没动,还是200毫秒。也就是说,我们把网关链路从1.7秒左右的延迟压缩到了10毫秒级别,整体耗时从2秒压到了100毫秒级别。这个结果说明,Ingress网关的默认配置在真实生产环境下确实有大量可挖掘的空间。

4. 实战中容易踩的坑与排查记录

4.1 坑一:Keepalive开了,但连接池没生效

第一次调完upstream-keepalive-connections之后,我一度以为事情结束了。结果第二天发现延迟虽然降了,但离预期还差很多。查了半天,才发现是后端Pod那边的Keepalive超时只有10秒,而Ingress的空闲连接保持是900秒。后端主动断开之后,Ingress连接池里的连接变成了“半死连接”,下一次请求依然要重新建连。

这个问题在容器环境特别常见,因为后端Pod经常因为发布、扩缩容、健康检查失败而重建,IP变了,连接池里缓存的旧连接自然全部失效。我的解决方法是:后端服务的server.keepalive.timeout统一设为60秒以上,同时给后端Pod配置了preStop钩子,让Pod在终止前先等待一段时间,把存量请求处理完再退出,减少连接池的剧烈波动。

4.2 坑二:证书链配置让TLS握手依然很慢

证书链精简之前,我一度怀疑TLS握手慢是TLS版本问题。结果切到TLS 1.3之后,握手只快了一点点。后来抓包才发现,服务器在握手里发送了一整条长度为8KB的证书链,多出来的几个RTT全耗在慢启动传输上了。把证书链精简到2层并开启OCSP Stapling之后,握手立刻降到60毫秒以内。

这个坑的教训是:TLS握手慢,先看证书链大小,再看TLS版本,顺序不能反。很多人一上来就追新版本,结果证书链根本没精简,属于治标不治本。

4.3 坑三:压测工具给了假数据

有段时间我优化完一个参数,压测显示P99反而涨了,差点把参数回滚。后来发现是压测脚本里的URL带了一个随机参数,后端接口有缓存,随机参数导致缓存命中率下降,延迟自然上去。这属于压测变量没控制好。

另一个常见压测陷阱是“连接耗尽”。wrk/vegeta这些工具默认创建的连接数受限于压测机的文件句柄数,如果ulimit -n不够,连接还没到Ingress就被本机挡住了,压出来的延迟根本没参考价值。压测前先把ulimit -n调大,并且用多台压测机分摊流量,才能测出真实水平。

4.4 坑四:调优后P99依然有尖刺

在连接复用和TLS优化都做完之后,P99整体下来了,但每隔几分钟就会冒出一个300ms的尖刺。排查了很久,最后发现是Ingress Controller的Pod因为内存超限被OOMKilled,重启之后连接池冷启动,紧接着的请求全部需要重新建连。

给Ingress Controller设置合理的资源request和limit,同时开启健康检查和优雅停机,能让Pod滚动更新的时候不那么“粗暴”。另外我还发现,HPA的伸缩策略默认是基于平均值,如果流量周期性波动大,平均触发阈值可能不够灵敏。改成按P99触发伸缩,或者调整HPA的指标周期,尖刺明显减少。

4.5 坑五:内核参数调过头,Nginx拒绝连接反而变多

somaxconn从默认128调到65535之后,我以为万事大吉,结果某个压测场景里Nginx日志开始报连接拒绝。原因是内核accept队列变长了,但Nginx的worker_connections没跟上,队列里的连接长时间得不到处理,客户端耐心耗尽直接断开。

后来我把Nginx的worker_connections也相应调大,并且把backlog参数和somaxconn对齐,问题才消失。这里的教训是:系统的网络参数是一条链路的,必须排队调整,不能只改其中一个环节。

5. 适用场景与我的经验备案

5.1 不同规模场景的优化建议

聊了这么多,最后给不同类型的团队一个可以直接套用的建议:

场景推荐动作理由
小型集群、低并发只做连接复用和TLS1.3改动最小,收益最大
中型集群、较高并发连接复用 + Controller参数 + HPA把默认配置和实际并发对齐
大型集群、超高并发加上内核参数调优,或评估迁移网关网络栈的优化空间必须充分挖掘

具体到你的业务,如果只是内部管理系统,几十个并发,可能连这些优化都不用做。但如果你的业务对外,用户遍布全国甚至全球,那就得按“每毫秒必争”的态度去对待每一个环节。

5.2 调优成果怎么保持,不让它悄悄反弹

性能优化做完不是终点。ingress-nginx每次升级,ConfigMap的key可能有变化,默认值也可能会变,以前调好的参数可能在新版本里根本不被识别。所以我把关键参数都写进了IaC(基础设施即代码)仓库,每次升级都会跑一遍自动化检查,比对ConfigMap的实际值和期望值。

另外,我还给Ingress的关键指标配了告警。不是等用户反馈慢才去查,而是当upstream_connect_time的平均值超过10毫秒时就直接告警,因为那通常意味着连接池失效或后端连接出了问题。这类问题早发现一分钟,就能少影响一批线上用户。

5.3 最后一点个人体会

这次优化做完,我最深的体会不是某个参数有多神奇,而是“性能问题不要靠猜,要把请求链路画出来,分段计时,用数据说话”。大部分性能问题都能在数据面前快速定位,剩下的就是耐心和细心。如果你也遇到类似的问题,建议从连接复用开始排查,那是最容易出效果也最不容易出错的地方。

另外多说一句,Ingress网关的优化有一个天然的好处:它处于所有流量的必经之路,你在这里每优化一毫秒,全站所有接口的体验都会跟着提升。这笔账算下来,投入产出比极高。希望这篇锦囊能帮你在“救赎之路”上少走一些弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进