You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang代理服务器动态切换端点的同步问题排查与修复咨询

动态切换后端的Golang代理服务器同步问题分析

问题描述

开发一款可动态切换后端端点的Golang代理服务器,调用switchOverToNewEndpoint()切换到新端点1.1.1.1后,仍有部分代理对象保持与原端点8.8.8.8的连接未被关闭。

完整代码

package main

import (
    "net"
    "sync"
    "sync/atomic"
    "time"
)

type Proxy struct {
    ID       int32
    From, To *net.TCPConn
}

var switchOver int32 = 0

func SetSwitchOver() {
    atomic.StoreInt32((*int32)(&switchOver), 1)
}

func SwitchOverEnabled() bool {
    return atomic.LoadInt32((*int32)(&switchOver)) == 1
}

var proxies map[int32]*Proxy = make(map[int32]*Proxy, 0)
var proxySeq int32 = 0
var mu sync.RWMutex

func addProxy(from *net.TCPConn) {
    mu.Lock()
    proxySeq += 1
    proxy := &Proxy{ID: proxySeq, From: from}
    proxies[proxySeq] = proxy
    mu.Unlock()

    var toAddr string
    if SwitchOverEnabled() {
        toAddr = "1.1.1.1"
    } else {
        toAddr = "8.8.8.8"
    }
    tcpAddr, _ := net.ResolveTCPAddr("tcp4", toAddr)
    toConn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        panic(err)
    }
    proxy.To = toConn
}

func switchOverToNewEndpoint() {
    mu.RLock()
    closedProxies := proxies
    mu.RUnlock()

    SetSwitchOver()
    for _, proxy := range closedProxies {
        proxy.From.Close()
        proxy.To.Close()
        mu.Lock()
        delete(proxies, proxy.ID)
        mu.Unlock()
    }
}

func main() {
    tcpAddr, _ := net.ResolveTCPAddr("tcp4", "0.0.0.0:5432")
    ln, _ := net.ListenTCP("tcp", tcpAddr)
    go func() {
        time.Sleep(time.Second * 30)
        switchOverToNewEndpoint()
    }()
    for {
        clientConn, err := ln.AcceptTCP()
        if err != nil {
            panic(err)
        }
        go addProxy(clientConn)
    }
}

问题根源分析

你推测的代码段确实是核心问题所在:

mu.RLock()
closedProxies := proxies
mu.RUnlock()

Go中的map是引用类型,上述代码只是复制了map的引用,并未创建副本。后续遍历closedProxies时,若addProxy正在添加新代理,或switchOverToNewEndpoint中的delete操作修改了原map,会触发map并发读写竞态,导致遍历遗漏部分代理对象,最终这些未被遍历到的代理会保留与原端点的连接。

而你计划替换的代码是正确的修复方案:

closedProxies := make([]*Proxy, 0)
mu.RLock()
for _, proxy := range proxies {
    closedProxies = append(closedProxies, proxy)
}
mu.RUnlock()

这段代码在读锁保护下,将当前所有代理对象复制到独立切片中。后续遍历切片时,即使原map被修改,也不会影响遍历结果,确保切换前存在的所有代理都能被关闭和清理。

额外潜在问题

除了上述核心问题,代码中还有其他需要修复的点:

  • proxySeq竞态:proxySeq +=1未使用原子操作,并发场景下会出现ID重复或丢失,应替换为atomic.AddInt32(&proxySeq, 1)。
  • 连接失败的资源泄漏:addProxy先将代理加入map再建立后端连接,若连接失败触发panic,已加入map的代理无法被清理,导致资源泄漏。应调整逻辑:先建立后端连接,成功后再将代理加入map。
  • 切换时序问题:当前先调用SetSwitchOver()再关闭旧连接,会导致切换过程中新连接直接连到新端点,但旧连接仍在处理。若需严格的切换原子性,应先收集所有旧代理,再设置切换标记,最后关闭旧连接。

验证建议

  1. 启用竞态检测:运行代码时添加-race参数:go run -race main.go,原代码的map引用复制+并发修改会触发明确的竞态警告,修复后警告消失。
  2. 并发场景测试:编写批量客户端连接的测试用例,模拟高并发下的切换操作,检查切换后原端点的连接数是否为0,或proxiesmap是否已清空旧代理。
  3. 日志验证:在addProxy和switchOverToNewEndpoint中添加日志,记录代理的端点信息和关闭状态,排查是否有遗漏的代理未被处理。

内容的提问来源于stack exchange,提问作者HFX

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 18:47:42