首页系统综合问题Rust 编译模型之殇

Rust 编译模型之殇

时间2023-01-16 05:58:46发布分享专员分类系统综合问题浏览159

今天小编给各位分享trait的知识,文中也会对其通过Rust 编译模型之殇和对比 Go 语言,Rust 有什么优势和劣势?等多篇文章进行知识讲解,如果文章内容对您有帮助,别忘了关注本站,现在进入正文!

内容导航:

  • Rust 编译模型之殇
  • 对比 Go 语言,Rust 有什么优势和劣势?
  • rust可以开发分布式系统吗
  • Rust 语法很丑陋吗?如果是,为什么丑陋呢?为什么设计成这样呢?
  • 一、Rust 编译模型之殇

    Rust 编译缓慢的根由在于语言的设计。

    我的意思并非是此乃 Rust 语言的设计目标。正如语言设计者们相互争论时经常说的那样,编程语言的设计总是充满了各种权衡。其中最主要的权衡就是:运行时性能和编译时性能。而 Rust 团队几乎总是选择运行时而非编译时。

    因此,Rust 编译时间很慢。这有点让人恼火,因为 Rust 在其他方面的表现都非常好,唯独 Rust 编译时间却表现如此糟糕。

    Rust 与 TiKV 的编译时冒险:第 1 集

    在 PingCAP,我们基于 Rust 开发了分布式存储系统 TiKV 。然而它的编译速度慢到足以让公司里的许多人不愿使用 Rust。我最近花了一些时间,与 TiKV 团队及其社区中的其他几人一起调研了 TiKV 编译时间缓慢的问题。

    通过这一系列博文,我将会讨论在这个过程中的收获:

    为什么 Rust 编译那么慢,或者说让人感觉那么慢;

    Rust 的发展如何造就了编译时间的缓慢;

    编译时用例;

    我们测量过的,以及想要测量但还没有或者不知道如何测量的项目;

    改善编译时间的一些思路;

    事实上未能改善编译时间的思路;

    TiKV 编译时间的历史演进;

    有关如何组织 Rust 项目可加速编译的建议;

    最近和未来,上游将对编译时间的改进。

    PingCAP 的阴影:TiKV 编译次数 “余额不足”

    在 PingCAP,我的同事用 Rust 写 TiKV。它是我们的分布式数据库 TiDB 的存储节点。采用这样的架构,是因为他们希望该系统中作为最重要的节点,能被构造得快速且可靠,至少是在一个最大程度的合理范围内(译注:通常情况下人们认为快和可靠是很难同时做到的,人们只能在设计/构造的时候做出权衡。选择 Rust 是为了尽可能让 TiKV 能够在尽可能合理的情况下去提高它的速度和可靠性)。

    这是一个很棒的决定,并且团队内大多数人对此都非常满意。

    但是许多人抱怨构建的时间太长。有时,在开发模式下完全重新构建需要花费 15 分钟,而在发布模式则需要 30 分钟。对于大型系统项目的开发者而言,这看上去可能并不那么糟糕。但是它与许多开发者从现代的开发环境中期望得到的速度相比则慢了很多。TiKV 是一个相当巨大的代码库,它拥有 200 万行 Rust 代码。相比之下,Rust 自身包含超过 300 万行 Rust 代码,而 Servo 包含 270 万行(请参阅 此处的完整行数统计)。

    TiDB 中的其他节点是用 Go 编写的,当然,Go 与 Rust 有不同的优点和缺点。PingCAP 的一些 Go 开发人员对于不得不等待 Rust 组件的构建而表示不满。因为他们习惯于快速的构建-测试迭代。

    在 Go 开发人员忙碌工作的同时,Rust 开发人员却在编译时间休息(喝咖啡、喝茶、抽烟,或者诉苦)。Rust 开发人员有多余的时间来跨越内心的“阴影(译注:据说,TiKV 一天只有 24 次编译机会,用一次少一次)。

    概览:TiKV 编译时冒险历程

    本系列的第一篇文章只是关于 Rust 在编译时间方面的历史演进。因为在我们深入研究 TiKV 编译时间的具体技术细节之前,可能需要更多的篇章。所以,这里先放一个漂亮的图表,无需多言。

    TiKV 的 Rust 编译时间

    造就编译时间缓慢的 Rust 设计

    Rust 编译缓慢的根由在于语言的设计。

    我的意思并非是此乃 Rust 语言的设计目标。正如语言设计者们相互争论时经常说的那样,编程语言的设计总是充满了各种权衡。其中最主要的权衡就是:运行时性能和编译时性能。而 Rust 团队几乎总是选择运行时而非编译时。

    刻意的运行时/编译时权衡不是 Rust 编译时间差劲的唯一原因,但这是一个大问题。还有一些语言设计对运行时性能并不是至关重要,但却意外地有损于编译时性能。Rust 编译器的实现方式也抑制了编译时性能。

    所以,Rust 编译时间的差劲,既是刻意为之的造就,又有出于设计之外的原因。尽管编译器的改善、设计模式和语言的发展可能会缓解这些问题,但这些问题大多无法得到解决。还有一些偶然的编译器架构原因导致了 Rust 的编译时间很慢,这些需要通过大量的工程时间和精力来修复。

    如果迅速地编译不是 Rust 的核心设计原则,那么 Rust 的核心设计原则是什么呢?下面列出几个核心设计原则:

    实用性(Practicality):它应该是一种可以在现实世界中使用的语言;

    务实(Pragmatism):它应该是符合人性化体验,并且能与现有系统方便集成的语言;

    内存安全性(Memory-safety):它必须加强内存安全,不允许出现段错误和其他类似的内存访问违规操作;

    高性能(Performance):它必须拥有能和 C++ 比肩的性能;

    高并发(Concurrency):它必须为编写并发代码提供现代化的解决方案。

    但这并不是说设计者没有为编译速度做任何考虑。例如,对于编译 Rust 代码所要做的任何分析,团队都试图确保合理的算法复杂度。然而,Rust 的设计历史也是其一步步陷入糟糕的编译时性能沼泽的历史。

    讲故事的时间到了。

    Rust 的自举

    我不记得自己是什么时候才开始意识到,Rust 糟糕的编译时间其实是该语言的一个战略问题。在面对未来底层编程语言的竞争时可能会是一个致命的错误。在最初的几年里,我几乎完全是对 Rust 编译器进行 Hacking(非常规暴力测试),我并不太关心编译时间的问题,我也不认为其他大多数同事会太关心该问题。我印象中大部分时间 Rust 编译时总是很糟糕,但不管怎样,我能处理好。

    针对 Rust 编译器工作的时候,我通常都会在计算机上至少保留三份存储库副本,在其他所有的编译器都在构建和测试时,我就会 Hacking 其中的一份。我会开始构建 Workspace 1,切换终端,记住在 Workspace 2 发生了什么,临时做一下修改,然后再开始构建 Workspace 2,切换终端,等等。整个流程比较零碎且经常切换上下文。

    这(可能)也是其他 Rust 开发者的日常。我现在对 TiKV 也经常在做类似的 Hacking 测试。

    那么,从历史上看,Rust 编译时间有多糟糕呢?这里有一个 简单的统计表 ,可以看到 Rust 的自举(Self-Hosting)时间在过去几年里发生了怎样的变化,也就是使用 Rust 来构建它自己的时间。出于各种原因,Rust 构建自己不能直接与 Rust 构建其他项目相比,但我认为这能说明一些问题。

    首个 Rust 编译器 叫做 rustboot,始于 2010 年,是用 OCaml 编写的,它最终目的是被用于构建第二个由 Rust 实现的编译器 rustc,并由此开启了 Rust 自举的历程。除了基于 Rust 编写之外,rustc 还使用了 LLVM 作为后端来生成机器代码,来代替之前 rustboot 的手写 x86 代码生成器。

    Rust 需要自举,那样就可以作为一种“自产自销(Dog-Fooding)”的语言。使用 Rust 编写编译器意味着 Rust 的作者们需要在语言设计过程的早期,使用自己的语言来编写实用的软件。在实现自举的过程中让 Rust 变成一种实用的语言。

    Rust 第一次自举构建是在 2011 年 4 月 20 日。该过程总共花了 一个小时,这个编译时间对当时而言,很漫长,甚至还觉得有些可笑。

    最初那个超级慢的自举程序慢的有些反常,在于其包含了糟糕的代码生成和其他容易修复的早期错误(可能,我记不清了)。rustc 的性能很快得到了改善,Graydon 很快就 抛弃了旧的 rustboot 编译器 ,因为没有足够的人力和动力来维护两套实现。

    在 2010 年 6 月首次发布的 11 个月之后,Rust 漫长而艰难的编译时代就此开始了。

    注意

    我本想在这里分享一些有历史意义的自举时间,但在经历了数小时,以及试图从2011年开始构建 Rust 修订版的障碍之后,我终于放弃了,决定在没有它们的情况下发布这篇文章。作为补充,这里作一个类比:

    兔子飞奔几米(7):rustboot 构建 Rust 的时间;

    仓鼠狂奔一公里(49):在 rustboot 退役后使用 rustc 构建 Rust 的时间;

    树獭移动一万米(188):在 2020 年构建 rustc 所需的时间。

    反正,几个月前我构建 Rust 的时候,花了五个小时。

    Rust 语言开发者们已经适应了 Rust 糟糕的自举时间,并且在 Rust 的关键早期设计阶段未能识别或处理糟糕编译时间问题的严重性。

    (非)良性循环

    在 Rust 项目中,我们喜欢能够增强自身基础的流程。无论是作为语言还是社区,这都是 Rust 取得成功的关键之一。

    一个明显非常成功的例子就是 Servo。Servo 是一个基于 Rust 构建的 Web 浏览器,并且 Rust 也是为了构建 Servo 而诞生。Rust 和 Servo 是姊妹项目。它们是由同一个(初始)团队,在(大致)同一时间创造的,并同时进化。不只是为了创造 Servo 而创建 Rust,而且 Servo 也是为了解 Rust 的设计而构建的。

    这两个项目最初的几年都非常困难,两个项目都是并行发展的。此处非常适合用 忒修斯之船 做比喻——我们不断地重建 Rust,以便在 Sevro 的海洋中畅行。毫无疑问,使用 Rust 构建 Servo 的经验,来构建 Rust 语言本身,直接促进了很多好的决定,使得 Rust 成为了实用的语言。

    这里有一些关于 Servo-Rust 反馈回路的例子:

    为了 自动生成HTML解析器,实现了带标签的 break 和 continue 。

    在分析了 Servo 内闭包使用情况之后实现了,所有权闭包(Owned closures)。

    外部函数调用曾经被认为是安全的。这部分变化(改为了 Unsafe)得益于 Servo 的经验。

    从绿色线程迁移到本地线程,也是由构建 Sevro、观察 Servo 中 SpiderMonkey 集成的 FFI 开销以及剖析“hot splits”的经验所决定的,其中绿色线程堆栈需要扩展和收缩。

    Rust 和 Servo 的共同发展创造了一个 良性循环,使这两个项目蓬勃发展。今天,Servo 组件被深度集成到火狐(Firefox)中,确保在火狐存活的时候,Rust 不会死去。

    任务完成了。

    前面提到的早期自举对 Rust 的设计同样至关重要,使得 Rust 成为构建 Rust 编译器的优秀语言。同样,Rust 和 WebAssembly 是在密切合作下开发的(我与 Emscripten 的作者,Cranelift 的作者并排工作了好几年),这使得 WASM 成为了一个运行 Rust 的优秀平台,而 Rust 也非常适合 WASM。

    遗憾的是,没有这样的增强来缩短 Rust 编译时间。事实可能正好相反——Rust 越是被认为是一种快速语言,它成为最快的语言就越重要。而且,Rust 的开发人员越习惯于跨多个分支开发他们的 Rust 项目,在构建之间切换上下文,就越不需要考虑编译时间。

    直到 2015 年 Rust 1.0 发布并开始得到更广泛的应用后,这种情况才真正有所改变。

    多年来,Rust 在糟糕的编译时间的“温水中”被慢慢“烹煮 ”,当意识到它已经变得多么糟糕时,已为时已晚。已经 1.0 了,那些(设计)决策早已被锁定了。

    这一节包含了太多令人厌倦的隐喻,抱歉了。

    运行时优先于编译时的早期决策

    如果是 Rust 设计导致了糟糕的编译时间,那么这些设计具体又是什么呢?我会在这里简要地描述一些。本系列的下一集将会更加深入。有些在编译时的影响比其他的更大,但是我断言,所有这些都比其他的设计耗费更多的编译时间。

    现在回想起来,我不禁会想,“当然,Rust 必须有这些特性”。确实,如果没有这些特性,Rust 将会是另一门完全不同的语言。然而,语言设计是折衷的,这些并不是注定要成 Rust 的部分。

    借用(Borrowing)——Rust 的典型功能。其复杂的指针分析以编译时的花费来换取运行时安全。

    单态化(Monomorphization)——Rust 将每个泛型实例转换为各自的机器代码,从而导致代码膨胀并增加了编译时间。

    栈展开(Stack unwinding)——不可恢复异常发生后,栈展开向后遍历调用栈并运行清理代码。它需要大量的编译时登记(book-keeping)和代码生成。

    构建脚本(Build scripts)——构建脚本允许在编译时运行任意代码,并引入它们自己需要编译的依赖项。它们未知的副作用和未知的输入输出限制了工具对它们的假设,例如限制了缓存的可能。

    宏(Macros)——宏需要多次遍历才能展开,展开得到的隐藏代码量惊人,并对部分解析施加限制。过程宏与构建脚本类似,具有负面影响。

    LLVM 后端(LLVM backend)——LLVM 产生良好的机器代码,但编译相对较慢。

    过于依赖LLVM优化器(Relying too much on the LLVM optimizer)——Rust 以生成大量 LLVM IR 并让 LLVM 对其进行优化而闻名。单态化则会加剧这种情况。

    拆分编译器/软件包管理器(Split compiler/package manager)——尽管对于语言来说,将包管理器与编译器分开是很正常的,但是在 Rust 中,至少这会导致 cargo 和 rustc 同时携带关于整个编译流水线的不完善和冗余的信息。当流水线的更多部分被短路以便提高效率时,则需要在编译器实例之间传输更多的元数据。这主要是通过文件系统进行传输,会产生开销。

    每个编译单元的代码生成(Per-compilation-unit code-generation)——rustc 每次编译单包(crate)时都会生成机器码,但是它不需要这样做,因为大多数 Rust 项目都是静态链接的,直到最后一个链接步骤才需要机器码。可以通过完全分离分析和代码生成来提高效率。

    单线程的编译器(Single-threaded compiler)——理想情况下,整个编译过程都将占用所有 CPU 。然而,Rust 并非如此。由于原始编译器是单线程的,因此该语言对并行编译不够友好。目前正在努力使编译器并行化,但它可能永远不会使用所有 CPU 核心。

    trait 一致性(trait coherence)——Rust 的 trait(特质)需要遵循“一致性(conherence)”,这使得开发者不可能定义相互冲突的实现。trait 一致性对允许代码驻留的位置施加了限制。这样,很难将 Rust 抽象分解为更小的、易于并行化的编译单元。

    “亲密”的代码测试(Tests next to code)——Rust 鼓励测试代码与功能代码驻留在同一代码库中。由于 Rust 的编译模型,这需要将该代码编译和链接两次,这份开销非常昂贵,尤其是对于有很多包(crate)的大型项目而言。

    改善 Rust 编译时间的最新进展

    现状并非没有改善的希望。一直有很多工作在努力改善 Rust 的编译时间,但仍有许多途径可以探索。我希望我们能持续看到进步。以下是我最近一两年所知道的一些进展。感谢所有为该问题提供帮助的人。

    Rust 编译时 主要问题:

    跟踪各种工作以缩短编译时间。

    全面概述了影响 Rust 编译性能的因素和潜在的缓解策略。

    流水线编译 ( 1 , 2 , 3 )

    与上游代码生成并行地对下游包进行类型检查。现在默认情况下在稳定(Stable)频道上。

    由 @alexcrichton 和 @nikomatsakis 开发。

    并行 rustc ( 1 , 2 , 3 )

    并行运行编译器的分析阶段。稳定(Stable)频道尚不可用。

    由 @Zoxc , @michaelwoerister , @oli-obk , 以及其他一些人开发。

    MIR 级别的常量传播(constant propagation)

    在 MIR 上执行常量传播,从而减少了 LLVM 对单态函数的重复工作。

    由 @wesleywiser 开发。

    MIR 优化

    优化 MIR 应该比优化单态 LLVM IR 更快。

    稳定(Stable)编译器尚不可用。

    由 @wesleywiser 和其他人一起开发。

    cargo build -Ztimings ( 1 , 2 )

    收集并图形化有关 Cargo 并行建造时间的信息。

    由 @ehuss 和 @luser 开发。

    rustc -Zself-profile ( 1 , 2 , 3 )

    生成有关 rustc 内部性能的详细信息。

    由 @wesleywiser 和 @michaelwoerister 开发。

    共享单态化(Shared monomorphizations)

    通过消除多个包(crate)中出现的单态化来减少代码膨胀。

    如果优化级别小于 3,则默认启用。

    由 @michaelwoerister 开发。

    Cranelift 后端

    通过使用 cranelift 来生成代码,减少了 Debug 模式的编译时间。

    由 @bjorn3 开发。

    perf.rust-lang.org

    详细跟踪了 Rust 的编译时性能,基准测试持续增加中。

    由 @nrc , @Mark-Simulacrum , @nnethercote 以及其他人一起开发。

    cargo-bloat

    查找二进制文件中占用最多空间的地方。膨胀(Bloat)会影响编译时间。

    由 @RazrFalcon 和其他人一起开发。

    cargo-feature-analyst

    发现未使用的特性(features)。

    由 @psinghal20 开发。

    cargo-udeps

    发现未使用的包(crate)。

    由 @est31 开发。

    twiggy

    分析代码大小,该大小与编译时间相关。

    由 @fitzgen , @data-pup 以及其他人一起开发。

    rust-analyzer

    用于Rust的新语言服务器,其响应时间比原始 RLS 更快。

    由 @matklad , @flodiebold , @kjeremy 以及其他人一起开发。

    “如何缓解 Rust 编译时间带来的痛苦”

    vfoley 写的博文。

    “关于 Rust 代码膨胀的思考”

    @raphlinus 写的博文。

    Nicholas Nethercote 对 rustc 的优化工作:

    “2019 年 Rust 编译器如何提速”

    “Rust 编译器的速度持续变快”

    “可视化 Rust 编译”

    “如何在 2019 年进一步提升 Rust 编译器的速度”

    “如何在 2019 年最后一次提升 Rust 编译器”

    对于未上榜的人员或项目,我需要说一声抱歉。

    作者介绍:Brian Anderson 是 Rust 编程语言及其姊妹项目 Servo Web 浏览器的共同创始人之一,他目前在 PingCAP 担任高级数据库工程师。感谢 Rust 中文社区翻译小组对本文翻译及审校上的贡献:翻译:张汉东、黄珏珅;审校 :吴聪。

    声明:本文系作者投稿,版权归其所有。

    一、对比 Go 语言,Rust 有什么优势和劣势?

    我并没有什么编程的经验,觉得编程实在是太复杂了,不喜欢去研究太多,对这个也不怎么懂,只能说自己是个半吊子,就是所掌握的知识,也是东拼西凑的,朋友和我说点儿,自己去书上看一点儿,只能说根据自己的体验给出一些体会吧。


    Rust的优势是:

    1、Rust把安全、精确的内存管理作为一切的中心放在首要的位置。

    2、Rust同时拥有特别强的控制性和特别强的安全性。

    3、Rust语言通过: 优秀的类型系统设计、 严格的编译器静态审查、 配合程序员局部核对、加上少量的运行时校验,保障了内存安全。

    4、Rust的语言特别的复杂,导致学习曲线比较陡峭,对于初学者来说难度较大。但学通之后将终生受益。

    5、效率高,速度特别的快

    6、 支持范型
    7、 社区活跃度很高,更加的强调了社区的作用。

    8、Rust 有更强的语义,更容易捕获错误的逻辑,编译器直接检查出你代码中的不安全的部分

    Rust的劣势是:

    1、 语言相对来说比较复杂,对于新手来说,让新手摸不着头脑。
    2、还不算太稳定。

    其实我觉得什么代码啊编程啊这些东西还是比较适合理工的学生去研究,我一看脑袋就大,完全不明白在讲什么。我大概了解的就是这些,语言的话大家可以多方面的去了解,也不是说有缺点就是不好,看配置看个人吧,每个人习惯不一样,也许有的人用不稳定的还觉得挺好呢,有的人就喜欢比较完美的,在我看来编程这个东西真的是很复杂,会有很多的代码,这些代码弄得我自己头都大了,有的时候还得去恶补一下。

    二、rust可以开发分布式系统吗

    rust是可以开发分布式系统的。

    引子

    构建一个分布式系统 并不是一件容易的事情,我们需要考虑很多的问题,首先就是我们的系统到底需要提供什么样的功能,譬如:

    • 一致性:我们是否需要保证整个系统的线性一致性,还是能容忍短时间的数据不一致,只支持最终一致性。

    • 稳定性:我们能否保证系统 7 x 24 小时稳定运行。系统的可用性是 4 个 9,还有 5 个 9?如果出现了机器损坏等灾难情况,系统能否做的自动恢复。

    • 扩展性:当数据持续增多,能否通过添加机器就自动做到数据再次平衡,并且不影响外部服务。

    • 分布式事务:是否需要提供分布式事务支持,事务隔离等级需要支持到什么程度。

    • 上面的问题在系统设计之初,就需要考虑好,作为整个系统的设计目标。为了实现这些特性,我们就需要考虑到底采用哪一种实现方案,取舍各个方面的利弊等。

      后面,我将以我们开发的分布式 Key-Value TiKV 作为实际例子,来说明下我们是如何取舍并实现的。

      TiKV

      TiKV 是一个分布式 Key-Value store,它使用 Rust 开发,采用 Raft 一致性协议保证数据的强一致性,以及稳定性,同时通过 Raft 的 Configuration Change 机制实现了系统的可扩展性。

      TiKV 提供了基本的 KV API 支持,也就是通常的 Get,Set,Delete,Scan 这样的 API。TiKV 也提供了支持 ACID 事务的 Transaction API,我们可以使用 Begin 开启一个事务,在事务里面对 Key 进行操作,最后再用 Commit 提交一个事务,TiKV 支持 SI 以及 SSI 事务隔离级别,用来满足用户的不同业务场景。

      Rust

      在规划好 TiKV 的特性之后,我们就要开始进行 TiKV 的开发。这时候,我们面临的第一个问题就是采用什么样的语言进行开发。当时,摆在我们眼前的有几个选择:

    • Go,Go 是我们团队最擅长的一门语言,而且 Go 提供的 goroutine,channel 这些机制,天生的适合大规模分布式系统的开发,但灵活方便的同时也有一些甜蜜的负担,首先就是 GC,虽然现在 Go 的 GC 越来越完善,但总归会有短暂的卡顿,另外 goroutine 的调度也会有切换开销,这些都可能会造成请求的延迟增高。

    • Java,现在世面上面有太多基于 Java 做的分布式系统了,但 Java 一样有 GC 等开销问题,同时我们团队在 Java 上面没有任何开发经验,所以没有采用。

    • C++,C++ 可以认为是开发高性能系统的代名词,但我们团队没有特别多的同学能熟练掌握 C++,所以开发大型 C++ 项目并不是一件非常容易的事情。虽然使用现代 C++ 的编程方式能大量减少 data race,dangling pointer 等风险,我们仍然可能犯错。

    • 当我们排除了上面几种主流语言之后,我们发现,为了开发 TiKV,我们需要这门语言具有如下特性:

    • 静态语言,这样才能最大限度的保证运行性能。

    • 无 GC,完全手动控制内存。

    • Memory safe,尽量避免 dangling pointer,memory leak 等问题。

    • Thread safe,不会遇到 data race 等问题。

    • 包管理,我们可以非常方便的使用第三方库。

    • 高效的 C 绑定,因为我们还可能使用一些 C library,所以跟 C 交互不能有开销。

    • 综上,我们决定使用 Rust,Rust 是一门系统编程语言,它提供了我们上面想要的语言特性,但选择 Rust 对我们来说也是很有风险的,主要有两点:

    • 我们团队没有任何 Rust 开发经验,全部都需要花时间学习 Rust,而偏偏 Rust 有一个非常陡峭的学习曲线。

    • 基础网络库的缺失,虽然那个时候 Rust 已经出了 1.0,但我们发现很多基础库都没有,譬如在网络库上面只有 mio,没有好用的 RPC 框架,HTTP 也不成熟。

    • 但我们还是决定使用 Rust,对于第一点,我们团队花了将近一个月的时间来学习 Rust,跟 Rust 编译器作斗争,而对于第二点,我们就完全开始自己写。

      幸运的,当我们越过 Rust 那段阵痛期之后,发现用 Rust 开发 TiKV 异常的高效,这也就是为啥我们能在短时间开发出 TiKV 并在生产环境中上线的原因。

      一致性协议

      对于分布式系统来说,CAP 是一个不得不考虑的问题,因为 P 也就是 Partition Tolerance 是一定存在的,所以我们就要考虑到底是选择 C - Consistency 还是 A - Availability。

      我们在设计 TiKV 的时候就决定 - 完全保证数据安全性,所以自然就会选择 C,但其实我们并没有完全放弃 A,因为多数时候,毕竟断网,机器停电不会特别频繁,我们只需要保证 HA - High Availability,也就是 4 个 9 或者 5 个 9 的可用性就可以了。

      既然选择了 C,我们下一个就考虑的是选用哪一种分布式一致性算法,现在流行的无非就是 Paxos 或者 Raft,而 Raft 因为简单,容易理解,以及有很多现成的开源库可以参考,自然就成了我们的首要选择。

      在 Raft 的实现上,我们直接参考的 etcd 的 Raft。etcd 已经被大量的公司在生产环境中使用,所以它的 Raft 库质量是很有保障的。虽然 etcd 是用 Go 实现的,但它的 Raft library 是类似 C 的实现,所以非常便于我们用 Rust 直接翻译。在翻译的过程中,我们也给 etcd 的 Raft fix 了一些 bug,添加了一些功能,让其变得更加健壮和易用。

      现在 Raft 的代码仍然在 TiKV 工程里面,但我们很快会将独立出去,变成独立的 library,这样大家就能在自己的 Rust 项目中使用 Raft 了。

      使用 Raft 不光能保证数据的一致性,也可以借助 Raft 的 Configuration Change 机制实现系统的水平扩展,这个我们会在后面的文章中详细的说明。

      存储引擎

      选择了分布式一致性协议,下一个就要考虑数据存储的问题了。在 TiKV 里面,我们会存储 Raft log,然后也会将 Raft log 里面实际的客户请求应用到状态机里面。

      首先来看状态机,因为它会存放用户的实际数据,而这些数据完全可能是随机的 key - value,为了高效的处理随机的数据插入,自然我们就考虑使用现在通用的 LSM Tree 模型。而在这种模型下,RocksDB 可以认为是现阶段最优的一个选择。

      RocksDB 是 Facebook 团队在 LevelDB 的基础上面做的高性能 Key-Value Storage,它提供了很多配置选项,能让大家根据不同的硬件环境去调优。这里有一个梗,说的是因为 RocksDB 配置太多,以至于连 RocksDB team 的同学都不清楚所有配置的意义。

      关于我们在 TiKV 中如何使用,优化 RocksDB,以及给 RocksDB 添加功能,fix bug 这些,我们会在后面文章中详细说明。

      而对于 Raft Log,因为任意 Log 的 index 是完全单调递增的,譬如 Log 1,那么下一个 Log 一定是 Log 2,所以 Log 的插入可以认为是顺序插入。这种的,最通常的做法就是自己写一个 Segment File,但现在我们仍然使用的是 RocksDB,因为 RocksDB 对于顺序写入也有非常高的性能,也能满足我们的需求。但我们不排除后面使用自己的引擎。

      因为 RocksDB 提供了 C API,所以可以直接在 Rust 里面使用,大家也可以在自己的 Rust 项目里面通过 rust-rocksdb 这个库来使用 RocksDB。

      分布式事务

      要支持分布式事务,首先要解决的就是分布式系统时间的问题,也就是我们用什么来标识不同事务的顺序。通常有几种做法:

    • TrueTime,TrueTime 是 Google Spanner 使用的方式,不过它需要硬件 GPS + 原子钟支持,而且 Spanner 并没有在论文里面详细说明硬件环境是如何搭建的,外面要自己实现难度比较大。

    • HLC,HLC 是一种混合逻辑时钟,它使用 Physical Time 和 Logical Clock 来确定事件的先后顺序,HLC 已经在一些应用中使用,但 HLC 依赖 NTP,如果 NTP 精度误差比较大,很可能会影响 commit wait time。

    • TSO,TSO 是一个全局授时器,它直接使用一个单点服务来分配时间。TSO 的方式很简单,但会有单点故障问题,单点也可能会有性能问题。

    • TiKV 采用了 TSO 的方式进行全局授时,主要是为了简单。至于单点故障问题,我们通过 Raft 做到了自动 fallover 处理。而对于单点性能问题,TiKV 主要针对的是 PB 以及 PB 以下级别的中小规模集群,所以在性能上面只要能保证每秒百万级别的时间分配就可以了,而网络延迟上面,TiKV 并没有全球跨 IDC 的需求,在单 IDC 或者同城 IDC 情况下,网络速度都很快,即使是异地 IDC,也因为有专线不会有太大的延迟。

      解决了时间问题,下一个问题就是我们采用何种的分布式事务算法,最通常的就是使用 2 PC,但通常的 2 PC 算法在一些极端情况下面会有问题,所以业界要不通过 Paxos,要不就是使用 3 PC 等算法。在这里,TiKV 参考 Percolator,使用了另一种增强版的 2 PC 算法。

      这里先简单介绍下 Percolator 的分布式事务算法,Percolator 使用了乐观锁,也就是会先缓存事务要修改的数据,然后在 Commit 提交的时候,对要更改的数据进行加锁处理,然后再更新。采用乐观锁的好处在于对于很多场景能提高整个系统的并发处理能力,但在冲突严重的情况下反而没有悲观锁高效。

      对于要修改的一行数据,Percolator 会有三个字段与之对应,Lock,Write 和 Data:

    • Lock,就是要修改数据的实际 lock,在一个 Percolator 事务里面,有一个 primary key,还有其它 secondary keys, 只有 primary key 先加锁成功,我们才会再去尝试加锁后续的 secondary keys。

    • Write,保存的是数据实际提交写入的 commit timestamp,当一个事务提交成功之后,我们就会将对应的修改行的 commit timestamp 写入到 Write 上面。

    • Data,保存实际行的数据。

    • 当事务开始的时候,我们会首先得到一个 start timestamp,然后再去获取要修改行的数据,在 Get 的时候,如果这行数据上面已经有 Lock 了,那么就可能终止当前事务,或者尝试清理 Lock。

      当我们要提交事务的时候,先得到 commit timestamp,会有两个阶段:

    • Prewrite:先尝试给 primary key 加锁,然后尝试给 second keys 加锁。如果对应 key 上面已经有 Lock,或者在 start timestamp 之后,Write 上面已经有新的写入,Prewrite 就会失败,我们就会终止这次事务。在加锁的时候,我们也会顺带将数据写入到 Data 上面。

    • Commit:当所有涉及的数据都加锁成功之后,我们就可以提交 primay key,这时候会先判断之前加的 Lock 是否还在,如果还在,则删掉 Lock,将 commit timestamp 写入到 Write。当 primary key 提交成功之后,我们就可以异步提交 second keys,我们不用在乎 primary keys 是否能提交成功,即使失败了,也有机制能保证数据被正常提交。

    三、Rust 语法很丑陋吗?如果是,为什么丑陋呢?为什么设计成这样呢?

    Rust 的缩写是完全可以接受的嘛,也不是非常多,fn mod 这些都是很正常的缩写嘛,之前看到有人说 Rust 丑陋,应该是说指针语法繁杂,现在指针语法已经被统一和消除了(类型上来说貌似只有 & 是特殊的语法),一般使用是不会遇到让人厌恶的情景的。
    This release also marks the complete removal of the `~` and `@` syntax in favor of library types `Box` and `Gc`.

    - 0.11 changelog
    原本语法中有 ~ 和 @ 前者是最普通的指针,后者是垃圾回收指针。
    于是愈发会有奇葩的凌乱感,比如说

    let foobar = 42i
    &'a~@foobar // foobar 前面的都是指针

    现在这些语法被范型代替了:
    声明:

    let foobar = 42i
    &'a box Gc::new(foobar)

    类型

    &'a Box
    虽然变长了但是更统一了。

    当然我是从 0.11 开始学的,前面版本的语法我没用过,可能弄错。
    题外话:
    或许 Rust 是一个参与度最高的语言。
    曾经有人评价,一个工程项目一般是:一开始是简洁但是不完善的,在第二次设计的时候是繁杂并完善的,进行了第三次设计才能变成简洁但是完善的。
    我觉得 Rust 是在进行一个飞快的试错过程。Rust 在快速的迭代中不断的试错,变得复杂然后变得简单,每一个人都可以参与到语言的设计中去,只要你有干货辅佐你的观点,那么你只用写在 issues 里面就行了。Pull Requests · rust-lang/rfcs · GitHub
    最重要的是在 1.0 发布之前,你的好想法都不会因为兼容性而被拒绝,也就是说没有任何兼容性包袱(这就是为什么 Rust 一直给人语法不稳定的印象),同时已经有不少人和不少项目正在用 Rust 书写,Rust 编译器本身和 Servo 都是非常庞大的项目,有几十万行级别的代码吧,语言中如果有什么常见的坑那么必然会被填平,我觉得或许会成为一个坑很少的语言。基本上我的代码编译通过了就不会有问题。
    使用中遇到了坑请提交 iusses。

    关于trait的问题,通过《rust可以开发分布式系统吗》、《Rust 语法很丑陋吗?如果是,为什么丑陋呢?为什么设计成这样呢?》等文章的解答希望已经帮助到您了!如您想了解更多关于trait的相关信息,请到本站进行查找!

    爱资源吧版权声明:以上文中内容来自网络,如有侵权请联系删除,谢谢。

    trait
    谷歌新系统Fuchsia OS官网上线,要取代安卓布局物联网? 6个 好用的绘图软件,分分钟提高你的工作效率