optimize2ai.md - 文档

AI 代码审查优化记录

两轮 AI 代码审查对 gatewayworker-go 的所有修复和优化,按严重程度排序。


第一轮:核心 Bug 修复 + 基础优化

🔴 #1 并发读 map → fatal panic

文件: pkg/gateway/gateway.go L357

问题: sendToWorker 释放 RLock 后,无锁访问 g.workerConns[selectedKey]。Go runtime 检测到并发 map 读写会直接 fatal crash,不可 recover。

-	return g.sendEncrypted(g.workerConns[selectedKey], encoded)
+	g.mu.RLock()
+	wConn := g.workerConns[selectedKey]
+	g.mu.RUnlock()
+	return g.sendEncrypted(wConn, encoded)

🔴 #2 连接池初始化竞态条件

文件: pkg/gateway_api/gateway_conn.go L207-211

问题: ensurePool() 多 goroutine 同时调用时,pool == nil 检查非原子操作,可能多次初始化或读到半初始化的 pool。

-var pool *queryConnPool
-func ensurePool() {
-	if pool == nil {
-		initPool()
-	}
-}
+var (
+	pool     *queryConnPool
+	poolOnce sync.Once
+)
+func ensurePool() {
+	poolOnce.Do(initPool)
+}

🟡 #3 sync.Map 类型断言无 ok 检查

文件: pkg/register/register.go L218, L236

问题: value.(net.Conn) 如果类型不匹配会 panic。虽然当前逻辑正确,但缺乏防御性。

-	value.(net.Conn).Write([]byte(line))
+	if conn, ok := value.(net.Conn); ok {
+		conn.Write([]byte(line))
+	}

🟡 #4 gateway_api 入口无 nil 检查

文件: pkg/gateway_api/gateway_api.go L29, L39

问题: 如果忘记调用 SetBusinessWorker()bw 为 nil,所有 API 调用直接 panic。

 func sendCmd(clientID string, cmd uint8, message []byte, extData string) {
+	if bw == nil {
+		return
+	}
 	...

🟢 #5 sendEncrypted 热路径减少分配 (×5 处)

文件: gateway.go, business_worker.go, gateway_conn.go, gateway_worker_handler.go

问题: 每次发送消息都 make(4) + append(lenBuf, encrypted...),产生 2 次堆分配。这是每条消息都经过的热路径。

-	lenBuf := make([]byte, 4)
-	binary.BigEndian.PutUint32(lenBuf, uint32(len(encrypted)))
-	_, err = conn.Write(append(lenBuf, encrypted...))
+	buf := make([]byte, 4+len(encrypted))
+	binary.BigEndian.PutUint32(buf[:4], uint32(len(encrypted)))
+	copy(buf[4:], encrypted)
+	_, err = conn.Write(buf)

效果: 每条消息减少 1 次堆分配。高并发下(10 万 msg/s)减少约 10 万次/秒的 GC 压力。


第二轮:锁优化 + 内存安全

🔴 #6~#8 广播方法持锁执行 I/O

文件: gateway_worker_handler.gohandleSendToAll, handleSendToUID, handleSendToGroup

问题: 持 RLock 调用 cc.Conn.Write()。WebSocket 的 Write 可能因客户端慢消费而阻塞数秒。持锁期间所有写操作(BindUID、JoinGroup、LeaveGroup)全部排队等待。

万级连接广播时锁可能持有数秒,导致整个 Gateway 卡顿。

-	g.mu.RLock()
-	defer g.mu.RUnlock()
-	for _, cc := range g.clientConns {
-		cc.Conn.Write(data.Body)
-	}
+	var targets []ClientConn
+	g.mu.RLock()
+	for _, cc := range g.clientConns {
+		targets = append(targets, cc.Conn)
+	}
+	g.mu.RUnlock()
+	// 锁外批量写
+	for _, conn := range targets {
+		conn.Write(data.Body)
+	}

效果: 锁持有时间从 O(N×网络延迟) 降到 O(N×指针拷贝) ≈ 微秒级。


🟢 #9 心跳包重复编码

文件: pkg/gateway/gateway.gopingWorkerLoop

问题: 每 25 秒对每个 Worker 都 NewEmptyData() + Encode(),生成完全一样的心跳包。

+	pingGd := protocol.NewEmptyData()
+	pingGd.Cmd = protocol.CmdPing
+	pingPayload := protocol.Encode(pingGd)
+
 	for _, wconn := range g.workerConns {
-		gd := protocol.NewEmptyData()
-		gd.Cmd = protocol.CmdPing
-		g.sendEncrypted(wconn, protocol.Encode(gd))
+		g.sendEncrypted(wconn, pingPayload)
 	}

🟡 #10 TextProtocol.Encode append 污染

文件: pkg/gateway/protocol_text.go

问题: append(data, '\n') 如果 data 底层数组有剩余 capacity,会直接改写调用方的底层数据,导致隐蔽的数据损坏。

-	return append(data, '\n')
+	buf := make([]byte, len(data)+1)
+	copy(buf, data)
+	buf[len(data)] = '\n'
+	return buf

🟡 #11 TCPClientConn.Read 缓冲区内存泄漏

文件: pkg/gateway/client_conn.go

问题: c.buf = c.buf[n:] 反复 reslice 后,底层数组前端已消费的字节永远不会被 GC 回收。长时间运行的 TCP 连接会逐渐泄漏内存。

-	c.buf = c.buf[n:]
+	remaining := len(c.buf) - n
+	if remaining == 0 {
+		c.buf = c.buf[:0]
+	} else {
+		newBuf := make([]byte, remaining)
+		copy(newBuf, c.buf[n:])
+		c.buf = newBuf
+	}

🟡 #12 gateway_sdk 认证加密忽略错误

文件: pkg/gateway_sdk/gateway_sdk.go L117

问题: encrypted, _ := crypto.Encrypt(...) 忽略了加密错误,如果 aesKey 有问题会发送空数据。

-	encrypted, _ := crypto.Encrypt(protocol.Encode(authData), c.aesKey)
+	encrypted, err := crypto.Encrypt(protocol.Encode(authData), c.aesKey)
+	if err != nil {
+		conn.Close()
+		return nil, fmt.Errorf("encrypt auth failed: %w", err)
+	}

🟢 #13 gateway_sdk 3 处 append 优化

文件: pkg/gateway_sdk/gateway_sdk.gogetConn, sendToGateway, sendAndRecv

与 #5 相同的 append(lenBuf, encrypted...) 模式,统一改为 make+copy


第一二轮修复统计

等级 数量 说明
🔴 必修(会导致 crash/卡死) 5 并发 map panic、竞态初始化、锁下 I/O 阻塞
🟡 应修(防御性 / 内存安全) 5 类型断言、nil 检查、append 污染、内存泄漏、错误处理
🟢 优化(性能提升) 3 热路径分配、心跳复用、gateway_sdk 一致性
总计 13

未改动的项(不值得改)

代码 理由
addrStrfmt.Sprintf 非热路径,改 strconv 收益 < 1%
protocol.Encode 每次 make 不同调用编码不同数据,必须独立分配
register.go 字符串 + 认证时执行一次
*net.TCPAddr 断言不加 ok TCP accept 的连接一定是 TCPAddr
FrameProtocol.Encode 的 append 客户端协议编码,频率远低于内部通讯
handleSelect 锁内 json.Marshal 纯 CPU 操作不涉及 I/O,不阻塞
用户回调不加 recover Go 惯例让 panic 暴露,方便调试

第三轮:GatewaySDK 深度审查

针对 pkg/gateway_sdk/gateway_sdk.go 的专项审查。

🔴 #14 sendAndRecv 恶意 rLen 导致 OOM panic

文件: pkg/gateway_sdk/gateway_sdk.gosendAndRecv

问题: 收到 rLen = 4GB 时,make([]byte, rLen) 直接 OOM panic,无法 recover。

 rLen := binary.BigEndian.Uint32(rLenBuf)
+if rLen > protocol.MaxEncryptedPacketSize {
+    c.evictConn(addr)
+    return nil, fmt.Errorf("response too large: %d bytes", rLen)
+}
 ciphertext := make([]byte, rLen)

🔴 #15 sendAndRecv 读失败不踢连接 → 连接池"僵尸连接"

文件: pkg/gateway_sdk/gateway_sdk.gosendAndRecv

问题: io.ReadFull 失败后,死连接留在 pool 中。后续所有请求都走这条死连接,持续失败直到 50s TTL。

 if _, err = io.ReadFull(conn, rLenBuf); err != nil {
+    c.evictConn(addr)
     return nil, err
 }

同时对 ReadFull(conn, ciphertext) 也做了相同修复。


🔴 #16 getGatewayAddresses 持锁做网络 I/O

文件: pkg/gateway_sdk/gateway_sdk.gogetGatewayAddresses

问题: mu.Lock() 持有期间执行 net.DialTimeout(3s 超时)+ bufio.Scanner(5s 超时),所有其他 goroutine 全部阻塞。

-c.mu.Lock()
-defer c.mu.Unlock()
-// ... 整个函数都在锁内 ...

+c.mu.Lock()
+if 缓存有效 { Unlock; return cached }
+oldCache := c.addrCache
+c.mu.Unlock()
+// 锁外执行网络 I/O
+for _, regAddr := range c.RegisterAddr { ... }
+// 成功后 Lock 更新缓存
+c.mu.Lock()
+c.addrCache = resp.Addresses
+c.mu.Unlock()

🟡 #17 getConn 认证 Write 错误被忽略

文件: pkg/gateway_sdk/gateway_sdk.gogetConn

问题: conn.Write(buf) 的返回值被丢弃,写入失败的连接仍被放入 connPool。

-conn.Write(buf)
+if _, err = conn.Write(buf); err != nil {
+    conn.Close()
+    return nil, fmt.Errorf("write auth failed: %w", err)
+}

🟢 #18 GetAllClientCount 串行查询

文件: pkg/gateway_sdk/gateway_sdk.goGetAllClientCount

问题: 逐个 Gateway 串行查询,而其他查询方法已使用 queryAllGateways 并发查询。

-for _, addr := range addrs {
-    resp, err := c.sendAndRecv(addr, gd)
-    ...
-}
+results := c.queryAllGateways(gd)
+for _, r := range results { ... }

🟢 #19 evictConn 统一提取 + 连接 Close

问题: 原先 sendToGatewaysendAndRecv 的错误路径中,delete(c.connPool, addr) 但不 Close 连接,导致文件描述符泄漏。

+func (c *GatewaySDK) evictConn(addr string) {
+    c.mu.Lock()
+    if e, ok := c.connPool[addr]; ok {
+        e.conn.Close()
+        delete(c.connPool, addr)
+    }
+    c.mu.Unlock()
+}

🟢 #20 MaxEncryptedPacketSize 常量提取

问题: 50*1024*1024 魔法数字散落在 4 个文件中(gateway.go、business_worker.go、gateway_conn.go、gateway_sdk.go)。

修复: 提取到 protocol.MaxEncryptedPacketSize 常量,修改一处全局生效。


第三轮总结

等级 数量 说明
🔴 必修(crash / 阻塞) 3 OOM panic、僵尸连接、锁下 I/O
🟡 应修(静默失败) 1 认证写入错误忽略
🟢 优化(性能 / 可维护) 3 并发查询、evictConn 提取、常量统一
本轮小计 7

三轮累计

等级 累计
🔴 必修 8
🟡 应修 6
🟢 优化 6
总计 20