欢迎你们前往腾讯云社区,获取更多腾讯海量技术实践干货哦~git
构建一个分布式 Key-Value Store 并非一件容易的事情,咱们须要考虑不少的问题,首先就是咱们的系统到底须要提供什么样的功能,譬如:github
一致性:咱们是否须要保证整个系统的线性一致性,仍是能容忍短期的数据不一致,只支持最终一致性。算法
稳定性:咱们可否保证系统 7 x 24 小时稳定运行。系统的可用性是 4 个 9,还有 5 个 9?若是出现了机器损坏等灾难状况,系统可否作的自动恢复。编程
扩展性:当数据持续增多,可否经过添加机器就自动作到数据再次平衡,而且不影响外部服务。缓存
分布式事务:是否须要提供分布式事务支持,事务隔离等级须要支持到什么程度。安全
上面的问题在系统设计之初,就须要考虑好,做为整个系统的设计目标。为了实现这些特性,咱们就须要考虑到底采用哪种实现方案,取舍各个方面的利弊等。网络
后面,我将以咱们开发的分布式 Key-Value 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 事务隔离级别,用来知足用户的不一样业务场景。异步
在规划好 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 是否能提交成功,即便失败了,也有机制能保证数据被正常提交。
在 TiKV 里面,事务的实现主要包括两块,一个是集成在 TiDB 中的 tikv client,而另外一个则是在 TiKV 中的 storage mod 里面,后面咱们会详细的介绍。
RPC 应该是分布式系统里面经常使用的一种网络交互方式,但实现一个简单易用而且高效的 RPC 框架并非一件容易的事情,幸运的是,如今有不少能够供咱们进行选择。
TiKV 从最开始设计的时候,就但愿使用 gRPC,但 Rust 当时并无能在生产环境中可用的 gRPC 实现,咱们只能先基于 mio 本身作了一个 RPC 框架,但随着业务的复杂,这套 RPC 框架开始不能知足需求,因而咱们决定,直接使用 Rust 封装 Google 官方的 C gRPC,这样就有了 grpc-rs。
这里先说一下为何咱们决定使用 gRPC,主要有以下缘由:
gRPC 应用普遍,不少知名的开源项目都使用了,譬如 Kubernetes,etcd 等。
gRPC 有多种语言支持,咱们只要定义好协议,其余语言都能直接对接。
gRPC 有丰富的接口,譬如支持 unary,client streaming,server streaming 以及 duplex streaming。
gRPC 使用 protocol buffer,能高效的处理消息的编解码操做。
gRPC 基于 HTTP/2,一些 HTTP/2 的特性,譬如 duplexing,flow control 等。
最开始开发 rust gRPC 的时候,咱们先准备尝试基于一个 rust 的版原本开发,但无奈遇到了太多的 panic,果断放弃,因而就将目光放到了 Google gRPC 官方的库上面。Google gRPC 库提供了多种语言支持,譬如 C++,C#,Python,这些语言都是基于一个核心的 C gRPC 来作的,因此咱们天然选择在 Rust 里面直接使用 C gRPC。
由于 Google 的 C gRPC 是一个异步模型,为了简化在 rust 里面异步代码编写的难度,咱们使用 rust Future 库将其从新包装,提供了 Future API,这样就能按照 Future 的方式简单使用了。
关于 gRPC 的详细介绍以及 rust gRPC 的设计还有使用,咱们会在后面的文章中详细介绍。
很难想象一个没有监控的分布式系统是如何能稳定运行的。若是咱们只有一台机器,可能时不时看下这台机器上面的服务还在不在,CPU 有没有问题这些可能就够了,但若是咱们有成百上千台机器,那么势必要依赖监控了。
TiKV 使用的是 Prometheus,一个很是强大的监控系统。Prometheus 主要有以下特性:
基于时序的多维数据模型,对于一个 metric,咱们能够用多种 tag 进行多维区分。
自定义的报警机制。
丰富的数据类型,提供了 Counter,Guage,Histogram 还有 Summary 支持。
强大的查询语言支持。
提供 pull 和 push 两种模式支持。
支持服务的动态发现和静态配置。
能跟 Grafana 深度整合。
由于 Prometheus 并无 Rust 的客户端,因而咱们开发了 rust-prometheus。Rust Prometheus 在设计上面参考了 Go Prometehus 的 API,但咱们只支持了 最经常使用的 Counter,Guage 和 Histogram,并无实现 Summary。
后面,咱们会详细介绍 Prometheus 的使用,以及不一样的数据类型的使用场景等。
要作好一个分布式的 Key-Value Store,测试是很是重要的一环。 只有通过了最严格的测试,咱们才能有信心去保证整个系统是能够稳定运行的。
从最开始开发 TiKV 的时候,咱们就将测试摆在了最重要的位置,除了常规的 unit test,咱们还作了更多,譬如:
Stability test,咱们专门写了一个 stability test,随机的干扰整个系统,同时运行咱们的测试程序,看结果的正确性。
Jepsen,咱们使用 Jepsen 来验证 TiKV 的线性一致性。
Namazu,咱们使用 Namazu 来干扰文件系统以及 TiKV 线程调度。
Failpoint,咱们在 TiKV 不少关键逻辑上面注入了 fail point,而后在外面去触发这些 fail,在验证即便出现了这些异常状况,数据仍然是正确的。
上面仅仅是咱们的一些测试案例,当代码 merge 到 master 以后,咱们的 CI 系统在构建好版本以后,就会触发全部的 test 执行,只有当全部的 test 都彻底跑过,咱们才会放出最新的版本。
在 Rust 这边,咱们根据 FreeBSD 的 Failpoint 开发了 fail-rs,并已经在 TiKV 的 Raft 中注入了不少 fail,后面还会在更多地方注入。咱们也会基于 Rust 开发更多的 test 工具,用来测试整个系统。
上面仅仅列出了咱们用 Rust 开发 TiKV 的过程当中,一些核心模块的设计思路。这篇文章只是一个简单的介绍,后面咱们会针对每个模块详细的进行说明。还有一些功能咱们如今是没有作的,譬如 open tracing,这些后面都会慢慢开始完善。
咱们的目标是经过 TiKV,在分布式系统领域,提供一套 Rust 解决方案,造成一个 Rust ecosystem。这个目标很远大,欢迎任何感兴趣的同窗加入。
此文已由做者受权腾讯云技术社区发布,转载请注明原文出处
原文连接:https://cloud.tencent.com/community/article/906224?utm_source=bky
海量技术实践经验,尽在腾讯云社区!