节选自《大数据日知录:架构与算法》十四章
14.1.2 tao图数据库
facebook是目前世界上最著名的社交网站,如果从数据抽象的角度来看,facebook的社交图不仅包括好友之间的关系,还包括人与实体以及实体与实体之间的关系,每个用户、每个页面、每张图片、每个应用、每个地点以及每个评论都可以作为独立的实体,用户喜欢某个页面则建立了用户和页面之间的关系,用户在某个地点签到则建立了用户和地点之间的关系……如果将每个实体看作是图中的节点,实体之间的关系看作是图中的有向边,则facebook的所有数据会构成超过千亿条边的巨型实体图(entity graph)。实体图中的关系有些是双向的,比如,朋友关系;有些则是单向的,比如用户在某个地点签到。同时,实体还具有自己的属性,比如某个用户毕业于斯坦福大学,出生于1988年等,这些都是用户实体的属性。图14-2是facebook实体图的一个示意片段。
图14-2 facebook实体图(fbid是facebook内部唯一的id编号)
facebook将所有的实体及其属性、实体关系数据保存在tao图数据库中,网站页面的数据读写请求都由tao来提供服务。tao是一个采用数据“最终一致性”的跨数据中心分布式图数据库,由分布在多个数据中心的数千台服务器构成,为了能够实时响应应用请求,tao以牺牲强一致性作为代价,系统架构更重视高可用性和低延时,尤其是对读操作做了很多优化,以此保证在极高负载的情况下生成网站页面时的高效率。
tao为客户端封装了图操作相关的数据访问api,使得客户端不仅可以访问实体及其属性,也可以方便地访问各种实体关系数据。比如,对于关系数据的访问可以提供如下关系列表方式的查询接口:
(id, atype)->[anew,…, aold]
其中,id代表某个实体的唯一标记,atype指出关系类型(朋友关系等),关系列表则按照时间先后顺序列出id指向的其他满足atype类型关系的实体id列表。例如,(i,comment)就可以列出关于i的所有评论信息。
1.tao的整体架构
tao是跨越多个数据中心的准实时图数据库,其整体架构如图14-3所示。首先,tao将多个近距离的数据中心组合成一个分区(region),这样形成多个分区,每个分区内的缓存负责存储所有的实体和关系数据。其中,在一个主分区的数据库和缓存中集中存储原始数据,其他多个从分区存储数据副本(这是一种比较独特的设计方式,建议读者在此处先花些时间考虑一下其设计的出发点,然后阅读后续内容)。
之所以如此设计架构,是出于如下考虑:缓存结构是tao中非常重要的一部分,对于快速响应用户读请求有巨大的帮助作用,而缓存需要放在内存中,如果内存资源成本低且足够大,那么理想的情况是每个数据中心都存放完整的数据副本以快速响应用户的读操作,避免用户跨数据中心读取数据这种耗时操作。但是考虑到要存储的数据量太大(pb级),每个数据中心都分别存储一份完整的备份数据成本过高,所以退而求其次,将在地域上比较接近的多个数据中心作为一个整体来完整地存储所有的备份数据,因为数据中心地域接近,所以通信效率也较高,这样就在成本和效率之间做了一种权衡和折中。
在每个分区会存储完整的实体及其关系数据,tao在分区内的存储架构可划分为三层(见图14-3),底层是mysql数据库层,因为数据量太多,将数据分表后形成若干数据切片(shard),一个数据切片由一个逻辑关系数据库存储,一台服务器可存储多份数据切片。第二层是与底层数据切片一一对应的缓存层,称之为主cache层(leader cache),主cache负责缓存对应的逻辑数据库内容,并和数据库进行读写通信,最上层是从cache层(follower cache),多个从cache对应一个主cache,负责缓存主cache中的内容。tao将缓存设计成二级结构降低了缓存之间的耦合程度,有利于整个系统的可扩展性,当系统负载增加时,只要添加存储从cache的服务器就能很方便地进行系统扩容。
2.tao的读写操作
客户端程序只能与最外层的从cache层进行交互,不能直接和主cache通信(见图14-4)。客户端有数据请求时,和最近的从cache建立联系,如果是读取操作且从cache中缓存了该数据,则直接返回即可,对于互联网应用来说,读操作比例远远大于写操作,所以从cache可以响应大部分网站负载。
如果从cache没有命中用户请求(cache miss),则将其转发给对应的主cache,如果主cache也没有命中,则由主cache从数据库中读取,并更新主cache(图14-4中标a和d的位置展示了这一逻辑),然后发消息给对应的从cache要求其从主cache加载新数据。
对于读取操作,所有的分区不论主从都遵循上述逻辑,但是对于客户端发出的写操作,主分区和从分区的行为有所不同。对于主分区来说,当从cache接收到写操作请求,将其转给对应的主cache,主cache负责将其写入对应的逻辑数据库,数据库写操作成功后,主cache向对应的从cache发出消息告知原信息失效或者要求其重新加载。对于从分区来说,当从cache接收到写请求时,将其转给本分区对应的主cache,此时主cache并不直接写入本地数据库,而是将这个请求转发到主分区的主cache(图14-4中标c的位置说明了此种情况),由其对主数据库进行写入。
也就是说,对于写操作,不论是主分区还是从分区,一定会交由主分区的主cache来更新主数据库。在主数据库更新成功后,主数据库会通过消息将这一变化通知从分区的从数据库以保持数据一致性,也会通知从分区的主cache这一变化,并触发主cache通知从分区的从cache更新缓存内容(见图14-4标b的位置)。
请思考:为何从分区的主cache在读操作未命中时从本地数据库读取,而不是像写操作一样转发到主分区?由本地数据库读取的缺点是很明显的,会带来数据的不一致,因为从数据库可能此时是过期数据,那么这么做的目的何在或者说有何好处?
答案:因为读取数据在cache中无法命中的概率远远大于写操作的数量(在facebook中,大约相差20倍),所以跨分区操作对写操作来说,整体效率影响不大,但是如果很多读操作采取跨分区的方法,读取操作效率会大幅降低。tao牺牲数据一致性是为了保证读取操作的低延迟。
3.tao的数据一致性
tao为了优先考虑读操作的效率,在数据一致性方面做出了牺牲,采取了最终一致性而非强一致性。在主数据库有数据变化通知从数据库时,采取了异步通知而非同步通知,即无须从数据库确认更新完成,即可返回客户端对应的请求。所以主数据库和从数据库的数据达到一致有一个时间差,在此期间,可能会导致从分区的客户端读出过期数据,但是经过较小的时延,这种数据变化一定能够体现到所有的从数据库,所以遵循最终一致性。
具体而言,在大多数情况下,tao保证了数据的“读你所写”一致性。即发出写操作的客户端一定能够读到更新后的新数值而非过期数据,这在很多情况下是很有必要的,比如,用户删除了某位好友,但如果还能在消息流看到这位好友发出的信息,这是不能容忍的。
tao是如何做到这一点的?首先,如果数据更新操作发生在主分区,由上述写入过程可知,一定可以保证“读你所写”一致性,比较棘手的情形是从分区的客户端发出写请求。在这种情形下,从cache将请求转发给主cache,主cache将写请求再次转发给主分区的主cache,由其写入主数据库,在写入成功后,从分区的主cache通知本分区的从cache更新缓存值,以上操作是同步完成的,尽管此时从分区的数据库可能还未接收到主数据库的更新消息,但是从分区的各级cache已经同步更新了,之后在这个从分区发出的读请求一定可以从各级cache中读到新写入的内容。通过这种手段就可以保证从分区的“读你所写”一致性。