Files
bl/docs/rpc-blocking-reconnect-review-2026-04-27.md
xinian 45f1485a11
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
feat: 支持跨服战斗原始cmd/data转发
2026-04-27 06:12:13 +08:00

7.7 KiB
Raw Blame History

RPC 阻塞、并发承载与重连复用检查

日期2026-04-27

1. 检查范围

本次主要检查以下两层:

  • 业务 RPC 封装:common/rpc/rpc.go
  • 底层 websocket JSON-RPCcommon/utils/go-jsonrpc/client.go
  • 底层 websocket 重连循环:common/utils/go-jsonrpc/websocket.go
  • 业务调用点:logic/controller/login_main.gologic/controller/fight_巅峰.go

另外同步看了本轮已有的跨服战斗转发改动:

  • logic/service/fight/pvp/proxy.go
  • logic/service/fight/pvp/service.go
  • logic/service/fight/pvpwire/types.go
  • logic/service/player/rpc.go

2. 结论

2.1 现在的 RPC 会不会阻塞

会。

当前业务侧 KickMatchJoinOrUpdateMatchCancelRegisterLogic 都是同步 RPC 调用。
调用 goroutine 会一直等到底层返回响应、连接错误,或者调用方自己的 context 超时。

原先的问题是:

  • 这些业务 RPC 方法没有 context.Context 入参
  • 调用方没法给单次 RPC 设置超时
  • 一旦对端卡住或网络异常,调用方可能长期挂住

这会直接影响:

  • 登录踢人流程
  • 巅峰匹配加入/取消
  • 重连后的逻辑服重新注册

2.2 并发会不会顶不住

结论是:底层支持多路并发,但原实现存在明显背压点。

底层 go-jsonrpc 的设计不是“单请求单连接”,而是:

  • 一个 RPCClient
  • 一个 websocket 连接
  • 多个请求共用一个 requests 通道
  • 通过请求 ID 做响应分发

所以它本身支持并发复用同一条连接。

但原实现有两个风险:

  1. client.setupRequestChan() 里的 requests 是无缓冲通道
    handleWsConn 主循环发送不过来时,调用方会在“写入请求通道”这一步被卡住。

  2. 业务调用没有统一超时
    即使底层连接还能用,某个慢 RPC 也可能把业务 goroutine 长时间挂住。

这不代表“完全扛不住”,但高并发下会更容易出现请求堆积和业务侧等待放大。

2.3 重连后 URL 会不会复用

会复用。

当前 websocket client 在初始化时把地址保存在 connFactory 中,重连时走的还是同一个 addr

  • common/utils/go-jsonrpc/client.go
  • websocketClient(...) 中构造 connFactory
  • common/utils/go-jsonrpc/websocket.go
  • tryReconnect(...) 中再次调用 c.connFactory()

也就是说:

  • 重连不是只发一次注册 RPC 就结束
  • 重连后不是一次性临时连接
  • 而是替换 wsConn.conn 为新连接
  • 后续 RPC 仍然继续复用同一个 RPCClient 和同一个目标 URL

2.4 重连后是不是必须再发一次注册 RPC

现在不是了。

当前实现已经改成:

  • 客户端在 websocket 建连 URL 上直接带 logic_id / logic_port
  • 服务端在握手阶段创建 reverse client 后,立刻根据 URL 参数完成 logic 注册
  • 连接断开时再根据同一身份清理 cool.Clientmap

这样重连时:

  • 仍然走同一个 URL
  • 新连接在握手阶段就知道 client 身份
  • 不需要再依赖重连成功后的二次 RegisterLogic(id, port) RPC

保留 RegisterLogic 只是兼容已有接口,不再是重连链路的必要步骤。

3. 本轮已做修改

3.1 给业务 RPC 增加显式超时能力

修改文件:

  • common/rpc/rpc.go
  • logic/controller/Controller.go
  • logic/controller/login_main.go
  • logic/controller/fight_巅峰.go

改动内容:

  • Kick
  • RegisterLogic
  • MatchJoinOrUpdate
  • MatchCancel

统一改成带 context.Context 的签名。

新增:

  • common/rpc/rpc.go
  • ClientCallTimeout = 5 * time.Second

调用侧现在会显式设置超时,避免业务 goroutine 无限等待。

3.2 连接握手阶段直接注册 logic 身份

修改文件:

  • common/rpc/rpc.go
  • common/utils/go-jsonrpc/server.go
  • common/utils/go-jsonrpc/options_server.go

行为调整:

  • 客户端建连 URL 直接携带 logic_id / logic_port
  • 服务端握手时把原始 *http.Request 放入 RPC 上下文
  • reverse client 建好后立即读取 URL 参数并注册 logic client
  • 连接关闭时按相同 key 自动清理 cool.Clientmap

这样重连后不需要额外补发一次注册 RPC。

3.3 底层请求通道增加缓冲

修改文件:

  • common/utils/go-jsonrpc/client.go

改动:

  • requests := make(chan clientRequest, 1024)

目的:

  • 把“调用方 goroutine 立刻卡在请求投递”这个点往后挪
  • handleWsConn 主循环留一个有限缓冲区

这不是彻底消除背压,只是把最硬的无缓冲阻塞改掉。

3.4 重连回调不再承担注册职责

修改文件:

  • common/rpc/rpc.go

改动:

  • 去掉 StartClient(...) 中依赖 WithReconnFun(...) 做补注册的逻辑

目的:

  • 让“谁是这个 logic client”变成握手时就已确定的连接属性
  • 避免重连后再发一笔注册 RPC

4. 这轮修改后的判断

4.1 RPC 还会不会阻塞

会,但现在阻塞是“有边界的同步等待”,不是“无上限死等”。

也就是:

  • 业务仍然是同步 RPC 模式
  • 但调用方现在有明确超时
  • 超时后能返回错误,不会无限挂住

4.2 并发有没有改善

有改善,但不是彻底做成高吞吐 RPC 网关。

现在比原来更稳的点:

  • 业务调用有超时
  • 请求投递通道有缓冲
  • 重连时不再额外补发一次注册 RPC

仍然保留的现实限制:

  • 单条 websocket 连接仍然只有一个写口
  • handleWsConn 仍是单主循环
  • 极端并发下仍会出现排队,只是不会像原来那样更早卡死

5. 关于“URL 复用”的最终确认

最终确认如下:

  1. RPCClient 建立时会把目标 rpcaddr 固定到 connFactory
  2. 这个 rpcaddr 现在已经带上 logic_id / logic_port
  3. 连接断开后,tryReconnect(...) 继续使用这个 connFactory
  4. 新连接建立后,服务端在握手阶段直接按 URL 参数注册 reverse client
  5. RPCClient 后续 RPC 继续走新连接

所以这里是:

  • 复用同一个目标 URL
  • 复用同一个 RPCClient
  • 复用同一个请求分发模型

不是:

  • 重连后靠额外发一次 RegisterLogic
  • 才让后续 RPC 可用

现在身份识别和注册已经前置到连接握手本身。

6. 还没解决的风险

6.1 连接级串行写仍然存在

虽然请求可以并发入队,但真正写 websocket 还是串行的。
如果将来 login 侧 RPC 量继续上升,还是可能需要继续做:

  • 更细的调用隔离
  • 独立连接池
  • 或把部分强同步调用改为异步消息

6.2 1024 缓冲不是容量上限方案

当前只是经验值,不是经过压测得出的最终值。
如果峰值比预期高,还可能继续积压。

6.3 业务上仍然是同步等待模式

比如:

  • 登录踢人
  • 匹配加入

仍然依赖 RPC 成功/失败来推进。
只是现在不会无限挂死,但高峰期延迟仍可能直接体现在业务响应上。

7. 验证情况

已完成:

  • gofmt 已执行
  • go test ./rpc in common 通过
  • go test -run '^$' . in common/utils/go-jsonrpc 通过

受环境限制未完整确认:

  • common/utils/go-jsonrpc 全量测试在 sandbox 下需要本地监听端口
  • logic 模块测试受当前环境 /proc/sys/kernel/osrelease 读取失败影响,无法作为本轮改动的有效回归结论

另外,common 模块全量 go test ./... 还会被仓库内已有的 fmt.Println("%.2f") 这类历史问题拦住,与本次 RPC 改动无关。

8. 建议的下一步

如果后面还要继续收敛这块,建议优先级如下:

  1. 给 login 侧关键 RPC 增加更明确的耗时日志和超时日志
  2. requests 队列积压增加指标或告警
  3. 评估 Kick 和匹配 RPC 是否需要拆连接
  4. 如果 login 压力继续上涨,再考虑把部分同步入口改成异步投递 + 状态查询