ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

nginx stream 模块详解:四层代理与TCP/UDP负载均衡配置实战

nginx stream 模块详解:四层代理与TCP/UDP负载均衡配置实战 很多人用 nginx 做了好几年 HTTP 服务一提起 nginx 就想到反向代理、静态缓存、负载均衡。但实际业务里还有一类非常常见的需求代理 MySQL、Redis 这类数据库连接给 TCP、UDP 流量做四层负载均衡或者做一个不关心协议内容的内网网关。遇到这种场景很多人第一反应是再部署一套 HAProxy 或者 LVS。其实 nginx 自己就带了一个 stream 模块专门干这种四层转发和负载均衡的活。麻烦的是不少发行版自带的 nginx 二进制包默认根本没把这个模块编译进去你写了stream {}配置nginx -t直接给你报unknown directive stream。这篇文章就把这个事彻底讲清楚怎么确认模块是否存在、怎么把 stream 模块加进 nginx、加完怎么写四层代理配置以及在线上折腾过的人才会知道的那几个坑。1. 先搞清楚 stream 模块到底是干什么的1.1 七层代理和四层代理的本质区别HTTP 模块工作在第七层它能读懂 HTTP 协议看到 URL 可以按路径转发看到 Header 可以按域名分流看到 Cookie 可以做会话保持。而 stream 模块工作在第四层它转发的是原始的 TCP 或 UDP 数据流根本不关心你传的是 MySQL 协议、Redis 协议还是自定义加密数据它只负责把字节流从一端搬到另一端。打个比方HTTP 代理像快递公司会检查包裹面单、按地址分拣、还知道里面装的是什么类别的商品stream 代理更像一条水管谁从这头灌进去什么那头就原样流出来什么。所以 stream 模块对上层协议完全透明适合任何基于 TCP 或 UDP 的业务而且性能开销很小。1.2 典型场景清单什么时候你会用到它数据库代理客户端不直接连数据库而是连 nginx 的某个端口由 stream 转发到后端的 MySQL、Redis、MongoDB。数据库服务器不暴露公网地址只跟 nginx 互通安全性和可维护性都上来了。SSH 跳板与端口转发统一入口管理多台机器的 22 端口配合 allow/deny 做来源 IP 白名单控制。内网微服务的四层网关服务间通过固定端口调用不强依赖 HTTP用 stream 做 L4 负载均衡后端实例扩缩容对调用方无感。UDP 场景DNS 请求转发、SNTP 时间同步、游戏服务器和物联网设备的 UDP 长连接stream 都支持。TLS 终结与 SNI 分流用ssl_preread在不解密的情况下根据客户端 Hello 里的域名信息直接把 TLS 流量分流到不同后端这是 nginx 处理加密流量的杀手锏。1.3 为什么用 nginx 而不是再上 HAProxyHAProxy 当然是优秀的四层代理但很多团队本来就已经在用 nginx。如果你只需要偶尔加一两个 TCP 端口的转发再引入一套 HAProxy意味着要多维护一个二进制、多管理一套配置语法、多熟悉一套监控指标。nginx 的 worker 进程模型、热加载机制、access_log 体系在 stream 模块里完全复用运维心智负担小得多。stream 模块还自带 TCP 健康检查、proxy_timeout、allow/deny、基础限速足够覆盖大多数场景。只有当你要做特别复杂的四层策略和精细的负载均衡算法时才值得考虑专门的 L4 设备或软件。2. 先确认现状你手里的 nginx 到底有没有 stream2.1 用 nginx -V 看一眼编译参数在动手之前先检查别盲目编译。终端执行nginx -V注意这里必须是大写V小写v只显示版本号。输出里如果有一长串configure arguments就直接搜里面有没有--with-streamnginx -V 21 | tr \n | grep stream有结果说明二进制里已经编进了 stream 相关模块你直接写stream {}配置就能用。没结果也别急看下一节。2.2 看看发行版打包和动态模块的情况CentOS/RHEL/AlmaLinux系统仓库自带的 nginx 通常只编译了 HTTP 相关模块nginx -V基本看不到 stream。但你装的是 nginx 官方源的话可以单独安装动态模块包包名一般是nginx-mod-stream或nginx-module-stream装完在 nginx.conf 里加一行load_module就能启用。Debian/Ubuntu不同发行版打包策略不一样有的默认 nginx 带了 stream有的没带。不要想当然直接nginx -V确认。官方源码编译这是最稳妥的选择。方案 A 是直接下载官方源码在 configure 阶段加--with-stream重新编译方案 B 是如果你的现网 nginx 当初是用--with-streamdynamic编出来的且有--with-compat可以只编一个动态模块.so出来用load_module加载而不动主程序。2.3 一个容易忽略的点运行中的 nginx 和 nginx -V 是不是同一个排查问题第一步永远是确认路径。有些机器上/usr/sbin/nginx是发行版自带的/usr/local/nginx/sbin/nginx是后来源码装的两者完全不是同一个二进制。建议先看进程路径ps -ef | grep nginx找到 master 进程的完整路径再用这个路径去执行nginx -V这样查出来的才是真实生效的编译参数。我见过不止一次有朋友检查了/usr/sbin/nginx -V没看到 stream然后气急败坏地编译了半天最后发现线上跑的其实是另一个目录里的版本。3. 添加 stream 模块的两种主流方案3.1 方案一安装官方动态模块包如果你用的是 nginx 官方源并且只要最基础的 TCP/UDP 转发功能动态模块包是最省事的dnf install nginx-mod-stream装完之后需要确认模块路径然后编辑/etc/nginx/nginx.conf在events {}块之前加上load_module /usr/lib64/nginx/modules/ngx_stream_module.so;测试并重载nginx -t nginx -s reload动态模块方案有个前提编译 nginx 主程序的那个 configure 里带了--with-compat参数否则模块版本和 ABI 对不上加载时会报二进制不兼容。大多数官方发行版仓库打包时都带了 compat但你自己手工编出来的老 nginx 未必有遇到这种情况就老老实实走源码重编方案。3.2 方案二源码编译一劳永逸源码编译适合需要定制模块组合的场合比如你既要 stream又要ssl_preread还想顺带加realip甚至要裁剪掉用不到的 HTTP 模块。整个过程并不复杂。第一步安装编译工具和依赖库dnf install -y gcc make pcre-devel zlib-devel openssl-develDebian/Ubuntu 系对应的是apt-get install -y build-essential libpcre3-dev zlib1g-dev libssl-dev第二步下载源码并解压。以 1.26.x 系列为例官方地址是https://nginx.org/download/nginx-1.26.3.tar.gz你们实际部署时选自己需要的稳定版本即可。第三步configure。关键参数在这./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_v2_module \ --with-http_realip_module \ --with-http_gzip_static_module \ --with-pcre我解释几个和 stream 强相关的参数--with-stream核心开关开启四层流模块必须加。--with-stream_ssl_module让 stream 可以处理 TLS比如listen 443 ssl和ssl_certificate指令。--with-stream_realip_module配合 PROXY protocol 获取真实客户端 IPnginx 前面还有 L4 负载均衡时特别有用。--with-stream_ssl_preread_module启用 SNI 预读能力不解密就能按域名分流这个功能在多域名共用一台 nginx 的四层入口场景里是神器。第四步编译安装make -j$(nproc) make install默认安装到/usr/local/nginx。如果你的现网 nginx 是用发行版包管理的建议把--prefix指到已有 nginx 的安装根目录并仔细核对--conf-path、--pid-path等参数否则可能出现两套配置目录并存、sbin 路径分叉的情况后续维护会很痛苦。3.3 替换现网 nginx 而不丢失配置的三个细节线上机器往往已经跑着业务重编完之后怎么无缝替换是关键。我的习惯做法是先备份现网配置和二进制cp -a /usr/local/nginx/conf /usr/local/nginx/conf.bak再cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak。编译完成后先把新的配置文件测试通过/usr/local/nginx/sbin/nginx -t确保stream {}等新写法语法没问题。如果只是配置文件更新用kill -USR2 旧master进程PID启动新 master再把旧 worker 优雅退出这就是常说的平滑升级。平滑升级命令序列我给一个可以直接抄的版本假设以 PID 文件管理kill -USR2 $(cat /usr/local/nginx/logs/nginx.pid) sleep 2 kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid.oldbin)USR2信号让 nginx 用磁盘上的新二进制拉起一套新 master 和 worker新老进程并存完成平滑切换确认新版本稳定后QUIT让旧 master 优雅退出。回滚也很简单把.bak二进制拷回去再发一次 USR2 即可。另外要注意如果前端网络环境是纯内网、不能访问外网下载源码那就得提前在能联网的机器上把所有依赖包下载好内网机器做本地 yum 源或者直接拷 rpm 离线安装。aarch64 架构尤其要留意 pcre、openssl 这些依赖是否匹配对应架构别拿 x86_64 的包硬装。4. 配置实战从最简单的 TCP 代理到多场景组合4.1 最小可用的 MySQL TCP 代理编译装好后在 nginx.conf 顶层加一个stream块。注意它和http块平级不能写进http里面。示例stream { upstream mysql_backend { server 192.168.10.11:3306 max_fails3 fail_timeout30s; server 192.168.10.12:3306 max_fails3 fail_timeout30s; } server { listen 3306; proxy_pass mysql_backend; proxy_timeout 300s; proxy_connect_timeout 10s; } }这一段的逻辑和 HTTP 反向代理很像upstream定义后端池server定义入口监听端口proxy_pass指定转发目标。proxy_timeout控制后端连接的空闲超时数据库连接池通常需要长时间保持我习惯给到 300 秒甚至更长避免频繁断连。设置后重启或 reload用mysql -h nginx服务器IP -P 3306一测就知道通不通了。4.2 UDP 场景DNS 负载均衡UDP 代理是 stream 模块另一大强项。这里有个非常容易踩坑的点TCP 代理只需要proxy_pass就够了但 UDP 代理必须告诉 nginx 每个请求期望收到几个响应包这就是proxy_responses指令存在的意义。stream { upstream dns_backend { server 192.168.10.101:53; server 192.168.10.102:53; } server { listen 53 udp; proxy_pass dns_backend; proxy_responses 1; proxy_timeout 5s; } }listen 53 udp中的udp关键字是必须的不加默认按 TCP 监听。DNS 查询一个请求通常对应一个响应所以proxy_responses 1是常规值。这个参数如果不配nginx 不知道何时算一次完整交互会话回收和健康检查逻辑都会变得不可控。4.3 访问控制与日志四层代理也能写审计日志很多人以为 stream 模块很简陋其实基础的访问控制、日志、限流它都有。四层代理做来源 IP 白名单非常实用stream { log_format stream_basic $remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received $session_time; access_log /var/log/nginx/stream_access.log stream_basic; server { listen 3306; allow 10.0.0.0/8; allow 172.16.0.0/12; deny all; proxy_pass 192.168.10.11:3306; } }allow和deny的匹配顺序是从上往下先放行的规则要写前面。加了这条之后不在白名单里的客户端连握手阶段就会被直接断开后端数据库只会看到来自 nginx 的连接攻击面小很多。日志里的$protocol会显示 TCP 或 UDP$bytes_sent和$bytes_received对排查流量异常很有参考价值。4.4 进阶用法TLS 终结与基于 SNI 的分流stream 模块除了做纯透传还能直接在四层做 TLS 卸载。比如后端是明文 HTTP你想在入口统一终结 TLS可以这样写stream { server { listen 443 ssl; ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key; proxy_pass 192.168.10.31:8080; } }更高级的玩法是基于 SNI 分流。假设一个 nginx 四层入口要同时转发数据库和 Redis你不想让客户端手动指定端口而是希望通过连的是哪个域名来自动分流ssl_preread就派上用场了stream { map $ssl_preread_server_name $backend { default mysql_pool; redis.internal.example.com redis_pool; mysql.internal.example.com mysql_pool; } upstream mysql_pool { server 192.168.10.11:3306; server 192.168.10.12:3306; } upstream redis_pool { server 192.168.10.21:6379; server 192.168.10.22:6379; } server { listen 443; ssl_preread on; proxy_pass $backend; } }ssl_preread on让 nginx 只读取 TLS ClientHello 里的 SNI 字段不解密、不握手然后根据映射结果把流量转给对应后端。因为不解密性能开销几乎可以忽略这种方案在加密流量统一入口场景下非常实用。4.5 性能调优别让四层代理变成瓶颈四层转发本身开销很小但配置不当也会出问题。几个我实测过有用的参数worker_processes auto;配合worker_connections适当调大比如 4096 甚至更高。四层连接通常比 HTTP 请求保持时间长连接数上限容易先触顶。upstream 里加keepalive 16;可以复用后端连接减少频繁建立 TCP 连接的开销。注意这是 stream 模块自己的 keepalive和 http 模块的 keepalive 用法类似但属于不同上下文。如果后端连接建立慢把proxy_connect_timeout减小让失败请求快速失败不要全部堆在 nginx 上傻等。大量并发短连接场景下适当减少access_log的写入频率或者按需关闭日志 I/O 在高并发四层转发时往往比想象中更影响性能。5. 常见问题排查实录与避坑指南5.1 高频故障速查表现象常见原因解决办法nginx -t报unknown directive stream模块没编译或没加载nginx -V确认缺模块就按第 3 节方案补load_module报二进制不兼容编译主程序时没加--with-compat或模块版本与主程序版本不一致版本完全对齐后重编或直接整个 nginx 重编reload 后端口冲突startup 失败stream server监听的端口已被其他进程占用ss -lntup查看端口占用改监听端口或释放冲突端口UDP 代理间歇性失败漏了proxy_responses或参数不准确按协议响应包数量配置proxy_responses不确定就抓包确认数据库连接空闲后频繁断开proxy_timeout太短空闲连接被回收调大到 300s 或更长按业务保持周期设置把 HTTP 指令写进 stream 块报错上下文用混了记住http {}和stream {}是平级且隔离的两个上下文新配置测试通过但线上不生效reload 没执行或执行的不是同一个 nginx 二进制nginx -s reload用ps -ef确认实际路径平滑升级后老 worker 一直不退旧连接长时间不断开等自然超时或手动kill -QUIT旧 worker5.2 几个我踩过并补过的坑第一个坑是编译参数和版本对齐。我之前在一台 AlmaLinux 9 上折腾动态模块主程序是系统源里的 nginx动态模块却从官方源下载结果版本差了一个小版本load_module直接报 module version mismatch。后来学乖了要么全走官方源要么全走源码编译混用只会给自己挖坑。第二个坑是stream配置块的位置。第一次写的时候习惯性地把它塞进了http {}里面nginx -t报错还不理解后来才想起来 stream 和 http 是两个顶层上下文就像毛坯房里的两间独立房间想反过来在 stream 里写proxy_cache这种 http 指令也一样会报错。第三个坑是 UDP 代理的proxy_responses。我记得第一次配 DNS 转发时没写这个参数客户端偶尔能解析、偶尔超时排查了很久最后看官方文档才明白 UDP 是无连接的nginx 需要知道什么时候算一次完整的请求-响应交互。5.3 顺带回答几个面试高频问题nginx 的 stream 和 http 模块有什么区别stream 工作在第四层转发原始 TCP/UDP 流量不解析应用层协议http 模块工作在第七层能基于域名、URI、Header 做精细化路由。http 模块能做 TCP 转发吗不能。listen加proxy_pass在 http 上下文里只处理 HTTP 协议要对任意 TCP 流量透传必须用 stream。stream 能实现 TLS 终结吗能编译时加--with-stream_ssl_module在server块用listen 443 ssl配合ssl_certificate。不了解后端是什么协议能用 nginx 做转发吗能纯四层透明转发不关心协议只要 TCP/UDP 端口对上就行。最后分享一点个人体会我维护过不少 nginx 四层入口最大的感受是stream 模块一旦加进二进制nginx 的定位就不只是 Web 服务器了它会变成整个网络架构里非常关键的流量中枢。数据库代理、DNS 转发、SSH 跳板、TLS 分流全部可以在同一套 nginx 配置体系里管理。所以在编译阶段多花十分钟想清楚自己要哪些 stream 子模块真的比以后反复重编省时间。我个人的习惯是新装 nginx 时只要条件允许就把--with-stream、--with-stream_ssl_module、--with-stream_ssl_preread_module全部编进去哪怕当前用不到也不至于以后业务提需求时手忙脚乱。配置变更之前永远先nginx -t升级之前永远留一份旧二进制做回滚这两条习惯我到现在都严格遵守。
返回列表