关于后端:技术专栏丨Rust-语言简介及其在-Fabarta-技术栈中的应用

8次阅读

共计 9924 个字符,预计需要花费 25 分钟才能阅读完成。

导读:Rust 是一门重视性能和平安的零碎编程语言,通过其独特的所有权零碎、借用零碎和类型零碎,胜利地解决了传统零碎编程中的许多难题。其开发者敌对的语法、丰盛的规范库和弱小的社区反对,使得 Rust 成为当今编程畛域中备受关注的语言之一。

01 引言

Rust 曾经不算是一门年老的语言了,其诞生工夫跟 Go 语言差不多。2006 年 Rust 作为 Graydon Hoare 的集体我的项目呈现,2007 年 Google 开始设计 Go。但很显著,Go 的倒退要好得多。Rust 在 2015 年才公布了 1.0 版本,而 Go 在 2016 年曾经成为了 TIOBE 的年度语言。相较而言 Rust 的倒退和前景仿佛不怎么好,但其实这与 Rust 语言的定位有十分大的关系。Rust 最后是作为一种在零碎编程畛域里代替 C/C++ 而呈现的语言,其倒退天然要迟缓许多。因为在零碎编程畛域,每走一步都要求十分扎实。

我对 Rust 印象比拟粗浅的有两件事件:首先是看到一篇文章称其学习曲线十分平缓,过后就比拟好奇一门语言能够难到什么水平。其次则是因为 Linus Torvalds 决定在 Linux Kernel 里增加对 Rust 的反对。Linus 以严苛闻名,能受到 Linus 的青眼相对不是一件容易的事件,这阐明 Rust 这门语言必然有独到之处。

最近几年,微软、AWS 等大型商业公司逐步开始应用 Rust 来编写或重写重要零碎。开源界很多器重平安因素的组件,如 sudo/su 也在应用 Rust 进行重写。Rust 除了在零碎编程畛域变得流行起来,也是 WASM 畛域里的举荐语言。这一方面阐明 Rust 语言曾经逐渐成熟,另一方面也阐明了 Rust 有十分强的体现能力,在各个领域都能胜任。所以 Fabarta 在开发多模态智能引擎 ArcNerual 时,通过多方面的衡量后抉择了 Rust 语言。

在过来的一年多的工夫里,团队从 0 到 1 开始学习并利用 Rust,在开发效率、安全性、并发、异步编程等畛域都有深刻的实际,咱们胜利公布了 ArcNerual 多模态智能引擎(ArcNerual 详情能够参见附录 1、附录 2)。回过头来看,这是一个十分胜利的决定。团队成员之前大多具备 C/C++ 的背景,在上手速度上会有一些劣势,前期也有一些 Java/ 前端背景的同学染指,在其余团队成员的帮忙下,上手工夫均不超过三个月,实际下来并不像网上一些文章写的那样有十分平缓的学习曲线。

本文旨在简要叙述 Rust 语言的个性、劣势以及理论生产我的项目中须要的一些注意事项,以便帮忙您评估目前团队背景、适宜畛域、上手老本、相熟周期、收益等各个方面的内容,最终能够正当决策是否要在新的我的项目中引入 Rust 语言。

02 Rust 语言概述

Rust 的倒退经验了多个阶段,从最后的试验性质到逐步成熟为一门零碎级编程语言。Rust 最后只是 Graydon Hoare 的集体我的项目,他那时是 Mozilla 的雇员。所以起初 Mozilla 公司开始反对这个我的项目,并于 2010 年 5 月首次公开公布。在其晚期阶段,Rust 次要是作为 Mozilla 的钻研我的项目,致力于解决在编写浏览器引擎(如 Firefox 的 Gecko 引擎)时所面临的内存平安和并发性等挑战。

起初,其独特的所有权零碎和内存安全性引起了业界的宽泛关注。2010 年之后,Rust 的开发逐步成为一个开源社区驱动的过程,而不再仅仅是 Mozilla 的外部我的项目。随着社区的一直壮大和创始人的离去,Rust 逐步超过了繁多公司的我的项目,成为一个凋谢、多元的编程语言社区,并不受个别核心人物所管制。

Rust 是一种零碎级编程语言,其设计指标能够次要概括为以下三个方面:

  • 安全性(Safety): Rust 的最重要设计指标之一 即是提供高水平的内存安全性与并发安全性。 通过所有权零碎、借用查看器和生命周期机制,Rust 在编译时可能避免许多常见的内存谬误,如空指针援用和缓冲区溢出。这使得开发者可能编写更加强壮、牢靠的代码,缩小了许多传统零碎级编程语言中容易呈现的安全漏洞,当然从某些方面来说,安全性保障引入是 Rust 难以学习的次要起因,但只有相熟并使用切当,这会是一个极大的劣势,实际上 C++ 也开始探讨引入内存平安机制。
  • 高生产力(Productive)“ 能编译就能工作 ”。 C/C++ 的程序员会分明的晓得编译通过只是很小的一步,后续还有十分经典的相似“Segmentation fault”谬误等着去排查与解决,但 Rust 在这方面给了开发者十分强的信念,叠加零老本形象准则,开发者能够释怀的将内存、并发等问题交给 Rust,从而极大的晋升开发效率。
  • 实用性(Practicality): Rust 的设计旨在成为一门实用的零碎级编程语言,“you can do anything with Rust”。 Rust 不仅具备对底层硬件的间接控制能力,同时容许开发者应用高级形象来表白简单概念。它在性能和开发者敌对性之间找到均衡,使得开发者可能编写高性能的代码,同时又不就义开发者的便利性。

受害于 LLVM(Low Level Virtual Machine)这样的编译器基础设施,Rust 在这些设计指标的领导下实现了古代语言的所有特色:

  • 动态类型零碎:在编译时查看类型,这有助于提前捕捉谬误,进步代码的稳定性和可维护性。Rust 有一套弱小的类型零碎来帮忙咱们正确地编写代码。
  • 主动内存治理:加重开发者手动治理内存的累赘。古代语言个别通过垃圾回收(garbage collection)机制来实现,Rust 应用了独特的所有权与借用查看零碎,在性能上更有劣势。
  • 模块化设计:模块化对软件工程的重要性显而易见,在此基础之上,Rust 更进一步提供了先进的包治理与构建工具 Cargo,能够十分不便的治理模块及依赖。
  • 丰盛的编程范式:无论是过程式还是面向对象编程和函数式编程,Rust 均提供了弱小的反对,这使得开发者可能抉择适宜特定问题的编程格调。
  • 异样解决:站在伟人的肩膀上,提供对立、高效的机制来解决异样,使得开发者可能更容易地辨认和解决运行时谬误。
  • 多线程和并发反对、跨平台性。

除此之外,Rust 有一些独特或值得强调的特色:

  • 所有权零碎:独特的所有权零碎确保内存治理的安全性,防止了悬垂指针和内存透露等问题。
  • 借用和生命周期:引入借用和生命周期的概念,使得在不移交所有权的状况下平安地拜访和批改数据。
  • 零老本形象:容许开发者应用高层次的形象,但不引入额定的运行时开销,放弃了零碎编程语言的高性能个性。
  • 模式匹配:弱小的模式匹配语法使得开发者可能优雅地解决各种简单的数据结构和状态。
  • 异步编程反对:async/await 语法和 Future trait 的引入使得异步编程更加易读和高效。
  • 元编程:独特而弱小的宏编程零碎。
  • 敌对的 FFI(Foreign Function Interface):容许 Rust 与其余编程语言特地是 C 语言进行交互。从而实现跨语言的协同开发与利用现有资产。

在本文中,咱们次要通过所有权零碎、借用和生命周期、零老本形象这几个个性来深刻察看一下 Rust 语言,其余的一些个性能够关注本专栏后续的专题解读文章。

03 Rust 语言个性

所有权零碎

所有权零碎能够说是 Rust 中最为独特和外围的性能了,它旨在解决内存安全性和并发性问题。正是所有权概念和相干工具的引入,Rust 才可能在没有垃圾回收机制的前提下保障内存平安。同时,这也是大家认为 Rust 难学的根本原因,因为它对整个语言产生了影响,让 Rust 看起来和其余语言十分不一样。

所有权零碎自身并不简单,简略的说,就是在 Rust 中,Rust 中的每一个值都有一个对应的变量作为它的所有者,在同一时间内,值有且仅有一个所有者,举一个简略的例子:

let s1 = String::from("hello, world!");
let s2 = s1;
println!("{}, world!", s1);  // 编译出错,s1 不再可用了。

如果下面这个简略的例子让你有新奇感,这就阐明了 Rust 语言的不同。所有权零碎的引入,让 Rust 默认应用一种叫做“挪动”的语义。在下面这个例子中,s1 被挪动到了 s2,要了解挪动及其老本,你须要了解一个变量所占用的内存什么时候在栈上调配、什么时候在堆上调配以及他们在内存中的大抵布局。这与领有垃圾回收机制的语言是不同的,也是零碎编程语言里须要时刻关注的概念。

毫无疑问,所有权零碎的引入减少了编程时的复杂度与一些内存拷贝操作(绝对于 C/C++ 来说),但这个老本是必要的。所有权零碎定义了一套规定,定义了在程序执行过程中如何传递、借用和开释这些值。要害的概念包含“借用”、“生命周期”。接下来,咱们来进一步看看。

借用和生命周期

借用和援用

不是所有的中央都须要挪动所有权,借用和援用正是为了在不挪动所有权的状况下拜访值。借用能够是不可变的(\&T)或可变的(\&mut T),容易推导出一个变量能够有多个不可变借用,但只容许同时有一个可变的借用:

  • 不可变借用:应用 \&T 语法,容许多个中央同时援用雷同的值,但这些援用是只读的,不能批改原始值。这有助于防止数据竞争和并发问题。
fn main() {
    let original = 42;
    let reference = &original;  // 不可变借用
    println!("The original value is: {}", original);
    println!("The reference value is: {}", reference);
}
  • 可变借用:应用 \&mut T 语法,容许代码获取对值的可变援用,但在同一时间只能有一个可变援用存在。这防止了数据竞争,确保在任何时候只有一个中央可能批改值。
fn main() {
    let mut original = 42;
    let reference = &mut original;  // 可变借用
    *reference += 10;  // 批改原始值
    println!("The modified value is: {}", original);
}

生命周期

生命周期是 Rust 用来治理援用有效性的一种机制。它们是一系列标注,用于批示援用在代码中的无效范畴,以便编译器在编译时查看援用的合法性。简略的说 Rust 会在产生借用的中央调配一个生命周期,从而进行援用有效性的查看。晚期的 Rust 应用一个非常简单的基于词法的办法调配生命周期,有很多中央依赖开发者进行标注,给开发者造成了较大的限度与累赘。但随着 RFC-2094-nll(Non Lexical Lifetimes)的优化,Rust 编译器的推导能力越来越强,须要开发者标注的中央曾经大大减少。

生命周期应用单引号来示意,比方上面的函数中,Rust 自身没有方法晓得 s1 和 s2 的生命周期,须要依赖开发者指出,其中 ‘a 即为生命周期的标注:

fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {if s1.len() > s2.len() {s1} else {s2}
}

生命周期的引入使得 Rust 可能在不放弃内存安全性的同时,容许灵便的数据援用和传递。同时编译器可能在编译时进行查看,确保代码的正确性,是借用查看的幕后功臣。针对生命周期引入的一些限度及学习老本,Rust 成立了 Polonius 工作组来对借用查看进行进一步优化,置信很快开发者不再需关注这个概念,让生命周期真正退居幕后。

零老本形象

零老本形象是 Rust 中的一项根本准则,体现在语言设计的各个方面。零老本形象是指在高级别表白代码概念时,底层生成的机器码可能放弃高效执行,而不引入运行时性能开销。这一个性使得开发者可能以更形象、更通用的形式编写代码,而不用放心形象带来的性能损耗,“在编译时能解决的,不留到运行时”

这体现在十分多的方面:

  • 比方通过所有权零碎解决主动内存治理,而不是引入垃圾回收。
  • Rust 中的泛型(Generics)容许开发者编写实用于不同数据类型的通用代码。通过在编译时实例化泛型代码,Rust 能够生成与手动编写特定类型代码相媲美的性能,同时确保代码的通用性和灵活性。
  • 引入了 trait 的概念,使得开发者可能定义形象接口。通过 trait,能够在不同类型上实现雷同的行为,而不会引入额定的运行时开销。这种形式容许代码更好地适应多态性,同时放弃高性能。
  • Rust 弱小的宏零碎(Macros)容许开发者编写模式匹配和代码生成的通用模板,这些宏在编译时开展,生成理论的代码。通过宏,能够在代码中引入更高层次的形象,而不会导致性能损耗。

这些零老本形象的机制与准则,使得 Rust 成为一门同时谋求性能和形象的古代编程语言,在保障开发效率的同时不损失性能。

04 Fabarta 对 Rust 的利用

在实现 ArcNerual 的时候,咱们的开发语言选型次要思考几个方面:

  • 不心愿手动治理内存以晋升开发效率以及防止由内存引起的安全性及谬误。
  • 因为是底层数据库系统,所以也不想引入垃圾回收器而造成的不可预感的零碎卡顿。
  • 有沉闷的社区与丰盛高质量的三方库。
  • 不便利用宏大的其余语言特地是 C/C++ 资产。
  • 在满足上述条件的状况下,尽可能的高性能。

从以上这几个方面登程,咱们没有费特地多的时间就选定了 Rust。抉择一门新的语言,在最开始写代码的时候,总是会写成本人最相熟的那门语言。以咱们团队为例,团队成员大多具备 C/C++ 背景,所以最开始的代码就是 C/C++ 格调,而后随着一直的相熟与深刻,开始缓缓造成一些最佳实际。一些计划会依据编写的零碎、团队的偏好、零碎的压力而有所不同。从 Fabarta 的实际来看,咱们认为以下几个问题在最后就须要特地留神。

编程范式 / 设计模式的抉择

在很多语言中这都不是一个问题,或者语言自身曾经帮你做出了抉择。比方 C++/Java 中,个别都会抉择面向对象的模式。然而 Rust 中稍有不同,他并没有提供一种次要的编程范式。你能够依据我的项目的需要和编写代码的上下文抉择不同的范式或模式,以充分发挥 Rust 的灵活性和表达力。Rust 反对多种编程范式,包含面向对象、函数式、过程式等,同时还具备弱小的模式匹配和零老本形象的能力,而不用拘泥于其中的一种,这会让你的代码更加简洁高效。

比方在面向对象编程中,咱们常常应用继承来实现多态。但 Rust 并不反对继承,而是应用 trait 来提供动静散发机制。比方在 ArcNerual 中咱们会提供很多数据库算子,这些算子具备雷同的行为,咱们将这些雷同的行为形象为 trait:

pub trait Processor {fn do_process(&self) {...}
} 

不同的算子会依赖不同的数据:

struct VertexScan {...}

struct Filter {...}

但它们有雷同的行为:

impl Processor for VertexScan {fn do_process(&self) {...}
}

impl Processor for Filter {fn do_process(&self) {...}
}

咱们能够应用动静散发模式来实现多态:

fn run_processor(op: &dyn Processor) {op.do_process();
    ...
}

这一方面算是一种对象编程的模仿,动静散发相对来说会有一些运行时开销,编译期的一些优化比方内联优化无奈在动静散发的代码中实现,然而通过 Rust 的模式匹配,齐全能够将上述行为转为动态散发,在下面的例子中,咱们当时晓得有哪些算子,所以能够应用 Rust 弱小的 enum:

pub enum AnyProcessor {VertexScan(VertexScan),
    Filter(Filter),
    ...
}

impl Processor for AnyProcessor {fn do_process(&self) {
        match self {AnyProcessor::VertexScan(vertex_scan) => vertex_scan.do_process(),
            AnyProcessor::Filter(filter) => filter.do_process(),
            ...
        }
    }
}

而这些减少的代码能够通过宏的形式轻松去除。在 ArcNerual 的实现过程中,咱们充沛应用了 Rust 多范式的灵活性来升高代码的复杂度与晋升性能。业界在不同的畛域也积淀出了不同的模式,比方游戏编程里提出了一种 ECS(Entity-Component-System)模式,团队在应用 Rust 之初,能够多思考这个问题,联合 Rust 的多范式编程,会对咱们解决问题有十分大的帮忙。

并发模式的抉择

出于以下几个起因的思考,与编程范式一样,Rust 反对多种并发模式并把选择权也交给了开发者:

  • Rust 须要有管制底层的能力,所以要提供和 OS 过程 / 线程绝对应的概念。
  • Rust 不能有一个特地大的运行时,所以没有像 Go 一样提供协程(晚期已经有过 green thread,曾经去除)。
  • 强调零老本形象,不须要为用不到的能力付费。

所以你须要据本人的需要抉择相应的并发模型。如果是计算密集型程序,那简略地应用操作系统多线程模型就足够了;如果是 IO 密集型,可能事件驱动模型就能满足需要。

在 Rust 生态系统中,异步编程变得越来越广泛,特地是在解决网络和大规模并发的应用程序中。对于 ArcNerual 来说,有大量的 IO 操作,也有大量的计算操作,所以 咱们抉择了异步模式并应用了 tokio 运行时,这里的要害是要尽早做出抉择。尽管能够在应用异步模型时,同时应用其余并发模型,但强烈建议不要这么做或者思考分明后再决定。因为在实质上,同步和异步是两套不兼容的并发模型。异步中引入同步代码是一个十分大的危险,可能会引起各种意料之外的情况。在选定异步模式后,对于应用到的锁、通信等依赖包也须要审慎引入。在 ArcNerual 的开发过程中,咱们就遇到过因为引入不适用于异步编程的库而引起程序阻塞。对于异步编程与 tokio 方面的实际能够参见文末附录 3。

safe/unsafe 的应用

在最后 Rust 的借用查看器可能会让你感到莫名的阻力,Rust 社区也经常有这方面的探讨,比方在《Rust Koans》(详见附录 4) 这篇文章中就活泼的形容了这种感觉。当 C/C++ 程序员发现有 unsafe Rust 的时候可能会眼前一亮,终于回到了相熟的畛域,然而,团队必须明确在什么场景下能够用 unsafe Rust,依据咱们实际的教训,初期仅倡议在调用内部代码的时候应用,前期能够在性能优化的时候有针对性的进行引入,须要及早和团队明确,避免 unsafe 代码失控。

内存调配

过来零碎级编程语言中始终偏向于由程序员来治理内存,这次要是基于性能方面的考量,因为一次内存调配操作相对来说是比拟耗时的。有一些语言比方 Zig,强调没有隐式内存调配,每一个内存调配操作均须要程序员来操作并带入自定义内存分配器。Rust 作为一门以主动内存治理为重要特色的零碎级编程语言,其实是综合了几种语言的个性,目前在内存调配方面整体显得比较复杂而凌乱,这也是 Rust 社区目前面临的一个重要课题。Rust 最后应用的内存分配器是 Jemalloc,起初因为 Jemalloc 的多平台反对问题、Rust 程序嵌入问题等起因,切换为各个系统的默认分配器。

目前 Rust 会应用零碎的默认分配器,在 Linux 上就是 ptmalloc2。相对来说其性能不如 tcmalloc/jemalloc,Rust 提供了相干接口,能够通过代码进行更改,比方在 ArcNerual 中咱们通过如下代码将全局内存分配器设置为 Jemalloc:

use jemallocator::Jemalloc;

#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

除了更改全局分配器以外,一些容器还反对传入自定义的 allocator,比方 Vec 的残缺定义是:

pub struct Vec<T, A = Global>
where
    A: Allocator,
{/* private fields */}

同时提供了 new\_in / with\_capacity\_in 函数能够传入 allocator,比方:

#![feature(allocator_api)]

use jemallocator::Jemalloc;
let mut vec: Vec<i32, _> = Vec::new_in(Jemalloc);

这样能够批示 vec 在分配内存时从指定的 allocator 里调配。不过这里除了写起来有点麻烦外,更大的问题在于不是所有的数据结构都反对自定义 allocator,比方 String 目前就不反对自定义 allocator。当然能够通过一些 unsafe 的代码来达成自定义分配器的指标,但无疑代码写起来要简单得多:

 let size = 1024;
 let layout = std::alloc::Layout::from_size_align(size, 8).unwrap();
 let ptr = unsafe {std::alloc::alloc(layout) };

 let mut s = unsafe {let slice = std::slice::from_raw_parts_mut(ptr, layout.size());
     String::from_utf8_unchecked(slice.to_vec())
 };

 s.push('1');
 ...

而且须要本人治理内存,不合乎咱们应用 Rust 的大准则。相对来说,目前在 Rust 语言层面提供的内存分配机制不是很欠缺。这些问题从 2015 年就开始探讨,但目前始终没有太大的停顿。最近有 rust wg-allocators 工作组的人员示意要重新考虑 allocator trait(详见附录 5、附录 6),冀望后续在内存调配方面有所停顿。

如果对内存调配有很高的要求,咱们倡议 一方面能够抉择一个相熟的分配器并依据业务场景进行调优,另一方面则是在利用层面进行优化,比方引入 arena 分配器等。

05 总结与瞻望

总体而言,Rust 是一门重视性能和平安的零碎编程语言,通过其独特的所有权零碎、借用零碎和类型零碎,胜利地解决了传统零碎编程中的许多难题。其开发者敌对的语法、丰盛的规范库和弱小的社区反对,使得 Rust 成为当今编程畛域中备受关注的语言之一。

Fabarta 这一年多的实际下来,有一些感悟心愿能帮到你决定是否投入 Rust 语言:

  • 第一还是要保持没有银弹。 如果想通过换一门语言来解决所有问题,那么 Rust 并不能。
  • 然而,工具特地是语言会极大的影响团队能力。 简略来说,团队应用工具的能力边界基本上就是团队的能力边界。在这方面,Rust 提供了十分广阔的范畴。
  • 团队的背景决定了 Rust 的学习曲线。 出于零碎级编程语言的个性,Rust 尽管不须要手动治理内存,但如果分明背地的细节,学习曲线虽高于其余语言但并不算平缓。
  • Rust 具备古代语言的所有特色,周边工具比方代码格调查看、测试、三方生态都十分丰盛与沉闷,咱们最后对这方面有一些放心,但实际下来齐全没有问题。
  • Rust 能够极大地晋升开发效率与零碎稳定性,进而给团队往前走的信念。 ArcNerual 在过来一年胜利公布了多个大版本,整体停顿甚至超过了团队之前的预期,Rust 的采纳是咱们胜利的必要条件之一。

Rust 依然在高速倒退,目前随着微软、AWS 等大型商业公司的采纳,拉平学习曲线、更好的异步编程、更优的内存调配、更丰盛的生态反对等都在打算或施行中,在嵌入式、Linux 内核中也有很大的停顿。对于 Fabarta 来说,咱们也将持续在 Rust 里深刻并一直拓展产品与团队能力边界。

附录

1. 杨成虎:存储 & 计算是过来,记忆 & 推理才是将来 

2. 一文读懂 Fabarta ArcGraph 图数据库丨技术专栏 

3. 摸索 Tokio Runtime 丨技术专栏

4.Rust Koans:https\://users.rust-lang.org/t/rust-koans/2408/1

5.allocator trait:https\://github.com/rust-lang/rust/issues/32838

6.allocator trait:https\://shift.click/blog/allocator-trait-talk/

本文作者

谭宇

Fabarta 资深技术专家

目前次要专一于 AI 时代的多模数据库引擎 ArcNeural 的建设。退出 Fabarta 之前曾任阿里云资深技术专家,主攻数据库、云计算与数字化转型方向。是 Tair / OceanBase 晚期开发团队成员;曾负责建设阿里巴巴团体数据库 PaaS 平台,率领团队实现了阿里巴巴数据库的容器化、存储计算拆散、在离线混部等重大改革;在阿里云寰球技术服务部期间提出并建设了飞天技术服务平台,对企业数字化转型有粗浅的了解并有丰盛的实践经验。

正文完
 0