1、端口复用基础回顾

各位好,我是老赵。今天咱们开始聊端口复用。说实话,这个主题我讲了快十年,每次都有新感悟。端口复用听起来高大上,说白了就是——让多个服务共享同一个端口。

你可能会问:一个端口不是只能给一个进程用吗?嗯,正常情况下确实如此。但现实世界哪有那么多端口给你用?尤其是公网 IP 稀缺、防火墙规则复杂的时候,能少开一个端口就少开一个。我当年在电商公司做架构时,一台机器上跑了七八个 Web 服务,要是每个都开独立端口,运维能疯掉。

什么是端口复用

端口复用,英文叫 Port Reuse 或 Port Sharing。它的核心思想是:多个服务进程监听同一个传输层端口,由前置的调度层根据协议特征或连接属性,把请求分发给正确的后端。

举个例子:

  • Nginx 监听 80 端口,同时代理 PHP-FPM 和静态文件服务
  • HAProxy 监听 443 端口,背后是多个 HTTPS 应用
  • SSH 的端口转发功能,也能算一种轻量级复用

说白了,端口复用不是让两个进程同时 bind 同一个端口——那是做不到的(除非用 SO_REUSEADDR 的特殊场景)。真正的做法是:一个进程占住端口,然后由它做二次分发。

核心要点:端口复用 ≠ 多进程 bind 同一端口。它是在应用层或传输层之上,通过一个中间层实现的多路分发。

TCP 端口复用原理

TCP 是面向连接的协议。它的端口复用,依赖的是 TCP 报文中的目标端口 + 源 IP + 源端口 这个三元组来做区分。

我画个图帮你理解:

TCP 端口复用原理图 客户端 A 192.168.1.10:54321 客户端 B 192.168.1.20:54322 客户端 C 192.168.1.30:54323 端口复用层 监听 :80 根据 Host/SNI 分发 Nginx / HAProxy Web 服务 A 127.0.0.1:8080 Web 服务 B 127.0.0.1:8081 Web 服务 C 127.0.0.1:8082 所有客户端访问同一个 :80 端口,复用层根据 HTTP Host 头或 TLS SNI 分发到不同后端

你看,三个客户端都访问同一个 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 端口

一句话总结:端口复用的本质是「一个入口,多个出口」。入口是公网端口,出口是内网服务。中间层负责做路由和分发。