5大架构:细数数据平台的组成与扩展
【译者介绍】
蔡延亮,北京大学计算机硕士毕业,明略数据技术合伙人。专注于大数据解决方案的研发和实施,拥有丰富的大数据分析平台建设实施经验。熟悉商务智能 (BI)系统的设计、架构和演进规划,擅长其在电信运营商的应用;在数据ETL处理、模型设计、数据备份、生命周期管理、安全管理等领域有丰富的实践经 验;熟悉数据挖掘、机器学习等分析算法和工程应用;熟悉软件项目管理。
导读:One size does not fit all! 数据处理平台已不集中于传统关系型数据库,各种其他平台层出不穷,也各有其适用范围。
从哪些角度去理解各种数据处理平台的设计思想及发展演进呢?下面我们从几个角度讨论一下:
一、单机存储引擎设计(数据的位置)
从某种意义上说,当我们处理数据的时候,实际上是在管理数据的位置,管理数据在CPU的位置,数据相对其他数据的位置。CPU特别适合处理顺序性操作数据指令,这样他可以进行数据预取。但是随机读取操作使得预取功能几乎失效,好多预取到缓存、前端总线的数据都是无效的。
传统意义上说,磁盘的存取性能要弱于内存,但是要分随机存取及顺序存取不同的场景下讨论。在流式顺序处理场景,磁盘及SSD的读取速度已经超过内存随机读取速度。
我们如何尽量实现数据的顺序存取呢?让我们设计一个很简单的数据库开始,存取一个文件。
1、数据存储和更新
追加写可以让我们尽量保持顺序存储文件。但是当数据要进行更新的时候,有两种选择,一种是在数据原地进行更新操作,这样我们就有了随机IO操作。另一种是把更新都放到文件末尾,然后需要读取更新数据的时候进行替换。
2、数据读取
一下子读取整个文件,也是很耗费时间的事情,例如数据库中的全表扫描。当我们读取文件中某一个字段时候,我们需要索引。索引的方式有多种,我们可 以用一种简单的固定数值大小的有序数组来做索引,数组里存的是当前数据在文件中的存储偏移量。还有其他索引技术,如hash索引,位图索引等。
索引相当于在数据之上又加了一层树状结构,可以迅速的读取数据。但是打破了我们前面讲的数据的追加写,这些数据都是根据索引随机写入的。在数据库上建立索引的时候都会遇到这个问题,在传统的机械式磁盘上,这个问题会造成1000倍的性能差异。
有三种方法可以解决上述问题:
1)把索引放到内存中,可以随机存储和读取,把数据顺序存储到硬盘上。MongoDB,Cassandra都是采取这种方式。这种方式有一个弊端是存储的数据量受限于内存的大小,数据量一大,索引也增大,数据就饱和了。
2)第二种方式是把大的索引结构,拆成很多小的索引来存储。在内存中批量进来的数据,当积累到一个预定的量,就排序然后顺序写到磁盘上,本身就是 一个小的索引,数据存储完,最后加一块小的全局索引数据即可。这样读取数据的时候,要遍历一些小的索引,会有随机读取。本质是用部分小的随机读换取了整体 的数据顺序存储。我们通过在内存中保存一个元索引或者Bloom filter来实现处理那些小索引的低延迟。
日志结构的归并树(log structed merge tree, 简称LSM tree)是一种典型的实现,有三个特征:
a)一组小的、不变的索引集。
b)只能追加写 ,合并重复的文件。
c)少量的内存索引消耗换来读取的性能提升。这是一种写优化索引结构。
HBase、Cassandra、Bigtable都是通过这种比较小的内存开销来实现读取和存储的平衡
3)列式存储或者面向列的存储(暴力方式)。
纯列式存储和谷歌bigtable那种列式存储还是有所不同的,大家最好分开来看,虽然占用了同一个名字。列式存储很好理解,就是把数据按照列顺 序存储到文件中,读取的时候只读需要的列。列式存储需要保持每一列数据都有相同的顺序,即行N在每一列都有相同的偏移。这很重要,因为同一查询中可能要返 回多个列的数据,同时可能我们要对多列直接进行连接。每一列保持同样的顺序我们可以用非常简单的循环实现上述操作,且都是高效的CPU和缓存操作。
列式存储的缺点是更新数据的时候需要更新每一个列文件中的相应数据,一个常用的方法就是类似LSM那种批量内存写的方式。
当查询只是返回某几列数据,列式存储可以大规模减少磁盘IO。除此之外,列式存储的数据往往属于同一类型,可以进行高效的压缩,一些低延迟,高压缩率的扫描宽度、位填充算法都试用。即使对于未压缩的数据流,同时可以进行针对其编码格式的预取。
列式存储尤其适用于大表扫描,求均值、最大最小值、分组等聚合查询场景。
列式存储天然的保持了一列中数据的顺序性,方便两列数据进行关联,而heap-file index结构关联时候,一份数据可以按顺序读取,则另一份数据就会有随机读取了。
典型优势总结:
列式压缩,低IO
列中每行数据保持顺序,可以按照行id进行关联合并
压缩后的数据依然可以进行预取
数据延迟序列化
上面讨论的数据顺序存取的几种方案,在很多数据处理平台的最优技术方案中大都有参考。
通过heap-file结构把索引存储在内存,是很多NoSQL数据库及一些关系型数据库的首选,例如Riak,CouchBase和MongoDB,模型简单并且运行良好。
要处理更大量的数据,LSM技术应用更为广泛,提供了同时满足高效存储和读取效率的基于磁盘的存取结构。HBase、Cassandra、RocksDB, LevelDB,甚至MongoDB最新版也支持这种技术。
列式存储在MPP数据库里面应用广泛,例如RedShift、Vertica及hadoop上的Parquet等。这种结构适合需要大表扫描的数据处理问题,数据聚合类操作(最大最小值)更是他的主战场。
Kafka 通过追加式的文件或者预定义的offset集来存储消息队列。你来消费消息,或者重新消费消息都是很高效的顺序IO操作。这和其他的消息中间件架构上有所 不同,JMS和AMQP都需要上面提到过的额外的索引来管理选择器和session信息。他们最终性能表现像一个数据库而非文件。
为满足读和写不同业务场景的优化,以上这些技术多少都有某些方面的折中,或者把问题简化,或者需要硬件支持,作为一种拓展的方法。
二、分布式集群存储设计(并行化)
把数据放到分布式集群中运算,有两点最为重要:分区(partition)和副本(replication)。
分区又被称为sharding,在随机访问和暴力扫描任务下都表现不错。
通过hash函数把数据分布到多个机器上,很像单机上使用的hashtable,只不过这儿每一个数据桶都被放到了不同的机器上。
这样可以通过hash函数直接去存储数据的机器上把数据取出来,这种模式有很强的扩展性,也是唯一可以根据客户端请求数线性扩展的模式。请求会被独立分发到某一机器上单独处理。
我们通过分区可以实现批量任务的并行化,例如聚合函数或者更复杂的聚类或者其他机器学习算法,我们通过广播的方式在所有机器上使任务同时执行。我们还可以运行分治策略来使得高计算的任务在一个更短的时间内解决。
这种批处理系统在处理大型的计算问题时有不错的效果,但只能提供有限并发,,因为执行任务时会非常消耗集群的资源。
所以分区方式在两个极端情况非常简单:
直接hash访问
广播,然后分而治之。在这两种情况之间还有中间地带,那就是在NoSQL数据库中常用的二级索引技术。
二级索引是指不是构建在主键上的索引,意味着数据不会因为索引的值而进行分区。不能直接通过hash函数去路由到数据本身。我们必须把请求广播到所有节点上,这样会限制了并发性,每一个请求都会卷入所有的节点。
因此好多基于key-value的数据库拒绝引入二级索引,虽然它很有价值,例如Hbase和Voldemort。 也有些数据库系统包含它了,因为它有用,例如Cassandra、MongoDB、Riak等。重要的是我们要理解好他的效益及他对并发性所造成的影响。
解决上述并发性瓶颈的一个途径是数据副本,例如异步从数据库和Cassandra、MongoDB中的数据副本。
实际上副本数据可以是透明的(只是数据恢复时候使用)、只读的(增加读的并发性),可读写的(增加分区容错性)。这些选择都会对系统的一致性造成影响,这是CAP理论中的一个研究课题(也许CAP理论不像你想象中的那么简单)。
这些对一致性的折中,给我们带来一个值得思考的问题?一致性到底有什么用?
实现一致性的代价非常昂贵。在数据库中是用串行化来保证ACID的。他的基本保证是所有操作都是顺序排列的。这样实现起来的代价非常昂贵,所以好多关系型数据库也不把他当成默认选项。
所以说要想在包含分布式写操作的系统上实现强一致性,如同坠入深渊。(补充说明,Consistency, 在ACID和CAP中同时出现,但是意义不一样,我这儿说的是在CAP中的定义:所有的节点在同一时间看到的是同样的数据)
解决一致性问题的方案也很简单,避免他。假如不能避免它把他隔离到尽可能少的写入和尽可能少的机器上。
避免一致性问题其实很简单,尤其是你的数据是一串不再改变的事实描述。web日志就是一个很好的例子,不用担心一致性问题,因为日志存下来后就是不变的事实描述。
当然有些业务场景是必须要保证数据一致性的,例如银行转账时候。有些业务场景感觉上是必须保持一致性的,但实际上不是,例如标记一个交易是否有潜在的欺诈,我们可以先把它更新到一个新的字段里面,另外再用一条单独的记录数据去关联最开始的那个交易。
所以对一个数据平台来说有效的方式是去避免或者孤立需要一致性的请求,一种孤立的方法是采取单一写入者的策略,Datamic就是典型的例子。另一种物理隔离的方法就是去区分请求中可变和不可变的字段,分别查询。
Bloom/CALM把这种理念走的更远,默认的配置选项就是无序执行的策略,只有在必要的时候才启用顺序执行读写语句。
前面是我们必须考虑的一些点,现在思考如何把这些设计组装在一起做成一个数据处理平台?
三、架构
1、命令查询职责分离架构(CQRS)
最常用的架构就是用传统关系型数据库存取数据,上层承接各种应用。这种架构会遇到一些瓶颈,比如当数据吞吐量大到一定程度,就会遇到消息传递、负载均衡、扩容、并发性能降低等问题。为保持ACID特性,扩容问题尤其严峻。
一种解决方案是CQRS(Command Query Responsibility Segregation),命令查询职责分离)架构,该模式从业务上分离修改 (Command,增,删,改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为。从而使得逻辑更加清晰,便于对不同部分 进行针对性的优化。
还有一种简单的方式是把读和写的请求进行分离,写数据侧进行写优化处理,类似于日志文件结构。读数据侧进行读优化处理。比较代表性的实现如Oracle的GoldenGate和MongoDB的Replica Sets .
还有一些数据库,采用增加一层引擎的方式来实现上述思想。Druid就是一个很典型的例子,他是一个开源的、分布式的、实时的、列式存储的分析引 擎。列式存储特别适合需要加载大的数据块,且数据块分到多个文件中的场景。Druid把一些近线实时数据放到写优化的存储中,然后随着时间的推移逐步把这 些数据迁移到读优化的存储中。当Druid接收到请求,会同时把请求转发给读、写优化的存储,然后把返回的查询结果根据时间标记进行排序反馈给用户。像 Druid这 种类似的系统,通过一层抽象实现了CQRS的优点。
2、操作/分析桥(Operational/Analytic Bridge)架构
另一种相似的处理方式是操作/分析桥(Operational/Analytic Bridge),读和写优化视图被事件流所区分,数据流的状态是被永久保存的,所以异步视图可以通过后来的更新被重组或增强。
这样前端模块可以提供同步的读和写,这样可以简单高效的读取刚被写入的数据,或者保持复杂的ACID事物管理。
后端模块利用异步性、状态不变性、去扩展离线处理进程,具体方式可以采用副本、异化、或者完全使用不同的存储引擎。信息桥,连接前端与后端,允许上层应用使用访问数据处理平台的数据。
这种模试比较适合中级数量的部署,尤其是至少包含部分的、不可
避免的动态视图请求。
3、批处理架构(Hadoop)
如果我们的数据是一次写入,多次读,不在改变的场景,上面可以部署各种复杂的分析型应用。采取批处理模式的hadoop无疑是这种平台最广用和出色的代表了。
Hadoop平台提供快速的读写访问,廉价的存储,批处理流程,高吞吐信息流,和其他抽取、分析、处理数据的工具。
批处理平台可以主动拉取或者被推进来多种数据源的数据,将其存储进HDFS,后续可以处理成多种优化的数据格式。数据可以被压缩,清洗,去结构 化,聚合,处理为一种读优化的格式例如Parquet,或者直接被加载到数据服务层或者数据集市。通过这些过程,数据可以被查询或者处理。
这种结构在大批量的、数据不再改变的场景表现良好,一般可以到100TB以上,这种结构的进化是缓慢的,数据处理速度一般也是以小时为单位的。
4、lambda架构
有时候我们并不想等待小时后才得到结果,这是该架构的一个缺陷。一种解决方法就是加一个流处理层,就是常说的lambda架构。
lambda架构在批处理的架构上增加了一个流处理层,如同在一个拥挤城镇新建一条高架桥。流处理层可以用主流的Storm或者Samza实现。lambda架构的本质是可以快速的返回一个近似的结果,精确的结果在后续返回。
所以流处理旁路提供一个流处理窗口期内最好的结果,可以先被上层应用所使用,后续批处理流程计算出精确结果在覆盖掉前面的近似结果。这种架构是对 精准度和反馈时间做了一个聪明的平衡,作为后续发展,Spark平台同时提供了批处理和流处理模块(虽然流处理实际上市用微型批处理来实现的)。这种架构 也可以满足 100TB以上数据的处理。
这种架构的另一种代表叫kappa架构,但是本文作者没看中那种架构,觉得叫kappa属于吃饱了撑的。
5、流式处理架构
不像是批处理架构,把数据存储到HDFS上,然后在上面执行各种跑批任务。流处理架构把数据存储到可扩展的消息或者日志队列,例如kafka,这样数据就可以被实时的处理成三级视图、索引, 被数据服务层或者数据集市供上层应用使用。
这和去掉批处理层的lambda架构很相似,在消息层可以存储处理海量的数据,有足够强大的流处理引擎可以hold住这些数据处理进程。
流处理结构可以用来解决“应用集成”问题,这是个头疼复杂的问题,IT传统大佬:Oracle,Tibco,Informatica都曾经试图想解决,一些部分结果是有用的,但不是真的解决,始终在寻找一套真正可用的解决方案。
流式处理平台提供了一种解决该问题的可能性,他继承了O/A桥平台的优点:多样化的异步存储形式和重新计算视图的能力,把一致性请求给隔离。系统 保存的数据是日志的话,很天然的拥有不变性。Kafka可以保存高容量和吞吐量的历史记录,意味着可以重新计算数据状态,而不是持续的设置检查点。
类似流处理架构的工具还有Goldengate,用来向大型数据仓库同步数据,不过他在数据副本层缺乏高吞吐量支持,在数据模型管理层过于复杂。
四、小结:
我们开始于数据的位置,用来读写数据的顺序地址,从而说明了我们用到组件对该问题的折衷。我们讨论了对一些组件的拓展,通过分区和副本构建分布式的数据处理平台。最后我们阐述了观点:尽量在数据处理平台中把一致性的请求隔离。
数据处理平台自身也是一个动态调整变化的平台,依据业务需求,会把写优化转为读优化,把强一致性依赖转为开放的流式、异步、不变的状态。
有些东西我们必须留在思想中,顺序的结构化模式是一种,时序、分布式、异步是另一种。
我们要坚信:经过认真的解决,这些问题都是可控的。
附(知识补充):
简单介绍一下heap-file结构(和链表结构很相似):
支持追加数据(append)
支持大规模顺序扫描
不支持随机访问
下面是Heap file自有的一些特性:
数据保存在二级存储体(disk)中:Heapfile主要被设计用来高效存储大数据量,数据量的大小只受存储体容量限制;
Heapfile可以跨越多个磁盘空间或机器:heapfile可以用大地址结构去标识多个磁盘,甚至于多个网络;
数据被组织成页;
页可以部分为空(并不要求每个page必须装满);
页面可以被分割在某个存储体的不同的物理区域,也可以分布在不同的存储体上,甚至是不同的网络节点中。我们可以简单假设每一个page都有一个唯一的地址标识符PageAddress,并且操作系统可以根据PageAddress为我们定位该Page。
一般情况下,使用page在其所在文件中的偏移量就可以表示了。