Linux 的口复端口复用,最常被提起的口复场景是 Nginx 和 Apache 抢 80 端口。两个进程同时监听同一个端口,口复内核居然不报错——这事放在 Windows 上大概率直接失败。口复Linux 能做到,口复靠的口复是 SO_REUSEPORT 这个套接字选项。它允许多个 socket 绑定到完全相同的口复 IP 和端口组合,内核负责把进来的口复连接分发出去。
但很多人把 SO_REUSEPORT 和 SO_REUSEADDR 搞混。口复后者出现得更早,口复用途也不同。口复SO_REUSEADDR 主要解决的口复是 TIME_WAIT 状态下的端口占用问题。一个服务重启时,口复旧连接还处于 TIME_WAIT,口复正常情况下内核不让新进程绑定这个端口。口复SO_REUSEADDR 就是告诉内核:别拦我,我知道自己在干什么。它并不允许两个活跃的监听 socket 同时存在,这点跟 SO_REUSEPORT 有本质区别。
内核怎么分发连接
启用 SO_REUSEPORT 后,多个 socket 被放进同一个哈希桶。内核收到 SYN 包时,用四元组做哈希,结果决定交给哪个 socket。这意味着同一个客户端的连接始终落到同一个进程上——对需要维护连接状态的服务来说,这个特性很关键。Nginx 1.9.1 之后支持 reuseport 指令,每个 worker 进程独立创建监听 socket,内核直接分发,省掉了 accept 锁的争抢。实测在 32 核机器上,QPS 比传统的 accept 互斥模式高出不少。
不过有个坑:如果多个进程绑定的 IP 不同,比如一个绑 0.0.0.0:80,另一个绑 192.168.1.5:80,那 SO_REUSEPORT 不生效,内核会直接拒绝。必须所有 socket 的地址和端口完全一致才行。另外,内核版本也有要求,3.9 才引入这个特性,之前的版本只能干瞪眼。
不只是负载均衡
端口复用还有个冷门用法:热升级。老进程还在跑,新进程已经绑定同一个端口开始监听。老进程处理完手头的连接后退出,新进程无缝接管。这种平滑过渡在需要零停机的场景里很实用。HAProxy 和 Envoy 都用了类似机制。
再往深了说,SO_REUSEPORT 还能用来做简单的内核态负载均衡。不用 LVS,不用 iptables 转发,直接让内核把连接散到多个进程。配置也简单,每个进程设置一下 socket 选项就行。但要注意,哈希算法是基于四元组的,所以来自同一 IP 的大量连接可能压到同一个进程上。如果客户端 IP 集中,负载可能不均。这时候得考虑用 SO_ATTACH_REUSEPORT_CBPF 挂个 BPF 程序自定义分发逻辑,不过那就复杂多了。
一些实际观察
我在一台 4 核的测试机上跑过对比。单进程监听 80 端口,压到 3 万 QPS 左右 CPU 就满了。换成 4 个进程启用 SO_REUSEPORT,QPS 能到 9 万多。提升明显,但也不是线性翻倍——内核分发本身有开销,连接建立和销毁的成本也在那摆着。
还有个细节:SO_REUSEPORT 组里的 socket 数量变化时,内核会重建哈希表。如果频繁启停进程,可能会丢连接。所以生产环境里,进程数最好固定,别搞动态伸缩那一套。
端口复用不是银弹。它解决的是特定问题:多进程监听同一端口、内核级分发、热升级。用对了场景,省事又高效;用错了,调试起来一头雾水。搞清楚 SO_REUSEADDR 和 SO_REUSEPORT 的区别,是第一步。