1. 深入研究 Cassandra 后
重读 Dynamo Paper
2010 年 7 月
Schubert Zhang
重新认真地研读了 Dynamo Paper,读的很慢,足足花了一个星期的时间。与上次读该 Paper
不同,这次带着 Cassandra 的实现来理解它。但最终得出了负面的结论:不认为 Dynamo 的
架构是个好架构,在大规模的生产系统中,这个架构引入了太多复杂性和设计原则矛盾。或
者说 Dynamo 的架构对于存储大规模的数据是不合适的。
Cassandra 社区目前还非常活跃,但通过一段时间对 Cassandra 的研究、使用和理解,感
觉 Dynamo 的架构约束在 Cassandra 中也带来了不少问题,而为解决这些问题引入的复杂性
和维护性问题也开始突出。 随着多个曾经选择 Cassandra 的公司(Twitter、Flowdock、Facebook
的 rumor,Digg 和 Reddit 虽然仍在按计划倚重 Cassandra,但估计难逃失败)宣布“暂时”
离开 Cassandra,我们也要重新基于我们自己的理解来审视这个架构。
记得同样出自 Facebook 的 Joydeep Sen Sarma (Hive 的作者之一)曾经写过一篇 blog:
Dynamo: A flawed architecture 挑 Dynamo 的毛病,以前没有仔细看,重新读一读发现他的评
论似乎有点吹毛求疵,啥问题也没讲清楚,没有太多参考价值。但我认为 Dynamo 的架构缺
陷确实是存在的: 用这种 Consistency Hash 的环状拓扑来做存储系统, 特别是大规模(每个节
点存储数百 GB、数 TB 甚至数十 TB)存储系统,是不合适的。要想真的弄清楚问题,还是
要结合 Cassandra 来仔细读一读 Dynamo 的 Paper。
下面就个人理解,对一些问题进行粗略的讨论。
1. 采用 consistent hashing 实现数据 partitioning 带来的问题,Scalability 问题。
分布式存储系统必然要把数据 partition/distribute 到不同的节点上存储,4.2 节和图 2 描
述了基于 consistent hashing 的 partitioning algorithm,那是一个类似 Chord 的 hash ranges
组成的 ring。 但这种相对刚性的 hash partitioning 方法, 在节点加入、节点故障和 balancing
等方面的处理是相当复杂的。
(1) 首先,设计原则中节点的动态对 ring 中其他节点的影响最小的原则将被打破。
在4.2节中有这样的原则描述“The principle advantage of consistent hashing is that departure or
arrival of a node only affects its immediate neighbors and other nodes remain unaffected.”但为了
balancing和节点的能力异构的支持,打破了这个原则: To address these issues, Dynamo “
uses a variant of consistent hashing (similar to the one used in [10, 20]): instead of mapping a node to a
single point in the circle, each node gets assigned to multiple points in the ring. To this end, Dynamo
uses the concept of “virtual nodes”. A virtual node looks like a single node in the system, but each
node can be responsible for more than one virtual node. Effectively, when a new node is added to the
2. system, it is assigned multiple positions (henceforth, “tokens”) in the ring.”这样当一个新物理节
点加入时,同样期待所有其他物理节点向其迁移数据。当一个物理节点故障时,同
样期待将其数据由所有其他物理节点暂管。
而为了尽量让不同的物理机器对同一数据存储N个replicas,在配置部署这些virtual
nodes时也有些复杂: To address this, the preference list for a key is constructed by skipping
“
positions in the ring to ensure that the list contains only distinct physical nodes.”
当一个节点存储的数据量较大时(TB), 新节点的加入和离开会造成相邻节点的大量
数据迁移,这在我们研究Cassandra时也发现这个严重的问题。本来加入节点是为
了缓解系统的负载, 但因为要在少数节点之间大量迁移数据, 造成新的大量的磁盘
IO、网络IO和CPU负荷将压垮整个系统。(补充:后来在Reddit和Digg所报告的事
故中确实发生了这类严重问题。) Cassandra目前还没有实现“virtual nodes”,那么
Dynamo实现了“virtual nodes”就可以了吗?“virtual nodes”就像Bigtable的Tablet,在
数据Balance时, 可以起到在整个集群中分散负载的作用, 但看Paper的描述中可见,
这种“virtual nodes”还是要靠人去配置和维护的,可想其规划和维护难度有多大(下
节介绍)。
(2) 刚性的 hash partitioning,在集群中节点故障和恢复是常态的环境下的复杂性。
在6.2节提到为了实现balance,所做的一些优化,提到了三种partitioning strategy,
其中strategy-1和目前Cassandra的RandomPartitioner一样,即节点的位置token和
partition range placement一致,是个最刚性的partitioning strategy,这将带来几个问
题:i) 当新节点加入时,需要从相邻节点迁移数据,而要迁移哪些数据需要从相
邻节点做全面的scan才能分离出来,开销极大。如果放在后台慢慢做,新节点的
bootstraping过程将是漫长的。ii) 节点加入或退出时,因为range的动态变更,相应
的Merkle Tree必须重新构建,这也是很漫长的过程。还有些其他的维护问题都较难
实现。这种策略在现实大规模存储系统中几乎不可用。strategy-2是一种过渡策略,
一般不采用。strategy-3是较理想的策略,例如先将ring划分为10000个range,假如
现在有100台同样的机器,则每个节点分配100个range(所谓“virtual nodes”) ,这样
在节点动态时,迁移数据和计算Merkle Tree都是以range为单位的。这种策略把
partition range placement和节点布局的token分离了。但必须维护一个全局一致的节
点布局tokens和partition ranges间的映射,并且实现partition ranges的assignment,这
个逻辑也没有讲清楚如何实现,相信也是较为复杂的。其实如果采用strategy-3,和
bigtable中的tablet指派和分布方法就类似了,只是bigtable实现的是master指派,
Dynamo可能是Gossip-based(至少Cassandra是Gossip-based) ,但在逻辑上并没有本
质差异。bigtable tablet的划分是采用最安全可靠和鲁棒的B+Tree分裂算法,用户不
必去维护一个空间划分的映射,动态性和灵活性都有保证。Dynamo/Cassandra的做
法就丑陋多了。
这也是为什么在 Cassandra 的 maillist 中,Jonathan Ellis 曾指出“nodes being down
should be a rare event, not a normal condition”这样和设计初衷相反的回答。且这种假
设在大规模分布式系统中是不成立的,看看 GFS 和 Bigtable 中前面大量篇幅告诉
大家的,节点故障是常见的而不是偶然的。
3. 在Cassandra Paper 5.4节,最后得出这样的结论“An administrator uses a command line tool
or a browser to connect to a Cassandra node and issue a membership change to join or leave the
cluster.”即节点的动态退到依赖于人工管理。如果您维护着几百上千台机器怎么
办?且实际情况是,即便是人工维护,也会出乱子,系统还是会长时间不可用或超
负荷。
(3) Cassandra 中没有实现 Virtual Nodes,而计划是在节点间交换 Load 信息来将负荷轻
的节点移到负荷重的节点处去分担其负荷。 但这种方法如果做成动态的, 节点在 ring
上的移动将很频繁,而造成控制混乱。如果靠手动维护,将使维护工作很重。另外,
当节点故障时,同样会使迁移工作很繁重。
2. 一致性问题
eventually-consistent 因为在数据 write 的时候少关心一致性问题, 而采用离线一致性检查
和 read repair 的方式修正一致性。
(1) 对于我们常常遇到的 write 多 read 少的应用数据, repair 几乎不能保证数据的一
read
致性。结果是数据长期不一致,甚至永远不一致。因为很多数据可能很久或者永远
也不会被读到。
(2) Read 时做的一致性检查,虽然只比较 Digest 在网络 IO 上减少了字节数,但要知道
Read 操作是 Random,Random 对磁盘负荷的开销是巨大的。因为要比对 Digest 判
断是否 Read Repair,每个 read 操作实际上都有 hit 3 个 replica 的磁盘,这个开销是
惊人的,甚至远远超过因为所谓的 Eventually Consistent 在 write 操作是不须写全 3
份所获得的好处。如果评估过 Cassandra 就知道其 Read 性能有多差,并且对系统负
荷的影响有多大。
其实我觉得 eventually-consistent 中在 Read 时 Repair 一致性问题本身就是错误的,
我们知道为了读写最终要落到磁盘上,而为了写的快,现在一般的做法是要尽量做
sequentially 写而避免 random 写, Bigtable 就是这个基本原则(CommitLog+SSTable),
Cassandra 号称是 Dynamo 和 Bigtable 的“完美”结合(这是错误的) ,在写磁盘上
也是基于 CommitLog+SSTable。大家都知道顺序写是很快的,所以 Write 的时候保
证一致性是可以做到很快的(GFS 正时这样做的) 。但 Read 的时候因为谁也不知道
applications 要读那个数据,必然是 Random 的,选择这个时候去 Repair,必然压垮
本来就忙的很的磁盘。
(3) 离线检查采用非常好的 Merkle Tree 算法,但这种算法比较基于静态数据是比较有
效的,每次比较必须对现有数据重建 Merkle Tree。对于总是有数据不一致(例如,
节点动态、网络动态、磁盘动态等)的情况下,很难保证持续的一致性,而且每次重
建 Merkle Tree 需花太多系统资源和时间。如果评估过 Cassandra 就知道其做大量数
据的 Anty-Entropy 时系统几乎要忙死。
(4) Dynamo 的数据模型是简单的 key-value,而且每个 key-value 都很小(如<1MB)。
Merkle Tree 用于这种数据模型也许是适用的。但对于 Cassandra 的数据模型
Key-Columns,因为操作时的粒度是 Column,随时都可能出现同一个 Key 的不同
Column 不一致的情况,因此不一致的情况会更普遍,这种同步策略是否适应有待
考验。不可否认,如果 Dynamo 每个节点只存储书 GB 数据甚至更少,因为大部分
甚至全部数据可以 Cache 在内存中,或许这种架构是可以应付的。
3. Hinted Handoff 的不可见性