大促压测那次我印象特别深。负责的某业务集群在 8 核 16G 的节点上跑着Nginx Ingress压测目标是从 3000 QPS 往 10000 推。结果刚到 5000 左右接口延迟从 20ms 直接飙到 2 秒连接开始大量超时监控面板上 503 和 502 一片红。当时第一反应是怀疑后端应用扛不住又怀疑云上负载均衡出了问题折腾了大半天最后才定位到问题根本不在后端而在 Ingress 这一层。这篇文章整理的就是那段时间压测、调优、踩坑的全过程给正在搞 Nginx Ingress 高并发实践的同学做个参考。1. 高并发下 Nginx Ingress 的真实瓶颈一次压测引发的思考1.1 压测数据难看最初的怀疑方向全错了先说第一次压测时的状态。节点资源是 8 核 16GIngress 副本只有 2 个后端业务服务压测时 CPU 占用只有 30% 左右带宽也远没有跑满但压测端看到的接口成功率惨不忍睹。第一反应永远是后端的问题于是让业务团队去查代码查数据库连接池查中间件。折腾了一下午业务侧怎么压都稳定直连后端容器 IP 压测完全没问题说明后端根本不是瓶颈。后来我登录到 Ingress-controller 的容器里看日志发现大量这类记录kubectl logs -n ingress-nginx deployment/ingress-nginx-controller --tail2000 | grep upstreamaccess 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 到 keepalive2.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: 60worker-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_rlimit2.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: 60upstream-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% 降到了 0P99 延迟从 1200ms 降到了 60ms 左右。2.3 proxy 超时与缓冲区被忽视的延迟源头调完 keepalive 之后又有新问题冒出来了部分接口偶尔出现超时日志里显示 upstream 连接可以建立但响应读不完。这就是 proxy 层的超时参数的问题。data: proxy-connect-timeout: 5 proxy-read-timeout: 60 proxy-send-timeout: 60proxy-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: false3. 高可用部署让 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-nginxpodAntiAffinity保证多个副本不要落在同一个节点上避免一台宿主机出问题把整个入口全带走。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: 5preStop 的作用是让 Kubelet 在发送 SIGTERM 之前等待一段时间期间 Pod 状态还是 Ready负载均衡器仍然会把流量转发过来但新请求其实已经不会让这个 Pod 处理了——因为滚动更新的顺序是先启动新 Pod等新 Pod Ready 后才会对旧 Pod 执行 preStop。理解这个顺序很关键。terminationGracePeriodSeconds需要大于 preStop 时间不然 preStop 还没执行完就被强杀了。我把这个值设成 60preStop 设 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: 5limit-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: truesocket-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 左右18000P99 延迟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_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_fin_timeout30 fs.file-max655350第二开启 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 在你手里就不再是玄学。
阅读完成 · 觉得有帮助?