分布式架构基础

1-分布式系统

分布式系统是其组件分布在连网的计算机上,组件之间通过传递消息进行通信和动作协调的系统。一个标准的分布式系统在没有任何特定业务逻辑约束的情况下,一般有以下几个特征:

  • 分布性:一个分布式系统中的计算机在空间部署上是可以随意分布的,这些计算机可能处于不同的机柜上,不同的城市的机房中,或者是世界上任何一个角落。同时,机器的分布情况也会随时变动;
  • 并发性:在一个计算机网络中,程序运行过程中的并发性操作是非常常见的行为,例如同一个分布式系统的多个节点,可能会并发地操作一些共享的资源,诸如数据库或分布式存储等,如何准确并高效地协调分布式并发操作也成为了分布式系统架构与设计中最大的挑战之一;
  • 缺乏全局时钟:分布式系统中多个进程可能在空间上分布式在各处,进程之间通过交换消息来相互协作,密切的协作通常取决于对程序动作发生的时间的共识。但在分布式系统中,各进程与时钟同步所达到的准确性是有限的,即没有一个正确时间的全局概念;
  • 故障独立性:组成分布式系统的所有计算机,都有可能发生任何形式的故障。单个模块的故障相对容易预知并设计应对逻辑,但分布式系统可能以新的方式出现故障。网络故障导致互连的计算机的隔离,但它们并不一定会停止运行,而且程序很难判断是网络故障还是因为延迟。同样,当被网络隔离计算程序在异常终止时,也许不能马上通知与它通信的其他组件了解。系统的每个组件会单独地出现故障,而其他组件还在运行。

分布式系统能够实现高可用、高吞吐、大容量存储、海量计算、并行计算等优异能力,天然的分布性和可伸缩等特性,也打破了物理上单机的瓶颈,使其能不断支撑着业务的发展而演进,并推进了云计算、大数据、人工智能等领域的发展。但是正如每个硬币都有两面,分布式系统的复杂性,也使其在设计、研发、运行、维护、安全性等方面都面临更多的挑战。

1.1-分布式系统的目标与要素

分布式系统的目标是提升系统的整体性能和吞吐量另外还要尽量保证分布式系统的容错性(假如增加10台服务器才达到单机运行效果2倍左右的性能,那么这个分布式系统就根本没有存在的意义)。

即使采用了分布式系统,我们也要尽力运用并发编程、高性能网络框架等等手段提升单机上的程序性能。

分布式系统设计两大思路:中心化和去中心化

  • 中心化设计

    • 两个角色: 中心化的设计思想很简单,分布式集群中的节点机器按照角色分工,大体上氛围两种角色: “领导”“干活的”
    • 角色职责: “领导”通常负责分发任务并监督“干活的”,发现谁太闲了,就想发设法地给其安排新任务,确保没有一个“干活的”能够偷懒,如果“领导”发现某个“干活的”因为劳累过度而病倒了,则是不会考虑先尝试“医治”他的,而是一脚踢出去,然后把他的任务分给其他人。其中微服务架构 Kubernetes 就恰好采用了这一设计思路。
    • 中心化设计的问题
      1. 中心化的设计存在的最大问题是“领导”的安危问题,如果“领导”出了问题,则群龙无首,整个集群就奔溃了。但我们难以同时安排两个“领导”以避免单点问题。
      2. 中心化设计还存在另外一个潜在的问题,既“领导”的能力问题:可以领导10个人高效工作并不意味着可以领导100个人高效工作,所以如果系统设计和实现得不好,问题就会卡在“领导”身上。
    • 领导安危问题的解决办法: 大多数中心化系统都采用了主备两个“领导”的设计方案,可以是热备或者冷备,也可以是自动切换或者手动切换,而且越来越多的新系统都开始具备自动选举切换“领导”的能力,以提升系统的可用性。
  • 去中心化设计

    • 终生地位平等: 在去中心化的设计里,通常没有“领导”和“干活的”这两种角色的区分,大家的角色都是一样的,地位是平等的,全球互联网就是一个典型的去中心化的分布式系统,联网的任意节点设备宕机,都只会影响很小范围的功能。
    • “去中心化”不是不要中心,而是由节点来自由选择中心。 (集群的成员会自发的举行“会议”选举新的“领导”主持工作。最典型的案例就是ZooKeeper及Go语言实现的Etcd)
    • 去中心化设计的问题: 去中心化设计里最难解决的一个问题是 “脑裂”问题 ,这种情况的发声概率很低,但影响很大。脑裂问题,这种情况的发生概率很低,但影响很大。脑裂指一个集群犹豫网络的故障,被分为至少两个彼此无法通信的单独集群,此时如果两个集群都各自工作,则可能会产生眼中的数据冲突何错误。一般的设计思路是,当集群半段发声了脑裂问题是,规模较小的集群就“自杀”或者拒绝服务。

1.2-分布式与集群的区别

  • 分布式: 一个业务分拆多个子业务,部署在不同的服务器上
  • 集群: 同一个业务,部署在多个服务器上。比如之前做电商网站搭的redis集群以及solr集群都是属于将redis服务器提供的缓存服务以及solr服务器提供的搜索服务部署在多个服务器上以提高系统性能、并发量解决海量存储问题。

2-分布式理论

2.1-CAP理论

CAP理论又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

归纳如下:

  • 一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值。等同于所有节点访问同一份最新的数据副本;集群中所有节点的数据时刻保持一致。
  • 可用性(Availability):每一个请求,都一定能够收到一个响应,无论响应成功还是失败。在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求,对数据更新具备高可用性。
  • 可扩展性/分区容忍性Partition-tolerance):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

CAP原理告诉我们,这三个因素最多只能满足两个,不可能三者兼顾。对于分布式系统来说,分区容错是基本要求,所以必然要放弃一致性。对于大型网站来说,分区容错和可用性的要求更高,所以一般都会选择适当放弃一致性。对应CAP理论,NoSQL追求的是AP,而传统数据库追求的是CA,这也可以解释为什么传统数据库的扩展能力有限的原因。

也就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡。

任何一个分布式系统,都不能够同时满足一致性,可用性,分区容错性。

所以在分布式系统可供选择的方案有三种:

  • CP:选择了数据一致性和分区容错性,放弃了可用性。一旦发生网络分区,受影响的服务就需要等待数据一致,因此在等待期间无法对外继续提供服务。
  • AP:选择了可用性和分区容错性,放弃了数据(强)一致性。
  • CA:放弃分区容错性,加强一致性和可用性,其实就是传统的单机数据库的选择。

原因:

  • CA满足的情况下,P不能满足的原因:
        数据同步(C)需要时间,也要正常的时间内响应(A),那么机器数量就要少,所以P就不满足
    
  • CP 满足的情况下,A不能满足的原因:
        数据同步(C)需要时间, 机器数量也多(P),但是同步数据需要时间,所以不能再正常时间内响应,所以A就不满足
    
  • AP 满足的情况下,C不能满足的原因:
        机器数量也多(P),正常的时间内响应(A),那么数据就不能及时同步到其他节点,所以C不满足
    

微服务注册中心的选择:

  • Zookeeper:CP设计,保证了一致性,集群搭建的时候,某个节点失效,则会进行选举行的leader,或者半数以上节点不可用,则无法提供服务,因此可用性没法满足。如金融行业
  • Eureka:AP原则,无主从节点,一个节点挂了,自动切换其他节点可以使用,去中心化。如电商系统

2.2-BASE理论

BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。

BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。

基于CAP理论,CAP理论不适用于数据库事务(因为更新一些错误的数据而导致数据出现紊乱,无论什么样的数据库高可用方案都是徒劳) ,虽然XA事务可以保证数据库在分布式系统下的ACID特性,但是会带来性能方面的影响。

  • 基本可用(Basically available):在分布式系统出现不可预知的故障时,损失系统部分可用性。
    • 响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒。
    • 系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
    • 数据库采用分片模式,把100万用户数据分布在5个实例上,其中一个实例故障了,仍然可以保证80%的可用用户数据。
  • 软状态(Soft-state):什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种 “硬状态”。软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
  • 最终一致性(Eventually-consistent):最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

ACID和BASE的区别与联系

ACID是传统数据库常用的设计理念,追求强一致性模型。BASE支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性,ACID和BASE代表了两种截然相反的设计哲学。

ACID 是传统数据库系统常用的设计理论,追求强一致性模型。BASE 常用于大型分布式系统,只需要保证最终一致性。在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。

2.3-一致性模型

数据一致性来说,简单说有三种类型,当然,如果细分的话,还有很多一致性模型,如:顺序一致性,FIFO一致性,会话一致性,单读一致性,单写一致性等。下面介绍主要的三种一致性模型:

  • Strong Consistency 强一致性:新的数据一旦写入,在任意副本任意时刻都能读到新值。比如:文件系统,RDBMS,Azure Table都是强一致性的。
  • Eventually 最终一致性:当你写入一个新值后,有可能读不出来,但在某个时间窗口之后保证最终能读出来。比如:DNS,电子邮件、Amazon S3,Google搜索引擎这样的系统。
  • Weak 弱一致性:当你写入一个新值后,读操作在数据副本上可能读出来,也可能读不出来。比如:某些cache系统,网络游戏其它玩家的数据和你没什么关系,VOIP这样的系统。

从这三种一致型的模型上来说,我们可以看到,Weak和Eventually一般来说是异步冗余的,而Strong一般来说是同步冗余的(多写),异步的通常意味着更好的性能,但也意味着更复杂的状态控制。同步意味着简单,但也意味着性能下降。

以及其他变体:

  • Causal Consistency(因果一致性):如果Process A通知Process B它已经更新了数据,那么Process B的后续读取操作则读取A写入的最新值,而与A没有因果关系的C则可以最终一致性。
  • Read-your-writes Consistency(读你所写一致性):如果Process A写入了最新的值,那么 Process A的后续操作都会读取到最新值。但是其它用户可能要过一会才可以看到。Facebook的数据同步就是采用这种原则。
  • Session Consistency(会话一致性):一次会话内一旦读到某个值,不会读到更旧的值。
  • Monotonic Read Consistency(单调一致性):一个用户一旦读到某个值,不会读到比这个值更旧的值,其他用户不一定。

3-分布式系统的主要组成

3.1-分布式服务

分布式系统下节点间服务通讯有两种途径,一种是通过 RPC(Remote Procedure Call) 实现两点间通讯,可以设计成同步通讯,也可以是异步的;一种是通过消息中间件实现通信,一般是异步通讯方式。

  • RPC 调用

    RPC 调用也是分布式环境下常见的通讯方式,有同步的模式,也有异步的模式。同步模式下,客户端发起调用并阻塞当前线程,直到服务端处理完毕返回响应才完成整个调用过程;异步模式下,客户端发起调用后立即返回,并记录当前请求,直到接收到服务端响应后才找到对应的请求进行结果的处理,并完成整个调用过程。

    RPC 调用中一般只有两个角色:客户端和服务端,但在分布式环境下服务端可能有多台机器,他们共同组成一个集群,每台都提供一样的服务。这时就要有一种机制能让客户端将持续不断的请求均衡的分发到所有的服务端,一般将这种机制称为「负载均衡」。负载均衡可以在客户端实现,也可以通过专门的负载均衡设备实现。如果采用客户端负载均衡方案,需要让客户端感知所有服务端的机器地址和端口,需要引入一套「服务发现和集成的体系」,好处是通讯是点对点的,比较高效,且不存在单点。如果使用专门负载均衡设备,则客户端只需感知负载均衡设备的地址,由它来对接所有服务端,并均衡服务请求,这种模式比较简单,但存在性能单点,且延长了请求链路,稳定性也会受影响。无论是客户端还是专门设备做负载聚合,都可以根据不同的场景采用适合的负载均衡策略,常见的策略有随机(Random)、轮询(RoundRobin)、最快连接数(Fastest Connection)、最小连接数(Least Connection)等等。

  • 消息中间件

    消息中间件一般是独立部署的一组应用程序,负责消息的接收和投递。整个通讯过程是异步的,有消息发布者、消息中间件和消息订阅者三个角色,消息从发布端发出后,会被消息中间件接收并做持久化,消息的消费有两种常见的模式,一种是消息中心主动投递的模式(推模式),一种是订阅者主动拉取的模式(拉模式)。在推模式下,发布者发布消息,消息发到消息中心做持久化,然后被投递到订阅端集群,一般来说会选择在所有订阅者中随机选一台投递,在这种模式下,要求消息订阅者集群的消息消费速度要能跟上发布消息是速度;在拉模式下,消费者根据队列里消息总数来做均分,主动连上消息中间件进行消费,对消费速度没有速度限制,所以一般拉模式的消息中间件会提供更强的消息扛积压能力。

    由于消息被持久化,所以可以做到消息的可靠投递,保证了分布式环境下通讯的可靠达到。在分布式环境下,一个系统可能会在一次事务操作中通过消息的方式去改变其它系统的状态(主要指存储的状态),为了能模拟类似事务的一致性,异步消息可以利用 DB 的持久化来支持事务型消息。事务型消息指的是发布消息的应用系统在本地数据库事务操作序列中发送的消息。此类消息的投递与数据库事务状态保持一致,当事务状态是提交时,消息会被投递到订阅者,当事务状态是回滚时,消息不会被投递到订阅者,这也是分布式环境下消息中间件需要支持的特殊场景。

3.2-分布式数据存储

  1. 分布式缓存

    当传统数据库面临大规模数据访问时,磁盘 I/O 能力往往成为性能瓶颈,从而导致过高的响应延迟,而比较常用的提升性能的手段,是在应用和数据库之间加入一层分布式缓存来提升数据访问性能,分布式缓存将高速内存作为数据对象的存储介质,理想情况下可以获得内存级的读写性能,常见的缓存实现会通过 LRU 算法来缓存访问最多的数据,进而提升缓存效率。

    通过分布式缓存服务器集群,将缓存数据分布到集群多台服务器上可在一定程度上改善缓存的可用性,同时也能起到扩展缓存容量的作用,当一台缓存服务器宕机的时候,只有部分缓存数据丢失,重新从数据库加载这部分数据不会对数据库产生很大影响。

    分布式缓存还具有支持弹性扩展的能力,通过动态增加或减少节点,应对变化的数据访问负载,提供可预测的性能与扩展性,同时也能最大限度地提高资源利用率

    分布式缓存比较典型应用场景包括:

    应用对象缓存:缓存系统作为 ORM 框架的二级缓存对外提供服务,目的是减轻数据库的负载压力,加速应用访问;

    状态缓存:缓存包括 Session 会话状态及应用横向扩展时的状态数据等,这类数据一般是难以恢复的,对可用性要求较高,多应用于高用集群;

    并行处理:通常涉及大量中间计算结果需要共享;

    事件处理:分布式缓存提供了针对事件流的连续查询 (continuous query) 处理技术,满足实时性需求;

    极限事务处理:分布式缓存为事务型应用提供高吞吐率、低延时的解决方案,支持高并发事务请求处理,多应用于铁路、金融服务和电等领域。

    通过配置合理容量的分布式缓存,能在提升应用数据访问性能的同时,降低总体拥有成本,因此架构设计上做到合理利用缓存,变得就越来越重要。

  2. 分布式文件系统

    分布式文件系统,是指允许文件通过网络在多台计算机中分散存储的共享文件系统,它通过网络将分布在不同区域的多台计算机连接在一起从而组合成容量更大,处理能力更强的分布式文件存储系统。分布式文件系统很好的解决单台计算机存储和处理能力存在上限的问题,具备大的存储容量和处理能力的扩展能力。

    在分布式文件系统中,客户端并非直接访问底层存储,而是通过网络以特定的通信协议和服务器沟通,进而完成文件存储操作,借由通信协议的设计,可以让客户端和服务器端都能根据访问控制清单或是授权,来限制对于文件系统的访问。由于分布式文件系统的网络接入特性,使得不同用户都能通过网络接入分布式文件系统,实现文件数据的共享,从用户角度看,分布式文件系统与单机文件系统看到的视图是一样的,使用方式也相同。

    分布式文件系统除了能提供比单机文件系统更大的存储容量和处理能力外,通过数据复制与容错,也拥有了本地文件系统所无法具备的数据备份、数据安全等优点,也就是说,即使系统中有一小部分的节点脱机,整体来说系统仍然可以持续运作而不会有数据损失。

  3. 分布式数据库

    随着数据量的高速增长,分布式数据库技术也得到了快速的发展,传统的关系型数据库开始从集中式模型向分布式架构发展。分布式数据库是指利用高速计算机网络将物理上分散的多个数据存储单元连接起来组成一个逻辑上统一的数据库。分布式数据库的基本思想是将原来集中式数据库中的数据分散存储到多个通过网络连接的数据存储节点上,以获取更大的存储容量和更高的并发访问量。

    分布式数据库可以按需增加、减少数据库处理节点,很好地解决了单台数据库的存储和处理能力上限问题;除此之外,分布式数据库往往具备较强的容错能力,通过数据复制、副本冗余,当某个处理节点出现故障时,能够自动恢复,对使用者没有感知。最后,分布式数据库底层采用廉价服务器,相比传统单机数据库使用的高端服务器和高端存储,成本大幅度降低。

    在分布式数据库中,客户端并非直接访问底层的存储系统,而是使用结构化查询语言(SQL语言)访问数据库节点,再由数据库节点的 SQL 引擎翻译成针对底层存储系统的操作。一套分布式数据库往往会服务多个业务,分布式数据库内部支持多个业务之间的隔离,当某个业务出现异常时,只会影响该业务,不会对其它业务造成影响。蚂蚁的 OceanBase 是一个典型的分布式数据库。使用分布式数据库,将大大降低单库故障的发生机率,并简化单库故障的恢复机制。再结合分布式数据中间件在业务维度数据分片和路由、数据源动态调整、应用层高可用容灾、异构数据源适配等能力,将能很好的实现分布式环境下 DB 的高性能访问和高可用。

  4. 分布式数据中间件

    关系型数据库是经典的持久化解决方案,但在海量业务场景下也会遇到单表的容量瓶颈和单库的性能瓶颈。按照分布式的思想,数据也要拆分,让集中在单点的读写访问分布到多个 DB 服务器上,从而获得容量和性能上的弹性延伸能力。根据业务场景可分为垂直拆分(按业务)、水平拆分(按请求/用户做哈希,或者做区间拆分)、读写拆分等。

    按照垂直拆分后的数据会造成本来单库事务变成跨库事务,很难通过传统数据库的机制来保证 ACID,需要引入一种应用层的,基于服务的事务协调机制。这个下文会有专门阐述。按照水平拆分的数据,需要按照某些业务维度将数据拆分到不同的库表,从而解决数据水平扩展的问题,也提升了整体的 QPS,使数据层具备 TB+/天 的吞吐能力。数据拆分后,每次数据访问需要按照分库分表的规则进行路由,本质上是通过规则对原始 SQL 进行重写,拆分成多条 SQL,并分发给链接多个库的数据源去执行,并汇总结果。由于整个过程都在应用层进行,为屏蔽复杂性,需要将上述逻辑封装一层中间件类库中,并通过标准 JDBC 接口对外暴露接口,让业务透明的完成整个数据库的访问过程。

    通过一层统一的分布式数据中间件,让业务通过标准 SQL 和标准 JDBC 接口就能访问一组理论容量「无限大」的关系型数据库集群,并且具有很好的性能。同时,通过这层中间件,也能实现所有数据库参数的动态调整、路由规则动态调整、数据库分片权重的调整,从而在应用层实现数据库访问的高可用,且不依赖于数据库高可用机制,甚至提供更灵活强大的故障容忍能力。此外,标准 SQL 和 JDBC 屏蔽了底层数据库的实现,使应用具备透明对接异构数据源的能力,实现复杂场景的业务访问和一些双写双读的需求。

3.3-分布式事务

传统关系型数据库的事务模型必须遵守 ACID 原则。在单数据库模式下,ACID 模型能有效保障数据的完整性,但是在大规模分布式环境下,单库无法承载高并发和海量数据,所以数据会被通过垂直拆分或水平拆分到不同的 DB 中,一个业务往往会跨越多个数据库。在 JavaEE 规范中使用 2PC (2 Phase Commit, 两阶段提交) 来处理跨 DB 环境下的事务问题,但是 2PC 是反可伸缩模式,也就是说,在事务处理过程中,参与者需要一直持有资源直到整个分布式事务结束。这样,当业务规模达到千万级以上时,2PC 的局限性就越来越明显,系统可伸缩性会变得很差。基于 BASE 的思想可以仿照 2PC 的方式,在应用层来实现分布式环境下多个事务协调一致。 在充分保障分布式环境下高可用性、高可靠性的同时兼顾数据一致性的要求,其最大的特点是保证数据最终一致 (Eventually consistent)。

在分布式事务中,可以简单分成事务发起方和事务参与者两个角色。发起方负责启动分布式事务,触发创建主事务记录。发起方是分布式事务的总体协调者,负责调用参与者的服务,记录相应的事务日志,并感知整个分布式事务状态来决定整个事务是 COMMIT 还是 ROLLBACK。参与者是分布式事务中的一个原子单位,所有参与者都必须定义 prepare、commit、rollback 3个基本接口,并保证其业务数据的幂等性,也必须保证 prepare 中的数据操作能够被提交 (COMMIT) 或者回滚 (ROLLBACK)。

整个协调过程从发起方开启一个本地事务开始,过程中参与者会显式调用参与方的业务方法,这些显式调用的方法即为一阶段的 prepare 方法,一般参与方会在该方法内锁定资源。这些方法调用可以采用第一小节提到的「消息中间件」,也可以采用「RPC 通讯」。一旦这次本地事务成功,则本地事务提交,发起方会调用第二阶段的真正 commit 方法,来让所有参与方的事务完成第二阶段的提交。本地事务提交失败或者在这之前某个逻辑中失败(可能是本地逻辑异常,也可能参与方一阶段失败),则本地事务失败,发起方会负责完成一阶段的所有回滚操作,避免出现不一致情况。

在分布式环境下,单点故障是个常态,所以无论是发起方还是参与方都可能在任何一刻不可用,此时需要一个第三方的恢复系统来感知所有的事务状态,并通过定期轮询来发现异常状态的事务记录,并将其恢复至最终一致。