FAQ
为什么 Client 端中间件使用 Arc?
细心的你会发现,我们在 Client 端的生成代码中,会把用户传入的 Req 包装成 Arc
这么设计的原因是因为,Client 端相比 Server 端,需要做更多复杂的服务治理逻辑,特别是有一些服务治理的逻辑和 Rust 的所有权模型是相悖的,比如:如果连接失败,那么就换个节点重试;甚至更复杂的超时重试等逻辑。如果我们直接使用 Req,那么当我们第一次执行 inner service 的 call 时,所有权就已经被 move 了,我们没有办法做重试逻辑。
同时,使用 Arc 还能帮助我们规避在 middleware 下并发访问带来的问题(如做 mirror/diff 等场景),在此不过多赘述。
并且,client 端本身不应该修改 Req,所以也不需要拿到可变的权限。
而在 server 端,不会有这么复杂的使用场景,并且最终是要把所有权给到用户的 handler 的,因此 server 端直接使用 Req 所有权即可。
为什么 volo-cli 生成的代码中要单独分拆成出 volo-gen crate?
因为 Rust 的编译是以 crate 为单元的,把生成代码单独作为一个 crate 可以更好地利用编译缓存(idl 一般不会经常变动)。
和 Kitex 的兼容性如何?
Volo 与 Kitex 完全兼容,包括元信息传递等功能。
poll_ready(背压)哪去了?
在 Tower 的 Service 中,有一个方法 poll_ready
,用来在请求之前先确定下游 Service 有足够的处理能力,并在处理能力不足时提供背压。
这是一个非常精妙的设计,Tower 在 inventing-the-service-trait 这篇介绍文章中,也有详细介绍这么设计的原因。
但是在我们真实的开发体验中,我们总结出了以下的经验:
- 绝大多数的
poll_ready
的实现都是直接self.inner.poll_ready(cx)
;剩下的 poll_ready 实现更干脆,直接Poll::Ready(Ok(()))
。 poll_ready
一般不会真正跨服务去 check 负载(也就是说,不会真的发个请求问下游“大兄弟,你还能支棱起来不?”),所以一般也就是在本地的中间件(比如 Tower 的例子是速率限制中间件)里面根据某些特定条件判断一下。- 基于上两条,几乎所有的
poll_ready
场景,我们都可以直接在call
里面做达到一样的效果,因为实践中外层的service
在返回Poll::Pending
的时候就是空等,不如直接采用async-await
的方式来编写代码,更符合人体工程学。 - 至于(可能的)资源浪费的问题,一般来说可能发生拦截的中间件,肯定是放在越前面越好,所以通过合理地排布中间件的顺序,就能解决这个问题。
因此,出于“如无必要勿增实体”的原则,也为了提升易用性,我们最终决定在我们的设计中不包含 poll_ready
方法。