Linux Nginx 怎么测试 keepalive 长连接是否成功复用
前言调优时常遇到这个问题给upstream加上keepalive 32之后怎么证明连接真的被复用了改完配置看不到任何报错但心里没底——万一每个请求还是重建 TCP 连接那这次调优等于白做。另一种情况更隐蔽压测时上游端口耗尽Cannot assign requested address或者后端连接数异常高怀疑就是 keepalive 没生效却拿不出证据。难点在于Nginx 没有「上游连接被复用了几次」这种可以直接读出来的变量。$upstream_addr只告诉你请求被转发到了哪台后端$upstream_status只告诉你后端返回了什么状态码它们都不反映「这条 TCP 连接是新的还是旧的」。所以验证只能靠间接证据。还有一点要先分清楚「客户端到 Nginx」和「Nginx 到上游」是两条独立的链路各自的 keepalive 是分开配置、分开验证的。很多人只想着上游那一侧其实下行那一侧有现成的日志变量可以直接看到复用次数。本文基于 nginx 1.24.x涉及版本差异的指令会单独标注。一、两条链路的配置与可观测性对照维度客户端 ↔ Nginx下行Nginx ↔ 上游上行开启方式默认开启keepalive_timeout控制必须在upstream块里写keepalive N且要配合两个代理头关键指令keepalive_timeout、keepalive_requests、keepalive_timekeepalive、keepalive_requests1.15.3、keepalive_timeout1.15.3、keepalive_time1.19.10日志变量$connection、$connection_requests可直接观察没有对应变量只能间接验证验证难度低看日志即可中需要抓包或跟踪系统调用下行的这两个变量值得单独说明它们是整套验证里最省事的手段$connection连接的唯一序号。每建立一条新的 TCP 连接这个序号就 1。$connection_requests当前这条连接上已经处理过的请求数包含本次。同一条连接上这个值从 1 一路涨上去就是复用发生的直接证据。二、下行验证日志变量加 curl先给一个专用日志格式只加两行配置就能拿到复用证据log_format keepalive_demo $remote_addr [$time_local] conn$connection conn_reqs$connection_requests status$status $request; server { listen 80; server_name localhost; access_log /var/log/nginx/keepalive.access.log keepalive_demo; location / { root /usr/share/nginx/html; index index.html; } }sudo nginx -t sudo systemctl reload nginxcurl 默认会在同一条连接上连续发多个请求抓-v的输出可以肉眼确认curl -sv -o /dev/null \ http://127.0.0.1/ \ -o /dev/null http://127.0.0.1/ \ -o /dev/null http://127.0.0.1/ 21 \ | grep -Ei connected to|re-using|left intact期望看到的关键行curl 版本不同措辞略有差异* Connected to 127.0.0.1 (127.0.0.1) port 80 * Re-using existing connection with host 127.0.0.1 * Re-using existing connection with host 127.0.0.1 * Connection #0 to host 127.0.0.1 left intact只有一次Connected to后面两次是Re-using说明下行复用是通的。再看日志里同一条连接上的计数tail -n 3 /var/log/nginx/keepalive.access.log # 期望形如conn 值相同conn_reqs 依次递增 # 127.0.0.1 [29/Sep/2026:10:00:00 0800] conn15 conn_reqs1 status200 GET / HTTP/1.1 # 127.0.0.1 [29/Sep/2026:10:00:00 0800] conn15 conn_reqs2 status200 GET / HTTP/1.1 # 127.0.0.1 [29/Sep/2026:10:00:00 0800] conn15 conn_reqs3 status200 GET / HTTP/1.1想要一个可量化的对比用ab的-k选项开启 keep-alive跑一轮然后统计日志里出现过多少个不同的conn值# -k 表示使用 HTTP Keep-Alive ab -k -n 1000 -c 10 http://127.0.0.1/ /dev/null # 统计不同连接的数量把输出重定向到空文件后不影响日志写入 grep -o conn[0-9]* /var/log/nginx/keepalive.access.log | sort -u | wc -l判断标准不是某个具体的最大值而是对照关系开-k时不同连接数应该远小于请求总数量级接近并发数-c而把-k去掉重跑同样的命令不同连接数会接近 1000。这两个数字都要你自己在目标环境上跑出来别人机器上的数值说明不了你的配置是否生效。想更贴近真实并发可以用wrkwrk -t2 -c10 -d10s http://127.0.0.1/三、上行验证抓包数 SYN上行这一侧没有日志变量最可靠的办法是数 TCP 的 SYN 包。真正的「新建连接」一定会有一个不带 ACK 标志的 SYN 包复用则不会。配置部分先写对这是前提upstream app_backend { server 127.0.0.1:8080; # 每个 worker 进程缓存的空闲连接数上限注意是「空闲」不是总数 keepalive 32; # 单条连接最多处理多少个请求nginx 1.15.3 keepalive_requests 1000; # 空闲连接在池子里保留多久nginx 1.15.3 keepalive_timeout 60s; # 单条连接的最长存活时间nginx 1.19.10默认 1h keepalive_time 1h; } server { listen 80; location /api/ { proxy_pass http://app_backend; # 这两个是上行 keepalive 生效的必要条件缺一不可 proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }proxy_set_header Connection 的作用是把请求里的Connection头清空。Nginx 默认会向上游发送Connection: close官方文档里明确写了默认只重定义Host和Connection两个字段带着这个头后端处理完就会关闭连接池子里永远留不住东西。验证步骤两个终端配合# 终端 1只匹配「带 SYN 且不带 ACK」的包也就是真正的建连 sudo tcpdump -i lo -n -l \ tcp port 8080 and tcp[tcpflags] tcp-syn ! 0 and tcp[tcpflags] tcp-ack 0# 终端 2发 200 个请求 for i in $(seq 1 200); do curl -s -o /dev/null http://127.0.0.1/api/health done判读方式如果复用生效终端 1 打印的建连数量会远远小于 200大致与同一时刻的并发数同量级如果每个请求都新建连接打印数量会接近 200。想看反例把proxy_http_version 1.1;和proxy_set_header Connection ;两行注释掉、reload再跑一遍同样的循环做对比。另一个观察角度是看当前的 ESTABLISHED 连接数请求发完之后池子里的空闲连接仍然保持 ESTABLISHED 状态直到keepalive_timeout到期。ss -tn state established ( dport :8080 or sport :8080 )还可以直接跟踪系统调用看 Nginx worker 到底有没有在connect()# 需要 rootRHEL 系先 dnf install strace sudo strace -f -e traceconnect \ -p $(pgrep -f nginx: worker process | head -n 1) 21 \ | grep -F :8080跟踪期间只发请求、不发新连接时应该看不到新的connect()调用。要注意两点strace本身有性能开销只在测试实例上用Nginx 有多个 worker请求可能落到没被跟踪的那个进程上所以这个方法的结论不如抓包可靠。四、最容易「以为生效了」的情况排查这类问题有一个固定顺序先确认配置在正确的上下文里再确认两个必需的代理头都在最后才去看池子大小和超时。前两步占问题的大多数尤其是第一个keepalive只在upstream块里有效写在location里 Nginx 会直接报错拒绝启动proxy_http_version 1.1与proxy_set_header Connection 必须出现在每一个用到该 upstream 的 location 里漏掉一个那条路径上的请求就全是短连接上游后端的空闲超时如果比 Nginx 的keepalive_timeout短会出现「池子里的连接其实早被后端关了」的情况Nginx 拿到一条半关闭的连接去发请求日志里就会出现upstream prematurely closed connection while reading response header from upstream并返回 502。所以 Nginx 侧的keepalive_timeout应当小于后端服务的空闲超时。常见坑点只加了keepalive参数没加两个必需的代理头❌upstream里写了keepalive 32;location 里只有proxy_pass✅ 必须同时写proxy_http_version 1.1;和proxy_set_header Connection ;。Nginx 默认向上游发Connection: close不清理这个头池子永远是空的。试图用$upstream_addr判断复用❌ 看到日志里$upstream_addr一直是同一个地址就断定连接复用了 ✅$upstream_addr只说明「路由到了哪台后端」负载均衡到同一台后端也可以每次新建连接。上行的复用只能靠抓包数 SYN或跟踪connect()验证。压测工具没开 keep-alive得出错误结论❌ 用ab -n 1000 -c 10 http://...没有-k测完发现每个请求一条连接断定 Nginx 配置有问题 ✅ab不加-k就是每次都新建连接这是工具的行为不是 Nginx 的行为。测下行复用要么加-k要么用 curl 一条命令发多个 URL。Nginx 的空闲超时比后端长❌keepalive_timeout 300s;而后端服务的空闲超时是 60 秒 ✅ 出现upstream prematurely closed connection加 502 的经典组合。让 Nginx 侧的空闲超时明显小于后端例如 Nginx 60s、后端 75s或者在后端统一调整。keepalive数值按「总连接数」估❌ 并发 500写keepalive 500;以为够用 ✅keepalive N是每个 worker 进程的空闲连接缓存上限总量还要乘worker_processes。这个值设太小不会出错只是复用率上不去设太大则会长期占着后端连接不释放需要结合后端连接数上限一起定。在location里写keepalive❌location /api/ { keepalive 32; proxy_pass ...; }✅keepalive只允许出现在upstream上下文写错位置nginx -t会直接报keepalive directive is not allowed here先过配置检查再谈调优。看到ss里连接数没变就以为没生效❌ 抓包看到请求期间没有新 SYN但ss里的 ESTABLISHED 数量也几乎没变怀疑配置没生效 ✅ 恰好相反这正是复用生效的表现连接一直在池子里待着不新建也不关闭。要看到状态变化可以观察经过keepalive_timeout之后连接数才回落。压测流量被负载均衡分散只测到一台后端❌upstream里有两台后端抓包只盯着127.0.0.1:8080另一台的 8081 没看结论不完整 ✅ 抓包表达式要覆盖全部上游端口或者临时把 upstream 缩到单台再做验证最后再恢复多台配置复测。总结验证目标方法判读依据下行复用是否生效日志里加$connection/$connection_requests同一个conn值上conn_reqs递增下行复用的量化对比ab -k与不带-k各跑一轮统计不同conn数两者数量级差异明显上行复用是否生效tcpdump数「带 SYN 不带 ACK」的包建连数远小于请求数上行复用的补充证据ss观察 ESTABLISHED 连接是否长期驻留请求结束后连接仍存在上行复用的直接证据strace -e traceconnect跟踪 worker稳定期看不到新的connect()记住三句话下行看日志变量上行只能靠抓包数 SYN上游 keepalive 生效的前提是两个代理头缺一个就等于没配Nginx 侧的空闲超时必须小于后端的空闲超时否则省下来的连接会以 502 的形式还回来。验证时一定要准备一组「配置正确」和「配置故意写错」的对照数据自己跑出来的对照关系才是可信的证据。

相关新闻

C++中的Primer拷贝控制和资源管理详解

C++中的Primer拷贝控制和资源管理详解

贝控制和资源管理通常,管理类外资源的类必须定义拷贝控制成员,这种类需要通过析构函数来释放对象所分配的资源。一旦一个类需要析构函数,那么它儿乎肯定也需要一个拷贝构造函数和一个拷贝赋值运算符。为了定义这些成员,我们首先必…

2026/10/4 14:47:36 阅读更多 →
精简组件臃肿:用 TaoToken 统一 Key 通道瘦身 AI 工具链配置

精简组件臃肿:用 TaoToken 统一 Key 通道瘦身 AI 工具链配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 14:46:36 阅读更多 →
2026年6月免费大模型API深度横评:TaoToken统一Key接入智谱、豆包、DeepSeek实测

2026年6月免费大模型API深度横评:TaoToken统一Key接入智谱、豆包、DeepSeek实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 14:46:35 阅读更多 →

最新新闻

vscode使用插件KoroFileHeader添加注释,以及解决快捷键冲突详解(fileheader、cursorTip)

vscode使用插件KoroFileHeader添加注释,以及解决快捷键冲突详解(fileheader、cursorTip)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 15:37:10 阅读更多 →
openrig装配指南:Claude Code与Codex多工具共存实践

openrig装配指南:Claude Code与Codex多工具共存实践

1. 从“openrig”这个标题说起:它到底想解决什么问题第一次看到“openrig”这个词,我脑子里蹦出来的不是某个具体软件,而是一种很典型的开发者诉求:把散落在终端里的 AI 编码工具,用一个统一的“架子”给支棱起来。rig…

2026/10/4 15:37:10 阅读更多 →
Ubuntu避坑指南:Windows10 用 SSH 连接 Ubuntu 的配置与 TaoToken 统一 Key 接入

Ubuntu避坑指南:Windows10 用 SSH 连接 Ubuntu 的配置与 TaoToken 统一 Key 接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 15:37:10 阅读更多 →
插件系统原理与排查实战:从IAR、Harness到MusicFree

插件系统原理与排查实战:从IAR、Harness到MusicFree

开门见山说个现象:你越是频繁接触开发工具、嵌入式IDE、开源播放器,越会撞见一个词刷屏——plugins。最近我看到好几个相关热搜,从"IAR plugins 是干什么的"到"harness failed to load plugins web boot: 2 entries did not a…

2026/10/4 15:37:09 阅读更多 →
MacOS安装go-oci8踩坑实录:从Oracle Instant Client到TaoToken统一Key配置

MacOS安装go-oci8踩坑实录:从Oracle Instant Client到TaoToken统一Key配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 15:37:09 阅读更多 →
编译中文乱码问题排查:从编码声明到 TaoToken 统一 Key 的工程化配置

编译中文乱码问题排查:从编码声明到 TaoToken 统一 Key 的工程化配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 15:36:09 阅读更多 →

日新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 11:40:45 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:36 阅读更多 →