1、端口复用基础回顾
各位好,我是老赵。今天咱们开始聊端口复用。说实话,这个主题我讲了快十年,每次都有新感悟。端口复用听起来高大上,说白了就是——让多个服务共享同一个端口。
你可能会问:一个端口不是只能给一个进程用吗?嗯,正常情况下确实如此。但现实世界哪有那么多端口给你用?尤其是公网 IP 稀缺、防火墙规则复杂的时候,能少开一个端口就少开一个。我当年在电商公司做架构时,一台机器上跑了七八个 Web 服务,要是每个都开独立端口,运维能疯掉。
什么是端口复用
端口复用,英文叫 Port Reuse 或 Port Sharing。它的核心思想是:多个服务进程监听同一个传输层端口,由前置的调度层根据协议特征或连接属性,把请求分发给正确的后端。
举个例子:
- Nginx 监听 80 端口,同时代理 PHP-FPM 和静态文件服务
- HAProxy 监听 443 端口,背后是多个 HTTPS 应用
- SSH 的端口转发功能,也能算一种轻量级复用
说白了,端口复用不是让两个进程同时 bind 同一个端口——那是做不到的(除非用 SO_REUSEADDR 的特殊场景)。真正的做法是:一个进程占住端口,然后由它做二次分发。
核心要点:端口复用 ≠ 多进程 bind 同一端口。它是在应用层或传输层之上,通过一个中间层实现的多路分发。
TCP 端口复用原理
TCP 是面向连接的协议。它的端口复用,依赖的是 TCP 报文中的目标端口 + 源 IP + 源端口 这个三元组来做区分。
我画个图帮你理解:
你看,三个客户端都访问同一个 80 端口。复用层收到 TCP 连接后,根据 HTTP 请求中的 Host 字段,或者 TLS 握手时的 SNI 扩展,把连接转发给不同的后端服务。后端服务监听在内网端口上,对外不可见。
这里有个关键点:TCP 连接的四元组(源 IP、源端口、目标 IP、目标端口)是唯一的。复用层拿到连接后,可以基于应用层信息做路由,而不是靠传输层。这也是为什么叫「端口复用」而不是「端口共享」——复用的是端口号,但连接是独立的。
我的经验:有一次线上排查问题,发现 Nginx 的 upstream 配置写错了,导致所有请求都转发到了同一个后端。表面上看端口复用正常,但实际流量全偏了。所以,端口复用层一定要加健康检查和流量监控,否则出问题你都不知道。
UDP 端口复用原理
UDP 的端口复用和 TCP 不太一样。UDP 是无连接的,一个端口收到的数据报,可以同时被多个 socket 接收——前提是它们都设置了 SO_REUSEADDR 或 SO_REUSEPORT 选项。
我举个例子:
- SO_REUSEADDR:允许多个 socket bind 同一个端口,但通常用于多播或广播场景
- SO_REUSEPORT:Linux 3.9+ 引入,允许多个进程/线程 bind 同一个端口,内核做负载均衡
你想想看,UDP 没有连接的概念,每个数据报都是独立的。内核收到一个 UDP 报文后,会把它交给所有 bind 了该端口的 socket 之一(取决于哈希或随机策略)。这就天然支持了端口复用。
| 特性 | TCP 端口复用 | UDP 端口复用 |
|---|---|---|
| 复用方式 | 应用层代理/转发 | 内核多 socket 分发 |
| 连接状态 | 有状态(四元组) | 无状态(每个报文独立) |
| 常用工具 | Nginx, HAProxy, Envoy | iptables, DNS 服务器, QUIC |
| 配置复杂度 | 中等(需配置路由规则) | 较低(内核参数即可) |
| 典型场景 | Web 服务、API 网关 | DNS、NTP、VoIP |
我记得有一次做 DNS 服务器优化,用 SO_REUSEPORT 让多个 worker 进程共享 53 端口。效果立竿见影,CPU 利用率从 70% 降到了 40%。内核自动做负载均衡,省去了应用层的锁竞争。
注意:UDP 的 SO_REUSEPORT 在 Linux 上虽然好用,但要注意哈希冲突。如果客户端 IP 分布不均匀,某些 worker 可能被饿死。我建议配合 SO_ATTACH_FILTER 做更细粒度的分发。
常见应用场景
Nginx 的端口复用
Nginx 是端口复用的典型代表。一个 Nginx 进程监听 80 端口,背后可以代理 PHP、Python、静态文件、反向代理到其他服务。它的核心机制是 虚拟主机(Virtual Host)。
# Nginx 配置示例:同一端口,多个站点
server {
listen 80;
server_name blog.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8081;
}
}
server {
listen 80;
server_name static.example.com;
location / {
root /var/www/static;
}
}
你看,三个 server 块都监听 80 端口。Nginx 根据请求的 Host 头,决定把请求交给哪个后端。这就是端口复用的典型实现。
我个人习惯在 Nginx 的端口复用层加一层 access_log 区分,每个虚拟主机写不同的日志文件。排查问题时,直接看对应日志就行,不用在混杂的日志里大海捞针。
HAProxy 的端口复用
HAProxy 比 Nginx 更底层,它工作在传输层和应用层之间。HAProxy 的端口复用,通常用于 TCP 代理 和 SSL 终结。
# HAProxy 配置示例:同一端口,多种协议
frontend web_front
bind *:443
mode tcp
tcp-request inspect-delay 5s
acl is_http req.ssl_sni -i blog.example.com
acl is_api req.ssl_sni -i api.example.com
use_backend blog_servers if is_http
use_backend api_servers if is_api
backend blog_servers
server blog1 127.0.0.1:8080 check
backend api_servers
server api1 127.0.0.1:8081 check
HAProxy 通过 SNI(Server Name Indication) 来区分不同的 TLS 连接。客户端在 TLS 握手时就会告诉服务器它要访问哪个域名,HAProxy 根据这个信息做路由。
我曾经遇到过一个坑:某些老版本的客户端不支持 SNI,导致所有请求都落到了默认后端。后来我在 HAProxy 里加了 default_backend 兜底,才算彻底解决。
其他场景
- SSH 端口转发:通过本地端口转发,把远程服务的端口映射到本地,实现端口复用
- Docker 端口映射:宿主机一个端口映射到多个容器的不同端口
- Kubernetes Service:ClusterIP 模式下,一个 Service IP 的端口可以分发到多个 Pod
- iptables REDIRECT:把流量从 80 端口重定向到透明代理的 3128 端口
一句话总结:端口复用的本质是「一个入口,多个出口」。入口是公网端口,出口是内网服务。中间层负责做路由和分发。