关于大型网站技术演进的思考(七)–存储的瓶颈(7)

 

  本文开篇提个问题给大家,关系数据库的瓶颈有哪些?我想有些朋友看到这个问题肯定会说出自己平时开发中碰到了一个跟数据库有关的什么什么问题,然后如何解决的等等,这样的答案没问题,但是却没有代表性,如果出现了一个新的存储瓶颈问题,你在那个场景的处理经验可以套用在这个新问题上吗?这个真的很难说。

  其实不管什么样的问题场景最后解决它都要落实到数据库的话,那么这个问题场景一定是击中了数据库的某个痛点,那么我前面的六篇文章里那些手段到底是在解决数据库的那些痛点,下面我总结下,具体如下:

  痛点一:数据库的连接数不够用了。换句话说就是在同一个时间内,要求和数据库建立连接的请求超出了数据库所允许的最大连接数,如果我们对超出的连接数没有进行有效的控制让它们直接落到了数据库上,那么就有可能会让数据库不堪重负,那么我们就得要分散这些连接,或者让请求排队。

  痛点二:对于数据库表的操作无非两种一种是写操作,一种是读操作,在现实场景下很难出现读写都成问题的事情,往往是其中一种表的操作出现了瓶颈问题所引起的,由于读和写都是操作同一个介质,这就导致如果我们不对介质进行拆分去单独解决读的问题或者写的问题会让问题变的复杂化,最后很难从根本上解决问题。

  痛点三:实时计算和海量数据的矛盾。本系列讲存储瓶颈问题其实有一个范畴的,那就是本系列讲到的手段都是在使用关系数据库来完成实时计算的业务场景,而现实中,数据库里表的数据都会随着时间推移而不断增长,当表的数据超出了一定规模后,受制于计算机硬盘、内存以及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保证这个集群的可靠性,好了,他们做法里最关键的一点来了,如何保证主键获取的高效性,他说他们没有让每次生成主键的操作都是直接访问集群,而是在集群和主键使用者之间做了个代理层,集群也不是频繁生成主键的,而是每次生成一大批主键,这一大批主键值按队列的方式缓存在代理层了,每次主键使用者获取主键时候,队列就消耗一个主键,当然他们的系统还会检查主键使用的比率,当比率到达阀值时候集群就会收到通知,马上开始生成新的一批主键值,然后将这些值追加到代理层队列里,为了保证主键生成的可靠性以及主键生成的连续性,这个主键队列只要收到一次主键请求操作就消费掉这个主键,也不关心这个主键到底是否真的被正常使用过,当时我还提出了一个自己的疑问,要是代理挂掉了呢?那么集群该如何再生成主键值了,他说他们的系统没有单点系统,就算是代理层也是分布式的,所以非常可靠,就算全部服务器全挂了,那么这个时候主键生成服务器集群也不会再重复生成已经生成过的主键值,当然每次生成完主键值后,为了安全起见,主键生成服务会把生成的最大主键值持久化保存。

  其实这位朋友的主键设计方案其实核心设计起点就是为了解决主键的排序问题,这也为实际使用单独主键设计方案找到了一个很现实的场景。如果能做到保证主键的顺序性,同时数据落地时候根据这个顺序依次进行的,那么在单库做排序查询的精确度就会很高,查询时候我们把查询的条数均匀分布到各个服务器的表上,最后汇总的排序结果也是近似精确的。

  自从和这位朋友聊到了主键生成服务的设计问题后以及我今天讲到的一致性哈希的问题,我现在有点摒弃前文里说到的固定哈希算法的主键设计方案了,这个摒弃也是有条件限制的,主键生成服务的方案其实是让固定哈希方案更加完善,但是如果主键本身没有排序性,只有唯一性,那么这个做法对于排序查询起不到什么作用,到了水平扩展,固定哈希排序的扩展会导致大量数据迁移,风险和成本太高,而一致性哈希是固定哈希的进化版,因此当我们想使用哈希来分布数据时候,还不如一开始就使用一致性哈希,这样就为后续的系统升级和维护带来很大的便利。

  有网友在留言里还提到了哈希算法分布数据的一个问题,那就是硬件的性能对数据平均分配的影响,如果水平拆分所使用的服务器性能存在差异,那么平均分配是会造成热点问题的出现,如果我们不去改变硬件的差异性,那么就不得不在分配原则上加入权重的算法来动态调整数据的分布,这样就制造了人为的数据分布不均衡,那么到了上层的计算操作时候某些场景我们也会不自觉的加入权重的维度。但是作为笔者的我对这个做法是有异议的,这些异议具体如下:

  异议一:我个人认为不管什么系统引入权重都是把问题复杂化的操作,权重往往都是权益之计,如果随着时间推移还要进一步扩展权重算法,那么问题就变得越加复杂了,而且我个人认为权重是很难进行合理处理的,权重如果还要演进会变得异常复杂,这个复杂度可能会远远超出分布式系统,数据拆分本身的难度,因此除非迫不得已我们还是尽量不去使用什么权重,就算有权重也不要轻易使用,看有没有方式可以消除权重的根本问题。

  异议二:如果我们的系统后台数据库都是使用独立服务器,那么一般都会让最好的服务器服务于数据库,这个做法本身就说明了数据库的重要性,而且我们对数据库的任何分库分表的解决方案都会很麻烦,很繁琐甚至很危险,因此本篇开始提出了如果我们解决瓶颈问题前先考虑下硬件的问题,如果硬件可以解决掉问题,优先采取硬件方案,这就说明我们合理对待存储问题的前提就是让数据库的硬件跟上时代的要求,那么如果有些硬件出现了性能瓶颈,是不是我们忽视了硬件的重要性了?

  异议三:均匀分布数据不仅仅可以合理利用计算资源,它还会给业务操作带来好处,那么我们扩展数据库时候就让各个服务器本身能力均衡,这个其实不难的,如果老的服务器实在太老了,用新服务器替换掉,虽然会有全库迁移的问题,但是这么粗粒度的数据平移,那可是比任何拆分方案的数据迁移难度低的多的。

  好了,本篇就写到这里,祝大家工作生活愉快!

Leave a Reply