Rust与Go的对比

在各个领域,Rust 都已经成为一流的语言。最近,我们通过将服务的实现从 Go 切换到 Rust,极大地提升了该服务的性能。这里我阐述了重新实现服务为何是有价值的、该过程是如何实现的以及由此带来的性能提升。

Read States 服务

我们从 Go 切换到 Rust 的服务叫做“Read States”服务。它的唯一目的是跟踪用户阅读了哪些频道和信息。每当用户连接的时候,每当消息发送的时候,每当消息被读取的时候,都会访问 Read States。简而言之,Read States 处于最关键的位置。我们希望能够保证服务器始终让人感觉快捷无比,所以必须要确保 Read States 是非常快速的。

在 Go 的实现中,Read States 无法支持产品的需求。在大多数情况下,它都是很快速的,但是每几分钟我们就会看到很大的延迟峰值,这对于用户体验来说是很糟糕的。经过调查,我们确定峰值是由 Go 的核心特性引起的,也就是其内存模型和垃圾收集器(GC)。

为何 Go 无法满足我们的性能目标

为了阐述 Go 为什么无法满足我们的需求,我们首先需要讨论数据结构、规模、访问模式以及服务架构。

我们用来存储读取状态信息的数据结构被简便地称为“Read State”。服务器 有数十亿的 Read State。每个用户(User)的每个频道(Channel)都有一个 Read State。每个 Read State 都有多个计数器需要自动更新,并且经常会被重置为零。例如,其中有个计数器用来记录你某个频道中被提及了多少次。

为了快速获取原子计数器的更新,在每个 Read State 服务器中都保存了一个 Read State 的最近最少使用(LRU,Least Recently Used)的缓存。每个缓存中都有数百万的用户,每个缓存中又会有数千万的 Read State。每秒钟会有成千上万的缓存更新。

对于持久化来讲,我们使用 Cassandra 数据库集群作为缓存的支撑。在缓存键清除(eviction)的时候,我们会将 Read State 提交到数据库。每当 Read State 更新的时候,我们会将数据库提交调度到未来的 30 秒。每秒钟会有成千上万的数据库写入操作。

在下图中,我们可以看到 Go 服务的峰值采样时间帧的响应时间和 CPU(图表数据基于 Go 1.9.2。我们尝试了版本 1.8、1.9 和 1.10 版本,但没有任何改善。从 Go 到 Rust 的第一次切换是在 2019 年 5 月完成。)。正如我们所看到的,基本每两分钟就会出现延迟和 CPU 峰值。

为何每两分钟会出现峰值?

在 Go 中,当缓存键清除时,内存不会立即释放。相反,垃圾收集器每隔一定的时间就会运行一次,以便于查找不再被引用的内存并释放它。换句话说,Go 并不是在内存用完后立即释放,内存会挂起一段时间,直到垃圾收集器确定它真的是不再需要了。在垃圾收集的时候,Go 必须要做大量的工作来确认哪些内存是空闲的,这可能会降低程序的运行速度。

这些峰值看起来确实是垃圾收集器对性能的影响,但是我们所编写的 Go 代码已经非常高效了,内存分配很少。我们并没有制造太多的垃圾。

在深入研究了 Go 的源码之后,我们了解到至少每两分钟,Go 将强制运行一次垃圾收集。换句话说,如果垃圾收集器已经有两分钟没有运行了,不管堆增加了多少,Go 依然会强制运行垃圾收集。

我们认为可以优化垃圾收集器,使其运行地更加频繁,从而防止出现较大的峰值,因此我们在服务中实现了一个端点,在运行时修改垃圾收集器的 GC 百分比。令人遗憾的是,无论我们如何配置 GC 百分比,都不会发生任何变化。为什么会这样呢?事实证明,这是因为我们分配内存的速度不够快,从而导致无法强制垃圾收集频繁进行。

我们继续深入研究,发现出现如此大的峰值并不是因为有大量待释放的内存,而是因为垃圾收集器要扫描整个LRU 缓存,以便于确定内存是否完全没有被引用。鉴于此,我们认为更小的 LRU 缓存会更快,因为垃圾收集器要扫描的内容会更少。所以,我们在服务上添加了另外一项配置,允许修改 LRU 缓存的大小,并修改了架构,让每台服务器上能有许多的 LRU 缓存分区。

我们是正确的。LRU 缓存越小,垃圾回收的峰值越小。

但是,缩小 LRU 缓存的代价就是第 99 个百分位延迟时间的增长。这是因为,如果缓存比较小的话,用户的 Read State 在缓存中的几率就会降低。如果它不在缓存中,那么我们就需要进行数据库加载。

对不同的缓存容量进行了大量的负载测试之后,我们发现了一个看起来还不错的设置。虽然这不能让人完全满意,但是也是可以接受的,而且当时还有更重要的事情要做,所以我们让服务就这样运行了很长一段时间。

在那段时间里,我们看到 Rust 在其他地方越来越成功,于是我们一致决定要完全基于 Rust 创建用于构建新服务所需的框架和库。这个服务是移植到 Rust 的最佳候选,因为它很小而且是自包含的,但是我们也希望 Rust 能够修复这些延迟峰值的问题。所以,我们接受了将 Read States 移植到 Rust 的任务,希望 Rust 是一门合格的服务语言并且提升用户体验(澄清一下,我认为,你们并不应该为了要使用 Rust,就将所有的服务使用 Rust 重写一遍)。

Rust 中的内存管理

Rust 非常快并且节省内存:它没有运行时和垃圾收集器,能够支撑性能关键型的服务、可以运行在嵌入式设备中并且能够很容易地与其他语言集成(引自 Rust 官网)。

Rust 没有垃圾收集,所以我们认为它不会有与 Go 相同的延迟峰值问题。

Rust 使用了一种比较独特的内存管理方法,其中包含了内存“所有权”的概念。简而言之,Rust 会跟踪谁能够读写内存。它知道程序什么时候使用内存,并在不再需要内存的时候立即释放它。它在编译时强制执行内存规则,这样它根本不可能出现运行时内存错误(当然,除非你使用 unsafe)。我们不需要手动跟踪内存,编译器会处理它。

因此,在 Read States 服务的 Rust 版本中,当用户的 Read State 从 LRU 缓存中清除时,它会立即从内存中释放。Read State 内存不会等待垃圾收集器来收集它。Rust 知道它不会再使用了,并立即释放它。在 Rust 中并没有运行时进程来确定是否应该释放它。

异步的 Rust

但是,Rust 生态系统有一个问题。在这个服务重新实现的时候,Rust 稳定版并没有很好的异步 Rust 功能。但是对于网络服务来说,异步编程是必需的。有一些社区库支持异步 Rust,但是它们需要大量的样板式处理,而且错误消息非常模糊不清。

幸运的是,Rust 团队正在努力使异步编程变得更加简单,并且该功能可以在 Rust 不稳定的 nightly 版本中使用。

我从来都不惧怕接受那些看起来很有前途的新技术。例如,我们是 Elixir、React、React Native 和 Scylla 的早期采用者。如果某项技术很有前途,并能够给我们带来好处,我们不介意处理其固有的困难和不稳定性。这也是我们在不到 50 名工程师的情况下能够快速达到 2.5 亿用户的方法之一。

接受 Rust nightly 版本的异步特性就是我们愿意拥抱新的、有前途的技术的另外一个佐证。作为一个工程团队,我们认为值得使用 Rust nightly 版本,并承诺为 nightly 版本做出提交贡献直到异步功能在稳定环境下得到完全支持。我们一起处理出现的各种问题,此后 Rust 稳定版支持了异步 Rust(参见该网址)。终于苦尽甘来。

实现、负载测试和发布

实际的重写相当简单。首先,我们有一个大致的转换,然后我们把它进行有意义的优化。例如,Rust 有一个很好的类型系统,对泛型提供了广泛的支持,因此我们可以抛弃那些仅仅因为缺少泛型而存在的 Go 代码。另外,Rust 的内存模型能够推断出线程之间的内存安全性,因此我们能够抛弃 Go 中所需要的跨 goroutine 的内存保护。

刚开始进行负载测试时,我们马上就对结果感到非常满意。Rust 版本的延迟和 Go 版本一样好,而且没有延迟峰值!

值得注意的是,在编写 Rust 版本时,我们只对性能优化进行了非常基本的思考。即使只是基本的优化,Rust 也能够超越手动调优的 Go 版本。这深切证明了相对于深入研究 Go,使用 Rust 编写高效的程序有多么的容易。

但我们并不满足于简单地匹配 Go 的性能。经过一些性能分析和性能优化之后,我们能够在每个性能指标上击败 Go。在 Rust 版本中,延迟、CPU 和内存指标都更好。

Rust 版本中的性能优化包括:

在 LRU 缓存中,更改为使用 BTreeMap 取代 HashMap 以优化内存占用。将最初的指标库替换为使用现代 Rust 并发功能的指标库。减少我们正在执行的内存副本的数量。对此感到满意之后,我们决定推出这项服务。

由于我们进行了负载测试,所以发布过程相当顺利。我们把它放到一个金丝雀部署的节点上,查找到一些缺失的边缘情况,并修复了它们。不久之后,我们就把它推广到整个环境之中。

以下是测试的结果,Go 是紫色的线,Rust 是蓝色的线。

提高缓存的容量

在服务成功运行了几天之后,我们决定重新提高 LRU 的缓存容量。如上所述,在 Go 版本中,提高 LRU 缓存上限会导致更长的垃圾收集时间。现在,我们不再需要处理垃圾收集,因此我们认为可以提高缓存的上限并能够获得更好的性能。我们增加了内存容量,优化了数据结构以使用更少的内存 (仅仅为了好玩),并将缓存容量增加到 800 万条 Read States。

下面的结果不言自明。注意,现在平均时间以微秒计算,获取提及数的最大耗时以毫秒计算。

生态系统的演化

最后,Rust 的另一个好处是它有一个快速演化的生态系统。最近,tokio(我们使用的异步运行时) 发布了 0.2 版。我们进行了升级,它免费带来了 CPU 方面的优化。下面你可以看到 CPU 在 16 号左右开始就一直很低。

最后的思考

现在,我在其软件栈的许多地方都在使用 Rust。我们将它用于游戏 SDK、Go Live 的视频捕获和编码、Elixir NIFs 以及其他几个后端服务等等。

当开始一个新项目或软件组件时,我们都会考虑使用 Rust。当然,我们只在有意义的地方使用它。

除了性能之外,Rust 对于工程团队还有许多好处。例如,如果产品需求发生了变化,或者发现了关于该语言的新知识,Rust 的类型安全性和借用检查器(borrow checker )使代码重构变得非常容易。除此之外,Rust 的生态系统和工具都是非常优秀的,它们背后有强大的驱动力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/601752.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

window环境运行nacos源码

为了研究一下nacos,所以从git上下载了nacos源码并且启动。 1.首先下载源码 git地址:https://github.com/alibaba/nacos.git 2.使用ideal打开源码 nacos的启动通过nacos-console模块执行。 3.修改为单机启动模式 Nacos默认启动是集群模式,单机模式需…

c++的学习之路:17、stack、queue与priority_queue

摘要 本文主要是介绍一下stack、queue、priority_queue的使用以及模拟实现,文章末附上代码以及思维导图。 目录 摘要 一、stack的介绍和使用 1、stack的介绍 2、stack的使用 3、stack的模拟实现 二、queue的介绍和使用 1、queue的介绍 2、queue的使用 3、…

填字母游戏【蓝桥杯】/博弈+dfs

填字母游戏 博弈dfs #include<iostream> #include<map> using namespace std; //要用map存储已经处理过的字符串不然会超时 map<string,int> m; //dfs返回的就是结果 int dfs(string s) {//剪枝if(m.find(s)!m.end()) return m[s];//找到LOL代表输了if(s.fi…

K8s学习七(服务发现_2)

Ingress Service 主要用于集群内部的通信和负载均衡&#xff0c;而 Ingress 则是用于将服务暴露到集群外部&#xff0c;并提供灵活的 HTTP 路由规则。在实际应用中&#xff0c;它们通常结合使用&#xff0c;Service 提供内部通信和负载均衡&#xff0c;Ingress 提供外部访问和…

一款轻量、干净的 Laravel 后台管理框架

系统简介 ModStart 是一个基于 Laravel 的模块化快速开发框架。模块市场拥有丰富的功能应用&#xff0c;支持后台一键快速安装&#xff0c;让开发者能快的实现业务功能开发。 系统完全开源&#xff0c;基于 Apache 2.0 开源协议&#xff0c;免费且不限制商业使用。 系统特性 …

产品经理和项目经理的区别

1. 前言 本文深入探讨了产品经理与项目经理在职责、关注点以及所需技能方面的显著区别。产品经理主要负责产品的规划、设计和市场定位,强调对用户需求的深刻理解和产品创新的推动;而项目经理则侧重于项目的执行、进度控制和资源管理,确保项目按时、按质、按预算完成。两者在…

redis的简单操作

redis中string的操作 安装 下载可视化软件&#xff1a;https://gitee.com/qishibo/AnotherRedisDesktopManager/releases。 Mac安装redis&#xff1a; brew install redisWindows安装redis: 安装包下载地址&#xff1a;https://github.com/tporadowski/redis/releases 1.…

【JavaWeb】Day38.MySQL概述——数据库设计-DQL

数据库设计——DQL 介绍 DQL英文全称是Data Query Language(数据查询语言)&#xff0c;用来查询数据库表中的记录。 查询关键字&#xff1a;SELECT 查询操作是所有SQL语句当中最为常见&#xff0c;也是最为重要的操作。在一个正常的业务系统中&#xff0c;查询操作的使用频次…

最长上升子序列(线性dp)-java

主要是解决最长上升子序列问题&#xff0c;推出状态转移方程。 文章目录 前言 一、最长上升子序列问题 二、算法思路 1.最长上升子序列思路 三、代码如下 1.代码如下&#xff08;示例&#xff09;&#xff1a; 2.读入数据 3.代码运行结果 总结 前言 主要是解决最长上升子序列问…

【深入理解Java IO流0x03】解读Java最基本的IO流之字节流InputStream、OutputStream

在开始前&#xff0c;我们再来回顾一下这张图&#xff1a; 本篇博客主要为大家讲解字节流。 我们都知道&#xff0c;一切文件&#xff08;文本、视频、图片&#xff09;的数据都是以二进制的形式存储的&#xff0c;传输时也是。所以&#xff0c;字节流可以传输任意类型的文件数…

【SERVERLESS】腾讯云上实操

我们先来看看 云计算的 发展 流程 虚拟化的云计算方式&#xff1a; IaaS基础设施即服务【邮箱、微信、支付宝等】PaaS平台即服务【数据库服务、大数据计算平台】Saas软件即服务【云服务器、cpu、硬盘】 早期我们可能只听过上面这三个词 ,但是随着云计算的发展&#xff0c;新…

HIS系统是什么?一套前后端分离云HIS系统源码 接口技术RESTful API + WebSocket + WebService

HIS系统是什么&#xff1f;一套前后端分离云HIS系统源码 接口技术RESTful API WebSocket WebService 医院管理信息系统(全称为Hospital Information System)即HIS系统。 常规模版包括门诊管理、住院管理、药房管理、药库管理、院长查询、电子处方、物资管理、媒体管理等&…