7.7 KiB
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.gologic/service/fight/pvp/service.gologic/service/fight/pvpwire/types.gologic/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 做响应分发
所以它本身支持并发复用同一条连接。
但原实现有两个风险:
-
client.setupRequestChan()里的requests是无缓冲通道
当handleWsConn主循环发送不过来时,调用方会在“写入请求通道”这一步被卡住。 -
业务调用没有统一超时
即使底层连接还能用,某个慢 RPC 也可能把业务 goroutine 长时间挂住。
这不代表“完全扛不住”,但高并发下会更容易出现请求堆积和业务侧等待放大。
2.3 重连后 URL 会不会复用
会复用。
当前 websocket client 在初始化时把地址保存在 connFactory 中,重连时走的还是同一个 addr:
common/utils/go-jsonrpc/client.gowebsocketClient(...)中构造connFactorycommon/utils/go-jsonrpc/websocket.gotryReconnect(...)中再次调用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.gologic/controller/Controller.gologic/controller/login_main.gologic/controller/fight_巅峰.go
改动内容:
KickRegisterLogicMatchJoinOrUpdateMatchCancel
统一改成带 context.Context 的签名。
新增:
common/rpc/rpc.goClientCallTimeout = 5 * time.Second
调用侧现在会显式设置超时,避免业务 goroutine 无限等待。
3.2 连接握手阶段直接注册 logic 身份
修改文件:
common/rpc/rpc.gocommon/utils/go-jsonrpc/server.gocommon/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 复用”的最终确认
最终确认如下:
RPCClient建立时会把目标rpcaddr固定到connFactory- 这个
rpcaddr现在已经带上logic_id/logic_port - 连接断开后,
tryReconnect(...)继续使用这个connFactory - 新连接建立后,服务端在握手阶段直接按 URL 参数注册 reverse client
- 原
RPCClient后续 RPC 继续走新连接
所以这里是:
- 复用同一个目标 URL
- 复用同一个
RPCClient - 复用同一个请求分发模型
不是:
- 重连后靠额外发一次
RegisterLogic - 才让后续 RPC 可用
现在身份识别和注册已经前置到连接握手本身。
6. 还没解决的风险
6.1 连接级串行写仍然存在
虽然请求可以并发入队,但真正写 websocket 还是串行的。
如果将来 login 侧 RPC 量继续上升,还是可能需要继续做:
- 更细的调用隔离
- 独立连接池
- 或把部分强同步调用改为异步消息
6.2 1024 缓冲不是容量上限方案
当前只是经验值,不是经过压测得出的最终值。
如果峰值比预期高,还可能继续积压。
6.3 业务上仍然是同步等待模式
比如:
- 登录踢人
- 匹配加入
仍然依赖 RPC 成功/失败来推进。
只是现在不会无限挂死,但高峰期延迟仍可能直接体现在业务响应上。
7. 验证情况
已完成:
gofmt已执行go test ./rpcincommon通过go test -run '^$' .incommon/utils/go-jsonrpc通过
受环境限制未完整确认:
common/utils/go-jsonrpc全量测试在 sandbox 下需要本地监听端口logic模块测试受当前环境/proc/sys/kernel/osrelease读取失败影响,无法作为本轮改动的有效回归结论
另外,common 模块全量 go test ./... 还会被仓库内已有的 fmt.Println("%.2f") 这类历史问题拦住,与本次 RPC 改动无关。
8. 建议的下一步
如果后面还要继续收敛这块,建议优先级如下:
- 给 login 侧关键 RPC 增加更明确的耗时日志和超时日志
- 对
requests队列积压增加指标或告警 - 评估
Kick和匹配 RPC 是否需要拆连接 - 如果 login 压力继续上涨,再考虑把部分同步入口改成异步投递 + 状态查询