[译] 唐刘:Rust in TiKV
这是PingCAP的首席架构师唐刘在 Rust 专场 Meetup 英文演讲稿的翻译篇。鉴于译者水平有限,错误之处还请批评指正。文末可以点击阅读原文查看英文原版。
大家好,今天我想和大家谈谈我们是如何在 TiKV 中使用 Rust 的。
开始之前,请允许我先做下自我介绍。我叫唐刘,PingCAP 的首席架构师。在我加入 PingCAP 之前,我在金山和腾讯工作过。我热爱开源,而且开发过一些项目,比如 LedisDB 、go-mysql 等等。
首先,我会解释下为什么我们选择 Rust 开发 TiKV,然后再向你们简要展示下 TiKV的基本架构,以及一些关键技术。最后,我会介绍下我们未来的计划。
什么是 TiKV
好的,现在开始。首先,什么是 TiKV ? TiKV 是一个分布式的 Key-Value 数据库,有以下几个特点:
- 异地复制 : 我们使用 Raft 和 Placement Driver 进行异地复制来保证数据的安全性。
- 水平扩展 : 如果发现快速的数据增长很快就要突破系统容量,我们可以通过直接添加节点的方式来扩展系统容量。
- 一致性分布式事务:我们使用基于 Google Percolator、优化后的两阶段提交协议来支持分布式事务。你可以使用 “begin” 来开启一个事务,然后做些事情,之后再使用 “commit” 或者 “rollback” 来完成这个事务。
- 分布式计算的协处理器 : 就像 HBase 那样,我们支持协处理器框架来让用户直接在 TiKV 中做计算。
- 和 TiDB 融合就像 Spanner 之于 F1 :使用 TiKV 作为 TiDB 的后端存储引擎,我们可以提供最好的分布式关系型数据库。
我们需要一门语言具有...
正如你所见,TiKV 有很多强大的功能。为了开发这些功能,我们也需要一个强大的编程语言。这种编程语言应该具备这些特点:
- 快速 : 我们非常重视 TiKV 的性能,所以我们需要一个在运行时运行很快的语言。
- 内存安全:对于一个准备运行很长时间的程序,我们不想遇到任何内存问题,比如野指针、内存泄漏等。
- 线程安全:我们必须保证数据一直都是一致的,所以任何数据竞争问题都必须避免。
- 和 C 高效绑定: 我们严重依赖 RocksDB,所以我们必须尽可能快地调用 RocksDB 的 API,不能有一点性能上的下降。
为什么不是 C++?
为了开发高性能服务,在大多数场景下 C++ 可能是最好的选择,但是我们并没有选择它。我们能够预料到会花费大量的时间在避免内存问题或者数据竞争问题上。并且
C++ 没有官方包管理器,这让维护和编译第三方依赖变得异常麻烦和困难,进而导致很长的研发周期。
为什么不是 Go ?
一开始我们考虑的是使用 Go ,但是接着就放弃了这个想法。Go 的 GC 能修复很多内存问题,但是有时仍然会停止运行中的进程。无论这个停止的时间有多短,我们都不能承受的起。Go 也没有解决数据竞争问题。即使我们在测试或运行时使用两次data -race 进行检测(注:go 语言可以用 -race 命令行参数检测数据竞争),也仍然不够。
另外,尽管我们可以使用 Goroutine 很容易写出并发逻辑,我们仍然不能忽视调度器的运行时开销。几天前我们遇到一个问题:我们使用多个 Goroutine 共享同一个
Context,却发现性能很糟糕,所以我们不得不让一个 Goroutine 使用一个子
Context,然后性能才变好点。
严格来说,CGO 有很严重的性能开销,但是我们必须无延迟地调用 RocksDB 的
API。基于以上原因,我们没有选择 Go,即使 Go 是我们团队最喜爱的语言。
所以,我们转向了 Rust...
但是 Rust...
Rust 是一门系统编程语言,由 Mozilla 在维护。它是一门非常强大的语言,但是你可以看看下面的曲线图,Rust 的学习曲线非常陡峭。
我用过很多编程语言,像 C++、Go、Python、Lua 等等,Rust 对我来说是最难掌握的一门语言。在 PingCAP,我们会让新同事至少花一个月的时间来学习 Rust,先和编译错误做斗争,然后再提高 Rust 水平。这个在学习 Go 的过程中从来没发生过。
此外,Rust 的编译时间很长,比 C++ 都长。每次当我输完 cargo build 命令开始构建 TiKV 的时候,我就可以做好多个俯卧撑了。
尽管 Rust 出来很长一段时间了,但是仍然缺乏很多库和工具,有些第三方项目至今还没有在生产环境中验证过。使用 Rust 对我们来说有很大的风险。最为严重的是,我们很难找到 Rust 程序员,因为在中国知道 Rust 的人寥寥无几,我们很缺人手。
然后,为什么还是选择 Rust ?
尽管 Rust 有以上种种缺点,但是它的好处深深地吸引我们。Rust 是内存安全的,我们再也不必担心内存泄漏和野指针问题。
Rust 是线程安全的,所以也没有任何数据竞争问题。所有的安全都是由编译器保证的。所以在大多少情况下,编译一旦通过,我们确信程序就能安全地运行。
Rust 没有 GC 开销,所以我们不会遇到 “stop the world” 问题。通过 FFI 调用 C 程序是非常快的,所以我们不担心调用 RocksDB API 会有性能上的降低。最后,Rust
有个官方的包管理器 crate ,我们找到很多库可以直接拿来用。
我们做了一个艰难而伟大的决定:使用 Rust!
TiKV 时间线
这里你可以看到 TiKV 的时间线。我们在 2016 年1月1号开始开发 TiKV,然后在2016年4月1号开源了,就像 Gmail 在每个4月1号愚人节那样做的,这并不是一个玩笑。2016年10月份,TiKV 第一次被使用在生产环境,那时我们甚至都还没有发布
beta 版。2016年11月,我们发布了第一个 beta 版;接着2016年12月发布 RC1 版本,今年2月份发布 RC2 版本。稍后我们计划4月份发布 RC3 版本,在6月份发布第一个 GA 版本。
如你所见,TiKV 的开发非常快,发布的版本都很稳定。选择 Rust 已经被证明是一个正确的决定。感谢 Rust。
TiKV 架构
现在让我们深入探究 TiKV。你可以从 TiKV 的架构中看到 TiKV 的层次很清晰,很容易理解。
TiKV 在底层使用了 RocksDB,一种高性能的持久化 Key-Value 存储,来作为后端的存储引擎。
接下来一层是 Raft KV 层。TiKV 使用 Raft 算法来实现异地复制数据。TiKV 被设计来存储海量数据,一个 Raft 组远不够。所以我们按范围切分,然后把每个范围的数据作为一个独立的 Raft Group。我们把这种方式命名为 Multi-Raft Groups。
TiKV 提供了一个简单的 Key-Value API,包括 SET、GET、DELETE 等方法,我们可以像使用其他任何分布式 Key-Value 存储系统那样使用 TiKV 的 API。TiKV 的上层也使用这些 API 来支持更高级的功能。
在 Raft 层之上是 MVCC。TiKV 中保存的所有 Key 必须包含一个全局唯一的时间戳,这个时间戳由 Placement Driver 来分配。TiKV 使用这个时间戳来支持分布式事务。
在最上层,是 KV 和协处理器 API 层,用来处理客户端的请求。
Multi-Raft
这是一个 Multi-Raft 的例子。
你可以看到有4个 TiKV 节点。在每一个节点存储中,我们有多个 Region。Region 是数据迁移和 Raft 复制的基本单元。每一个 Region 会被复制到三个节点中去。一个 Region 的三个副本组成了一个 Raft Group。
水平扩展</扩展>
水平扩展(初始状态)
这是一个水平扩展的例子。首先,我们有四个节点,节点A有3个 Region,其他节点都是2个 Region。
当然了,节点A比其他节点更忙,我们想减轻一下它的压力。
水平扩展(添加新节点)
我们添加一个新的节点 E,开始把节点 A 中的 Region 1 转移到节点 E。但是我们发现 Region 1 的 Leader 在节点 A 中,所以我们首先把 Leader 从节点 A 转移到节点B。
水平扩展(均衡)
之后 Region 1 的 Leader 在节点B中,然后我们在节点 E 中添加一个 Region 1 的副本。
然后我们从节点 A 中移除 Region 1 的副本。所有这一切都是被 Placement Driver 自动执行的。我们唯一要做的就是,如果发现系统繁忙就添加节点。非常简单,不是吗?
一个简单的写入流
这是一个简单的写入流:当客户端发送一个写请求给 TiKV,TiKV 首先解析协议,然后把请求分发给 KV 线程,KV 线程执行一些事务逻辑并把请求发送给 Raft 线程,在TiKV 复制 Raft Log 并且应用到 RocksDB 之后,整个写请求就结束了。
关键技术
现在,让我们关注一些核心技术。
关于网络层,我们使用了一个已经被广泛使用的协议 Protocol Buffers 来快速地对数据进行序列化和反序列化。
首先,我们使用** MIO **来构建网络框架。尽管 MIO 封装了底层的网络操作,但是它仍然是非常基础的库,我们必须手动地接受和发送数据,来解码或者编码我们自定义的网络协议。事实上很不方便。所以从 RC2 版本之后,我们已经在用 gPRC 来重构网络层了。gRPC 的优势非常明显。我们再也不用关心如何处理网络,只关系我们自己的逻辑,代码也看起来简单清晰。同时,用户可以很容易地使用其他编程语言来构建 TiKV 的客户端。我们已经在开发 TiKV 的 Java 客户端。
关于** 异步框架 **。接收到请求之后,TiKV 把请求分发给不同的线程来异步处理。一开始我们使用 MIO 加回调来处理异步请求,但是回调会中断代码逻辑,很难正确地阅读和写好代码,所以我们使用 tokio-core 和 futures 来重构代码,我们认为这是一种更为现代化的风格,对于未来的 Rust 来说。有时,我们也用线程池来分发简单的任务,以后会用 futures-cpupool。
关于** 存储 **,我们使用 rust-rocksdb 来访问 RocksDB。
关于** 监控 **,我们写了个 Prometheus 的 Rust 客户端,而且这个客户端在官方
wiki 上被推荐。关于性能监测,我们使用开启了监测功能的 jemallocator,用 clippy
来监测我们的代码。
未来计划
Ok, 以上就是我们已经做的和正在做的事情。我们未来将会做的事情有:
- 让 TiKV 更快,比如移除 Box。我们为了写代码容易,在 TiKV 中使用了大量的 box
技术,事实上一点也不高效。我们做性能测试发现,动态分发比静态分发至少慢3倍,以后我们会直接使用 Trait。 - 让 TiKV 更稳定,比如引入 Rust sanitizer。
- 贡献更多的 Rust 开源模块,比如 Raft 库,open-tracing 等等
- 更加深度地参与其他 Rust 项目,比如 rust-gRPC
- 在中国的社交媒体上写更多关于 Rust 的文章,组织更多的 Rust 聚会
- 成为中国强有力的 Rust 倡导者