Design Data-Intensive Applications 读书笔记十七 第六章:分区和第二索引

分区和第二索引

我们目前讨论的分区方法都是基于键值数据结构,通过key确定数据存储的分区,通过key将读取写入请求转发至对应分区。

如果在分区方法中包含第二索引,事情会变得更复杂。第二索引通常不是用来确定记录的,而是用来计算特定值的发生频次:如用户123的所有动作,所有红色汽车等。第二索引在关系型和文档数据库中都很常见。很多键值存储(HBase和Voldemort)避免第二索引,因为实现起来很复杂,但是某些(Riak)开始添加第二索引,因为它很有用。最终,第二索引是Solr和Elasticsearch这些搜索服务存在的理由。

使用第二索引的问题是,它不直接参与分区。主要有两个方法来分区:文档分区和术语分区。

利用文档第二索引分区

如图6-4例子,每个列表项都有独立ID,称为文档ID,数据库可以使用文档ID分区。

你想让用户可以根据颜色和厂商查询汽车,可以用color和make作为第二索引(文档数据库中是字段,关系型数据库中是列)。如果你声明了这个索引,数据库可以自动建立索引,例如,添加红色汽车的时候,数据库会自动将其添加进索引color:red对应的文档id列表。

这种方法,每个分区都是完全独立的:每个分区维护自己的第二索引和文档。分区不会关心其他分区的数据。当你需要作出增删和更新的时候,只需要处理特定分区上特定id的数据就行。因此,文档分区索引被称为局部索引(后面会讲到全局索引)。

但是从文档分区索引中读取需要注意:如果你对文档id不做出特别操作,特定颜色或者特定厂商的汽车不会在相同分区上,如图6-4

查询这种分区数据库被称为 分散/收集 ,这使得使用第二索引查询消耗很大。即便是同时查询多个分区,分散/收集也会放大延迟。 尽管如此,它还是被广泛使用: Mon‐goDB, Riak , Cassandra , Elasticsearch , SolrCloud , 和VoltDB,都使用了文档分区第二索引。大多数数据库供应商建议自己设计第二索引的模式来让第二索引查询能集中在单独的分区上,但是这一般是不可能的,特别是在一个查询中使用多个第二索引查询的时候。

术语第二索引分区

上节使用了文档建立第二索引来进行分区(局部分区),我们可以构建一个全局索引覆盖所有分区数据。但是我们不能只在一个节点上存储全局索引,因为这很可能会成为瓶颈,违背了分区的初衷。所以全局索引也要被分区。

如图6-5,所有红色汽车属于color:red ,同时这个索引是被分区的,可以将以a到r开头的颜色放在分区1,s到z的开头的颜色放在分区2。可以用同样的方法对make索引分区。

我们称这种方法为术语分区,因为使用术语决定索引的分区。例子中术语是color:red。“术语”(term)来自于全文索引,在那里,术语指代出现在文档中的单词。我们可以用术语自身对索引分区,或者用术语的哈希值。部分情况下用术语的区间来分区很有用(比如数字属性),使用哈希值分区能使负载更加均衡。

全局索引的好处就是读取快,不需要进行局部索引的分散/搜集 工作,客户端只需要将请求发送至特定分区。但是缺点是,写入时很复杂,因为文档的每个术语都可能在不同分区,写入一个文档会影响到多个分区的索引。

理想情况下,索引实时更新,每写入一个文档,都会反映至索引。但是使用术语分区索引,写入时就需要一个分布式的事务,并不是所有的数据库都支持。

实际操作中,更新全局索引通常是异步进行,也就是说,写入后可能需要过段时间才能看到更新。 Amazon DynamoDB声明能在一般环境下在1秒内更新索引,但是在设备故障的情况下,会有更长的延迟。其他使用术语分区索引有 Riak的搜索功能和Oralce的数据仓库,他们允许选择局部索引还是全局索引。

 

分区再平衡

随着时间推移,系统会面对如下变化:

1、查询量上升,需要增加CPU来处理工作负载。

2、数据量上升,需要增加内存和磁盘来存储数据。

3、机器故障,其他机器需要接管它的职责。

这些改变都需要能将数据和请求从一个节点转移至另一个节点。这个在集群中将负载从一个节点转移至另外一个节点的过程称为再平衡。

无论使用哪种分区方式,再平衡过程需要满足下面的最低要求:

1、再平衡后,节点间的工作负载(数据量,读写量)应该趋于一致。

2、再平衡过程中,系统能继续接收读写。

3、不应该有除必要的数据外的更多的数据被转移,否则会拖慢再平衡进度和增加网络和I/O负载。

 

再平衡策略

有几个不同的策略

 

不应该做什么:哈希值取余(hash mod N)

但是用哈希键来分区时,我们使用了键的范围来分区。那么为什么不直接取余数来分区?比如:hash(key) 对10取余,会得到0-9中的数字,如果我们有10个节点,编号为0-9,这种方法不是更简单?

问题就是,如果节点数N发生变化,那么绝大多数key需要从一个节点转移到另一个节点。例如hash(key)=123456。如果你一开始有10个节点,key会被存储在节点6上(123456 % 10 =6),如果增长到11个节点,key需要移动到节点3(123456 % 11 = 3) ,如果增长到12个节点,需要移动到节点0。如此平凡的移动让再平衡过程开销很大。

 

固定数量分区

有一个最简单的方法:固定分区的数量(大于节点数),每个节点上分配数个分区。例如数据库集群中有10个节点,可以划分1000个分区,每个节点分配100个分区。

现在集群中新增加了一个节点,新节点会从已有的节点上“偷取”少量节点,直到所有节点上分区数趋于平衡。过程如图6-6,如果集群中移除了节点,这个过程会反过来。

原则上,你可以根据硬件条件来分配每个节点上的分区数,如果某个节点的配置高,可以往该节点分配更多的分区,让其承担更多的工作。

使用这种再平衡策略的有 Riak , Elasticsearch , Couchbase ,和 Voldemort。

在配置中,分区数一般是在数据库建立时设置的,后续不可变的。尽管原则上分区可以合并或者切割,但是分区数固定,操作起来很方便,所以很多固定分区数据库不支持分区切割。因此设置分区数时,一般参照你可能拥有的最多的节点数,来因对后续的数据增长。但是每个分区的管理也需要成本,分区数太多反而会降低生产效率。

如果数据库的总量变化很大(开始数据量小,后续会增长至很大),那么选择分区数是个难题,因为每个分区的数据量与总数据量间的倍数是固定的。如果分区数少了,那么每个分区的数据量会变得很大。如果分区数很大,那么会带来额外的管理成本。理想的结果的分区数“刚刚好”。这在数据量变化很大的情况下很难达到。

 

动态分区

对于使用key范围分区的数据库来说,固定分区数,固定分区边界可能很不方便。如果划分的分区边界导致数据集中在一个分区,其他分区都是空的,那么再手动调整分区边界会很麻烦。

因此,使用key值范围分区的数据库,如 HBase和RethinkDB,使用动态分区。如果分区的大小达到一个阈值( HBase中默认为10GB),那么它会被近乎平均地被划分为两半。相反的,如果一个分区上大量的数据被删除了,数据量低于一个阈值,那么它会被合并到邻近的分区。这个过程类似于B-Tree。

每个分区被分配至一个节点,每个节点上会有多个分区。一个大的分区被分割后,其中的一半会被传送至其他节点,来平衡工作负载。在HBase中,一般是HDFS来传输分区文件。

动态分区的好处就是可以适应数据量的增长。如果数据量小,分区数就小,管理开销就很小。如果数据量很大,分区数也可以通过配置来限制。

但是,有个问题就是,开始时数据库是空的,那么就只有一个分区,因为没有前置信息来判断哪儿是分区边界。那么在数据量很小时,不足以分区时,所有的写入都会发送至一个节点,而其他节点一直闲置。为应对这种问题 HBase 和MongoDB允许初始设置分区边界(称为预划分)。这需要你知道key的分布的大致情况。

动态分区可以用于key范围分区数据和哈希分区数据。

 

按照节点数设置分区数

动态分区中,节点数与总数据量成比例,每个分区的数据量在设定的范围内。在固定数量分区中,每个分区的数据量与总数据量成比例。两种方法都没有考虑到节点数。

第三种选择就是 Cassandra 和 Ketama使用的,按照节点数量等比例设置分区数,就是你每个节点上设置固定数量的分区。这种情况下,如果节点数不变,每个分区的大小随着数据量的增长而等比例增长;如果添加新的节点,会随机选取部分分区进行分割,其中一半会被新的节点接管,这样增加节点数后,每个分区的大小就会下降。这样,更多的数据用更多的节点存储,每个分区的大小相对稳定。

添加新节点,分割分区时随机选取节点进行的,这可能产生非公平操作,但是在大量的分割后,整体上会趋于均衡,新节点会承担相对公平的负载。

随机选取分区划分的边界需要分区是使用哈希来划分的这个条件(如此,就可以在一个范围内选取数字来计算对应的边界值)。因此这种方法接近“一致性哈希”的原始定义。

 

请求路由

我们已经在多个节点上运行多个分区在。现在还有一个问题:客户端怎么知道要向哪个节点发送请求?再平衡后节点上的分区会发生变化,需要面对一个问题:如果想读取key:“foo”对应的值,要请求哪个地址和端口?

这个类型的问题被称为“服务发现”(service discovery)。不仅出现在数据库中,任何通过网络连接的应用都会面对这个问题,特别是那些运行在多个节点上,追求高可用性的应用。不少公司有自用的服务发现软件,部分已经开源了。

有一些方法解决这些问题:

1、客户端能连接任意节点。如果节点刚好有客户端需要的数据,就直接返回数据;否则将请求转发至合适的节点,然后接收返回的数据,转发至客户端。

2、将所有的请求发送至一个路由层,节点负责处理请求然后回复请求;路由层自己不处理任何请求,它只是作为发现分区的负载均衡器。

3、需要客户端发现节点和节点上的分区,这种情况下,客户端直接连接合适的节点。

所有的方法中,有一个核心问题,有路由功能的组件(节点,路由层,客户端)如何得知节点上分区的改变?

这是个难题,因为这需要所有的组件都协商一致,否则请求就会被发送至错误的节点,没法正确地处理。有一些在分布式系统中达成一致的协议,但是它们很难实现。

很多分布式系统依赖如Zookeeper的独立的框架来记录集群的元数据,如图6-8。每个节点注册进Zookeeper,Zookeeper维护分区与节点的对应关系。其他组件,如路由层或者发现分区的客户端能够从Zookeeper订阅信息。当分区改变,或者增加删除节点时,Zookeeper会通知路由层更新路由信息。

例如LinkedIn的 Espresso使用了Helix(依赖Zookeeper)来管理集群,实现了如图6-8的路由层。 HBase, SolrCloud,和 Kafka也使用了Zookeeper。 MongoDB有一个类似的架构,但是它依赖自己的配置服务和mongos进程来实现路由层。

Cassandra和Riak用了不同的方法:在节点上使用gossip协议来探测集群状态的变化。请求可以发送至任意一个节点,节点会转发至合适的节点(图6-7的方法1)。这种模型增加了复杂度,但是不需要依赖类似Zookeeper的外部框架。

Couchbase没有自动再平衡,这简化了操作。它使用被称为moxi的路由层,来检测集群节点的变化。

当使用路由层或者发送请求至随机节点方法时,客户端依然需要找到地址和端口,这通常没有分区分配至节点的变化速度快,所以使用DNS就可以了。

 

平行执行查询

目前我们关注的都是对于单一key的查询(在使用文档分区第二索引时增加了分散/收集查询)。这一层,绝大多数NoSQL分布式数据库都支持。

但是,支持大量并行查询( massively parallel processing,MPP)的关系型数据库通常用于分析,支持更加复杂的查询。典型的数据库查询支持join,filtering, grouping, 和aggregation 操作。MPP查询优化器将复杂查询拆分成多个执行阶段和分区,其中很多可以在集群中不同节点上并行执行。需要扫描大部分数据库的查询尤其受益于并行执行。

快速并行执行是个专业问题,对于商业分析很重要。我们会在10章讨论。

 

总结

这章我们讨论了怎么讲大数据集拆分成小的子集。当你再一台机器上存储和处理数据很困难时,分区就是必要的。

分区的目标就是将数据量和负载均匀的分散到多态机器,避免热点(额外承受更高负载的节点)。这需要选择适于数据的分区方法,在添加和移除节点时需要再平衡。

我们讨论了两个主要的分区方法:

1、key范围分区,key被排序,然后被划分为不同的连续区间,一个分区对应一个连续区间。排序利于范围查询,但是如果应用经常访问相邻的key有造成热点的风险。使用这个方法,分区一般使用动态再平衡方法,分区时将大的分区分成两个字分区。

2、Hash分区。对每个key使用哈希函数,每个分区对于一个范围的哈希值。这个方法破坏了key的有序性,很难高效进行范围查询。但是能均衡的分配负载。

使用hash分区时,最好提前固定每个节点上分区数量,然后当添加或者移除节点时,移动整个分区。也可以使用动态分区。

也可以使用混合分区,用key的一部分确定分区,另一部分用于排序。

我们也讨论了分区和第二索引。第二索引也需要被分区,有两种方法:

1、文档分区索引(局部索引)。第二索引以类似主键的方式在所在的分区内存储键和值。这意味着写入时只有一个分区需要更新,读取时需要从多个分区读取。

2、术语分区索引(全局索引)。第二索引与分区分开了。一个完整的第二索引可能包含主键对应的所有分区上的值。当文档写入时,多个分区的第二索引需要更新,但是读取时只需要从一个分区读取。

最后我们讨论了转发请求至合适分区上的技术,从简单的负载平衡至复杂并行查询引擎都需要的技术。

设计上,每个分区都是独立的,因此分区后的数据库可以扩展是多个机器。但是,很难提出写入多个分区的技术:如果一个分区成功了,其它的失败呢?下一章会讨论这个问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值