☰
Nginx Ingress高并发压测排障:从连接数耗尽到keepalive调优实战
2026/10/21 12:49:27 网站建设 项目流程

大促压测那次,我印象特别深。负责的某业务集群在 8 核 16G 的节点上跑着Nginx Ingress,压测目标是从 3000 QPS 往 10000 推。结果刚到 5000 左右,接口延迟从 20ms 直接飙到 2 秒,连接开始大量超时,监控面板上 503 和 502 一片红。当时第一反应是怀疑后端应用扛不住,又怀疑云上负载均衡出了问题,折腾了大半天,最后才定位到问题根本不在后端,而在 Ingress 这一层。这篇文章整理的就是那段时间压测、调优、踩坑的全过程,给正在搞 Nginx Ingress 高并发实践的同学做个参考。

1. 高并发下 Nginx Ingress 的真实瓶颈:一次压测引发的思考

1.1 压测数据难看,最初的怀疑方向全错了

先说第一次压测时的状态。节点资源是 8 核 16G,Ingress 副本只有 2 个,后端业务服务压测时 CPU 占用只有 30% 左右,带宽也远没有跑满,但压测端看到的接口成功率惨不忍睹。第一反应永远是后端的问题,于是让业务团队去查代码,查数据库连接池,查中间件。折腾了一下午,业务侧怎么压都稳定,直连后端容器 IP 压测完全没问题,说明后端根本不是瓶颈。

后来我登录到 Ingress-controller 的容器里看日志,发现大量这类记录:

kubectl logs -n ingress-nginx deployment/ingress-nginx-controller --tail=2000 | grep "upstream"

access log 里全是upstream connect error、connect() failed (99: Cannot assign requested address)。看到Cannot assign requested address的时候我基本就明白了:这不是什么玄学问题,是连接数耗尽,端口不够用了。

回头看监控,Ingress 节点的 CPU、内存一直没打满,但 TCP 连接数从几千一路涨到几万,然后开始有大量连接处于TIME_WAIT状态。这就是高并发下最常见的隐蔽瓶颈——资源看着没满,实际连接资源早已耗尽。

1.2 流量进入集群后的层层消耗

要理解 Nginx Ingress 在高并发下为什么容易成为瓶颈,得先看清一条请求从外面进来要经过多少层。我一般会给团队画这样的链路:

外部客户端 -> 云负载均衡器 -> ingress-nginx Pod -> Service -> 后端业务 Pod

每一层都有不可避免的开销:

环节主要开销高并发时的放大效应
客户端建连TCP 三次握手、TLS 握手握手消耗 CPU,延迟叠加
Ingress 处理HTTP 解析、路由匹配、rewrite、header 处理每个请求都要消耗 CPU 周期
Upstream 拨号Ingress 到后端新建 TCP 连接连接数爆发,端口耗尽,延迟增加
日志写入access log 同步写盘高 QPS 下磁盘 IO 被拖垮

如果 Keepalive 没有正确配置,那问题会被放大得非常夸张。5000 QPS 意味着每秒钟要新建 5000 条 TCP 连接到后端 Pod,每条连接在断开后都会进入TIME_WAIT状态,按 60 秒的默认超时计算,系统里最多会堆积 30 万条连接记录。而节点默认的可用端口范围只有 6 万左右,不耗尽才怪。这一层的消耗才是高并发下最需要优先处理的点。

2. 真正决定并发上限的核心参数:从 worker 到 keepalive

2.1 worker_processes 不是越大越好,关键在连接模型

很多人一想到 Nginx 调优,第一件事就是调 worker 数量。Ingress-nginx 的 ConfigMap 里确实有worker-processes,但并不是无脑调到最大。

Nginx 的每个 worker 是一个单线程事件循环,它处理的是事件而不是阻塞的线程。在 CPU 密集的 HTTP 解析、TLS 握手场景下,worker 数量等于 CPU 核数通常是最合理的配置。设置太多会导致上下文切换开销变大,设置太少又浪费多核能力。

我当时的配置是这样的:

apiVersion: v1 kind: ConfigMap metadata: name: ingress-nginx-controller namespace: ingress-nginx data: worker-processes: "8" worker-rlimit-nofile: "65535" max-worker-connections: "65535" worker-shutdown-timeout: "60"

worker-rlimit-nofile这个参数很容易被忽略,它决定了一个 worker 进程能打开的文件描述符上限。默认值在小并发下够用,但高并发下每个连接都要占用一个 fd,如果这个值不调大,即使 CPU 有余量,连接数也被卡死了。

还有个容易被忽略的点:Ingress-nginx 容器本身没有跑在宿主机上,而是运行在 Pod 里,但 worker 相关的限制同样会生效。如果发现配置改了没生效,先确认对应的 ConfigMap 是否挂载成功:

kubectl -n ingress-nginx exec deployment/ingress-nginx-controller -- cat /etc/nginx/nginx.conf | grep worker_rlimit

2.2 keepalive 是最高性价比的优化,没有之一

在一次完整的压测调优里,收益最大的配置就是 Keepalive。这里要分两段来看,很多人只关注了一段,忽略了另一端,结果效果就打了对折。

第一段是客户端到 Ingress的 Keepalive。Nginx Ingress 默认开启,客户端复用 TCP 连接,减少重复握手。第二段是Ingress 到后端 Pod的 Keepalive,这一端默认是关闭的,只会在配置了 upstream keepalive 之后才生效。

我当时的 ConfigMap 里加了这些:

data: keep-alive: "1024" keep-alive-requests: "10000" upstream-keepalive-connections: "1024" upstream-keepalive-requests: "10000" upstream-keepalive-timeout: "60"

upstream-keepalive-connections表示每个 worker 进程最多保持多少条到后端的长连接,upstream-keepalive-requests表示一条长连接最多复用多少次。这里有个我认为非常关键的细节:keep-alive-requests和upstream-keepalive-requests一定要设置得足够大。

我见过不少线上配置,keep-alive-requests用的是默认的 100,也就是说一条连接复用 100 次就关了。5000 QPS 下,每秒也会产生 50 次连接重建。设置成 10000 之后,连接就能稳定复用很长时间,TIME_WAIT 连接数量会下降几个数量级。

这段配置的效果立竿见影。我压测时没有调整任何后端代码,仅仅靠启用 upstream keepalive,丢请求率就从接近 10% 降到了 0,P99 延迟从 1200ms 降到了 60ms 左右。

2.3 proxy 超时与缓冲区,被忽视的延迟源头

调完 keepalive 之后,又有新问题冒出来了:部分接口偶尔出现超时,日志里显示 upstream 连接可以建立,但响应读不完。这就是 proxy 层的超时参数的问题。

data: proxy-connect-timeout: "5" proxy-read-timeout: "60" proxy-send-timeout: "60"

proxy-connect-timeout是建立与后端连接的超时时间。这个值不一定要很大,因为如果后端真的建连超过几秒,基本说明后端有问题,与其花时间等,不如快速失败让上游重试。proxy-read-timeout是指两次读事件之间的间隔超时,不是整个响应必须在这个时间内完成,这点很多人理解错了。

如果一个后端接口处理本身就要 30 秒,但proxy-read-timeout只设了 10 秒,那这接口一定反复失败。高并发下这样的误杀还挺常见的,因为压测时接口 RT 波动变大,一次 GC 暂停就可能超过这个阈值,然后 Ingress 主动断开连接,前端看到的就是 504。

还有个相关参数是proxy-buffering。如果后端返回大响应体,关闭 buffering 会让数据边到边发,首字节延迟低,但在高并发下会增加 Ingress 到客户端的连接占用时间。我的建议是:普通接口保持默认打开,需要流式响应或 SSE 的接口单独用注解关闭。

nginx.ingress.kubernetes.io/proxy-buffering: "false"

3. 高可用部署:让 Ingress 本身先不挂掉

3.1 多副本、反亲和与 Pod 分布

调优参数只是让单机能力变强了,但如果 Ingress 这个组件本身挂了,再高的单机性能也没意义。入口网关的可用性要求比普通业务服务高一个等级,因为它一挂,整个集群的流量全部断了。

我现在的标准部署是这样的:

apiVersion: apps/v1 kind: Deployment metadata: name: ingress-nginx-controller namespace: ingress-nginx spec: replicas: 3 template: spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app.kubernetes.io/name operator: In values: - ingress-nginx topologyKey: kubernetes.io/hostname topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app.kubernetes.io/name: ingress-nginx

podAntiAffinity保证多个副本不要落在同一个节点上,避免一台宿主机出问题把整个入口全带走。topologySpreadConstraints更进一步,让副本尽量均匀分布在不同可用区,这个在多可用区集群里非常重要。

这里要提醒一句:Ingress 副本多了之后,前面的负载均衡器必须有办法把所有副本都作为后端。如果用的是 NodePort 类型暴露,那每个节点上的 NodePort 都要能被负载均衡器转发到。否则副本再多,负载均衡器只往一个节点打流量,就起不到高可用的作用。

3.2 滚动更新不丢请求,细节在 preStop 和就绪探针

有一次我执行了一次 Ingress 升级,结果在滚动更新期间线上出现了持续 20 秒的 502。排查下来原因很简单:旧 Pod 收到 SIGTERM 之后立刻被销毁,但此时它还有不少正在处理的请求,连接被强制断开,自然就 502 了。

解决方案是给 Ingress 加上 preStop 钩子,让 Pod 在退出前先休眠几秒,给正在处理的请求一个缓冲时间:

spec: terminationGracePeriodSeconds: 60 containers: - name: controller image: registry.cn-hangzhou.aliyuncs.com/google_containers/nginx-ingress-controller:v1.9.4 lifecycle: preStop: exec: command: - sh - -c - sleep 10 readinessProbe: httpGet: path: /healthz port: 10254 initialDelaySeconds: 10 periodSeconds: 5

preStop 的作用是让 Kubelet 在发送 SIGTERM 之前等待一段时间,期间 Pod 状态还是 Ready,负载均衡器仍然会把流量转发过来,但新请求其实已经不会让这个 Pod 处理了——因为滚动更新的顺序是先启动新 Pod,等新 Pod Ready 后,才会对旧 Pod 执行 preStop。

理解这个顺序很关键。terminationGracePeriodSeconds需要大于 preStop 时间,不然 preStop 还没执行完就被强杀了。我把这个值设成 60,preStop 设 10 秒,实测滚动更新期间请求零丢失。

3.3 外部负载均衡器与 Ingress 的协作细节

Ingress 前面那层 Load Balancer,很容易成为隐形杀手。它跟 Ingress 之间有各自的连接超时和健康检查机制,如果两者参数不协调,会出现很多奇怪的现象。

最常见的坑是:负载均衡器的空闲连接超时时间比 Ingress 的 keepalive 超时时间短。假设 LB 的空闲超时是 60 秒,而 Ingress 的客户端 keepalive 是 75 秒,那 LB 会在连接空闲到 60 秒时主动断开连接。客户端不知道这层已经断了,继续在这个连接上发请求,就会触发一次 TCP 层重连,在高并发下表现为偶发请求延迟抖动。

所以 LB 的空闲超时最好设置得比 Ingress 的 keepalive 超时更长,或者反过来,让 Ingress 的 keepalive 超时比 LB 更短,保证是 Ingress 主动关闭连接。两者之间留出余量,就能避免大量半开连接。

健康检查也要注意。很多负载均衡器的健康检查默认走的是 TCP 端口探测,如果只是探测端口通不通,那 Ingress 即使处于异常状态(比如 worker 全部被打满),端口仍然是通的,LB 依然会往里打流量。更好的做法是让 LB 健康检查走 HTTP 路径,直接探测 Ingress 的/healthz接口,这个接口能真实反映组件是否还能处理请求。

4. 高并发下的流量治理:性能之外的保命手段

4.1 限流限速:保护后端也是保护自己

压测调优到一定程度之后,我发现高并发下真正需要思考的问题不是"怎么让 Ingress 撑住更多流量",而是"怎么防止异常流量把后端击穿"。Ingress 是流量入口,如果它不设防,后端再强壮也可能被冲垮。

Nginx Ingress 的限流注解非常好用,可以在 Ingress 规则里直接配置:

annotations: nginx.ingress.kubernetes.io/limit-rps: "100" nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"

limit-rps是每秒最大请求数,limit-burst-multiplier是突发流量的倍数。它的原理类似令牌桶算法:桶里每分钟固定补充令牌,请求来了要拿令牌才能放行;burst允许一定程度的突发,比如短时间内一瞬间涌入 500 个请求,也不会全部被拒。

我遇到过一种情况:某个接口平时 QPS 只有几十,大促时被外部脚本刷到了上千。如果没有限流,这个请求会全部打到数据库,把数据库连接池打满,然后雪崩到整个应用。有了 Ingress 限流之后,超额的请求直接在入口被拦截,返回 429,后端压力保持平稳。限流对用户来说可能体验变差,但总比整个服务 502 要好得多。

4.2 灰度发布与精细化路由:流量切换不再心跳加速

高并发活动前最怕什么?最怕改配置和发版本。Ingress 提供的 canary 机制对流量切换帮助很大,可以按 header、cookie 或者权重来做灰度,不必一下子把所有流量都切过去。

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-service-canary annotations: nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-weight: "10" spec: rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: my-service-new port: number: 80

这个配置表示把 10% 的流量切到新版本服务。确认稳定后,把权重改成 20、50、100 逐步放大。使用这种方式做流量切换,最大的好处是随时可以回退,把权重改回 0 就能立即切回旧版本。在大促准备阶段,我用它做了一次稳妥的版本升级,全程没有中断过流量。

4.3 TLS 会话复用与 HTTP/2 的取舍

高并发场景下 TLS 握手开销是个大头。一次完整的 TLS 握手需要多个 RTT,如果每个连接都完整握手一次,CPU 消耗和延迟都会明显上升。

Nginx Ingress 支持 TLS 会话复用,可以在 ConfigMap 里开启:

data: ssl-session-cache: "true" ssl-session-cache-size: "10m" ssl-session-tickets: "true"

socket-session-cache在服务端保存会话信息,客户端复用 session ID 可以跳过部分握手步骤;ssl-session-tickets则是用 ticket 的方式让会话状态跟随客户端。这两个搭配使用,握手的资源消耗能下降一大截。

HTTP/2 也值得提一下。它能在一条连接上多路复用多个请求,对于有小对象并发请求的前端页面来说提升明显。但开启 HTTP/2 之后要注意:有些后端服务对 HTTP/2 的 keepalive 处理不完善,可能出现连接卡死。如果开了 HTTP/2 观察到奇怪的连接悬挂问题,可以先关掉验证一下是否是它导致的。

5. 压测验证与监控告警:如何证明调优有效

5.1 压测模型和工具选择,别一上来就打满

调优工作做完后,最关键的一步是验证效果。压测不是简单地拿工具把流量灌进去看数字,而是要设计合理的测试模型。

我惯用的方式是阶梯加压:先跑一个低并发,比如 100 并发,持续 3 分钟观察指标;然后逐步升到 500、1000、2000 并发,每个档位跑 5 到 10 分钟。这样做的好处是能观察到系统在不同压力下的拐点,而不是直接打爆之后才回过头来分析。

压测工具方面,wrk和vegeta都可以。简单验证用 wrk 最方便:

wrk -t8 -c1000 -d120s --latency https://api.example.com/v1/health

-t8表示 8 个线程,-c1000表示 1000 个连接,-d120s表示持续 120 秒。wrk 默认会复用连接,所以它压测的是连接复用情况下的性能;如果需要模拟短连接场景,可以使用wrk2或者自己写压测脚本。

我调优前后的一组数据对比,非常直观:

指标调优前调优后
稳定支撑 QPS5000 左右18000+
P99 延迟1200ms45ms
连接数峰值端口耗尽,大量 TIME_WAIT稳定在数百条长连接复用
502/503 错误率高并发下超过 10%0

这组数据说明:在高并发场景下,参数层面的调优价值比大多数人都想象的更大。后端服务一行代码没动,仅靠 Ingress 的配置调整,整体吞吐能力就翻了将近 4 倍。

5.2 关键监控指标与告警阈值

调优之后,监控是后续长期稳定运行的基础。ingress-nginx 自带 Prometheus metrics,可以在 ConfigMap 里开启抓取端口。重点盯以下指标:

指标含义建议告警阈值
nginx_ingress_controller_requests每秒请求数对比基线,突然上涨 200% 告警
nginx_ingress_controller_request_duration_seconds请求延迟分布P99 超过基线 50% 持续 5 分钟
nginx_ingress_controller_nginx_process_connections当前连接数接近 max-worker-connections 的 80%
nginx_ingress_controller_upstream_latency_seconds到后端的延迟P99 超过 500ms 持续 5 分钟

告警规则用 PromQL 可以这么写:

# 5xx 比例超过 1% 持续 5 分钟告警 sum(rate(nginx_ingress_controller_requests{status=~"5.."}[5m])) / sum(rate(nginx_ingress_controller_requests[5m])) > 0.01 # 延迟 P99 上升告警,需要配合 histogram_quantile histogram_quantile(0.99, sum(rate(nginx_ingress_controller_request_duration_seconds_bucket[5m])) by (le, host)) > 0.5

第一条规则是最基础也最有效的:Ingress 的 5xx 比例一上来,基本说明有系统性问题。比起看后端应用指标,这里能更早发现问题,因为所有流量都会经过它。

6. 实战踩坑记录与排查思路

6.1 容易翻车的配置误区,我全踩过

写到这里,把我踩过的坑集中列一下,每个坑都对应一次线上事故或者压测失败的教训。

第一,只调 worker 参数不调系统内核参数。Ingress 容器运行在节点内核之上,宿主机的net.ipv4.ip_local_port_range、net.ipv4.tcp_tw_reuse、net.ipv4.tcp_fin_timeout等参数直接决定了连接上限。当时我发现压测数据到了某个节点就上不去,查下来是宿主机默认端口范围太窄。现在我会在集群节点的内核参数里加上这些:

sysctl -w net.ipv4.ip_local_port_range="1024 65535" sysctl -w net.ipv4.tcp_tw_reuse=1 sysctl -w net.ipv4.tcp_fin_timeout=30 fs.file-max=655350

第二,开启 upstream keepalive 但后端服务自己不支持长连接。有些后端框架对连接复用支持得不好,特别是老版本的 HTTP/1.0 服务,连上之后服务端主动断开,Ingress 复用连接等于反复撞上关闭的连接。这时候要么升级后端服务,要么在后端服务前面再挂一层反向代理来终结 keepalive。

第三,调整参数后没有重载生效。Nginx Ingress 的 ConfigMap 改动,正常情况下会自动触发重载,但如果内容有语法问题或者引用了不存在的注解,重载会失败。不要相信"我改了配置就肯定生效了",一定要通过kubectl logs观察实际的 reload 日志,必要时直接重启 Deployment 来强制生效。

第四,日志级别开到 DEBUG 忘了关。压测期间为了排查问题把log-level调成了 debug,结果一压测,光日志就把磁盘 IO 打满了,造成一种"怎么调都跑不快"的假象。排查完问题之后第一件事就是把日志级别改回 notice 或 warn。

6.2 一次完整排障链路复盘:从 502 到根因只花了四步

最后分享一次比较典型的线上排障过程,整个链路很有代表性。某天上午,监控突然报警,某个核心 API 的 502 比例飙升到 15%,业务方反馈页面大面积转圈。

第一步,看 Ingress 日志确认错误类型。执行命令之后发现大量upstream connect error,说明 Ingress 到后端连接失败。此时后端 CPU、内存指标都正常,说明不是后端资源问题。

第二步,检查后端服务连接状态。登录后端节点执行ss -s,发现TIME_WAIT数量到了 3 万多个,明显异常。这说明 Ingress 和后端之间的连接在频繁建立和断开,长连接没有生效。

第三步,排查 Ingress 的 upstream keepalive 配置。打开 ConfigMap 一看,upstream-keepalive-connections确实配置了,但再一看 Ingress controller 版本,发现新版本里这个字段已经从upstream-keepalive-connections改成了upstream-keepalive-connections_per-worker,旧字段虽然不会报错,但已经不起作用了。

第四步,用新字段名重新配置并重载,观察连接数变化。很快TIME_WAIT数量从三万多降到了几百,502 全部消失。

这个案例想表达的核心观点是:Nginx Ingress 的很多问题,表面上看是网络问题,实际上是连接模型和配置版本的匹配问题。排障的时候不要一上来就怀疑云平台、怀疑网络,先看日志给的方向,再验证连接状态,最后对照配置,一步步缩小范围。你花的每一分钟在日志和连接状态上,都会在定位根因时加倍省回来。

那次大促之后,我养成了一个习惯,每次调整 Ingress 相关参数都会顺手再用 wrk 压一轮短连接场景,因为短连接最能暴露连接管理的问题。高并发实践这件事,参数只是结果,真正值钱的还是对连接模型的直觉——哪些配置会影响连接生命周期,哪些问题本质上是连接资源不足,想通了这些,Nginx Ingress 在你手里就不再是玄学。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询