海量存储系列之十一、十二、十三

ps : 最近霸神推了一把,粉丝增加不少,顿时亚历山大。。还是希望大家用轻松一点的心态来看待我的这些科普文。
如果想精细推敲,欢迎在后面留言,我一定会与您讨论与分享。

上一期我们主要在介绍hash相关的切分方式,那么这次我们来看一下有序结构的切分

 

有序结构的拆分,目前主要就是使用树或类似树的结构进行拆分,这里主要就是指HBase和MongoDB.

 

使用树结构切分,带来的好处就如hbase和mongoDB的宣传标语一样,可以无缝的实现自由扩展。但反过来,带来的问题其实也不少,下面我们一起来看一看吧。

 

首先复习B树知识http://qing.weibo.com/1765738567/693f0847330008ii.html

 

在B树中,最关键的处理逻辑是如果单个节点数据满的时候,应该进行节点分裂和节点合并。

 

那么,其实在HBase中也有类似这样的过程。

 

对于巨大量的数据来说,整个树的Branch节点都有可能超过单机的内存大小上限,甚至超过单机的硬盘大小上限。

这时候就需要把BTree进行拆分,这种拆分的最标准实现映射,就是HBase.

(图片版权方在:http://blog.csdn.net/HEYUTAO007/article/details/5766951)

 

 

 

看这个图可能会比较晕,没关系,听我分析之。

首先,整个Hbase就是为了解决一个B树非常巨大,以至于单机无法承载其branch and root节点之后,使用分布式存储的方式来提升整个树的容灾量的一种尝试。

 

抽象的来看,每一个HRegion都是一个Btree的Node,这个Node会挂在在某个Region server上面,RangeServer内可以存放多个Hregion ,其实就是Btree的branch节点了,但因为Branch也很多,以至于单机无法存放所有branch节点,因此就还需要一层结构来处理这个问题。这就是HMaster 。

上图

 

 

 

虽然可能有点抽象,不过本质来说就是这样一个东西。

当然,细节有点变化:

HMaster ,在上面的图中是单个点,实际的实现是一个btree,三层结构的。

 

因为HMaster的数据不经常发生变化,同时,每次请求都去访问HMaster,那么HMaster所承担的读写压力就过大了。所以,HBase增加了一个客户端的Cache.来存HMaster中的这几层BTree.

 

于是,可怜的Hbase又得考虑如何能够将HClient和HMaster中的数据进行同步的问题。

 

针对这个问题,Hbase提出的解决思路是,既然变动不大,那就允许他错吧,只要咱知道出错了,改正了就行了。

也即,允许HClient根据错误的Btree选择到错误的Region Server,但一旦发现自己所选的数据在那台Region server上无法找到,则立刻重新更新自己的HMaster表。已达到同步。

 

 

这基本上就是BTree的分布式实践中做的最好的HBase的一些过程了。

 

然后然后,私货时间开始: )

 

借助HDFS,Hbase几乎实现了无限的扩展性,但整体结构过于复杂和庞大了,最终,他只解决了一个K-V写入的问题,同时又希望对所有用户屏蔽底层的所有数据节点的具体位置。

这套思路有其优势之处(也就是Btree的优势):

1.      纯粹log场景,btree管理起来非常方便

2.      支持范围查询

 

但可能的劣势其实也很多

1.      结构繁杂,在各种角色中进行数据同步,这件事本身听起来就已经很吓人了。然而,最终,他只是解决了一个按照K找到V的过程。。Hash一样可以做到

2.      Region server ,维护难度较高,核心数据结构点,虽然该机器可以认为是个接近无状态的机器,但如果想拿一台空机器恢复到可以承担某个Region server的指责,这个过程需要的时间会很长,导致的问题就是,系统的一部分数据不可用,甚至发生雪崩。

3.      BTree 在不断追加append的时候,其实是有热点的,目前没有很好地办法能在按照时间序或按照自增id序列的时候保证所有的数据存储机都能够比较均衡的写入数据。会存在热点问题,这个问题的源头在BTree需要有序并连续,这意味着连续的数据只会被写在一个region块内,这个问题在单机btree其实也是存在的,但有raid技术,以及有二级索引,所以问题没有那么明显。(感谢@bluedavy)

 

综上,HBase其实从一开始是一个面向后端处理的数据引擎,在数据一致性上是可以期待的,但对于线上系统来说,他违背了重要的一个原则:简单。所以我“个人”对这一点持保留态度。

 

不过,这么多大牛在努力的经营HBase这个产品,那么我也乐观其成,毕竟能把这么复杂的东西整的能在这么多台机器上用,也是个巨大成就了。

 

MongoDB其实也是在学Hbase的这种有序的BTree结构,不过它的实现就简单的多了。

就是把数据拆分成一段一段的数据,用一个公用的配置角色存储这段数据所在的分片。查询时进行二分查找找到。

思路类似。

 

从角色来看

他的规则引擎实现就是个有序数据的实现,可以认为是个两层有序结构查找.第一层决定数据的具体机器(Mongos+config server),第二层决定数据在该机的具体位置MongoServer。

 

 

好了,画个图用了20分钟,今天的介绍就到这里,下期我们来探讨分布式场景下一个必要的过程。数据的迁移方式讨论。

http://qing.weibo.com/1765738567/693f084733000bxj.html 下一篇


上一篇

目前,团队blog和sina 轻博客的发布进度已经完全相同,后续会全部

时间隔了比较久了,因为最近在过年临近,所以都在准备这方面的事情。这里提前祝大家新年快乐。

然后还是回到我们的正题儿吧:)

本章,我们主要来讨论数据的管理和扩容中最重要的一个部分,数据迁移。

数据迁移是数据运维中最为重要的一个部分,在前面的文章中已经提到过,作为有状态的数据节点,在互联网行业的主要追求就是,无限的水平扩展能力,这种水平扩展,主要用于解决两类问题,一类是磁盘空间不足的问题,一类是性能不足的问题。

为了达到这种能力,一般来说主要也就是这样一个思路,尽可能的让数据不动,只通过规则变动的方式来完成扩容,如果这种方式无法满足要求,那么再通过移动数据的方式,来满足其他的一些需求。

下面来进行下分析。

只通过变动规则的方式扩容,举个最简单的例子,就是一组按照时间或自增id的数据。那么最简单的切分方式,就是按照时间或id的范围,将一组数据直接映射到某个具体的机器上,类似

if(gmt> = 2010 and gmt < 2011)

returndataNode1;

elseif( gmt >= 2011 and gmt < 2012)

returndataNode2;

elseif(gmt >= 2012 and gmt < 20121223)

returndataNode3;

使用这种方式的好处,显而易见,就是不用动数据,方法简单。

但带来的坏处也明显,就是不移动数据,那么如果一组数据已经成为热点,那么永远也没有机会将热点数据分开到不同的机器里用以减轻热点的损耗了。而,这种情况是非常有可能的,对于一对多的模型,如果按照一去存储数据,那么因为多的数据量的不断扩展,会最终导致单个机器的数据量和io超限。

为了解决上述矛盾,就需要引入数据的迁移的方法了,简单来说,就是按照规则将数据从原来的一组机器上,迁移到新的一组机器上去,这样规则和数据一起变动,就可以有效的解决上面所说的热点问题,尽可能让所有的机器均匀的发挥效用。

思路很简单,但工程实践就复杂多了,下面来描述几种扩容的模式,希望大家能针对这几种场景以及我的分析,对如何解决这个问题有个更深入的认识。

所有有状态的数据,其实都需要有扩容的策略,在这些扩容的模式中,最简单的莫过于对cache节点的扩容了。因为cache本身其实只是一个一致的数据的一个快照,快照的意义就在于:如果你对快照的数据是否正确有异议,可以直接去从数据的源头再查一次写回快照中,即可保证数据的最新。

那么对于缓存数据,一般来说缓存的更新逻辑有两种,一种是写的时候同步更新缓存。一种是先读缓存,缓存没有的时候读数据库读出最新值后更新缓存,一般来说是两种缓存模式组合使用,因为没有副作用。对于这种情况的缓存节点扩容,最简单的做法是,只需要直接改变规则即可。

如,假设原来的数据是按照id% 4进行切分的,那么如果规则换为id% 8.那么有一半的数据就无法被访问到。但没关系,因为业务的实际逻辑是,如果读不到,就读穿缓存去数据库里面取数据再更新回缓存,所以很快,数据会按照新的id% 8 进行填充,扩容就完成了。

当然,实际的扩容比这个要复杂一点,因为,要考虑规则变动后,读穿的次数增多,导致数据库压力上升的问题,所以要尽可能的避免过多的数据读穿缓存,这时候会使用我们在以前的文章中讨论过的一致性hash或虚拟节点hash,使用缓慢更新映射关系的方式,来降低扩容对数据库带来的压力。

以上是最简单的规则和数据一起移动的例子,从上述例子可以分析出,其实规则迁移的最主要问题在于如何保证规则变更时,数据能够在规则发生变动的时候对外部保证数据是最新的读取,在缓存扩容的case中,这个数据保证最新的任务,是由数据库这个组件来完成的。所以缓存扩容是相对最为简单的。

那么,自然的就会产生另外一个疑问:对于数据库,怎么保证这个一致性的读取呢?这也是我们这一章要阐明的最重要的问题。

数据的一致性读,一般来说就只有两种做法。第一种是共享内存指针,说白了就是数据只有一份,但指向该数据的指针可能是多个。还有一种就是数据复制,数据的复制,保证一致性的难度会很大。更多的情况是按照实际的需求,取两种模式的折衷。

对数据节点的扩容而言,其实核心就是数据的复制,既然复制,那么一致性就非常难以保证,于是我们也就只能尽可能巧妙地利用手头的工具,取折衷,用以尽可能的减少不一致的影响。

为了解决这个一致性的问题,我们需要在规则上引入版本,这个概念,主要是用于规定什么时候数据应该以什么规则进行访问。这样,就可以避免数据复制过程中所带来的不一致的问题了。

假设,我们原来的规则,版本号为0,新的规则,版本号为1.那么,开始的时候,客户端所持有的数据的切分规则是版本0,所有数据在老的一组机器上进行读取和写入,不会出现问题。当我给定v0和v1两个版本同时存在时,从客户端就可以意识到,目前的规则是两份并存,数据可能是不一致的,这时候最简单的处理策略是,阻止一切读取和写入,这样数据的不一致就不会发生了(哈哈,因为本身不允许读写了嘛。。),而当规则变为只有v1的时候,那么客户端就可以知道,目前只有一个规则了,按照这个规则,进行数据访问就可以了。

使用版本号,就可以让客户端能够有机会意识到数据在某个时间段可能存在着不一致,应该加以针对性的处理,这样就可以规避数据读写的不一致的问题了。

解决了不一致的问题,下面紧接着要解决的问题有两个:

我如何知道应该让哪些数据移动到哪台机器上?

我如何尽可能的减小规则并存时的停写的数据范围?

针对这个问题,外面开源的社区,最常用的解决方法是一致性hash。

在一致性hash中,在某个地方加一组机器,可以很容易的预测应该将哪个节点的数据移动到新的节点上。同时,又可以预测,哪些节点不会受到影响,哪些不受到影响的节点,完全可以开放读取,而受到影响的节点,则阻止访问即可。

如上图中,

node4和node2中间,加了一个node5,那么很容易的可以知道,只需要将node4中的一部分数据,写入新的node5即可。而node2,node1,node3的数据不受到影响,可以继续允许访问。

这样就可以比较成功的解决上面提到的两个问题了。

但从http://qing.weibo.com/1765738567/693f084733000963.html这篇文章的讨论中,我们也很容易可以看到,一致性hash也有他自己的问题。

于是,自然就有人要问,有没有其他的做法呢?

自然是有啦,下面来介绍一下淘宝TDDL在这方面的工程实践吧。以下是纯粹干货,目前暂时没见过业内使用类似方式,这种模式在淘宝也经历了较多次的自动扩容考验,能够满足我们的需求,相信也一定能满足您的需求,因为它什么都没做,也什么都做了:).

首先是需求描述:分析淘宝的需求,简单概括就是一句话,业务方的规则需求,复杂到无以复加,绝非简单一致性hash或简单btree可以满足,为了不同的业务需求,会有种类很多的切分规则。

需求分析:

需求分析其实就是挖掘需求的含义,找到哪些是真实的需求,哪些不是,将不是的砍掉,看看剩下的能不能满足的过程:)

扩容系统的技术特点:

规则系统要自定义,因为这是业务核心,只有业务知道他们的数据怎么分配会获得比较均匀的访问模型。

扩容“不是”常态,一般来说扩容的周期是3个月~6个月,甚至更长。如果一个业务,每6天要扩容一次,那采购人员绝对会抄家伙找他们team干架去了

扩容本身不是不能做,但难度较大,一般来说需要几个人一起参与,最少有数据运维人员,系统运维人员以及开发人员参与,一帮苦13程序员夜里3点多闹钟叫起来,睡眼朦胧的进行机械的操作。难度可想而知。

基于这些技术特点,可以作如下分析

业务的变化要求数据扩容的规则要尽可能的自定义,可以有些预先定义好的规则模型,但不能强制要求业务必须走定义好的模型。

自动扩容,意义不大,如果只是让业务人员根据数据点个确定,是最容易被接受的扩容模式

要尽可能的避免扩容本身对业务本身带来的影响,同时要尽可能减轻开发人员的熬夜次数。

所以我们设计了如下的系统,他满足以下特性

规则完全自定义,你可以随便写任何的ifelse等脚本代码。

只对扩容需求提供决策支持和方案生成,但决策由人进行。

除了决策

在上一章中,我们主要介绍了规则引擎中最重要的一个部分,自动扩容,在今天的章节,我们主要还是介绍一下我们在淘宝TDDL中的工程实践吧。

首先从原理开始吧。

先来一张图

这张图以前也出现过,我们在里面着重介绍了规则引擎

规则引擎是什么呢?
对应在上述例子里面,其实就是DBNum = pk % 3 这个规则。

他的变化可能很多,比如对于一致性hash则变为一个if – else 的表达式(见前面)
也可能有其他的变化。
所以,我们要回归本源,问一个问题,什么是规则引擎?

抽象来看,规则引擎在做的事情是,根据一组输入条件(例如主键id,或者用户id+时间,或者一个rowKey),进行了一种计算,然后返回在某个机器某个表上执行的结果。这种计算要保证,在规则本身不发生变动的情况下,同一组输入条件,返回的永远是相同的结果。

想想这种描述像什么?:-) 我个人认为很像函数的定义,那么让我们换一下表述方式吧:
假设输入数据为x(主键id,用户id_时间,或者rowKey) ,经过运算F,返回了该数据在某台机器上这个结果y.那么表达式就是
y = F(x)

这是第一层抽象,为了方便表述,我们后面都以这种方式进行表述。

这种规则引擎,在几乎所有“有状态”的数据存储中都会用到,在我们的工程实践中,我们发现这套引擎需要非常灵活的表现能力,才能适应不同用户的不同需求,比如有些场景中,业务方会给出一批经过数据分析以后的大卖家,他们固定的就拥有大量数据,会对其他人造成影响,这时候规则引擎必须能够对各种不同的场景进行适应。

因为规则能够决定数据的分布是否均匀,因此规则是整套系统中最重要的核心组件。

有了规则引擎,我们要追寻的下一个目标就是,如何能够在尽量少的影响业务的正常使用的前提下,改变规则,以达到均衡访问或扩容的目标。

要达到这个规则,第一个需要做的事情就是要能够分辨,哪些数据应该被移动,以及从哪个源头移动到哪个目标去。
要解决这个问题,在当时能够想到的方法有两个,一个是定死的规则,比如一致性hash,一致性hash,因为规则本身的入参是定死的,输出也是定死的,所以可以知道从哪里移动到哪里。但这也会带来问题,因为有些业务根本不是使用一致性hash来完成的,他们可能有自定义的函数(如:如果卖家id=2000,那么走特殊的机器) 。
一旦有这样的自定义函数,那么就很难通过分析规则来获取需要迁移的数据是哪些以及应该从哪里移动到哪里这些属性了。

于是,就必须有另外的方法。

我们采取的方案,是完全放开F,采取多版本的方式来获得“哪些数据应该被移动,以及从哪个源头移动到哪个目标去”,这两个信息。

原理如下:

我们假设有老规则 F0 ,以及新规则F1.
对于相同的输入X,我们能得到两个y,也即
y0 = F0(x) 以及y1 = F1(x)

对两个y进行比较(compare) ,能够获取两种结果: 结果1 : y0 == y1. 结果2 : y0 != y1.

思考这两种结果的含义,不难明白其中的含义:
如果y0 == y1,那么意味着,对于相同的数据x,在老规则和新规则中,数据都在同一个库的同一张表上(y相同),这条数据在老规则换为新规则的时候是不需要移动的。
而,如果y0 != y1,那么意味着,这条数据,如果将规则从F0换为F1,则数据需要被移动,移动的方向应该是从y0到y1.

这样,我们就很轻松的使用多版本的方式,获得了“哪些数据应该被移动,以及从哪个源头移动到哪个目标去”,这两个信息。

最后,在知道了上面的两个关键的信息后,还需要一套东西来帮用户把数据尽可能平滑的从一个源机器中移动到目标机器中。

这就是我们在平衡迁移中进行的思考,如果有想探讨的欢迎一起参与。

下面,我们进入工程实践,来看一下我们的规则引擎在做的事情吧。

角色介绍

对于规则引擎,它实现了如下特性:
多版本支持
只有支持多版本,才能够方便的知道哪些数据应该从哪里移动到哪里去。
枚举支持
用来支持用户按照日期进行切分,但需要注意的是,这里的日期切分不是传统意义上B树模型的那种切分方式,原因见后续分析。
内建多种切分函数支持
允许方便的直接使用内置定义的一致性hash,虚拟节点hash等函数方法,减少代码量。

与规则引擎配套的,还有一套我们目前叫做“大禹”的项目工程,他主要完成了以下几件事:
切分数据收集
能够协助收集用户切分后的数据状态,如访问热点情况,硬件情况等。
决策支持
能够帮助用户定义新的扩容策略,但我们不做“自动化扩容”,因为扩容本身不是常态。
自动迁移
能够根据用户的多版本规则,协助用户自动化的进行规则迁移,最终能够将数据迁移导致的不可用时间降低到深夜1分钟内,基本不造成影响。
工程实践描述

在我们的工程实践中,我们选择了groovy来实现java的规则引擎,使用javaScript来实现跨平台的规则引擎。

从规则引擎来看,他只需要一个引擎,能够运行一个函数就可以了,所以上述平台都可以满足我们的需求,从速度角度考虑,我们选择了可静态编译的groovy和js v8引擎。

在这个引擎之上,我们对引擎进行了包装,针对淘宝的特殊需求进行了二次开发:
在淘宝,有很大一批数据是需要按照多个条件进行切分的,如,按照用户切库后,按照时间切表等,针对这种需求,我们要扩展原来的函数定义,允许用户使用类似table+”_”+ #userid# % 1024 +”_” + dayofmonth(#gmt#);
这样的方式来拼装类似table_0001_23这样的表后缀.实现多维度的切分。

同时,还需要满足用户的范围查询需求,如,返回一个用户在某个时间段内的所有数据。这往往意味着可能要遍历多个分表的需求,针对这种需求,我们允许用户使用表达式的方式填入y = F(x)中的’x’ ,如 ‘x’ = (gmt <= now() ) and (gmt >’2012-01-01′ ) 这样的输入参数。
针对这样的参数,传统的解决方案是使用排序后的树形结构来满足查询(如hbase),我们认为,因为数据节点的个数本身是有限的,我们没有必要维持复杂的数据结构,只需要使用枚举的方式就可以达到类似的效果,因为颗粒度可控。

对于大禹工程

排开数据收集以及分析后展现之外,最重要的部分无疑是能够根据多版本的规则进行自动化的扩容和迁移这一块了。

他的主要流程如下:

举个例子来说明这个流程

从整体来看,大禹在做的事情就是,全量迁移所有需要移动的数据,然后将在全量过程中产生的增量数据append到新节点上,然后部分停写1分钟,推送规则的新版本。完成迁移。

我们假定原来有一台机器,里面有两条记录:
row a : id = 0 ,name = “a”
row b : id = 1 ,name = “b”

切分的规则为 id % 1 ,
那么我们根据表达式 y0 = id % 1 ,分别将id(row a) = 0 ;id(row b) = 1代入表达式,得到y0(row a) = 0; y0(row b) = 0;
这两个结果。

然后,我们要将机器扩容为两台,
这时候规则变为 y1 = id % 2,分别将id(row a) = 0 ;id(row b) = 1代入表达式,得到y0(row a) = 0; y0(row b) = 1;

这时候,用户新写入了一条数据row c : id = 3 , name=”c”

因为用户在使用老规则写入,所以使用老规则后,数据应该通过老规则计算出结果y0(row c) = id % 1 = 0;
在按老规则写入后,数据就已经可见了,这时候,大禹会读取这条记录,按照新规则进行计算,y1(row c) = id % 2 = 1; 因为1 != 0,所以row c 需要进行迁移,迁移目标是从0机器–> 1机器。

这时候大禹会将这条数据保存在本地磁盘中。

而如果假定row d 通过新老规则计算出的结果y0 (row c) == y1 ( row c) 则该数据会被大禹增量复制组件丢弃,因为数据在规则变动后不需要移动位置。

在增量开启后,会进行全量的迁移。
全量的过程与增量类似,是按照选择条件,将老机器内的指定数据遍历一次,对每一条记录,进行老规则和新规则的计算,如果计算结果相同则丢弃,计算结果不同,则将数据写入新规则算出后的结果。

当全量结束后,大禹增量复制组件会将记录在本地磁盘中的增量数据覆盖到全量后的数据上,并且继续随着新的数据产生,将数据双写在老规则和新规则所对应的机器上。并发出catch up的状态指令。

在catch up后,我们可以认为,老规则内的数据和新规则内的数据,是异步一致的,中间的数据延迟是异步复制的延迟,一般来说在几百个毫秒内。

这时候,就可以选择一个合适的时机,比如夜里5点,进行部分停写,等待新老数据绝对一致以后,发布新规则。完成迁移。

整个迁移过程,只有最后的“部分停写,等待新老数据绝对一致以后,发布新规则。完成迁移“ 是会影响业务应用的,这之前的所有过程都是个外加过程,对业务完全没有影响,就算异常失败了,也可以全部放弃掉以后重新来过,这就保证了整套逻辑的尽可能简单清晰。

好的软件就是少做不该做的事情的软件嘛 :)

以上是好的地方,下面来自暴家丑,说说不足。

规则引擎所面向的目标,其实是有状态数据的节点管理,对于节点管理来说,大家的追求一般都是有共识的,也就是说,可以按照需求,随便的增加或减少节点。但遗憾的是,目前在我们的工程实践中,目前还没能很好的解决“随便”这个需求。

所谓随便,就是指可以达到这样一个效果,某天某个监控人员,发现某些数据突然的成为热点了,那么它可以快速反应,点个按钮,上线100台机器,立刻load下降,保证了系统稳定。然后呢,发现某个集群load很低,就点个按钮,下线100台机器作战略储备。

可惜,这样的事情在有状态的机器中是很难做到的,原因很简单,有状态节点的数据迁移是需要成本的,而且成本不小,这也是为什么foursquare会挂的原因。

以上,就是我对淘宝TDDL 数据库切分tool kits中规则引擎和配套的自动扩容组件的介绍了。
目前淘宝的TDDL组件被广泛的使用在淘宝300多个不同的业务系统中,并且没有使用过强制命令进行推广。

在未来的一个Q内,我们会逐渐的开源我们目前的这套工程实践产品,希望有更多的人能够受益。


posted on 2013-07-23 17:48  Java码界探秘  阅读(163)  评论(0编辑  收藏  举报

导航