用Rust重构了后端

March 7, 2024

1

大二的时候我在一家银行实习,当时组内正在用Flask写后端,我也就用这个框架搭了我的个人网站,因为我也不会别的。网站本身非常简单,SPA + REST,前端从后端要东西,后端返回之后再渲染出来,整个过程没什么效率和内存方面的顾虑,所以拿Python写似乎正好合适。

代码撸得很快,相比于其他语言,Python的每一行都甜得像一颗语法糖,各种内置函数比如zip连锁比较让我有一种在写Markdown的感觉。前端用React写完之后,后端写了几天就上线了,接下来几年时间在里面小修小补,加了一些别的endpoint,然后就一直安安稳稳的运行到现在。然而运行起来虽然没问题,但是写起来抱怨还是有的,主要来说是这几个:

  1. 部署困难。代码很快就写好了,但是真正部署到云服务器上的时候,虽然一开始可能没什么问题,但是时间长了geventflask,python本体,wsgi之间总有几个版本互相不兼容。然而解决这个冲突又没有什么很好的解决办法,只能反复来回实验。
  2. 代码质量容易下降。一开始一切都很简洁,加上是弱类型语言,随便一个什么变量定义一下就可以拿来用,但是时间久了这些简洁的短代码都会变成后续维护的障碍。(后来在3.5中,官方加入了type hint,这个实在是太舒服了,太必要了,写type check是一种美德)。
  3. Python2和Python3互不兼容。现在似乎一切都是py3,也就没有这个问题了。但是最早开始写代码的时候py2还没有完全被淘汰。我的Mac内置了Python2,于是我只好单独去安装Python3,然而各种配置设置起来相当麻烦,还需要专门看教程才能正确的配置好环境。
  4. 作为跨平台语言,在不同os下适应的并不是很好。我一开始只有一台Mac,服务器上面选择了Linux,然后过了两年自己装机换了Windows,三个系统一起捣乱,不知道什么时候就会看到奇怪的报错,只能说这波跨的不是很平台。
  5. 哪怕有virtualenv协助,pip依旧是一个逆天的包管理器。node_modules已经被黑成碳了,但是pip的安装逻辑和依赖地狱也不遑多让(Pipfile.lock竟然需要第三方协助)。加上python松散自由的社区,很多package完全没有完整的文档支撑。一是用起来一头雾水,二是明明只装了几个包,结果pip freeze出来的东西却有一大堆。

至于速度和多线程这两个被大多数人黑的主要问题,因为使用场景的原因我倒没有什么感受。网站上线了三四年,最早写的代码现在来看已经非常烂,很多逻辑都拧巴在一起,也完全没有任何的顺序和报错机制。在代码前后都是问题的情况下,小修小改似乎很难一口气解决所有根本问题,不能再犹豫了,一定要出重拳!于是趁着放假就决定干脆整个重写了事。

语言选择了这几年很火的Rust,原因除了我还不会之外,主要是因为它的一些新机制看着很有趣。选择了一个看着还比较轻量级的后端框架Actix,学习了一下语言特性,然后就是动手一点点把每个endpoint迁移过来。Rust的代码明显比Python麻烦一些,多了一些关键词和符号,而且并不像Python一样在很多逻辑细节上显得理所应当,反而是违背直觉的。这些限制强迫我在写代码的时候去用电脑的方式思考,从向CPU索取一块内存开始,到这块内存被重新释放出去结束,而不是想到哪里写到哪里。我认为这种脚铐对于我这种菜鸟来说是有益的。

2

写了一小段时间的Rust,感受还是有一些的:

  1. 官方撰写的“The Book”内容相当丰富,并且有完整的翻译和代码片段。虽然对于完全的初学者来说似乎会比较难,但是应该也没有人会拿Rust来入门写代码,很适合有了一点别的语言上的经验后再来看。
  2. 有了其他编程语言的前车之鉴,Rust团队在语言诞生之初就在工具链上下了很大功夫。无论是rustc(编译器),cargo(包管理器),还是rustfmt(格式管理)都能看得出从别的语言上借鉴的地方,但是都比较好的解决了一些前辈们的历史遗留问题。当一个社区已经发展到一定规模,再想推出一款放之四海皆准的标准工具是相当困难的(君不见npm, yarn, pnpm, bun之事乎)。
  3. Rust引入了所有权机制来处理垃圾回收,虽然类似的手法在C++ RAII中已经出现了,而且思路基本一致,但是这毕竟也只是一个可以选择的设计模式,加上C++的历史,也只能是一个可供选择的模式,不能强加给用户,而Rust作为新生语言做起来就没有这种顾虑。在以往的语言中,GC的处理基本分为两种:一种是自动GC,比如java,js,或者python。用户不需要去处理分配出去的内存,因为这些编译器都会帮你处理好;另外一种是手动GC,比如C++,用户手动调用一块内存出来(malloc),用完了再手动释放掉。而在Rust(以及RAII)中,每一块内存都被制定了一个所有者,当所有者超出范围的时候,其所拥有的内存会被自动释放掉。以往的手动回收需要用户去考虑什么时候释放内存,而Rust中因为每一块内存从语法上被保证了一定会被释放掉,所以用户需要去考虑的反而是是什么时候不释放内存,而这一点需要通过所有权转移以及生命周期控制完成。
  4. 编译器的提示以及报错机制太强大了,有一种被老师手把手上课的感觉,让我短暂地忘记了Segmentation fault (core dumped)
  5. Option完全代替了Nullnull这个东西在任何语言里处理起来都很麻烦,经常会在毫无防备的时候被这个值偷袭一手。Rust把可能为Null的数值都分为了Some()None,虽然逻辑比较奇怪,而且要不停地在代码里unwrap,但是总体上来说还是让代码更加严谨,处理异常的时候更加方便了。

总而言之,Rust作为一名新生的语言,完全没有大多数流行语言身上沉重的历史包袱,可以在设计上取各家之长,然而总体上又没有过于激进的设计。学习过程虽然对新手不太友好,也暂时很难想象我会用这个语言去做一些大的项目,但是写起来确实是相当舒服。对于我自己写的小项目,内存安全其实对结果影响并不大,本身代码就那么小,等漏完内存都不知道猴年马月了,然而知道自己的小进程有在好好利用CPU施舍的权限,这依旧是一件令人心情愉悦的事情。

3

重构完了,扔到服务器上,一行cargo build --release编译出来,然后把新的executable放上去跑,部署就完成了。现在时间已经过去一个多月,用Rust重构后的后端也一直没有什么问题,每天接到一点点请求,然后把处理好的数据发送回去,在我租用的服务器上默默占用着微不足道的内存。刚开始重写的时候很激动,久违的有一种刚上大学做出第一个side project的感觉。中间也遇到了一些不算太复杂的问题,但是很快的都解决了,本来想趁热打铁写一些感想,再放几段代码上来分享一下技巧,但是总算是没有写,一直拖到现在。大概是因为我大部分时候不知道能写什么,也是因为我觉得这样做有某种炫耀,有某种尴尬。但是最后还是写了点,因为实在觉得好不容易在自己擅长的领域探索了一点,什么都不记录有一些可惜。加上博客很久没更新了,连新建文档的脚本都忘了怎么跑,最后终归是记录了一点想法,有总比没有强。

软件行业每年都会出很多新的技术,而Rust只是其中发展的不错的一个,起码到现在为止,还不能说对传统的产业带来了什么冲击。充其量是一股新鲜血液,然而绝大多数技术还是要靠着老的血液维持着。总有人说写代码是一个日新月异的行业,新的东西会不断地代替旧的东西,而旧的东西会被不断地淘汰。然而过了这么多年,C++还在,JSP还在,OpenGL还在,Java前几个月发布了21,但是我在公司里写的还是十年前的Java 8。Python没有干掉Matlab,因此我也不认为Rust能牛到让核心代码改朝换代。限制技术发展的早已不是省下来的那一点内存和编译速度,而是某些超脱于代码之外的规律。Rust是一个非常好的工具,然而像Python,C++这些也是相当不错的工具。现实中的需求总量就那么多,只要能差不多地解决问题,具体怎么实现实在是一个比较无所谓的事情。我希望我可以说出我有代码洁癖,要求自己每一行写的都趋于完美之类的话,然而事实是大部分情况下我没有,我只希望我自己写的代码能解决我的问题,给公司写的代码能解决甲方的问题,如果时间精力允许的话就做得优雅一点,如果不行的话就算了。很难说这是不是已经偏离了一开始的初衷,但是这是另外一个需要解决的问题,而现在的情况确实是这样。

我希望Rust越来越火,最好火到全世界所有人只写Rust,我也就不用花几百块钱去买内存条了;但是我更希望我自己代码写的更好一点,好到有公司愿意给我工资,好到我可以切身感受到新技术带来的进步,好到我可以享受自己的工作成果。这并不是一件简单的事情——说是最难的事情也不为过。我希望我能办得到,毕竟还很年轻,尚能饭。在做到这点之前,我还需要努力。