线上有个业务一直反馈页面打开要2秒左右一开始大家都以为是后端某接口慢前后端吵了好几轮也没结论。后来我拉上运维把链路从头到尾捋了一遍才发现问题根本不在业务代码而是卡在K8s Ingress网关这层。那次优化做完同样的接口从2秒落到了100毫秒上下前后端都愣了。这篇文章就是那次“救赎之路”的完整复盘怎么定位瓶颈、怎么压测验证、动了哪些参数、踩了哪些坑给正在被Ingress延迟折磨的同行一个可复现的排查框架。顺便说一句整个过程不涉及换网关、不涉及改业务代码纯粹是把K8s Ingress的流量链路和连接模型吃透然后把配置“拧”到正确的位置。不管你是刚接触K8s的中级工程师还是被线上延迟问题搞得焦头烂额的集群维护者按这个思路走一遍大概率也能找到属于自己的那100毫秒。1. 2秒到底卡在哪先画请求链路再谈性能优化1.1 一次普通请求的真实路径在动任何参数之前我先做了一件事把一条请求从客户端到后端Pod之间经过的所有环节全部列出来。K8s Ingress网关的性能问题绝大多数不是“某个组件慢”而是“请求路径上每个环节都在叠加延迟”最终量变引起质变。以最常见的Nginx Ingress为例一次请求大致经历这样一条链路客户端发起请求首先做DNS解析拿到入口IP请求到达云负载均衡器或自建LB再转发到NodePortkube-proxy把流量转发到Ingress Controller所在的PodIngress Controller收到请求依据Ingress规则匹配后端ServiceIngress Controller与后端Pod建立连接并转发后端Pod处理完业务逻辑后原路返回响应看上去就六步每一步看似都很快但问题在于如果连接没有复用每一步都可能在做重复的“握手”动作。DNS解析几十毫秒TCP握手一个RTTTLS握手两三个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连接45msTLS握手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之间用了KeepaliveNginx到后端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的方式或者每请求新建socketvegeta更适合按固定QPS持续施压看系统在稳定流量下的延迟表现echo GET https://your-ingress-domain/api/health | \ vegeta attack -rate500 -duration30s | \ vegeta report --typehistogramk6则是写脚本做多阶段压测比如前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_timeNginx到后端的连接建立耗时判断口径很简单如果请求总延迟很高但upstream_response_duration_seconds很低说明瓶颈不在后端而在Ingress本身或连接建立过程。如果upstream_connect_time始终大于1ms说明后端连接没有复用每次都在重新建连。除了Prometheustcpdump抓包也是排查握手的利器。在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 4432.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完整握手需要两个RTTTLS 1.3只需要一个RTT还支持0-RTT恢复。在Nginx里开启TLS 1.3的方式很简单data: ssl-protocols: TLSv1.2 TLSv1.3 ssl-session-cache: true ssl-session-tickets: truessl-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默认继承宿主机的内核参数。如果宿主机用的是默认的somaxconn128而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.somaxconn65535 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_fin_timeout15为什么这些参数能降低延迟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网关的优化有一个天然的好处它处于所有流量的必经之路你在这里每优化一毫秒全站所有接口的体验都会跟着提升。这笔账算下来投入产出比极高。希望这篇锦囊能帮你在“救赎之路”上少走一些弯路。