• Email:
  • Feeds

  • Archive for the ‘data’ Category


    Dynamo一个缺陷的架构设计(译)

    在云计算的时代,Dynamo可以说是一本实现分布式存储的红宝书,借鉴Dynamo实现的产品如雨后春笋般冒出。前段时间本人曾在Twitter上戏称

    这年头,如果一个号称有“海量数据”的互联网公司,不做一个自己的Dynamo, 出去都不好意思跟人打招呼
    (http://twitter.com/xmpp/status/8023241449)

    另外一方面对于Dynamo设计思想也有不少反对的声音,比如2009/11/1在Hacker News上链接的一篇文章Dynamo: A flawed architecture引起不少争议,最后竟引起Amazon CTO Werner Vogels在Twitter上回应

    Darn, someone figured out that Dynamo is a flawed architecture. Luckily its only use is storing hundreds of millions of shopping carts :-)
    (http://twitter.com/Werner/statuses/5345892061)
    汗,有人发现Dynamo是一个缺陷的架构,幸运的是,我们只用它来存储了成百上亿的购物篮数据。:-)

    以下是这篇批判Dynamo文章大部分中心观点,所翻译的观点并不代表Tim立场。

    –译文开始–

    Dynamo: A flawed architecture

    在发表此文章之前,我也争论过Dynamo是否适合我们的系统。但是我很清楚这篇论文充满缺陷,它将错误的引导了读者让大家相信其设计,它的很多设计前后自相矛盾。下文会详细介绍这些缺陷。

    Dynamo的最终一致性

    首先,最终一致性对开发者意味什么呢?

    1. 写入的数据不能在后续的读操作中获取到。
    2. 写入的数据也有可能在后续的读操作中获取到,但读到后可能下一次又读不到。
    3. 因此对写操作后面的读取没有SLA(Service Level Agreement)保证。

    举例说明,由于Dynamo是一个key value存储,我们假设value中存储的是一个list, 当list写入数据之后另外一个client却未读取到,这时候它需要写入数据的话只能重新构建一个新的list,添加要存的值并将新list存入,这就会导致老的list数据丢失。

    (Update: 论坛上一些人指出,由于Vector Clock机制,数据丢失的场景不可能出现,我同意,不过我再提出几个其他问题。)

    1. Cassandra未用vector clock, 而只用client timestamps也达到了同样效果。
    2. Dynamo依赖合并冲突来解决此问题,一些场合下冲突很难解决。比如从list中错误的截取操作。(if deletion from the list is a valid operation – then how would one reconcile after mistaken truncation?)
    3. 另外一个场景,读取到脏数据后可能会影响后续的写入。(a stale read may end up affecting writes to other keys)

    一般的常识是读取脏数据是需要避免的,但是Dynamo中无任何措施来避免读取脏数据以及避免读取脏数据的客户端再次写入,这个在单IDC环境其实是完全可以避免的。

    Quorum一致性

    (译者注:Quorum是Dynamo的一个核心特性,主要思想是 写最小节点数W + 读最小节点数R > 所有节点数N)
    Dynamo开始就提到系统按最终一致性设计,但是在4.5中却提出用Quorum的方法来实现一定程度的一致性,意思是如果R+W>N, 则读操作就具备(强)一致性了。明显是误导。由于节点会出现不可用的情况,尤其在跨IDC情况下,任一节点随时都有可能离开quorum组,当它离开再加入的时候,R个节点返回的数据就是不一致的,因为故障节点的数据只具备“最终一致性”,而在当时返回的只能是脏数据。

    这就带来一个明显的问题,为什么要让未同步到最新数据的节点加入组?答案是Dynamo中无任何方法来判断一个节点是否数据同步,也无法判断有哪些数据不同步。因此只能做一个完全数据比较才能判断,Dynamo中用一种叫Merkle Tree的方法来实现,这个当然是一个代价昂贵且不灵活的操作,因为为了不影响Dynamo正常的读写业务,同步需要在后台执行。

    实现强一致性也可以用读取所有节点(R=N)的方式来达到,不过有2个问题。

    1. 一旦有一个节点未同步,读取就会失败。
    2. 读取的代价极高。

    我并不是第一个发现这些问题的人,比如另一知名的Cassandra产品Cassandra-225中就提到用一个中心commit log的方法来解决此问题。

    WAN considerations 跨IDC的问题

    值得指出的是,如果将Dynamo部署到多个机房,节点的断续情况会很容易发生。当一个节点连接不到,Dynamo的”hinted handoff”策略会使用一致性哈希算法将数据放入下一个节点。在多IDC环境下,下一节点通常在另一机房,因此会造成异地数据传输增加。当异地整个IDC都连不上网络分裂情况发生时,数据需要很长时间才能完全恢复。

    Disaster Recovery 灾难恢复

    Dynamo最终一致性及同步的设计对于是节点故障是有价值的,但是却无法估算有多少数据未同步。如果改用常规的commit log方式的话,很容易就能实现故障恢复并且计算未同步的数据量。

    未使用时间一致性(译者:基于timestamp的合并?)在某些场合下很难合并冲突。

    一致性还是可用性 Consistency versus Availability

    一般认为Dynamo选择了CAP理论中的AP,而BigTable选择了CA。不幸的是,Dynamo并没有搞清什么是A(availability)和P(Partition Tolerance)。读者被误导只能在C和P中做一个取舍,这个当然是错的。我们很容易在单IDC实现一致性及高可用性。大部分商业数据库就是如此,HBase/HDFS也是如此。

    很多人误以为即使在单IDC架构中,Dynamo方式比BigTable/GFS架构更合理。但Dynamo的优势其实是在多IDC。

    中心化还是去中心化

    Dynamo中提到

    In the past, centralized control has resulted in outages and the goal is to avoid it as much as possible. This leads to a simpler, more scalable, and more available system.
    过去,中心化设计导致了很多灾难,我们意识到要远离中心化。去中心化后,系统会更简洁,更具有可扩展性及高可用性。

    中心化确实会形成瓶颈,但是没有证据说明中心化就低可用性。大部分专业的存储系统通过双机热备的方式都具备高可用性。简单的说,只需要所有中心模块(电源,主板,RAID,交换机等)都按双份的方式来设计,只需要额外增加一点硬件成本,这些系统基本可以达到5个9的可用性。

    值得讽刺的是Dynamo其实在部分情况下还是一个中心化的体系,如交换机故障发生了网络分片,服务器分成2个独立的小网,这时候Dynamo对客户端是不可用的,尽管客户端可以连接上Dynamo。

    更讽刺的是我们看到Dynamo很多一致性问题都是去中心化设计所导致。

    –译文完–

    此文的讨论也非常精彩,对于想深入了解Dynamo的朋友是不可多得的资料。可参看 http://news.ycombinator.com/item?id=915212

    Friendfeed的MySQL key/value存储

    这是一篇2009年初的资料How FriendFeed uses MySQL to store schema-less data,相信大部分人已经看过了。如Fenng的中文介绍FriendFeed 使用 MySQL 的经验。本文从不同的角度再补充下。作者几个月前也曾经在广州技术沙龙作过一次Key value store漫谈的演讲,许多参会人员对key value方向存在强烈的使用意愿,但同时也对完全抛弃MySQL存在疑虑,本文介绍的方案也可以给这些人员一些架构参考。

    需求

    250M entities, entities表共有2.5亿条记录,当然是分库的。

    典型解决方案:RDBMS

    问题:由于业务需要不定期更改表结构,但是在2.5亿记录的表上增删字段、修改索引需要锁表,最长需要1小时到1天以上。

    Key value方案

    评估Document类型数据库,如CouchDB
    CouchDB问题: Performance? 广泛使用? 稳定性? 抗压性?

    MySQL方案

    MySQL相比Document store优点:

    • 不用担心丢数据或数据损坏
    • Replication
    • 非常熟悉它的特性及不足,知道如何解决

    结论

    综合取舍,使用MySQL来存储key/value(schema-less)数据,value中可以放:
    Python dict
    JSON object

    实际friendfeed存放的是zlib压缩的Python dict数据,当然这种绑定一种语言的做法具有争议性。

    表结构及Index设计模式

    feed数据基本上都存在entities表中,它的结构为

    mysql> desc entities;
    +----------+------------+------+-----+-------------------+----------------+
    | Field    | Type       | Null | Key | Default           | Extra          |
    +----------+------------+------+-----+-------------------+----------------+
    | added_id | int(11)    | NO   | PRI | NULL              | auto_increment |
    | id       | binary(16) | NO   | UNI |                   |                |
    | updated  | timestamp  | YES  | MUL | CURRENT_TIMESTAMP |                |
    | body     | mediumblob | YES  |     | NULL              |                |
    +----------+------------+------+-----+-------------------+----------------+

    假如里面存的数据如下

    {
    "id": "71f0c4d2291844cca2df6f486e96e37c",
    "user_id": "f48b0440ca0c4f66991c4d5f6a078eaf",
    "feed_id": "f48b0440ca0c4f66991c4d5f6a078eaf",
    "title": "We just launched a new backend system for FriendFeed!",
    "link": "http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c",
    "published": 1235697046,
    "updated": 1235697046,
    }

    如果要对link字段进行索引,则用另外一个表来存储。

    mysql> desc index_link;
    +-----------+--------------+------+-----+---------+-------+
    | Field     | Type         | Null | Key | Default | Extra |
    +-----------+--------------+------+-----+---------+-------+
    | link      | varchar(255) | NO   | PRI |         |       |
    | entity_id | binary(16)   | NO   | PRI |         |       |
    +-----------+--------------+------+-----+---------+-------+
    2 rows in set (0.00 sec)

    优点是

    • 增加索引时候只需要 1. CREATE TABLE,2.更新程序
    • 删除索引时候只需要 1. 程序停止写索引表(实际就是一个普通表),2. DROP TABLE 索引表

    这种索引方式也是一种值得借鉴的设计模式,特别是key value类型的数据需要索引其中的内容时。

    Memcached数据被踢(evictions>0)现象分析

    很多同学可能熟知Memcached的LRU淘汰算法,它是在slab内部进行的,如果所有空间都被slabs分配,即使另外一个slab里面有空位,仍然存在踢数据可能。你可以把slab理解为教室,如果你的教室满了,即使别的教室有空位你的教室也只能踢人才能进人。

    mc

    本文介绍的却是另外一种现象。今天监控发现线上一memcached发生数据被踢现象,用stats命令看evictions>0,因为以前也出现过此问题,后来对这个参数增加了一个监控,所以这次主动就发现了。由于给memcached分配的内存远大于业务存储数据所需内存,因此初步判断是“灵异现象”。

    第一步,netstat查看所有连接,排除是否被一些未规划的client使用,经排查后断定无此可能。

    第二步,用tcpdump抽样检查set的指令,排除是否有忘记设cache过期时间的client,初步检查所有典型的业务都有expire time。

    第三步,Google,未果

    第四步,看源代码,了解evictions计数器增加时的具体细节,oh, no…

    in items.c, memcached-1.2.8,

    125         for (search = tails[id]; tries > 0 && search != NULL; tries--, search=search->prev) {
    126             if (search->refcount == 0) {
    127                 if (search->exptime == 0 || search->exptime > current_time) {
    128                     itemstats[id].evicted++;
    129                     itemstats[id].evicted_time = current_time - search->time;
    130                     STATS_LOCK();
    131                     stats.evictions++;
    132                     STATS_UNLOCK();
    133                 }
    134                 do_item_unlink(search);
    135                 break;
    136             }
    137         }

    从源代码发现踢数据只判断一个条件,if (search->refcount == 0),这个refcount是多线程版本计数用,在当前服务器未启用多线程情况下,refcount应该始终为0,因此初步判断memcached是从访问队列尾部直接踢数据。

    为了证实想法,设计以下场景:

    1. 部署一个memcached测试环境,分配比较小的内存,比如8M
    2. 设置1条永远不过期的数据到memcached中,然后再get一次,这条数据后续应该存在LRU队尾。
    3. 每隔1S向memcached set(并get一次) 1,000条数据,过期时间设为3秒。
    4. 一段时间后,stats命令显示evictions=1

    按我以前的理解,第2步的数据是永远不会被踢的,因为有足够过期的数据空间可以给新来的数据用,LRU淘汰算法应该跳过没过期的数据,但结果证实这种判断是错误的。以上业务的服务器发生被踢的现象是由于保存了大量存活期短的key/value,且key是不重复的。另外又有一业务保存了小量不过期的数据,因此导致不过期的数据惨遭被挤到队列踢出。

    本来这个问题就告一段落了,但在写完这篇文章后,顺便又看了新一代memcached 1.4.1的源代码,很惊喜发现以下代码被增加。

    items.c, memcached 1.4.1

    107     /* do a quick check if we have any expired items in the tail.. */
    108     int tries = 50;
    109     item *search;
    110
    111     for (search = tails[id];
    112          tries > 0 && search != NULL;
    113          tries--, search=search->prev) {
    114         if (search->refcount == 0 &&
    115             (search->exptime != 0 && search->exptime < current_time)) {
    116             it = search;
    117             /* I don't want to actually free the object, just steal
    118              * the item to avoid to grab the slab mutex twice ;-)
    119              */
    120             it->refcount = 1;
    121             do_item_unlink(it);
    122             /* Initialize the item block: */
    123             it->slabs_clsid = 0;
    124             it->refcount = 0;
    125             break;
    126         }
    127     }

    重复进行上述测试,未发生evictions。

    9/8 Update: 注意到L108的tries=50没有?试想把测试第2步设置51条不过期数据到cache中,情况会怎样?因此新版的Memcached也同样存在本文描述问题。

    几条总结:

    • 过期的数据如果没被显式调用get,则也要占用空间。
    • 过期的不要和不过期的数据存在一起,否则不过期的可能被踢。
    • 从节约内存的角度考虑,即使数据会过期,也不要轻易使用随机字符串作为key,尽量使用定值如uid,这样占用空间的大小相对固定。
    • 估算空间大小时候请用slab size计算,不要按value长度去计算。
    • 不要把cache当作更快的key value store来用, cache不是storage。