ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

K8s Ingress网关性能调优:从2秒到100毫秒的排查实战

K8s Ingress网关性能调优:从2秒到100毫秒的排查实战

接到运维告警的时候,第一反应是后端应用又出问题了。可躲开那一波“2 秒左右”的慢请求,我盯着监控面板看了好一阵——应用自身 P50 只有 30 毫秒,P99 却在 1.8 秒到 2.3 秒之间反复横跳。所有慢请求都卡在 K8s Ingress 网关上。这个标题里的“从 2 秒到 100 毫秒”,我理解成一段完整的性能救赎过程:先把拖延症到底出在哪一环节找明白,再动刀,最后把优化成果稳定下来。如果你也在维护 K8s Ingress 网关,或者被莫名其妙的偶发延迟折磨过,这篇内容足够当成一份排查手册来用。下面我直接按实操顺序讲。

1. 先弄清楚这 2 秒到底花在哪里,而不是马上升级网关

很多团队遇到网关变慢,第一反应是换组件、加副本、提升节点配置。其实 2 秒这个量级,往往和机器性能没有太大关系,甚至和 Nginx 本身的处理能力也无关。我见过太多案例,问题都出在连接建立、DNS 解析、连接复用这类“看不见的握手环节”。

1.1 用一条完整链路拆出延迟来源

我先从请求路径说起。一次普通的访问,在 K8s 环境里大体是:

客户端 -> 四层负载均衡 -> Ingress Controller Pod -> Service -> 后端 Pod

延迟可能分布在这条链路的多个位置。我们不能只看 Ingress 监控里的平均耗时,那会让真正的问题被平均掉。我把定位延迟位置的方法拆成三步。

第一步,在客户端机器上直接看时间分布。使用curl的分阶段计时,能先粗分出“DNS 解析”“TCP 连接”“TLS 握手”“等待响应”几段各自消耗了多少时间:

curl -o /dev/null -s -w \ "dns:%{time_namelookup} tcp:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n" \ https://gateway.example.com/health

如果tcp耗时高,说明客户端到网关之间的网络或负载均衡层有问题;如果ttfb高但total不高,说明网关到后端链路上消耗了更多时间;如果tls高,则要查证书链和会话复用。

第二步,横向对比 Ingress 访问日志里的时间字段和后端应用日志里的接收时间。Ingress Nginx 的日志中有request_time和upstream_response_time,如果前者远大于后者,说明网关内排队或上游连接建立环节拖了后腿。

第三步,必要时抓包。不是每次都抓,而是当值从毫秒级跳到秒级时,看是不是发生了 TCP 重传、SYN 重传,或者等某个超时窗口。我那次排查中,最终就是在抓包里看到一条规律性的 DNS 查询等待,才锁定问题方向。

1.2 Ingress 网关常见的性能卡点清单

把常见的卡点整理成一张清单,按出现频率排序,后面排查时能快速对照:

卡点典型表现根源
DNS 解析异常偶发 1~2 秒延迟,重试后正常Nginx resolver 对 AAAA 查询处理不当,或 CoreDNS 响应慢
上游连接复用不足高并发下延迟飙升每次请求都新建 TCP 连接,握手开销大
Service 端点变化频繁发布窗口内延迟明显后端 Pod 重建后,Nginx 连接池失效,重新建立连接
Worker 进程配置过小大量请求排队单 Worker 处理能力不足,accept 锁成为瓶颈
内核参数限制高并发时连接建立失败或超时本地端口耗尽、TIME_WAIT 堆积

那次 2 秒问题的核心,确实出在 DNS 环节,但后续优化如果不把连接复用一起搞定,优化完也只能从 2 秒变成 300 毫秒,很难达到 100 毫秒。所以我建议你按链路逐层排查,而不是只盯一个点。

2. 方案选型:为什么我选择保留 Ingress Nginx,而不是立即换网关

排查完一圈后,团队里有人提议直接换网关组件,认为 Nginx Ingress 不行。我理解这种情绪,但“换”解决不了已经存在的配置缺陷。先做方案对比,再决定是否要动大手术。

2.1 三种主流网关的适用场景对比

我拿 Nginx Ingress Controller、Traefik、Envoy 类网关做了对比。这里的比较不是分优劣,而是看谁更匹配现有环境和团队能力。

特性Nginx Ingress ControllerTraefikEnvoy 类网关
配置成熟度极高,文档和案例最多中等,配置风格新较高,偏云原生
性能上限强,靠配置调优中规中矩强,指标丰富
运维成本低,社区方案多中,自定义能力有限较高,学习曲线陡
动态配置能力依赖 Reload优秀,配置热更新优秀,xDS 驱动
排障友好度高,日志和抓包直观中较高,但链路概念多

对大多数以业务开发为主、没有专职网关团队的团队来说,Nginx Ingress 仍然是性价比最高的选择。它能处理常见的吞吐需求,而且遇到问题时,网上几乎能找到所有场景的答案。

2.2 基于现状的选型结论

我最终选择保留 Nginx Ingress,理由很简单:当前问题不是“组件性能不够”,而是“组件配置没调对”。在配置层面解决 DNS 解析和连接复用之后,吞吐和延迟都有明显改善。换网关意味着重学配置体系、重写路由规则、重调监控告警,这些成本都要分摊到每一次发布里,风险远大于收益。

如果你的场景是成千上万条路由且频繁动态变更,或者需要精细的灰度流量拆分和熔断能力,那时候再考虑 Envoy 类网关才值得。但如果只是延迟不达标,我建议先在当前组件上做深度优化。网关选型从来都是“够用、能维护”优先,而不是参数越新越好。

3. 从 2 秒到 100 毫秒的分步实操与核心参数

确认保留 Nginx Ingress 之后,我开始做分步优化。整个过程大概持续了两天,每一步都是有明确目的和验证方法的。下面按我执行顺序讲解,并给出可以直接参考的配置。

3.1 先用压测建立可信的基准线

没有基准线,后面所有改动都说不清是否有效。我先用压测工具打了一轮请求,记录优化前的 P50、P95、P99。这里推荐使用hey或者ghz,它们对高并发压测的支持比传统工具更直观,且能直接输出分位延迟统计。

hey -n 20000 -c 300 -m GET https://gateway.example.com/health

注意压测不能只跑一次,至少要取三次数据并确认结果稳定。如果第一次跑出来的延迟曲线忽高忽低,说明环境里还有其它干扰因素,需要先排除。我当时记录了优化前的数据:请求总量 2 万、并发 300,P50 在 420 毫秒左右,P95 为 1.6 秒,P99 达到 2.1 秒。这就是我们需要解决的目标基线。

压测期间也要观察 Ingress Pod 的资源消耗。如果 CPU 已经打满,那后续的参数调优意义不大,得先扩容。一般 Ingress 的 CPU 使用率在压测时最好低于 70%,否则容易压到 Pod 配额边缘。

3.2 第一刀:DNS 解析与 resolver 缓存

回到那次最核心的问题。抓包时我发现网关在处理请求时,会向集群 DNS 发起解析请求,但解析过程异常缓慢。进一步分析后,问题来自 Nginx 默认的 resolver 行为:它会在解析 A 记录时同时等待 AAAA 记录返回,而当前网络环境对 IPv6 支持不完整。在某些查询组合下,Nginx 进入了一段较长的等待期,等满一个超时窗口后才完成解析,这个超时量级正好和 2 秒接近。

解决办法是让 Nginx resolver 缓存结果更久,并且关闭当前网络不适用的 IPv6 AAAA 查询。在 Ingress Controller 生成 Nginx 配置的模块中,可以通过注入自定义片段的方式,把resolver指令改成:

resolver 10.96.0.10 valid=30s ipv6=off;

这里的集群 DNS 地址以你实际环境为准,大多数 K8s 集群中 CoreDNS 服务的 ClusterIP 可以在kube-system命名空间下看到。valid=30s表示对解析结果缓存 30 秒,ipv6=off则直接省去 AAAA 查询。30 秒这个值不是随便写的,它保证网关不会因为缓存过久导致后端 Service 的端点变化得不到及时感知,同时避免每个请求都发起重复解析。

这一刀之后,延迟大头立刻消除。但这里有个环节容易踩坑:修改 Nginx 配置后并不一定马上生效,需要确认 Ingress Controller 完成了一次配置 Reload。有些修改可以通过 Annotation 实现,有些需要重启 Controller Pod 才能生效。我的建议是尽量把通用配置放在 Controller 的 ConfigMap 中,而不是给每条 Ingress 单独加 Annotation。

3.3 第二刀:Worker 进程与连接池配置

DNS 问题解决后,延迟降到了一定程度,但距离 100 毫秒还有距离。接下来我把优化重点放到连接复用上。Nginx 默认对上游的 keepalive 连接数量通常偏小,导致大量请求都在重新建连。TCP 三次握手加 TLS 开销,在并发场景下会放大成很明显的延迟。

我先看 Ingress Controller ConfigMap 当前的配置,然后做以下调整:

apiVersion: v1 kind: ConfigMap metadata: name: ingress-nginx-controller namespace: ingress-nginx data: worker-processes: "auto" worker-connections: "32768" keep-alive: "128" proxy-connect-timeout: "2" proxy-read-timeout: "60" proxy-send-timeout: "60" enable-multi-accept: "true" enable-reuse-port: "true"

这里逐项解释。worker-processes设成auto一般够用,Controller 会按节点 CPU 核数自动分配。worker-connections决定了每个 Worker 能同时处理的连接数上限,调大它能缓解高并发时的排队。

keep-alive是 Nginx 与上游后端之间的空闲连接池数量。常见默认值很小,我调到128后,复用效果立竿见影。proxy-connect-timeout控制与上游建立 TCP 连接的超时,原本太大会拖慢失败场景的响应,改小是合理的,但不要小于 1 秒,否则极端网络抖动下容易误判后端故障。

enable-multi-accept让每个 Worker 可以一次从监听队列中取多个连接,而不是一个接一个处理。enable-reuse-port则让多个 Worker 共享同一监听端口,减少锁竞争。这两个开关对高并发场景都很实用。

如果你使用的 ingress-nginx 版本较新,可能在 ConfigMap 里还会看到upstream-keepalive-connections、upstream-keepalive-requests这类键名。不同版本的键名有差异,改之前先看当前 Controller 版本支持的配置项,别照抄旧文档。最好的确认方式是在改完后查看 Controller 日志有没有出现unknown configuration key的警告。

3.4 第三刀:TLS 会话复用与超时参数

如果业务通过 HTTPS 访问,TLS 握手是隐藏的延迟大头。客户端第一次握手免不了,但后续请求完全可以通过会话复用省掉一个完整握手过程。我在 Controller ConfigMap 中补充了 TLS 相关配置:

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

开启ssl-session-cache后,同一个客户端再次连接时能复用会话参数,不需要重新走完整密码套件协商。ssl-session-tickets我选择关闭,因为 Ticket 在负载均衡多实例下需要统一密钥管理,如果只在本机缓存,会话复用成功率会下降,反而增加开销。OCSP 装订开启后,证书状态查询不再由客户端单独发起,也能省掉一部分握手时间。

超时参数也要一起调。我习惯把proxy-read-timeout和proxy-send-timeout设置为 60 秒,避免积聚过多的慢连接占用连接池。很多团队会把这两个值设成几秒,结果业务中一个稍长的数据库查询就会导致网关断连,那属于为了性能牺牲正确性,不推荐。

3.5 第四刀:Linux 内核层面收尾

前面几刀解决的是应用层问题,但压测并发到一定程度后,我观察到 Pod 所在节点的本地端口出现过短暂不足,部分连接出现Cannot assign requested address报错。这属于经典的短连接太多导致的端口表耗尽,Nginx 大量向上游建连时会快速消耗本地端口。

我在节点上调整了内核参数,这类变更需要通过sysctl或节点初始化脚本固化:

net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_tw_reuse = 1 net.ipv4.ip_local_port_range = 2048 65000 net.core.somaxconn = 32768

调小tcp_fin_timeout让处于 FIN_WAIT 状态的连接更快释放,开启tcp_tw_reuse让 TIME_WAIT 套接字在安全条件下复用,扩大ip_local_port_range给高并发连接留更多本地端口。这些参数不是越高越好,tcp_tw_reuse需要内核版本支持且要确认不会影响 NAT 环境下的连接合法性。我在云环境实测过,这批调整对延迟的贡献没有前几刀大,但在极端流量下能避免“毫无征兆的连接建立失败”。

全部调整完后,我再跑了一轮相同参数的压测:P50 降到 25 毫秒,P95 降到 60 毫秒,P99 稳定在 98 毫秒左右。从 2 秒到 100 毫秒,在数据层面达成了标题里的目标。

4. 优化过程中踩过的坑与排查实录

调优过程不是一路绿灯。这一节我把真正影响进度的几个坑说出来,避免你重复踩。

4.1 偶发延迟回弹:每次发布窗口都会出现

完成参数调优后,系统稳定了大概半天。第二天下午,延迟监控又出现几个超过 500 毫秒的点,且时间恰好和一次版本发布重叠。这个问题一开始很迷惑,因为参数没变,后端应用也换了新版本,无法确认是哪边引入的。

通过对比 Ingress 访问日志和发布记录,我发现问题是这样的:后端 Pod 在滚动更新时,旧 Pod 被销毁,Nginx 收到新的 Endpoint 后需要重新建立上游连接,而新 Pod 的 readiness 检查还没完全通过。这时候请求如果被分配到正在启动中的 Pod,连接建立会等待,表现就是偶发超时或慢请求。

解决思路不是在 Nginx 层调参,而是规范发布流程。我为后端服务完善了 readinessProbe,确保新 Pod 真正具备处理能力后才继续滚动;同时调整了滚动更新策略,让maxUnavailable更保守一些。这里想强调一点:网关优化从来不只是改 Nginx 配置,下游应用的生命周期行为也会直接影响延迟表现。

4.2 服务自身 DNS 问题带来的“假阴性”

优化后我在验证某个新接入服务时,又看到了 200 毫秒以上的响应,排查却始终没有头绪。后来发现是服务自身代码里每次请求调用下游时都在做 DNS 解析,而没有复用连接。这类慢请求从监控上看经过网关时确实消耗了时间,但根子其实在应用侧。它给我的教训是:要区分“网关造成的慢”和“网关暴露出来的慢”。通过访问日志中的upstream_response_time,如果后端已经响应很快,则问题在网关;如果后端本身就慢,就别让网关背锅。团队里如果有应用开发者,建议在客户端 SDK 层统一开启连接池和 DNS 缓存,而不是让每个项目各写各的。

4.3 高并发下客户端端口耗尽

还有一个坑出现在压测并发从 300 往 500 提升时,压测机的端口表被打满,大量请求建立连接失败。这类问题容易被误判成“网关高负载”。确认方式很简单:在压测机上看ss -s,如果 TIME_WAIT 数量异常高,或者报错里有Cannot assign requested address,那就是端口耗尽。

我调整了压测机内核参数后恢复,但这也说明一个问题:如果线上有大规模并发客户端,比如很多应用服务器同时访问网关,它们也可能出现类似情况。优化网关的同时,也要让调用方的连接池足够大,并尽量复用长连接。很多“网关变慢”的误报,实际是调用方自己把本地端口耗尽了。

5. 结果沉淀与后续长期治理

优化完成只是开始。如何保证这些参数在下次版本升级、配置变更、人员交接后不丢失,才是长期稳定运行的难点。

5.1 优化前后的量化对比

我整理了一张最终对比表,供团队后续复盘和汇报时直接使用。这份数据是固定压测参数、同一时间段采集的,具有可对比性:

指标优化前优化后改善幅度
P50420 ms25 ms大幅下降
P951.6 s60 ms明显改善
P992.1 s98 ms达到目标
请求成功率99.2%99.95%提升

如果你也想达到类似效果,建议不要追求某个绝对值,而是看同一测试条件下的改善趋势。目标是降低 P99,而不是把 P50 压到最低,因为 P50 的优化很容易通过设置超时来实现,但那样会牺牲长尾请求体验。

5.2 把配置固化为代码,防止漂移

我踩过一个典型坑:Ingress Controller 的 ConfigMap 被团队成员手工改过几次,后来升级版本时忘记了之前的参数,导致延迟回退。现在我把所有相关配置统一维护在 Git 仓库里,包括 ConfigMap、启动参数、内核插件配置,通过类似 GitOps 的流程进行变更。每次修改都有记录,升级前会对比当前生效配置和仓库配置的差异。

这一点对于多人维护的集群尤其重要。网关配置漂移比应用配置漂移更隐蔽,因为它不会立刻报错,只会让性能缓慢劣化。

5.3 用监控与告警守住这个 100ms 的红线

最后一步是监控。如果只优化不监控,问题可能在下一次发布或配置变更后卷土重来。我建立了分位延迟监控,重视 P99 接口耗时和上游连接池指标。对于 Nginx Ingress,可以关注 Controller 自身暴露的指标,比如连接数、请求处理耗时分布。如果 Prometheus 中已有接入,可以用类似下面的查询来做 P99 监控:

histogram_quantile(0.99, sum(rate(nginx_ingress_controller_nginx_process_requests_seconds_bucket[5m])) by (le) )

同时也设置了告警:P99 超过 200 毫秒并持续 5 分钟就触发通知,避免问题只在业务投诉时才被发现。监控的价值不在于看板多漂亮,而在于它能在用户感知之前暴露异常。

我个人这里还有一个小习惯:每次优化完,会把压测命令和结果留档,在真实环境中形成一套可重复的“体检流程”。下次团队里有人提出“网关好像慢了”,先用同一套流程跑数据再下结论。这个习惯帮我挡掉过不少基于感觉的误判。

优化这个 2 秒问题之后,我对网关排障的态度有了明显改变:先看清延迟分布,再逐个击破,不盲目换组件。K8s Ingress 网关只要配置合理,完全能撑住高并发场景。把 DNS 缓存、连接复用、Worker 参数、内核参数这几刀砍到位,从 2 秒到 100 毫秒并不是什么玄学,而是一套可以复现的方法。

RELATED READING

延伸阅读

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