分布式是个大命题,持续记录对它的理解和梳理其脉络。(持续更新中~)

一、分布式互斥

  多服务需要访问同一临界资源的场景很多,需要有效的分布式同步与互斥手段。

1、集中式算法

  有一个中央服务器,用于协调各个服务器对临界资源的访问。
  优点:简单,项目早期ROI较高。
  缺点:访问一次临界资源交互次数较多,请求过多后中央服务器会成为性能瓶颈,并且要保证中央服务器的可用性。

2、分布式算法

  每次要访问临界资源,要向所有服务器发送请求,其它所有服务同意时间戳最小的请求能够获得本次使用的机会。Hadoop修改一个文件便是使用的该算法。
  优点:公平,无需一个中央服务器,适合p2p场景。
  缺点:沟通成本过高,可用性较差,适合请求频率低,集群规模小的场景。

3、令牌环算法

  多个服务进程围成一个环,循环传递一个令牌,持有令牌的服务有资格使用临界资源。
  优点:稳定,可用性高,沟通成本低。
  缺点:一个服务需要使用资源时,即使其它服务不需要使用资源,也必须等待令牌转到自己。

4、分布式锁

  先说锁是什么,在一篇文章中这样写到”锁是实现多线程同时访问同一共享资源,保证同一时刻只有一个线程可访问共享资源所做的一种标记。”这个解释妙在可以等同地搬到分布式场景。所以分布式锁一般有三种实现方式:
  (1)基于数据库实现:建立一张锁表,对锁的操作即对表中一条记录的操作。要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。数据库对共享资源做了唯一性约束,如果有多个请求被同时提交到数据库的话,数据库会保证只有一个操作可以成功。
优点:简单。
缺点:基于磁盘的数据库IO开销大,只适用于并发量低的场景,并且发生死锁的时候只能人工介入。
  (2)基于缓存实现:缓存的数据全部都在内存,因此速度比基于磁盘要快得多。一般可以使用Redis的setnx指令,key为锁id,val为过期时间。返回0表示其它服务已经获得锁,返回1表示当前服务获得锁。
  优点:性能高,可以跨集群部署,可设置超时时间解除死锁。
  缺点:超时时间难以确定,因为一个服务持锁的时间可能很长。

二、分布式事务

  关于分布式事务事务,在网上看到一个很好的总结:“分布式事务本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。”

1、BASE理论

  (1)基本可用(Basically Available)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。典型的如服务降级。
  (2)软状态(Soft State)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。
  (3)最终一致性(Eventual Consistency)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。

2、消息事务+最终一致性

  引入消息中间件做持久化操作,将与其它系统的交互推迟执行。如在淘宝系统中用户成功建立了一个订单,接下来需要仓库系统完成相应操作,如果整个过程都要用户等着那体验是很不好的,如果用户成功建立订单并支付就不需要等待了,体验会好很多,此时可以用消息中间件记录订单状态,用户侧立刻返回成功,然后由消息中间件根据业务与仓库系统交互。

3、TCC

  TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样。
  我个人理解TCC与二阶段的区别在于,TCC是部分控制,Try、Confirm和Cancel接口均由业务完成,而二阶段是完全控制,全部由事务服务本身保证。

4、二阶段提交

  以大多数据库都实现了的XA二阶段提交协议来说,一般包含一或多个RM(Resource Managers,一般为数据库),一个TM(Transaction Manager,协调全局事务的各个事务,mysql中为连接mysql的客户端)。
  第一阶段,TM会向所有用到的RM发送事务操作请求,RM们执行后并不提交,而是告诉TM自己准备好数据可以提交了。
  第二阶段,如果所有RM都返回数据准备ok,则TM会让所有RM提交事务;如果有大于等于一个RM数据准备失败,则TM会让所有RM都进行回滚。
  但二阶段提交是存在问题的:
  (1)第二阶段中,TM发生故障了,则所有RM的临界资源都会被锁定,阻塞其它事务。
  (2)第二阶段中,部分RM节点网络发生了波动,会导致其收不到提交指令,从而一直锁定临界资源。

5、三阶段提交

  三阶段提交相比二阶段增加了超时机制,当超过时间没有收到期望的指令,则中断事务,防止前面提到的一直锁定临界资源的问题。另外三阶段提交分为三个步骤:
  第一阶段,TM询问所有RM是否可以进行事务操作。
  第二阶段,若RM都回复可以,说明网络状况还行,TM就向RM们发送执行事务指令,否则发送中断事务指令。
  第三阶段,若RM都回复事务执行成功,则TM就像RM们发送提交事务指令,否则发送中断事务指令,回滚事务。

三、可用性

解决可用性的问题,有如下的要点:
(1)硬件损坏或节点掉线:复制;
(2)集群新增或移除节点如何可用:一致性哈希;
(3)主节点不可用后如何挑选从节点:选举算法;
(4)请求集中在某个节点导致不可用:负载均衡;
(5)瞬时流量过高导致节点不可用:限流。

1、复制

  分布式系统通过复制协议将数据同步到多个存储节点,并确保多个副本数据的一致性。
(1)强一致性:主备同步均成功才返回客户端写成功,如果备副本出现问题将阻塞写操作。
(2)弱一致性:主副本本地同步成功即返回写成功,然后异步地将数据同步给备副本。

2、一致性哈希

  如果使用简单哈希算法,目前有三个节点,则通过key%3只会哈希出三个值:0,1,2。这时如果新加入一个节点,则需要修改为key%4哈希出四个值,这就导致了原来0,1,2节点中的数据都有可能哈希到其它节点中去,整个集群需要大洗牌,导致不可用。
  这个案例中,核心是%3到%4发生了变化,导致集群大洗牌,所以要解决这个问题很简单,就是%一个固定值,比如%232 ,这样不管集群节点数量的改变,key的哈希值是不变的,那如何通过哈希值找到相应节点呢?可以将哈希值从0到232看成一个环,同样也对每个节点进行哈希,分布在这个环上,然后每个节点负责自己到下个节点这个区域内的哈希值,如图:
图片3.png
  这样,当某个节点不可用了,只是将原本它负责的哈希值变为了另一节点的任务,并没有让整个集群大动干戈。而上面的这种做法,其实就是一致性哈希,不过上面的做法并不是完美的,它无法解决数据倾斜的问题:大部分数据集中在小块区域上,小部分节点处理集群大部分任务,所以一种方法是,每个节点不再哈希一次,而是哈希多次,这样哈希环上就有了更加密集的节点,以解决数据倾斜的问题。
  在Redis中,将每个哈希值认为是一个槽,槽的数量是固定的,每个节点负责一定数量的槽,所以其本质也就是一致性哈希的做法。

3、分布式选举

  主节点不可用后,需要从从节点中挑选出一个新的主节点,而挑选的算法一般有以下这些:
(1)Bully算法
  前提是集群中每个节点均知道其他节点的 ID,然后每次选举都选节点ID最大的节点。
  这种算法优点是简单、快,缺点是每当有ID更大的节点加入集群时,都可能触发选举,选举的频率可能会比较高。目前MongoDB就是用的该算法,采用最后操作的时间戳作为节点ID。
(2)Raft算法
  这个是典型的多数派选举算法,每次选举,拥有超过半数选票的节点成为主节点。一般投票的标准是投第一个收到消息的节点,它的科学性在于最先收到消息,说明和自己和它之间的网络状况最好,它成为主节点最可靠。
  这种算法优点是实现简单,快,稳定,缺点是通信量比较大,每个节点都需要和所有其它节点通信,但是它没有Bully算法的问题,当有新节点加入时,虽然会触发一轮新的选举,但是不一定会切主。
  Raft的一个动画:https://raft.github.io/

4、负载均衡

这个在微服务也有提到,一般的方法就是:
(1)轮询:顺序地将请求落到不同的节点中;
(2)加权轮询(Nginx中的做法):按照权重顺序地将请求落到不同的节点中;
(3)随机
(4)一致性哈希
(5)基于反馈的负载均衡策略:每个集群有一个总控节点根据全局负载信息进行整体调度。工作节点通过心跳包将节点的负载信息,如CPU,内存,磁盘,网络等资源使用率,读写次数及读写数据量发送给给主控节点。主控节点根据这些信息决定请求/任务放到哪个服务器上执行,或者生成迁移任务,将一些任务从负载高的机器迁移到负载低的机器。

5、限流

  有时需要限制流量,以防止瞬时大流量导致的服务不可用,常见的限流算法有漏桶算法和令牌桶算法。
(1)漏桶算法:请求先集中到一个流量固定的漏桶中,然后漏桶以固定流速将流量放出。
(2)令牌桶算法:有一个装着令牌(token)的桶,它按照qps的速率补充令牌,满了则溢出。当一个请求到来时,它需要先从桶中取出一个令牌,才能继续进行后续操作。所以简单来说,令牌桶算法是基于令牌的补给速率限制qps,对于这个算法我自己也写了一个轻量的小切面,博客地址:http://www.liuyukang.com/archives/cppxianliuqi。

四、微服务

1、概述

  针对单机服务性能瓶颈的问题,一般有两种解决方案,即对原服务进行水平拆解和垂直拆解。水平拆解指将同一服务copy多份,做并行化改造,各服务之间没有关系。垂直拆解指将服务拆成多个小服务,各自通过网络进行通信,与微内核有异曲同工之处。这两种解法优劣显而易见,水平拆解成本一般更低,并且耦合的业务之间都在单机中通信,效率更高,但依然会出现单一服务过于臃肿,维护成本高的问题。垂直拆解成本更高,但是维护成本相对来说更低,更加适合互联网快速迭代的今天。
  微服务便是垂直拆解的一套方案,不过它并没有一个标准,更多的是一种思想,Martin Fowler则在Microservices(https://www.martinfowler.com/articles/microservices.html )中对这套架构进行了一些总结,这里做简单摘要:
(1)Componentization via Services
通过服务而不是库来让整个系统组件化,从而让组件的修改给系统的修改带来最小开销。
(2)Organized around Business Capabilities
对团队的划分,不是根据技术类型来的,而是根据业务来的,很有意思,因为同一业务技术间的沟通是频繁的,不同业务间的沟通是稀疏的,所以根据技术类型来对团队划分会带来比较大的沟通成本。
(3)Products not Projects
每个小团队都要对自己的产品的整个生命周期负责,而不是以交付为目标。
(4)Smart endpoints and dumb pipes
微服务架构的其中一个重点是让服务更加内聚与解耦,所以一个服务暴露的接口往往是粗粒度的,适合使用HTTP的RESTful API或轻量级的消息总线做消息传递。
(5)Decentralized Governance
去中心化管理。
(6)Decentralized Data Management
去中心化管理数据,每个服务自行管理自己的数据库。
(7)Infrastructure Automation
CI/CD。
(8)Design for failure
分布式的一个重头戏:容错,高可用。
(9)Evolutionary Design
不断迭代优化设计。

2、服务发现

  微服务架构中,多个组件需要交互,那么如何知道对方的IP端口呢,直接保存那是不实际的,因为ip和端口都是可能随着服务的升级变化的,所以需要有一种机制获取服务提供者的网络信息,所以有了服务发现组件consul,它需要有以下功能:
(1)服务注册表:记录各个微服务的信息,如名称(不变的),IP,端口号等,并提供查询服务API,注册服务API,注销服务API。
(2)服务注册与发现:在服务启动时,将自己的信息注册到服务发现组件上。在需要与某一服务交互时,从服务发现组件上查询服务API。
(3)服务检查:定时检查下线服务,从服务注册表中移除该实例。

3、负载均衡

  微服务架构中,某一个服务可能存在多个实例,所以需要有负载均衡器,将请求合理分摊到各个实例上去。一般的负载均衡算法有轮询,随机等,首先从服务发现组件获取某服务的实例列表,然后根据负载均衡算法请求相应实例。

4、服务调用容灾

  服务崩了或其它原因都会导致某个服务的不可用,所以需要一种方式发现某个服务的不可用,并用恰当的手段来处理。一般使用请求响应时间判断服务是否不可用,使用熔断器处理不可用的服务,它统计一段时间内调用失败的次数,如果失败次数过多,则直接返回错误,不浪费CPU时间。
  通常在分布式存储集群中,某个节点不可用了,会通过选举算法选出一个从节点代替主节点,当然这与微服务的场景不太一样,因为微服务的多实例是对等的,大多数分布式存储集群内是分主从节点的,它们并不对等。

5、微服务网关

  客户端可能需要调用多个接口才能完成一次业务需求,这就增加了客户端的复杂性,每个服务还都需要独立的认证,未来迭代难以重构。因此,需要有一个门面来封装应用程序的内部结构,客户端只与门面交互即可,这个门面就是微服务网关。
  一般来说,微服务网关的核心是一系列的过滤器,用以完成:
(1)身份认证:过滤非法请求
(2)埋点:提供数据统计切面
(3)动态路由:动态地将请求路由到不同后端集群
(4)负载分配
(5)静态响应处理:部分静态资源直接响应,避免转发到内部集群

6、服务调用跟踪

  我们写程序时通常会假定网络很健硕,但这是不恰当的,文章The Eight Fallacies of Distributed Computing中也进行了阐述,网络往往很脆弱,网络资源也很有限,所以如果我们能够跟踪每个请求经过了哪些微服务,请求耗时时间,业务逻辑耗时等指标,那么就能更好地分析系统瓶颈,解决系统问题。

7、服务动态配置

  配置管理是很麻烦的,并且为了修改一个配置还需要重启服务,代价也是很大的,因此微服务架构中,配置往往统一管理,做到:
(1)集中管理配置
(2)不同环境,不同配置:如测试和生产环境,国内与国外环境,可能都有不同的配置。
(3)运行期间动态调整:不停止微服务,修改配置并自动更新

六、分布式存储

1、CAP

分布式存储系统有三个系统指标CAP:
  一致性(Consistency):指对数据进行写操作之后,读操作返回之前写操作的值。也叫外部一致性,与ACID中的一致性意义不同。
  可用性(Availability):指任一时刻访问数据库数据都能得到回应。
  分区容错性(Partition Tolerance):区间通信可能出错。
  这三个指标不能同时满足,最多只能同时满足其中两个,而分区容错性是分布式系统必须满足的,所以一般只有两种选择:保证数据强一致性与分区容错性,放弃可用性;保证可用性与分区容错性,只保证数据弱一致性。

2、HDFS

七、分布式计算

1、MapReduce

  MapReduce的核心是分治,将一大批数据进行拆分,然后并行地进行map()操作,map的结果会存放在本地磁盘的k个磁盘区,k是Reduce任务的个数,第i个Reduce任务会读取每个map任务的第i个磁盘区的结果,进行reduce操作,最后输出。
  问题:这是一种批量计算的形式,会先收集数据并将其缓存起来,等到缓存写满时才开始处理数据。因此,批量计算的一个缺点就是,从数据采集到得到计算结果之间经历的时间很长。这种模式下任务运行完成之后,整个任务进程就结束了,属于短任务模式。但,任务进程的启动和停止是一件很耗时的事儿,因此 MapReduce 对处理实时性的任务就不太合适了。
图片4.png
(图片来自网络,侵删)

2、Stream

  因为MapReduce这种批量计算框架无法应对处理实时无边界数据的场景,所以流式计算框架应运而生,专门处理流数据:以大量、快速、时变的流形式持续在应用中产生的数据。
  流计算强调的是实时性,数据一旦产生就会被立即处理,当一条数据被处理完成后,会序列化存储到缓存中,然后立刻通过网络传输到下一个节点,由下一个节点继续处理,而不是像 MapReduce 那样,等到缓存写满才开始处理、传输。为了保证数据的实时性,在流计算中,不会存储任何数据,就像水流一样滚滚向前。
图片5.png
(图片来自网络,侵删)

(1)Storm

(2)Spark

八、分布式调度

参考:

https://www.martinfowler.com/articles/microservices.html
《Spring Cloud与Docker微服务架构实战》
《The Eight Fallacies of Distributed Computing》
《分布式技术原理与算法解析》
https://www.cnblogs.com/cxxjohnson/p/9145548.html
https://blog.csdn.net/qq_35642036/article/details/88809234
https://www.jianshu.com/p/735a3d4789fc
https://raft.github.io/
https://www.cnblogs.com/xuwc/p/9123078.html
https://blog.csdn.net/weixin_42333737/article/details/106130057
《大规模分布式存储系统-原理解析与架构实战》
《分布式系统设计》
https://blog.csdn.net/Little_Fire/article/details/80605233
https://blog.csdn.net/jiangjunshow/article/details/100798240