第一部分:什么是端口重用
一、先建立两个前提概念
1.1 TCP 连接的完整标识:四元组
一个 TCP 连接由四元组唯一定位:
(源 IP,源端口,目的 IP,目的端口)
- 监听 socket(服务器端)绑定的是「本地 IP + 本地端口」,等待客户端连接。
- 已建立连接 (established)由完整四元组区分。即使 1000 个客户端都连到服务器的
:8000,它们的「源端口」各不相同,所以互不冲突。
这就是为什么一个服务器端口能同时服务成千上万个客户端------因为连接是被四元组区分的,而不是只靠服务器端口。理解这一点,是理解端口复用的钥匙。
1.2 端口绑定的默认规则:独占
当服务器进程调用 bind() 绑定 IP:Port 时,默认情况下这个地址是独占 的。此时如果另一个进程再 bind() 同一个地址,会报 Address already in use(地址已占用)。
端口重用,就是要打破这个「独占」默认,让 bind 同一地址从报错变成合法。
二、TIME_WAIT:端口复用的「起因」
2.1 什么是 TIME_WAIT
TCP 关闭连接是四次挥手。主动关闭的一方 (通常是服务器调用 close())在发送最后一个 ACK 后,会进入 TIME_WAIT 状态,持续约 2MSL(Maximum Segment Lifetime,Linux 默认 60 秒)。
TIME_WAIT 存在的两个目的:
- 保证最后一个 ACK 能被对方收到(如果 ACK 丢失,对方会重发 FIN,需要能再次应答)。
- 让旧连接的迟到报文在网络中「死掉」,避免同一个四元组被新连接复用后,收到上一个连接的残留数据。
2.2 TIME_WAIT 带来的麻烦
问题在于:进入 TIME_WAIT 的 socket,其 本地IP:本地端口 二元组被锁定 了。如果服务器进程被重启(或快速重启),新进程马上 bind() 同一个端口,会撞上这个处于 TIME_WAIT 的地址,报 Address already in use,必须等 60 秒才能重新绑定。
这就是「重启服务器要等 1 分钟端口才能再用」这个经典现象的根源。
三、两种「端口重用」选项
针对上面两个问题(独占 bind、TIME_WAIT 锁),内核提供了两个 socket 选项,名字相似但作用完全不同:
3.1 SO_REUSEADDR ------ 解决 TIME_WAIT 复用
作用 :允许新 socket 绑定到处于 TIME_WAIT 状态的地址,跳过 2MSL 等待。
- 典型场景:快速重启。旧进程关闭后端口进入 TIME_WAIT,新进程带上这个选项能立刻重新绑定。
- 关键:它不允许多个进程「同时」监听同一端口。它是「先后复用」------旧的不在了,新的顶上来。
3.2 SO_REUSEPORT ------ 允许多个 socket 同时监听
作用 :允许多个 socket 同时 绑定到完全相同的 IP:Port,内核把它们组成一个「监听组」,把新连接按哈希负载均衡地分发到组内某个 socket。
- 典型场景:
- 多进程/多线程共享同一端口(如 Nginx 的多个 worker、Go 程序开多个 listener),提升多核利用率。
- 零停机重启/热更新:新旧两个进程短暂同时监听,实现「先起新、再停旧」。
- 分发粒度是「连接」(不是请求),按四元组哈希,保证同一连接始终落在同一个 socket。
3.3 一句话总结
| 解决什么问题 | 能否「同时」多进程监听 | 本质 | |
|---|---|---|---|
SO_REUSEADDR |
TIME_WAIT 等待 | ❌ 不能 | 先后复用(重启) |
SO_REUSEPORT |
独占绑定 | ✅ 能 | 同时共存(负载均衡/平滑切换) |
REUSEADDR 解决「先后复用」,REUSEPORT 解决「同时共存」。
3.4 还有一个易混点:UDP 下的 REUSEADDR
在 UDP 里,SO_REUSEADDR 配合 SO_REUSEPORT 能让多个 socket 同时绑定同一端口来接收广播/多播------这是另一套语义,与 TCP 不同,本文不展开。
第二部分:Gateway 的端口重用实现
结论先行:gateway 用
SO_REUSEPORT(封装在 framework 库reuseport包),默认全局开启,真正目的是配置热更新的零停机切换,而不是让多个业务进程长期共享端口。
四、配置层:一个指针字段的设计巧思
端口重用的开关是 NetworkConfig.ReusePort,定义在 framework/core/config/system.go:75:
go
type NetworkConfig struct {
SkipOccupiedPort bool `config:"skip_occupied_port"`
// ReusePort 为 nil 表示「未配置」,用于区分「未设置」和「显式 false」
ReusePort *bool `config:"reuse_port"`
}
注意它是 *bool(指针),不是 bool 。这个设计很关键:指针能区分三种状态------nil(没配)、&true(显式开)、&false(显式关)。而 applyDefaultReusePort 只对 nil 补默认值,不覆盖用户的显式选择:
go
// proxy/gateway.go:397
func applyDefaultReusePort(cfg *common.EntryConfig, disableReuseByDefault bool) {
if cfg.NetworkConfig.ReusePort == nil { // 只处理「未配置」的
reuse := !disableReuseByDefault // 默认 = 不 disable,即开启
cfg.NetworkConfig.ReusePort = &reuse
}
}
判断函数 system.go:79:
go
func (cfg NetworkConfig) ReusePortEnabled() bool {
return cfg.ReusePort != nil && *cfg.ReusePort
}
gateway.yml 中的对应配置:
yaml
gateway:
disable_reuse_port_by_default: false # false = 所有 entry 默认开启 reuse_port
entry:
- name: my_es_entry
network:
binding: $[[env.GW_BINDING]] # 0.0.0.0:8000
reuse_port: true # 单 entry 显式开启
相关约束 (entry.go:86-88):reuse_port 与 skip_occupied_port 互斥,同时开启直接报错------因为前者靠 SO_REUSEPORT 共存,后者靠「自动换一个空闲端口」绕开占用,两种策略语义矛盾。
五、监听层:核心分支(proxy/entry/entry.go:248-270)
Entrypoint.Start() 里决定用哪种方式创建 listener:
go
var ln net.Listener
var err error
if this.config.NetworkConfig.ReusePortEnabled() {
// 根据监听地址是 IPv4 还是 IPv6,决定用 tcp4 / tcp6
network := "tcp4"
if host, _, err := net.SplitHostPort(this.listenAddress); err == nil {
if ip, err := netip.ParseAddr(host); err == nil && ip.Is6() {
network = "tcp6"
}
}
ln, err = reuseport.Listen(network, this.config.NetworkConfig.GetBindingAddr())
} else {
ln, err = net.Listen("tcp", this.listenAddress)
}
- 开了
reuse_port→ 走reuseport.Listen(底层设SO_REUSEPORT)。 - 没开 → 走普通
net.Listen。 - 注意:
reuseport.Listen只支持tcp4/tcp6,所以要先按地址族判断 network 类型。
监听成功后,listener 传给 fasthttp server(entry.go:420):
go
if err := this.server.Serve(ln); err != nil { ... }
六、端口重用的真正用途:零停机热更新
这是 gateway 里 SO_REUSEPORT 的核心价值 ------不是为了「多进程长期共享」,而是为了配置变更时不停机。
启动完成后,gateway.go:417 调 handleConfigureChange() 监听配置变化。当 entry 配置变更时(gateway.go:248-336),新旧 entry 按 reuse_port 分成两组,用三阶段切换:
#mermaid-svg-mAP35PMWDMouQ61L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mAP35PMWDMouQ61L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mAP35PMWDMouQ61L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mAP35PMWDMouQ61L .error-icon{fill:#552222;}#mermaid-svg-mAP35PMWDMouQ61L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mAP35PMWDMouQ61L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mAP35PMWDMouQ61L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mAP35PMWDMouQ61L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mAP35PMWDMouQ61L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mAP35PMWDMouQ61L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mAP35PMWDMouQ61L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mAP35PMWDMouQ61L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mAP35PMWDMouQ61L .marker.cross{stroke:#333333;}#mermaid-svg-mAP35PMWDMouQ61L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mAP35PMWDMouQ61L p{margin:0;}#mermaid-svg-mAP35PMWDMouQ61L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mAP35PMWDMouQ61L .cluster-label text{fill:#333;}#mermaid-svg-mAP35PMWDMouQ61L .cluster-label span{color:#333;}#mermaid-svg-mAP35PMWDMouQ61L .cluster-label span p{background-color:transparent;}#mermaid-svg-mAP35PMWDMouQ61L .label text,#mermaid-svg-mAP35PMWDMouQ61L span{fill:#333;color:#333;}#mermaid-svg-mAP35PMWDMouQ61L .node rect,#mermaid-svg-mAP35PMWDMouQ61L .node circle,#mermaid-svg-mAP35PMWDMouQ61L .node ellipse,#mermaid-svg-mAP35PMWDMouQ61L .node polygon,#mermaid-svg-mAP35PMWDMouQ61L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mAP35PMWDMouQ61L .rough-node .label text,#mermaid-svg-mAP35PMWDMouQ61L .node .label text,#mermaid-svg-mAP35PMWDMouQ61L .image-shape .label,#mermaid-svg-mAP35PMWDMouQ61L .icon-shape .label{text-anchor:middle;}#mermaid-svg-mAP35PMWDMouQ61L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mAP35PMWDMouQ61L .rough-node .label,#mermaid-svg-mAP35PMWDMouQ61L .node .label,#mermaid-svg-mAP35PMWDMouQ61L .image-shape .label,#mermaid-svg-mAP35PMWDMouQ61L .icon-shape .label{text-align:center;}#mermaid-svg-mAP35PMWDMouQ61L .node.clickable{cursor:pointer;}#mermaid-svg-mAP35PMWDMouQ61L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mAP35PMWDMouQ61L .arrowheadPath{fill:#333333;}#mermaid-svg-mAP35PMWDMouQ61L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mAP35PMWDMouQ61L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mAP35PMWDMouQ61L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mAP35PMWDMouQ61L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mAP35PMWDMouQ61L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mAP35PMWDMouQ61L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mAP35PMWDMouQ61L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mAP35PMWDMouQ61L .cluster text{fill:#333;}#mermaid-svg-mAP35PMWDMouQ61L .cluster span{color:#333;}#mermaid-svg-mAP35PMWDMouQ61L div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mAP35PMWDMouQ61L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mAP35PMWDMouQ61L rect.text{fill:none;stroke-width:0;}#mermaid-svg-mAP35PMWDMouQ61L .icon-shape,#mermaid-svg-mAP35PMWDMouQ61L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mAP35PMWDMouQ61L .icon-shape p,#mermaid-svg-mAP35PMWDMouQ61L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mAP35PMWDMouQ61L .icon-shape .label rect,#mermaid-svg-mAP35PMWDMouQ61L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mAP35PMWDMouQ61L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mAP35PMWDMouQ61L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mAP35PMWDMouQ61L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} reuse_port=false
reuse_port=true
配置变更,entry 需重建
新 entry 按 reuse_port 分两组
阶段1:先停旧 entry
(不能与旧 listener 共存)
阶段2:直接启动新 entry
SO_REUSEPORT 与旧 listener 并存
阶段3:停掉剩余旧 entry
零停机切换完成
reuse_port=false的 entry:必须先停旧的再启新的(否则 "address already in use")。切换瞬间有短暂中断。reuse_port=true的 entry :新 listener 靠SO_REUSEPORT和旧 listener 并存 ,实现「先启动新、再停旧」的零停机切换。
这正是 gateway 默认开启 reuse_port 的原因:让配置热更新不丢连接。
七、底层实现:手工 socket 调用
7.1 三层封装
reuseport.Listen 来自 framework 库,它只是 tcplisten 的一层薄封装(framework/lib/fasthttp/reuseport/reuseport.go:35):
go
func Listen(network, addr string) (net.Listener, error) {
ln, err := cfg.NewListener(network, addr)
if err != nil && strings.Contains(err.Error(), "SO_REUSEPORT") {
return nil, &ErrNoReusePort{err}
}
return ln, err
}
var cfg = &tcplisten.Config{
ReusePort: true, // SO_REUSEPORT ------ 端口复用核心
DeferAccept: true, // TCP_DEFER_ACCEPT ------ 等有数据才 accept
FastOpen: true, // TCP_FASTOPEN ------ 快速握手
}
一次性开了 3 个性能选项,不只是端口复用。
兜底说明:
Listen里若NewListener报错且错误信息含SO_REUSEPORT,会返回明确的ErrNoReusePort错误------用于在系统不支持SO_REUSEPORT时给出可辨识的失败原因,而不是把底层报错直接抛给调用方。
7.2 完全绕开 Go 标准库,手工执行 socket 生命周期
tcplisten.NewListener(tcplisten.go:51)没有走 net.Listen,而是手动执行整个流程:
go
socket(SOCK_STREAM)
→ setsockopt(SO_REUSEADDR=1) # 快速复用 TIME_WAIT
→ setsockopt(TCP_NODELAY=1) # 关 Nagle
→ setsockopt(SO_REUSEPORT=1) # ★ 端口复用核心
→ setsockopt(TCP_DEFER_ACCEPT=1) # 等有数据才 accept
→ setsockopt(TCP_FASTOPEN=1) # TFO 快速握手
→ bind(addr) → listen(somaxconn)
→ net.FileListener 包回 net.Listener
7.3 为什么要手工走系统调用?
Go 标准库的 net.Listen 只在内部设 SO_REUSEADDR,不暴露 SO_REUSEPORT 。要复用端口,就必须在 bind() 之前自己设好 SO_REUSEPORT,所以只能手工控制 socket 创建的全过程。
八、完整调用链(mermaid)
#mermaid-svg-ypmDw3HAKKmiauTg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ypmDw3HAKKmiauTg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ypmDw3HAKKmiauTg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ypmDw3HAKKmiauTg .error-icon{fill:#552222;}#mermaid-svg-ypmDw3HAKKmiauTg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ypmDw3HAKKmiauTg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ypmDw3HAKKmiauTg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ypmDw3HAKKmiauTg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ypmDw3HAKKmiauTg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ypmDw3HAKKmiauTg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ypmDw3HAKKmiauTg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ypmDw3HAKKmiauTg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ypmDw3HAKKmiauTg .marker.cross{stroke:#333333;}#mermaid-svg-ypmDw3HAKKmiauTg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ypmDw3HAKKmiauTg p{margin:0;}#mermaid-svg-ypmDw3HAKKmiauTg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ypmDw3HAKKmiauTg .cluster-label text{fill:#333;}#mermaid-svg-ypmDw3HAKKmiauTg .cluster-label span{color:#333;}#mermaid-svg-ypmDw3HAKKmiauTg .cluster-label span p{background-color:transparent;}#mermaid-svg-ypmDw3HAKKmiauTg .label text,#mermaid-svg-ypmDw3HAKKmiauTg span{fill:#333;color:#333;}#mermaid-svg-ypmDw3HAKKmiauTg .node rect,#mermaid-svg-ypmDw3HAKKmiauTg .node circle,#mermaid-svg-ypmDw3HAKKmiauTg .node ellipse,#mermaid-svg-ypmDw3HAKKmiauTg .node polygon,#mermaid-svg-ypmDw3HAKKmiauTg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ypmDw3HAKKmiauTg .rough-node .label text,#mermaid-svg-ypmDw3HAKKmiauTg .node .label text,#mermaid-svg-ypmDw3HAKKmiauTg .image-shape .label,#mermaid-svg-ypmDw3HAKKmiauTg .icon-shape .label{text-anchor:middle;}#mermaid-svg-ypmDw3HAKKmiauTg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ypmDw3HAKKmiauTg .rough-node .label,#mermaid-svg-ypmDw3HAKKmiauTg .node .label,#mermaid-svg-ypmDw3HAKKmiauTg .image-shape .label,#mermaid-svg-ypmDw3HAKKmiauTg .icon-shape .label{text-align:center;}#mermaid-svg-ypmDw3HAKKmiauTg .node.clickable{cursor:pointer;}#mermaid-svg-ypmDw3HAKKmiauTg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ypmDw3HAKKmiauTg .arrowheadPath{fill:#333333;}#mermaid-svg-ypmDw3HAKKmiauTg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ypmDw3HAKKmiauTg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ypmDw3HAKKmiauTg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ypmDw3HAKKmiauTg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ypmDw3HAKKmiauTg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ypmDw3HAKKmiauTg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ypmDw3HAKKmiauTg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ypmDw3HAKKmiauTg .cluster text{fill:#333;}#mermaid-svg-ypmDw3HAKKmiauTg .cluster span{color:#333;}#mermaid-svg-ypmDw3HAKKmiauTg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ypmDw3HAKKmiauTg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ypmDw3HAKKmiauTg rect.text{fill:none;stroke-width:0;}#mermaid-svg-ypmDw3HAKKmiauTg .icon-shape,#mermaid-svg-ypmDw3HAKKmiauTg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ypmDw3HAKKmiauTg .icon-shape p,#mermaid-svg-ypmDw3HAKKmiauTg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ypmDw3HAKKmiauTg .icon-shape .label rect,#mermaid-svg-ypmDw3HAKKmiauTg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ypmDw3HAKKmiauTg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ypmDw3HAKKmiauTg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ypmDw3HAKKmiauTg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 操作系统内核
github.com/valyala/tcplisten v1.0.0
infini.sh/framework 仓库
infini.sh/gateway 仓库
true
false
proxy/gateway.go
热更新三阶段切换
applyDefaultReusePort 默认开
proxy/entry/entry.go
Entrypoint.Start()
NetworkConfig.
ReusePortEnabled()?
lib/fasthttp/reuseport
Listen(network, addr)
tcplisten.Config
{ReusePort, DeferAccept, FastOpen}
NewListener()
fdSetup():
SetsockoptInt(...)
socket() → setsockopt → bind() → listen()
内核 reuseport group
按四元组哈希分发连接
net.Listen 普通监听
socket 选项设置顺序(时序图)
系统内核 socket tcplisten.NewListener reuseport.Listen gateway Entrypoint 系统内核 socket tcplisten.NewListener reuseport.Listen gateway Entrypoint #mermaid-svg-ou8GVrC9reNWvIwj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ou8GVrC9reNWvIwj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ou8GVrC9reNWvIwj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ou8GVrC9reNWvIwj .error-icon{fill:#552222;}#mermaid-svg-ou8GVrC9reNWvIwj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ou8GVrC9reNWvIwj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ou8GVrC9reNWvIwj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ou8GVrC9reNWvIwj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ou8GVrC9reNWvIwj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ou8GVrC9reNWvIwj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ou8GVrC9reNWvIwj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ou8GVrC9reNWvIwj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ou8GVrC9reNWvIwj .marker.cross{stroke:#333333;}#mermaid-svg-ou8GVrC9reNWvIwj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ou8GVrC9reNWvIwj p{margin:0;}#mermaid-svg-ou8GVrC9reNWvIwj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ou8GVrC9reNWvIwj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ou8GVrC9reNWvIwj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ou8GVrC9reNWvIwj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ou8GVrC9reNWvIwj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ou8GVrC9reNWvIwj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ou8GVrC9reNWvIwj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ou8GVrC9reNWvIwj .sequenceNumber{fill:white;}#mermaid-svg-ou8GVrC9reNWvIwj #sequencenumber{fill:#333;}#mermaid-svg-ou8GVrC9reNWvIwj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ou8GVrC9reNWvIwj .messageText{fill:#333;stroke:none;}#mermaid-svg-ou8GVrC9reNWvIwj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ou8GVrC9reNWvIwj .labelText,#mermaid-svg-ou8GVrC9reNWvIwj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ou8GVrC9reNWvIwj .loopText,#mermaid-svg-ou8GVrC9reNWvIwj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ou8GVrC9reNWvIwj .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ou8GVrC9reNWvIwj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ou8GVrC9reNWvIwj .noteText,#mermaid-svg-ou8GVrC9reNWvIwj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ou8GVrC9reNWvIwj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ou8GVrC9reNWvIwj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ou8GVrC9reNWvIwj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ou8GVrC9reNWvIwj .actorPopupMenu{position:absolute;}#mermaid-svg-ou8GVrC9reNWvIwj .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ou8GVrC9reNWvIwj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ou8GVrC9reNWvIwj .actor-man circle,#mermaid-svg-ou8GVrC9reNWvIwj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ou8GVrC9reNWvIwj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Listen("tcp4", ":8000")1cfg.NewListener(network, addr)2newSocketCloexec(SOCK_STREAM, IPPROTO_TCP)3Setsockopt SO_REUSEADDR = 14Setsockopt TCP_NODELAY = 15Setsockopt SO_REUSEPORT = 16Setsockopt TCP_DEFER_ACCEPT = 17Setsockopt TCP_FASTOPEN = 1 (qlen 16K)8Bind(sockaddr)9Listen(fd, somaxconn)10net.FileListener 包装成 net.Listener11返回 listener12
九、技术细节
1. SO_REUSEPORT 在 Linux 是魔法数字 0x0F
tcplisten_linux.go:15:
go
const soReusePort = 0x0F
BSD 系(tcplisten_bsd.go)直接 syscall.SO_REUSEPORT。Linux 当年 syscall 包没导出该常量,所以硬编码 0x0F。
2. backlog 从内核读取
soMaxConn() 读 /proc/sys/net/core/somaxconn,超过 65535 截断(Linux backlog 是 uint16)。fastopen 队列 fastOpenQlen = 16*1024。
3. 平台差异
framework 库按编译平台分文件实现:
| 文件 | 编译条件 | 实际设的选项 |
|---|---|---|
tcplisten_linux.go |
linux | SO_REUSEPORT(0x0F) + TCP_DEFER_ACCEPT + TCP_FASTOPEN |
tcplisten_bsd.go |
darwin/freebsd/netbsd/openbsd | SO_REUSEPORT(DEFER_ACCEPT/FASTOPEN 空实现 TODO) |
reuseport_windows.go |
windows | 只有 SO_REUSEADDR,注释明确「Windows 不支持 SO_REUSEPORT,用 SO_REUSEADDR 替代」 |
reuseport_aix.go |
aix | SO_REUSEADDR + SO_REUSEPORT 两个都设 |
十、关键
结论
- gateway 的端口复用 =
SO_REUSEPORT(Linux 3.9+),目的是配置热更新零停机,默认全局开启。 - 实现链路:
reuse_port(*bool指针区分三态)→framework/reuseport.Listen→valyala/tcplisten手工socket + setsockopt(SO_REUSEPORT) + bind + listen。
平台差异
- Windows 上
reuse_port: true实际退化成SO_REUSEADDR,语义已变:不是内核负载均衡的多进程共享,只是 TIME_WAIT 快速复用。 gateway.yml里disable_reuse_port_by_default注释印证:「If the host doesn't support SO_REUSEPORT, settrueto disable this behavior」。
也就是说:在 Linux 上部署才有真正的端口复用与零停机切换;在 Windows 上 reuse_port 名存实亡,只是把选项「模拟」出来避免报错。
十一、总结
gateway 的 reuse_port → framework/reuseport.Listen → valyala/tcplisten 手工 socket + setsockopt(SO_REUSEPORT) + bind + listen,在 Linux 上真正实现了内核级端口复用与负载均衡(服务于配置热更新零停机切换);Windows 上则退化为 SO_REUSEADDR。