Nginx stream模块编译安装指南:从四层代理到TCP/UDP负载均衡

发布时间:2026/10/14 3:03:08
Nginx stream模块编译安装指南:从四层代理到TCP/UDP负载均衡 1. 先搞清楚stream模块到底管什么1.1 四层代理与七层代理的区别做了好几年nginx相关的工作被问到最多的问题之一就是“stream模块怎么加”。尤其是那些从系统源直接装好nginx、然后照着网上的配置文档写stream配置块的人——一执行nginx -t就报unknown directive stream当场卡住。这个场景我见了太多次基本可以断定不是配置写错而是你手里的nginx二进制里压根没有stream模块的代码。很多人分不清nginx的http {}和stream {}到底有什么区别。简单说http {}是七层应用层代理它看得懂HTTP协议能根据域名、URL路径、请求头去路由而stream {}是四层传输层代理它只看IP和端口做的事情就是把TCP或UDP数据包原样搬过去根本不关心里面跑的是什么协议。打个比方七层代理像酒店前台要问清楚你找谁、去哪个房间四层代理像大楼门卫只看你进的是哪个门门里面做什么它不管。所以stream模块能代理的范围非常广MySQL的3306、Redis的6379、MongoDB的27017、ES的9200还有各种私有TCP协议、UDP的DNS和syslog都能走它。你在一个nginx上开四个端口分别转发到不同的后端服务器这在很多公司里都是拿nginx当轻量级四层入口用的常规操作。1.2 没有stream模块的nginx有多受限如果你用的nginx来自发行版默认包或者编译的时候没加--with-stream那你的nginx就只具备HTTP/HTTPS反向代理能力。你想写类似这样一段配置stream { server { listen 3306; proxy_pass 192.168.1.10:3306; } }执行nginx -t的时候会直接给你一句nginx: [emerg] unknown directive stream in /etc/nginx/nginx.conf。而且请注意stream不是一个可以在http {}里面写的指令它是一个和http平级的顶层配置块写在主配置的顶层跟events {}、http {}并列。这跟upstream写在http里面是两个完全不同的层级很多人一开始就把位置搞错了。还有一种常见误判是“我已经用nginx反代了网页为什么MySQL连不上”原因很简单HTTP反向代理只处理HTTP流量stream模块没编译进去的话3306这个端口你写在http的server里是没法正确转发的。所以弄清楚自己机器上nginx的“底细”是动手前的第一件事。2. 动手前的准备模块选型和环境确认2.1 静态编译还是动态模块怎么选给nginx加stream模块有两条路一是直接编译进nginx二进制静态编译二是把stream做成动态模块在配置文件里用load_module加载。先说结论如果你是自己从源码编译图省心、图稳定直接静态编进去就好如果你用的是官方仓库里已经分好包的nginx那优先找对应的动态模块包装上。为什么会有动态模块这条线因为nginx官方从1.9.11开始支持动态加载模块把一些不常用的功能拆成.so文件按需装。官方nginx源里就有nginx-mod-stream这个包装上以后系统里会多一个ngx_stream_module.so你在nginx.conf最顶上写一行load_module /usr/lib64/nginx/modules/ngx_stream_module.so;就能用stream了。这么做的好处是nginx主程序不用动模块独立升级坏处是动态模块必须和nginx主程序的版本、编译参数严格一致否则启动就报module version mismatch。自己手动编译的时候除非你有明确的模块热插拔需求否则没必要折腾动态静态编译最直接一次到位。2.2 先看看现有nginx是什么底细不要急着下载源码第一步永远是体检当前环境。执行nginx -V 21注意是大写的V输出里会带上完整的configure参数。正常你会看到类似这样的内容nginx version: nginx/1.24.0 built by gcc 8.5.0 20210514 (Red Hat 8.5.0-18) configure arguments: --prefix/usr/share/nginx --modules-path/usr/lib64/nginx/modules --with-http_ssl_module ...如果configure arguments里已经有--with-stream那你什么都不用做直接跳过本文的编译部分去写配置就行。如果没有再看两条路径如果是发行版自带的nginx先找找有没有现成的stream模块包。比如RHEL/CentOS系统看/usr/lib64/nginx/modules/目录下有没有ngx_stream_module.soDebian系看/usr/share/nginx/modules/。有的话直接加载不折腾编译。如果找不到现成模块文件或者你的nginx是别人编译的、来路不明那就老老实实走源码编译顺便把nginx升级到你能掌控的版本。这里插一句经验查看nginx -V输出的时候把configure arguments整段复制出来存成文本后面编译新版本要用这也是我踩过坑之后养成的习惯。很多人编完新nginx结果把原来用的--with-http_stub_status_module之类模块丢了一脸懵就是因为没留原始参数。2.3 依赖包和源码准备编译nginx需要三样基础依赖PCRE或PCRE2、zlib、OpenSSL。PCRE是正则库nginx的rewrite模块要依赖它zlib是压缩库gzip模块要用OpenSSL是HTTPS和TLS相关模块的基础。即使你只用stream、不用HTTPconfigure脚本默认也会尝试带上HTTP相关模块所以这三个装齐最省事。CentOS/RHEL系的安装命令yum install -y gcc make pcre-devel zlib-devel openssl-develDebian/Ubuntu系apt install -y build-essential libpcre3-dev zlib1g-dev libssl-dev有些文章会说“不加rewrite模块就不需要pcre”理论上是可以用--without-http_rewrite_module绕过但为了一个入库依赖去砍掉标准功能属于没事找事除非你确定要编译一个纯stream的精简nginx否则不建议这么干。源码去官网下载区拿。版本选择上我的建议是生产环境用stable版本别追mainlinemainline新功能多但迭代快没必要在生产上尝鲜。下载完先校验一下SHA256再解压wget https://nginx.org/download/nginx-1.26.2.tar.gz sha256sum nginx-1.26.2.tar.gz tar zxf nginx-1.26.2.tar.gz cd nginx-1.26.23. 实操一步步把stream模块编译进去3.1 configure参数怎么填才不亏nginx的configure脚本是编译前最重要的一步它决定你要编哪些模块、装到哪个目录。一个既覆盖stream又保住常用HTTP功能的参数组合大概是这样的./configure \ --prefix/usr/local/nginx \ --with-stream \ --with-stream_ssl_module \ --with-stream_realip_module \ --with-stream_ssl_preread_module \ --with-http_ssl_module \ --with-http_stub_status_module \ --with-http_gzip_static_module逐个解释一下--prefix指定安装根目录默认就是/usr/local/nginx不写也能装但写上让你对路径有数。--with-stream是核心开关没有它一切白搭。--with-stream_ssl_module让你能在stream里做TLS终结和TLS透传--with-stream_realip_module用于在多层代理场景下拿真实客户端IP--with-stream_ssl_preread_module这个我重点说——它支持在TLS握手阶段就根据SNI域名信息做路由不用解密流量就把不同域名的请求分流到不同后端这个功能在混部场景下极其好用后面配置环节会讲。如果你只是想升级现有nginx、不动原功能正确姿势是先把旧版本的configure参数取出来在原有基础上追加--with-stream系列OLD_ARGS$(nginx -V 21 | sed s/.*configure arguments: //) ./configure $OLD_ARGS --with-stream --with-stream_ssl_module这里有个很现实的坑旧版本编译参数里的--with-opensslxxx这类路径参数如果指向的是源码目录升级时一定要确认目录还在、版本还兼容否则configure容易报错。我建议如果之前是用的系统OpenSSL就把这个参数删掉直接用系统的。3.2 编译安装的完整过程configure没有报错之后就进入编译环节。多核机器一定要加-j参数不然编译会让你等到怀疑人生make -j$(nproc)第一次make的时候日志会刷很多cc编译命令最后生成objs/nginx这个二进制文件。这一步如果中途挂了多数是依赖没装齐看报错里缺什么库再补装就行不用慌。编译完成后如果你是全新安装make install装完之后nginx会被放到/usr/local/nginx/sbin/nginx配置文件是/usr/local/nginx/conf/nginx.conf。如果你是给已有nginx“补模块”千万别直接对正在跑的二进制make install覆盖。正确做法是先把旧二进制备份cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak cp objs/nginx /usr/local/nginx/sbin/nginx然后执行平滑升级见下小节。直接覆盖正在运行的nginx二进制文件理论上信号机制可以继续跑但一旦进程重启就是新二进制生效中途出问题没有回滚路径属于纯赌命。3.3 老版本nginx如何平滑升级nginx的平滑升级是它比较优雅的设计之一核心机制是“新老master并存老worker渐退”。手动操作步骤是kill -USR2 $(cat /usr/local/nginx/logs/nginx.pid)这个信号让老master启动一个新的master进程新master用的是磁盘上已经替换过的新二进制会重新读取配置文件、重新绑定端口。绑定成功后kill -WINCH $(cat /usr/local/nginx/logs/nginx.pid.oldbin)这个信号让老master把自己的worker进程优雅退掉只留老master做“后手”。这时候你观察一下日志确认新进程正常服务再给老master发QUIT信号彻底收尾kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid.oldbin)如果升级后发现不对劲回滚办法是给老master发HUP信号它会重新拉起老版本的worker进程然后你再把旧二进制考回去。这套流程多演练几次比你在凌晨两点靠猜强得多。其实更省事的方式是直接用make upgrade。nginx生成的Makefile里本来就有upgrade目标它会自动执行“测试配置→发USR2→sleep→发WINCH”这一套make upgrade前提是你已经把新二进制放到了configure指定的路径下。我两种方式都用过生产环境建议手动一步步来看得清楚每一步的反馈不推荐自动化一把梭。3.4 验证模块是否真正生效编译安装完第一件事不是写复杂配置而是验证模块真的进去了。用两条命令/usr/local/nginx/sbin/nginx -V 21 | grep stream这条如果输出里出现--with-stream说明模块已编进二进制。再验证配置能正常解析写一个最小可用的stream配置stream { server { listen 12345; proxy_pass 127.0.0.1:22; } }然后执行/usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginx用nc或者直接ssh到本机的12345端口测试能不能打通到22端口。能通模块就真正生效了。这一步别看简单很多人在“编译成功”和“配置生效”之间栽跟头make install成功了但跑的还是PATH里那个旧nginx所以测试的时候一定用绝对路径别被PATH里的同名二进制误导。4. stream模块的实战配置速写4.1 最基础的TCP端口转发stream模块最简单的场景就是纯端口转发。比如你要把外网机器的3306端口转发到内网MySQL服务器stream { server { listen 3306; proxy_pass 192.168.1.10:3306; proxy_timeout 300s; } }这段配置的含义nginx监听3306端口进来的TCP连接全部原样转发到192.168.1.10:3306。proxy_timeout 300s是“两次读写操作之间的超时时间”默认是10分钟如果你代理的是常连接类型比如MySQL连接池建议显式调大不然后端空闲一会儿就被切断客户端那边就会看到莫名其妙的连接断开错误。这里要注意一点stream里的proxy_pass后面不能跟URL路径它只接受“IP:端口”或者“上游组名”不能像HTTP那样写proxy_pass http://xxx/nginx。另外listen指令是可以多重定义的一个stream块里可以放很多个server每个监听不同的端口互不干扰。我还见过有人把stream的server和http的server听同一个端口这不行端口在两个上下文里是全局共享的冲突了会直接报bind() to 0.0.0.0:xxxx failed。4.2 UDP转发与典型场景stream模块不止管TCPUDP也能代理这个能力被很多人忽略了。一个非常典型的场景是内网DNS转发公司内部有一组DNS服务器你想让临时客户端的DNS查询统一走一个入口。stream { upstream dns_servers { server 192.168.1.20:53; server 192.168.1.21:53; } server { listen 53 udp; proxy_pass dns_servers; } }关键就在listen 53 udp;这个udp参数没有它nginx默认是监听TCP的UDP包直接丢弃。syslog也是一样很多网络设备把日志通过UDP 514发出来你想统一收口再转发到日志服务器直接在stream里写一个listen 514 udp;的server即可。UDP代理有个容易踩的点UDP是无连接的nginx为了保持会话一致性默认会基于客户端地址加端口做会话关联确保同一个客户端的后续数据包尽量打到同一个后端。所以你自己做负载均衡时如果后端是无状态UDP服务问题不大如果后端有会话状态尽量别在UDP上乱配轮询算法hash $remote_addr consistent;这类一致性哈希更稳妥。4.3 四层负载均衡配置stream模块提供四层负载均衡能力这是它最有价值的使用方式之一。比如公司里有两台Redis从节点你想让应用统一走一个入口stream { upstream redis_backend { least_conn; server 192.168.1.30:6379 max_fails3 fail_timeout30s weight5; server 192.168.1.31:6379 max_fails3 fail_timeout30s weight1; } server { listen 6379; proxy_pass redis_backend; } }least_conn是连接数最少优先适合长连接场景轮询是默认策略不用写hash $remote_addr consistent;适合要求客户端IP固定的场景。max_fails3和fail_timeout30s的意思是30秒内失败3次就把这个后端标记为不可用等30秒后再试探恢复。这是官方文档里说的被动健康检查全靠连接失败来判定没有主动探活能力。如果你需要主动健康检查比如定期去ping一下后端端口判断是否存活那就不是社区版nginx能干的活了那是nginx Plus的商业功能。我实际做过很多替代方案要么用脚本配合API每小时检查后端要么干脆在stream前面放一层LVS之类的东西做探活。社区版nginx的被动检查对绝大多数场景够用了别在早期就把需求设计得太复杂。4.4 SSL透传与TLS终结按需选stream模块在SSL场景下有两种玩法很多人分不清。第一种叫“SSL透传”也就是nginx不做任何解密把加密数据原样转给后端由后端自己终结TLS。比如你有一组HTTPS后端想把443端口负载均衡一下stream { upstream https_backend { server 192.168.1.40:443; server 192.168.1.41:443; } server { listen 443; proxy_pass https_backend; } }这种模式的好处是nginx完全不碰证书私钥留在后端安全性边界清晰可用于协议不能解密、或后端需要看到客户端真实TLS信息的场景。第二种叫“TLS终结”需要编译时带了--with-stream_ssl_module。nginx这边持有证书把TLS解开后再以明文TCP转发给后端stream { server { listen 9443 ssl; ssl_certificate /etc/nginx/certs/example.crt; ssl_certificate_key /etc/nginx/certs/example.key; proxy_pass 127.0.0.1:8080; } }第三种是前面提到过的SNI分流用ssl_preread在读TLS握手时只看SNI就能路由特别适合一个端口上要同时承载多个域名、又要保持四层代理效率的场景stream { map $ssl_preread_server_name $backend { default web_backend; mysql.internal db_backend; } upstream web_backend { server 192.168.1.50:443; } upstream db_backend { server 192.168.1.51:3306; } server { listen 443; ssl_preread on; proxy_pass $backend; } }这段的惊喜点是你可以在同一个443端口上根据TLS握手阶段携带的域名把流量分流给Web服务或者MySQL服务——流量全程不解密效率极高。不过要注意proxy_pass用了变量之后如果变量里是域名而不是IPnginx需要配置resolver来做动态解析。我最开始就是在没配resolver的情况下直接用域名变量启动时报no resolver defined to resolve检查了半天才发现问题这里给后来人提前打个预防针。5. 常见问题与排查实录5.1 编译环节的坑逐个点名我整理这几年遇到过的编译报错做成一个速查表方便你对照报错特征原因解决办法configure: error: the HTTP rewrite module requires the PCRE library缺少PCRE开发库安装pcre-devel或pcre2-devel后重新configureconfigure: error: SSL modules require the OpenSSL library缺少OpenSSL开发库安装openssl-devel或指定--with-openssl源码路径make: command not found没装编译工具链安装gcc make编译过程中Killed进程消失内存不足被OOM降低make -j并行数比如make -j1或加大内存make install后nginx还是旧版本覆盖的不是PATH里的那个二进制或没重启用绝对路径验证执行平滑升级还有一个容易忽略的configure脚本参数写错字符比如把--with-stream_ssl_module写成--with-http_ssl_module不会报错但stream的SSL功能就是没有。验证的方法就是nginx -V看清楚生成的配置参数一个都不要漏。5.2 报unknown directive stream先查二进制nginx: [emerg] unknown directive stream是搜索量最高的stream问题没有之一。遇到这个报错先别急着怀疑配置语法99%的情况是模块没进二进制。排查动作三步走第一步nginx -V 21看configure arguments里有没有--with-stream第二步如果没有导入官方仓库的nginx-mod-stream动态模块包然后在nginx.conf最顶部加load_module指令位置必须在events和http之前第三步再nginx -t验证。注意动态模块文件路径一定写到.so文件本身不是目录很多人栽在这里写成了load_module /usr/lib64/nginx/modules;少了文件名启动直接报错。另外提一句如果你用的是旧nginx二进制编译完新nginx之后没有替换那你配置写得再漂亮也没用因为运行的进程根本没有新代码。升级完记得nginx -V确认版本号和configure参数都对再谈配置。5.3 stream disconnected before completion这类报错如何定位在stream相关搜索里“stream disconnected before completion”出现的频率很高。我先说个容易被误解的点这句报错一般不是nginx吐出来的而是各种客户端SDK、数据库工具、分布式任务在执行过程中发现自己维护的长连接流“在响应完成之前被断开了”然后报出这句话。比如gRPC客户端、某些同步工具、远程任务调度工具都会出现类似文案。那它跟nginx有什么关系当你确实是通过nginx的stream模块代理这些流量时连接断开的原因往往要从三个方向找第一nginx侧的proxy_timeout。stream模块的proxy_timeout默认10分钟它定义的是“两次连续读或写操作之间的超时”。如果你的业务场景是长连接但长时间没有数据交互比如空闲的MySQL连接池连接到了时间就会被nginx掐掉。解决办法是显式调大比如proxy_timeout 3600s但别无脑设成无限大否则半死不活的连接会一直占着资源。第二后端主动断开。MySQL的wait_timeout、Redis的空闲超时、gRPC服务端的keepalive策略都可能导致后端先关连接。这种时候nginx只是“如实转达”了断连锅在后端。判断方法打开stream的访问日志看断开时间是落在一次完整交互的哪个阶段或者直接看后端日志有没有对应的Aborted connection之类的记录。第三四层入口的健康检查误伤。max_fails搭配fail_timeout做被动健康检查时如果后端偶发一次握手失败连接可能被标记为不可用随后新连接就被转到其他后端已有的连接则可能因为后端摘除而中断。排查时可以临时把max_fails调大观察报错是否消失。我自己的排查习惯是优先开access log和error log再利用tcpdump抓包看是哪一侧先发的FIN或RST。谁先发FIN谁主动关的一目了然省去很多互相甩锅的时间。5.4 用好stream的日志和调试手段stream模块的日志体系是可以独立配置的。给它单独开一份访问日志和错误日志能让排查效率翻倍stream { log_format streamlog $remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received $session_time; access_log /var/log/nginx/stream-access.log streamlog; error_log /var/log/nginx/stream-error.log debug; }$protocol能显示这条连接是TCP还是UDP$status是会话状态码$session_time是会话时长这几个变量对定位断连问题非常有用。error_log开到debug级别会比较吵但关键时刻能看到握手、代理的每一步排完问题记得调回notice。再看当前nginx持有哪些四层连接直接用ssss -tnp | grep nginx可以看到每个ESTABLISHED连接的本地端口和远端地址配合访问日志能还原出一条连接完整的生命周期。如果怀疑网络层面有问题tcpdump -ni eth0 port 3306抓一下看有没有大量重传这些基础操作往往比翻文档快得多。最后再多说一句关于调试的习惯改完stream配置先nginx -t再nginx -s reload。stream配置和http配置在同一个主配置文件里reload是热加载的不会断已有连接这个机制我用了很多年基本没有因为这个出过岔子。真正需要小心的反而是你同时改了nginx二进制和配置那种情况建议走完整平滑升级流程别图省事直接reload。6. 我这几年总结的几个实操心得编译stream模块这件事本身并不难难的是把这件事纳入到nginx的整个生命周期管理里去。我现在的习惯是不管初始需求多简单编译nginx时都默认带上--with-stream --with-stream_ssl_module --with-stream_ssl_preread_module因为编译成本几乎为零而后端需要代理TCP/UDP的场景出现频率远比想象中高。等真的需要MySQL端口转发、Redis集群入口、UDP接收端的时候才发现nginx缺模块再走一遍编译升级流程那才是真的折腾。第二个习惯是每次configure之后把完整的参数保存到源码目录下的一个文件里跟编译产物放一起./configure ... --with-stream ... echo configure ... --with-stream ... build_args.txt下次升级的时候直接把这个文件翻出来改不用靠nginx -V去反推。别小看这个细节nginx的configure参数动辄十几个凭记忆写漏一个模块编译完了才发现纯浪费时间。第三个心得是关于“能用包管理器就不自己编译”。如果机器能用官方nginx源优先装官方源里的nginx和nginx-mod-stream包让包管理器管版本和依赖只有在内网离线环境、或者需要编译自定义模块、或者对nginx版本有特殊要求的时候才走源码编译。自己编译意味着从这一刻起安全补丁、版本升级都得自己盯着这个隐性成本很多人一开始是没算进去的。stream模块加入之后nginx就不再只是一个HTTP服务器了它同时是四层流量入口、端口转发工具、TCP/UDP负载均衡器。把编译、升级、排错这条链路走顺了后面很多网络层需求都能在nginx这里一站式解决这也是我这些年一直愿意在nginx上持续投入的原因。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询