NGINX配置proxy_http_version 1.1和proxy_set_header Connection ""【转】
开场
碰到一个线上问题:高负载下,后端 App Pod 一重启,经过 Nginx 转发的这条链路 TPS 恢复特别慢,要 10 分钟左右才能回满;但低负载下完全没这现象,而且直连 App 服务又一点事没有。
复现文件:一个 web-nginx 的 Deployment YAML,一份线上 nginx.conf。我第一反应其实不是 Nginx。因为低负载完全正常,直连 App 也正常,而问题只发生在“高并发 + 后端 Pod 重启”这个组合里。所以我当时先怀疑的是 Pod 摘除、kube-proxy/IPVS,甚至考虑过是不是旧 Pod 的连接没有及时清掉。
结果最后真正让我停下来看的,反而是 nginx.conf 里一眼就能看到的 keepalive 64。
这篇文章记录我完整的复现与排查过程,报错、压测数据、IPVS 状态都在下面,你可以照着复盘。
先说结论(省得你滑到底)
客户 nginx.conf 的 upstream 里写着 keepalive 64,但 location 的 proxy_pass 没有 proxy_http_version 1.1,也没有 proxy_set_header Connection ""。而 Nginx 对 proxy_pass 默认走 HTTP/1.0——upstream 里的 keepalive 是"死配置",压根不生效。每个请求仍然给后端新建一条 TCP 连接,高并发下 TCP 连接风暴把链路打崩,Pod 一重启,恢复到正常要等很久。
修复只需在 location 补两行:
proxy_http_version 1.1;
proxy_set_header Connection "";
环境:测试现场(已脱敏)
| 项目 | 值 |
|---|---|
| Kubernetes | v1.23.15 |
| OS | Ubuntu 20.04.1 LTS, Kernel 5.4.0-51 |
| Nginx | 1.26.3(Deployment 2 副本) |
| kube-proxy | IPVS |
| CNI | Calico |
| 后端 | app-upg:9090(2 副本,重启的是它) |
| 命名空间 | <脱敏>(测试业务命名空间) |
我的复现测试集群:这是按复现 K8s 版本(v1.23.15)独立搭建的一套隔离复现环境,复现里我用 echoserver 充当客户后端 App(命令里是 test-app-svc:8080),没有触碰客户真实环境,只复刻链路形态。后面所有删除 Pod、IPVS 采样等操作都只在这套隔离环境里做。
数据链路:
wrk → Nginx(2 pod) → SVC(ClusterIP) → kube-proxy(IPVS) → App(2 pod)。
我先怀疑了什么
环境 Deployment 里 Nginx 的探针、preStop sleep 10 这些我扫了一眼,先放一边。真正扎眼的是这份线上 nginx.conf:
upstream gatewayApp {
server app-upg:9090 max_fails=3 fail_timeout=10s;
keepalive 64; # ← 写着呢
keepalive_requests 300;
keepalive_timeout 5s;
}
location /app {
proxy_pass http://gatewayApp;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_next_upstream error timeout http_502 http_503;
proxy_connect_timeout 1s;
# ← 没有 proxy_http_version 1.1
# ← 没有 proxy_set_header Connection ""
}
第一眼:keepalive 有啊,64 都写了。这配置"看起来"挺完整。但我心里打了个问号:keepalive 要和 proxy_http_version 1.1 + Connection "" 成对使用才生效,这套配置里根本没有这两行。
我的判断是:在本文这个 Nginx 代理场景中,proxy_pass 默认使用 HTTP/1.0;如果希望使用 upstream keepalive,需要让代理到上游的请求采用 HTTP/1.1,并避免向上游发送 Connection: close。但我不能只靠"记得文档"就下结论,得场景复现并给出有力结论。
设计三组对照,把"问题在哪一环"钉死
为了不让"Nginx 就是凶手"成为我的一厢情愿,我做了三组对照:
| 测试 | 路径 | Nginx 配置 | 目的 |
|---|---|---|---|
| Test A | wrk → App SVC(直连) | 无 Nginx | 验证 App 本身是否正常 |
| Test B | wrk → Nginx → App SVC | 客户原始配置(无 proxy_http_version 1.1) |
试着复现客户问题 |
| Test C | wrk → Nginx → App SVC | 加 proxy_http_version 1.1 + Connection "" |
验证修复是否有效 |
三组统一参数:wrk -t8 -c500 -d600s(8 线程 500 并发跑 10 分钟),压测启动约 40 秒后 kubectl delete pod --force --grace-period=0 删掉 1 个后端 Pod,模拟"Pod 重启"。
先做一个直连测试
直连 App SVC(跳过 Nginx),删一个 Pod,看 App 自己行不行:
kubectl exec -n wx-demo fortio -- sh -c '
nohup wrk -t8 -c500 -d600s --latency http://test-app-svc:8080/ > /tmp/wrk_testA.log 2>&1 &'
# 40 秒后删 1 个后端 Pod
kubectl delete pod -n wx-demo -l app=test-app --force --grace-period=0
结果:App没问题
总请求 12,610,067 HTTP 错误 0 (0%) QPS 21,016 P50 6.49ms P99 1.29s
直连 0 错误,QPS 2 万,删 Pod 也毫无影响。 → App 和 IPVS 本身没问题,问题排除到"Nginx 这一段"。这一步很重要:把 App 端先摘干净,后面所有怀疑都集中在 Nginx 链路上,不会被表象带偏。
把Nginx原配置搬进复现环境
换客户原始 Nginx 配置,同样删一个后端 Pod,结果:
总请求 908,405 HTTP 错误 259,763 (28.6%) QPS 1,513.78 P50 339ms P99 1.00s
我第一次看到 Test B 的 1513 QPS 时,其实就基本确定 Nginx 这一层有问题了。因为直连是 2.1 万 QPS,同样的 App、同样的 500 并发,只是中间多了一层 Nginx,性能直接掉了一个数量级。
问题真的复现了:28.6% 的错误率,QPS 从 2 万砸到 1,513。和客户描述完全对得上(10 分钟 TPS 起不来)。
本想看 IPVS 现场,结果它“太干净”了
测试过程:我在 master 节点起了一个每 6 秒采样的 ipvsadm 监控,盯着 test-app-svc 的 real server 表:
看被删那个后端 RS 在删除瞬间前后的状态(采样间隔 6 秒,23:47:46 → 23:47:52):
ipvsadm -L -n -t 10.233.26.224:8080
结果没复现:这套环境里我压根没抓到 ActiveConn 堆积。
IPVS 没抓到我想要的证据
到这里其实有点尴尬。我原本以为能抓到旧 Pod 的 ActiveConn 堆积,然后顺着连接一直追到旧 Pod。
结果没有。ActiveConn基本一直是0,我甚至把采样间隔从6s调整到3s,还是没有。
# 删除前(23:47:46):两个后端都在表里
TCP 10.233.26.224:8080 rr
-> 10.233.92.10:8080 Masq 1 0 0
-> 10.233.96.8:8080 Masq 1 0 0
# 删除后(23:47:52):旧后端 96.8 已被清掉,新 pod 96.9 进入表里,仍是 2 个后端
TCP 10.233.26.224:8080 rr
-> 10.233.92.10:8080 Masq 1 0 0
-> 10.233.96.9:8080 Masq 1 0 0
这时候我反而觉得这个结果挺重要:不能因为客户现场观察到了某个现象,就强行让自己的复现环境也“复现”出来。
⚠️ 这点我必须如实说:我没有复现出客户现场的 ActiveConn 堆积。不同的后端应用(环境是高开销业务、我这边是瞬时响应的 echoserver)、不同的采样频率,都会影响能不能观察到这个现象。所以这篇我不把"连接堆积/路由到死 Pod"当成我验证过的事实,只当它是客户现场的观测现象 + 一种可能的机制推断。
反而是nginx.conf里的两行配置暴露了问题
在 location 里补上 proxy_http_version 1.1; 和 proxy_set_header Connection "";,其它不动,重跑同样压测+删 Pod:
总请求 12,225,973 HTTP 错误 6,877 (0.056%) QPS 20,371 P50 3.50ms P99 1.54s
- • 错误率从 28.6% → 0.056%(降幅 99.8%)
- • QPS 从 1,513 → 20,371(提升 13.5 倍,已恢复到直连水平)
- • 新 Pod 删除后自动接管流量,链路恢复如初
我不想靠“看配置猜原因”,所以直接做了三个实验。数据对比(都是 500 并发、10 分钟、同一部署):
| 指标 | Test A 直连 | Test B 客户原始 | Test C 修复 |
|---|---|---|---|
| 总请求 | 12,610,067 | 908,405 | 12,225,973 |
| HTTP 错误 | 0 (0%) | 259,763 (28.6%) | 6,877 (0.056%) |
| QPS | 21,016 | 1,513.78 | 20,371 |
| P50 | 6.49ms | 339ms | 3.50ms |
根因到底怎么解释(含我的推断边界)
先看铁证(压测数据钉死的):直连 0 错误 → 经 Nginx 原始配置 28.6% 错误 → 加 proxy_http_version 1.1 + Connection "" 后降到 0.056%。链路里唯一变化的就是 Nginx 这两行,问题定位在 Nginx ↔ 后端的连接管理上,确定无疑。
至于"为什么慢、为什么错误持续",我的理解是:
- 1. 已经验证的:
原配置下高并发场景出现 28.6% HTTP 错误、QPS 1513;增加两项代理连接配置后,错误率下降到 0.056%,QPS 恢复到 20371。 - 2. 可以合理推断:
原配置下 Nginx 到 upstream 的连接复用没有按预期工作,高并发时需要频繁建立上游连接,因此连接建立成本明显增加。 - 3. 目前尚未验证:
至于环境现场为什么会出现长达 10 分钟的恢复过程,以及旧 Pod 连接是否进一步受到 conntrack/IPVS 等机制影响,我这次的 echoserver 环境没有复现,因此这里不把它作为最终根因。
这次排障,我最后记住了什么?
这次排查下来,真正让我印象比较深的,其实不是最后加上的那两行配置,而是一个很容易被忽略的问题:
配置文件里“写了”,不代表它真的按你想的方式生效。
复现环境的 upstream 里明明已经有:
keepalive 64;
第一眼看过去,很容易觉得连接复用已经配置好了。
但实际测试下来,原配置在 500 并发、后端 Pod 重启的场景下,QPS 只有 1513.78,HTTP 错误率达到 28.6%。
而在同样的测试条件下,只增加:
proxy_http_version 1.1;
proxy_set_header Connection "";
QPS 就恢复到了 20371,错误率下降到 0.056%。
所以这次排障给我最大的一个提醒就是:
排查配置问题时,不要只看“有没有配”,还要确认“这个配置到底有没有生效”。
另外还有一个细节,我觉得也值得记录下来。
这次我原本想通过 IPVS 的 ActiveConn 去证明旧 Pod 连接没有及时释放,但在自己的复现环境里并没有抓到这个现象。
所以最后文章里,我没有把它硬写成确定的根因。
客户现场看到的现象,可以作为排查线索;自己没有验证出来的,就不要当成已经证明的结论。
对于技术排障来说,我越来越觉得,“没复现出来”本身也是测试结果。
这次最终能确认的是:
- • 原始 Nginx 配置在高并发 + Pod 重启场景下确实可以复现明显性能问题;
- • 直连 App 时没有出现同样的问题;
- • 修改 Nginx 上游连接配置后,QPS 和错误率明显恢复;
- • IPVS
ActiveConn长时间堆积这一现象,在我的复现环境中没有验证出来。
这可能也是我现在比较喜欢记录这类问题的原因。
不是单纯把“正确配置”贴出来,而是把我当时怎么怀疑、怎么验证、哪里没验证出来,以及最后到底能确定什么都记录下来。
如果你也在折腾 Kubernetes、Nginx、Ingress、IPVS 这些东西,后面我还会继续记录一些实际环境里的问题复现和测试过程。
这里是《云原生测试笔记》,关注一下,后面继续一起踩坑、排坑。
转自
Nginx 配了 keepalive,为什么高并发下还是恢复很慢?我做了 3 组对照测试

浙公网安备 33010602011771号