接到运维告警的时候,第一反应是后端应用又出问题了。可躲开那一波“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 Controller | Traefik | Envoy 类网关 |
|---|---|---|---|
| 配置成熟度 | 极高,文档和案例最多 | 中等,配置风格新 | 较高,偏云原生 |
| 性能上限 | 强,靠配置调优 | 中规中矩 | 强,指标丰富 |
| 运维成本 | 低,社区方案多 | 中,自定义能力有限 | 较高,学习曲线陡 |
| 动态配置能力 | 依赖 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 优化前后的量化对比
我整理了一张最终对比表,供团队后续复盘和汇报时直接使用。这份数据是固定压测参数、同一时间段采集的,具有可对比性:
| 指标 | 优化前 | 优化后 | 改善幅度 |
|---|---|---|---|
| P50 | 420 ms | 25 ms | 大幅下降 |
| P95 | 1.6 s | 60 ms | 明显改善 |
| P99 | 2.1 s | 98 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 毫秒并不是什么玄学,而是一套可以复现的方法。