INFINI Gateway 服务器端口重用(SO_REUSEPORT)实现与分析

第一部分:什么是端口重用

一、先建立两个前提概念

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 存在的两个目的:

  1. 保证最后一个 ACK 能被对方收到(如果 ACK 丢失,对方会重发 FIN,需要能再次应答)。
  2. 让旧连接的迟到报文在网络中「死掉」,避免同一个四元组被新连接复用后,收到上一个连接的残留数据。

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, set true to 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。

相关推荐
沫璃染墨1 小时前
从零入门计算机网络系列(十八):TCP连接管理详解——从三次握手与四次挥手到TIME_WAIT机制
运维·服务器·网络·c++·网络协议·tcp/ip·tcp
LabVIEW开发1 小时前
Omron PLC:接 LabVIEW 常踩的 5 个坑
服务器·网络·labview·labview知识·labview功能·labview程序
南京码讯光电技术有限公司2 小时前
工业5G/4G路由器怎么选?核心参数解析与工业级网关选购指南
网络·智能路由器
爱和冰阔落2 小时前
【Linux网络】JsonCpp 怎么实现序列化?从 Request/Response 到网络计算器消息设计
linux·运维·网络·php
feng_you_ying_li15 小时前
linux之数据链路层
linux·网络·智能路由器
国科安芯18 小时前
人在回路中的总线:商业空间站环境控制与生命保障系统CANFD网络的可靠性与功能安全
网络·嵌入式硬件·安全·架构·php·抗辐射
国科安芯19 小时前
算力上天之后的电与静:星上边缘计算载荷带外管理网络的低功耗调度与辐射容错
网络·人工智能·嵌入式硬件·安全·边缘计算·低轨卫星·商业航天
wdeok19 小时前
Socket编程UDP-简单的聊天室
网络·网络协议·udp
QXWZ_IA1 天前
定位数据怎么推给 IoT 平台?消息协议选型与接入准备
网络·人工智能·物联网·安全