写点什么

再谈大型网站技术应用——下篇

用户头像
Jerry Tse
关注
发布于: 2020 年 07 月 15 日

本文可以当做《浅谈大型网站技术应用及适用场景》的细化版,并且是《再谈大型网站技术应用——上篇》的延续。



1. 分布式数据库

上篇文章介绍过MySQL主从复制和主主复制,但是这两种复制方式只能提升读取数据的性能,无法提高写入数据性能,更无法提高数据存储能力。如果想要进一步提升数据库写数据性能和数据存储能力,我们需要数据库分片(分库分表)。



1.1 分片定义

将原先一个表中的数据拆分到不同数据库中不同的表存储

拆分后,原先有一台机器处理数据写入和存储,现在有多台机器同时处理。数据写入性能和存储能力成倍提升。

1.2 实现方式

我们如何将一张表的数据分拆,有两个关键操作:

  • 写入的时候如何判断数据写入哪个数据库的哪张表

  • 读取数据的时候从哪个数据库的哪张表获取数据

上面所述都依赖分片路由算法,也就是分片策略的支持。



1.2.1 分片策略

1. 连续分片

连续分片是根据数据某种自然增长趋势,将数据按照一定范围分布到不同的库表中的策略。

例如我们可以根据主键id范围分片

id>=0 and id<1000 -> schema_0.table_0

id>=1000 and id< 2000 -> schema_1.table_1

或者根据日期范围分片

createTime = '2020-1' -> schema_0.table_0

createTime = '2020-2'-> schema_1.table_1



优点:

  • 扩容无需迁移数据

  • 分片规则范围内范围查询性能较好(范恩内一张表就能返回所有数据)

缺点:

  • 存在数据不均匀性(一年十二个月数据量不均匀)

  • 无法有效分散热点数据负载(三月份数据访问量较大)



2. 离散分片

离散分片是为了解决连续分片数据不均匀和热点问题的,思路就是将数据尽量均衡的分散的分散于各个数据节点中。解决方法通过余数哈希和一致性哈希作为路由算法。

余数哈希

我们通过对主键id取哈希然后在取模,判断数据节点

hash(id)%3=0 -> schema_0.table_0

hash(id)%3=1 -> schema_1.table_1

hash(id)%3=2 -> schema_2.table_2



优点:

  • 数据均匀的分布在各个存储节点中,各个节点均衡分担读写压力

缺点:

  • 扩容需要迁移数据

  • 公式被除数(节点总数)造成结果变化

  • 多向迁移,各个节点间相互迁移数据



一致性哈希

关于一致性哈希算法可以参考这篇文章《一致性哈希算法简单实现》,在此不再赘述。



优点:

  • 数据均匀的分布在各个存储节点中,各个节点均衡分担读写压力(在引入虚拟节点的情况下)

  • 相较于余数哈希,扩容相对容易

  • 迁移数据量相对较小,之前已环内部分数据

  • 单向迁移,只需从已有节点向新增节点迁移数据部分数据

扩容数据迁移问题

无论是余数哈希或者一致性哈希,均存在扩容后数据迁移问题,增加了扩容的执行难度。有没有无需数据迁移的扩容方案呢?

在余数哈希中,我们是通过hash(key)%节点数量来确定key存在于哪个节点,如果我们固定了节点数量这个被除数,那公式的结果也就不会变化了。但是如果我们固定了节点数量,又如何增加节点呢?这里我们借鉴了一致性哈希的虚拟节点概念。我们固定的是虚拟节点数量,虚拟节点分布在物理节点上,并且有对应关系。

使用MySQL举例:

扩容前:

我们预先规定有六个数据库实例(schema),部署于两台机器上。



一个MySQL数据库中可以有多个schema,所以一台物理机器可以部署多个schema。

物理节点扩容后:

我们新增了一台物理节点Node3,将schema2和schema6迁入Node3。因为数据到schema的映射没有变化,这有schema到物理节点映射的有变化,我们只需要改变这个映射关系即可,通常就是改变MySQL的客户端的连接地址配置。

这里需要注意,我们还是将schema3和schema6的数据从Node1迁入到Node3,所以严格意义上说我们还是需要迁移数。但是不同于普通的余数哈希,我们不需要在不同的节点间范围迁移数据。我们只需将以后数据整体迁移新节点即可,通过主从复制的方式大大降低了数据迁移的复杂性。

扩容的步骤:

  1. 以Node3为从库,Node1和Node2为主库,迁移schema3和schema6中数据。

  2. 待数据迁移完毕,修改客户端数据库连接配置。



在这个方案中,我们将schema当做虚拟节点,扩容的操作仅需要将虚拟节点重新分配到新的物理节点上,这样数据到schema的关系不会改变,只改变schema到物理节点的关系即可。这个方案将一致性哈希和余数哈希相结合。



方案优点:

  • 路由算法简单实现

  • 数据迁移相对简单,可以整体迁移

方案缺点:

  • 需要提前规划好数据库schema总数,后期不能修改



1.2.3 硬编码实现

上面我们介绍的分片策略就是确定数据落在那个节点或者,从那个节点读取数据的逻辑方式。

我们可以在插入或查询数据库代码中自己编写相应的逻辑判断节点选择。但是这样通常将分片逻辑侵入到具体业务中,通常不采用这种方式。



1.2.3 中间件实现

通常我们使用分布式数据库中间件来屏蔽分片的复杂性,让操作分片表向操作单张表一样。



Jar模式(去中心化)

中间件以Jar包的形式被引入到业务代码中,在JDBC提供额外的服务器,我们可以理解为增强版的JDBC驱动。



优点:

  • 完全代理数据库操作,业务无感知

  • 无需额外中间件部署和部署即可实现分布式数据库逻辑

  • 性能损耗较小

  • 支持多数据库

缺点:

  • 相对代理数据库连接损耗略高

  • 需要针对每一种语言开发类包



代理模式(中心化)

分布式数据库中间件作为数据库代理存在,客户端通过访问代理来访问数据库



优点:

  • 客户端程序对于分库分表逻辑完全透明(无需关心任何分片逻辑配置)

  • 数据库链接消耗较低

  • 多语言支持



缺点:

  • 代理节点需要保证高可用(主备)

  • 代理节点容易成为性能瓶颈

  • 需要针对每一中数据库开发的代理服务



Jar包模式无中心化架构,适用于Java开发的高性能的轻量级OLTP应用(前端用户应用场景);代理模式提供异构语言的支持,适用于OLAP应用以及对分片数据库进行管理和运维的场景(后端管理应用场景)。



1.3 数据分片后的问题

  • 无法使用夸库的jion

  • 本地事务变成分布式事务

  • 聚合函数和分页算法执行效率不高

1.4 数据库部署演化过程

1.4.1 单台数据库



一开始我们的网站通常为一个单体应用,一台数据库部署结构



1.4.2 主主、主从复制



随着网站访问量增大,我们采用主从复制完成读写分离提升读操作性能,使用主主复制保证高可用。



1.4.3 业务分库

随着网站访问量进一步增加,单一数据库已经无法满足我们需求,我们根据业务将数据分拆到多个数据库中部署。



通常业务分库是分库分表的前提条件,在分库分表前都需要先进行业务分库。

1.4.4 分库分表

随着数据量进一步增大,单表存储数据量已经超过数据库存储极限,我们需要将单表拆分到不同数据库的不同表中。



以上为常规网站演化数据库部署演化步骤,但是如果我们在设计初期就可以预估到某些表数据量未来有较大增长,可以提前规划业务拆分和分库分表。



2. ‍ACID特性

‍事务是关系型数据库重要特性及功能,ACID是保证事务可靠必须满足的四个特性:

  • ‍原子性:一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • ‍一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。

  • ‍隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)

  • ‍持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

‍(注:以上内容参考维基百科中文站)



其他的相对好理解,单独说一下一致性,一致性和原子性表达的同一个概念,只不过原子性强调的是事务中执行的方法,当事务结束的时候,要么所有方法都执行了,要么一条也没有执行。一致性强调的事务对于数据库状态的影响。



事务开始之前数据库为状态1,事务结束之后,要么事务提交成功数据库变为状态2,要么事务回滚数据库又恢复到状态1。就是事务开始和结束两种状态,不会出现事务执行过程中的第三种状态,这样就保证了数据库的完整性。



这里的一致性和下面我们要介绍的CAP中的一致性概念是不同的。后者是描述多节点数据内容保持一致。这里主要强调事务前后数据库数据完整,无中间状态。显然在多数据库节点情况下为了保证各个数据库节点数据完整性,需要一个事务管理器的角色协调各个节点的SQL执行或回滚,这就是分布式事务。个人感觉ACID中的一致性比CAP中的一致性约束还要强烈。



通常我们认为关系型数据库支持事务,满足ACID特性,所以是数据强一致性的代表。而为了在多数据库节点情况下支持分布式事务,采用各个节点协调,两段式提交等手段,保证了数据一致性但是会极大的影响系统性能和可用性,这就要引出我们下面的CAP原理



3. CAP原理

3.1 一致性

一致性是说,每次读取的数据都应该是最近写入(最新)的数据或者不响应或返回一个错误。不能返回过期的数据。一致性要保证各个节点中数据在任何时间都是相同的。

3.2 可用性

可用性是说,每次请求都应该得到一个相应,而不是返回错误或者失去响应(超时),不过这个响应不需要保证数据是最近写入的。可用性要保证系统在任何时间都可以给用户响应结果。



一致性和可用性互相矛盾。一个是可以没有响应或者返回错误,但是如果返回响应必须保证数据最新。另一个是必须有响应,但是数据可以为旧数据。

3.3 分区耐受性

分区耐受性是说,即使因为网络原因,部分服务器节点之间消息丢失或者延迟,系统依然应该是可以操作的。



3.4 原理说明

三个属性只能满足两条。通常对于一个分布式系统而言,网络失效一定会发生,所以,分区耐受性是必须要保证的,因此只能在可用性和一致性上二选一。

当节点间网络不可用时,如果我们选择了一致性,系统就可能返回错误或者不响应,即系统不可用。如果选择了可用性,系统总是有响应,但是响应的数据不一定是最新。



3.5 场景示例

我们用MySQL主主复制的例子说明CAP原理:

对于单机数据库而言,因为只有一个节点,一旦宕机系统整体不可用,所以我们提升节点数,组成数据库集群对外提供服务。

主主复制提升为两个节点,一主一备。因为是两台服务器,必须保证分区耐受性。

  • 主节点宕机,主节点消息因为网络延时没有同步到备用节点,启用备用节点,集群从故障中恢复,客户端得到了响应数据,但是数据有可能不能最新的,这样就满足了可用性,但是不满足一致性。

  • 主节点数据未丢失,最终同步到从节点

  • 主节点数据丢失,永远不会同步从节点

  • 主节点宕机,主节点消息因为网络延时没有同步到备用节点,不启动备用节点,集群无法从故障中恢复,客户端得到的响应异常。这样就满足的一致性,但是不满足可用性。



我们之所以使用主主复制,就是为了避免数据库单点,保证数据库高可用。所以通常情况下我们会选择当主节点宕机的时候启用备用节点,忽略特殊场景下的数据一致性的可能性,保证集群可用性。



当主节点宕机的时候,从节点代替主节点工作之前这一段时间,这一段时间系统依然是不可用的。

所以可用性和一致性是分布式系统的两个极端状态,在满足分区容错性的前提下,我们不可能通过完全放弃一致性而获得绝对的可用性(业务也不允许我们这么做),反之依然,我们只是通过设计偏向于满足可用性和一致性中间某种状态, 偏向于那种状态多一点,系统的违背这种状态的发生概率就小一些。。例如主主复制的例子,大部分情况下系统都是可用的,只有主备切换的一瞬间系统才会出现不可用的情况。



4. BASE理论

BASE理论是对CAP理论的延伸,也是我们在设计大型的分布式系统架构中遵循的原则。这个原则核心思想也是将可用性和一致性要求降低,使其在分布式系统中达到一个平衡。



  • Basically Available(基本可用)系统在出现不可用预知的故障的时候,允许丧失部分可用享,如响应时间上的损失和功能上的损失。



  • Soft state(软状态),是指允许系统中的数据库存在中间状态,并认为该中间状态的存在不会影响系统整体可用性,即允许系统在不同节点的数据副本之间进行数据同步过程中存在延时。



  • Eventually Consistent(最终一致性)是指系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,因此最终一致性的本质是需要系统保证数据能够达到一致,而不需要实时保证系统数据的强一致性。



5. CAP实际场景分析

我们以有状态服务即数据库部署形式来说明CAP原理实际场景中的应用。

5.1 单实例情况

因为只有一个节点,没有网络分区的情况,没有数据延时,所有数据一致性可以保证。但是有单点问题,一个节点宕机,系统不可用,所以无论是关系型数据库或菲关系型数据库,只要是单实例形式部署,都满足CP。

5.2 多副本情况

单一节点就有单点问题,我们需要多节点多副本来保证系统可用性,一旦一个副本发生问题,我们可以从另一个副本中获取我们要读取的数据。但是涉及到多副本的写入,不同的处理策略会得到不同的可用性和一致性偏向。

副本写入三种情况

  • 同步写入:对于写操作,必须等待所有副本都写入返回,一旦网络异常,写入操作就会返回异常,写操作不可用了,牺牲了一定的额系统可用性。

  • 异步写入:无需等待写操作实际完成,直接返回,各节点异步更新写入结果。这样一旦网络异常,数据将不一致,牺牲了一定的系统一致性。

  • 折中方案:以上两种方案都过于绝对,现实中写入操作往往不会等待所有节点都完成,也不会一个节点也不等,我们会通过写入副本数的和读取副本数的来调节可用性和一致性,以达到一个中间状态。

副本写入和读取折中方案分析

我们定义一下三个变量:

N:同一份数据的副本个数

W:写操作需要确保成功的副本个数

R:读操作需要读取的副本个数

  1. 当W+R>N时,偏向一致性。

我们举例说明一下:

我们都定义三个副本,写入副本和读取副本不同:

  • 情况一,写入的时候等待两个副本成功,由于同步延时,数据有可能是两新一旧的组合。读取的时候读取两个节点如果相同直接返回,如果不同通过时间戳确定哪个是最新数据再返回()。

  • 情况二,写入的时候等待一个副本成功,由于同步延时,数据有可能是一新两旧的组合。读取的时候读取三个节点,如果相同直接返回,如果不同通过时间戳确定哪个是最新数据在返回。(写数据性能最高)

  • 情况三,写入的时候等待三个副本成功,读取的时候读取一个节点,因为一共就三个分布,所以读取的数据一定是最新的。

所以,当W+R>N时,数据的一致性都是可以得到保证的,系统为强一致性。

注:Cassandra就采用相同机制保证一致性,并且如果数据不相同会进行写修复操作



  1. 当W+R<=N时,偏向可用性。

我们同样举例说明:

我们都有三个副本,写入副本和读取副本不同:

  • 情况一,写入的时候等待两个副本成功,由于同步延时,数据有可能是两新一旧的组合。读取一个节点时有可能读取到旧数据。致使数据不一致。

  • 情况二,写入的时候等待一个副本成功,由于同步延时,数据有可能是一新两旧的组合。读取的时候读取两个节点。如果相同直接返回,如果不同通过时间戳确定哪个是最新数据在返回。但是可能同时读取到两个旧节点数据。致使数据不一致。

所以,当W+R<=N时,数据的一致性无法得到保证,系统为弱一致性。



以上就是类Dynamo系统用于控制分布式存储系统中的一致性级别的策略,我们可以借鉴参考。



参数合理选择

实际使用中我们要合理选择各种参数,权衡利弊。对于分布式存储服务,副本越多数据越安全,但是过多的副本,致使我们在写入和读取的时候访问过多的节点,对性能有一定影响。所以我们需要根据场景综合考虑。



通过上面的例子,再一次说明在分布式系统中可用性和一致性不是零和博弈,他们是你中有我我中有你的关系。我们需要通过合理的设计保证分布式系统的可用性和一致性相对平衡。



6. 分布式系统关键点

不同于单点应用,分布式集群采用冗余(多节点和多数据副本)的策略,所以会遇到新的问题:

  • 节点选择问题

  • 节点伸缩问题

  • 多节点可用性问题

  • 多节点数据一致性问题

以上即是分布式问题的关键问题,下面介绍如何解决这些问题,解决的方式就构成了分布式系统的关键点。

6.1 路由算法

分布式系统都是多个节点共同对外提供服务,所以需要一个路由选择算法判断请求到哪个节点提供服务。路由算法根据场景分为无状态和有状态两种。

无状态场景

如果各个节点部署的都是无状态服务,那么这就是一个无状态场景,例如无状态服务负载均衡器场景。无状态场景相对简单,路由算法只需保证均衡性,通常使用随机、轮询等各种简单方式处理。

有状态场景

像缓存或者数据库节点通常不是无状态的,这就是有状态场景。在这种场景下,负载均衡算法除了均衡性,还需要保证同一个请求落到同一个节点,这就不是有状态场景路由算法能解决的了。

通常我们采用余数哈希或者一致性哈希解决。



6.2 节点伸缩方式

节点可伸缩是分布式系统的灵活性和扩展性的重要体现。节点伸缩也分为无状态和有状态两种。

无状态场景

在无状态场景下,我们可以任意增减的节点,对系统没有任何影响。

有状态场景

在有状态情况下增减节点就没有这么简单。

  • 数据库主从复制场景,我们新增从节点就涉及到主节点数据同步复制。

  • 分布式数据库新增节点还会遇到节点间数迁移问题。

  • NoSQL数据库新增节点也会遇到数据迁移和再平衡问题,只不过通常有NoSQL自己的机制处理,无需人工干预。

节点伸缩往往和路由算法有很大关系,路由算法直接影响节点伸缩时数据迁移的复杂性和困难度。例如余数哈希路由算法的数据数据迁移难度远远大于一致性哈希。所以选择一个合理的路由算法不仅有助于访问请求和数据存储的均衡性,也有助于后续系统扩容场景。



6.3 可用性、一致性权衡

高可用系统设计的原则之一就是避免单点。如何避免单点,核心思路就是冗余。有了冗余,我们就可以设计失效转移(failover)。冗余和失效转移是高可用系统设计核心要素。

无状态服务场景

对于无状态场景通常使用另一个服务节点作为主节点的备份节点,就是主备模式。我们只需要关注的是哪个节点作为主节点对外提供服务,哪个节点作为备份服务待命,当主服务有问题的时候切换备份服务代替主服务对外提供服务器,然后恢复有问题的主服务节点即可。

有状态数据存储场景

对于有状态服务冗余有两个含义:

  • 服务冗余,保证服务高可用

  • 数据冗余、保证数据可靠性

服务冗余通常采用主备模式,数据冗余采用多副本模式。

MySQL主主、主从复制,Redis主从复制,MongoDB副本集,Cassandra的数据复制都是采用类似策略。



因为存在多个数据副本,如前文5.2内容所述,会出现可用性和一致性相互取舍问题。这是一个有状态分布式服务(数据存储)设计时必须要考虑的问题。

此外,为了保证系统高可用,故障恢复也需要我们考虑。



7. 总结

大型网站所采用的分布式系统的复杂性远远大于单体应用,但是带来性能和能力上的提升也是巨大的,可以说没有分布式系统就没有当今的互联网天下。

所以分布式应用是我们不得不去学习掌握的技术。需要注意的是不能一上来陷入到各种具体的技术和实现中,需要梳理出整体架构,掌握关键点,接下来深入了解知识点,最后将知识点融会贯通,也就是总分总的学习过程。

我们通过《浅谈大型网站技术应用及适用场景》《再谈大型网站技术应用——上篇》 和本篇文章一共三篇文章梳理了分布式系统架构的知识体系,希望对你有所帮助。



发布于: 2020 年 07 月 15 日阅读数: 60
用户头像

Jerry Tse

关注

还未添加个人签名 2018.11.02 加入

还未添加个人简介

评论

发布
暂无评论
再谈大型网站技术应用——下篇