第二个 worker 一起 bind,终端弹出 Address already in use。有人随手加上 SO_REUSEADDR,以为「复用」这个词就够了。有人听过 SO_REUSEPORT,分不清该在哪一层设、设几次。Linux 手册 socket(7) 把两个选项写在一起,对照着看就不容易混。下面按手册口径拆开讲,不讨论怎么去抢别人已经在听的端口,也不写野路子劫持。
SO_REUSEADDR 放宽的是什么
SO_REUSEADDR 表示:校验 bind(2) 地址时,规则放宽一点,允许复用本地地址。对 AF_INET 来说,通常仍不能在已经有活跃 listen socket 占着同一地址时再抢同一端口。当监听方绑在 INADDR_ANY 加某个端口上时,其它本地地址想再绑这个端口一般也不行。它解决的更多是 TIME_WAIT 一类场景下的「地址暂时被认为还占用着」,方便服务重启后尽快再 bind,不是「多个独立 listener 和平共处」的开关。
手册 NOTES 里还提了一点和别的系统不一样的地方:在 Linux 上,想复用端口时,前一个绑过这个端口的程序和后来想复用的程序,通常都需要设过 SO_REUSEADDR。只在新进程上打开,有时仍绑不上。日常写服务时两边都设,能少踩一点跨平台直觉差。重启后偶发 EADDRINUSE、过一会儿又好了,优先怀疑的是旧连接还在 TIME_WAIT,而不是「内核不允许你再听」。
SO_REUSEPORT 才是多 listener
SO_REUSEPORT(Linux 3.9 起)才允许多个 AF_INET / AF_INET6 socket 绑到同一个地址。手册写得很死:每个 socket 都要在调用 bind(2) 之前设好这个选项,包括第一个。为防端口被别的用户抢走,绑同一地址的进程需要相同的有效用户 ID。TCP 和 UDP 都能用。
TCP 上,每个线程各自一个 listener,再 accept,往往比「一个线程 accept 再分发」或「多线程抢同一个 listen fd」更均匀。UDP 上,多进程 / 多线程各自收包,也比大家抢同一个 socket 好分一些。需要自定义把包分给 reuseport 组里哪一个 socket 时,还有 SO_ATTACH_REUSEPORT_CBPF / SO_ATTACH_REUSEPORT_EBPF。UDP 从较新的内核起支持,TCP 也随后跟上。BPF 程序按加入组的顺序给 socket 编号,返回非法下标会退回普通 REUSEPORT 分配。那是进阶用法,日常先把「每个 fd 都在 bind 前设 REUSEPORT」做对就够。
实操时别踩的点
- 只开
SO_REUSEADDR、指望两个服务同时听同一端口,按手册描述通常行不通。报错看起来像「端口被占」,根因却是选项选错。 - 多进程 / 多线程 listener:每个 socket 都要设
SO_REUSEPORT,漏掉一个就会在 bind 阶段炸。第一个 listener 也不能省。 - 不要指望靠这些选项去「接管别人已经在听的端口」。同 euid 限制本身就是防劫持的设计。
- 排错时顺手确认一下是不是旧进程还占着端口、是不是绑到了
0.0.0.0和具体网卡地址冲突,别把所有EADDRINUSE都怪到缺 REUSEPORT 上。 - HTTP 反向代理、多 worker 模型里,如果框架文档要求显式打开 reuseport,就按框架的 socket 创建路径设,别只在业务层「以为内核会自动帮你」。
写代码时建议把 setsockopt 紧挨在 socket() 之后、bind() 之前,并检查返回值。有的封装库把 option 暴露成构造参数或 listen 辅助函数。若文档写明「内部已设 REUSEPORT」,仍应用 getsockopt 或启动日志确认一次,避免以为设了其实没设。容器网络、宿主端口映射再叠一层时,宿主侧看到的占用和容器内 bind 失败也可能不是同一回事,排错时把 ss -lntp / netstat 的输出和进程树对上再改选项。
你们线上是单进程 listen 多线程 accept,还是一开始就按 REUSEPORT 拆多个 listener?设漏一个 fd 的坑踩过没有?