嘉宾 | 刘喆、黄金
编辑 | 李慧文
Rust 作为一门备受关注的新编程语言,它在保持使用 JavaScript 等语言进行开发时所拥有的内存安全性的同时,还能够实现不亚于 C 的性能。不过大多数公司还没有大量应用它。白海科技作为国内唯数几个“All in Rust”的公司,最近正在进行由 Java 向 Rust 的全方位重构。
我们采访了白海科技的联合创始人兼技术负责人刘喆以及他们的后端开发工程师黄金(他也是 QCon 北京【Rust 深度实践案例】专题的讲师),和两位老师一起探讨一些 Rust 重构的经验。
InfoQ:白海科技为什么要使用 Rust 重构整个系统呢?为什么要选择“All in Rust”呢?
刘喆 & 黄金:我们“All in Rust”是分阶段进行,目前还没有实现对“整个系统的”重构,前端还是用的 React。
实际上,我们第一版的计算引擎本身就是 Rust 写的,有一些辅助的中间件是 Python 的 SDK,我们目前只是重构其它部分,主要是后端的相关部分。
我们选择 Rust,最主要的原因是因为 Rust 的高性能、高安全和可靠性与 IDP 产品的核心需求高度匹配:性能是 IDP 开发中追求的最重要的因素之一,Rust 拥有不亚于 C 的性能;在引擎层面,Rust 具有可靠的安全性和稳定性;在数据接入层,Rust 足够底层,可以做更多其它语言比较吃力的事。
此外,对于企业级生产项目的开发,采用 Rust 更能保证程序的稳定性。Rust 初学者开发项目确实比其他语言更难,开发速度也会慢一些。但是,这是由于 Rust 编译器的复杂性对初学者的开发速度起到了一定的限制和阻碍。当被编译器教导完,成功编译后,相当于已经避免了程序本身 99% 的问题。
比如写 Java 的时候,我们偶尔会忘记判断空指针导致线上运行报错。但 Rust 完全避免了这一点,无论是 Option 还是 Result,都需要强制你处理不同情况才能通过编译(生产级别我们强制要求不能使用 unwrap 这类操作,代码 review 保证规范成功实施),所有权的概念让你也无需 GC 了。
InfoQ:请问重构过程中,您遇到过什么问题吗?当时是怎么解决的呢?在这个过程中有什么思考吗?请举两三个例子详细谈一谈。
刘喆 & 黄金:白海科技需要重构的部分 Java 代码行数有两万多行。从 Java 重构到 Rust,有两方面的核心问题:
- 一个是业务层面,尽量保证相关联的其它模块(比如前端)不做改动或少做改动,即兼容之前的接口
- 另一个是技术层面,有些 Java 代码重构到 Rust 后,就不能用原来的写法了,从简单的部分变量 clone()、有些并发场景下共享的实现改为用 channel 到复杂的一些调度算法需要重新设计。
我们计划后端部分不会保留 Java 代码的。因此,我们替换的过程是:先并行一段时间,重构完成的 API 就切换到 Rust 部分,直到全部切换完成。
如果一个项目天然就是使用 Rust 来编写的,那可以直接按 Rust 的思路,使用 Rust 的机制,比如类型系统、Trait、所有权、生命周期、宏、Tokio 全家桶等,来构思相关的设计和实现,比较自然;但对于重构项目,之前的设计和实现都不是按刚说的这一套来的,如果还按原来的思路,有些地方会遇到一些麻烦,比如 Java 中的 String 可能对应 Rust 的 String/str/OsString 等;原来的 Java 中如果用了多层的继承(各种设计模块的滥用)会带来一些麻烦;如果用到了二进制的数据交换,不同语言的实现有可能有不同,比如我们解析 ZMQ 中的消息,就费了很久的时间。
从重构过程的实际体验来说,我认为 Java 跟 Rust 最大的不同点是 Java 程序是有虚拟机的,不在乎编译的平台,跨平台运行也可以使用同样的 jar 包。
但我们使用 Rust 重构之后的程序,为了最终的可执行文件能够尽量最小化,使用的都是动态链接系统 Lib 库这类方式,导致有些我们开发机上面能跑的程序到了线上环境中有跨平台的兼容性问题,比如本地开发机是 Ubuntu、MacOS 等,线上之前使用的是 CentOS,就出现了部分程序需要的包不兼容或者不存在导致程序运行失败。我们的解决方法目前是修改运行环境的容器镜像,装上这些必需库。
类似的问题还有:JVM 虚拟机对于 fork() 出的子进程的 Sinal 的处理与 Linux 原生有区别,对于 Java 程序 Fork 出的子进程 Python Kernel,发送 SIGINT(kill -2) 时,只会中断 Python 进程的代码运行,不会将整个父进程杀死。
然而替换 Rust 实现后,当时我们在处理 Kernel 的"中断运行但不退出 Python 子进程"这个操作时,会将整个 Kernel 进程杀掉。为了解决这个问题,我们在这个 Rust 重构的 Kenrel 程序中忽略掉 SIGINT,在里面的 Python 进程中重新处理 SIGINT 抛出 KeyboardInterrupt(默认 Python 子进程会跟随它父进程的信号处理,不重新处理相当于达不到中断代码运行的效果)。
其实经过了这些问题之后,我有想过如果我们重构以后的这系列产品,要做单机版本的话,重构之后的 Rust 版本程序会不会给我们带来一些不便?
相对于 Java 来说,答案是肯定的,Rust 没有虚拟机,不支持一套编译多平台运行,会需要额外的处理。但考虑到我们可以使用交叉编译这类方法编译不同平台的运行程序,所以这也是可接受的。Rust 在我眼里,目前已经是一个可以承载开发企业生产级别产品的成熟语言了。
InfoQ:在重构过程中,如何保持系统的稳定性呢?
刘喆 & 黄金:如上一个问题,我们是分模块、分 API 逐个替换的, Java 部分有单元测试的代码和 API 的测试代码。Java 单元测试的部分,我们基本用 Rust 的 test 宏重写了,还增加了一些额外的部分,以保证原有的功能还可以正常运行。而 API 的测试代码,没有改动的直接测试了,参数有改动的同步修改,以保证每一个更换的 API 都还可以正常工作。
除了逐个功能替换外,我们在把 Rust 重构后的程序正式上线生产环境前,会经历三个测试阶段:本地开发机的单元测试与接口功能测试,与线上一致的内网环境的测试联调,以及线上环境的分区测试(我们只会给部分区的用户上线重构后的版本,待无异常反馈稳定之后再全面上线)。
如果出现问题我们会修复 Bug,重新经历这三个测试阶段,避免出现因为测试覆盖度不够而造成的 Bug 遗漏,进而影响整个线上系统的稳定性。
InfoQ:这次重构,白海科技大约投入了多少成本,获得了怎样的收益呢?您认为什么样的企业适合将代码重构为 Rust 呢?
刘喆:这次重构,我们投入了近一年的时间,完成了后端整体切换 Rust 的改造。主要的收益有三个方面:
- 一方面是发现了一些潜在的 Bug, Rust 编译器是个很好的助手和监督员。
- 另一方面所有开发成员统一在同一种风格和同一种要求下来协作,更加顺畅。
- 此外,由于我们整个产品是云原生架构,基于 Kubernetes 来构建,不再需要 JVM 之后,Image 的尺寸也减少了近 40%
技术永远是为了解决业务问题的,不解决实际问题的重构都是耍流氓。所以是否适合重构为 Rust,取决于业务关注的点,如果恰好业务需要高性能或高安全,那采用 Rust 是最优的选择。
有些企业还在观望 Rust ,我认为主要原因有两个:
- 从业务侧讲:大量成熟的业务运行良好,又有新的需求和增长的压力在,而这些业务又是成熟的 C 或 Java 或 Go 写的,相关人员不太有动力去换成 Rust
- Rust 本身目前也有自己的一些问题
InfoQ:那么,Rust 具体有哪些问题呢?
刘喆 & 黄金:第一,上手门槛高,导致市场上相关的工程师较少,人不好招。企业想扩大产出时招不到人,又要面临现有人员离职后无人能接手相关项目的风险。
第二,相关的生态还比较弱,还有很多轮子需要造,特别是高并发和低延迟部分。Rust 相关的成熟库还是太少,纯从零开始造的轮子,还是需要企业级的打磨才能更可靠,而造轮子和打磨都是需要时间的。比如我前几天在找一个高吞吐量的日志库, C 的生态中随便找一个每秒 100 万条日志就有一大把,而 crates.io 上唯一声称达到这个量级的 fast_log 还是有 Bug 的,最后的 flush 会丢日志,真实打出来的只有 30 万条左右,后面的都打不出来。
第三,crates.io 上的库大部分都还在 0.x 版本 ,1.x 以上版本的库很少,而且即使 1.x 以上版本了,还是有变更 API 的现象存在,稳定性没那么好。
第四,在后端或系统级开发中,Rust 跨平台开发还不成熟,无法做到像 Java 这种一次编译、多平台可部署运行的效果。有些功能开发甚至由于与操作系统强耦合,在跨平台时不止是简单交叉编译个目标平台的就可以了,还需要做对应的不同平台实现。
第五,企业级开发中,Rust 还达不到 Java 这种开发速度,例如 Java 的 Springboot 框架,上手简单,开发也简单,开发周期非常短就可完成企业级开发的任务。在大型互联网企业中,更看重效率与收益,对于大部分企业的应用场景,学习难度高且开发速度慢可能成为了他们不选择 Rust 技术栈的最大原因。
InfoQ:除了重构,白海科技还用 Rust 解决了什么难题呢?能否举两个例子分享一下您使用 Rust 的经验呢?
刘喆 & 黄金:我们的一个产品是给数据科学家使用的在线 IDE,涉及到 Python 代码的执行和写代码时的自动补全,代码执行的核心组件是个 Python Kernel, 这个是我们独立自研的,主要是基于 Pyo3 的向 Rust binary 嵌入 Python 的功能开发,替换了原来 Jupyter 所使用的 Ipykernel,实现了包括多客户端对接、语法解析、运行前语法错误提示、代码执行、图形和 DataFrame 输出渲染等等功能。
对比原来 Ipykernel 来说,它有以下几方面的核心提升:
- 更加轻量(我们只实现了我们需要的部分,Ipykernel 有一些为了兼容存在的历史包袱)
- 更加灵活(我们可以在写一些新功能时自己在自研 Kenrel 上加新代码写扩展)
- 性能更强(Rust 实现在中间层处理上会比 Python 快一些)
写代码的自动补全部分,我们基于微软提出的 LSP 协调,扩展了微软 pyright 的相关功能,实现了 WebSocket 对接、自动初始化环境、环境切换、Quickfix 等实用功能。在代码辅助这一点上,我们做到了同领域领先的水平,详细可以看 IDP Meetup 上我的分享。
InfoQ:白海科技现在只有前端没有使用 Rust,这是为什么呢?因为 Rust 和前端有什么不适配的问题吗?
刘喆 & 黄金:我们采用 Rust 是一个逐步替代的过程。目前优先在对性能和安全性要求最高的后端进行替代。后续随着相关人才的逐步充足、内部实践的逐步成熟、Rust 生态的逐步丰富,我们的前端也会采用 Rust。
从适配性来说,Rust 其实挺适合写前端的,毕竟是 Mozilla 公司开发的语言,对于前端的 WebAssembly 开发非常适合,目前也在业界也有很多应用。我们希望能有更多 Rust 的新鲜血液加入,共同推进 Rust 的应用。
InfoQ:能否从架构落地的整个过程的角度,讲讲 Rust 的优势?
刘喆:架构确定到设计实现的部分,Rust 对于工程化的支持还是比较完善的:
- 组件选型方面 Cargo 和 crates.io 支持良好。Java 有 Maven,C 相应地就没那么方便。
- 模块间接口对接或数据交换的部分,Rust 和 Java / C 不相上下,接口和数据定义明确,容易理解,C 相对复杂一些。
- 代码测试方面,Rust 内置的 test 宏,很好地解决了这个问题,其它语言都需要第三方插件才能做到的事,Rust 天然就有了。在 CI(持续集成)的时候,你甚至可以要求 test 不通过就不能合并代码,满满的现代感。
- 代码开发阶段,编译器 /rust-analyzer/fmt/clippy 这 4 者的组合,让写代码真的成了一种享受:在代码上线之前,基本上发现了所有的错误,所有成员写出来的代码风格都是一样的,都是按最优秀的程序员定制的规则来写的。无论是看别人的代码还是自己写,就两个字:舒服!
InfoQ:Rust 一直存在编译时间问题,白海科技是怎么解决这个问题的呢?
黄金:在刚开始编写 Rust 新项目的时候,其实我还没有在意过这个问题,并没有太大影响。但是到后面我们写的代码量越来越大,使用的 crate 以及自己造的轮子越来越多,编译时间就成了一个非常大的问题,尤其是在 release 编译的条件下。
第一个处理是:我们开始理清所有依赖的 crate 的 feature,坚决只使用用到的部分,不做无效依赖。我们自己写的 crate 也加上了条件编译的 feature,并且划分的粒度极细,在库依赖时也如上述所说处理。这样显著地减少了编译时所依赖的库,减少了编译时间。
第二个处理是:调试过程中编译,可能会多次测试,所以在除了最终的正式上线版本前,我们使用的都是 DeBug 编译。Debug 增量编译速度是可以接受的。这样减少了 release 编译的优化时间,也并不影响功能测试。在最后上线的版本中,编译一次优化的 release 版本就好了。
InfoQ:您是什么时候开始接触 Rust 的呢?作为 Rust 社区用户,您怎么看待 Rust 现在的社区生态环境呢?
刘喆:我是近两年开始接触 Rust 的,但是之前接触过 C /Lisp/Haskell 等跟 Rust 有渊源的语言,所以入门起来还不算太麻烦,特别是有了 rust-analyzer 这样的辅助工具之后,开发起来还算顺畅。
Rust 社区现在环境的话,有以下几个方面:
- 总体上工程师的数量还是偏少,规模效应不明显,在工业上大面积推开就还需要大量的时间
- 相关库的丰富度和成熟度还有待进一步提高
- 所有权和生命周期等相关特性,保证了代码的安全性,同时在一定程度上限制了语言的表现力
- 社区发展得晚,但壮大速度很快。crate.io 上发布 crate 的数量每年都有很大增长
InfoQ:Rust 用户相比其他语言来说,一直较少,您的 Rust 团队是如何组建的呢?
刘喆 & 黄金:我们最开始的 Rust 团队是自己培养的,决定了用 Rust 来开发,大家就一起边学习边开发。因为大家都有计算机科班的背景,又有过多年的大数据相关开发经验,所以除了刚开始跟编译器“斗争”得比较厉害,后来就都还好了。后来又有新的写过 Rust 的同学加入我们,队列就组建起来了。
俗话说“不怕神一样的对手,就怕猪一样的队友”。Rust 对工程师的要求相对较高,在客观上保证了我们的“队友”不会变成“猪队友”。
其实推广 Rust 也好,还是组建 Rust 团队也好,更大的困难不是来自于 Rust 开发者人少。更多是因为很少有人愿意放弃现在更好找工作的技术栈转 Rust 岗位,害怕以后找不到工作。对于这种情况,我们更愿意展现我们公司的热情,让求职者对公司更有信心,鼓励这类犹豫的求职者转向 Rust 开发岗位。
嘉宾介绍
刘喆,白海科技联合创始人兼技术负责人,51CTO/CSDI/into100/spark summit 等多项技术峰会讲师,华南理工特邀讲师。辽宁工程技术大学硕士毕业,专业为数据挖掘方向,曾在百度负责 Hadoop 集群的运维开发,在人民搜索负责运维开发,在明略科技担任高级技术总监,负责广告监测全流程设计、大数据平台和 AI 算法平台。有丰富的数据开发和架构经验,有超大规模数据集群开发和大数据团队管理经验。
黄金,白海科技后端开发工程师。目前阶段主要负责将原先使用 Java 技术栈的项目以 Rust 语言重构的工作。之后主导公司自主研发的 Rust 实现类 Ipykernel,替换了线上 IDP 环境的 Python 运行内核。并推进公司技术栈向“All in Rust“转变。
https://qconplus.infoq.cn/2022/beijing/presentation/4561
外部链接
刘喆:IDP Meetup 中关于代码辅助的分享https://www.bilibili.com/video/BV1ea411v74g?spm_id_from=333.337.search-card.all.click