1. 端口复用基础:从一次线上故障说起
先讲个我自己的经历。几年前我负责一个网关服务,压测到 8 万并发时,新连接突然全部失败。查了半天,发现是端口不够用了——每个连接都独占一个本地端口,系统默认的 28232 个端口很快被耗尽。
当时我就在想:能不能让多个连接共享同一个端口?这就是端口复用的起源。说白了,端口复用就是让多个 socket 绑定到同一个 IP 和端口上,而不是每个 socket 独占一个端口。
1.1 什么是端口复用
端口复用,英文叫 Port Reuse。它允许不同的 socket 绑定到相同的 IP 地址和端口号。正常情况下,一个端口只能被一个 socket 占用。但有了端口复用,多个 socket 可以共享同一个端口。
举个例子:你的 Nginx 监听 80 端口,同时你的测试程序也想绑定 80 端口。没有端口复用,第二个程序会报错 "Address already in use"。开启端口复用后,两个程序都能绑定成功。
核心概念:端口复用不是让多个连接共用同一个端口,而是让多个 socket 可以绑定到同一个端口。每个连接仍然有独立的四元组(源 IP、源端口、目标 IP、目标端口)。
1.2 为什么需要端口复用
我总结了一下,端口复用主要解决三个场景的问题:
- 高并发连接:单机需要支撑百万级连接时,端口不够用。复用端口可以突破 65535 的限制。
- 快速重启服务:服务崩溃后立即重启,如果旧连接还在 TIME_WAIT 状态,新进程就无法绑定端口。端口复用允许新进程立即接管。
- 多实例部署:同一台机器上跑多个服务实例,每个实例都想监听同一个端口。端口复用让这成为可能。
你想想看,一个 C10M(千万并发)场景下,如果每个连接都独占一个端口,那得需要多少个端口?端口复用就是解决这个问题的关键。
1.3 SO_REUSEADDR 与 SO_REUSEPORT 的区别
这两个选项经常被搞混。我刚开始学的时候也分不清,直到在一次线上事故中踩了坑才彻底搞明白。
| 特性 | SO_REUSEADDR | SO_REUSEPORT |
|---|---|---|
| 主要用途 | 允许重用 TIME_WAIT 状态的端口 | 允许多个进程绑定同一端口 |
| 绑定规则 | 后绑定的进程必须指定更具体的 IP | 所有进程绑定条件完全相同 |
| 负载均衡 | 无,由内核随机分配 | 内核提供,近似均匀分配 |
| 安全性 | 低,恶意程序可劫持端口 | 高,只允许同一用户下的进程 |
| Linux 版本 | 所有版本支持 | 3.9+ 内核支持 |
SO_REUSEADDR 详解
这个选项解决的是 TIME_WAIT 问题。当一个 TCP 连接关闭后,端口会进入 TIME_WAIT 状态,默认持续 2MSL(约 60 秒)。这段时间内,其他程序无法绑定这个端口。
设置 SO_REUSEADDR 后,即使端口处于 TIME_WAIT 状态,新 socket 也能绑定。这在服务重启时特别有用。
// C 语言示例:设置 SO_REUSEADDR
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt SO_REUSEADDR failed");
exit(EXIT_FAILURE);
}
避坑指南:我曾经在开发环境遇到过一个问题——设置了 SO_REUSEADDR 后,两个不同端口的服务互相干扰。后来发现是因为绑定了通配地址 0.0.0.0,导致端口冲突。记住:SO_REUSEADDR 只解决 TIME_WAIT 问题,不解决端口冲突问题。
SO_REUSEPORT 详解
这个选项是 Linux 3.9 内核引入的。它允许多个进程或线程绑定到完全相同的 IP 和端口。内核会在这些 socket 之间做负载均衡,将新连接均匀分发。
// C 语言示例:设置 SO_REUSEPORT
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) < 0) {
perror("setsockopt SO_REUSEPORT failed");
exit(EXIT_FAILURE);
}
SO_REUSEPORT 的好处很明显:
- 多进程监听同一端口,天然支持多核扩展
- 内核做负载均衡,减少锁竞争
- 进程独立,一个进程崩溃不影响其他进程
注意:SO_REUSEPORT 不是万能的。如果连接处理时间差异很大,内核的简单哈希分发可能导致负载不均。我曾在压测中发现,某些 worker 进程的连接数比其他进程多 30%。解决方案是使用 SO_REUSEPORT 的 BPF 分流功能,或者在上层加一层负载均衡。
1.4 端口复用的知识体系
下面这张图展示了端口复用的核心知识结构,我把它画出来方便你理解:
1.5 实际应用场景
我整理了几个常见的应用场景,你可以对照自己的项目看看:
- Nginx 多 worker 进程:每个 worker 进程都监听 80 端口,使用 SO_REUSEPORT 让内核做负载均衡。这是最经典的应用。
- Redis 哨兵模式:哨兵进程需要快速接管主节点端口,使用 SO_REUSEADDR 避免 TIME_WAIT 阻塞。
- 自定义代理服务器:代理服务器需要大量本地端口连接后端,使用端口复用可以大幅提升连接数。
我的建议:如果你的服务需要频繁重启,优先使用 SO_REUSEADDR。如果是多进程高并发场景,SO_REUSEPORT 是更好的选择。但要注意,SO_REUSEPORT 在 Linux 3.9 以下内核不可用,生产环境需要确认内核版本。