5大架构:细数数据平台的组成与扩展

作为软件工程师,不可避免地受到周围计算机工具的影响,语言、框架、甚至执行过程都会影响我们构建的软件

数据库亦如此,基于一种特殊的方式,不可避免地影响到我们对应用程序中易变和共享状态的处理。

过去的十多年,我们采用不同的方式去探寻这个世界。采用不同理念的一小众开源项目,它们不断成长,你中有我,我中有你。平台集成了这些工具,每个控件通常都能提高某些基础硬件或者系统效能。结果是平台无法通过任何单一的工具解决某些问题,不是太过笨重,就是局限于某一特定部分。

因此当今数据平台多种多样,从简单的缓存层、多语言持久化层到整个集成数据管道,针对多种特定需求的多种解决方案。在某些方面,确实有不错的表现。

因此本对话的目的就是解释一些流行的方式方法如何发挥作用,为什么会有如此表现。我们先来考虑组成它们的基本元素,这样便于在后续的讨论中对这些认识通盘地考虑。

从某种抽象的角度看,当我们处理数据时,实际上就是对其进行局部性(locality)处理,局部性到CPU、局部性到我们需要的其它数据。有序地获取数据是其中很重要的部分,计算机很擅长序列化的操作,这些操作是可以预测的。

(译者注:局部性是计算机中一种预测行为,通过缓存、内存中预取指令、处理器管道分支预测等技术来提高性能;更多参见《操作系统精髓与设计原理》。)

若是有序地从硬盘中获取数据,数据会预获取存入硬盘缓存、页缓存、以及不同层级的CPU缓存中,这可以极大地提升性能。但这对随机数据寻址意义不大,这些数据存于主内存、硬盘或者网络中。实际上,预获取反倒会拉低随机负载能力:不论是各种缓存或是前端总线,充满了不太会被用到的数据。

硬盘通常被认为性能稍低,而主内存稍快些。这种认识不见得一直是对的,随机和有序主内存负载之间相差一两个数量级。用某种语言管理内存,事情往往会变得更加糟糕。

从硬盘有序获取的数据流性能确实好过随机寻址主内存,或许硬盘并不像我们想的那样跟乌龟似的,至少在有序获取的情况不会很慢。固态盘(SSD),特别是采用PCIe接口,正如它们显示不同的权衡,将事情复杂化。但采用这两种获取模式带来的缓存收益是不变的。

译者注:数据流就是大量连续到达的、潜在无限的有序数据序列,这些数据按顺序存取并被读取一次或有限次。

假设我们要创建一个简单的数据库,首先从基础部分文件开始。

保持有序读和写,文件在硬件上会表现地很好。我们可以将写入的数据放入文件的末尾,可以通过扫面整个文件进行数据读取。任何我们希望的处理过程可以随着数据流穿过CPU而成真,比如过滤,聚合、甚至做一些更复杂的操作,总之非常完美。

倘如数据发生诸如更新这样的变化会怎样?

我们有多个选择,在某个位置更新这个值。我们需要利用固定长度的字段,在我们浅显的思想实验中这是没有问题的。不过在某个位置更新数据意味着随机输入输出流(IO),这会影响性能。

替代的办法是将更新值放置在文件的末尾,在读取值时对过期的数据进行处理。

我们第一次做出权衡,将“日记”或者“日志”放在文件末尾,就能保证有序获取进而提高性能。另外倘若某处需要更新数据,可以实现每秒300次左右的读取,前提是更新数据刷入底层介质中。

实际上完整的读取文件是很慢的,获取十亿字节(GB)数据,最好的硬盘也需要花费数秒,这是一个数据库全表扫描所花费的时间。

我们时常只需要一些特定的数据,比如名为“bob”的客户,这时扫描整个文件就不妥当,我们需要一个索引。

我们可用许多不同类型的索引,最简单的一种是固定长度的有序数组,比如本例中的客户名,和对应的偏移量一起存放在一个堆文件中。有序数组可以进行二进制搜索查找。同样,我们可以用树结构、位图索引、哈希索引、字典索引等。这里是一个树的结构图。

索引就像是在数据中添加了一个总览结构,值是有序排放的,这样我们就能快速获取我们想要读取的数据。但总览结构有个问题,数据进来时需要随机写。因此理想的、写优化仅仅追加文件;考虑到写会打散文件系统,这会使一切变慢。

如果你将许多索引放入一个数据库表中,那你一定熟悉这个问题。假定我们使用的机械盘,用这种方式维护某个索引的硬盘完整性,速度大约慢1000倍。

幸运的是,这里有几种解决方案。这里我们讨论三种,它们都是一些极端地例子。在现实世界中,远没有这么复杂,但在考虑海量存储时这些概念会特别有用。

译者加:

  • 第一种内存映射文件
  • 第二种较小的索引集合,采用元索引或者布隆过滤算法(Bloom Filter)做一些优化
  • 第三种简单匹配算法(brute force)又叫面向列(Column Oriented)

第一种方案是将索引放入主内存,随机写问题分隔到随机存储存储器(RAM),堆文件依旧在硬盘中。

这是一种简单但行之有效的方案,可以解决我们随机写的问题。这种方式在许多数据库中已得到应用,比如MongoDB、Cassandra、Riak、以及其他采用此优化类型的数据库,它们常常用到内存映射文件。

译者注:内存映射文件是虚拟内存单个分段,可以与文件或者类文件资源的某部分建立直接字节对字节的关联,即文件中的数据存放位置在内存中有对应的地址空间,这时对文件的读写可以直接用指针来做,而不需要read/write函数,处理大文件时可以显著提高输入输出流(IO)性能。

倘若数据量远超主内存,这种策略就失效了。特别是存在大量小的对象时,问题特别显眼;索引增长很大,最后存储越过了可用主内存的容量。多数情况下,这样做是没有问题的,但如果存在海量数据,这样做就会成为一种负担。

一种流行的方式抛开单个的“总览”索引,转而采用相对较小的索引集合。

这是一个简单的理念:数据进来,我们批量地将其写入主内存。一旦内存数据足够多,比如达到MB,我们就对它们进行排序,而后将它们作为单个小的索引写入硬盘中。最后得到的是一个小的、由不变索引文件组成的年表。

那么这样做的好处是?这些不变的文件集合被有序地流化处理,这样就能快速地写,最重要的是无需将整个索引加载入内存中。真棒!

当然它也有一个缺点,当读操作时需要询问非常多的小索引。我们将随机IO(RandomIO)写问题变为读问题。不过这确实一个很好的权衡策略,而且随机读比随机写更容易优化。

存储一个小的元索引(meta-index)在内存中或者采用布隆过滤算法(Bloom Filter),提供一种低内存方式,评估单个索引文件在读操作中是否需要被询问。即使保持快速地、有序化写操作,这种方式的读操作性能几乎可以和单个总览索引相媲美。

实际开发中,偶尔也需要清理孤子更新,但它有序读和写确实不错。

我们创建的这个结构称作日志结构合并树(Log Structured Merge Tree),这种存储方式在大数据工具中应用较大,如HBase、Cassandra、谷歌的BigTable等,它能用相对较小的内存开销平衡写、读性能。

将索引存储在内存中,或者利用诸如日志结构合并树(Log Structured Merge Tree)这样的写优化索引结构,绕开“随机写惩罚”(random-write penalty)。这是第三种方案为纯粹的简单匹配算法(Pure brute force)。

回到开始的文件例子,完整地读取它。如何处理文件中的数据,可以有许多选择。简单匹配算法(brute force)通过列而非行来存储数据,这种方法叫做面向列。

需要注意的是真实的列存储及其遵循的大表模式(Big Table pattern)之间存在一种不好的命名术语冲突。尽管它们有一些相似的地方,事实上它们是不同的,所以将它们视为不同的事情是一件明智的。

面向列是一种简单的理念,和行存储数据不同,通过列分割每一行,将数据追加到单个文件末尾。接着在每个单独的文件中存储每一列,一旦需要只需读取需要的列。

这样可以确保文件的含有相同的序列,即每个列文件的第N行含有相同的地址或者偏移量。这个很重要,在某一时刻读取多列,来服务一个单一的查询。意味着“连接(joining)”列速度飞快,倘若所有的列含有相同的序列,我们就能在一个紧凑的循环中这么做,此循环有很好缓存和CPU利用率。许多实现大量使用向量( vectorisation)进一步优化简单连接和过滤操作吞吐量。

写操作可以提高只在文件末尾追加( being append-only)性能。不利的地方是很多文件需要更新时,文件的每个列需要单独写入数据库。最常见的解决方案是采用类似日志结构合并(LSM)方式,进行批量化的写操作。许多列类型的数据库通过给表添加一个完整的序列来提升读的性能。

通过列分割数据可以极大地减少从硬盘中读取的数据量,只要查询操作在所有的列的子集中。

除此之外,单独列中的数据通常可以很好的压缩。可以利用列数据类型优势去压缩,特别是在我们熟悉列的数据类型时。这意味着我们能利用有效的、低成本的编码方式,比如行程长度编码、delta、位组合(bit-packed)等。对一些编码来说,谓词可以直接用来做压缩流。

一种简单匹配算法(brute force)特别适合大规模扫描操作,诸如平均值、最大值、最小值、分组等聚类函数就是这方面的典型。

这和先前提到的“堆文件和索引(‘heap file & index)”方式不同,很好的理解这一点可以问自己,诸如此类的列方式和每一个字段带有索引的“堆和索引”方式有什么不同?

问题的关键是索引文件序列:多路查找树(Btree)等会依据检索的字段排序,两次检索的数据连接一端涉及流操作,另一端第二个索引位置进行检索随机读取。平衡树总体上说效率低于包含两个相同序列索引列连接,我们再一次提高了序列化访问。

译者注:结论是平衡树连接性能不如两个相同序列索引列连接

我们都想将最好的技术作为数据平台控件,提升其中的某种核心功能,胜任一组特定的负载。

将索引存于内存而非堆文件为丛多非关系型数据库(NoSQL)所喜爱,比如Riak、Couchbase或者Mongodb,甚至一些关系型数据库,这种简单的模型效果不错。

设计用来处理海量数据集的工具乐意采用LSM方式,这样可以快速获取数据,得到基于硬盘结构 读一样好的性能。HBase、 Cassandra、RocksDB、 LevelDB 甚至Mongo现在也支持这种方式。

每个文件的列(Column-per-file)引擎常用于数据库大规模并行处理(MPP),比如Redshift或者Vertica,以及Hadoop stack中的Parquet。这些数据引擎最大的问题是需要大的遍历,聚合是这些工具最重要的特质。

诸如卡夫卡(Kafka)采用一个简单的、基于硬件的高效消息规范。消息可以简单地追加到文件的末尾,或者从预定的偏移量处读取。可以从某个偏移量读取消息,来来回回,你可以从上次结束的偏移量处读取。看得出是很不错的有序输入输出(IO)。

这和多数面向消息的中间件不同,JMS(Java消息服务)和AMQP(高级消息队列协议)说明文档需要额外的索引,来管理选择器和会话消息。这意味着它们结束某个行为的方式更像数据库,而非某个文件。著名的论述是1995年Jim Gray发表的队列就是数据库(Queue’s are Databases).

可见所有的方式都需要这样那样的权衡,作为一种分布式手段,使事情变得简单、硬件更加用户友好。

我们分析了存储引擎的一些核心方法,其实只是做了一些简要说明,现实世界这些是要复杂的多,不过概念确实是很有用的。分布式数据平台不仅仅是一个存储引擎,还需要考虑并行。

对于横跨多台计算机的分布式数据我们需要考虑两个核心点,分区(partition)和复制(replication)。分区有时指的是分库分表(sharding),在随机读取和简单匹配工作负载(brute force workloads)表现不俗。

如果是基于哈希的分区模型,借助哈希函数,数据就能均摊到一组机器上(译者注:理想的结果是这样的)。同哈希表工作方式相似,每个桶(bucket)盛放某个机器节点。

这样通过哈希函数,直接访问包含此数据的机器读取来数据。这是一种很经典的分布式模式,也是唯一一种随着客户端请求增加呈现线性分布的模式(译者注:简单点说就是均摊)。请求隔离到单台计算机上,由集群中的单台计算机为其服务。

利用分区提供并行批量计算,比如聚合函数或者诸如聚众或者机器学习的复杂算法。最大的不同是所有的计算机在同一时刻采用广播的方式,在很短的时间采用分治的策略解决大规模计算问题。

批量处理系统很好地处理大规模问题,但在执行过程中少有并发,容易耗尽集群资源。

两个极端且特别简单的方式:一端直接访问,另一端分治地进行广播。需要注意的是终端之间的中间地带,最好的例子就是非关系型数据库(NoSQL)中跨越多台计算机的二级索引。

二级索引有别于主键索引,这就意味着数据分区不再借助索引中的值。不再使用哈希函数直接分发,而是广播请求给所有的计算机。这会制约并发,任何一个节点与每一个请求都有关。

也是这个原因许多键值存储不愿采用二级索引,即使它的应用很广泛,Hbase和Voldemort就是如此。不过诸如MongoDb、Cassandra、Riak等数据库采用二级索引,不管咋说二级索引还是蛮有用的。但理解它们在整个系统并发的影响还是很重要的。

复制解决并发瓶颈,或许你熟悉备份,不论是异步到从服务器,还是复制到诸如Mongo或者Cassandra这样的NoSQL存储中。

实际上备份是不可见的(仅仅用于恢复)、只读(增加并发量)、或者读写(增加网络分区下的可用性),选择哪种方式需要从系统的一致性出发做出权衡。这是CAP(Consistency、Availability、Partition-Tolerance)理论的简单应用,当然CAP理论远非我们想象中的那么简单。

译者注:网络分区( network partitions)指某个网络设备出错导致网络分离,比如某个数据库挂掉。

权衡一致性给我们带来一个重要的问题,什么时候需要保证数据的一致性?

一致性的代价是昂贵,在数据库的世界里,原子性由线性化(linearisabilty)做保障,这样可以确保所有的操作有序排列.但代价也是昂贵的,实际上这完全是被禁止的,许多数据库并不将此作为一个独立(isolation)执行单元。鉴于此,很少将此设为默认值。

简而言之,你想分布式写的系统保持强一致性,系统会变慢。

注意一致性这个术语有两个应用场景,在原子性和CAP中,当然其意思是不同的。我通常采用CAP中的定义,对所有的节点而言数据在某一时刻是相同的。

解决一致性问题的方法其实很简单,就是避免它。如果无法避免,隔离它为其分配尽可能少的写操作和计算机资源。

避免一致性问题一般不难,特别是数据为不变的事实流时,网络日志集合就是一个很好的例子。无需关注一致性,因为这些日志作为事实是不会改变的。

需要一致性的用例,比如转账、使用优惠码这种非交换行为。

当然从传统的眼光看一些事情需要一致性,但实际上却也未必。比如一个行动从一个可变状态变成一个新的相关事实集合,就可以避免这种变化状态。通常是直接对新字段进行更新,考虑到标记一个事务存在潜在的欺诈,我们可以简单地利用某个事实流和原始的事务进行关联。

译者:好观点

在数据平台中移除所有一致性需求、或者隔离它都是很有用的。一种隔离方式是利用单个写原则,涉及几个方面,比如Datomic;另一种方式是拆分可变的和非可变的来隔离一致性需求。

诸如Bloom/CALM扩展了这些理念,支持默认状态下的无序概念,除非需要才做排序。因此我们有必要做一些基本的权衡,那我们如何利用这些特性去建立一个数据平台?

一个典型的应用架构或许应该是这样的:有一组处理将数据写入某个数据库,然后将其读出,对于许多简单的工作负载这是没有问题的,许多成功的应用都是基于此模式。但随着吞吐量的增加,此模式越来越难以适用;在应用领域这个问题或许可以通过消息传递、演员(actors)、负载均衡加以解决。

另外一个问题是这种方式将数据库作为一个黑盒,数据库是一个透明的软件。它们提供了海量的特征,但也提供了极少的原子拆分的机制。这样做有很多好处,默认状态下是安全的;但保护过度地扼杀我们的需求进而限制系统的分布式,这就很烦人。

命令查询职责分离(CQRS Command Query Responsibility Segregation)可以简单地解决此问题。

译注:

  1. 实现一Druid
  2. 实现二操作分析桥(Operational/Analytic Bridge)
  3. 实现三批量管道
  4. 实现四拉姆达框架(Lambda Architecture)
  5. 实现五卡帕(Kappa)框架又叫流数据平台

想法其实很简单,分离读写工作负载:最佳写入状态时写入,最切贴的例子比如某个简单日志文件;最佳读取状态时读取。有多种实现方式,比如用于关系型数据库的Goldengate工具、内部复制集成的诸如MongoDB的Replica Sets这样的产品。

许多数据库底层的行为就是这样,Druid是一个不错的例子,它是一个开源的、分布式、时序化、列式分析引擎。列式存储表现不俗,特别是大规模数据录入,数据必须分散到许多文件中。为了得到更好的写性能,Druid存储近期的新数据到某个最佳写入状态中,然后逐渐转移到最佳读取存储状态。

一旦查询Druid,请求就会同时派发到最佳写和最佳读控件中,对结果进行组合(移除冗余),返回给用户。Druid借助时间标记每条记录来进行排序。

诸如此类的组合方式提供了单个抽象下的CQRS好处。

另一种相似的方式是操作分析桥(Operational/Analytic Bridge),利用单个事件流拆分最佳读以及最佳写视图。流处在一种不断变化的状态,因此异步视图可以在随后的日子里被重写和增强。

前端提供了同步读和写,这么做即可以简单快速地读取已写入的数据,又可以支持复杂的原子事务。

后端采用异步、不变状态的优势来提高性能,比如借助复制、反范式化、甚至完全不同的存储引擎扩展线下处理。前后端之间的消息桥连方便应用通过平台去监听数据流。这种模型很适合中等规模的部署,可变视图至少存在一部分、不可避免的需求。

设计不变的状态,以便容易地去支持大规模数据集和更加复杂的分析。Hadoop栈中独一无二的实现——批量管道,就是一个典型的例子。

Hadoop栈最精彩的地方就是其丛多的工具,不管是快速读写访问、还是廉价地存储、抑或批量处理、高吞吐消息、或者提取、处理、分析数据,hadoop生态体系应有尽有。

批量管道从多种资源中获取数据,将其放入HDFS,接着对其进行处理,进而提供一个原始数据持续优化的版本。

数据可能得到富集、清理、反范式化、聚集、移到一个诸如Parquet的最佳读模式,或者加载进服务器层或者数据集市,处理之后的数据可以被检索和处理。

此框架适用于不变数据、以及对数据进行大规模获取和处理,比如100太字节(TBs)。此框架处理过程很缓慢,以小时为单位。

批量管道的问题是通常我们不想等几个小时去获取一个结果。常见的做法是添加一个流层,有时又叫拉姆达框架(Lambda Architecture)。

拉姆达框架保留了批量管线,不过增加了快速流层实现迂回,就像在忙乱的小镇架了一个支路,流层采用诸如Storm、Samza流处理工具。

拉姆达框架核心是我们最乐意快速粗略作答的,但我想在最后做一个精确的回答。

流层绕过了批量层,提供了最佳回答,它的核心就在流视图中。这些会写入一个服务器层。稍好批量管道计算出精确的数据并覆盖之前的值。

用响应来平衡精度是个不错的做法,两个分支在流和批量处理层都有编码,这种模式的一些实现是有问题的。解决办法,一是将此逻辑简单抽象到一个可复用的通用库中,比如处理都写入了诸如Python、R语言这样的外源库中。二是诸如Spark这样的系统同时提供了流和批量处理功能,当然spark中的流只是少量的批处理。

因此这种模式适合比如100TB的海量数据平台,将流和已存、富集的、批量分析函数结合起来。

另外一种解决慢数据管道的方式,称之为卡帕(Kappa)框架。起初我以为这个架构名称不对,现在我不太确定。不管它是什么,我叫它流数据平台,其实这个已经有人这么叫了。

流数据平台相对批量模式更有优势:与将数据存储在HDFS中划分给新的批量任务不同,数据分散存储在消息系统或者诸如kafka日志中。批处理就变成了记录系统,数据流经过实时处理生成三层结构:视图、索引、服务或者数据集市。

与拉姆达(lambda)框架的流层相似,不一样的是没有批处理层。显然这就要求消息层能够存储、供应海量数据,并且具有强大有效的流处理器来处理此过程。

天下没有免费的午餐,问题很棘手,流数据平台运行速度并没有同等批量处理系统快多少。但将默认的方法“存储和处理”切换为“流和处理”,可以极大地提高快速获取结果的可能性。

流数据平台方式还可以用来解决“应用集成”问题,应用集成这个棘手的问题困惑Informatica、Tibco和Oracle等大的供应商好多年了。对许多数据库而言是有益的,但不是一种变革性方案。应用集成至今停留在找寻切实可行方案的话题上。

流数据平台提供了一个潜在的解决方案:利用操作分析桥的丛多优势—多种异步存储格式以及重新创建视图的能力—但这会增加已有资源中一致性需求:

系统记录变为日志,易于增强数据的不变性。诸如Kafka等产品内部保留了足够的数据量和吞吐量,将其作为历史记录来用。这就意味着回复是一个重演、重新生成状态的处理过程,而非常态化地检验。

相似的方式很在就有应用,早于最新出现的数据湖或者Goldengate等工具,后者将数据放入企业级数据仓库。复制层缺乏吞吐量和管理复杂的schema变化使此方法大打折扣。看似最后第一个问题已经解决,但作为最后一个问题,还没有定论。

~

回到局部性,读和写按序寻址,是控件内部最需要权衡的部分。我们观看了如何拓展这些控件,提高了分库分表和复制最基本的性能。重新审视一致性将其作为一个问题,在构建平台时隔离它。

不过数据平台本身需要用单一、全局的方式来平衡这些控件达到最佳状态。不断重建,从最佳写状态迁移到最佳读状态,从一致性约束转移到流、异步、不变状态的开放地带。

需要记住几件事,一是schema,二是时间、分布式、异步系统风险。但这些问题都是可控的,前提是你认真对待。未来大数据领域可能会出现这样一些新的工具、革新,逐渐掺入到平台中,解决过去和现在更多的问题。
译者注:schema 指数据库完整性约束。

译者注:schema 指数据库完整性约束。




导读: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在其所在文件中的偏移量就可以表示了。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值