我是如何用Rust重写Node项目的

45 阅读10分钟

GitHub主页: github.com/hyperlane-d… 联系邮箱: root@ltpp.vip

大家好,我是一名计算机科学专业的大三学生。最近我完成了一个很有挑战性的任务:用Rust重写了一个原本用Node.js开发的项目。这个过程充满了挑战,但也让我学到了很多东西。今天我想和大家分享一下这个经历。

这个项目是我大二时做的一个在线协作平台,类似于简化版的Notion。它支持多人实时编辑文档、评论、分享等功能。当时我用Node.js和Express框架开发,前端用的是React。项目上线后,在我们学校内部使用,用户反馈还不错。

但是随着用户数量的增加,系统开始出现一些问题。最明显的就是性能问题。在高峰期,系统会变得很慢,有时候甚至会出现超时。我尝试了各种优化方法,比如添加缓存、优化数据库查询、使用CDN等等,但效果都不太明显。

更让我头疼的是稳定性问题。系统运行一段时间后,内存占用就会不断增长,最后不得不重启服务。我花了很多时间排查,发现是因为WebSocket连接没有正确清理导致的内存泄漏。虽然最后解决了,但这让我对Node.js的可靠性产生了怀疑。

今年暑假,我在一家互联网公司实习,接触到了Rust语言。公司的后端服务都是用Rust开发的,性能和稳定性都非常好。我开始思考,能不能用Rust重写我的项目,彻底解决性能和稳定性问题。

做出这个决定并不容易。首先,我对Rust并不熟悉,虽然在实习期间学了一些,但还远远谈不上精通。其次,重写一个项目需要花费大量的时间和精力,而我还要准备考研,时间非常紧张。但是我还是决定试一试,因为我觉得这是一个很好的学习机会。

我首先做的是技术调研。我需要找到一个合适的Rust Web框架来替代Express。我调研了几个主流的Rust Web框架,包括Actix-web、Rocket、Warp等等。最后我选择了一个基于Tokio的框架,因为它的性能最好,而且API设计也比较直观。

然后我制定了一个详细的重写计划。我决定采用渐进式重写的策略,而不是一次性全部重写。首先重写核心的API服务,然后逐步迁移其他功能。这样可以降低风险,也可以让我在重写过程中不断学习和调整。

第一步是重写用户认证模块。这是整个系统的基础,也是最关键的部分。在Node.js版本中,我使用了JWT进行身份认证。在Rust版本中,我也采用了相同的方案,但实现方式完全不同。

Rust的类型系统让我必须明确定义每个数据结构。在Node.js中,我可以随意传递对象,不需要关心类型。但在Rust中,我必须为每个API的请求和响应定义明确的类型。一开始我觉得这很麻烦,但后来我发现,这种严格的类型检查可以在编译时发现很多潜在的bug。

我记得有一次,我在定义一个API的响应类型时,忘记了处理某个字段可能为空的情况。在Node.js中,这种错误只能在运行时发现,而且可能导致程序崩溃。但在Rust中,编译器直接报错,强制我处理这种情况。虽然一开始觉得麻烦,但这确实提高了代码的健壮性。

第二步是重写数据库访问层。在Node.js版本中,我使用的是Sequelize ORM。在Rust版本中,我选择了SQLx,这是一个异步的数据库驱动,支持编译时的SQL检查。

SQLx的一个很酷的功能是,它可以在编译时检查SQL语句的正确性。如果SQL语句有语法错误,或者查询的字段不存在,编译器就会报错。这在Node.js中是不可能的,只能在运行时发现错误。

我还实现了一个连接池来复用数据库连接。在Node.js版本中,虽然也有连接池,但配置比较复杂,而且容易出现连接泄漏。在Rust版本中,连接池的管理更加自动化,而且由于Rust的所有权系统,连接泄漏基本上不可能发生。

第三步是重写WebSocket服务。这是整个项目中最复杂的部分,因为需要处理大量的并发连接,而且要保证实时性。在Node.js版本中,我使用的是socket.io库。在Rust版本中,我使用了这个框架提供的WebSocket支持。

重写WebSocket服务的过程让我对并发编程有了更深的理解。在Node.js中,虽然是单线程的,但通过事件循环可以处理并发。在Rust中,我可以使用真正的多线程并发,而且由于Rust的类型系统,不会出现数据竞争。

我实现了一个广播机制,当一个用户编辑文档时,可以实时通知其他正在查看该文档的用户。在Node.js版本中,这个功能的实现比较复杂,而且性能不太好。在Rust版本中,我使用了一个高效的发布订阅模式,性能提升了很多。

第四步是重写文件上传功能。这个功能需要处理大文件的上传,对性能要求比较高。在Node.js版本中,上传大文件时经常会出现内存占用过高的问题。在Rust版本中,我使用了流式处理,可以边读边写,不需要把整个文件加载到内存中。

我还实现了断点续传功能。当上传中断时,可以从中断的地方继续上传,不需要重新开始。这个功能在Node.js版本中很难实现,但在Rust版本中,由于有更好的底层控制能力,实现起来相对容易。

第五步是重写搜索功能。在Node.js版本中,我使用的是Elasticsearch。在Rust版本中,我也继续使用Elasticsearch,但使用了一个Rust的客户端库。这个库的性能比Node.js的客户端好很多,而且类型安全。

整个重写过程花了我三个月的时间。期间我遇到了很多困难,有时候一个问题会卡住我好几天。但是每次解决问题之后,都会有很大的成就感,而且对Rust的理解也会更深入。

重写完成后,我进行了详细的性能测试。测试环境是我租的一台云服务器,配置是四核八GB内存。我使用wrk进行压力测试,模拟了不同的并发场景。

在API接口的测试中,Node.js版本的QPS是一万五千,平均响应时间是六毫秒。Rust版本的QPS达到了十五万,平均响应时间只有零点六毫秒。性能提升了十倍。

在WebSocket连接的测试中,Node.js版本最多可以支持五千个并发连接,再多就会出现性能问题。Rust版本可以轻松支持五万个并发连接,而且响应时间非常稳定。

在文件上传的测试中,上传一个一GB的文件,Node.js版本需要二十秒,而且内存占用会达到五百MB。Rust版本只需要五秒,内存占用只有五十MB。

更重要的是稳定性的提升。我让Rust版本连续运行了一个月,期间不断地发送请求,系统一直非常稳定,没有出现任何内存泄漏或崩溃。内存占用一直保持在一百MB左右,CPU使用率也很稳定。

相比之下,Node.js版本运行几天后就需要重启,否则内存占用会不断增长。这种稳定性的差异,对于一个需要长期运行的服务来说,是非常重要的。

我还测试了资源占用。在相同的负载下,Node.js版本需要四核八GB的服务器才能稳定运行。而Rust版本只需要两核四GB就够了。这意味着,使用Rust可以节省一半的服务器成本。

除了性能和稳定性,我还发现Rust版本的代码质量更高。虽然Rust的代码行数比Node.js多了大概百分之二十,但代码的逻辑更清晰,错误处理更完善。而且由于类型系统的保护,重构代码时更有信心,不用担心引入新的bug。

我还发现,Rust的编译时检查可以发现很多潜在的问题。比如我在重构一个函数时,忘记更新某个调用点,编译器立即报错。在Node.js中,这种错误只能在运行时发现,而且可能很难排查。

部署方面,Rust版本也更简单。编译出来的是一个独立的二进制文件,不需要安装运行时环境,也不需要安装依赖包。只需要把二进制文件拷贝到服务器上就可以运行。而Node.js版本需要安装Node运行时和一大堆npm包,部署比较麻烦。

我还做了一个Docker镜像的对比。Node.js版本的Docker镜像大小是五百MB,而Rust版本只有二十MB。这意味着镜像的拉取和部署都会更快。

通过这次重写,我对Rust有了更深入的理解。我认识到,Rust不仅仅是一门高性能的语言,更重要的是它的设计理念:通过编译时的检查来保证程序的正确性和安全性。这种理念虽然增加了开发的难度,但大大提高了代码的质量。

我也认识到,技术选型对项目的成功非常重要。虽然Node.js很流行,开发效率也很高,但在高性能、高可靠性的场景下,Rust确实是更好的选择。当然,这不是说Node.js不好,而是说不同的技术适合不同的场景。

对于想要尝试Rust的朋友,我有几点建议。首先,不要被Rust的学习曲线吓倒。虽然一开始会觉得很难,但只要坚持下去,一定能够掌握。其次,要多写代码,多实践。光看书是不够的,只有在实践中遇到问题、解决问题,才能真正理解Rust。

第三,要善于利用社区资源。Rust社区非常活跃,遇到问题可以在论坛上提问,基本上都能得到帮助。而且有很多优秀的开源项目可以学习,阅读源码是提高水平的好方法。

第四,要有耐心。学习Rust需要时间,不要期望短期内就能精通。但是一旦掌握了,你会发现这个投入是非常值得的。

现在我的项目已经完全迁移到了Rust版本,用户反馈非常好。系统的响应速度明显提升了,而且再也没有出现过崩溃或超时的问题。这让我对Rust更有信心了。

我也在考虑把这个项目开源,让更多的人可以学习和使用。如果你对这个项目感兴趣,可以访问文章开头的GitHub链接。我的邮箱也在开头,欢迎和我交流讨论。

最后,我想说,技术的世界是不断变化的,我们要保持学习的热情,勇于尝试新技术。虽然学习新技术需要付出时间和精力,但这个过程本身就是一种成长。让我们一起在技术的道路上不断前进,共同进步。

GitHub主页: github.com/hyperlane-d… 联系邮箱: root@ltpp.vip