目录 一、引言 二、Rust核心特性 1. 所有权 2. 生命周期和引用 三、用Rust构建生产级应用 1. 合理利用引用减少数据拷贝 2. FFI(Foreign Function Interface) 3. Tokio 四、Rust应用发布 1. 上传镜像 2. 发布 3. 上监控 五、结论 在流量日益增长的今天,随着用户需求的不断增加和性能要求的提升,一个能够更好地处理高并发、低延迟和资源有效利用的计算层是十分重要的。尽管在过去我们平台使用Java开发的计算层提供了稳定的服务支撑,但面对日益增长的流量和低延迟的需求,Java不可避免地开始显现局限性: 垃圾回收:Java 的自动内存管理依赖于垃圾回收机制,而垃圾回收虽然简化了开发工作,却可能引入不可预测的延迟。 内存使用效率:Java 的内存管理通常比手动管理的语言消耗更多的内存,因为它必须保留足够的空间来处理对象分配和回收。 异步处理瓶颈:虽然Java近年来强化了异步编程支持,但在极限性能优化方面,仍存在不可忽视的不足。 在此背景下,经过调研和实验验证,我们发现了Rust这个计算层改造升级的语言选型。Rust语言以其出色的内存管理、安全性和高效性能而闻名。Rust的所有权模型可以在编译时捕捉大多数内存错误,从而减少运行时错误,这对需要高可靠性和稳定性的系统尤为重要。此外,Rust没有垃圾回收机制,这意味着我们可以更好地预测和控制内存使用,提高应用程序的性能和资源利用率。 通过使用Rust对计算层改造升级,我们的系统获得了如下的提升: 相比于Java,减少了30%的CPU核数。 高效内存管理,减少了70%的内存使用。 服务更稳定,Bug少。 Rust 能够突破传统编程语言的瓶颈,主要得益于其独特的所有权、借用和生命周期机制。这些特性使 Rust 在编译阶段就能够确保内存安全和线程安全,从而最大程度地减少运行时错误和不确定性。接下来,我们将深入探讨 Rust 在并发模型、所有权、生命周期和借用方面的优势。 所有权 Rust 的所有权(Ownership)是该语言独特的内存管理机制,它确保内存安全性和并发性而不需要垃圾回收器。所有权机制通过编译时检查来保证安全性,避免绝大多数的运行时错误,例如空指针或数据竞争。 Rust所有权规则 Rust的所有权有三个主要规则: 所有值(除Copy类型)有且只有一个拥有者。 当所有者离开作用域,值会被自动释放,不需要手动回收。 值的所有权可以被移动或者借用。 为了方便理解,这里展示Rust、C++和Java对象赋值的异同来理解所有权的运行机制。 可以看到,将a赋值给b时,Java会将a指向的值的引用传递给b,而C++则会产生一个新的副本。从某种意义来说,在内存管理上,Java和C++选择了相反的权衡。代价是Java需要垃圾回收来管理内存,而C++的赋值会消耗更多的内存。不同于Java和C++,Rust选择了另一种方案:移动所有权。即将a指向的堆内存地址“移动到b上”,这时只有b可以访问这段内存,a则成为了未初始化状态并禁止使用。 Rust的所有权概念内置于语言本身,在编译期间对所有权和借用规则进行检查。这样,程序员可以在运行之前解决错误,提高代码的可靠性。 共享所有权 尽管Rust规定大多数值会有唯一的拥有者,但在某些情况下,我们很难为每个值都找到具有所需生命周期的单个拥有者,而是希望某个值在每个拥有者使用完后就自动释放。简单来说,就是可以在代码的不同地方拥有某个值的所有权,所有地方都使用完这个值后,会自动释放内存。对于这种情况,Rust提供了引用计数智能指针:Rc和Arc。 Rc和Arc非常相似,唯一的区别是Arc可以在多线程环境进行共享,代价是引入原子操作后带来的性能损耗。Rc和Arc实现共享所有权的原理是,Rc和Arc内部包含实际存储的数据T和引用计数,当使用clone时不会复制存储的数据,而是创建另一个指向它的引用并增加引用计数。当一个Rc或Arc离开作用域,引用计数会减一,如果引用计数归零,则数据T会被释放。这种机制也叫共享所有权机制。 这时就有好奇的小伙伴问了,既然可以在多个地方共享所有权,那不是违背了所有权的初衷,从而引入了数据竞争的问题?放心,Rust的开发者早就想到了这个问题,引用计数智能指针是内部不可变的,即无法对共享的值进行修改。那这就又引入了一个问题:如果要对共享的值进行修改怎么办?对于这种情况Rust也提供了解决方案,使用Mutex等同步原语即可避免数据竞争和未定义行为。以下是一个案例,如何在多线程访问数据,并安全的进行修改。 生命周期和引用 在 Rust 中,生命周期(lifetimes)和引用(references)是两个密切相关的概念,它们共同构成了 Rust 的所有权系统的重要组成部分。生命周期用于确保引用在使用时是有效的,从而防止悬空引用和数据竞争等问题。 引用 前面提到,Rust值的所有权可以被借用,它允许在不获取数据所有权的情况下访问数据。Rust中有两种类型的引用: 不可变引用 (&T):允许你读取数据,但不允许修改。 可变引用 (&mut T):允许你修改数据。 在使用引用的时候需要满足以下规则: 在同一时间只能有一个可变引用。 多个不可变引用可以同时存在,但在可变引用存在时,不能有不可变引用。 每个引用都有一个生命周期,表示该引用在程序中的有效范围,且引用的生命周期不能超过被借用的值的生命周期。 生命周期 在 Rust 编程语言中,生命周期用于确保引用在使用时是有效的。生命周期的存在使得 Rust 能够在编译时检查引用的有效性,从而防止悬空引用。如下是一个Rust编译器检查生命周期的例子: 这里编译器将r的生命周期记为'a,x的生命周期记为'b。可以明显看出,内部块的'b比外部块的'a生命周期小,当x离开作用域被释放时,r仍然持有x的引用。所以当把生命周期为'a的r想引用生命周期为'b的x时,编译器发现了这个问题,并拒绝通过编译,保证了程序不会出现悬垂引用。 生命周期标注 正如我们看到的,Rust的引用代表对值的一次借用,它们有着种种限制,所以,在函数中、在结构体中等等位置上使用引用时,你都要给Rust编译器一些关于引用的提示,这种提示,就是生命周期标记。对于简单的情况,聪明的Rust编译器可以自动推断出引用的生命周期。对于一些模棱两可的情况,编译器也无法推断引用是否在程序运行期间始终有效,这时就需要我们提供生命周期标注来提示编译器我们的代码是正确的,放我过去吧。 生命周期标注并没有改变传入的值和返回的值的生命周期,我们只是向借用检查器指出了一些用于检查非法调用的一些约束而已,而借用检查器并不需要知道 x、y 的具体存活时长。而事实上如果函数引用外部的变量,那么单靠 Rust 确定函数和返回值的生命周期几乎是不可能的事情。因为函数传递什么参数都是我们决定的,这样的话函数在每次调用时使用的生命周期都可能发生变化,正因如此我们才需要手动对生命周期进行标注。 相信第一次看到生命周期的小伙伴们都感觉概念非常难理解,且写出的代码非常丑,简直要逼死强迫症。但是有得就有舍,要写出安全且高效的Rust代码,就要学会理解和使用生命周期。如果实在不想用,那就多用Rc和Arc吧。 了解了Rust最核心的基本知识和特性后,你已经成为了一个合格的Rust练习生,可以开始用Rust愉快的进行开发工作了。但是要使用Rust开发高性能的生产级应用,只了解到这种程序是不行的。当初笔者信心满满地将第一个Rust应用发布到测试环境后,竟然发现效率比Java版本还低,于是开始了长期的瓶颈排查和调优,且调优时间远大于编码时间。最终我们的应用在相同吞吐量的条件下,CPU使用率从高于Java 20%优化到低于Java 40%。在这个过程中,也总结了一些经验进行分享。 合理利用引用减少数据拷贝 相信很多刚接触Rust的小伙伴在面对同一份数据需要在多处使用的情况时,为了逃避复杂的生命周期问题,会倾向于使用Clone来创建数据副本。如果这样做的话,一份数据在内存中重复出现多次,带来的cpu和内存消耗会让你会怀疑人生,为什么这么相信Rust的性能而不相信自己能啃下生命周期这块硬骨头呢? 有一个应用场景,我们从数据源得到若干个源数据,根据业务逻辑聚合成batch并存储到远端或者本地。聚合的逻辑可以有两种方式: 将源数据的所有权移动到batch。 将源数据拷贝一份到batch。 然而这两种方式都不可取。第一种方式的问题是,我们不知道一份源数据是不是只会被使用一次。而使用第二种方式则会消耗更多的CPU,且占用内存成倍上升。 前面提到,Rust的值是可以借用的,如果在batch中不获得所有权,而是存储引用,那么可以几乎零消耗的实现需求。以上述应用场景为例,这里介绍我们是怎么解决这个问题的。 首先给出源数据Data和Batch的定义: 假设需求是将Data的msg字段在Batch里存储num次,我们很容易写出这样的代码: 看起来是不是很合理,和其他语言也没有什么区别,当信心满满按下编译后,会发现天空飘来五个字:编译不通过。原因很简单,因为编译器发现被引用对象data的生命周期小于batch,data的在当前循环结束后就会销毁,batch存储的引用就变成了野指针。我们可以做如下修改: 可以看到,我们对代码做了一些小改动: 在循环外初始化了一个Vec,并保存每次得到的data。 record_data函数上增加了生命周期标注。 为什么这么做呢?我们已经知道最初版本是因为data的生命周期小于batch,导致batch不能存储data的引用。解决这个问题的思路很简单,提升data的生命周期不就完了。假设batch的生命周期是'a,data的生命周期是'b,很明显'a是大于'b的,因为batch的生命周期是整个main函数,而data的生命周期仅仅在loop内。我们在batch同样的作用域内定义一个容器,它的生命周期也是'a。在每次得到data后把它存入容器中,那data就不会在循环结束的时候被销毁了。 同时,在record_data函数定义上,我们也要使用标注告诉编译器batch和data的生命周期是相等的。如果data的生命周期大于batch,我们也可以在参数中定义data的生命周期为'a,因为实际的生命周期和参数生命周期标注无需一致,只需要实际的生命周期大于参数生命周期就行了。如果你有强迫症,也可以在参数中标注实际的生命周期,只需要加上适当的生命周期约束就行了: 经过这些小改动,你的应用会比粗暴的使用拷贝提升许多性能并且节约大量内存使用。经过我们的测试,在类似需求中将需要大量拷贝的操作替换成引用,可以节省一倍的内存,CPU使用率也下降了20%。 FFI (Foreign Function Interface) 在一些情况下,我们项目使用的编程语言在实现一些功能时,想使用现成的依赖库来实现复杂的逻辑,但是因为生态不完善,导致缺少此类库或者现存的依赖库不成熟。在使用Rust时,这种现象尤其普遍。很多热门组件没有为Rust提供官方API,非官方实现功能和性能又得不到保证,且更新不稳定。难道Rust进阶之路就要到此为止? Rust很贴心地提供了跨语言交互能力,对FFI的良好支持可以让开发者方便的在Rust代码中调用C程序。如果我们需要的依赖库刚好有C/C++的实现,就能使Rust完成主要逻辑,把一些Rust不完善的功能通过C/C++实现,而且性能也不会受到影响。在Rust程序调用C代码也非常简单: 1. 声明外部函数 2. 在RUST中调用C函数 3. 将C程序编译打包为静态/动态链接库 4. 然后编译 Rust 文件并链接到链接库 尽管用Rust调用C程序已经非常方便,但是仍需要注意这些问题: 处理数据类型:在 Rust FFI 中,需要特别注意数据类型的转换和处理。Rust 和其他语言的数据类型可能存在差异,需要进行适当的转换。例如,Rust的i32和C的int可以直接相互转换。而字符串的传递之所以需要特殊处理,是因为Rust的字符串实现和C/C++不一样。C/C++的字符串指针只包含地址,且字符串后有“


{let counter = Arc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let counter_clone = Arc::clone(&counter);let handle = thread::spawn(move || {// 锁定 Mutex 以安全地访问数据let mut num = counter_clone.lock().unwrap();*num += 1; // 修改数据});handles.push(handle);}// 等待所有线程完成for handle in handles {handle.join().unwrap();}// 获取最终计数值println!("Final count: {}", *counter.lock().unwrap());}
fn main() {let r;// ---------+-- 'a//|{ //|let x = 5;// -+-- 'b|r = &x; //| |} // -+ |//|println!("r: {r}"); //|}
struct Data {condition: bool,num: i32,msg: String}
struct Batch<'a> {msgList: Vec<&'a str>}
fn main() {let batch: Batch = Batch:new();// 初始化Batchloop {let data:Data = dataSource.getData();// 从数据源获得datarecordData(batch, &data);if (batch.len() > 100) {// batch存储的数据大于100条时,存储并清空save(batch);batch.clear();}// ------------------- data的生命周期到此结束}// ------------------- batch的生命周期到此结束}fn record_data(batch: Batch, data: Data) {if(condition) {// 根据条件将msg保存num次for i in 0..data.num {batch.msgList.push(&data.msg);}}}
fn() {let batch: Batch = Batch:new();// 初始化Batchlet dataList: Vec<Data> = Vec::new();// dataList的生命周期和batch一样loop {let data: Data = dataSource.getData();// 从数据源获得datadataList.push(data);// 将data保存在dataList,提升生命周期if(batch.len() > 100) {for data_ref: &Batch in dataList.iter() {record_data(batch, data_ref);// 此时data的生命周期和batch相等}save(batch);batch.clear();dataList.clear();}}}fn record_data<'a>(batch: Batch<'a>, data: &'a Data) {if(condition) {// 根据条件将msg保存num次for i in 0..data.num {batch.msgList.push(&data.msg);}}}
// 'b: 'a表示'b的生命周期能够覆盖'afn record_data<'a>(batch: Batch<'a>, data: &'b Data) where 'b: 'a {......}
extern "C" {fn c_add(a: i32, b: i32) -> i32;}
fn main() {unsafe {c_add(1, 2); }}
g++ -std=c++17 -shared -fPIC -o libhello.so hello.cpp
rustc main.rs hello.o
暂无讨论,说说你的看法吧


