记录一次 Tokio 异步场景下 RPC 接口 timeout 的排查
在排查一个线上问题的过程中,对 Tokio 有了更深入的理解。
问题现象
服务部署在 K8s Pod 中,运行时主要包含两部分:
- 对外提供实时数据查询的 RPC 接口;
- 后台持续执行的数据处理任务,用来更新内部状态,保证 RPC 返回结果的正确性。
这两部分都运行在 Tokio 异步 runtime 上。上线后发现,客户端循环请求 RPC 接口时,会概率性出现 timeout。
排查过程
一开始先怀疑是 K8s 网络问题。简单排查后,虽然链路上偶尔有一些不稳定现象,但不足以解释当前这个问题。
为了缩小范围,我在项目里临时加了一个测试 RPC 接口,只做最简单的计算逻辑:接收一个数字并返回 x2。同时暂停后台数据处理任务,继续对这个测试接口做高频访问。
结果很直接:测试 RPC 响应非常稳定,没有再出现 timeout。
这说明问题大概率不在 RPC 框架本身,也不只是网络问题,而是后台任务运行时干扰了 RPC 请求的处理。
一开始的误判
最开始我怀疑过是不是内部锁竞争导致的阻塞,但后面发现不太像。
因为 timeout 发生时,更像是 RPC Server 根本没有及时处理到请求,而不是请求进入业务逻辑后卡在某个锁上。也就是说,问题更可能出在任务调度层,而不是单纯的业务临界区竞争。
原因分析
继续看项目实现后发现,RPC Server 和后台数据处理任务共用了同一个 Tokio runtime。
而后台异步任务里,其实混入了一些高 IO 的同步操作,比如构建索引这类工作。问题的关键不只是“任务重”,而是:
同步操作和异步操作没有解耦,导致本应非阻塞的异步运行时,被高 IO 的同步任务占住了执行资源。
在这种情况下,后台任务虽然表面上是通过 async 组织起来的,但其中实际执行的同步逻辑仍然会长时间占用 worker 线程。最终结果就是:
- 后台任务持续挤占 runtime 资源;
- RPC 请求得不到及时调度;
- 客户端表现为概率性 timeout。
临时修复
为了验证判断,我先做了资源隔离:
- 将 RPC Server 放到单独线程中运行;
- 为它创建独立的 Tokio runtime;
- 后台任务继续跑在原来的 runtime 上。
隔离之后,RPC timeout 问题就消失了。
这至少说明,问题方向判断是对的:请求路径和后台任务路径之间存在明显的运行时资源竞争。
更优雅的解决方式
不过更合理的方案,其实不是简单拆两个 runtime,而是先把模型理顺:
- 异步逻辑继续留在 Tokio runtime 中;
- 高 IO / 高耗时的同步任务明确下沉到独立线程池或
spawn_blocking; - 避免把同步重任务直接塞进 async 任务链路里。
更本质地说,这个问题不是 Tokio 本身有问题,而是项目里同步和异步职责没有分清。当同步操作被包装在异步任务内部时,运行时调度就很容易被拖垮,最终首先受影响的,通常就是对延迟最敏感的 RPC 接口。
小结
这次排查最后确认了一点:
RPC timeout 不是单纯的网络问题,也不是简单的锁竞争,而是后台异步任务中混入了高 IO 的同步操作,导致共享 Tokio runtime 的调度资源被挤占。
临时做 runtime 隔离可以止血,但更优雅的方案还是要回到架构本身: 把同步重任务和异步请求处理彻底解耦。