Post

记录一次 Tokio 异步场景下 RPC 接口 timeout 的排查

记录一次 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 隔离可以止血,但更优雅的方案还是要回到架构本身: 把同步重任务和异步请求处理彻底解耦。

This post is licensed under CC BY 4.0 by the author.