基于Rust实现业务架构的重构

即将开播:6月19日,互联网银行架构师魏生谈互联网开放银行实施路径的探索与思考

新兴的编程语言中,Rust以高性能、内存安全为卖点在编程世界里广受好评。除了比较繁琐的语法,难于理解的变量所有权和生命周期,入门门槛比较高以外基本上没有其他的缺点了。现在处于编程语言百家争鸣的时代,如何选择合适的语言以及在合理时间解决问题成了一门学问。本文我们介绍一个案例为了解决瓶颈将将服务从Node.js迁移到Rust从而解决实际问题节省生产成本。过程中深入探讨了导致需要更改语言的一些细节,以及该过程中如何决策的,希望能给予大家一些启发。

概述

案例涉及的是一个企业的业务监控系统,该系统用来以帮助开发人员监控业务API。当客户的应用程序调用API时,会向系统发送日志,系统对发送的日志中进行监控和分析。

系统数据流为平均每分钟处理30k 的API调用。每个客户都会进行很多个API的调用。系统的处理分为两个关键部分:日志提取和日志处理。

基于Rust实现业务架构的重构

起初的系统中是通过Node.js构建提取服务。Node.js接收日志,与elixir服务进行通信检查用户的访问权限,用Redis检查速率限制,然后将日志发送到CloudWatch。CloudWatch部署了触发器,触发事件通知数据处理程序处理。

系统提取有关API调用的信息,包括从用户应用程序发送的每个调用的有效负载(请求和响应)。这些文件的大小被限制为1MB,但是仍然涉及大量的数据需要处理。处理程序以异步的形式发送和处理所有内容,目标是使信息尽快提供给最终用户。

所有内容都托管在亚马逊云AWS Fargate上,并对其设置为在4000 req/min阈值触发自动缩放。

整个流程都运行的很好,但是费用却非常昂贵。由于AWS是按照CloudWatch存储的使用来收费的,存储的越多,需要支付的费用就越多。

为了解决费用的问题,于是就有一个救援计划。

Kinesis救援和灾难

为了解决昂贵的CloudWatch存储费用问题,在将日志传送到CloudWatch之前,使用了Kinesis Firehose前置处理。Kinesis Firehose可能熟悉少,但是知道kafka的人可能多,那么Kinesis Firehose就是AWS云中的Kafka。使用Kinesis Firehose前置处理,可以用可靠的方式将数据流传递到多个目的地。只需对日志处理程序进行很少的更新,就可以从CloudWatch和Kinesis Firehose提取日志。通过该架构的更改,可以将日成本下降到之前的千分之六。

基于Rust实现业务架构的重构

新架构中系统将日志数据通过Kinesis传递到s3中,从而触发日志处理程序。新架构运行后,一切都ok。但是过几天出现了异常。。。监控仪表板上的一些异常情况。系统在收集垃圾,很多垃圾!

垃圾回收(GC)是某些编程语言自动释放不再使用内存的一种方式。发生这种情况时,程序将会暂停。这称为GC暂停。对内存进行的写操作越多,需要进行的垃圾回收就越多,因此暂停时间会增加。对于系统服务,这些暂停的速度越来越快,足以导致服务器重新启动并给CPU造成压力。发生这种情况时,它看起来就像是服务器已关闭(因为它暂时处于关闭状态),并且在客户端会有大量的5xx错误,而代理所尝试提取的日志中大约有6%出现了这个错误。

下面图显示了垃圾回收的暂停时间和暂停频率:

基于Rust实现业务架构的重构

在某些情况下,暂停时间超过了4秒(如左图所示),并且每分钟最多有400次暂停(如右图所示)。

经过更多研究分析后,似乎成为AWS Javascript SDK中内存泄漏的导致的该问题的发生。尝试将资源分配增加到极限,例如减小缩放阈值到1000 req/min自动缩放,但是没有问题仍没有解决。

可能的解决方案

由于不能能使用上面的kninesis方案,因此需要新的解决方案来解决问题。可选的方案有以下几种。

Elixir

如前的架构介绍,系统使用Elixir服务检查客户访问权限。该服务是私有的,只能从虚拟私有云(VPC)中访问。由于从未遇到过该服务的任何可扩展性问题,并且大多数逻辑已经存在。所以可选择简单地从该服务中将日志发送到Kinesis,而跳过Node.js服务层。这是一个值得尝试的方案。

做了一番改进后,系统进行了测试。效果会好一点,但仍然不是很佳。系统的基准测试表明,GC垃圾收集的水平仍然很高,并且在使用日志时仍会有5xx的日志返回给用户。

Golang

系统也考虑到Golang。这是一个很好的选择方案,但是,毕竟Golang也是一种垃圾收集语言。虽然可能可以实现比上述更高效,但随着规模的扩展,很可能还会遇到类似的问题。考虑到这些限制,系统需要一个更好的选择。

以Rust为核心进行重新架构

在系统最初的实现和备份中,核心问题都是相同的:垃圾回收。解决方案是使用一种具有内存管理更好的并且没有垃圾回收的语言。那么可选择的语言就到了Rust。

Rust

Rust不是垃圾收集的语言。Rust依赖于称为变量生命周期和所有权的概念。所有权是Rust的最独特功能,它使Rust无需垃圾收集器即可保证内存安全。

所有权是一个经常使Rust难以学习和编写的概念,但又使它非常适合像这个项目遇到的情况。Rust中的每个值都有一个所有者变量,因此在内存中有一个分配点。一旦该变量超出范围,内存将会立即释放。

由于提取日志所需的代码很小,应该非常值得尝试。为了对此进行测试,通过问题的瓶颈:向Kinesis发送大量数据。第一个基准测试非常成功。

所以Rust最终成了救世主,最后决定将原型充实并在生产系统的部署。

在这些实验过程中,并没有直接使用Rust直接替换原始的Node.js服务,而是重构了日志提取的大部分架构。新服务的核心是通过Envoy代理,在其中Rust应用程序作为辅助工具。

新架构流程

基于Rust实现业务架构的重构

当用户应用程序中Agent将日志数据发送到系统时,它将首先进入Envoy代理。Envoy查看请求并与Redis通信以检查速率限制,授权详细信息和使用配额之类的内容。接下来,与Envoy一起运行的Rust应用程序准备日志数据,并将其通过Kinesis传递到s3存储桶中进行存储。然后,S3触发​​日志处理程序处理,Elastic Search开始对其进行索引。这样,最终用户就可以访问仪表板中的数据。

性能和资源对比

新架构中使用了更少(更小)的服务器,但是可以处理更多数据,而不会出现任何之前的gc 5xx问题。

对比新旧架构的服务延迟。在旧的Node.js架构下服务的延迟数如下图,可以看到平均响应时间接近1700ms的峰值:

基于Rust实现业务架构的重构

通过Rust服务的实施,新架构中,即使在最高峰期间,延迟也降至90ms以下,平均响应时间保持在40ms以下。

基于Rust实现业务架构的重构

旧架构下Node.js应用程序在任何给定时间都会使用约1.5GB的内存,CPU的负载约为150%。

新架构下Rust服务使用了大约100MB的内存,而仅占用了2.5%的CPU负载。

结论

相关推荐