跟着tikv源码学rust-0:开篇和准备

最近一段时间,非常关注tidb这个开源项目。个人感觉,这个项目和蚂蚁的OceanBase是从两个层次,尝试从数据库层面上解决应用扩展的痛点。前者关注金融级应用,因此更强调跨数据中心的实物一致性和高可用;后者相比之下更为“亲民”,作为一个后端程序员,能够有朝一日将一切持久化的扩展问题都交给数据库,开发一套业务代码,能够在几十到几十万并发访问下“平趟”,是件多爽的事!

不过羞羞地说,眼下对tidb存储服务tikv的开发语言rust都还没入门,想顺利的分析代码进而有所贡献有点儿不切实际。不过根据我之前对rust的简单学习感受来说,这门语言学习曲线太陡了。不结合一个实体项目,反复嚼rustbook实在很难理解那么多零碎复杂的特性。所以我决定换个思路,从tikv入手,看看优质rust项目的开发套路,边看边学,应该感悟会更加深刻。

我的初步打算是,从对tikv感兴趣的几个功能模块入手,对代码进行由表及里的分析,结合之前对数据库存储开发一点儿经历,学习分布式数据库存储的原理和架构。对于每部分代码用到的rust语言的feature,回到rustbook或者其他学习材料,进行学习和总结。希望能坚持下去。

学习/开发环境

  • 操作系统:MacOS Sierra 10.12.5
  • IDE:Visual Studio Code 1.14.2(插件:rust 0.4.2 + racer)
  • Rust: rustup管理nightly-2017-05-29-x86_64-apple-darwin (tikv基于该环境编译和测试)

第一个PR

为了给自己迈出第一步的契机,参加了PingCAP的社区活动:十分钟成为Contributor,为tikv提交了本人的第一个pr。pr本身没什么可说的,只是实现一个简单的abs内建函数。但作为一个对rust只有理论基础的人,借此机会完整地对tikv进行一次编译,还是踩了些坑,得到了不少实践感受。

nightly版本、jemalloc和libc

和大多数rust项目一样,tikv也是night-only的。使用rustup升级到最新的nightly,编译tikv出现如下编译错误

jemalloc编译错误

到rustup的lib目录下翻了翻,果然有两个对应libc的rlib文件。在1.20前似乎都只有一个libc文件。网上查了很久也没找到原因,所以暂时只能乖乖用PingCAP推荐的nightly-2017-05-29-x86_64编译了。

note: 后来发现

librocksdb的版本

tikv底层使用facebook的rocksdb作为单节点的kv存储。rocksdb是一个C++工程,所以其头文件的版本也至关重要。在写这篇文章的时候,tikv刚刚把对rocksdb的版本依赖从5.5.1升级到5.6.1。如果没有安装对应版本的rocksdb头文件,会出现如下编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
running: "c++" "-O0" "-ffunction-sections" "-fdata-sections" "-fPIC" "-g" "-m64" "-std=c++11" "-o" "/Users/baihe/project/github/tikv/target/debug/build/librocksdb_sys-865a78dfa907ba49/out/crocksdb/c.o" "-c" "crocksdb/c.cc"
cargo:warning=crocksdb/c.cc:2115:12: error: no member named 'max_background_jobs' in 'rocksdb::Options'; did you mean 'max_background_flushes'?
cargo:warning= opt->rep.max_background_jobs = n;
cargo:warning= ^~~~~~~~~~~~~~~~~~~
cargo:warning= max_background_flushes
cargo:warning=/usr/local/include/rocksdb/options.h:506:7: note: 'max_background_flushes' declared here
cargo:warning= int max_background_flushes = 1;
cargo:warning= ^
cargo:warning=crocksdb/c.cc:3181:11: warning: 7 enumeration values not handled in switch: 'kColumnFamilyName', 'kFilterPolicyName', 'kComparatorName'... [-Wswitch]
cargo:warning= switch (prop) {
cargo:warning= ^
cargo:warning=crocksdb/c.cc:3210:11: warning: 10 enumeration values not handled in switch: 'kDataSize', 'kIndexSize', 'kFilterSize'... [-Wswitch]
cargo:warning= switch (prop) {
cargo:warning= ^
cargo:warning=2 warnings and 1 error generated.
exit code: 1

rustfmt问题

PingCAP团队使用的rustfmt是 0.6 的版本,如果使用最新版本会导致测试用例编译失败。

rust-clippy

tikv项目中使用了rust-clippy。这是一个常用的rust源码检查工具,帮助开发者保证代码质量,避免不当的代码实践。由于本人目前rust零基础,却仍希望未来用rust做些事情的希望,这类工具对我是非常有价值的。

rust-clippy本身是一个rust编译器插件,tikv中将它作为一个optional依赖,通过cargo或者rustc在编译时控制feature:clippy来实现打开/关闭该插件。

clippy在打开状态下,可以检查出类似如下的代码问题:

1
2
3
4
5
6
7
src/main.rs:8:5: 11:6 warning: you seem to be trying to use match for destructuring a single type. Consider using `if let`, #[warn(single_match)] on by default
src/main.rs:8 match x {
src/main.rs:9 Some(y) => println!("{:?}", y),
src/main.rs:10 _ => ()
src/main.rs:11 }
src/main.rs:8:5: 11:6 help: Try
if let Some(y) = x { println!("{:?}", y) }

非常棒,我准备把rust-clippy作为以后rust项目的必备依赖。rust-clippy还有其他使用方法,具体可以浏览其github主页文档。值得一提的是,rust-clippy也是个nightly-only项目。

Rust学习点

这个系列应该是本人通过tikv源码学习rust和数据库技术的笔记。因此希望在每篇文章的结尾,对于这部分工作学习到的rust的关键点进行总结。并对这些关键点做编号,帮助反向索引。

KP-01:条件编译和feature

上文中提到的rust-clippy作为编译器插件,由feature控制打开/关闭状态,因此去查询了feature和条件编译相关的功能。

属性(Attribute)

属性是rust中支持的一种修饰符(Annotation),通常用在一个声明(struct、mod、……)上,具体定义可以看rustbook第一版中对于属性的描述。完整的reference在这里,等有机会在看(估计就不会看……)。

cfg/cfg_attr属性

在rust语言的一大堆属性中,有一类特殊属性,可以根据编译器传入的feature开关,控制代码编译的行为。该属性主要有如下两种方式:

1
2
3
4
5
6
7
8
9
10
#[cfg(foo)]
struct Foo;
#[cfg(feature = "bar")]
struct Bar
#[cfg(target_os = "macos")]
fn macos_only() {
// ...
}

放到C语言里,相当于预编译开关,代码比解释更明白:

1
2
3
4
5
6
7
8
9
10
11
12
13
#if foo == true
struct Foo;
#endif
#ifdef bar
struct Bar;
#endif
#if target_os == "macos"
void macos_only() {
// ...
}
#endif

另外在cfg属性里还支持布尔组合,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[cfg(any(foo, bar))]
fn needs_foo_or_bar() {
// ...
}
#[cfg(all(unix, target_pointer_width = "32"))]
fn on_32bit_unix() {
// ...
}
#[cfg(not(foo))]
fn needs_not_foo() {
// ...
}

不用看文档,猜也能才出来allanynot对应的是与、或、非。这些布尔表达式也支持嵌套,来实现更为复杂的条件判断。但总体而言,我还是更喜欢C语言的写法。

cfg_attr属性有两个操作数,可以基于条件来设置其他属性。

1
2
#[cfg_attr(a, b)]
struct Foo;

在条件a满足的情况下,相当于

1
2
#[b]
struct Foo;

否则就完全没有作用。

这篇文章描述了很多基于cfg_attr的有趣玩法,特别是可以实现动态文档和动态宏定义,有兴趣可以实践一下。

feature和plugin

1
2
#![feature(plugin)]
#![cfg_attr(feature = "dev", plugin(clippy))]

上述代码出现在tikv源代码tikv-server.rs的文件开头,有了对于条件编译的相关背景,我们知道上述代码的作用是:

  1. 打开feature:plugin用于支持插件加载
  2. 如果编译器传入feature包含dev,使插件clippy生效

note: 为啥用的是#![cfg]/!#[cfg_attr]而不是#[cfg]/#[cfg_attr]?看看文档就知道了。

在tikv的cargo.toml文件中

1
2
3
4
5
6
7
8
[features]
default = []
dev = ["clippy"]
...
[dependencies]
clippy = {version = "*", optional = true}
...

这样,在tikv编译过程中,我们就可以通过执行cargo build --features "dev"将参数--cfg feature="foo"传递给rustc编译器,就会引入optional依赖clippy,并依照代码中的cfg_attr属性为编译器加载clippy提供的插件。

根据crates.io文档The Manifest Format,feature是用户在cargo.toml中定义的编译器flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[features]
# 默认feature集合,设置为空
default = []
# foo是没有依赖的feature,主要用于条件编译,例如:`#[cfg(feature = "foo")]`
foo = []
# dev是依赖于optional依赖clippy的feature。一方面dev可以作为alias让我们以更可读的方式描述feature,
另一方面可以通过optional依赖引入该feature的扩展功能,如clippy提供的编译器插件。
dev = ["clippy"]
# session是对于外部依赖cookie提供的另一个feature:cookie的alias
session = ["cookie/session"]
# feature可以是一个组依赖,其中然包括optional依赖,也可以是session这种其他feature
group-feature = ["jquery", "uglifier", "session"]
[dependencies]
cookie = "1.2.0"
jquery = { version = "1.0.2", optional = true }
uglifier = { version = "1.5.3", optional = true }
clippy = { version = "*", optional = true }

stable/nightly的区别

这是我学习rust最困惑的地方,似乎接触到的所有rust项目都声明自己是nightly-only,那stable还有个毛用啊?直到在A tale of two Rusts这篇文章中看到这么一句话:

Stable Rust is dead. Nightly Rust is the only Rust.

Rust的stable和nightly的差别,可以类比python的2和3,甚至差异更大。文章中任务rust的nightly可以被认为是另一门变成语言。一方面,很多feature只有在nightly中才可以使用,这些特性在rustc的-Z参数中。如果在stable中使用该参数,会看到如下信息:

1
2
> rustc -Z extra-plugins=clippy
error: the option `Z` is only accepted on the nightly compiler

这些feature需要在nightly中经过实践验证,稳定后才有可能移入stable中。

另一方面,存在一个重要的feature,永远不大可能从nightly进入stable。就是rust-clippy用到的:

1
#![feature(plugin)]

可以认为凡是需要code-generation的rust程序,都得使用该feature。为啥改feature不可能stable,原文中的描述没怎么看懂,先放到这里,以后参悟:

Why compiler plugins can never be stable, though? It’s because the internal API they are coded against goes too deep into the compiler bowels to ever get stabilized. If it were, it would severely limit the ability to further develop the language without significant breakage of the established plugins.

参考

  1. rustbook-1st中的Conditional CompilationAttributes
  2. Quick tip: the #[cfg_attr] attribute
  3. A tale of two Rusts
  4. crates.io文档The Manifest Format