277 lines
7.7 KiB
Go
277 lines
7.7 KiB
Go
# 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 压力继续上涨,再考虑把部分同步入口改成异步投递 + 状态查询
|