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

277 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RPC 阻塞并发承载与重连复用检查
日期2026-04-27
## 1. 检查范围
本次主要检查以下两层
- 业务 RPC 封装`common/rpc/rpc.go`
- 底层 websocket JSON-RPC`common/utils/go-jsonrpc/client.go`
- 底层 websocket 重连循环`common/utils/go-jsonrpc/websocket.go`
- 业务调用点`logic/controller/login_main.go``logic/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 会不会阻塞
当前业务侧 `Kick``MatchJoinOrUpdate``MatchCancel``RegisterLogic` 都是同步 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 压力继续上涨再考虑把部分同步入口改成异步投递 + 状态查询