Amazon Dynamo – 纠结的设计
从09年第一次阅读Dynamo论文,到最近阅读Amazon S3的一篇专利,一路过来对论文的理解可以简单归结为两个字 – 纠结。第一次看到Amazon Dynamo论文,有一种眼前一亮的感觉,Dynamo通过巧妙地组合一些P2P技术在Amazon构建了一个工程上可行的系统,CAP,NWR, Vector Clock一时成为流行词,更为神奇的是,Dynamo号称能够让用户设置不同的NWR策略从而在CAP三者之间获得一个很好的权衡,鱼和熊掌亦可兼得。而Amazon S3系统的核心基本可以分为两个部分:Blob系统存储对象(Object) + 类似Dynamo的系统存放Keymap,即”Key => Object在Blob系统中的存放位置”之间的映射关系。Amazon Dynamo论文获得OSDI最佳论文奖,这个系统也在Amazon的购物车和S3上有应用,似乎是个卓越的设计。然而,随着我接触越来越多的分布式系统,我发现Dynamo系统设计本身相当纠结,应用场景比较有限,其中的某些设计具有误导性。
1, 一致性考虑。”Eventually Consistency”是从Dynamo这个系统开始流行起来的。高可用性系统必然涉及到Replication,假设数据存储三份,为了保证性能,可以通过”Quorum Commit”的方式,即两份写成功就返回客户端,同一份数据同一时刻有一个主节点(称为Master节点)提供写服务。如果Master节点宕机,其它Slave同步节点可能没有最新的数据,但是Slave和Master上的数据是最终一致的,某些对一致性要求弱的应用可以读取Slave上的数据,Master和Slave的数据同步时间窗口为秒级。然而,Dynamo中的”Eventually Consistency”感觉有点挂羊头卖狗肉的意思,虽然Dynamo的多个副本最终会是一致性,但是可能需要处理冲突。Dynamo设计允许同一份数据同一时刻被多个节点修改,并且没有类似Paxos这样的协议进行协调,因此可能出现如下的问题:
- 写入的数据不能再后续的读操作中获取到
- 写入的数据有可能在后续的读操作中获取到,但读到后可能下一次又读不到
- 对写操作后面的读操作没有SLA保证
我不清楚为什么Dynamo不采用通常的commit log的方式进行Replication。由于Dynamo中没有任何方式保证一定能够读取到写入的数据,因此,复杂的冲突合并工作只能留给客户端,我想,也只有Amazon购物车的同学才有这个时间和兴趣来把玩这样的系统。Dynamo中的Vector Clock方法虽然能在一定程度上缓解这个问题,但不能从根本上避免冲突。从理论上讲,多个节点保持操作顺序完全一致只有两种思路:第一种思路是主备,同一时刻只有一个节点提供更新服务并复制到其它节点;第二种思路就是Paxos-based replication。
由于Amazon Dynamo的冲突合并实在是过于复杂,后来的系统,包括Cassandra以及Amazon S3,都倾向于采用”Last-write wins”,即根据更新时间戳来自动处理冲突。另外,即使是”Last-write wins”,理论上仍然不能解决问题,因为,不管采用什么算法,多机之间的系统时间都是不可能做到完全同步的。从理论上将,Amazon Dynamo是一个弱一致的系统,任何读操作都不完全保证读到最新的数据。因此,实际应用中很多非常有用的操作,比如事务,CAS操作,在Dynamo的设计中是不可能得到支持的。这就是为什么Dynamo只适用于Amazon的购物车应用和S3,购物车应用的客户端可以处理冲突,而且客户端同学也愿意花时间适应Dynamo古怪的设计,S3是一个功能单一的Key-Value系统,只支持根据主键Get, Put和Delete三种最为基本的操作。在单数据中心内部设计一个功能如此受限的系统有很多简单并且更为有效的选择,完全没有必要做成Dynamo这样。Amazon后来有一个Simpledb的云存储服务,虽然它的实现没有公开,但是Simpledb需要支持类似CAS的乐观锁机制,我认为和Dynano对于一致性和Replication的考虑是不同的,后续会有专门的文章猜测Simpledb的内部实现。
2, 分区可容忍性。有的同学也许会说,Dynamo是一个AP的系统,分区可容忍性解决得好,适用多数据中心的场景,适合提供云服务。刚开始我也是这么认为的,并且极力说服自己接受Dynamo的设计。我们可以试着把分区可容忍性具体化,网络分区的情况我想主要有几种:第一种是交换机故障,第二种是数据中心之间的网络故障。首先,网络分区很多时候是可以通过硬件冗余的方式解决的。传统的解决方法,比如Bigtable最初采用数据中心内部强同步,数据中心之间异步复制的一致性策略,那么如果数据中心之间发生故障,要么停止写服务,要么牺牲一致性,以后再同步commit log进行补偿操作。而在Dynamo中似乎可以在发生网络分区的时候仍然接受更新服务,比如整个集群被网络分区为两个数据中心A和B,两个数据中心同时提供读写服务,对同一个Key的更新互相不可见,等到网络恢复后再进行冲突合并。但是,数据中心A和B的数据有哪些不一致没有人能够回答,只有上帝会告诉你:”网络恢复后数据中心A和B最终会是一致的”。这种设计太不可控,我认为不如Bigtable跨数据中心采用异步复制的方式。
3,种子节点设计。种子节点的设计也挺纠结的。我想,Dynamo最初希望做成一个P2P系统,但是,做着做着大家就发现如果没有节点知道全局的数据分布信息会使得设计变得非常复杂,一致性也很难保证,因此,引入种子节点(Seed)。这就像我们设计一个软件,开始以为找到了一种创新的方法可以解决困扰无数人的难题:总控节点的可扩展性问题,但是,做到后来发现想错了,于是打了一个补丁。
4, 系统测试。我不是测试人员,但是我想,当测试人员拿到这个的系统时一定相当郁闷。不保证一致性的系统将大大增加系统测试的复杂度,假设测试人员发现一个正确性问题,开发人员定位了很长时间告诉他:这是系统的行为,系统允许某些情况,比如机器时钟不同步,有宕机发生的时候丢数据。我不知道测试的同学此时做何感想。
总而言之,Amazon Dynamo组合运用了很多P2P的技术,确实设计出了一个可用的系统,但是这个系统的应用场景有限,工程上可以有很多更为简单有效的替代方案。”爱之深,恨之切”,虽然上面说了很多Dynamo的种种不是,但是,Dynamo系统中确实有很多方法值得我们学习,比如分布式Hash如何进行负载均衡,虽然系统总体上有一些缺陷,但Amazon Dynamo团队的工作仍然是卓越的,这里也非常感谢他们的分享。另外,这个系统也给了我一些关于云服务的一些思考:虽然云服务最为核心的技术是分布式存储,但是技术只是一个方面,产品定价,商业策略等往往更加重要,并且云服务包含的技术很多,虚拟机,云安全,等等,远远不只是分布式存储和计算。即使核心技术只能拿到80分,其它方面如果做得足够好,整个云服务产品也能拿到95。
【以上观点和某些同学可能有冲突,欢迎批评指正!】