|
在讲数据库水平拆分时候,我列出了水平拆分数据库需要解决的两个难题,它们分别是主键的设计问题和单表查询的问题,主键问题前文已经做了比较详细的讲述了,但是第二个问题我没有讲述,今天我将会讲讲如何解决数据表被垂直拆分后的单表查询问题。
要解决数据表被水平拆分后的单表查询问题,我们首先要回到问题的源头,我们为什么需要将数据库的表进行水平拆分。下面我们来推导下我们最终下定决心做水平拆分表的演进过程,具体如下:
第一个演进过程:进行了读写分离的表在数据增长后需要进行水平拆分吗?回答这个疑问我们首先要想想进行读写分离操作的表真的是因为数据量大吗?答案其实是否定的。最基本的读写分离的目的是为了解决数据库的某张表读写比率严重失衡的问题,举个例子,有一张表每天会增加1万条数据,也就是说我们的系统每天会向这张表做1万次写的操作,当然也有可能我们还会更新或者删除这张表的某些已有的记录,这些操作我们把它归并到写操作,那么这张表一天我们随意定义个估值吧2万5千次写操作,其实这种表的数据量并不大,一年下来也就新增的几百万条数据,一个大型的商业级别的关系数据库,当我们为表建立好索引和分区后,查询几百万条数据它的效率并不低,这么说来查询的效率问题还不一定是读写分离的源头。其实啊,这张表除了写操作每天还承受的读操作可能会是10万,20万甚至更高,这个时候问题来了,像oracle和mysql这样鼎鼎大名的关系数据库默认的最大连接数是100,一般上了生产环境我们可能会设置为150或者200,这些连接数已经到了这些关系数据库的最大极限了,如果再加以提升,数据库性能会严重下降,最终很有可能导致数据库由于压力过大而变成了一个巨锁,最终导致系统发生503的错误,如是我们就会想到采用读写分离方案,将数据库的读操作迁移到专门的读库里,如果系统的负载指标和我列举的例子相仿,那么迁移的读库甚至不用做什么垂直拆分就能满足实际的业务需求,因为我们的目的只是为了减轻数据库的连接压力。
第二个演进过程:随着公司业务的不断增长,系统的运行的压力也越来越大了,我们已经了解了系统的第一个瓶颈是从存储开始了,如是我们开始谈论方案如何解决存储的问题,这时我们发现我们已经做了读写分离,也使用了缓存,甚至连搜索技术也用上了,那么下个阶段就是垂直拆分了,垂直拆分很简单就是把表从数据库里拆出来,单独建库建表,但是这种直截了当的方案想想就能感到这样的做法似乎没有打中系统的痛点,那么系统的痛点到底是什么呢?根据数据库本身的特性,我们会发现痛点主要是三个方面组成:
第一个方面:数据库的连接数的限制。原库的某些表可能承担数据库80%的连接,极端下甚至可以超过90%的连接,而且这些表的业务操作十分的频繁,当其他小众业务的表需要进行操作时候,搞不好因为连接数被全部占用而不得不排队等待空闲连接的出现,那么这个时候我们就会考虑把这张表做垂直拆分,这样就减轻了原数据库连接的压力,使得数据库连接负载变得比较均衡。
第二个方面是数据库的读操作,第三个方面是数据库的写操作,虽然把读和写分成两个方面,但是这两个方面在我们做垂直拆分时候要结合起来考虑。首先我们要分析下数据库的写操作,单独的写操作效率都是很高的,不管我们的写是单条记录的写操作,还是批量的写操作,这些写操作的数据量就是我们要去写的数据的大小,因此控制写的数据量的大小是一件很容易很天然的操作,所以这些操作不会造成数据库太大负担,详细点的话,对于数据库而言,新增操作无非是在原来数据后面追加些记录,而修改操作或者删除操作一般都是通过建立了高效索引的字段来定位数据后再进行的操作,因此它的性能也是非常高的。而读操作看起来比写操作简单(例如:读操作不存在像事务这些乌七八糟因素的干扰),但是当读操作面对海量数据时候就严重挑战着数据库和硬盘的极限能力,因此读操作很容易产生瓶颈问题,而且这个瓶颈不管问题表是否读写失衡都会面临的。
前文里我详细列举了一个交易表设计的案例,其中我们可以看到数据库垂直拆分在实际应用里的运用,在例子里我们首先根据业务特点将交易表分成了实时交易表和历史交易表,这个做法其实就是将原交易表的读和写进行分离,但是这种分离和纯粹的读写分离相比会更加有深意,这个深意就是拆分实时和历史交易表也就是在分拆原表的读写操作的关联性,换句话说,如果我们不这么做的话,那么交易表的每次写和每次读几乎等价,这样我们没法单独解决读的性能问题,分出了历史交易表后我们再对历史交易表来做读的优化,那么这也不会影响到写操作,这样把问题的复杂度给降低了。在案例里我们对历史交易表进行了业务级别的水平拆分,但是这个拆分是以如何提升读的效率进行的,因此前文讲到的水平拆分里主键设计方案基本上派不上用场,因为这两种水平拆分的出发点是不同的,那么使用的手段和达到效果也将不一样。
由上所述,我们可以把数据库的水平拆分重新定义下,我在这几篇文章里一直讲述的水平拆分本质是从数据库技术来定义的,我把它们称为狭义的水平拆分,与狭义相对的就是广义的水平拆分,例如上文例子里把交易表根据业务特性分为实时交易表和历史交易表,这种行为也是一种水平拆分,但是这个拆分不会遵守我前面讲到主键设计方案,但是它的确达到水平拆分的目的,所以这样的水平拆分就属于广义的水平拆分了。
第三个演进过程:到了三个演进过程我们就会考虑到真正的水平拆分了,也就是上面提到的狭义的水平拆分了,狭义的水平拆分执行的理由有两个,一个那就是数据量太大了,另一个是数据表的读写的关联性很难进行拆分了,这点和垂直拆分有所不同,做垂直拆分的考虑不一定是因为数据量过大,例如某种表数据量不大,但是负载过重,很容易让数据库达到连接的极限值,我们也会采取垂直拆分手段来解决问题,此外,我们想减轻写操作和读操作的关联性,从而能单独对有瓶颈的写操作或读操作做优化设计,那么我们也会考虑到垂直拆分,当然数据量实在是太大的表我们想优化,首先也会考虑到垂直拆分,因为垂直拆分是针对海量数据优化的起始手段,但是垂直拆分可不一定能解决海量数据的问题。
狭义水平拆分的使用的前提是因为数据量太大,到底多大了,我们举个例子来说明下,假如某个电商平台一天的交易笔数有2亿笔,我们用来存储数据的关系数据库单表记录到了5千万条后,查询性能就会严重下降,那么如果我们把这两亿条数据全部存进这个数据库,那么随着数据的累积,实时交易查询基本已经没法正常完成了,这个时候我们就得考虑把实时交易表进行狭义的水平拆分,狭义的水平拆分首先碰到的难点就是主键设计的问题,主键设计问题也就说明狭义水平拆分其实解决的是海量数据写的问题,如果这张表读操作很少,或者基本没有,这个水平拆分是很好设计的,但是一张表只写不读,对于作为业务系统的后台数据库那基本是非常罕见的,。
前文讲到的主键设计方案其实基本没有什么业务上的意义,它解决的主要问题是让写入的数据分布均匀,从而能合理使用存储资源,但是这个合理分布式存储资源却会给查询操作带来极大的问题,甚至有时可以说狭义水平拆分后数据查询变得困难就是由这种看起来合理的主键设计方案所致。
我们还是以实时交易表的实例来说明问题,一个电商平台下会接入很多不同的商户,但是不同的商户每天产生的交易量是不同,也就是说商户的维度会让我们使交易数据变得严重的不均衡,可能电商平台下不到5%的商户完成了全天交易量的80%,而其他95%的商户仅仅完成20%的交易量,但是作为业务系统的数据表,进行读操作首先被限制和约束的条件就是商户号,如果要为我们设计的实时交易表进行狭义的水平拆分,做拆分前我们要明确这个拆分是由交易量大的少量商户所致,而不是全部的商户所致的。如果按照均匀分布主键的设计方案,不加商户区分的分布数据,那么就会发生产生少量交易数据的商户的查询行为也要承受交易量大的商户数据的影响,而能产生大量交易数据的商户也没有因为自己的贡献度而得到应有的高级服务,碰到这个问题其实非常好解决,就是在做狭义水平拆分前,我们先做一次广义的水平拆分,把交易量大的商户交易和交易量小的商户交易拆分出来,交易量小的商户用一张表记录,这样交易量小的商户也会很happy的查询出需要的数据,心里也是美滋滋的。接下来我们就要对交易量大的商户的交易表开始做狭义的水平拆分了,为这些重点商户做专门的定制化服务。
做狭义水平拆分前,我们有个问题需要过一下,在狭义水平拆分前我们需要先做一下广义的水平拆分吗?这个我这里不好说,具体要看实际的业务场景,但是针对我列举的实时交易的例子而言,我觉得没那个必要,因此拆分出的重点商户交易量本来就很大,每个都在挑战数据库读能力的极限,更重要的是实时交易数据的时间粒度已经很小了,再去做广义水平拆分难度很大,而且很难做好,所以这个时候我们还是直接使用狭义的水平拆分。拆分完毕后我们就要解决查询问题了。
做实时查询的标准做法就是分页查询了,在讲述如何解决分页查询前,我们看看我们在淘宝里搜索【衣服】这个条件的分页情况,如下图所示:

我们看到一共才100页,淘宝上衣服的商品最多了,居然搜索出来的总页数只有100页,这是不是在挑战我们的常识啊,淘宝的这个做法也给我们在实现水平拆分后如何做分页查询一种启迪。要说明这个启迪前我们首先要看看传统的分页是如何做的,传统分页的做法是首先使用select count(1) form table这样的语句查询出需要查询数据的总数,然后再根据每页显示的记录条数,查询出需要显示的记录,然后页面根据记录总数,每页的条数,和查询的结果来完成分页查询。回到我们的交易表实例里,有一个重要商户在做实时交易查询,可是这个时候该商户已经产生了1千万笔交易了,假如每页显示10条,记录那么我们就要分成100万页,这要是真显示在页面上,绝对能让我们这些开发人员像哥伦布发现新大陆那样惊奇,反正我见过的最多分页也就是200多页,还是在百度搜索发现的。其实当数据库一张表的数据量非常大的时候,select的count查询效率就非常低下,这个查询有时也会近似个全表检索,所以count查询还没结束我们就会失去等待结果的耐心了,更不要是说等把数据查询出来了,所以这个时候我们可以学习下淘宝的做法,当商户第一次查询我们准许他查询有限的数据。
我自己所做的一个项目的做法就是这样的,当某个商户的交易量实在是很大时候我们其实不会计算数据的总笔数,而是一次性查询出1000条数据,这1000条数据查询出来后存入到缓存里,页面则只分100页,当用户一定要查询100页后的数据,我们再去追加查询,不过实践下来,商户基本很少会查询100页后的数据,常常看了5,6页就会停止查询了。不过商户也时常会有查询全部数据的需求,但是商户有这种需求的目的也不是想在分页查询里看的,一般都是为了比对数据使用的,这个时候我们一般是提供一个发起下载查询全部交易的功能页面,商户根据自己的条件先发起这样的需求,然后我们系统会在后台单独起个线程查询出全部数据,生成一个固定格式的文件,最后通过一些有效手段通知商户数据生成好了,让商户下载文件即可。
对于进行了狭义水平拆分的表做分页查询我们通常都不会是全表查询,而是抽取全局的数据的一部分结果呈现给用户,这个做法其实和很多市场调查的方式类似,市场调查我们通常是找一些样本采集相关数据,通过分析这些样本数据推导出全局的一个发展趋势,那么这些样本选择的合理性就和最终的结论有很大关系,回到狭义水平拆分的表做分页查询,我们为了及时满足用户需求,我们只是取出了全部数据中的一部分,但是这一部分数据是否满足用户的需求,这个问题是很有学问的,如果是交易表,我们往往是按时间先后顺序查询部分数据,所以这里其实使用到了一个时间的维度,其他业务的表可能这个维度会不一样,但肯定是有个维度约束我们到底返回那些部分的数据。这个维度可以用一个专有的名词指代那就是排序,具体点就是要那个字段进行升序还是降序查询,看到这里肯定有人会有异议,那就是这种抽样式的查询,肯定会导致查询的命中率的问题,即查出来的数据不一定全部都是我们要的,其实要想让数据排序正确,最好就是做全量排序,但是一到全量排序那就是全表查询,做海量数据的全表排序查询对于分页这种场景是无法完成的。回到淘宝的例子,我们相信淘宝肯定没有返回全部数据,而是抽取了部分数据分页,也就是淘宝查询时候加入了维度,每个淘宝的店家都希望自己的商户放在搜索的前列,那么淘宝就可以让商家掏钱,付了钱以后淘宝改变下商家在这个维度里的权重,那么商家的商品就可以排名靠前了。
狭义水平拆分的本身对排序也有很大的影响,水平拆分后我们一个分页查询可能要从不同数据库不同的物理表里去取数据,单表下我们可以先通过数据库的排序算法得到一定的数据,但是局部的排序到了全局可能就不正确了,这个又该怎么办了?其实由上面内容我们可以知道要满足对海量数据的所有查询限制是非常难的,时常是根本就无法满足,我们只能做到尽量多满足些查询限制,也就是海量查询只能做到尽量接近查询限制的条件,而很难完全满足,这个时候我前面提到的主键分布方案就能起到作用了,我们在设计狭义水平拆分表主键分布时候是尽量保持数据分布均衡,那么如果我们查询要从多张不同物理表里取的时候,例如我们要查1000条数据,而狭义水平拆分出了两个物理数据库,那么我们就可以每个数据库查询500条,然后在服务层归并成1000条数据,在服务层排序,这种场景下如果我们的主键设计时候还包含点业务意义,那么这个排序的精确度就会得到很大提升。假如用户对排序不敏感,那就更好做了,分页时候如果每页规定显示10条,我们可以把10条数据平均分配给两个数据库,也就是显示10条A库的数据,再显示5条B库的数据。
看到这里有些细心的朋友可能还会有疑问,那就是居然排序是分页查询的痛点,那么我们可以不用数据库查询,而使用搜索技术啊,NoSql数据库啊,的确这些技术可以更好的解决分页问题,但是关系数据库过渡到搜索引擎和NoSql数据库首先需要我们转化数据,而狭义的水平拆分的数据表本身数据量很大,这个转化过程我们是没法快速完成的,如果我们对延时容忍度那么高,其实我们就没必要去做数据库的狭义水平拆分了。这个问题反过来说明了使用狭义拆分数据表的业务场景,那就是:针对数据量很大的表同时该表的读写的关联性是没法有效拆分的。
最后我要讲的是,如果系统到了狭义水平拆分都没法解决时候,我们就要抛弃传统的关系数据方案了,将该业务全部使用NoSql数据库解决或者像很多大型互联网公司那样,改写开源的mysql数据库。文章写道这里,我还是想说一个观点,如果一个系统有很强烈需求去做狭义的水平拆分,那么这个公司的某个业务那肯定是非常的大了,所以啊,这个方案以公司为单位应该有点小众了。
提个问题给大家,关系数据库的瓶颈有哪些?我想有些朋友看到这个问题肯定会说出自己平时开发中碰到了一个跟数据库有关的什么什么问题,然后如何解决的等等,这样的答案没问题,但是却没有代表性,如果出现了一个新的存储瓶颈问题,你在那个场景的处理经验可以套用在这个新问题上吗?这个真的很难说。
其实不管什么样的问题场景最后解决它都要落实到数据库的话,那么这个问题场景一定是击中了数据库的某个痛点,那么我前面的六篇文章里那些手段到底是在解决数据库的那些痛点,下面我总结下,具体如下:
痛点一:数据库的连接数不够用了。换句话说就是在同一个时间内,要求和数据库建立连接的请求超出了数据库所允许的最大连接数,如果我们对超出的连接数没有进行有效的控制让它们直接落到了数据库上,那么就有可能会让数据库不堪重负,那么我们就得要分散这些连接,或者让请求排队。
痛点二:对于数据库表的操作无非两种一种是写操作,一种是读操作,在现实场景下很难出现读写都成问题的事情,往往是其中一种表的操作出现了瓶颈问题所引起的,由于读和写都是操作同一个介质,这就导致如果我们不对介质进行拆分去单独解决读的问题或者写的问题会让问题变的复杂化,最后很难从根本上解决问题。
痛点三:实时计算和海量数据的矛盾。本系列讲存储瓶颈问题其实有一个范畴的,那就是本系列讲到的手段都是在使用关系数据库来完成实时计算的业务场景,而现实中,数据库里表的数据都会随着时间推移而不断增长,当表的数据超出了一定规模后,受制于计算机硬盘、内存以及CPU本身的能力,我们很难完成对这些数据的实时处理,因此我们就必须要采取新的手段解决这些问题。
我今天之所以总结下这三个痛点,主要是为了告诉大家当我们面对存储瓶颈问题时候,我们要把问题最终落实到这个问题到底是因为触碰到了数据库的那些痛点,这样回过头来再看我前面说到的技术手段,我就会知道该用什么手段来解决问题了。
好了,多余的话就说到这里,下面开始本篇的主要内容了。首先给大伙看一张有趣的漫画,如下图所示:

身为程序员的我看到这个漫画感到很沮丧,因为我们被机器打败了。但是这个漫画同时提醒了做软件的程序员,软件的性能其实和硬件有着不可分割的关系,也许我们碰到的存储问题不一定是由我们的程序产生的,而是因为好的炮弹装进了一个老旧过时的大炮里,最后当然我们会感到炮弹的威力没有达到我们的预期。除此之外了,也有可能我们的程序设计本身没有有效的利用好已有的资源,所以在前文里我提到如果我们知道存储的瓶颈问题将会是网站首先发生问题的地方,那么在数据库建模时候我们要尽量减轻数据库的计算功能,只保留数据库最基本的计算功能,而复杂的计算功能交由数据访问层完成,这其实是为解决瓶颈问题打下了一个良好的基础。最后我想强调一点,作为软件工程师经常会不自觉地忽视硬件对程序性能的影响,因此在设计方案时候考察下硬件和问题场景的关系或许能开拓我们解决问题的思路。
上面的问题按本篇开篇的痛点总结的思路总结下的话,那么就是如下:
痛点四:当数据库所在服务器的硬件有很大提升时候,我们可以优先考虑是否可以通过提升硬件性能的手段来提升数据库的性能。
在本系列的第一篇里,我讲到根据http无状态的特点,我们可以通过剥离web服务器的状态性主要是session的功能,那么当网站负载增大我们可以通过增加web服务器的方式扩容网站的并发能力。其实不管是读写分离方案,垂直拆分方案还是水平拆分方案细细体会下,它们也跟水平扩展web服务的方式有类似之处,这个类似之处也就是通过增加新的服务来扩展整个存储的性能,那么新的问题来了,前面的三种解决存储瓶颈的方案也能做到像web服务那样的水平扩展吗?换句话说,当方案执行一段时间后,又出现了瓶颈问题,我们可以通过增加服务器就能解决新的问题吗?
要回答清楚这个问题,我们首先要详细分析下web服务的水平扩展原理,web服务的水平扩展是基于http协议的无状态,http的无状态是指不同的http请求之间不存在任何关联关系,因此如果后台有多个web服务处理http请求,每个web服务器都部署相同的web服务,那么不管那个web服务处理http请求,结果都是等价的。这个原理如果平移到数据库,那么就是每个数据库操作落到任意一台数据库服务器都是等价的,那么这个等价就要求每个不同的物理数据库都得存储相同的数据,这么一来就没法解决读写失衡,解决海量数据的问题了,当然这样做看起来似乎可以解决连接数的问题,但是面对写操作就麻烦了,因为写数据时候我们必须保证两个数据库的数据同步问题,这就把问题变复杂了,所以web服务的水平扩展是不适用于数据库的。这也变相说明,分库分表的数据库本身就拥有很强的状态性。
不过web服务的水平扩展还代表一个思想,那就是当业务操作超出了单机服务器的处理能力,那么我们可以通过增加服务器的方式水平拓展整个web服务器的处理能力,这个思想放到数据库而言,肯定是适用的。那么我们就可以定义下数据库的水平扩展,具体如下:
数据库的水平扩展是指通过增加服务器的方式提升整个存储层的性能。
数据库的读写分离方案,垂直拆分方案还有水平拆分方案其实都是以表为单位进行的,假如我们把数据库的表作为一个操作原子,读写分离方案和垂直拆分方案都没有打破表的原子性,并且都是以表为着力点进行,因此如果我们增加服务器来扩容这些方案的性能,肯定会触碰表原子性的红线,那么这个方案也就演变成了水平拆分方案了,由此我们可以得出一个结论:
数据库的水平扩展基本都是基于水平拆分进行的,也就是说数据库的水平扩展是在数据库水平拆分后再进行一次水平拆分,水平扩展的次数也就代表的水平拆分迭代的次数。因此要谈好数据库的水平扩展问题,我们首先要更加细致的分析下水平拆分的方案,当然这里所说的水平拆分方案指的是狭义的水平拆分。
数据库的水平扩展其实就是让被水平拆分的表的数据跟进一步的分散,而数据的离散规则是由水平拆分的主键设计方案所决定的,在前文里我推崇了一个使用sequence及自增列的方案,当时我给出了两种实现手段,一种是通过设置不同的起始数和相同的步长,这样来拆分数据的分布,另一种是通过估算每台服务器的存储承载能力,通过设定自增的起始值和最大值来拆分数据,我当时说到方案一我们可以通过设置不同步长的间隔,这样我们为我们之后的水平扩展带来便利,方案二起始也可以设定新的起始值也来完成水平扩展,但是不管哪个方案进行水平扩展后,有个新问题我们不得不去面对,那就是数据分配的不均衡,因为原有的服务器会有历史数据的负担问题。而在我谈到狭义水平拆分时候,数据分配的均匀问题曾被我作为水平技术拆分的优点,但是到了扩展就出现了数据分配的不均衡了,数据的不均衡会造成系统计算资源利用率混乱,更要命的是它还会影响到上层的计算操作,例如海量数据的排序查询,因为数据分配不均衡,那么局部排序的偏差会变得更大。解决这个问题的手段只有一个,那就是对数据根据平均原则重新分布,这就得进行大规模的数据迁移了,由此可见,除非我们觉得数据是否分布均匀对业务影响不大,不需要调整数据分布,那么这个水平扩展还是很有效果,但是如果业务系统不能容忍数据分布的不均衡,那么我们的水平扩展就相当于重新做了一遍水平拆分,那是相当的麻烦。其实这些还不是最要命的,如果一个系统后台数据库要做水平扩展,水平扩展后又要做数据迁移,这个扩展的表还是一个核心业务表,那么方案上线时候必然导致数据库停止服务一段时间。
数据库的水平扩展本质上就是水平拆分的迭代操作,换句话说水平扩展就是在已经进行了水平拆分后再拆分一次,扩展的主要问题就是新的水平拆分是否能继承前一次的水平拆分,从而实现只做少量的修改就能达到我们的业务需求,那么我们如果想解决这个问题就得回到问题的源头,我们的前一次水平拆分是否能良好的支持后续的水平拆分,那么为了做到这点我们到底要注意哪些问题呢?我个人认为应该主要注意两个问题,它们分别是:水平扩展和数据迁移的关系问题以及排序的问题。
问题一:水平扩展和数据迁移的关系问题。在我上边的例子里,我们所做的水平拆分的主键设计方案都是基于一个平均的原则进行的,如果新的服务器加入后就会破坏数据平均分配的原则,为了保证数据分布的均匀我们就不能不将数据做相应的迁移。这个问题推而广之,就算我们水平拆分没有过分强调平均原则,或者使用其他维度来分割数据,如果这个维度在水平扩展时候和原库原表有关联关系,那么结果都有可能导致数据的迁移问题,因为水平扩展是很容易产生数据迁移问题。
对于一个实时系统而言,核心的业务表发生数据迁移是一件风险很大成本很高的事情,抛开迁移的操作危险,数据迁移会导致系统停机,这点是所有系统相关方很难接受的。那么如何解决水平扩展的数据迁移问题了,那么这个时候一致性哈希就派上用场了,一致性哈希是固定哈希算法的衍生,下面我们就来简单介绍下一致性哈希的原理,首先我看看下面这张图:

一致性哈希使用时候首先要计算出用来做水平拆分服务器的数字哈希值,并将这些哈希值配置到0~232的圆上,接着计算出被存储数据主键的数字哈希值,并把它们映射到这个圆上,然后从数据映射到的位置开始顺时针查找,并将数据保存在找到的第一个服务器上,如果主键的哈希值超过了232,那么该记录就会保存在第一台服务器上。这些如上图的第一张图。
那么有一天我们要添加新的服务器了,也就是要做水平扩展了,如上图的第二张图,新节点(图上node5)只会影响到的原节点node4,即顺时针方向的第一个节点,因此一致性哈希能最大限度的抑制数据的重新分布。
上面的例图里我们只使用了4个节点,添加一个新节点影响到了25%左右的数据,这个影响度还是有点大,那有没有办法还能降低点影响了,那么我们可以在一致性哈希算法的基础上进行改进,一致性哈希上的分布节点越多,那么添加和删除一个节点对于总体影响最小,但是现实里我们不一定真的是用那么多节点,那么我们可以增加大量的虚拟节点来进一步抑制数据分布不均衡。
前文里我将水平拆分的主键设计方案类比分布式缓存技术memcached,其实水平拆分在数据库技术里也有一个专属的概念代表他,那就是数据的分区,只不过水平拆分的这个分区粒度更大,操作的动静也更大,笔者这里之所以提这个主要是因为写存储瓶颈一定会受到我自己经验和知识的限制,如果有朋友因为看了本文而对存储问题发生了兴趣,那么我这里也可以指明一个学习的方向,这样就能避免一些价值不高的探索过程,让学习的效率会更高点。
问题二:水平扩展的排序问题。当我们要做水平扩展时候肯定有个这样的因素在作怪:数据量太大了。前文里我说道过海量数据会对读操作带来严重挑战,对于实时系统而言,要对海量数据做实时查询几乎是件无法完成的工作,但是现实中我们还是需要这样的操作,可是当碰到如此操作我们一般采取抽取部分结果数据的方式来满足查询的实时性,要想让这些少量的数据能让用户满意,而不会产生太大的业务偏差,那么排序就变变得十分重要了。
不过这里的排序一定要加上一个范畴,首先我们要明确一点啊,对海量数据进行全排序,而这个全排序还要以实时的要求进行,这个是根本无法完成的,为什么说无法完成,因为这些都是在挑战硬盘读写速度,内存读写速度以及CPU的运算能力,假如1Tb的数据上面这三个要素不包括排序操作,读取操作能在10毫秒内完成,也许海量数据的实时排序才有可能,但是目前计算机是绝对没有这个能力的。
那么现实场景下我们是如何解决海量数据的实时排序问题的呢?为了解决这个问题我们就必须有点逆向思维的意识了,另辟蹊径的处理排序难题。第一种方式就是缩小需要排序的数据大小,那么数据库的分区技术是一个很好的手段,除了分区手段外,其实还有一个手段,前面我讲到使用搜索技术可以解决数据库读慢的难题,搜索库本身可以当做一个读库,那么搜索技术是怎么来解决快速读取海量数据的难题了,它的手段是使用索引,索引好比一本书的目录,我们想从书里检索我们想要的信息,我们最有效率的方式就是先查询目录,找到自己想要看的标题,然后对应页码,把书直接翻到那一页,存储系统索引的本质和书的目录一样,只不过计算机领域的索引技术更加的复杂。其实为数据建立索引,本身就是一个缩小数据范围和大小的一种手段,这点它和分区是类似的。我们其实可以把索引当做一张数据库的映射表,一般存储系统为了让索引高效以及为了扩展索引查找数据的精确度,存储系统在建立索引的时候还会跟索引建立好排序,那么当用户做实时查询时候,他根据索引字段查找数据,因为索引本身就有良好的排序,那么在查询的过程里就可以免去排序的操作,最终我们就可以高效的获取一个已经排好序的结果集。
现在我们回到水平拆分海量数据排序的场景,前文里我提到了海量数据做分页实时查询可以采用一种抽样的方式进行,虽然用户的意图是想进行海量数据查询,但是人不可能一下子消化掉全部海量数据的特点,因此我们可以只对海量数据的部分进行操作,可是由于用户的本意是全量数据,我们给出的抽样数据如何能更加精确点,那么就和我们在分布数据时候分布原则有关系,具体落实的就是主键设计方案了,碰到这样的场景就得要求我们的主键具有排序的特点,那么我们就不得不探讨下水平拆分里主键的排序问题了。
在前文里我提到一种使用固定哈希算法来设计主键的方案,当时提到的限制条件就是主键本身没有排序特性,只有唯一性,因此哈希出来的值是唯一的,这种哈希方式其实不能保证数据分布时候每台服务器上落地数据有一个先后的时间顺序,它只能保证在海量数据存储分布式时候各个服务器近似均匀,因此这样的主键设计方案碰到分页查询有排序要求时候其实是起不到任何作用的,因此如果我们想让主键有个先后顺序最好使用递增的数字来表示,但是递增数字的设计方案如果按照我前面的起始数,步长方式就会有一个问题,那就是单库单表的顺序性可以保障,跨库跨表之间的顺序是很难保证的,这也说明我们对于水平拆分的主键字段对于逻辑表进行全排序也是一件无法完成的任务。
那么我们到底该如何解决这个问题了,那么我们只得使用单独的主键生成服务器了,前文里我曾经批评了主键生成服务器方案,文章发表后有个朋友找到我谈论了下这个问题,他说出了他们计划的一个做法,他们自己研发了一个主键生成服务器,因为害怕这个服务器单点故障,他们把它做成了分布式,他们自己设计了一套简单的UUID算法,使得这个算法适合集群的特点,他们打算用zookeeper保证这个集群的可靠性,好了,他们做法里最关键的一点来了,如何保证主键获取的高效性,他说他们没有让每次生成主键的操作都是直接访问集群,而是在集群和主键使用者之间做了个代理层,集群也不是频繁生成主键的,而是每次生成一大批主键,这一大批主键值按队列的方式缓存在代理层了,每次主键使用者获取主键时候,队列就消耗一个主键,当然他们的系统还会检查主键使用的比率,当比率到达阀值时候集群就会收到通知,马上开始生成新的一批主键值,然后将这些值追加到代理层队列里,为了保证主键生成的可靠性以及主键生成的连续性,这个主键队列只要收到一次主键请求操作就消费掉这个主键,也不关心这个主键到底是否真的被正常使用过,当时我还提出了一个自己的疑问,要是代理挂掉了呢?那么集群该如何再生成主键值了,他说他们的系统没有单点系统,就算是代理层也是分布式的,所以非常可靠,就算全部服务器全挂了,那么这个时候主键生成服务器集群也不会再重复生成已经生成过的主键值,当然每次生成完主键值后,为了安全起见,主键生成服务会把生成的最大主键值持久化保存。
其实这位朋友的主键设计方案其实核心设计起点就是为了解决主键的排序问题,这也为实际使用单独主键设计方案找到了一个很现实的场景。如果能做到保证主键的顺序性,同时数据落地时候根据这个顺序依次进行的,那么在单库做排序查询的精确度就会很高,查询时候我们把查询的条数均匀分布到各个服务器的表上,最后汇总的排序结果也是近似精确的。
自从和这位朋友聊到了主键生成服务的设计问题后以及我今天讲到的一致性哈希的问题,我现在有点摒弃前文里说到的固定哈希算法的主键设计方案了,这个摒弃也是有条件限制的,主键生成服务的方案其实是让固定哈希方案更加完善,但是如果主键本身没有排序性,只有唯一性,那么这个做法对于排序查询起不到什么作用,到了水平扩展,固定哈希排序的扩展会导致大量数据迁移,风险和成本太高,而一致性哈希是固定哈希的进化版,因此当我们想使用哈希来分布数据时候,还不如一开始就使用一致性哈希,这样就为后续的系统升级和维护带来很大的便利。
有网友在留言里还提到了哈希算法分布数据的一个问题,那就是硬件的性能对数据平均分配的影响,如果水平拆分所使用的服务器性能存在差异,那么平均分配是会造成热点问题的出现,如果我们不去改变硬件的差异性,那么就不得不在分配原则上加入权重的算法来动态调整数据的分布,这样就制造了人为的数据分布不均衡,那么到了上层的计算操作时候某些场景我们也会不自觉的加入权重的维度。但是作为笔者的我对这个做法是有异议的,这些异议具体如下:
异议一:我个人认为不管什么系统引入权重都是把问题复杂化的操作,权重往往都是权益之计,如果随着时间推移还要进一步扩展权重算法,那么问题就变得越加复杂了,而且我个人认为权重是很难进行合理处理的,权重如果还要演进会变得异常复杂,这个复杂度可能会远远超出分布式系统,数据拆分本身的难度,因此除非迫不得已我们还是尽量不去使用什么权重,就算有权重也不要轻易使用,看有没有方式可以消除权重的根本问题。
异议二:如果我们的系统后台数据库都是使用独立服务器,那么一般都会让最好的服务器服务于数据库,这个做法本身就说明了数据库的重要性,而且我们对数据库的任何分库分表的解决方案都会很麻烦,很繁琐甚至很危险,因此本篇开始提出了如果我们解决瓶颈问题前先考虑下硬件的问题,如果硬件可以解决掉问题,优先采取硬件方案,这就说明我们合理对待存储问题的前提就是让数据库的硬件跟上时代的要求,那么如果有些硬件出现了性能瓶颈,是不是我们忽视了硬件的重要性了?
异议三:均匀分布数据不仅仅可以合理利用计算资源,它还会给业务操作带来好处,那么我们扩展数据库时候就让各个服务器本身能力均衡,这个其实不难的,如果老的服务器实在太老了,用新服务器替换掉,虽然会有全库迁移的问题,但是这么粗粒度的数据平移,那可是比任何拆分方案的数据迁移难度低的多的。
好了,本篇就写到这里,祝大家工作生活愉快!
|