数据访问层
5.1.2数据库垂直/水平拆分的困难
随着网站业务的快速发展,数据量和访问量不断上升,数据库的压力越来越大。 更换更好的硬件(Scale Up)是一种解决方案,而且在我们能付得起硬件费用并且没 有到达硬件单机瓶颈时,这也是一个比较简单的解决方案。这有点像我们自己家中 计算机的升级换代。但是数据和访问量的增长很容易就会超过单机的极限,我们需 要找其他的方式来解决问题。
在不靠升级硬件的情况下,能够想到的处理方案就是给现有数据库减压。减压 的思路有三个,一是优化应用,看看是否有不必要的压力给了数据库(应用优化),二是看看有没有其他办法可以降低对数据库的压力,例如引人缓存、加搜索引擎等; 最后一种思路就是把数据库的数据和访问分到多台数据库上,分开支持,这也是我 们的核心思路和逻辑。
数据拆分有两种方式,一个是垂直拆分,一个是水平拆分。垂直拆分就是把一 个数据库中不同业务单元的数据分到不同的数据库里面,水平拆分是根据一定的规 则把同一业务单元的数据拆分到多个数据库中。无论是垂直拆分还是水平拆分,最后的结果都是将原来在一个数据库中的数据拆分到了不同的数据库中。所以原来单机数据库可以支持的特性现在就未必支持了。
垂直拆分会带来如下影响:
•单机的ACID保证被打破了。数据到了多机后,原来在单机通过事务来进行的处理逻辑会受到很大的影响。我们面临的选择是,要么放弃原来的单机事务,修改实现,要么引入分布式事务。
•一些Join操作会变得比较困难,因为数据可能已经在两个数据库中了,所以 不能很方便地利用数据库自身的Join 了,需要应用或者其他方式来解决。
•靠外键去进行约束的场景会受影响。
水平拆分会带来如下影响:
•同样有可能有ACID被打破的情况。
•同样有可能有Join操作被影响的情况。
•靠外键去进行约束的场景会有影响。
•依赖单库的自增序列生成唯一ID会受影响。
•针对单个逻辑意义上的表的查询要跨库了。
可以看到,数据库的拆分给应用带来的影响还是比较明显的,这里面列出的只是其中一部分。在实践中,只要是操作数据被拆分到不同库中的情况,就都会受到影响,例如原来的一些存储过程、触发器等也需要改写才能完成相应的工作了。
接下来,我们分析一下前面提到的具体问题及其应对。
5.1.3单机变为多机后,事务如何处理
事务的支持对业务来说是一个非常重要的特性,数据库软件对单机的ACID的事 务特性的支持是比较到位的,而一旦进行垂直或水平拆分后,我们所要面对的就是 多个数据库的节点了,也就是分布式事务了,这是一个难题。
5.1.3.1 了解分布式事务的知识
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点上。对于传统的单机上的事务,所有的事情都在 这一台机器上完成,而在分布式事务中,会有多个节点参与。
两阶段提交
我们接下来看一下两阶段提交协议,即2PC, Two Phase Commitment Protocol。
之所以称为两阶段提交,是相对于单库的事务提交方式来说的。我们在单库上完成相关的数据操作后,就会直接提交或者回滚,而在分布式系统中,在提交之前增加了准备的阶段,所以称为两阶段提交。
图5-4显示的就是第一阶段提交的情况,可以看到,参与操作的是事务管理器与 两个资源。
图5-5所示的是第二阶段的情况。
此外还会遇到的另外一种情况,就是在准备阶段有一个资源失败,那么在第二 阶段的处理就是回滚所有资源,如图5-6和图5-7所示。
前面对两阶段提交的介绍都是在理想状态下的情况。在实际当中,由于事务管 理器自身的稳定性、可用性的影响,以及网络通信中可能产生的问题,出现的情况会复杂很多。此外,事务管理器在多个资源之间进行协调,它自身要进行很多日志记录的工作。网络上的交互次数的增多以及引人事务管理器的开销,是使用两阶段提交协议使分布式事务的开销增大的两个方面。
因此,在进行垂直拆分或者水平拆分后,需要想清楚是否一定要引人两阶段的 分布式事务,在必要的情况下才建议使用。
这里也列一下两阶段提交协议的参考资料的链接: ’
http://en.wikipedia.org/wiki/Two-phase_commit_protocol
5.1.3.2大型网站一致性的基础理论——CAP/BASE
分布式事务希望在多机环境下可以像单机系统那样做到强一致,这需要付出比较大的代价。而在有些场景下,接收状态并不用时刻保持一致,只要最终一致就行。 我们这节一了解下CAP理论及其对于大型网站的意义。
CAP理论是Eric Brewer在2000年7月份的PODC会议上提出的(可能提出这个理论的时间出乎很多读者的意料),CAP的涵义如下。
- Consistency: all nodes see the same data at the same time,即所有的节点在同~• 时间读到同样的数据。这就是数据上的一致性(用C表示),也就是当数据写 入成功后,所有的节点会同时看到这个新的数据。
- Availability: a guarantee that every request receives a response about whether it was successful or failed,保证无论是成功还是失败,每个请求都能够收到一个 反馈。这就是数据的可用性(用A表示),这里的重点是系统一定要有响应。
- Partition-Tolerance : the system continues to operate despite arbitrary message loss or failure of part of the system,即便系统中有部分问题或者有消息的丢失,但系统仍能够继续运行。这被称为分区容忍性(用P表示),也就是在系统的一部分出现问题时,系统仍能继续工作。
但是,在分布式系统中并不能同时满足上面三项。图5-8更直观地解释了 CAP 理论,就是圆的总面积是不变的,我们不能同时增大C、A、P三者的面积,我们可 以选择其中两个来提升,而另外一个则会受到损失。那么,在进行系统设计和权衡 时,其实就是在选择CA、AP或是CP。
•选择CA,放弃分区容忍性,加强一致性和可用性。这其实就是传统的单机数 据库的选择。
•选择AP,放弃一致性,追求分区容忍性及可用性。这是很多分布式系统在设计时的选择,例如很多NoSQL系统就是如此。
•选择CP,放弃可用性,追求一致性和分区容忍性。这种选择下的可用性会比较低,网络的问题会直接让整个系统不可用。
从上面的分析可以看出,在分布式系统中,我们一般还是选择加强可用性和分区容忍性而牺牲一致性。当然,这里所讲的并不是不关心一致性,而是首先满足A 和P,然后看如何解决C的问题。
我们再来看看BASE模型,BASE涵义如下。
- Basically Available:基本可用,允许分区失败。
- Soft state:软状态,接受一段时间的状态不同步。
- Eventually consistent:最终一致,保证最终数据的状态是一致的。
当我们在分布式系统中选择了 CAP中的A和P后,对于C,我们采用的方式和 策略就是保证最终一致,也就是不保证数据变化后所有节点立刻一致,但是保证它 们最终是一致的。在大型网站中,为了更好地保持扩展性和可用性,一般都不会选 择强一致性,而是采用最终一致的策略来实现
5.1.3.3比两阶段提交更轻量一些的Paxos协议
下面介绍Paxos协议,它是一个比两阶段提交要轻量的保证一致性的协议。
在分布式系统中,节点之间的信息交换有两种方式,一种是通过共享内存共用 一份数据;另一种是通过消息投递来完成信息的传递。而在分布式系统中,通过消 息投递的方式会遇到很多意外的情况,例如网络问题、进程挂掉、机器挂掉、进程 很慢没有响应、进程重启等情况,这就会造成消息重复、一段时间内部不可达等现 象。Paxos协议是帮助我们解决分布式系统中一致性问题的一个方案。
使用Paxos协议有一个前提,那就是不存在拜占庭将军问题。拜占庭位于现在土 耳其的伊斯坦布尔,是东罗马帝国的首都。当时拜占庭罗马帝国国土辽阔,防御敌 人的各个军队都分隔很远,将军与将军之间只能靠信差传消息。在战争时,拜占庭 军队内所有将军和副官必须达成共识,决定出是否有赢的机会才去攻打敌人的阵营。 但是,在军队内可能有叛徒或敌军的间谍,扰乱将军们的决定又扰乱整体军队的秩 序,他们使得最终的决定结果并不代表大多数人的意见。这时,在已知有成员谋反 的情况下,其余忠诚的将军应该如何不受叛徒的影响达成一致的协议?拜占庭将军 问题就此形成。也就是说,拜占庭将军问题是一个没有办法保证可信的通信环境的 问题,Paxos的前提是有一个可信的通信环境,也就是说信息都是准确的,没有被 篡改。
Paxos算法的提出过程是,虚拟了一个叫做Paxos的希腊城邦,并通过议会以决 议的方式介绍Paxos算法。
首先把议员的角色分为了 Proposers、Acceptors和Learners,议员可以身兼数职,
介绍如下。
- Proposers,提出议案者,就是提出议案的角色。
- Acceptors,收到议案后进行判断的角色。Acceptors收到议案后要选择是否接 受(Accept)议案,若议案获得多数Acceptors的接受,则该议案被批准 (Chosen) 0
- Learners,只能“学习”被批准的议案,相当于对通过的议案进行观察的角色。 在Paxos协议中,有两个名词介绍如下。
- Proposal,议案,由Proposers提出,被Acceptors批准或否决。
- Value,决议,议案的内容,每个议案都是由一个{编号,决议}对组成。
在角色划分后,可以更精确地定义问题,如下所述:
•决议(Value)只有在被Proposers提出后才能被批准(未经批准的决议称为 “议案(Proposal)”)。
•在Paxos算法的执行实例中,一次只能批准(Chosen) —个Value。
- Learners只能获得被批准(Chosen)的Value。
对议员来说,每个议员有一个结实耐用的本子和擦不掉的墨水来记录议案,议 员会把表决信息记在本子的背面,本子上的议案永远不会改变,但是背面的信息可 能会被划掉。每个议员必须(也只需要)在本子背面记录如下信息:
- LastTried[p],由议员p试图发起的最后一个议案的编号,如果议员p没有发 起过议案,则记录为负无穷大。
- PreviousVote[p],由议员p投票的所有表决中,编号最大的表决对应的投票, 如果没有投过票则记录为负无穷大。
- NextBallot[p],由议员p发出的所有LastVote(b,v)消息中,表决编号b的最 大值。
基本协议的完整过程如下。
(1)议员p选择一个比LastTried[p]大的表决编号b,设置LastTried[p]的值为b, 然后将NextBallot(b)消息发送给某些议员。
(2) 从p收到一个b大于NextBallot[q]的NextBallot(b)消息后,议员q将 NextBallot[q]设置为b,然后发送一个LastVote(b,v)消息给p,其中v等于PreviousVote [q] (b<NextBallot[q]的 NextBallot(b)消息将被忽略)。
(3) 在某个多数集合Q中的每个成员都收到一个LaStV0te(b,V)消息后,议员p 发起一个编号为b、法定人数集为Q、议案为d的新表决。然后它会给Q中的每一个 牧师发送一个BeginBallot(b,d)消息。
(4) 在收到一个b=NextBallot[q]的BeginBall0t(b,d)消息后,议员q在编号为b 的表决中投出他的一票,设置PreviousVote [p]为这一票,然后向p发送Voted(b,q)
消息。
(5) p收到Q中每一个q的Voted(b,q)消息后(这里Q是表决b的法定人数集 合,b=LastTried[p]),将d (这轮表决的法令)记录到他的本子上,然后发送一条 Success(d)消息给每个q。
(6) 一个议员在接收到SuCCeSS(d)消息后,将决议d写到他的本子上。
从上面的介绍可以看出,Paxos不是那么容易理解的,不过总结一下核心的原则 就是少数服从多数。
大家会发现,如果系统中同时有人提议案的话,可能会出现碰撞失败,然后双 方都需要增加议案的编号再提交的过程。而再次提交可能仍然存在编号冲突,因此 双方需要再增加编号去提交。这就会产生活锁。
解决的办法是在整个集群当中设一个Leader,所有的议案都由他来提,这样就 可以避免这种冲突了。这其实是把提案的工作变为一个单点,而引发的新问题是如 果这个Leader出问题了该如何处理,那就需要再选一个Leader出来。
以上对于Paxos的介绍只是一个非常基础的介绍,读者如果想对此有更深人的了 解,可以阅读 The Part-Time Parliament、Paxos Made Simple、Consensus on Transaction Commit、Cheap Paxos、Fast Paxos等论文,也可以参考维基百科上Paxos的资料, 网址为-http://en.wikipedia.org/wiki/Paxos_(computer_science)0
5.1.4多机的Sequence问题与处理
当转变为水平分库时,原来单库中的Sequence及自增Id的做法需要改变。
在大家比较熟悉的Oracle里,提供对Sequence的支持;在MySQL里,提供 对Auto Increment字段的支持,我们都能很容易地实现一个自增的不重复Id的序 列。在分库分表后,这就成了一个难题。我们可以从下面两个方向来思考和解决 这个问题:
•唯一性
•连续性
如果我们只是考虑Id的唯一性的话,那么可以参考UUID的生成方式,或者根 据自己的业务情况使用各个种子(不同维度的标识,例如IP、MAC、机器名、时间、 本机计数器等因素)来生成唯一的Id。这样生成的Id虽然保证了唯一性,但在整个 分布式系统中的连续性不好。
接下来看看连续性。这里说的连续性是指在整个分布式环境中生成的Id的连续 性。在单机环境中,其实就是一个单点来完成这个任务,在分布式系统中,我们可 以用一个独立的系统来完成这个工作。
这里提供一个实现方案:我们把所有Id集中放在一个地方进行管理,对每个Id 序列独立管理,每台机器使用Id时都从这个Id生成器上取。这里有如下几个关键问 题需要解决。
•性能问题。每次都远程取Id会有资源损耗。一种改进方案是一次取一段Id, 然后缓存在本地,这样就不需要每次都去远程的生成器上取Id 了。但是也会 带来问题:如果应用取了一段Id,正在用时完全宕机了,那么一些Id号就浪 费不可用了。
•生成器的稳定性问题。Id生成器作为一个无状态的集群存在,其可用性要靠 整个集群来保证。
•存储的问题。这确实是需要去考虑的问题,底层存储的选择空间较大,需要 根据不同类型进行对应的容灾方案。下面介绍两种方式。
如图5-9所示,我们在底层使用一个独立的存储来记录每个Id序列当前的最大 值,并控制并发更新,这样一来Id生成器的逻辑就很简单了。
一种改变是直接把id生成器舍掉,把相关的逻辑放到需要生成Id的应用本身就 行了。也就是说,去掉应用和存储之间的这个独立部署的生成器,而在每个应用上 完成生成器要做的工作,即读取可用的Id或者Id段,然后给应用的请求使用,如图 5-10所示。
不过因为图5-10中的方式没有中心的控制节点,并且我们不希望生成器之间还 有通信(这会使系统非常复杂),因此数据的Id并不是严格按照进入数据库的顺序而 增大的,在管理上也需要有额外的功能,这些是需要权衡之处。
5.1.5应对多机的数据查询
5.1.5.1 跨库 Join
解决了 Sequence的问题,我们接下来看看Join的问题。
在分库后,如果需要Join的数据还在一个库里面,那就可以直接进行Jion操作。 例如,我们根据用户的id进行用户相关信息的分库,那么如果查询某个用户在不同 表中的一些关联信息,还是可以进行Join操作的。如果需要Join的数据已经分布 在多个库中了,那就需要完成跨库的Join操作,这会比较麻烦,解决的思路有如下 几种。
•在应用层把原来数据库的Join操作分成多次的数据库操作。举个例子,我们 有用户基本信息的数据表,也有用户出售的商品的信息表,需求是查出来登 记手机号为138XXXXXXXX的用户在售的商品总数。这在单库时用一个SQL 的Join就解决了,而如果商品信息与用户信息分开了,我们就需要先在应用 层根据手机号找到用户Id,然后再根据用户Id找到相关的商品总数。
•数据冗余,也就是对一些常用信息进行冗余,这样就可以把原来需要Join的 操作变为单表查询。这需要结合具体业务场景。
•借助外部系统(例如搜索引擎)解决一些跨库的问题。
5.1.5.2外键约束
外键约束的问题比较难解决,不能完全依赖数据库本身来完成之前的功能了。 如果要对分库后的单库做外键约束,就要求分库后每个单库的数据是内聚的,否则 就只能靠应用层的判断、容错等方式了。
5.1.5.3跨库查询的问题及解决
1.数据库分库分表的演化
我们接下来看一下合并查询的问题。合并查询问题产生的根源在于我们在进行 水平分库分表时,把一张逻辑上的表分成了多张物理上的表。例如,我们有一个用 户信息表,根据用户Id进行分库分表后,物理上就会分成很多用户信息表,如图5-11 所示。
从图5-11可以看到,最初用户信息保存在一个数据库中(最左边的部分);然后 进行了分库,变为了两个库(中间的部分),这两个库存储不同的用户信息,二者加 起来等于最初的那个库;然后又进行了分表(右边的部分),也就是在每个库里面 又把数据拆为了两张表,这两个库中四张表的数据加在一起等于最左边那个库中的 数据。
从逻辑概念上来说,用户信息应该放在一起存储,然而随着数据量、访问量的 上升,需要经历分库分表,此时用户信息在物理上是分布在多个数据库的多张表中 的,也就是说一张逻辑上的表对应了多张物理上的表,在应用中,对这张逻辑表的 查询就需要做跨库跨表的合并了。这个场景和前面的跨库Join还不同,跨库join是 在不同的逻辑表之间的Join,在分库后这些Join可能需要跨多个数据库,而我们现 在看到的合并查询是针对一个逻辑表的查询操作,但因为物理上分到了多个库多个 表,因而产生了数据的合并查询。
2.从具体例子看分库分表后查询的问题
来看一个具体的例子,假设我们有下面这样一个用户表(如表5-1所示),我们 需要找到某一(或某些)省份中符合一定年龄范围的用户。
在单表时,这是一个非常普通的查询,而分库分表后,我们可能会遇到一些麻 烦,具体取决于分库分表的方式。
如果是按照地域分库分表,就是说同一省份的用户信息分在同一数据库的同一 个表中,那么这个问题就变为一个单库单表的问题了。如图5-12所示,我们按省进 行了分库,单个省的所有用户信息都在同一个库中,所以在查询时,确定了省就确 定了唯一对应的数据库和表,就如同没有进行分库分表的情况。
如果在这个基础上我们又对库进行了分表,该怎么办?如图5-13所示。这时, 如果我们要查询某一个省的用户信息,那么还是与单库单表的情况相同;如果查询 多个省的用户信息,那么就可能要跨库(例如province in(‘浙江’,‘上海’)),也可能在 一个库中跨表(例如province in(‘浙江‘河南’))。
在这样的情况下,就需要对查询结果在应用上进行合并,这相对比较简单,但是在一些场景下需要进行较为复杂的操作,介绍如下。
(1)排序,即多个来源的数据查询出来后,在应用层进行排序的工作。如果从 数据库中查询出的数据是已经排好序的,那么在应用层要进行的就是对多路的归并 排序;如果查询出的数据未排序,就要进行一个全排序。
(2)函数处理,即使用Max、Min、Sum、Count等函数对多个数据来源的值进行相应的函数处理。
(3)求平均值,从多个数据来源进行查询时,需要把SQL改为查询Sum和 Count,然后对多个数据来源的Sum求和、Count求和后,计算平均值,这是需要 注意的地方。
(4)非排序分页,这需要看具体实现所采取的策略,是同等步长地在多个数据源上分页处理,还是同等比例地分页处理。同等步长的意思是,分页的每页中, 来自不同数据源的记录数是一样的;同等比例的意思是,分页的每页中,来自不 同数据源的数据数占这个数据源符合条件的数据总数的比例是一样的。举例说明 如下。
如图5-14所示,假设我们有两个数据源,符合条件的记录数分别是16条和8 条。如果进行每页4条数据的分页,则前面四页中会包含两个数据来源的各两条数 据,到第五页和第六页时,就只包含第一个数据源中的数据了。这就是我们所说的 等步长地从不同数据源中获取数据。图5-14中每个小方格代表了一条数据,其中的 数字代表该条信息在第几页结果中出现。
我们再来看一下等比例处理的情况,如图5-15所示。数据源与图5-14中的相同, 假设要进行每页6条数据的分页,那么第一页的6条数据是从数据源1取4条,从 数据源2取2条,每次都用这样的方式,到第四页时,刚好把两个数据源中的数据 都取完了。可以看到,每次取数据时,从数据源1和数据源2取出的数量不同,但 是占各自数据源总量的比例是相同的,因此用相同的次数完成了数据的获取。
(5)排序后分页,这是把排序和分页放在一起的情况,也是最复杂的情况,最 后需要呈现的结果是数据按照某些条件排序并进行分页显示。我们的数据是来自不 同数据源的,因此必须把足够的数据返回给应用,才能得到正确的结果,复杂之处 就在于将足够的数据返给应用。
来看一下图5-16,两个数据源中符合条件的数据已经排序好了,假设每个分页 需要4条数据,那么从图中可以看到,最终的每一页都是由两个数据源中的各2条 数据组成。那就是说,我们每一页都从两个数据源中各选择2条数据就行了。不过, 这个方法是不正确的,只是这个特殊的例子碰巧生效而已。
看一下图5-17的例子,排序合并后的第一页是由来自两个数据源的各2条数据 组成;排序合并后的第二页是全部来自数据源1的4条数据;第三页则是由两个数 据源的各2条数据组成;而第四页是由来自数据源1的1条数据和数据源2的3条 数据组成。因此,我们要从数据源中取足够多的数据才能保证结果的正确。
在取第一页结果时,应该考虑的最极端情况是最终合并后的结果可能都来自一 个数据源,所以我们需要从每个数据源取足一页的数据。例如,对于图5-17的情况, 第一页应该从每个数据源取4条数据,然后把这8条数据在应用中进行归并排序。
对于第二页,不是把每个数据源的第二页取回来进行合并排序,而是需要把每个数 据源的前两页也就是前8条数据都取回来进行归并排序,才能得到正确的结果。如 果要取第100页的数据,就要从每个数据源取前100页数据进行归并排序,才能得 到正确的结果。也就是说越往后翻页,承受的负担越重。
从上面可以看出,排序分页是合并操作中最复杂的情况了,因此,在访问量很 大的系统中,我们应该尽量避免这种方式,尤其是排序后需要翻很多页的情况。
- 一致性哈希算法带来的好处
一致性哈希(Consistent Hashing),是M丨T的Karger及其合作者在1997年发表 的学术论文中提出的,很多做分布式系统的读者是在Amazon的dynamo论文中了解 到一致性哈希的。图5-24展示了一致性哈希的含义。
一致性哈希所带来的最大变化是把节点对应的哈希值变为了一个范围,而不再 是离散的。在一致性哈希中,我们会把整个哈希值的范围定义得非常大,然后把这 个范围分配给现有的节点。如果有节点加人,那么这个新节点会从原有的某个节点 上分管一部分范围的哈希值;如果有节点退出,那么这个节点原来管理的哈希值会 给它的下一个节点来管理。假设哈希值范围是从0到100,共有四个节点,那么它们 管理的范围分别是[0,25)、[25,50)、[50,75)、[75,100]。如果第二个节点退出,那么 剩下节点管理的范围就变为[0,25)、[25,75)、[75,100],可以看到,第一个和第四个 节点管理的数据没影响,而第三个节点原来所管理的数据也没有影响,只需要把第 二个节点负责的数据接管过来就行了。如果是增加一个节点,例如在第二个和第三 个节点之间增加一个,则这五个节点所管理的范围变为[0,25)、[25,50)、[50,63)、 [63,75), [75,100],可以看到,第一个、第二个、第四个节点没有受影响,第三个节 点有部分数据也没受影响,另一部分数据要给新增的节点来管理。
读者可能从增加节点和减少节点的例子中觉察到了问题:新增一个节点时,除 了新增的节点外,只有一个节点受影响,这个新增节点和受影响的节点的负载是明 显比其他节点低的;减少一个节点时,除了减去的节点外,只有一个节点受影响, 它要承担自己原来的和减去的节点的工作,压力明显比其他节点要高。这似乎要增 加一倍节点或减去一半节点才能保持各个节点的负载均衡。如果真是这样,一致性 哈希的优势就不明显了。
- 虚拟节点对一致性哈希的改进
为了应对上述问题,我们引入虚拟节点的概念。即4个物理节点可以变为很多 个虚拟节点,每个虚拟节点支持连续的哈希环上的一段。而这时如果加人一个物理 节点,就会相应加入很多虚拟节点,这些新的虚拟节点是相对均匀地插入到整个哈 希环上的,这样,就可以很好地分担现有物理节点的压力了;如果减少一个物理节 点,对应的很多虚拟节点就会失效,这样,就会有很多剩余的虚拟节点来承担之前 虚拟节点的工作,但是对于物理节点来说,增加的负载相对是均衡的。所以可以通 过一个物理节点对应非常多的虚拟节点,并且同一个物理节点的虚拟节点尽量均匀 分布的方式来解决增加或减少节点时负载不均衡的问题。
5.2.4读写分离的挑战和应对
接下来我们看一下读写分离部分会遇到的挑战和应对。
图5-32所示是一个常见的应用使用读写分离的场景。通过读写分离的方案,可 以分担主库(Master)的读的压力。这里面存在一个数据复制的问题,也就是把主库 的数据复制到备库(Slave)去。
5.2.4.1主库从库非对称的场景
1.数据结构相同,多从库对应一主库的场景
读者对MySQL都比较熟悉,通过MySQL的Replication可以解决复制的问题, 并且延迟也相对较小。在多从库对应一主库的情况下,业务应用只要根据自身的业 务特点把对数据延迟不太敏感的读切换到备库来进行就可以了。可是,如果我们遇 到的是图5-33所示的情况呢?我们的Slave该如何做?数据复制又该如何做呢?
首先来看Slave。从成本上来说,Slave采用PC Server和MySQL的方案是比较 划算的。那么对于一个主库,需要多台采用MySQL的PC Server来对应,每台PC Server对应原来Master中的一部分数据,也就是进行了分库,如图5-34所示。
从图5-35可以看到,应用通过数据层访问数据库,通过消息系统就数据库的更 新送出消息通知,数据同步服务器获得消息通知后会进行数据的复制工作。分库规 则配置则负责在读数据及数据同步服务器更新分库时让数据层知道分库规则。数据 同步服务器和DB主库的交互主要是根据被修改或新增的数据主键来获取内容,采用 的是行复制的方式。
可以说这是一个不优雅但是能够解决问题的方式。比较优雅的方式是基于数据 库的日志来进行数据的复制。
2.主/备库分库方式不同的数据复制
数据库复制在读写分离中是一个比较关键的任务。一般情况下进行的是对称的 复制,也就是镜像,但是也会有一些场景进行非对称复制。这里的非对称复制是指 源数据和目标数据不是镜像关系,也指源数据库和目标数据库是不同的实现。
这是一个虚拟的订单的例子。在主库中,我们根据买家id进行了分库,把所有 买家的订单分到了 4个库中,这保证了一个买家查询自己的交易记录时都是在一个 数据库上查询的,不过卖家的查询就可能跨多个库了。我们可以做一组备库,在其 中按照卖家id进行分库,这样卖家从备库上查询自己的订单时就都是在一个数据库 中了。那么,这就需要我们完成这个非对称的复制,需要控制数据的分发,而不是 简单地进行镜像复制。
5.2.4.2如何做到数据平滑迁移
我们接下来看看数据库的平滑迁移。对于没有状态的应用,扩容和缩容是比较 容易的。而对于数据库,扩容和缩容会涉及数据的迁移。如果接受完全停机的扩容 或者缩容,就会比较容易处理,停机后进行数据迁移,然后校验并且恢复系统就可
以了;但是如果不能接受长时间的停机,那该怎么办呢?
对数据库做平滑迁移的最大挑战是,在迁移的过程中又会有数据的变化。可以 考虑的方案是,在开始进行数据迁移时,记录增量的日志,在迁移结束后,再对增 量的变化进行处理。在最后,可以把要被迁移的数据的写暂停,保证增量日志都处 理完毕后,再切换规则,放开所有的写,完成迁移工作。
我们来看一种简单的情况,假设数据库中就只有一个数据表,格式如表5-3 所示。
我们希望根据id取模把这个表划分在两个数据库中,也就是id mod 2为0的还 在原来的数据库表中,而id mod 2为1的分到新的数据库表中。假设我们只有4条 数据,我们来看一下前面描述的过程。
(1)首先我们确定要开始扩容,并且开始记录数据库的数据变更的增量日志, 如图5-38所示。
这时增量日志和新库表都还是空的。我们用id来标识记录,用v标识版本号(这 不是数据库表的业务字段,而是我们为了讲清楚平滑迁移过程而加上的标志)。
(2)接下来,数据开始复制到新库表,并且也有更新进来。可能会形成如图5-39 所示的局面。
可以看到,id=l和id=3的数据已经在新库表中了,但是id=l的记录版本是旧的, 而id=3的记录版本已经是新的了。
当我们把源库表中的数据全部复制到新库表中后,一定会出现的情况是,由于 在复制的过程中会有变化,所以新库表中的数据不全是最新的数据。
(3)当全量迁移结束后,我们把增量日志中的数据也进行迁移,如图5-40所示。
可以发现,这个做法并不能够保证新库表的数据和源库表的数据一定是一致的, 因为我们处理增量日志时,还会有新的增量日志进来,这是一个逐渐收敛的过程。
(4) 然后我们进行数据比对,这时可能会有新库数据和源库数据不同的情况, 把它们记录下来。
(5) 接着我们停止源数据库中对于要迁移走的数据的写操作,然后进行增量日 志的处理,以使得新库表的数据是新的。
(6)最后更新路由规则,所有新数据的读或写就到了新库表,这样就完成了整 个迁移过程。
有了平滑迁移的支持,我们在进行数据库扩容和缩容时就会相对标准化和容易 了,否则恐怕每次的扩容都要变成一个项目才能完成了。