
1. 先说结论Nginx代理TCP/UDP靠的是stream模块不是http模块很多刚接触Nginx的同学第一反应是在http块里写proxy_pass结果配了半天TCP代理就是不生效。这个坑我当年也踩过折腾了一个下午最后发现方向就错了。Nginx的http模块处理的是HTTP协议也就是七层。但TCP和UDP是四层协议Nginx没有在http上下文里做四层代理的能力。真正负责这件事的是stream模块它在Nginx 1.9.0版本之后被引入专门用于处理TCP和UDP流量的代理和负载均衡。简单理解http模块负责“看懂”HTTP报文能做URL路由、Header改写、Cookie保持这些七层的事情stream模块只负责“搬运”字节流它不关心内容是什么就像快递员只负责把包裹从A送到B不拆箱检查里面装的是什么。所以这篇内容我会重点讲清楚三件事stream模块的配置结构与核心指令TCP代理和UDP代理各自的配置姿势配置完之后怎么验证、怎么排错适合谁看如果你在用Nginx做数据库读写分离代理、内网服务端口转发、或者需要把某个TCP端口暴露给外部访问这篇文章可以直接帮你把配置落地。如果你只是想给Web站点做反向代理那http模块才是你的菜stream模块可以先了解一下原理。2. stream模块的配置结构它和http块是平级的不是嵌套关系先说一个最容易搞混的点。很多人在http块里面写stream配置或者把stream相关的指令塞进server块结果Nginx直接报错。原因很简单stream块是顶层指令它和http块平级不是一个可以嵌套的子模块。来看下面这个最小可用配置# nginx.conf 核心结构示意 worker_processes 1; events { worker_connections 1024; } stream { upstream backend_tcp { server 192.168.1.10:3306; server 192.168.1.11:3306; } server { listen 3306; proxy_pass backend_tcp; } } http { # 正常的Web服务配置 server { listen 80; server_name example.com; # ... } }注意看stream块和http块是并列的都在events块之后、include指令的位置附近。你可以把它理解为Nginx的三大流量处理上下文events管连接事件http管七层协议stream管四层协议。2.1 为什么stream模块值得单独开一个上下文我在实际项目里见过不少这样的场景公司内部有个老系统只暴露了一个TCP端口比如某个设备管理软件但这个端口只能被内网访问。现在需要让外网也能访问最简单的方案就是找一台有公网IP的机器装个Nginx把公网端口映射到内网IP的端口上。这个需求用stream模块就是几行配置的事。另一个高频场景是数据库代理。比如你有两个MySQL实例想做个简单的读写分离或者只是想让应用层连一个固定端口后端切换时应用层不用改配置。这时候stream模块DNS解析或静态上游列表就是一个轻量级的解决方案比你单独部署一套HAProxy轻量得多尤其是在已经有Nginx的机器上。还有一个场景UDP代理。比如你在内网跑了一个DNS服务、一个游戏服务器、或者一个日志采集的UDP接收端想通过一台公网机器转发UDP流量stream模块同样可以搞定。2.2 stream配置里的核心指令listen、proxy_pass、proxy_timeoutstream模块的server块里最核心的指令是listen和proxy_pass这一点和http块的server配置很像。但有几个独有的参数值得注意listen 3306;监听哪个端口TCP默认就是TCP如果写udp参数就监听UDP端口proxy_pass 127.0.0.1:3307;要转发到哪个后端地址可以是IP端口也可以是upstream组名proxy_timeout 300s;代理连接的空闲超时时间超过这个时间没有数据传输就断开我在配置TCP代理时一般会显式设置几个超时参数防止某些长连接被异常断开也防止僵尸连接占着资源不放stream { server { listen 9000; proxy_pass 192.168.1.20:9000; proxy_connect_timeout 10s; proxy_timeout 30m; } }proxy_connect_timeout是Nginx和后端建立连接的超时时间proxy_timeout是连接建立后两次读写操作之间的空闲超时。这里有一个实际经验如果是代理SSH或者数据库连接空闲超时不要设太短否则一个查询时间长一点连接就被Nginx掐断了业务端会收到connection reset之类的报错。2.3 配置生效前的检查清单改完配置第一步永远是检查语法nginx -t这条命令会告诉你配置有没有语法错误。确认没问题之后再重载nginx -s reload注意nginx -t只是检查语法不会真正加载新配置。nginx -s reload是平滑重载正在处理的请求不会被中断这个机制在代理长连接时特别有用因为不会打断正在传输的数据。3. TCP代理配置从一个真实的内网端口映射需求说起前面说了一堆理论现在来点实际的。假设你有这样一台机器公网IP203.0.113.10举例用内网有一台数据库服务器192.168.1.50端口3306需求让外网某个网段的机器能通过203.0.113.10:13306访问这个数据库用Nginx stream模块实现配置如下stream { server { listen 13306; proxy_pass 192.168.1.50:3306; } }就这么简单是的就这么简单。但我在实际配置中还会加上几个选项让它更健壮stream { server { listen 13306 so_keepaliveon; proxy_pass 192.168.1.50:3306; proxy_connect_timeout 5s; proxy_timeout 24h; proxy_buffer_size 16k; } }拆解一下这几个参数so_keepaliveon让Nginx监听的socket开启TCP keepalive能更早发现死连接。proxy_connect_timeout 5s和后端建立连接5秒超时如果后端挂了客户端不会一直傻等。proxy_timeout 24h空闲超时给得很长适合数据库长连接场景。proxy_buffer_size 16k设置代理缓冲区大小对数据库这种可能返回较大数据包的场景适当调大可以减少频繁的系统调用。这个参数默认一般是4k或8k具体看平台。配置完成后重启Nginx在客户端机器上测试nc -vz 203.0.113.10 13306能通说明代理已经生效。3.1 多个TCP端口映射用include组织配置实际项目中你不太可能只映射一个端口。比如同时要代理数据库3306、Redis 6379、某个内部API的8080端口。如果全堆在nginx.conf里文件会很难维护。我的做法是把每个项目的stream配置独立成一个文件用include引进来# nginx.conf stream块内 stream { include /etc/nginx/stream.d/*.conf; }然后在/etc/nginx/stream.d/目录下建一个db_proxy.conf# db_proxy.conf upstream mysql_backend { server 192.168.1.50:3306 max_fails3 fail_timeout30s; server 192.168.1.51:3306 backup; } server { listen 13306; proxy_pass mysql_backend; proxy_connect_timeout 5s; proxy_timeout 24h; }这里用到了upstream块这是stream模块里的后端服务器组。backup参数的意思是192.168.1.51这台机器作为备用节点正常情况下流量都走192.168.1.50只有当主节点挂了才会切换过去。max_fails3 fail_timeout30s的意思是30秒内如果连续3次连接失败就认为这台后端挂了暂时把它摘除。这种配置方式的好处是每个项目的流量入口单独管理改动时互不影响排查问题也快——直接看对应项目的配置文件就行不用在一坨配置里翻找。3.2 端口范围映射如果你有一堆端口要转发还有一种情况后端服务占用了一串连续端口比如某个游戏服务器需要UDP 27015-27030或者某个监控系统需要TCP 10000-10050。Nginx stream模块的listen指令支持端口范围写法stream { server { listen 10000-10050; proxy_pass 192.168.1.80; } }注意proxy_pass这里只写了IP没写端口。当监听的是一个端口范围时Nginx会保留客户端访问的原始端口转发到后端相同的端口上。也就是说客户端访问10020端口Nginx就转发到192.168.1.80的10020端口。这个特性在做端口批量映射时能省很多事。但我提醒一句端口范围映射时后端机器的安全组和防火墙策略一定要核对清楚因为Nginx只是无脑转发端口范围全部暴露出去该做的访问控制还得做。3.3 多实例负载均衡stream配合加权轮询有时你需要的不只是端口映射而是负载均衡。比如你有三台Redis从节点想让客户端均匀地分摊请求。stream模块的upstream支持加权轮询stream { upstream redis_backend { server 192.168.1.21:6379 weight3; server 192.168.1.22:6379 weight2; server 192.168.1.23:6379 weight1; } server { listen 16379; proxy_pass redis_backend; } }这里的weight是权重Nginx按权重比例分配新连接。比例3:2:1意味着每6个新连接里大约3个分给21这台2个分给22这台1个分给23这台。不过要泼一盆冷水如果后端是MySQL或Redis这种带状态的服务单纯的轮询负载均衡可能会导致数据一致性问题。比如MySQL主从复制客户端写到了主库但下次连接被轮询到了从库数据可能还没同步到就会读到旧数据。所以stream层做负载均衡适合无状态协议或者业务层面自己处理了数据一致性。如果你要代理的是数据库写操作更稳妥的做法是直接指到主库单点或者配合读写分离方案不要指望Nginx stream层帮你智能路由。4. UDP代理配置异步无连接的转发逻辑TCP代理搞清楚了UDP代理就更容易理解但有几个细节和TCP完全不同需要单独说。UDP是无连接协议客户端发一个数据报给服务端服务端不一定有回应。你无法像TCP那样建立一个连接会话然后持续搬运数据。所以Nginx做UDP代理的配置思路是只要收到一个来自客户端的UDP数据包就直接原样转发给后端的某个服务器然后把后端的响应再传回客户端。4.1 最小UDP代理配置假设你在内网有一台DNS解析服务器192.168.1.100:53想通过公网Nginx机器对外提供DNS解析服务但不想直接暴露内网IP就用UDP代理stream { server { listen 53 udp; proxy_pass 192.168.1.100:53; proxy_timeout 10s; } }注意listen 53 udp;这里多了udp参数这是和TCP代理配置的唯一区别。其他指令基本通用。我在实测UDP代理时发现一个比较关键的点proxy_timeout在UDP场景的含义和TCP不同。TCP的proxy_timeout是连接空闲超时UDP中它是Nginx维护一个“会话”的持续时间。UDP没有连接概念Nginx只能靠源IP源端口识别“这个客户端的一个会话”在proxy_timeout时间内来自同一个源IP:端口的后续数据包都转发给同一个后端超过这个时间没有新包就清理掉会话记录。所以UDP场景这个值不宜设太长否则Nginx内存里的会话表会越积越多也不宜设太短否则像DNS这种需要频繁交互的场景会话刚建立就被清了导致后端收到来自不同端口的数据包。我一般建议设10~30秒具体看业务类型。如果是DNS查询10秒够用如果是日志采集类UDP流可能60秒更合适因为你希望同一个客户端持续上报时保持在同一个“会话”内。4.2 UDP代理里的proxy_responses理解响应次数UDP代理时Nginx默认认为后端会响应客户端的每个数据包。但实际情况不是这样——比如某些物联网设备通过UDP发送心跳包后端可能只回复一个ACK也可能根本不回复。这里就要用到proxy_responses指令stream { server { listen 9999 udp; proxy_pass 192.168.1.200:9999; proxy_responses 0; proxy_timeout 10s; } }proxy_responses 0表示后端不会返回任何响应数据包。这种情况下Nginx只负责把客户端的UDP包转发给后端不去等待响应。如果不设这个参数Nginx默认期望后端对每个数据包都有响应在某些场景下会出现莫名其妙的延迟因为它一直在等待永远不会来的响应包。反过来如果后端对每个请求总是恰好返回一个响应用默认值就行不用显式配置。4.3 UDP代理的典型场景日志采集、DNS转发、NTP同步我在项目里最常用的UDP代理场景有三个场景一日志采集。公司内网跑了一个syslog-ng接收所有网络设备的日志UDP 514端口。日志源服务器因为安全原因不能直连内网于是配置Nginx UDP代理把公网514端口的UDP包转发到内网syslog-ng服务器。因为日志流量大、包到达频率高我把proxy_timeout设为30s防止会话频繁重建导致丢包。场景二DNS转发。给某个隔离网络里的机器提供DNS解析服务通过网关上的Nginx把53端口UDP转发到内网DNS服务器。这里的proxy_responses设置为默认值1因为DNS查询确实需要响应。场景三NTP时间同步。有一些设备需要通过NTP同步时间NTP走的是UDP 123端口。代理配置如下stream { server { listen 123 udp; proxy_pass 192.168.1.50:123; proxy_responses 1; proxy_timeout 5s; } }NTP请求和响应是一一对应的所以proxy_responses1没问题。我把timeout设短一点因为NTP客户端不会持续发送5秒不活跃就清理会话避免会话表膨胀。5. 配置生效前的验证TCP和UDP要分开测别只靠telnet配好Nginx代理很多人第一反应是telnet测端口通不通。TCP端口这么测没问题但UDP用telnet是测不了的因为telnet基于TCP。这里我分享一下我的完整验证清单。5.1 TCP代理的验证方法检查TCP代理我常用nc命令来做连通性测试nc -vz 127.0.0.1 13306-v是显示详细信息-z是零I/O模式只测试端口是否可以连接。这个命令成功说明Nginx在正常监听13306端口。但这只能说明Nginx端口开着不能证明数据能正确到达后端。更严格的验证是直接走一次业务协议。比如代理的是MySQL用mysql客户端连一下mysql -h 203.0.113.10 -P 13306 -u testuser -p能成功建立连接并执行SQL说明代理链路完整。如果代理的是SSH直接ssh -p 13306 user203.0.113.10能登录就说明没问题。这是最直观的验证方式——用真实业务流量去测而不是只看端口通不通。5.2 UDP代理的验证方法UDP代理的验证麻烦一些因为没有“连接”的概念端口通不通并不代表代理正常工作。我在生产环境验证UDP代理时一般用两种方式。方式一用ncat模拟UDP客户端和服务端。在后端服务器上先起一个UDP监听ncat -u -l 192.168.1.200 9999然后在客户端机器上通过Nginx代理发一个UDP包echo hello | ncat -u 203.0.113.10 9999如果后端服务器的ncat窗口打印出了hello说明UDP包成功穿透了Nginx代理到达后端。方式二用tcpdump抓包确认转发链路。在Nginx机器上看有没有收到客户端的UDP包、有没有发出到后端的UDP包tcpdump -i eth0 udp port 9999如果看到入方向有来自客户端的包同时出方向有发往后端的包说明Nginx在正常转发。如果只有入方向的包没有出方向的包可能是proxy_pass指向的后端地址有问题或者后端的UDP端口没有服务在监听。我在实际排查中发现一个非常隐蔽的问题Nginx的UDP代理在同一条UDP socket上进行收发如果后端返回的源端口不是Nginx发送时使用的端口响应可能会被丢掉。这个问题通常出现在后端做了SNAT或端口转换的情况下。如果你配置了UDP代理但客户端一直在超时可以抓包看看返回的UDP数据包的源IP:端口是不是Nginx发出的目标IP:端口。6. 常见故障排错从症状到根因的完整排查链路这一部分我整理了自己在实际运维中遇到的几种典型故障现象以及我是怎么一步步定位到问题根因的。如果你照着配完之后不生效多半逃不出下面这几种情况。6.1 现象一TCP代理能连上但马上断开客户端能连接到Nginx的代理端口但connection马上被重置或者关闭。我的排查顺序是这样的第一步检查后端服务本身是否正常。直接在Nginx机器上用nc连后端的真实端口nc -vz 192.168.1.50 3306如果不通说明后端服务没起来或者防火墙挡了。这是最简单的可能但经常被忽略。第二步检查Nginx错误日志。日志位置一般在/var/log/nginx/error.log如果看到类似connect() failed、upstream timed out这样的关键字说明Nginx连不上后端。第三步如果在日志里看到accept() failed之类的错误可能是文件描述符用尽了。检查一下Nginx的worker_connections设置和后端服务器的TCP连接数限制。6.2 现象二UDP代理丢包严重或不通UDP丢包的首要怀疑对象是防火墙。因为UDP没有连接状态很多防火墙规则对UDP带来额外的处理逻辑尤其是CSPF状态检测防火墙可能需要在防火墙上显式放行UDP回程流量。我在实际项目中遇到过一个问题内网防火墙只放行了出方向的UDP包没有放行回程的响应包导致客户端能发出数据但永远收不到后端返回的DNS响应。排查方法就是在Nginx机器和后端机器上同时抓包看数据包走到了哪一步。第二个怀疑对象是MTU。UDP包通常不像TCP那样有分片和重组机制如果数据包超过链路MTU会被直接丢弃。比如默认MTU是1500字节但UDP包加上IP头后达到1504字节就可能出现超长包被丢弃的情况。排查时可以用ping -M do -s 1472测一下到后端的MTU是否支持这么大的包。当然真实环境中很多网络设备会处理UDP分片但大包UDP流量在公网上确实容易出现莫名其妙丢包的问题。6.3 现象三配置改了半天不生效我之前犯过一个经典错误在http块里面写了stream配置以为和server嵌套一样结果nginx -t直接报错。stream块必须放在http块外面和它平级。另一个常见问题是改了配置但忘了reload。很多人以为改了nginx.conf之后服务会自动生效其实不会。必须执行nginx -t nginx -s reload而且要注意生产环境里如果你用了多个配置文件include改了某一个子配置也需要reload整个Nginx才生效因为配置在启动时就已经被读取进内存了。6.4 现象四代理MySQL时频繁出现连接被重置如果后端是MySQL代理一段时间后客户端报connection reset先看Nginx的proxy_timeout是否太短。MySQL连接如果在空闲状态下超过proxy_timeoutNginx会主动断开这个连接但MySQL服务器端可能还没感知到等到客户端发送下一个查询时就会发现连接已经断了。解决办法是调大proxy_timeout比如设置成24小时或者干脆设成1d。但如果你有大量空闲连接注意Nginx会为每个TCP代理连接维护一定的内存空闲连接太多也会占用资源。我自己的习惯是代理管理类服务SSH、数据库控制台设长超时代理数据面流量API接口设短超时或利用keepalive机制保证快速回收。7. 进阶Nginx四层代理的性能优化参数如果你代理的流量很大比如每秒几千个新连接默认配置可能扛不住。下面是几个我实测有效的优化方向。7.1 文件描述符与worker连接数TCP和UDP代理都会消耗文件描述符。每个TCP连接至少消耗一个fdUDP代理中的每个会话也消耗一个fd。默认的worker_connections是1024对生产环境来说太少了。我一般这样设置events { worker_connections 65535; use epoll; }同时确保系统的fd限制足够ulimit -n 65535如果要永久生效修改/etc/security/limits.conf加入nginx soft nofile 65535 nginx hard nofile 65535这是Nginx进程能打开的最大文件数。如果不改这个就算worker_connections设成65535系统也只会给Nginx分默认的1024个fd效果一样受限。7.2 keepalive连接池降低与后端的连接建立开销Nginx作为代理时每个新客户端连接都要向后端发起一个新连接。如果后端是MySQL这类服务连接建立本身有开销TCP握手、认证高并发时这开销会被放大。解决方案是用keepalive连接池stream { upstream mysql_backend { server 192.168.1.50:3306; keepalive 16; } server { listen 13306; proxy_pass mysql_backend; proxy_socket_keepalive on; } }keepalive 16表示Nginx会在和后端之间维持最多16个空闲连接新客户端接入时优先复用这些空闲连接而不是每次都重新握手。这个参数大小要权衡太小了复用率低太大了空闲连接占资源。我一般按worker进程数和QPS来调初始设16跑几天看监控再微调。不过注意keepalive连接池复用只对MySQL 5.7以上、Redis等支持连接复用的协议有效。如果后端协议要求每个连接都是全新的比如FTP主动模式的PORT命令处理连接池反而不合适。7.3 多worker进程与accept_mutex默认情况下Nginx有多个worker进程同时监听同一个端口由操作系统决定唤醒哪个进程去accept新的连接。在高并发场景下可能存在“惊群”现象——所有worker都被唤醒但只有一个能成功accept其他worker白忙活。Nginx自带的accept_mutex机制可以缓解这个问题events { accept_mutex on; worker_connections 65535; }accept_mutex on表示同一时刻只有一个worker进程被唤醒去接受新连接其他worker继续处理已有连接。这在多核服务器上不一定总是更快但通常能减少不必要的上下文切换。如果你看到Nginx worker的CPU使用率不均匀可以试试调整这个参数。7.4 流量控制防止单条连接占满所有资源代理服务如果对单条连接没有限制一个客户端可能会持续大量传输影响其他客户端的体验。我见过有人直接在stream层做限速server { listen 9000; proxy_pass 192.168.1.30:9000; proxy_download_rate 10m; proxy_upload_rate 5m; }proxy_download_rate 10m限制从后端读取数据的速度最大10MB/sproxy_upload_rate 5m限制向客户端发送数据的速度最大5MB/s。注意这里的方向容易搞混刚开始用的时候我总记反。实际测试一下就明白了——downloaderate 是新连接的下载方向uploadrate 新连接的上传方向从客户端视角看下载是Nginx把数据发给客户端上传是客户端把数据发给Nginx。如果项目中需要更细粒度的访问控制比如只允许某个IP网段访问代理端口可以在stream的server块里加allow和denyserver { listen 13306; proxy_pass 192.168.1.50:3306; allow 10.0.0.0/8; allow 192.168.1.0/24; deny all; }顺序很重要allow在前deny all在后Nginx按顺序匹配匹配到就停止。这样做的好处是不用依赖防火墙规则直接在Nginx层做白名单对端口映射场景很实用。8. 结尾我踩过的一个现实坑最后再说一个我自己的实际经历算是给这篇配置教程收个尾。有一段时间我做了一套基于Nginx stream模块的UDP日志代理配置看着完全没问题但日志就是过不来。抓包显示Nginx收到了UDP包也发出了UDP包但后端机器上就是收不到。折腾了一晚上最后发现是Nginx机器的路由问题——服务器的默认路由指向了公网网关而内网后端机器所在网段没有添加静态路由导致Nginx发出的UDP包走了错误的出口。解决方式很简单加一条静态路由ip route add 192.168.1.0/24 via 10.0.0.1 dev eth1但这个问题告诉我一个道理四层代理排错时不要只盯着Nginx配置本身网络路由、防火墙、MTU这些链路因素往往才是真正的问题根源。以后再遇到“Nginx配置没问题但代理不工作”的情况我会先查两端机器的路由表和防火墙状态再去纠结配置语法。配置Nginx的TCP和UDP代理本身难度不大核心就是把stream模块的结构搞清楚TCP和UDP场景分别注意对应的超时和响应参数再配好验证手段。希望这篇内容能帮你少踩几个我踩过的坑。