0%

用 Rust 来开发 Envoy?

前两天,在 envoy-wasm Slack 群里看到,有个 Google 大佬 Yan,又在提把 Rust 搞到 Envoy 里来

这次他提到了 Google 的 crubit,这个 C++ Rust 双向互操作工具,看着还蛮有意思的样子,简单记录下的

用 Rust 开发 Envoy

我印象中,这个在 Envoy 社区其实一直有讨论,包括 Envoy 的创始大佬 Matt 也是 Rust 的粉丝

有两个方向,一个是用 Rust 来写 Envoy 核心,另一个则是写扩展

前者是很激进的,我还没看到有人在尝试,后者则相对容易一些,至少可以看到 snowp 大佬尝试的 PR:用 Rust 来写了个 echo 的 demo,https://github.com/envoyproxy/envoy/pull/25409

哈哈,有点类似于,当年春哥给 Nginx 写的 echo-nginx-module:https://github.com/openresty/echo-nginx-module

不过,这个尝试已经弃坑了,Rust 与 Envoy 这种大型 C++ 交互,并不那么简单

Why not Wasm

Rust 可以编译为 Wasm,并且 Wasm 在 Envoy 也已经有集成,为什么还有人想折腾 Rust 原生扩展呢

群里大佬们也说了不少,结合我的理解聊聊:

  1. Wasm 需要拷贝内存

    Wasm 也有内存安全机制,字节码读写 VM 内的线性内存地址,所以拷贝是少不了的,现实的实现中,一次读取可能会有多次拷贝

  2. API 能力有限

    必须 proxy-wasm-cpp-sdk 包一层 API,Wasm 才能调用,封装链路很长

  3. Rust 语言特性支持不完善

    比如异步函数支持就不够好,需要 Wasm Vm 对异步运行时有支持

Crubit 解决什么

Crubit:C++/Rust 双向互操作工具,这是来自官方的定义:https://github.com/google/crubit

具体来说,包括两个点:

  1. 函数互相调用

    C++ 和 Rust 之间的函数可以互相调用

  2. 内存互相访问

    C++ 和 Rust 之间的数据结构,可以互相访问

理想的期望效果就是,Rust 可以像现在 C++ 一样方便来开发扩展

对比来说,可以简化的是:

  1. 不需要包一层 FFI API 来做跨语言调用,这是现有的非 C++ 扩展都面临的问题
  2. 不需要手写内存 Struct 的映射,甚至可以直接跨语言读写内存

工作机制

简单说,就是分析源码,自动生成 FFI Wrapper 代码

以 struct 为例,从这个 C++ struct:

1
2
3
4
struct Position {
int x;
int y;
};

会自动生成如下的 Rust struct:

1
2
3
4
pub struct Position {
pub x: ::core::ffi::c_int,
pub y: ::core::ffi::c_int,
}

这样,写 Rust 代码的时候,就可以像 Rust 一样来操作 C++ Struct 了,还是挺方便的

尤其是,像 Envoy 这种大型 C++ 工程,有很多的 Struct 嵌套,手写是个不小的工程,且维护成本也不小的

前景如何

首先 Crubit 这个项目目前还是初始 “MVP” 版本,看文档貌似还有不少的限制

并且,Yan 大佬也还只是一个想法,也还没有玩起来,估计任重而道远

如果真搞成了的话,应该用起来会方便很多了,估计会有新扩展用 Rust 来实现了

并且,如果走出去了这一步,以后没准越来越多的 Envoy 核心代码,也会由 Rust 来写了

那就让我们拭目以待吧~

还有硬骨头

不过,即使搞定了 C++ 和 Rust 的互相操作,还有一个硬骨头在前面等着,也就是异步调度

因为,Envoy 有一套基于单线程的异步并发模型,Rust 也有自己的异步抽象,这两个如何顺利的配合呢

简单说,前面解决的都是 Rust 的同步函数在 Envoy 上运行的问题

现在要解决的是,Rust 的异步函数如何在 Envoy 上运行的问题

tokio 可以么

直接用 tokio 这种异步运行时可以么?答案是不太好搞

tokio 是一个多线程的运行时,有自己的调度机制,我们可以简单类比为 Golang 的 runtime 调度

一个异步函数,有可能会被调度到不同的线程上来执行,这个就会打破 Envoy 的单线程并发模型的约定

(当然,理论上也是可以解决,每个 Envoy 线程,绑定一个单线程的 tokio 运行时)

并且,tokio 需要有自己的主循环来触发 epoll_wait,这个与 Envoy 自己的 epoll_wait 会有冲突的

除非将两者合并,或者 Envoy 自己撸一个 Rust 的异步运行时

这其中的工作量嘛,想想都头大

跟 MoE 有啥区别

哈哈,我的主业是搞 MoE 的,自然是需要拿出对比一番的

MoE 是把 Golang 嵌入到了 Envoy,跟这里聊的 Rust 嵌入到 Envoy 是同一个领域的不同方案

首先,我们碰到的问题是一样的

  1. 跨语言的函数调用,内存操作,需要一些 Wrapper 胶水代码
  2. Envoy 有单线程并发模型约定

但是,解题思路不太一样

互操作

MoE 是老老实实自己写 Wrapper 代码,因为定性是扩展开发,其实需要用的 API 并不会太多,总量也比较有限

Rust 方案,其实后续有进入 Envoy Core 代码的可能性,所以这块期望会更高

如果有了更通用的方案,以后 Envoy Core 中的 C++ 代码,慢慢被 Rust 替换掉,也未可知

单线程约束

MoE 比较取巧的绕过了单线程的约束,保留 Goroutine 自由的被调度到其他线程,也就是不需要 Golang runtime 加额外的限制

这样我们可以支持标准的 Golang runtime,现有的 Golang 库直接拿过来就可以用,而不需要改造

但是,我们自己写的 Wrapper 框架代码,又会同时保证 Envoy 的单线程并发约定,所以,也不会有并发问题

而 Rust 的异步机制,并不像 Golang 的 Goroutine 这种完善的重封装,本质上有点类似的 Lua 的协作式协程

甚至,异步运行时都交给第三方库来实现了(好歹 Lua 还是内置提供了 resume 和 yield 这样的调度 API)

所以,Rust 是有机会像 OpenResty 一样,将语言的异步调度和宿主的事件循环结合起来的

最终效果

MoE 一大亮点就是支持原生的 Golang,现有的 Golang 库直接拿来就可以用,而不需要改造

Rust 方案,如果真搞成的话,普世性会比 OpenResty 嵌入 Lua 的效果更好一些

因为 OpenResty 中 Lua 非阻塞库,需要依赖 Nginx 的 event loop 重写一次,比如网络库需要基于 cosocket 重写

而 Rust 的异步运行时,如果与 Envoy 的事件循环结合起来了话,应该现有的异步实现,也可以跑起来的

或许也不需要改造?估计还得取决于具体的实现

不过,虽然我对 Rust 并不熟,不过依然可以笃定这个坑小不了,没那么快到的

最后

还是很期待 Rust 进入 Envoy 的,虽然如果成了的话,会多了一个 Rust 扩展机制,跟 Golang 扩展机制竞争

不过,以我对 Rust 和 Golang 浅显的理解,这两个发展的路线并不太相同,Rust 更偏向于系统编程,Golang 更偏向于业务编程

也就是 Rust 更适合做 Envoy 核心,Golang 更适合做 Envoy 扩展

当然啦,纯粹个人 YY,欢迎大家技术交流~