Nginx stream 模块实战:四层 TCP/UDP 代理、负载均衡与 SSL 终结

系统讲解 Nginx stream 模块的四层代理能力,覆盖 TCP/UDP 代理转发、upstream 负载均衡与健康检查、stream 上的 SSL termination、会话粘性,以及 stream 与 http 块在同一配置中的协作方式与排错实践。

绝大多数 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_cacheproxy_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」更多文章

  1. Nginx Ingress Controller:Kubernetes 云原生网关的路由、证书与金丝雀发布
  2. Nginx 监控与可观测性:stub_status 指标、Prometheus 集成与容量规划
  3. Nginx 静态资源与页面加速:零拷贝、缓存验证与 Brotli 压缩联动