在新兴的编程语言中,Rust以其高性能和内存安全性在编程界广受好评。除了语法繁琐,变量所有权和生命周期难以理解,入门门槛比较高外,基本没有其他缺点。现在编程语言百家争鸣的时代,如何选择合适的语言,在合理的时间内解决问题,已经成为一门学问。在本文中,我们将介绍一个案例,我们将服务从Node.js迁移到Rust以解决瓶颈,从而解决实际问题并节省生产成本。过程中深入讨论了一些导致需要改语言的细节,以及过程中是如何做出决定的,希望能给大家一些启发。概览案例涉及一个企业业务监控系统,用于帮助开发者监控业务API。当客户的应用程序调用API时,会向系统发送日志,系统会对发送的日志进行监控和分析。系统数据流平均每分钟30k个API调用。每个客户端都会进行多次API调用。系统的处理分为两个关键部分:日志提取和日志处理。在原有系统中,提取服务是通过Node.js构建的。Node.js接收日志,与elixir服务通信以检查用户的访问权限,与Redis检查速率限制,并将日志发送到CloudWatch。CloudWatch部署触发事件的触发器,以通知数据处理程序进行处理。系统提取有关API调用的信息,包括从用户应用程序发送的每个调用的有效负载(请求和响应)。这些文件的大小限制为1MB,但仍涉及大量数据需要处理。处理程序异步发送和处理所有内容,目的是使最终用户尽快获得信息。一切都托管在AWSFargate上,并设置为以4000请求/分钟的阈值触发自动缩放。整个过程效果很好,但成本非常昂贵。由于AWS根据CloudWatch存储的使用情况收费,存储越多,需要支付的费用就越多。为了解决成本问题,有一个救助计划。Kinesis救灾为了解决CloudWatch存储成本昂贵的问题,在将日志传送到CloudWatch之前使用KinesisFirehose预处理。KinesisFirehose可能不太熟悉,但是Kafka可能很多人都知道,所以KinesisFirehose就是AWS云中的Kafka。使用KinesisFirehose预处理,可以将数据流以可靠的方式传送到多个目的地。只需对日志处理程序进行最少的更新,即可从CloudWatch和KinesisFirehose中提取日志。通过结构的改变,每天的成本可以降低到以前的6/1000。在新架构中,系统通过Kinesis将日志数据传递给s3,从而触发loghandler。运行新架构后,一切正常。但是几天后出了点问题。..监视仪表板上的一些异常情况。系统在收集垃圾,很多垃圾!垃圾回收(GC)是一些编程语言自动释放不再使用的内存的一种方式。发生这种情况时,程序将暂停。这称为GC暂停。您对内存执行的写入越多,需要进行的垃圾收集就越多,因此暂停时间会增加。对于系统服务,这些暂停的速度越来越快,足以导致服务器重新启动并对CPU造成压力。发生这种情况时,看起来服务器已关闭(因为它暂时关闭)并且客户端出现大量5xx错误,代理尝试拉取的日志中约有6%显示此错误。下图展示了垃圾回收的停顿时间和停顿频率:在某些情况下,停顿时间超过4秒(如左图),每分钟停顿次数高达400次(如右图)。经过更多的研究和分析,这似乎是AWSJavascriptSDK中的内存泄漏。尝试将资源分配增加到极限,例如将缩放阈值降低到1000req/min自动缩放,但问题仍然没有解决。可能的解决方案由于无法使用上述kninesis解决方案,因此需要一个新的解决方案来解决问题。选项如下。Elixir如前架构所述,系统使用Elixir服务来检查客户端访问权限。该服务是私有的,只能从虚拟私有云(VPC)中访问。由于从未遇到过服务的任何可伸缩性问题,并且大部分逻辑已经存在。所以有一个选项可以简单地将日志从该服务发送到Kinesis,跳过Node.js服务层。这是一个值得尝试的解决方案。在进行了一些改进后,对系统进行了测试。效果会好一些,但还是不大。系统的benchmarking发现GC垃圾回收的水平还是很高的,使用日志的时候还是会有5xx的日志返回给用户。Golang系统也将Golang考虑在内。这是一个不错的选择,但毕竟Golang也是一种垃圾收集语言。虽然有可能实现比上述更高的效率,但随着规模的扩大,您可能会遇到类似的问题。鉴于这些限制,系统需要更好的替代方案。以Rust为核心进行重构在系统的原始实现及其备份中,核心问题是相同的:垃圾收集。解决方案是使用一种具有更好内存管理且没有垃圾收集的语言。那么选择的语言就是Rust。RustRust不是垃圾回收语言。Rust依赖于称为变量生命周期和所有权的概念。所有权是Rust最独特的特性,它允许Rust在没有垃圾收集器的情况下实现内存安全。所有权是一个经常让Rust难以学习和编写的概念,但它非常适合像这个项目遇到的情况。Rust中的每个值都有一个所有者变量,因此在内存中有一个分配点。一旦变量超出范围,内存将立即被释放。由于提取日志所需的代码很小,因此值得一试。要对此进行测试,请查看问题的瓶颈:向Kinesis发送大量数据。第一个基准测试非常成功。于是Rust最终成了救世主,最终决定丰富原型并部署到生产系统中。在这些实验中,并没有直接用Rust替换原来的Node.js服务,而是重构了日志提取的大部分架构。新服务的核心是通过Envoy的代理,以Rust应用程序作为助手。新架构流程当用户应用中的Agent向系统发送日志数据时,会先到Envoy代理。Envoy查看请求并与Redis通信以检查速率限制、授权详细信息和使用配额等内容。接下来,与Envoy一起运行的Rust应用程序准备日志数据并将其通过Kinesis传递到s3存储桶中进行存储。S3然后触发日志处理程序处理,ElasticSearch开始对其编制索引。这样,最终用户就可以访问仪表板中的数据。性能和资源比较新架构中使用更少(更小)的服务器,但可以处理更多数据,而不会出现任何以前的gc5xx问题。比较新旧架构的服务延迟。服务在老Node.js架构下的延迟如下图所示。可以看出,平均响应时间接近峰值1700ms:通过Rust服务的实现,在新架构下,即使在峰值期间,延迟也降到了90ms以下。响应时间保持在40毫秒以下。旧架构上的Node.js应用程序在任何给定时间使用大约1.5GB内存,CPU负载约为150%。新架构下的Rust服务使用大约100MB的内存,只占用2.5%的CPU负载。结语大多数创业公司都一样,都会遇到业务爆发的阶段。当时最好的解决方案并不总是最好的解决方案。本案例中Node.js的架构就是如此。它使业务能够向前发展,但随着业务的飞速发展,业务最终会超过它。在这一点上简单的资源扩展将是昂贵的,而且成本高得令人无法接受。这时候就需要优化基础设施来满足新的需求。在这种情况下,虽然只是将Node.js换成Rust,但已经完成了架构的升级优化,完美解决了业务瓶颈。
