yanghao

共享数据

通常而言,在集群化的环境中,或者多服务共存的背景下,多个系统服务需要使用大量相同的数据,而将这些数据进行统一的管理、分配和共享称为数据共享,而这些数据称为共享数据。使用共享数据,可以使更多的服务充分地使用已有数据资源,减少数据提取、装配等重复计算和相应开销,而把精力重点放在服务本身的业务逻辑的处理上。

一、 弱一致性共享数据

在共享数据中,主要分为需要强一致性的共享数据和弱一致性的共享数据。

其中强一致性共享数据需要满足各个服务之间的同步要求,简而言之就是在任意时刻所有服务看到的数据是完全相同的一份。一般来说,通过一个集中的数据中心\数据库来获取数据,或者各个服务通过使用分布式锁服务来满足强一致性数据的同步需求。强一致性数据一般来说数据量较小或访问速度要求不高,数据量小可以使用单点的数据中心存储数据,访问速度要求不高可以通过paxos协议实现多服务/多数据库同步。这类数据的例子如账户余额信息,商品存货信息等。

弱一致性共享数据通常是需要频繁访问的经过拼装的大量数据,不涉及到同步问题,数据改变只要在一定时间内能够生效即可。大部分业务数据是弱一致性要求的,比如展示广告的广告基本信息,检索系统的数据,变化不敏感的页面信息等。根据性能要求不同,这类数据有不同存储方案:若cpu瓶颈远大于io开销,或者数据量巨大,则一般通过访问数据库\文件系统或内存数据库临时获取并拼装数据,比如检索系统的数据一般放在文件系统中;若响应速度要求很高,io瓶颈大于cpu计算开销,而全内存可以支撑的数据,则一般使用内存副本来完成,广告业务的基本信息可以用内存副本存储。

二、共享数据内存副本的同步问题

对于集群服务来说,内存副本就是本服务内部的内存数据,使用方式和响应速度完全和普通内存数据相同。对于数据中心而言,则需要一个架构来完成从数据提取、装配到同步到集群服务的流程。

本文以有道广告系统的设计和演化的实践为例,描述一个企业级共享数据处理的发展之路,从而提供管理和同步共享数据内存副本的一种可行思路。

有道广告共享数据管理的发展之路

一、数据库时代(阶段一)

这是一个原始的方法,数据使用实际上称不上是数据共享。集群服务使用的数据在每个服务内部使用定时任务读取数据库和文件系统,组装原始数据,生成内存副本。

数据库时间阶段一

二、数据服务层时代(阶段二– 阶段四)

*数据库时代的瓶颈

数据库时代的数据共享方式存在很多问题,包括在性能上和数据管理上。

在性能上的问题很明显:计算数据开销大。系统中的数据既有从数据库和文件系统读出的,又有需要每次读出以后经过计算的,读数据库再加上临时计算,占用了集群服务本身的大量io和cpu开销。并发数受限。随着集群规模的扩大,系统输入压力的增大,数据库横向备份从库的方式无法方便有效的解决并发连接问题,对于多个数据库从库而言,本身也没有负载均衡的高级功能。数据库性能瓶颈。传统数据库易用但不好用,性能问题我们自身无法进一步优化,尤其数据库主从同步与集群服务大量长时间的读需求冲突后造成的锁行或锁表让内存副本的同步效率低下。

同样,原始的数据生产方式还造成工程和管理上的问题。数据来源混乱,不易阅读。集群服务需要的成品数据没有可视化的视图来供相关人员追踪和调试,每个服务都需要依赖提供数据来源的所有服务(比如数据库、分布式文件系统等)。系统监控困难。由于每个服务独立生产数据,数据生产的流程和生产数据的正确性难以监控,一旦部分服务出现数据更新问题,则长时间无法反馈到运维人员。

*引入数据服务层(阶段二)

鉴于上述数据库时代的种种弊病,我们开始设计并实现数据服务层来解决共享数据同步的问题。针对于当时系统的问题,数据服务层设计时的主要思想是下面几点。

以数据为导向。隔离产品和后台数据,成为连接各上层服务的界面。解开上层服务和数据之间的耦合,各服务可以更关注于本身的功能、策略相关的部分。把数据服务隔离开以后,数据层不区分数据的功能属性,做的比较薄,而数据的存储、缓存和传输是独立的组件,有利于提升性能、扩展功能、测试和调试。

面向广播型数据。对于一类特定的数据,数据的生产者是少量,甚至单一的,而消费者则是众多的,少量的生产者保证没有冗余的数据计算。

持久化+缓存的存储策略。数据服务层分为两个子层:以分布式文件系统作为统一的持久层,在其上组织数据缓存层,作为上层服务使用数据服务层的入口。使用持久层,可以把维护数据持久化的任务隔离开,方便数据的调试、监测。缓存层可以横向扩展,甚至做成多级缓存结构,解决并发数受限的问题,并对缓存集群加入访问的负载均衡设计。

放松的一致性模型。借鉴最终一致性(Eventual Consistency)和连续一致性(Continuous Consistency)模型的思想,采用如下的一致性模型:第一,同一时刻数据的各个副本容许有一定的差异,在某种差异度量内认为副本是一致的。第二,如果数据没有更新,在有限的时间内所有副本会趋于最终一致。

*数据服务层的系统结构

按照上述设计思想,数据服务层的系统逐渐搭建起来。按照数据流,依次为持久层、缓存层和内存副本三个层次;按照同步系统节点划分,主要是数据生产者,数据缓存集群,客户端三个部分。

数据生产者:对于一种数据一般使用单独的一个数据生产者,定时从数据库、文件系统中读取数据,然后筛选、拼装成为合成数据,全量写入持久层。

数据缓存集群:由若干台缓存服务构成的集群组成了缓存层。每一台缓存服务都定时从持久层全量读取数据,记录每一条数据时间戳,为客户端提供基于数据时间戳的增量传输协议。

客户端:上层服务使用的api,定时根据增量传输协议通过网络连接从缓存集群获取数据进行同步。客户端对于缓存集群的选择,通过zookeeper进行负载均衡,选取负载最小的缓存进行连接。

数据服务层的系统结构

*数据服务层第一次优化(阶段三)

数据服务层引入后,我们曾持续对其进行性能优化,大的优化有两个阶段,第一个阶段的重点是优化系统资源占用,包括持久层存储空间、io数据传输速度等,第二个阶段主要是数据实时性的优化,将系统的一致性速度提高,以满足部分变化较为敏感的数据。当然,对于系统可用性以及系统监控方面的完善也在持续进行。

第一轮优化在数据服务层引入后就陆续开始了,该阶段进行了一次底层数据传输协议性能优化,优化的主要思路是session压缩。客户端与缓存之间的传输协议性能问题的主要原因是一次交互中的会话次数太多,且每次会话都只传输少量数据,导致传输的效率很低。从这一点入手,考虑将多次会话压缩,将每次传输的众小对象压缩成一个很大的对象然后一次性传输。

协议优化效果如下,可以看出,由于协议造成的传输问题已经解决。

数据服务层第一次优化

 

 

 

 

 

 

 

我们进一步优化了客户端与缓存之间的传输协议,本次修改的主要思路是减少数据传输量。变化的数据毫无疑问是不能减少的,但协议相关的附加数据却可以减少——比如时间戳。当时增量传输协议是客户端一次性将所有数据的时间戳传输给缓存,缓存通过时间戳来判断哪些数据需要更新——在最坏情况下,如果任何数据都没有改变,我们也需要传输所有数据的时间戳!这是一笔不小的开销。对于这种情况,我们使用了一种新的同步方式:基于版本号的增量传输。简单来说,对于每次数据改变,服务器端可以记录版本号,并对当次改变的数据保留一个标号集合。客户端同步时只需告诉服务器端当前自身数据的版本号,服务器端查找在此版本号之后所有更新的版本序列,它们的所有改变数据就是需要新传输的数据。为了处理版本号序列的不断增长,设计一个方案,当版本号序列长度超过阈值后,进行相邻版本两两合并(我们谓之退化)——合并后会产生少量的冗余传输,但客户端版本越新,冗余传输就越少,这是符合客户端不会长期没有同步的使用现状的。

后续一项重要的优化是持久层的增量存储——原架构中持久层每次全量存储的空间太大,考虑到数据安全的问题,进行每天若干次全量dump + 每次数据生产时根据上次持久层数据进行增量存储。这项优化为分布式文件系统节省了近20倍的存储空间。

*数据服务层第二次优化(阶段四)

随着公司业务系统的扩大和数据服务层的广泛应用,内存副本同步流程中的某些不足就暴露出来了——对于数据实时性的要求没有支持。设计时的一致性模型没有限制数据从改变到最终同步到上层服务内存副本的数据延迟,但显然,在业务系统中这项指标是必须看重的,对于对外系统中,数据延迟影响用户体验;对于后台服务而言,数据延迟可能影响计算的准确性、性能或者效果。

降低数据延迟需要考虑两方面的问题:让数据生产者实时将新数据生产出来,以及更快的将数据同步到内存副本中。为了解决数据生产的实时问题,我们加入了通知客户端,将某些维度的数据改变以消息的形式发送到rabbitMq 的exchange中。不同的生产者根据自身生产数据的需求,从rabbitMq中订阅所需的消息,并提供实时生产功能。

较为棘手的是如何将新数据更快的同步到内存副本中。首先,按照数据服务层的设计,生产者的数据生产后存放于持久层,缓存定时从持久层获取全量数据,进而进行数据更新——全量数据读取注定定时时间较长,影响缓存获取实时数据的能力。其次,缓存为客户端提供增量更新协议是基于版本号队列的,而通知客户端上线后,持续不断的新数据将会实时生产出来,版本号队列会迅速退化,传输效率难以保证。

我们对数据服务层架构做出改变,首先是分离持久层与缓存层的关系——持久层仅用于数据备份,在数据同步流程中去除。缓存直接与生产者通过网络进行增量更新,使用的传输协议与缓存到客户端的传输协议一致。统一传输协议的另一个好处是可以层级的扩展缓存服务:若生产者连接的缓存过多,网络io瓶颈,可以将缓存变为多层,某些缓存可以直接通过连接其余缓存获取数据。

分离持久层之后,剩下的问题就是寻找一种适合实时更新的增量传输协议。在考察了一些业界和学界常用的增量传输协议后,我们确定了使用MerkleTree协议。在业界的应用中,MerkleTree的大规模使用主要有两个:豆瓣小文件系统同步和cassandra数据备份。前者使用文件修改时间来表征小文件,并利用MerkleTree组织文件修改时间实时同步小文件修改。后者使用MerkleTree来对多个副本进行一致性检查,从而进行readrepair一致性修复操作。下面简单说一下MerkleTree和我们是如何利用其进行内存副本的同步的。

merkletree结构图

 

 

 

 

 

 

 

MerkleTree本质上来说是一棵hash树,以数据集合为例,每一个数据的hash值构成MerkleTree的一个叶子节点,而非叶子节点的值是所有它的儿子值的hash。很明显,在不考虑冲突的情况下,根节点相同的两颗MerkleTree代表这两个完全相同的数据集合。增量传输协议由客户端发起,将客户端根节点值发送给服务器端,若与服务器端根节点值一致,则同步完成,否则服务器端将把根结点的所有儿子节点发送给客户端,进行递归比较。对于不同的hash值,一直持续获取直到叶子节点,就可以完全确定已经改变的数据。以二叉树为例,所有的数据同步最多经过LogN次交互就可以完成,单一数据改变最多造成LogN次比较,全数据集改变最多需要2N次比较。

MerkleTree传输协议不会因为频繁更新而导致传输效率退化,并且在同步过程中还可以通过Copy On Write来实现lock free的更新。下面是进行MerkleTree协议优化后的传输量图形以及实时性优化之后的系统数据延迟情况。

两种协议在不同数据变化量的表现(传输量)
两种协议在不同数据变化量的表现(传输量)

在频繁小规模数据改变情况下,MerkleTree协议额外传输量大大低于时间戳协议;另外,随着总数据规模增长,MerkleTree额外传输量成对数增长,时间戳协议线性增长,对于10w数据情况下,变动数据低于约6%的时候MerkleTree占优,而当数据达到50w后,变动数据低于约15%的时候MerkleTree占优。

加入实时性优化前后延迟时间对比

加入实时性优化前后延迟时间对比

 

三、数据服务层当前应用情况

在当前有道计算广告系统中,数据服务层已经广泛应用于各个分布式服务上。包括广告计算相关的算法数据、展示相关的业务数据、渲染相关的样式数据以及策略相关的控制数据等均使用数据服务层进行组装、存储和同步。后续,也将持续地对其进行优化,可继续推广到部分有道其余产品线的业务数据中。

总结

经过上述分析可以看出,内存副本不是所有共享数据应用场景的解决方案,但有很大一类数据可以采用内存副本的方式进行共享——看看load到各个集群服务内存中的数据吧,他们大多都可以抽取出来成为一次性生产的共享数据。

对于大多数分布式集群服务而言,都会包含几种不同类型的数据——敏感的信息(如商品余量),需要花费大的代价来实时获取(比如通过网络连接数据中心),这部分数据通常数据量很小;庞大的数据(如网页搜索索引数据)通过分布式文件系统获取,忍受较大的io延迟;数据量中等,响应要求高(如qps或latency)的数据(普通业务数据基本都属于这部分),内存副本是它们的一个好选择。

内存副本区别于普通全内存的数据就在于它是一次性生产,多个分布式服务共同使用的数据,设计一套装配、组织和同步的框架是其核心所在。上述有道数据服务层的发展经历对于设计一套内存副本的数据流和控制流有不错的借鉴意义。