这是一个以 Rust 为优先,以 Python 为辅助,通过服务间调用实现混合语言开发,但对外只暴露一个服务的容器设计,简称 All in One。
我使用 Python 进行开发也有很长一段时间了,经历了很多的不同项目和不同公司,Python 本身的特性,导致它无法为你完全保证一定没有问题,即便是你写了很多单元测试和集成测试,甚至测试最后比代码都多,也依然是无法完全覆盖到所有逻辑分支和各种数据状态。
后来,我尝试了 Go,尝试了 Rust,我发现 Rust 能为我解决这个问题:实现的正确性。Rust 它强大的数据结构描述能力,优秀的业务逻辑代码表达能力,能实现读起来跟 Python 几乎一样的高级语言代码表达形态,外加上强制的编译期间错误提示,可以说几乎可以杜绝掉在 Python 和其它语言里面 99% 的人为因素或其它因素的疏忽影响导致的代码问题。
使用 Python,上线后是否能保证没有问题,你肯定不能完全打保票,各种手段和流程完善的情况下,也最多最多 90% 极限了,但是合理地善用 Rust,上线后我可以随随便便打 99% 的保票。
但是呢,Rust 目前也不是万能,毕竟“生态”这个事情不是你想如何就如何,有时候确实就是某个语言的实现会更好用或者更适合目前的状态,只要业务有需求,这是几乎完全无法避免的。
那么,Rust 和 Python 的组合,一般有两种情况:
- Python 为优先,把 Rust 的实现作为库嵌入使用,这条路目前是非常清晰和成熟了,而且收益很好,是一个合理方向;
- Rust 为优先,把 Python 的实现作为库嵌入 Rust 使用,这条路目前有解决方案,实现也能实现,但是呢,目前看起来不是非常的成熟,应用场景不是非常多,也增加了一些复杂度和其它问题。
而我们的目标是以 Rust 为优先,将大部分的数据结构和业务逻辑都以 Rust 进行描述和实现,仅使用 Python 来为特定功能提供支撑,尽量降低 Python 这部分的实现规模和复杂度。
那么还可以考虑这个方案:Rust 为优先,把 Python 的实现作为服务依赖,通过我们非常熟悉常见的方案如 RESTful API 来进行对接,这样成熟度高,也更容易控制。
如果是在一般的多服务架构下,可能增加一个服务部署,不是太大的问题,但是有时候,并不想依靠网关或外部路由来实现服务的识别和调用,甚至是有时候不想让外部知道这多个服务的存在,而是对外就只是一个服务即可,更多地还是想把对这个服务的绝对控制权放在服务内部,所以,这其实不就是所谓 All in One 的设计了嘛。
在容器时代,大多数情况下,都是尽量强调一个服务一个容器,不要在一个容器里面塞太多东西,不然后续无法在外部对里面的东西进行合理地调整,而 All in One 则恰恰相反,尽量简化对外的复杂度,而把很多事情放到内部控制。
在我们的这个场景中,期望对外依然还是一个服务,但是是由 Rust 进行实现和暴露接口,内部则通过组合调用 Python 的服务能力来实现业务需求,通过重复利用 Rust 的语言特性和 Python 的生态丰富性来满足业务需求的同时,也做到对外接口的严谨性,提升内部实现的代码质量,最终提高服务整体的稳定性和服务质量。
除基于 HTTP 的 RESTful API 之外,其实还有这么几个选项:
- 上面讲的 FFI 模式,把 Python 代码直接放在 Rust 里面调用
- 消息队列,通过 pub/sub 模式通过消息队列进行数据交换
- 基于 Unix Socket 的 IPC 进程间通信模式
其中消息队列由于不是常规的请求-响应模式,不适合常规的接口调用场景下使用,而 FFI 模式调 Python 目前也有它自身的问题,剩下基于 Unix Socket 的 IPC 进程间通信是有可能被同等于 RESTful API 进行替代使用的。
而 RESTful API 应该是目前市面上最常用和最常见的开发模式,成熟度最高,而是否有需要把 RESTful API 换成其它的 RPC 方式进行替代,看具体情况具体分析再做决策。
方案其实也很简单:supervisor
这个 supervisor 在裸机部署时代,发挥了很大的作用,它将我们的启动命令和各项参数进行固化为配置文件,然后维护服务的起停和各项操作,描述了这个服务的相关各项信息。
到了容器时代后,它就用得少了,但是在 All in One 这个场景下,它还是有它的作用的。
flowchart TD
subgraph XService
Rust --> Python
end
如前面所说,有时候并不是想完全用 Rust 就能完全用 Rust,无法避免要用到其它的一些组件来实现相应的功能,无论基于什么样的考虑和前提,总是有这样的情况存在的。
此时大概率 Python 实现的服务是不需要对外暴露的,所以此时 Nginx 是可以不用的,而直接暴露 Rust 服务的端口即可。
flowchart TD
subgraph XService
Nginx --> Rust
Nginx --> Python
end
在外部服务几乎没有任何感知的情况下,把一个纯 Python 写的服务,改为由 Rust + Python 进行实现。
这个过程,完全做到可以是渐进式地执行,毕竟如果项目庞大,不可能一朝一夕就完全改写完成。使用 nginx 在内部做一层代理层,这样统一使用 nginx 对外,通过配置文件将请求进行分发控制,这样在迁移的执行过程中,所有调整都在内部控制,对外是无感的。
由于最终的目标有可能是完全使用 Rust,以及在这个共存的过程中是不能对外有任何影响和任何需要外部配合的,那么在 nginx 这一层,请不要做任何路径的转换和前缀移除,直接原模原样转发是最合适的,相当于它仅仅只做一个路径识别并转发到对应的服务,不再做任何其它的多余操作和控制。
曾经我们执行过一个方案:利用 Nginx 制作一个中间代理层,介于服务和网关之间再插入一层,负责路由的二次判断和分发,专门部署用于服务迁移。这个方案在当时把动静搞得很大,只能说是组织处于那种状态下的特定选择吧,毕竟服务规模那么大,站在整个全局看,是需要这么一个专门的东西。
但其实如果只是站在单个服务的角度来看,其实目前这个方案对我来说可能更具备吸引力。
flowchart TD
subgraph XService
Nginx --> Rust
Nginx --> Python
Rust --> Python
end
也有时候可能内部之间既有依赖关系,被依赖的服务也可能还有对外提供服务暴露的情况,这个其实也应该不少见,比如迁移过程中发现有些实现确实就是要调人家的会比较合适,这样这个结构也是可以的。
- Rust 项目如果觉得未来项目可能会比较大,那么尽早使用工作区 (workspace) 模式是更好的。
- 如果是旧项目,在当前文件夹下初始化 Rust 项目是可以的,如果 src 目录冲突,可以考虑给原文件夹重命名
- 如果要用子目录明确区分开,也是可以的,这类似很多开源项目的结构,可以去参考它们的。
- 这里的 Nginx 也可以使用 OpenResty 进行平替,它内置更多一些组件,某些场景下可能使用起来更方便点
本文由 plane 触发思考:https://github.com/makeplane/plane