首页 > 代码库 > 关于大型站点技术演进的思考(七)--存储的瓶颈(7)

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

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

  事实上无论什么样的问题场景最后解决它都要落实到数据库的话。那么这个问题场景一定是击中了数据库的某个痛点,那么我前面的六篇文章里那些手段究竟是在解决数据库的那些痛点,以下我总结下,详细例如以下:

  痛点一:数据库的连接数不够用了。

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

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

  痛点三:实时计算和海量数据的矛盾。本系列讲存储瓶颈问题事实上有一个范畴的,那就是本系列讲到的手段都是在使用关系数据库来完毕实时计算的业务场景。而现实中,数据库里表的数据都会随着时间推移而不断增长,当表的数据超出了一定规模后,受制于计算机硬盘、内存以及CPU本身的能力,我们非常难完毕对这些数据的实时处理。因此我们就必需要採取新的手段解决这些问题。

  我今天之所以总结下这三个痛点,主要是为了告诉大家当我们面对存储瓶颈问题时候。我们要把问题终于落实到这个问题究竟是由于触碰到了数据库的那些痛点。这样回过头来再看我前面说到的技术手段,我就会知道该用什么手段来解决这个问题了。

  好了,多余的话就讲到这里,以下開始本篇的主要内容了。首先给大伙看一张有趣的漫画,例如以下图所看到的:

技术分享 

  身为程序猿的我看到这个漫画感到非常沮丧,由于我们被机器打败了。可是这个漫画同一时候提醒了做软件的程序猿,软件的性能事实上和硬件有着不可切割的关系,也许我们碰到的存储问题不一定是由我们的程序产生的,而是由于好的炮弹装进了一个老旧过时的大炮里,最后当然我们会感到炮弹的威力没有达到我们的预期。除此之外了,也有可能我们的程序设计本身没有有效的利用好已有的资源,所以在前文里我提到假设我们知道存储的瓶颈问题将会是站点首先发生故障的地方,那么在数据库建模时候我们要尽量减轻数据库的计算功能,仅仅保留数据库最主要的计算功能。而复杂的计算功能交由数据訪问层完毕。这事实上是为解决瓶颈问题打下了一个良好的基础。最后我想强调一点,作为软件project师常常会不自觉地忽视硬件对程序性能的影响。因此在设计方案时候考察下硬件和问题场景的关系也许能开拓我们解决这个问题的思路。

  上面的问题按本篇开篇的痛点总结的思路总结下的话。那么就是例如以下:

  痛点四:当数据库所在server的硬件有非常大提升时候。我们能够优先考虑能否够通过提升硬件性能的手段来提升数据库的性能。

  在本系列的第一篇里。我讲到依据http无状态的特点,我们能够通过剥离webserver的状态性主要是session的功能。那么当站点负载增大我们能够通过添加webserver的方式扩容站点的并发能力。事实上无论是读写分离方案,垂直拆分方案还是水平拆分方案细细体会下,它们也跟水平扩展web服务的方式有类似之处。这个类似之处也就是通过添加新的服务来扩展整个存储的性能,那么新的问题来了。前面的三种解决存储瓶颈的方案也能做到像web服务那样的水平扩展吗?换句话说。当方案运行一段时间后。又出现了瓶颈问题,我们能够通过添加server就能解决新的问题吗?

  要回答清楚这个问题,我们首先要具体分析下web服务的水平扩展原理,web服务的水平扩展是基于http协议的无状态。http的无状态是指不同的http请求之间不存在不论什么关联关系,因此假设后台有多个web服务处理http请求。每一个webserver都部署同样的web服务,那么无论那个web服务处理http请求,结果都是等价的。

这个原理假设平移到数据库,那么就是每一个数据库操作落到随意一台数据库server都是等价的,那么这个等价就要求每一个不同的物理数据库都得存储同样的数据,这么一来就没法解决读写失衡,解决海量数据的问题了。当然这样做看起来似乎能够解决连接数的问题,可是面对写操作就麻烦了,由于写数据时候我们必须保证两个数据库的数据同步问题。这就把问题变复杂了,所以web服务的水平扩展是不适用于数据库的。这也变相说明。分库分表的数据库本身就拥有非常强的状态性。

  只是web服务的水平扩展还代表一个思想。那就是当业务操作超出了单机server的处理能力。那么我们能够通过添加server的方式水平拓展整个webserver的处理能力,这个思想放到数据库而言。肯定是适用的。那么我们就能够定义下数据库的水平扩展,详细例如以下:

  数据库的水平扩展是指通过添加server的方式提升整个存储层的性能。

  数据库的读写分离方案,垂直拆分方案还有水平拆分方案事实上都是以表为单位进行的,假如我们把数据库的表作为一个操作原子。读写分离方案和垂直拆分方案都没有打破表的原子性,而且都是以表为着力点进行。因此假设我们添加server来扩容这些方案的性能,肯定会触碰表原子性的红线,那么这个方法也就演变成了水平拆分方案了,由此我们能够得出一个结论:

  数据库的水平扩展基本都是基于水平拆分进行的,也就是说数据库的水平扩展是在数据库水平拆分后再进行一次水平拆分。水平扩展的次数也就代表的水平拆分迭代的次数。因此要谈好数据库的水平扩展问题。我们首先要更加仔细的分析下水平拆分的方案,当然这里所说的水平拆分方案指的是狭义的水平拆分。

  数据库的水平扩展事实上就是让被水平拆分的表的数据跟进一步的分散,而数据的离散规则是由水平拆分的主键设计方案所决定的。在前文里我推崇了一个使用sequence及自增列的方案。当时我给出了两种实现手段。一种是通过设置不同的起始数和同样的步长,这样来拆分数据的分布,还有一种是通过估算每台server的存储承载能力。通过设定自增的起始值和最大值来拆分数据,我当时说到方案一我们能够通过设置不同步长的间隔,这样我们为我们之后的水平扩展带来便利。方案二起始也能够设定新的起始值也来完毕水平扩展,可是无论哪个方案进行水平扩展后。有个新问题我们不得不去面对,那就是数据分配的不均衡。由于原有的server会有历史数据的负担问题。

而在我谈到狭义水平拆分时候,数据分配的均匀问题曾被我作为水平技术拆分的长处。可是到了扩展就出现了数据分配的不均衡了,数据的不均衡会造成系统计算资源利用率混乱,更要命的是它还会影响到上层的计算操作。比如海量数据的排序查询。由于数据分配不均衡,那么局部排序的偏差会变得更大。解决问题的手段仅仅有一个。那就是对数据依据平均原则又一次分布,这就得进行大规模的数据迁移了,由此可见,除非我们认为数据是否分布均匀对业务影响不大,不须要调整数据分布,那么这个水平扩展还是非常有效果,可是假设业务系统不能容忍数据分布的不均衡,那么我们的水平扩展就相当于又一次做了一遍水平拆分。那是相当的麻烦。事实上这些还不是最要命的。假设一个系统后台数据库要做水平扩展。水平扩展后又要做数据迁移,这个扩展的表还是一个核心业务表,那么方案上线时候必定导致数据库停止服务一段时间。

  数据库的水平扩展本质上就是水平拆分的迭代操作,换句话说水平扩展就是在已经进行了水平拆分后再拆分一次,扩展的主要问题就是新的水平拆分能否继承前一次的水平拆分。从而实现仅仅做少量的改动就能达到我们的业务需求。那么我们假设想解决问题就得回到问题的源头,我们的前一次水平拆分能否良好的支持兴许的水平拆分。那么为了做到这点我们究竟要注意哪些问题呢?我个人觉得应该主要注意两个问题。它们各自是:水平扩展和数据迁移的关系问题以及排序的问题

  问题一:水平扩展和数据迁移的关系问题

在我上边的样例里,我们所做的水平拆分的主键设计方案都是基于一个平均的原则进行的,假设新的server增加后就会破坏数据平均分配的原则,为了保证数据分布的均匀我们就不能不将数据做对应的迁移。这个问题推而广之。就算我们水平拆分没有过分强调平均原则。或者使用其它维度来切割数据,假设这个维度在水平扩展时候和原库原表有关联关系,那么结果都有可能导致数据的迁移问题。由于水平扩展是非常easy产生数据迁移问题。

  对于一个实时系统而言,核心的业务表发生数据迁移是一件风险非常大成本非常高的事情。抛开迁移的操作危急,数据迁移会导致系统停机,这点是全部系统相关方非常难接受的。那么怎样解决水平扩展的数据迁移问题了,那么这个时候一致性哈希就派上用场了,一致性哈希是固定哈希算法的衍生,以下我们就来简介下一致性哈希的原理,首先我看看以下这张图:

 技术分享

  一致性哈希使用时候首先要计算出用来做水平拆分server的数字哈希值,并将这些哈希值配置到0~232的圆上,接着计算出被存储数据主键的数字哈希值,并把它们映射到这个圆上。然后从数据映射到的位置開始顺时针查找,并将数据保存在找到的第一个server上,假设主键的哈希值超过了232,那么该记录就会保存在第一台server上。这些如上图的第一张图。

  那么有一天我们要加入新的server了,也就是要做水平扩展了,如上图的第二张图。新节点(图上node5)仅仅会影响到的原节点node4。即顺时针方向的第一个节点,因此一致性哈希能最大限度的抑制数据的又一次分布。

  上面的例图里我们仅仅使用了4个节点,加入一个新节点影响到了25%左右的数据。这个影响度还是有点大,那有没有办法还能减少点影响了,那么我们能够在一致性哈希算法的基础上进行改进。一致性哈希上的分布节点越多,那么加入和删除一个节点对于整体影响最小。可是现实里我们不一定真的是用那么多节点,那么我们能够添加大量的虚拟节点来进一步抑制数据分布不均衡。

  前文里我将水平拆分的主键设计方案类比分布式缓存技术memcached。事实上水平拆分在数据库技术里也有一个专属的概念代表他,那就是数据的分区,仅仅只是水平拆分的这个分区粒度更大,操作的动静也更大,笔者这里之所以提这个主要是由于写存储瓶颈一定会受到我自己经验和知识的限制,假设有朋友由于看了本文而对存储问题发生了兴趣。那么我这里也能够指明一个学习的方向,这样就能避免一些价值不高的探索过程。让学习的效率会更高点。

  问题二:水平扩展的排序问题。当我们要做水平扩展时候肯定有个这种因素在作怪:数据量太大了。

前文里我说道过海量数据会对读操作带来严重挑战,对于实时系统而言。要对海量数据做实时查询差点儿是件无法完毕的工作,但是现实中我们还是须要这种操作,但是当碰到如此操作我们一般採取抽取部分结果数据的方式来满足查询的实时性。要想让这些少量的数据能让用户惬意,而不会产生太大的业务偏差。那么排序就变变得十分重要了。

  只是这里的排序一定要加上一个范畴,首先我们要明白一点啊。对海量数据进行全排序,而这个全排序还要以实时的要求进行,这个是根本无法完毕的。为什么说无法完毕,由于这些都是在挑战硬盘读写速度,内存读写速度以及CPU的运算能力,假如1Tb的数据上面这三个要素不包含排序操作,读取操作能在10毫秒内完毕。或许海量数据的实时排序才有可能。可是眼下计算机是绝对没有这个能力的。

  那么现实场景下我们是怎样解决海量数据的实时排序问题的呢?为了解决问题我们就必须有点逆向思维的意识了,另辟蹊径的处理排序难题。第一种方式就是缩小须要排序的数据大小,那么数据库的分区技术是一个非常好的手段,除了分区手段外,事实上另一个手段,前面我讲到使用搜索技术能够解决数据库读慢的难题,搜索库本身能够当做一个读库,那么搜索技术是怎么来解决高速读取海量数据的难题了,它的手段是使用索引,索引好比一本书的文件夹。我们想从书里检索我们想要的信息,我们最有效率的方式就是先查询文件夹。找到自己想要看的标题,然后相应页码,把书直接翻到那一页,存储系统索引的本质和书的文件夹一样。仅仅只是计算机领域的索引技术更加的复杂。事实上为数据建立索引,本身就是一个缩小数据范围和大小的一种手段,这点它和分区是类似的。我们事实上能够把索引当做一张数据库的映射表。一般存储系统为了让索引高效以及为了扩展索引查找数据的准确度,存储系统在建立索引的时候还会跟索引建立好排序,那么当用户做实时查询时候,他依据索引字段查找数据,由于索引本身就有良好的排序,那么在查询的过程里就能够免去排序的操作,终于我们就能够高效的获取一个已经排好序的结果集。

  如今我们回到水平拆分海量数据排序的场景。前文里我提到了海量数据做分页实时查询能够採用一种抽样的方式进行。尽管用户的意图是想进行海量数据查询。但是人不可能一下子消化掉所有海量数据的特点。因此我们能够仅仅对海量数据的部分进行操作。但是因为用户的本意是全量数据。我们给出的抽样数据怎样能更加精确点,那么就和我们在分布数据时候分布原则有关系,详细落实的就是主键设计方案了。碰到这种场景就得要求我们的主键具有排序的特点,那么我们就不得不探讨下水平拆分里主键的排序问题了。

  在前文里我提到一种使用固定哈希算法来设计主键的方案,当时提到的限制条件就是主键本身没有排序特性,仅仅有唯一性,因此哈希出来的值是唯一的,这种哈希方式事实上不能保证数据分布时候每台server上落地数据有一个先后的时间顺序,它仅仅能保证在海量数据存储分布式时候各个server近似均匀。因此这种主键设计方案碰到分页查询有排序要求时候事实上是起不到不论什么作用的。因此假设我们想让主键有个先后顺序最好使用递增的数字来表示,可是递增数字的设计方案假设依照我前面的起始数。步长方式就会有一个问题。那就是单库单表的顺序性能够保障。跨库跨表之间的顺序是非常难保证的,这也说明我们对于水平拆分的主键字段对于逻辑表进行全排序也是一件无法完毕的任务。

  那么我们究竟该怎样解决问题了。那么我们仅仅得使用单独的主键生成server了,前文里我以前批评了主键生成server方案,文章发表后有个朋友找到我谈论了下这个问题,他说出了他们计划的一个做法,他们自己研发了一个主键生成server,由于害怕这个server单点故障,他们把它做成了分布式,他们自己设计了一套简单的UUID算法,使得这个算法适合集群的特点。他们打算用zookeeper保证这个集群的可靠性,好了。他们做法里最关键的一点来了,怎样保证主键获取的高效性。他说他们没有让每次生成主键的操作都是直接訪问集群,而是在集群和主键使用者之间做了个代理层,集群也不是频繁生成主键的。而是每次生成一大批主键。这一大批主键值按队列的方式缓存在代理层了,每次主键使用者获取主键时候。队列就消耗一个主键,当然他们的系统还会检查主键使用的比率,当比率到达阀值时候集群就会收到通知。立即開始生成新的一批主键值。然后将这些值追加到代理层队列里,为了保证主键生成的可靠性以及主键生成的连续性,这个主键队列仅仅要收到一次主键请求操作就消费掉这个主键,也不关心这个主键究竟是否真的被正常使用过,当时我还提出了一个自己的疑问,要是代理挂掉了呢?那么集群该怎样再生成主键值了,他说他们的系统没有单点系统。就算是代理层也是分布式的,所以很可靠,就算所有server全挂了。那么这个时候主键生成server集群也不会再反复生成已经生成过的主键值,当然每次生成完主键值后,为了安全起见,主键生成服务会把生成的最大主键值持久化保存。

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

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

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

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

  异议二:假设我们的系统后台数据库都是使用独立server,那么一般都会让最好的server服务于数据库。这个做法本身就说明了数据库的重要性,并且我们对数据库的不论什么分库分表的解决方式都会非常麻烦。非常繁琐甚至非常危急,因此本篇開始提出了假设我们解决瓶颈问题前先考虑下硬件的问题,假设硬件能够解决掉问题,优先採取硬件方案。这就说明我们合理对待存储问题的前提就是让数据库的硬件跟上时代的要求,那么假设有些硬件出现了性能瓶颈,是不是我们忽视了硬件的重要性了?

  异议三:均匀分布数据不只能够合理利用计算资源,它还会给业务操作带来优点。那么我们扩展数据库时候就让各个server本身能力均衡,这个事实上不难的,假设老的server实在太老了,用新server替换掉,尽管会有全库迁移的问题。但是这么粗粒度的数据平移。那但是比不论什么拆分方案的数据迁移难度低的多的。

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

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