news 2026/10/18 11:34:51

Nginx stream模块四层代理实战:TCP/UDP转发、SNI分流与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nginx stream模块四层代理实战:TCP/UDP转发、SNI分流与避坑指南

这几年做基础架构,我排查过的线上事故里,至少有一半不是应用代码写错了,而是流量在负载均衡这一层出了问题。业务从单机长成多机集群之后,数据库、消息队列、游戏服、日志采集几乎都会要求“只暴露一个统一入口,内部再分发”。Nginx 的 stream 模块,也就是常说的四层 TCP/UDP 代理,是我目前用过最顺手的解法。它从 Nginx 1.9.0 开始进入开源版本,配置思路和 HTTP 反向代理很接近,但处理的不是 URL,而是 IP+端口级别的连接。这篇文章我尽量把 stream 模块的适用场景、配置要点、踩坑经验一次讲清楚,适合正在规划内网网关、或者打算用 Nginx 统一四层入口的运维和开发同学。

1. 为什么选择 Nginx 做四层代理

1.1 先分清四层代理和七层代理到底差在哪

在聊配置之前,必须先把概念捋直。我们平时用得最多的proxy_pass http://xxx属于七层代理,工作在应用层,能解析 HTTP 方法、URL、Header、Cookie,所以可以做域名路由、URL 改写、鉴权、限流这些精细操作。但它的局限也很明显:HTTP 代理只能处理 HTTP/HTTPS 流量,对于 MySQL 协议、Redis 协议、DNS 查询这些非 HTTP 流量就无能为力了。

四层代理的位置在传输层,也就是 TCP/UDP 这一层。它不关心载荷里是 SQL 还是 Redis 命令,只负责把从客户端进来的字节流原封不动地转发给后端,再把后端的响应原封不动地传回去。换句话说,四层代理是一个“透明的搬运工”,而七层代理是一个“会看内容的调度员”。Nginx 的 stream 模块做的就是这个搬运工的工作,但它的搬运能力比很多人想象中要强得多。

1.2 同类工具对比,Nginx 的优势到底在哪

做四层代理不是只有 Nginx 一个选择。我最早尝试过 LVS,它的性能确实强悍,基于内核态转发,但配置和维护成本高,而且 DR 模式需要调整后端服务器的网络参数,对跨机房、跨网段场景很不友好;后来也试过 HAProxy,它确实是个专业四层代理,TCP 调度能力极其成熟,不过如果团队已经熟练使用了 Nginx,再引入一套新配置语法的工具,学习成本和踩坑成本都要翻倍;iptables 的REDIRECT倒是能实现端口转发,但只能做简单的 DNAT,做不到灵活的负载均衡和健康检查,更谈不上按域名分流。

Nginx 的优势在于它把七层和四层的能力封装在同一套配置体系里。同一个进程可以既处理 HTTP 流量,又处理 MySQL 或 DNS 的流量,使用的是相似的语法结构,团队不用重新学一套系统。另外,Nginx 的 epoll 事件模型在流式转发场景下延迟很低,内存占用也小,实测一个中等规模的连接转发节点,内存消耗往往只有同样连接数的其他中间件的一半左右。它和 HAProxy 相比,最大的差异化其实是“能做基于 SNI 的应用层路由”,这一点在后面的配置里会详细展示。

2. stream 模块配置体系与核心指令拆解

2.1 stream 上下文的位置和边界

stream 配置块和 http 配置块一样,都位于 nginx.conf 的顶层,两者是并列关系。很多人第一次接触时会习惯性地把 stream 块写进 http 里,结果执行nginx -t直接报错。正常结构是这样的:

events { worker_connections 1024; } http { # 其他 HTTP 配置 } stream { # 四层代理配置 }

stream 块内部有自己的server指令,但没有location、if这些 HTTP 层的东西。stream 里的每一个server块就代表一个监听端口,端口背后可以直连后端,也可以挂到 upstream 组里做负载均衡。如果业务同时需要 HTTP 和 TCP/UDP 代理,直接在两个配置块里各写各的监听就行,互不干扰。

2.2 listen、proxy_pass 和 upstream 的基本用法

一个最简的 TCP 代理只需要三行:

stream { server { listen 3306; proxy_pass 192.168.1.10:3306; } }

意思很直白:Nginx 监听 3306 端口,把进入的连接转发给192.168.1.10:3306。这种写法适用于单机转发,不过生产环境里的后端往往是多个节点,这时就需要引入 upstream:

stream { upstream db_readonly { server 192.168.1.11:3306 weight=3; server 192.168.1.12:3306 weight=2; server 192.168.1.13:3306 backup; } server { listen 3306; proxy_pass db_readonly; } }

weight控制权重,backup表示热备节点,平时不接收流量,只有其他节点都不可用时才顶上。这里的语义和 HTTP upstream 完全一致,学过一次就能迁移使用。

2.3 TCP 和 UDP 的关键区别:udp 参数与 proxy_responses

TCP 和 UDP 在 stream 配置里最大的区别就在于listen后面的udp参数和proxy_responses指令。

stream { server { listen 53 udp; proxy_pass 192.168.2.53:53; proxy_responses 1; proxy_timeout 10s; } }

TCP 是面向连接的,Nginx 知道连接何时建立、何时关闭,所以它可以依赖连接状态来做管理;但 UDP 是无连接的,Nginx 怎么判断一个 UDP “会话”结束了?靠的是proxy_responses和proxy_timeout。proxy_responses表示后端预期返回的数据报数量,DNS 查询一般是请求一个包、回复一个包,所以设成 1;如果不知道或不确定回复数量,就省略这个参数,让 Nginx 自然等待proxy_timeout超时后关闭会话。没有这两个参数撑着,UDP 代理就很容易在转发一条数据报后,把后续所有客户端请求都错配到同一个后端,表现出的现象就是“流量歪了”。

3. 实操:TCP 代理、UDP 代理与灵活的分流方案

3.1 编译参数确认与环境准备

很多发行版自带的 Nginx 已经包含 stream 模块,但保险起见还是先确认一下:

nginx -V 2>&1 | grep stream

输出里如果有--with-stream,说明基础模块可用;如果要做 UDP 代理,这个就够了。不过要做 SSL 终结和 SNI 分流的话,还需要额外确认是否有--with-stream_ssl_module和--with-stream_ssl_preread_module。用系统包管理器装的 nginx 普遍带了这些,但用某些精简版编译包时,只有--with-stream,少了 SSL 相关模块,做 SNI 路由时会发现ssl_preread on;直接报 unknown directive。这个坑我踩过,所以建议先跑一次nginx -V再动手。

3.2 TCP 场景一:MySQL 读写分离的入口收敛

假设后端有主从两套数据库,我们希望外部只访问一个 MySQL 地址,主库承担写入、从库承担只读查询。应用层已经做好了读写分离,数据库前端只需要一个稳定的入口:

stream { upstream mysql_write { server 192.168.1.101:3306; } upstream mysql_read { server 192.168.1.102:3306 weight=3; server 192.168.1.103:3306 weight=3; server 192.168.1.104:3306 backup; } server { listen 3316; proxy_pass mysql_write; proxy_connect_timeout 5s; proxy_timeout 300s; } server { listen 3317; proxy_pass mysql_read; proxy_next_upstream on; } }

这里把写入口放在 3316,读入口放在 3317,应用侧的配置指向不同端口即可。proxy_connect_timeout 5s控制与后端建立连接的超时时间,超过 5 秒就让出连接,避免把失败请求长时间挂起;proxy_timeout是空闲超时,MySQL 长连接通常保持在分钟级,所以我习惯给到 300 秒。proxy_next_upstream on表示当前后端失败后尝试下一个节点,对数据库这种需要高可用的后端很关键。

3.3 TCP 场景二:按 SNI 把 TCP 流量分流到不同后端

这个场景是我觉得 stream 模块最有价值的地方。公司里有几个老旧系统复用了 443 端口,有的是 HTTPS 七层流量,有的是自研私有协议,还有的是远程运维通道。如果按端口区分,每个服务都得记一个新端口,很不友好;其实可以在同一端口上按 TLS 握手时的 SNI 字段做分流。

stream { map $ssl_preread_server_name $backend_pool { default web_backend; ops.internal.example.com ops_backend; legacy.internal.example.com legacy_backend; } upstream web_backend { server 10.0.0.10:443; } upstream ops_backend { server 10.0.0.11:22; } upstream legacy_backend { server 10.0.0.12:8443; } server { listen 443; proxy_pass $backend_pool; ssl_preread on; } }

核心是ssl_preread on,它让 Nginx 在不解密 TLS 流量的情况下,把客户端握手里的 SNI 域名读出来,写入变量$ssl_preread_server_name,然后通过map映射到对应 upstream。这样运维通道、老系统、普通业务都能共用 443 端口,对外只暴露一个入口。需要注意,这种模式的后续转发是完全透传的,Nginx 不会做 TLS 卸载,所以后端的证书是原始业务自己的,逻辑上更接近一个“智能端口转发器”。

3.4 UDP 场景一:DNS 查询的负载均衡

UDP 代理用得最多的场景之一就是 DNS。内部有多个 DNS 后端,前端统一收口在 53 端口:

stream { upstream dns_upstreams { server 192.168.1.120:53; server 192.168.1.121:53; server 192.168.1.122:53; } server { listen 53 udp; proxy_pass dns_upstreams; proxy_responses 1; proxy_timeout 10s; } }

这里的proxy_responses 1非常关键,因为 DNS 通常是一问一答。如果后端链路经常 10 秒以上没有响应,就要排查上游 DNS 是否真的在服务,而不是怀疑 UDP 代理丢包。UDP 转发对 Nginx 来说会和 TCP 一样选择一台后端建立映射,后续同一个客户端的 DNS 请求会尽量发往同一台后端,这本身就是一种天然的本地化缓存优化。

3.5 UDP 场景二:游戏服务器或自定义 UDP 协议

游戏服务器大比例使用 UDP 承载实时对战数据,当游戏从单体扩展成多区服集群时,客户端需要一个固定的接入 IP。Nginx stream 的 UDP 代理可以把不同玩家的数据报分散到不同区服节点:

stream { upstream game_region { hash $remote_addr consistent; server 10.0.0.20:9000; server 10.0.0.21:9000; } server { listen 9000 udp; proxy_pass game_region; proxy_timeout 30s; } }

这里用了hash $remote_addr consistent;做一致性哈希,让同一个玩家 IP 尽可能固定落到同一个区服节点,避免跨区传送进度数据。proxy_timeout 30s表示如果 30 秒内该玩家没有新的数据报,Nginx 就把这个 UDP 会话回收,释放占用的内存和 fd。对实时对战场景来说,这个值可以更短,像 5 到 10 秒就够,能显著降低空闲会话数量。

3.6 用 PROXY Protocol 保留客户端真实 IP

四层代理普遍面临一个问题:后端看到的 IP 是代理节点的 IP,不是真实客户端 IP。对数据库审计、风控系统来说,这个信息很重要。解决方法是使用 PROXY Protocol。它会在 Nginx 和后端建立连接时,附带一段明文协议头,把原始客户端地址信息传给后端。

stream { server { listen 443 proxy_protocol; proxy_pass 10.0.0.30:443; proxy_protocol on; } log_format stream '$proxy_protocol_addr - [$time_local] $protocol $status'; access_log /var/log/nginx/stream.log stream; }

listen ... proxy_protocol表示允许客户端通过 PROXY Protocol 接入,proxy_protocol on表示向后端转发时也带上该协议头。后端软件只要支持 PROXY Protocol,比如 HAProxy、Nginx stream、某些 Redis 前端,就能直接解析出真实来源 IP。日志格式里的$proxy_protocol_addr也是排障时很有用的变量,配合访问日志能定位真正发起请求的客户端。

4. 常见问题排查技巧与避坑实录

4.1 UDP 代理“时而正常时而异常”的真相

UDP 代理不像 TCP 那样有明确的三次握手,Nginx 通过proxy_responses和proxy_timeout控制会话生命周期。一旦把proxy_responses设成 1,而某个后端因为协议协商需要返回 2 个或更多响应包,Nginx 会在这个会话上等待额外的响应,结果表现为后续新请求迟迟得不到转发,或者被集中转移到其他节点。排查方式是观察 stream 访问日志里$status列,如果出现大量超时,优先检查后端返回包的数量与proxy_responses是否匹配。多数 UDP 异常都可以先从这两个参数下手,而不是怀疑网络丢包。

4.2 健康检查机制:开源版只有被动检查

很多朋友在职级上用过云厂商的负载均衡,习惯了主动健康检查,于是下意识地认为 Nginx stream 也该有interval、fall、rise这些主动探测参数。实际上开源版 Nginx 的 stream upstream 默认只支持被动健康检查,也就是通过max_fails和fail_timeout来判定节点是否健康:

upstream db_read { server 192.168.1.11:3306 max_fails=3 fail_timeout=10s; server 192.168.1.12:3306 max_fails=3 fail_timeout=10s; }

max_fails=3表示在fail_timeout时间内失败 3 次就标记为不可用,标记后 10 秒内不会再转发新请求过去。主动健康检查需要 Nginx Plus 版本才自带,开源版如果确实需要主动探测,可以写一个外部脚本定时检测端口或执行简单协议请求,然后通过 reload 接口动态调整 upstream,不过这属于复杂玩法,建议先从被动的 max_fails 入手,绝大多数场景够用。

4.3 大 UDP 报文被截断:别忘检查 MTU 和缓冲区

DNS 某些场景下响应包会比较大,比如 DNSSEC 或带额外记录时,UDP 报文可能超过 512 字节。Nginx 转发 UDP 时默认的缓冲区大小可能不足以承载大报文,结果就是客户端收到被截断的响应,甚至直接丢失。排查办法是看是否有buffer size too small之类的日志,解决办法是调大缓冲区:

server { listen 53 udp; proxy_pass dns_upstreams; proxy_responses 1; proxy_buffer_size 16k; }

proxy_buffer_size 16k能覆盖绝大多数 DNS 大包需求。同时也要注意,如果外网链路的 MTU 小于 1500,大 UDP 包还可能在传输中分片或丢弃,需要结合网络侧配置一起排查。

4.4 配置了 stream 但端口就是不监听

有时候nginx -t通过了,ss -lntu却看不到监听端口,这大概率是编译时没带 stream 模块。之前我接手过一台老服务器,系统用自带工具包精简编译了 Nginx,只带 HTTP 模块。配置里写stream { ... }本来应该直接报 unknown directive,但被外层配置文件 include 进来时可能因为错误提示不显眼而被忽略,结果代理一直不生效。遇到这种情况,第一件事是nginx -V看模块列表,缺模块就重新编译或者换一个组件包完整的系统 Nginx。

4.5 长连接场景下 upstream 连接池耗尽

TCP 代理在连接数高、连接建立频繁的场景下,比如短时间内大量客户端发起 MySQL 连接,会反复占用后端的 fd 资源。优化方案是利用 upstream 的keepalive缓存空闲连接:

upstream db_read { server 192.168.1.11:3306; server 192.168.1.12:3306; keepalive 16; }

keepalive 16表示每条后端连接保持 16 个空闲长连接。这个数字不宜设得过大,因为空闲连接也会占用后端资源;对于数据库这类每个连接本身占用内存较高的场景,建议先从 8 或 16 开始压测,看后端活跃连接数和内存变化再调整。这里要注意,stream 里的 keepalive 只针对 TCP 场景,UDP 没有连接池的概念。

4.6 排查连通性:先本机后链路,再抓包

四层代理排障一定要按层来。第一步在代理节点本机用nc或telnet测试后端端口是否通:nc -vz 192.168.1.11 3306。第二步确认 Nginx 监听正常:ss -lntup | grep 3306。第三步再测转发链路,如果还是有问题,用 tcpdump 同时在代理节点和后端节点抓包,对比客户端请求是否到了 Nginx、Nginx 是否转发到后端、后端是否回了包、回包是否又被 Nginx 送回了客户端。UDP 场景注意抓包时加-w存储,避免终端刷屏影响分析。

5. 从代理节点到统一流量入口:扩展实践与体会

5.1 一端口多用途:HTTP 和 stream 共存时的规划

stream 模块和 http 模块可以在同一个 Nginx 进程里共存,这给架构规划提供了很大的灵活性。我在实际项目中经常把 80 和 443 留给 HTTP 服务,把 3306、3317、6379、53 这些端口规划给四层代理。重点是要提前约定端口用途,避免将来为了新增一个 TCP 服务,又要重新梳理整个 iptables 或安全组规则。Nginx 在这种场景下本质上充当了内网流量的统一网关,所有东西都汇聚到一个配置文件里,审计和管理都很方便。

5.2 日志和监控是四层代理的生命线

TCP 和 UDP 代理的日志比 HTTP 日志少了很多信息,但也更纯粹。我在 stream 块里单独开了access_log,每次转发失败都会记录状态码和上下游地址,排查问题时非常有价值。建议在日志里至少包含$remote_addr、$protocol、$status、$bytes_sent、$bytes_received、$session_time、$upstream_addr这几个变量。$bytes_received对 UDP 场景特别有用,能直观看到是不是大量请求在上游就被丢弃了。

最后分享一个小技巧

我在生产环境里跑 stream 模块跑了三年多,最大的体会是:四层代理看似简单,真正的难点其实在于如何优雅地管理连接生命周期和节点状态。最后分享一个小技巧,每次改动 stream 配置后,不要只跑nginx -t,还应该用curl -v或nc实际测试一次转发链路,确认监听端口、upstream 节点、超时参数都符合预期。特别要注意的是proxy_timeout的取值,TCP 和 UDP 的语义不同,TCP 里它是空闲连接超时,UDP 里它决定了会话的存活时间,千万别直接复制一套配置到处套用,先想清楚端口后面是数据库还是缓存、是长连接还是短连接,再去调参数。这样才能让 Nginx 的四层代理真正变成内网流量入口里那枚稳定可靠的螺丝钉。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 11:34:21

GaussDB连接工具选型:DataStudio安装配置与存储过程调试实战

简介:面向华为高斯GAUSS数据库使用者的Data Studio工具资源包,内含2022年3月30日版集成开发管理工具及配套用户手册,适合数据库管理员、开发人员及大数据学习者用于日常开发、运维与调优。整个压缩包共521个文件,大小148.3MB&…

作者头像 李华
网站建设 2026/10/18 11:34:05

AI抢不走你的饭碗?从恐惧到指挥的实战指南

我认识张伟,不是那个全国人民都认识的张伟,是我前同事。上个月他给我打电话,语气里全是惊恐:“兄弟,AI要抢我饭碗了,给我出出主意。”事情很老套。他们部门接到新要求,所有项目周报、数据分析、…

作者头像 李华
网站建设 2026/10/18 11:33:30

YOLOv5交通标志检测项目落地避坑指南

简介:本资源是一套完整的YOLOv5交通标志物检测实战项目,面向计算机、人工智能及相关专业本科生,专为课程设计、期末大作业及深度学习图像识别入门实践打造。项目源自97分高分结课作品,涵盖从数据预处理、模型训练到推理部署的全流…

作者头像 李华
网站建设 2026/10/18 11:32:22

Java Swing象棋游戏:基于极大极小值搜索算法的人机对弈实现

简介:一套基于 Java Swing 的象棋游戏源码,利用极大极小值搜索算法实现人机对弈,并配有完整的文档说明。适合正在准备课程设计、毕业设计的计算机专业学生,也适合需要 Java 项目实战练习的开发者,可有效解决 Java GUI …

作者头像 李华
网站建设 2026/10/18 11:31:39

YOLOv12即插即用模块化改进:注意力机制与跨尺度融合实战

1. 模块即插即用的整体设计思路1.1 为什么我坚持用模块替换而不是重构整个网络YOLOv12 的改进工作做到第三部分,先说一下我的总体判断:如果你不是做学术研究、不是想发论文,而是手上有一个实际项目要落地,最好的做法不是把网络结构…

作者头像 李华
网站建设 2026/10/18 11:30:58

STM32驱动DS1302实时时钟:从时序模拟到定时开关实战

1. 为什么还要折腾 DS1302 这颗老芯片前阵子帮朋友做一个带定时开关的控制器,主控用的是 STM32F103C8T6 最小系统板,需求很简单:每天固定时间点触发继电器,断电重启后时间不能丢。第一反应肯定是上 DS3231,温补晶振、I…

作者头像 李华