分类:
2010-09-11 16:18:25
最近我为 cassandra 添加了一些小特性,于是我花了些时间来更仔细地考察了这个系统的内部设计。诸如 embedded service 这样的特性实际上并不需要对源代码和设计的深入理解,但其他特性,比如 truncate 就需要对系统中使用的不同算法有深入的了解,如写操作时如何进行的,读操作时如何进行的,值时如何删除的(提示:他们没有……)等等。
源代码虽然不是非常长,有大约91136行,但非常密级,而且有很多算法,所以直接读这些代码对我来说并不是非常简单。我是用如下的外科手段来进行行数计数的($ cassandra/trunk $ find * -name *.java -type f -exec cat {} \;|wc -l
)
我写这篇文章希望可以帮助其他人能更快地阅读这些代码。我不会介绍那些基础信息,比如“什么是Cassandra”,如何部署,如何检出代码,如何编译,如何下载thrift等。我也不想介绍算法最复杂的部分,比如ae-service使用的merkle-tree如何 ,Cassandra 中不同部分都是用的 bloom filter 是什么、如何工作,以及 gossip 是如何使用的。我不认为我适于解释所有这些问题,而且在cassandra的开发者wiki上也已经介绍这些了。我要写的就是我学习cassandra的途径,以及我在这个过程中学到了什么。我没有在其他地方发现过类似文档(当我完全完成的时候,可能我会把这些写入wiki),所以我觉得这对我下次再深入新的源代码会非常有用。
最后是一个免责声明:这里仅仅是我对系统如何工作的个人理解,它们是不完整、不确切的,特此警告。注意我也只是在学习,或多或少也是Cassandra菜鸟。也请注意,Cassandra 是一个运动目标,它一直在快速开发者,任何一个代码的快照或早或晚都会发生些变化。在本文写作的时候,官方版本是 0.6.1,不过我工作在 trunk 上,这个分支将来会成为 0.7.0。
这里是我所采取的几个步骤以及我学到的东西的一个描述。
首先你需要下载代码并运行单元测试。如果你使用 eclipes,IDEA,netbeans,vi,emacs等等这些的话,可能还需要配置一下。这非常简单,这里有更多介绍。
接下来你需要读一些背景材料,这依赖于你想搞哪个部分。我希望理解读操作、写操作以及值如何删除,所以我把下面每个文档都看了差不多五遍。没错,每个五遍。它们包含了大量的信息,我发现每次读我都能被更多的一些细节所吸引。我先读过这些文档,然后读源代码,确定我理解了算法如何在类和方法中实现,然后再读文档,然后再读源代码,读单元测试(并用debugger运行它们)等等。这是这些文档:
我还看了 Google BigTable 的论文和让人着迷的亚马逊的 Dynamo 的论文,不过这都是很久以前的事情了。它们是很好的背景材料,不过对于理解实际代码并不是必须的。
好了,读完所有这些文档,我开始知道能做什么、如何做了,但我感觉我还没有到达能写新特性的阶段。在读代码几次之后,我发现我有点晕了,还是不了解诸如“值到底是不是真的被删除了”,那个累负责哪个功能,有几个Stage,Stage之间的数据流是什么样的,还有“如何标记整个列族为删除”,这是我在truncate操作中真正想做的。
Cassandra 的操作使用的并发模型在 SEDA 论文中有介绍。这个并发模型大致是这样的,和很多其他兵法系统不同,一个操作,比如一个写操作,并不是在同一个线程中开始和结束的。相反,一个操作在一个线程中开始,之后把操作(异步)交给了另一个线程,然后再传递到下一个进程,直至完成。事实上,操作并不是在线程间流转,而是在stage间流转。操作从一个stage转向另一个。每个stage都和一个线程池相关联,这个线程池在方便的时候来执行这个操作。一些操作是 IO bound 的(译注:这里应该是CPU吧,猜的),另一些则受限于磁盘或网络,所谓“方便”取决于资源的可用性。SEDA 论文把这个过程解释得非常好(很好的文章,值得一读),简单地说你从中得到的是更高级别的并发性和更好的资源管理,资源包含 CPU、磁盘、网络等。
所以,要理解 Cassandra 的数据流,你首先需要理解 SEDA。然后你需要了解 Cassandra 中有哪些 Stage,以及这些 stage 之间数据时如何流动的。
十分幸运,作为一个起点,StageManager 类中包含了一个不完整 stage 列表:
public final static String READ_STAGE = "ROW-READ-STAGE"; public final static String MUTATION_STAGE = "ROW-MUTATION-STAGE"; public final static String STREAM_STAGE = "STREAM-STAGE"; public final static String GOSSIP_STAGE = "GS"; public static final String RESPONSE_STAGE = "RESPONSE-STAGE"; public final static String AE_SERVICE_STAGE = "AE-SERVICE-STAGE"; private static final String LOADBALANCE_STAGE = "LOAD-BALANCER-STAGE";
我就不具体介绍每个 stage 都负责什么了(因为我也不知道……)但我可以说大致说,ROW-READ-STAGE 在读操作中,ROW-MUTATION-STAGE 参与了写和删除操作,而 AE-SERVICE-STAGE 负责 anti-entropy (译注:整理?不知道怎么确切用中文表达了)。这不是一个完整的 stage 列表,根据你感兴趣的代码路径,用这个方法,你可以找到更多。比如,查看文件ColumnFamilyStore你可以找到更多的stage,如FLUSH-SORTER-POOL, FLUSH-WRITER-POOL 和 MEMTABLE-POST-FLUSHER。在 Cassandra 中,stage 由 ExecutorService 的实例来唯一标识,这差不多是一个线程池,他们有全大写的名字,如 MEMTABLE-POST-FLUSHER。
我画了一张混有类和stage的图来便于理解。这不是合法的UML,但我觉得这对于了解数据在系统中如何流动是个很好的方法。这不是全部类和stage的完整的图示,仅仅是我感兴趣的一部分。
可以使用一个 debugger 来读代码,运行一个单元测试是了解事情如何工作的非常棒的方法。我不是一个debugger的铁杆粉丝,但是他们有一个可取之处就是通过单步执行单元测试来学习新代码。所以我所做的证实单步执行代码中的单步执行。这非常酷。我还运行了 Hector 的单元测试,它使用 thrift 接口并运行一个嵌入的 cassandra 服务器,这个方法一针见血、界面友好而且还能学到更多东西。
接下来我所做的是使用一个工具来从已有代码中提取类图。这不是非常有用。
好,我使用的工具不是很棒,但这不是最关键的问题,问题是 cassandra 的代码书写方法使得类图对于理解代码作用很有限。UML 类图对于面向对象设计非常有效。类图的必要性在于列出类、成员以及它们的关系。比如类 A 是类 B 的一个列表,这样通过 UML 类图可以看出 A 是 B 的聚合,而且仅仅通过类图就可以学到很多。比如一架飞机有很多乘客。
Cassandra 是一个拥有坚实算法后台和优秀性能的系统,但是,老实说,依我之见,从好的面向对象实践的视角来看,它可不是一个很好的研究案例……它的类包含很多静态方法和成员,而且在很多地方,你可以看到一个类调用另一个类的静态方法。纯粹的C风格,所以我发现类图尽管从类的可视化以及类之间的关系方面有些帮助,但并不是非常有用。
我放弃了类图,继续进入下一种兔——序列图。
序列图非常适于实体之间的交互的抽象和可视化。这里,一个实例可能是一个类,一个 STAGE 或一个 thrift 客户端。很幸运,使用序列图,你不必太专注于序列图里实体的类型,你只需要把他们都表示为 actor 就行了(至少我觉得这么做就够了,希望UML大神们原谅)。
下面的图通过运行 Hector 的单元测试并使用一个(单节点)嵌入式 Cassandra 服务器得到。这个序列图并不很通用,它仅仅描述了一种可能的执行路径,而实际上可能有很多种,但我尼克让它们更简单一些,尽管有点不太精确。
读操作:
写操作:
最后提示:作为 Cassandra 的用户我应该使用 Keyspace, ColumnFamily, Column 这些名词。不过,代码中使用了 Table 这个名词。啥是 Table 呢?……原来,Table实际上就是 Keyspace…… 就是一个提示,仅此而已。
研究代码是一项艰巨而有成就感的工作,我希望这篇文章帮助你也有个好的起点,快点跑起来。