绝大多数 Nginx 教程都聚焦在 HTTP 层,但当业务涉及 MySQL、Redis、gRPC、消息队列、数据库复制或自研 TCP/UDP 协议时,需要的不是七层代理而是四层代理。Nginx 的 stream 模块正是为此而生:它在传输层工作,不解析 HTTP 语义,直接把 TCP 或 UDP 流量转发到后端。它与 http 块平级,共享同一套事件驱动内核,因此可以在一台 Nginx 上同时承载七层与四层代理。本文完整讲解 stream 模块的配置模型、负载均衡、健康检查、SSL 终结与排错方法。
一句话总结:
stream模块在传输层转发 TCP/UDP 流量,与http块平级共享事件内核,让一台 Nginx 同时成为七层与四层代理。
1. stream 模块与四层代理概览
一句话总结: stream 在 nginx.conf 顶层与 http 平级,每个 server 监听一个端口并把 TCP/UDP 流量原样转发给 upstream。
stream 上下文与 http 上下文一样都是顶层块,指令模型也高度相似:upstream 定义后端集群,server 定义监听端口,location 被 preread 阶段的匹配能力取代(如按 TLS SNI 或按源地址分流)。启用 stream 需要在编译或安装时带有 --with-stream,多数发行版默认已包含。
# nginx.conf 顶层:stream 与 http 平级
user nginx;
worker_processes auto;
events {
worker_connections 1024;
}
# 四层代理块
stream {
upstream mysql_backend {
server 10.0.1.10:3306;
server 10.0.1.11:3306;
}
server {
listen 3306;
proxy_pass mysql_backend;
}
}
# 七层代理块
http {
include /etc/nginx/mime.types;
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
}
}
proxy_pass 在 stream 块里可以指向 upstream 名,也可以直接写 host:port。与 http 不同,stream 不做 URI 解析、不改写请求,它默认把客户端到后端之间的字节流原样双向搬运。这个「不解析」的特性既带来了低开销,也意味着压缩、缓存、限流等 HTTP 层能力在 stream 下都不可用——需要限流时只能在 http 层做,或依赖后端的连接数限制。
| 维度 | http 模块 | stream 模块 |
|---|---|---|
| 工作层 | 七层(HTTP) | 四层(TCP/UDP) |
| 解析内容 | 请求行、请求头、响应头 | 不解析,仅透传字节 |
| 常见指令 | proxy_pass、limit_req、proxy_cache | proxy_pass、proxy_protocol、preread |
| 典型场景 | Web、API 网关 | MySQL、Redis、gRPC、数据库复制 |
2. TCP 代理与负载均衡
一句话总结: stream 的 upstream 支持轮询、加权、哈希等负载均衡算法,四层连接级均衡比七层请求级均衡更轻量。
四层代理的负载均衡在连接级别完成:客户端建立一条 TCP 连接,Nginx 把它映射到某个后端的一条连接。stream 的 upstream 同样支持 least_conn、random 与 hash 算法。默认的轮询对短连接(如普通查询)很友好,但长连接场景下连接可能长期钉在某个后端,需要按业务特征选择算法。
stream {
upstream redis_cluster {
# 长连接场景:按客户端 IP 哈希,保持同一客户端命中同一后端
hash $remote_addr consistent;
server 10.0.3.10:6379 weight=2;
server 10.0.3.11:6379;
server 10.0.3.12:6379;
}
server {
listen 6379;
proxy_pass redis_cluster;
proxy_timeout 5s; # 连接空闲超过 5s 断开
proxy_connect_timeout 3s;
}
}
四层连接数占用是评估负载能力的关键。每条 TCP 连接在 Nginx 上占用一个文件描述符与少量内存,worker_connections 决定单 worker 的连接上限。连接数估算公式与 http 相同:worker_processes × worker_connections。由于四层代理不做内容解析,CPU 开销显著低于七层,单台 Nginx 可以承载数万乃至数十万条四层连接。
# 为高连接数场景调整内核参数
worker_rlimit_nofile 200000;
events {
worker_connections 65535;
use epoll;
}
负载均衡之外,proxy_timeout 与 proxy_connect_timeout 需要按协议特征设置:MySQL 与 Redis 这类协议空闲连接常常是常态,proxy_timeout 设得过短会频繁断开被客户端误判为故障;反之过长则占用连接资源。通常把超时设置在「后端允许的空闲时间」之上,配合后端的 wait_timeout 一起规划。
3. UDP 代理与健康检查
一句话总结: UDP 是面向数据报的协议,stream 用
listen ... udp承接,proxy_responses与健康检查的匹配规则都与 TCP 不同。
UDP 代理通常用于 DNS、NTP、Syslog 或游戏服务器等基于数据报的服务。stream 的 UDP 监听与 TCP 共享 server 语法,通过 udp 关键字区分:
stream {
upstream dns_backend {
server 10.0.4.53:53;
server 10.0.4.54:53;
}
server {
listen 53 udp;
proxy_pass dns_backend;
proxy_responses 1; # 每个客户端请求期望收到 1 个响应数据报
}
}
proxy_responses 告诉 Nginx 在收到 N 个响应数据报后可以认为这次「会话」结束、复用客户端地址映射。DNS 请求通常一个请求对应一个响应,设为 1 即可。如果后端是多答多报的协议(如部分自定义协议),需要按实际情况调整,否则 Nginx 会长期占住客户端地址映射,导致新请求无法建立会话。
健康检查对 UDP 尤其重要,因为 UDP 没有握手,Nginx 无法通过连接建立与否判断后端是否健康。stream 模块提供 health_check 指令,可以配置自定义发送与匹配规则:
stream {
upstream dns_backend {
zone dns_zone 64k; # 开启 zone 才能使用健康检查
server 10.0.4.53:53;
server 10.0.4.54:53;
}
server {
listen 53 udp;
proxy_pass dns_backend;
health_check udp interval=5s passes=2 fails=3;
}
}
TCP 健康检查则是主动建立连接并验证连接成功,对 MySQL、Redis 这类服务可以直接用默认的 TCP 检查:
stream {
upstream mysql_backend {
zone mysql_zone 64k;
server 10.0.1.10:3306;
server 10.0.1.11:3306;
}
server {
listen 3306;
proxy_pass mysql_backend;
health_check interval=5s passes=2 fails=3;
}
}
注意:stream 的
health_check属于 Nginx Plus 的商业特性,开源版本可以使用nginx -s reload配合外部探活脚本,或用社区方案(如 nginx-stream-health-check 模块)实现等价能力。
4. stream 上的 SSL termination
一句话总结: stream 的 ssl 配置把 TLS 终结下沉到四层,让后端只面对明文流量,同时支持按 SNI 分发不同证书。
很多内网服务本身不做 TLS,客户端却要求加密传输。stream 模块可以在四层直接终结 TLS,后端仍然走明文,这样对后端的改造为零:
stream {
upstream tcp_tls_backend {
server 10.0.5.10:9000;
}
server {
listen 9000 ssl;
proxy_pass tcp_tls_backend;
ssl_certificate /etc/nginx/ssl/tcp.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/tcp.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:TCP_SSL:10m;
}
}
四层 SSL 终结的一个关键能力是按 SNI(Server Name Indication)分发。客户端在 TLS 握手 ClientHello 中携带目标域名,stream 可以在 preread 阶段读取 SNI,把它映射到不同的后端。这实现了「一个端口、多个加密服务」,类似 http 层的多虚拟主机:
stream {
map $ssl_preread_server_name $backend {
default default_backend;
mysql.internal mysql_backend;
redis.internal redis_backend;
}
upstream mysql_backend { server 10.0.1.10:3306; }
upstream redis_backend { server 10.0.3.10:6379; }
server {
listen 443;
proxy_pass $backend;
ssl_preread on; # 开启 SNI preread
ssl_certificate /etc/nginx/ssl/internal.crt;
ssl_certificate_key /etc/nginx/ssl/internal.key;
}
}
四层做 SSL 终结的取舍是「看得见握手、看不见内容」。Nginx 可以终结 TLS、看到明文后的协议前几个字节(用于进一步路由),但无法做 HTTP 层缓存与压缩。若既要加密又要七层能力,应把流量引到 http 块处理,stream 只负责按 SNI 或源地址做第一层分流。
5. stream 与 http 的配合
一句话总结: 同一进程内 stream 与 http 并行工作,常见组合是四层先按 SNI/源 IP 分流,七层再做内容级处理。
一台 Nginx 同时配置 stream 与 http 是常见架构:stream 监听入口端口做「第一跳」分流,把 TLS 流量按 SNI 分给不同 server,或把特定来源的流量引到指定后端;http 负责剩余的内容级处理。两者共享 worker 进程与事件循环,配置上互不干扰,但要注意监听端口不能冲突。
# 典型组合:80/443 走 http(Web),3306 走 stream(数据库)
http {
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
location / {
proxy_pass http://web_cluster;
}
}
}
stream {
server {
listen 3306;
proxy_pass mysql_backend;
}
server {
listen 11211;
proxy_pass memcached_backend;
}
}
另一个常见的配合是 stream 承载数据库、消息队列等协议,http 承载 API,二者共同构成服务的接入层。端口规划需要刻意设计:常规 Web 端口(80/443)与数据库端口(3306/6379/11211)在安全组、防火墙与监控告警上要分别对待,避免把数据库端口暴露到公网。stream 侧的访问日志独立配置,用 log_format 记录源地址、目标后端与连接时长,与 http 日志分开观察。
6. 会话保持与粘性
一句话总结: 四层代理没有 Cookie 可用,粘性靠 IP 哈希或 PROXY protocol 携带的真实客户端身份实现,且要考虑后端故障后的重哈希影响。
HTTP 层的会话粘性可以依赖 Cookie,四层流量没有 Cookie 概念。stream 的会话保持通常用 hash $remote_addr consistent 实现:同一客户端 IP 的 TCP 连接总是分发到同一后端。一致性哈希在新增或移除后端节点时,只会影响一小部分连接,不会引发全量重哈希。
stream {
upstream stateful_backend {
hash $remote_addr consistent;
server 10.0.6.10:8080;
server 10.0.6.11:8080;
server 10.0.6.12:8080;
}
server {
listen 8080;
proxy_pass stateful_backend;
}
}
如果客户端经过多层代理,$remote_addr 是离 Nginx 最近一跳的地址,可能无法代表真实客户端。此时需要 PROXY protocol 支持:客户端或前置 LB 在 TCP 连接建立后先发送一行 PROXY 头,携带原始源地址。Nginx 的 proxy_protocol 指令可以接收并透传这个信息:
stream {
server {
listen 8080 proxy_protocol; # 接收 PROXY protocol
proxy_pass backend;
}
}
# 后端也需要支持 PROXY protocol,或由 Nginx 向下游转发时补发
stream {
server {
listen 8080 proxy_protocol;
proxy_pass backend;
proxy_protocol on; # 向下游也发送 PROXY protocol
}
}
粘性设计要始终考虑故障场景:当后端宕机、从 upstream 摘除时,一致性哈希会把它承载的连接映射到其他节点。对有状态协议(如 WebSocket 长连接、数据库会话)而言,这意味着客户端需要重新建立会话。架构上应尽量让后端无状态或把状态外置到 Redis,降低粘性失效带来的影响。
7. 性能调优与排错
一句话总结: 四层代理的调优围绕文件描述符、缓冲区与超时展开,排错则从连接建立、握手、数据流动三个层面逐层定位。
四层代理的性能瓶颈通常是文件描述符上限与内存缓冲,而非 CPU。调优优先检查几个方向:worker_rlimit_nofile 是否覆盖了目标连接数;proxy_buffer_size 是否与协议的数据包大小匹配;proxy_timeout 与协议空闲行为是否冲突。
stream {
proxy_buffer_size 16k; # 适配较大数据报(如 DNS 响应)
proxy_timeout 30s; # 按协议空闲特征设定
proxy_connect_timeout 5s;
proxy_socket_keepalive on; # 保持后端 socket 的 TCP keepalive
}
排错时用 tcpdump 与 ss 定位问题分层。客户端能连上 Nginx 但拿不到数据,问题可能在 Nginx 到后端这一段;握手都建立不了,问题可能在监听或防火墙。Nginx 侧日志是重要线索,stream 的 error_log 会记录 upstream 连接失败、超时等信息:
# 查看四层代理的连接与监听状态
ss -tnp | grep nginx
# 抓取客户端与后端的双向流量,定位断点
tcpdump -i eth0 -s 0 port 3306 -w mysql.pcap
# 校验配置并重载
nginx -t && nginx -s reload
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 连接建立失败 | 后端未监听、防火墙拦截 | ss 检查后端端口,安全组放行 |
| 连接频繁断开 | proxy_timeout 过短 | 对照协议空闲超时调大 |
| 单后端过载 | 长连接未散开 | 换 least_conn 或 hash 算法 |
| UDP 请求无响应 | proxy_responses 不匹配 | 调整响应数据报数量 |
| 偶发超时 | keepalive 与后端冲突 | 检查后端连接池与负载 |
8. 总结
| 环节 | 要点 |
|---|---|
| stream 定位 | 四层传输代理,与 http 平级,共享事件内核 |
| TCP 负载均衡 | upstream 支持轮询、加权、哈希与一致性哈希 |
| UDP 代理 | listen … udp,proxy_responses 匹配响应数据报 |
| 健康检查 | TCP 探活 / UDP 自定义匹配,需开启 zone |
| SSL 终结 | stream 上 ssl 指令终结 TLS,SNI preread 按域名分流 |
| 与 http 配合 | 四层做第一跳分流,七层做内容级处理 |
| 会话粘性 | hash $remote_addr consistent,PROXY protocol 透传源地址 |
| 调优排错 | 文件描述符、缓冲区、超时与 tcpdump 分层定位 |
stream 模块把 Nginx 的能力从 Web 层延伸到传输层,让同一套事件驱动架构承载数据库、缓存、消息队列与自定义协议的全部南北流量。掌握四层代理的配置模型与排错方法,是构建完整接入层的关键一步。下一篇转向传输层之上的加密细节,深入 TLS 进阶与双向证书认证。
延伸阅读
- Nginx 反向代理与负载均衡 — 七层 proxy_pass 与 upstream 进阶
- Nginx 核心配置结构 — 配置上下文与指令继承基础
- Nginx HTTPS 与 TLS 加固 — TLS 协议与证书配置深化
- Nginx 日志分析与性能调优 — 日志与性能指标联动
- Nginx 限流与安全防护 — 传输层之外的安全策略
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。