nginx的map指令仅适用于http上下文,无法直接在stream模块中基于端口号做映射;正确做法是为每个业务端口配置独立的stream server块,或结合proxy protocol扩展与定制模块实现动态路由。

Nginx 的 map 指令本身不直接处理 TCP/UDP 流量——它仅在 HTTP 上下文中生效,用于变量映射。若需按端口号实现 TCP/UDP 多业务逻辑转发,必须结合 Nginx 的 stream 模块,并借助 map 预先生成动态 upstream 名称,再由 stream 块引用。这是目前最清晰、可维护性最强的实践路径。
端口号不能直接在 stream 中用 map 匹配
stream 模块不支持 $server_port 或 $remote_port 等变量参与 map 定义(这些变量在 stream 上下文不可用)。因此不能写成:
map $server_port $upstream_name {
8081 "service_a";
8082 "service_b";
}
这是因为 stream 阶段变量解析早于 map 执行时机,且核心变量受限。正确做法是:将端口信息“前置”到可被 map 捕获的位置,例如通过 监听不同 server 块 或 复用 proxy_protocol + 自定义 header(仅限 TLS/HTTP 封装场景)——但后者对纯 TCP/UDP 不适用。
推荐方案:为每个业务端口单独配置 stream server
最稳定、无需 hack 的方式是:每个需区分处理的端口,对应一个独立的 server 块,并在其中指定目标 upstream。适合端口数量可控(如几十个以内)、业务边界明确的场景:
- 监听 9001 → 转发至 MySQL 集群
- 监听 9002 → 转发至 Redis 集群
- 监听 9003 → 转发至自定义 TCP 协议服务
配置示例:
stream {
upstream mysql_cluster {
server 10.0.1.10:3306;
server 10.0.1.11:3306;
}
upstream redis_cluster {
server 10.0.1.20:6379;
server 10.0.1.21:6379;
}
server {
listen 9001;
proxy_pass mysql_cluster;
proxy_timeout 1s;
}
server {
listen 9002;
proxy_pass redis_cluster;
proxy_timeout 1s;
}
}
进阶方案:用 realip + map 实现“伪端口路由”(适用于带 proxy protocol 的 NLB/CLB 后置场景)
当你的 TCP/UDP 流量已由网络型负载均衡(NLB)或云厂商 CLB 统一接入,并启用了 Proxy Protocol v2 时,Nginx stream 可通过 set_real_ip_from 和 real_ip_header proxy_protocol 提取原始客户端信息,但仍无法获取原始目的端口。不过,部分云厂商(如阿里云 NLB)在 Proxy Protocol 扩展字段中可携带自定义 TLV,包括目的端口。此时可:
- 在 NLB 侧配置:对不同端口流量打上不同 TLV 标签(如 port=9001 → tag=“mysql”)
- 在 Nginx stream 中启用
proxy_protocol on; - 使用第三方模块(如
ngx_stream_proxy_protocol_module扩展版)解析 TLV 并注入变量(如$pp_dst_port) - 再用
map映射该变量 → upstream 名称(注意:此 map 必须定义在 stream 块外、http 块内无效;实际需用 lua 或 custom module 支持)
⚠️ 此路径依赖特定基础设施与扩展能力,非通用解法,生产环境慎用。
替代思路:用 Portmap 或专用隧道工具做端口分发层
若 Nginx stream 配置过于静态、端口频繁增删,可考虑在 Nginx 前加一层轻量级端口分发器:
- 用 Portmap 工具监听多个端口,按规则转发到本地不同端口(如 9001→127.0.0.1:19001,9002→127.0.0.1:19002)
- Nginx stream 统一监听 19001/19002 等内部端口,再做 upstream 分发
- 优势:Portmap 支持热重载、Token 认证、UDP/TCP 双协议,适合开发测试或中小规模多租户 TCP 网关











