mirror of
				https://github.com/cheetahlou/CategoryResourceRepost.git
				synced 2025-11-04 16:23:43 +08:00 
			
		
		
		
	mod
This commit is contained in:
		
							
								
								
									
										178
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/28 | 分布式高可靠之负载均衡:不患寡,而患不均.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/28 | 分布式高可靠之负载均衡:不患寡,而患不均.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
<audio id="audio" title="28 | 分布式高可靠之负载均衡:不患寡,而患不均" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c6/99/c6f3932d07173964ae0017a3026ef699.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好!我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
 | 
			
		||||
 | 
			
		||||
到目前为止,我已经为你介绍了分布式起源、分布式协调与同步、分布式资源管理与负载调度、分布式计算技术、分布式通信技术和分布式数据存储。可以说,掌握了这些内容,基本上就掌握了分布式的关键技术。
 | 
			
		||||
 | 
			
		||||
然而,只有可靠的分布式系统才能真正应用起来。那么,分布式系统的可靠性又是如何实现的呢?
 | 
			
		||||
 | 
			
		||||
不要着急,接下来几篇文章,我会和你一起学习分布式可靠性相关的知识,包括负载均衡、流量控制、故障隔离和故障恢复。
 | 
			
		||||
 | 
			
		||||
在这其中,负载均衡是分布式可靠性中非常关键的一个问题或技术,在一定程度上反映了分布式系统对业务处理的能力。比如,早期的电商抢购活动,当流量过大时,你可能就会发现有些地区可以购买,而有些地区因为服务崩溃而不能抢购。这,其实就是系统的负载均衡出现了问题。
 | 
			
		||||
 | 
			
		||||
接下来,我们就一起来打卡分布式高可靠之负载均衡。
 | 
			
		||||
 | 
			
		||||
## 什么是负载均衡?
 | 
			
		||||
 | 
			
		||||
先举个例子吧。以超市收银为例,假设现在只有一个窗口、一个收银员:
 | 
			
		||||
 | 
			
		||||
- 一般情况下,收银员平均2分钟服务一位顾客,10分钟可以服务5位顾客;
 | 
			
		||||
- 到周末高峰期时,收银员加快收银,平均1分钟服务一位顾客,10分钟最多服务10位顾客,也就是说一个顾客最多等待10分钟;
 | 
			
		||||
- 逢年过节,顾客数量激增,一下增加到30位顾客,如果仍然只有一个窗口和一个收银员,那么所有顾客就只能排队等候了,一个顾客最多需要等待30分钟。这样购物体验,就非常差了。
 | 
			
		||||
 | 
			
		||||
那有没有解决办法呢?
 | 
			
		||||
 | 
			
		||||
当然有。那就是新开一个收银窗口,每个收银窗口服务15个顾客,这样最长等待时间从30分钟缩短到15分钟。但如果,这两个窗口的排队顾客数严重不均衡,比如一个窗口有5个顾客排队,另一个窗口却有25个顾客排队,就不能最大化地提升顾客的购物体验。
 | 
			
		||||
 | 
			
		||||
所以,尽可能使得每个收银窗口排队的顾客一样多,才能最大程度地减少顾客的最长排队时间,提高用户体验。
 | 
			
		||||
 | 
			
		||||
看完这个例子,你是不是想到了一句话“不患寡,而患不均”?这,其实就是负载均衡的基本原理。
 | 
			
		||||
 | 
			
		||||
通常情况下,**负载均衡可以分为两种**:
 | 
			
		||||
 | 
			
		||||
- 一种是请求负载均衡,即将用户的请求均衡地分发到不同的服务器进行处理;
 | 
			
		||||
- 另一种是数据负载均衡,即将用户更新的数据分发到不同的存储服务器。
 | 
			
		||||
 | 
			
		||||
我在[第25篇文章](https://time.geekbang.org/column/article/168940)分享数据分布方法时,提到:数据分布算法很重要的一个衡量标准,就是均匀分布。可见,哈希和一致性哈希等,其实就是数据负载均衡的常用方法。那么今天,**我就与你着重说说服务请求的负载均衡技术吧。**
 | 
			
		||||
 | 
			
		||||
分布式系统中,服务请求的负载均衡是指,当处理大量用户请求时,请求应尽量均衡地分配到多台服务器进行处理,每台服务器处理其中一部分而不是所有的用户请求,以完成高并发的请求处理,避免因单机处理能力的上限,导致系统崩溃而无法提供服务的问题。
 | 
			
		||||
 | 
			
		||||
比如,有N个请求、M个节点,负载均衡就是将N个请求,均衡地转发到这M个节点进行处理。
 | 
			
		||||
 | 
			
		||||
## 服务请求的负载均衡方法
 | 
			
		||||
 | 
			
		||||
通常情况下,计算机领域中,在不同层有不同的负载均衡方法。比如,从网络层的角度,通常有基于DNS、IP报文等的负载均衡方法;在中间件层(也就是我们专栏主要讲的分布式系统层),常见的负载均衡策略主要包括轮询策略、随机策略、哈希和一致性哈希等策略。
 | 
			
		||||
 | 
			
		||||
今天,我着重与你分析的就是,中间件层所涉及的负载均衡策略。接下来,我们就具体看看吧。
 | 
			
		||||
 | 
			
		||||
### 轮询策略
 | 
			
		||||
 | 
			
		||||
轮询策略是一种实现简单,却很常用的负载均衡策略,核心思想是服务器轮流处理用户请求,以尽可能使每个服务器处理的请求数相同。生活中也有很多类似的场景,比如,学校宿舍里,学生每周轮流打扫卫生,就是一个典型的轮询策略。
 | 
			
		||||
 | 
			
		||||
**在负载均衡领域中,轮询策略主要包括顺序轮询和加权轮询两种方式**。
 | 
			
		||||
 | 
			
		||||
首先,我们一起看看**顺序轮询**。假设有6个请求,编号为请求1~6,有3台服务器可以处理请求,编号为服务器1~3,如果采用顺序轮询策略,则会按照服务器1、2、3的顺序轮流进行请求。
 | 
			
		||||
 | 
			
		||||
如表所示,将6个请求当成6个步骤:
 | 
			
		||||
 | 
			
		||||
1. 请求1由服务器1处理;
 | 
			
		||||
1. 请求2由服务器2进行处理。
 | 
			
		||||
1. 以此类推,直到处理完这6个请求。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/fc/ee/fcaab73150188671e7ec789041019eee.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
最终的处理结果是,服务器1处理请求1和请求4,服务器2处理请求2和请求5,服务器3处理请求3和请求6。
 | 
			
		||||
 | 
			
		||||
接下来,我们看一下**加权轮询**。
 | 
			
		||||
 | 
			
		||||
加权轮询为每个服务器设置了优先级,每次请求过来时会挑选优先级最高的服务器进行处理。比如服务器1~3分配了优先级{4,1,1},这6个请求到来时,还当成6个步骤,如表所示。
 | 
			
		||||
 | 
			
		||||
1. 请求1由优先级最高的服务器1处理,服务器1的优先级相应减1,此时各服务器优先级为{3,1,1};
 | 
			
		||||
1. 请求2由目前优先级最高的服务器1进行处理,服务器1优先级相应减1,此时各服务器优先级为{2,1,1}。
 | 
			
		||||
1. 以此类推,直到处理完这6个请求。每个请求处理完后,相应服务器的优先级会减1。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/82/46/8255a6acab7187c77397fecafdefd846.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
最终的处理结果是,服务器1处理请求1~4,服务器2处理请求5,服务器3会处理请求6。
 | 
			
		||||
 | 
			
		||||
以上就是顺序轮询和加权轮询的核心原理了。轮询策略的应用比较广泛,比如**Nginx默认的负载均衡策略就是一种改进的加权轮询策略。**我们具体看看它的核心原理吧。
 | 
			
		||||
 | 
			
		||||
首先,我来解释下Nginx轮询策略需要用到的变量吧。
 | 
			
		||||
 | 
			
		||||
- weight:配置文件中为每个服务节点设置的服务节点权重,固定不变。
 | 
			
		||||
- effective_weight:服务节点的有效权重,初始值为weight。 在Nginx的源码中有一个最大失败数的变量max_fails,当服务发生异常时,则减少相应服务节点的有效权重,公式为effective_weight = effective_weight - weight / max_fails;之后再次选取本节点,若服务调用成功,则增加有效权重,effective_weight ++ ,直至恢复到weight。
 | 
			
		||||
- current_weight:服务节点当前权重,初始值均为0,之后会根据系统运行情况动态变化。
 | 
			
		||||
 | 
			
		||||
假设,各服务器的优先级是{4,1,1},我还是将6个请求分为6步来进行讲解,如表所示:
 | 
			
		||||
 | 
			
		||||
1. 遍历集群中所有服务节点,使用current_weight = current_weight + effective_weight,计算此时每个服务节点的current_weight,得到current_weight为{4,1,1},total为4+1+1=6。选出current_weight值最大的服务节点即服务器1来处理请求,随后服务器1对应的current_weight减去此时的total值,即4 - 6,变为了-2 。
 | 
			
		||||
1. 按照上述步骤执行,首先遍历,按照current_weight = current_weight + effective_weight计算每个服务节点current_weight的值,结果为{2,2,2},total为6,选出current_weight值最大的服务节点。current_weight 最大值有多个服务节点时,直接选择第一个节点即可,在这里选择服务器1来处理请求,随后服务器1对应的current_weight值减去此时的total,即2 - 6,结果为-4。
 | 
			
		||||
1. 以此类推,直到处理完这6个请求。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/e7/56/e7e9d3e908bccd34fc4d5e9c9d3bcc56.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
最终的处理结果为,服务器1处理请求1、2、4、6,服务器2处理请求3,服务器3会处理请求5。
 | 
			
		||||
 | 
			
		||||
可以看到,与普通的加权轮询策略相比,这种轮询策略的优势在于,**当部分请求到来时,不会集中落在优先级较高的那个服务节点。**
 | 
			
		||||
 | 
			
		||||
还是上面的例子,假设只有4个请求,按照普通的加权轮询策略,会全部由服务器1进行处理,即{1,1,1,1};而按照这种平滑的加权轮询策略的话,会由服务器1和2共同进行处理,即{1,1,2,1}。
 | 
			
		||||
 | 
			
		||||
**轮询策略的优点**就是,实现简单,且对于请求所需开销差不多时,负载均衡效果比较明显,同时加权轮询策略还考虑了服务器节点的异构性,即可以让性能更好的服务器具有更高的优先级,从而可以处理更多的请求,使得分布更加均衡。
 | 
			
		||||
 | 
			
		||||
但**轮询策略的缺点**是,每次请求到达的目的节点不确定,不适用于有状态请求的场景。并且,轮询策略主要强调请求数的均衡性,所以不适用于处理请求所需开销不同的场景。
 | 
			
		||||
 | 
			
		||||
比如,有两个服务器(节点A和节点B)性能相同,CPU个数和内存均相等,有4个请求需要处理,其中请求1和请求3需要1个CPU,请求2和请求4需要2个CPU。根据轮询策略,请求1和请求3由节点A、请求2和请求4由节点B处理。由此可见,节点A和节点B关于CPU的负载分别是2和4,从这个角度来看,两个节点的负载并不均衡。
 | 
			
		||||
 | 
			
		||||
综上所述,**轮询策略适用于用户请求所需资源比较接近的场景**。
 | 
			
		||||
 | 
			
		||||
### 随机策略
 | 
			
		||||
 | 
			
		||||
随机策略也比较容易理解,指的就是当用户请求到来时,会随机发到某个服务节点进行处理,可以采用随机函数实现。这里,随机函数的作用就是,让请求尽可能分散到不同节点,防止所有请求放到同一节点或少量几个节点上。
 | 
			
		||||
 | 
			
		||||
如图所示,假设有5台服务器Server 1~5可以处理用户请求,每次请求到来时,都会先调用一个随机函数来计算出处理节点。这里,随机函数的结果只能是{1,2,3,4,5}这五个值,然后再根据计算结果分发到相应的服务器进行处理。比如,图中随机函数计算结果为2,因此该请求会由Server2处理。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/be/3e/be9136621188cec945c0aadf19be133e.png" alt="">
 | 
			
		||||
 | 
			
		||||
这种方式的优点是,实现简单,但缺点也很明显,与轮询策略一样,每次请求到达的目的节点不确定,不适用于有状态的场景,而且没有考虑到处理请求所需开销。除此之外,随机策略也没有考虑服务器节点的异构性,即性能差距较大的服务器可能处理的请求差不多。
 | 
			
		||||
 | 
			
		||||
因此,随机策略适用于,集群中服务器节点处理能力相差不大,用户请求所需资源比较接近的场景。
 | 
			
		||||
 | 
			
		||||
比如,我在[第19篇文章](https://time.geekbang.org/column/article/160408)中提到的RPC框架Dubbo,当注册中心将服务提供方地址列表返回给调用方时,调用方会通过负载均衡算法选择其中一个服务提供方进行远程调用。关于负载均衡算法,Dubbo提供了随机策略、轮询策略等。
 | 
			
		||||
 | 
			
		||||
### 哈希和一致性哈希策略
 | 
			
		||||
 | 
			
		||||
无论是轮询还是随机策略,对于一个客户端的多次请求,每次落到的服务器很大可能是不同的,如果这是一台缓存服务器,就会对缓存同步带来很大挑战。尤其是系统繁忙时,主从延迟带来的同步缓慢,可能会造成同一客户端两次访问得到不同的结果。解决方案就是,利用哈希算法定位到对应的服务器。
 | 
			
		||||
 | 
			
		||||
哈希和一致性哈希,是数据负载均衡的常用算法。 我在[第25篇文章](https://time.geekbang.org/column/article/168940)介绍哈希与一致性哈希时,提到过:数据分布算法的均匀性,一方面指数据的存储均匀,另一方面也指数据请求的均匀。
 | 
			
		||||
 | 
			
		||||
数据请求就是用户请求的一种,哈希、一致性哈希、带有限负载的一致性哈希和带虚拟节点的一致性哈希算法,同样适用于请求负载均衡。
 | 
			
		||||
 | 
			
		||||
所以,**哈希与一致性策略的优点**是,哈希函数设置合理的话,负载会比较均衡。而且,相同key的请求会落在同一个服务节点上,可以用于有状态请求的场景。除此之外,带虚拟节点的一致性哈希策略还可以解决服务器节点异构的问题。
 | 
			
		||||
 | 
			
		||||
但其**缺点是**,当某个节点出现故障时,采用哈希策略会出现数据大规模迁移的情况,采用一致性哈希策略可能会造成一定的数据倾斜问题。同样的,这两种策略也没考虑请求开销不同造成的不均衡问题。
 | 
			
		||||
 | 
			
		||||
应用哈希和一致性哈希策略的框架有很多,比如Redis、Memcached、Cassandra等,你可以再回顾下[第25篇文章](https://time.geekbang.org/column/article/168940)中的相关内容。
 | 
			
		||||
 | 
			
		||||
除了以上这些策略,还有一些负载均衡策略比较常用。比如,根据服务节点中的资源信息(CPU,内存等)进行判断,服务节点资源越多,就越有可能处理下一个请求;再比如,根据请求的特定需求,如请求需要使用GPU资源,那就需要由具有GPU资源的节点进行处理等。
 | 
			
		||||
 | 
			
		||||
### 对比分析
 | 
			
		||||
 | 
			
		||||
以上,就是轮询策略、随机策略、哈希和一致性哈希策略的主要内容了。接下来,我再通过一个表格对比下这三种方法,以便于你学习和查阅。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/5c/29/5c9708fcd30753cfa2dc8ebd1acd6329.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
## 知识扩展:如果要考虑请求所需资源不同的话,应该如何设计负载均衡策略呢?
 | 
			
		||||
 | 
			
		||||
上面提到的轮询策略、随机策略,以及哈希和一致性哈希策略,主要考虑的是请求数的均衡,并未考虑请求所需资源不同造成的不均衡问题。那么,如何设计负载均衡策略,才能解决这个问题呢?
 | 
			
		||||
 | 
			
		||||
其实,这个问题的解决方案有很多,常见的思路主要是对请求所需资源与服务器空闲资源进行匹配,也称调度。
 | 
			
		||||
 | 
			
		||||
关于调度,不知你是否还记得[第11篇文章](https://time.geekbang.org/column/article/150811)所讲的单体调度?我们可以**使用单体调度的思路**,让集群选举一个主节点,每个从节点会向主节点汇报自己的空闲资源;当请求到来时,主节点通过资源调度算法选择一个合适的从节点来处理该请求。
 | 
			
		||||
 | 
			
		||||
在这篇文章中,我提到了最差匹配和最佳匹配算法。这两种算法各有利弊,最差匹配算法可以尽量将请求分配到不同机器,但可能会造成资源碎片问题;而最佳匹配算法,虽然可以留出一些“空”机器来处理开销很大的请求,但会造成负载不均的问题。因此,它们适用于不同的场景,你可以再回顾下[第11篇文章](https://time.geekbang.org/column/article/150811)中的相关内容。
 | 
			
		||||
 | 
			
		||||
除此之外,**一致性哈希策略**也可以解决这个问题:让请求所需的资源和服务器节点的空闲资源,与哈希函数挂钩,即通过将资源作为自变量,带入哈希函数进行计算,从而映射到哈希环中。
 | 
			
		||||
 | 
			
		||||
比如,我们设置的哈希函数结果与资源正相关,这样就可以让资源开销大的请求由空闲资源多的服务器进行处理,以实现负载均衡。但这种方式也有个缺点,即哈希环上的节点资源变化后,需要进行哈希环的更新。
 | 
			
		||||
 | 
			
		||||
## 总结
 | 
			
		||||
 | 
			
		||||
今天,我主要带你学习了分布式高可靠技术中的负载均衡。
 | 
			
		||||
 | 
			
		||||
首先,我以超市收银为例,与你介绍了什么是负载均衡。负载均衡包括数据负载均衡和请求负载均衡,我在[第25篇文章](https://time.geekbang.org/column/article/168940)中介绍的数据分布其实就是数据的负载均衡,所以我今天重点与你分享的是请求的负载均衡。
 | 
			
		||||
 | 
			
		||||
然后,我与你介绍了常见的负载均衡策略,包括轮询策略、随机策略、哈希和一致性哈希策略。其中,轮询策略和随机策略,因为每次请求到达的目的节点不确定,只适用于无状态请求的场景;而哈希和一致性哈希策略,因为相同key的请求会落在同一个服务节点上,所以可以用于有状态请求的场景。
 | 
			
		||||
 | 
			
		||||
最后,我再通过一张思维导图来归纳一下今天的核心知识点吧。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/05/be/05ab2dc9bc1fa38073ef7044b6c6b3be.png" alt="">
 | 
			
		||||
 | 
			
		||||
加油,相信通过本讲的学习,你对分布式系统中的负载均衡有了一定的理解,也可以进一步对电商系统、火车票系统等涉及的请求负载均衡的问题进行分析了。加油,行动起来吧!
 | 
			
		||||
 | 
			
		||||
## 思考题
 | 
			
		||||
 | 
			
		||||
在分布式系统中,负载均衡技术除了各节点共同分担请求外,还有什么好处呢?
 | 
			
		||||
 | 
			
		||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										168
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/29 | 分布式高可靠之流量控制:大禹治水,在疏不在堵.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/29 | 分布式高可靠之流量控制:大禹治水,在疏不在堵.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,168 @@
 | 
			
		||||
<audio id="audio" title="29 | 分布式高可靠之流量控制:大禹治水,在疏不在堵" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/07/751d9f9e5c2b5e22f9f42e15f1a42b07.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好!我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
 | 
			
		||||
 | 
			
		||||
在上一篇文章中,我带你学习了分布式高可靠中的负载均衡。负载均衡的核心在于,将用户请求均匀分配到多个处理服务器处理,以解决单个服务器的单点瓶颈问题。但,如果用户请求数非常多的话,即便实现了负载均衡,服务器能力达到上限,还是无法处理所有的用户请求。
 | 
			
		||||
 | 
			
		||||
比如,类似双十一、双十二的秒杀场景,用户流量突增时,即使做了负载均衡,我们仍然会感受到点击抢购时,需要等待较长的时间。这背后的原理是什么呢?
 | 
			
		||||
 | 
			
		||||
你是不是想到了,这是因为系统控制了用户的请求量呢?没错,这就是今天我们要一起打卡的流量控制技术。
 | 
			
		||||
 | 
			
		||||
## 什么是流量控制?
 | 
			
		||||
 | 
			
		||||
说到流量控制,如果你学过计算机网络的话,第一反应肯定是网络传输中的流量控制。网络传输中的流量控制,就是让发送方发送数据的速率不要太快,让接收方来得及接收数据,具体的实现方法就是滑动窗口。
 | 
			
		||||
 | 
			
		||||
简单来讲,滑动窗口指的是,在任意时刻,发送方都维持一个连续的允许发送的数据大小,称为发送窗口;接收方也会维持一个连续的允许接收的数据大小,称为接收窗口。每次发送方给接收方发送数据后,必须收到接收方返回的确认消息,发送窗口才可向后移动,发送新的数据。
 | 
			
		||||
 | 
			
		||||
接下来,我们通过一个简单的例子,来看看滑动窗口在网络流量控制中,是如何发挥作用的吧。如图所示,发送窗口和接收窗口大小均为1,发送方发送数据D1后,只有接收到来自接收方的确认消息ACK,发送窗口才可向后移动,即发送方才可以发送后续数据D2。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/c9/44/c9eecdcf84f654a1ec95cf611f50a244.png" alt="">
 | 
			
		||||
 | 
			
		||||
这是网络传输中的流量控制,那么**具体到分布式系统中,流量控制又是什么呢**?
 | 
			
		||||
 | 
			
		||||
在前面提到的双十一、双十二秒杀场景中,用户流量突增,在这种高并发、大流量的情况下,服务器的处理能力成为电商系统的瓶颈,处理不好就会导致系统崩溃,服务不可用。而分布式系统中的流量控制,就是解决这类问题的一种关键技术。
 | 
			
		||||
 | 
			
		||||
通俗地说,分布式流量控制就是在分布式系统下,控制每个服务器接收的请求数,以保证服务器来得及处理这些请求,也就是说尽可能保证用户请求持续地被处理,而不是让大量的用户请求“阻塞”在服务器中,等待被执行。这就好比“大禹治水,在疏不在堵”。
 | 
			
		||||
 | 
			
		||||
接下来,我们就一起学习下分布式系统常用的流量控制策略吧。
 | 
			
		||||
 | 
			
		||||
## 分布式系统流量控制策略
 | 
			
		||||
 | 
			
		||||
还记得[第21篇文章](https://time.geekbang.org/column/article/163492)中讲到的消息队列吗?消息队列就是实现流量控制的一种方法,通过一个消息队列来存放用户的消息,然后服务器到消息队列中逐个消费,就可以避免消息过多时服务器处理不过来的情况。
 | 
			
		||||
 | 
			
		||||
除此之外,分布式系统的流量控制策略还有很多,常用的主要包括两种:漏桶策略和令牌桶策略。
 | 
			
		||||
 | 
			
		||||
### 漏桶策略
 | 
			
		||||
 | 
			
		||||
相信你看到“漏桶”两个字,头脑里应该已经有了一个漏桶的样子。确实,名字就已经很形象地说明了这种策略的含义。
 | 
			
		||||
 | 
			
		||||
如下图所示,有一个固定容量的水桶,桶底有一个小洞,水桶可以接收任意速率的水流,但无论水桶里有多少水,水从小洞流出的速率始终不变,桶里的水满了之后,水就会溢出。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/46/fb/46199df7adfecb93498361d1e747b5fb.png" alt="">
 | 
			
		||||
 | 
			
		||||
漏桶策略借鉴上述原理,无论用户请求有多少,无论请求速率有多大,“漏桶”都会接收下来,但从漏桶里出来的请求是固定速率的,保证服务器可以处理得游刃有余。当“漏桶”因为容量限制放不下更多的请求时,就会选择丢弃部分请求。这种思路其实就是一种“宽进严出”的策略。
 | 
			
		||||
 | 
			
		||||
比如,在某段时间内,系统每秒会有10个用户发出请求,但这些请求经过漏桶后,每秒始终只流出2个请求,也就是说服务器每秒最多处理2个请求。这样的话,无论请求速率有多大,都能达到限流的目的,避免服务器在短暂时间内需要处理大量请求,但由于处理能力受限导致系统崩溃,从而保证了系统的高可靠。
 | 
			
		||||
 | 
			
		||||
这种策略的好处是,做到了流量整形,即无论流量多大,即便是突发的大流量,输出依旧是一个稳定的流量。但其缺点是,对于突发流量的情况,因为服务器处理速度与正常流量的处理速度一致,会丢弃比较多的请求。但是,当突发大流量到来时,服务器最好能够更快地处理用户请求,这也是分布式系统大多数情况下想要达到的效果。
 | 
			
		||||
 | 
			
		||||
所以说,**漏桶策略适用于间隔性突发流量且流量不用即时处理的场景**,即可以在流量较小时的“空闲期”,处理大流量时流入漏桶的流量;不适合流量需要即时处理的场景,即突发流量时可以放入桶中,但缺乏效率,始终以固定速率进行处理。
 | 
			
		||||
 | 
			
		||||
目前,漏桶算法已经用于很多框架了,比如阿里开源的流量控制框架Sentinel中的匀速排队限流策略,就采用了漏桶算法;分布式追踪系统Jaeger中,有一种采集策略是速率限制类型,内部使用的也是漏桶算法等。
 | 
			
		||||
 | 
			
		||||
### 令牌桶策略
 | 
			
		||||
 | 
			
		||||
令牌桶策略,也是一个很形象的名字,指的是桶里放着很多令牌,请求只有拿到令牌才能被服务器处理。
 | 
			
		||||
 | 
			
		||||
如图所示,有一个固定容量的存放令牌的桶,我们以固定速率向桶里放入令牌,桶满时会丢弃多出的令牌。每当请求到来时,必须先到桶里取一个令牌才可被服务器处理,也就是说只有拿到了令牌的请求才会被服务器处理。所以,你可以将令牌理解为门卡,只有拿到了门卡才能顺利进入房间。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/31/88/31e10a5be4b2a45f60c929af19cedc88.png" alt="">
 | 
			
		||||
 | 
			
		||||
同样的,我们通过一个具体的例子,来加深对令牌桶策略的理解吧。
 | 
			
		||||
 | 
			
		||||
假设,令牌以每秒3个的速率放入到令牌桶中,桶的容量为10。通常情况下, 每秒会有2个用户请求,请求到来时就会到桶里取一个令牌,由于请求的速率低于放令牌的速率,因此令牌桶里令牌会逐渐增多,直到达到桶的容量。超过桶容量后,令牌会被丢弃。
 | 
			
		||||
 | 
			
		||||
当大流量到来时,比如某个时刻来了10个请求,此时桶里有10个令牌,因此,请求都会被服务器处理;但如果来的请求数不止10个,令牌会被取完,多余的请求取不到令牌,也就没办法及时被服务器处理,需要等待令牌。
 | 
			
		||||
 | 
			
		||||
通过上述的例子,就能看出这种策略的好处:当有突发大流量时,只要令牌桶里有足够多的令牌,请求就会被迅速执行。通常情况下,令牌桶容量的设置,可以接近服务器处理的极限,这样就可以有效利用服务器的资源。因此,这种策略**适用于有突发特性的流量,且流量需要即时处理的场景**。
 | 
			
		||||
 | 
			
		||||
在实际使用中,令牌桶算法也很常见。比如,Google开源工具包Guava提供的限流工具类RateLimiter,就是基于令牌桶算法来完成限流的。
 | 
			
		||||
 | 
			
		||||
### 两种策略对比
 | 
			
		||||
 | 
			
		||||
以上就是漏桶策略和令牌桶策略的核心原理了,接下来我们通过一张表格对比下这两种策略吧。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/7f/5e/7fd88d2b5ae52e0ca2eff1c4d957b65e.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
## Sentinel流量控制工作原理
 | 
			
		||||
 | 
			
		||||
我们都知道阿里的流量控制做得很好,特别是双十一、抢购等情况下。接下来,我以阿里开源的流量控制框架Sentinel为例,与你进一步介绍流量控制的工作原理。
 | 
			
		||||
 | 
			
		||||
Sentinel的核心是,监控应用的并发线程数或QPS(请求数 /每秒)指标,当达到系统设定的阈值时,Sentinel可以采取一定的策略对流量进行控制,以避免应用被瞬时高流量击垮,从而保证应用高可靠。
 | 
			
		||||
 | 
			
		||||
为此,在Sentinel中,关于流量控制有两种方式:一种是通过并发线程数进行流量控制,另一种是通过QPS指标进行流量控制。
 | 
			
		||||
 | 
			
		||||
**首先,我们看一下通过并发线程数进行流量控制。**
 | 
			
		||||
 | 
			
		||||
要理解这种限流方式,我需要先带你搞清楚什么是线程池。
 | 
			
		||||
 | 
			
		||||
我们知道,过多的线程会消耗非常多的系统资源,包括线程资源消耗、线程调度消耗等。为了解决这个问题,我们引入了线程池。线程池维护了多个启动着的线程,随时等待着去执行系统分配的任务,即系统每次需要处理任务时,可以直接从线程池中取线程,从而避免了创建和销毁线程的时间和资源等消耗。
 | 
			
		||||
 | 
			
		||||
同一时刻每个线程只能执行一个任务或请求,因此,可以通过并发线程数进行流量控制。我们看一个案例吧。
 | 
			
		||||
 | 
			
		||||
如图所示,假设现在线程池中有3个线程也就是说,最大并发处理数为3,现在有2个请求Q1和Q2到来,由于请求数少于线程数,因此请求可以被并发执行。线程池中启动着的线程1和线程2会进行相应的处理,而不会创建新线程,除此之外,线程处理完请求后也不会被销毁,而是回到线程池中继续等待新的请求。
 | 
			
		||||
 | 
			
		||||
但如果现在同时有4个请求到来,那么只有3个请求可以被并发处理,而剩下的一个请求要么丢弃,要么等待空闲线程。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/db/33/db6768e99d0fb5fb78a3b588f4b6ef33.png" alt="">
 | 
			
		||||
 | 
			
		||||
在分布式系统中,每个请求都会由一个线程去进行处理。当请求太多系统处理不过来时,意味着线程池可能已经被耗尽(线程池中无空闲线程),因此当请求过多时,执行请求的并发线程数自然会随之增加,当超过一定的阈值(比如线程池中线程总数)时,需要采取一定的策略来进行流量控制。
 | 
			
		||||
 | 
			
		||||
在Sentinel中,就采用了直接拒绝的方式,即新来的请求会直接拒绝。
 | 
			
		||||
 | 
			
		||||
**然后,我们再看一下通过QPS指标进行流量控制吧。**
 | 
			
		||||
 | 
			
		||||
QPS是指每秒的请求数,大流量也就意味着QPS大。当QPS达到阈值时,Sentinel提供了三种流量控制策略,分别是直接拒绝、预热(Warm Up)和匀速排队。
 | 
			
		||||
 | 
			
		||||
**直接拒绝,是最直接也是最暴力的方式**,与并发线程数流量控制采取的方式一致,就是当QPS达到系统设定的阈值时,直接拒绝新来的请求。
 | 
			
		||||
 | 
			
		||||
这种策略乍一听起来确实不是很好,但对于系统处理能力确切已知的情况(即阈值设定为每秒能接受的最大处理请求数),却非常实用。当请求超出阈值时,可以直接拒绝,因为系统已经没有更多的能力来处理多余的请求了。因此,该策略适用于对系统处理能力确切已知的场景。
 | 
			
		||||
 | 
			
		||||
接下来,我们看看**预热**。当系统的QPS长期处于一个较低水平时,一旦发生流量骤增,如果直接让系统每秒处理大量的请求,可能会因为服务器处理能力不足,导致系统崩溃。因此,Sentinel提供了一种“预热”机制,让系统的QPS缓慢增加,在一定的时间内逐渐增加到上限。
 | 
			
		||||
 | 
			
		||||
下面以一个例子为例,带你进一步理解预热的原理。如下图所示,假设通常情况下系统每秒处理3个请求,即QPS=3,当用户请求增加时,系统每秒处理的请求数相应增加,但不会一下子提高很多。比如,每秒增加1个处理请求,逐步达到QPS=10的处理上限,并不再继续增加,从而避免大流量一下子导致系统故障。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/b1/15/b1202d83009b9821cdd8552c935c5a15.png" alt="">
 | 
			
		||||
 | 
			
		||||
可以看出,预热这种策略有点像是一种特殊的令牌桶:放令牌的速率通常保持在一个较低的水平,当流量突增时,放令牌的速率不会一下子提高到最高水平,而是会慢慢增加,直到增加到最大速率则不可再增加。因此,该策略与令牌桶策略的适用场景类似,即适用于具有突发特性的流量,且流量可以即时处理的场景。
 | 
			
		||||
 | 
			
		||||
**匀速排队**的思想,其实本质就是漏桶策略。它会严格控制系统每秒处理的请求数,请求数很多时,请求之间的间隔也会保持一致。
 | 
			
		||||
 | 
			
		||||
如图所示,当QPS=5时,每隔200ms才允许服务器处理下一个请求。假设请求队列中有10个请求瞬间到达,服务器不会一下子全处理完,而是按照请求的顺序,每200ms处理一个请求,直到处理完所有请求。这时,处理的请求就像是在匀速排队,因此得名。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/17/ac/1762aebb4c415ef80f73593873f7f8ac.png" alt="">
 | 
			
		||||
 | 
			
		||||
该策略中,系统会设定一个时间间隔T,假设最大排队时长设置为6T,上次请求通过的时刻为t1。当新的请求在t2时刻到来的话,则进行判断,首先查看是否还有其他请求在排队。如果没有请求在排队,分两种情况:
 | 
			
		||||
 | 
			
		||||
- 当t2 - t1的值大于或等于时间间隔T,请求可以通过;
 | 
			
		||||
- 当t2 - t1的值小于T时,需要等待,直到t2 - t1的值达到时间间隔T时,才可以让请求通过。
 | 
			
		||||
 | 
			
		||||
而如果新请求到来时,已经有请求在排队,就需要计算该新请求的预期通过时间。比如,有3个请求在排队,则该新请求预期通过时间为t1+4T,因为需要等到在该请求前面的请求都通过后该请求才可通过,且两个请求通过的时间间隔必须达到T才可以。
 | 
			
		||||
 | 
			
		||||
另外,若排队的请求过多,新来的请求预期等待时间超出最大排队时长,即等待时间超过6T时,则直接拒接这个请求。
 | 
			
		||||
 | 
			
		||||
现在我想你应该理解了为什么说匀速排队策略本质就是漏桶策略了吧。因此,匀速排队的适用场景与漏桶策略类似,即适用于间隔性突发流量且流量不用即时处理的场景。
 | 
			
		||||
 | 
			
		||||
## 知识扩展:什么是拥塞控制?它与流量控制的区别是什么?
 | 
			
		||||
 | 
			
		||||
其实,在分布式领域拥塞控制与流量控制的区别还是蛮大的。为什么这么说呢?
 | 
			
		||||
 | 
			
		||||
今天,我们讲述的流量控制,主要是指业务上的流量,即用户请求。而拥塞控制通常针对的是网络上传输的数据,即网络上数据传输出现拥塞时应当如何控制。所以,这两个概念不是一回事儿。
 | 
			
		||||
 | 
			
		||||
但是,**对于网络上数据的传输而言,流量控制与拥塞控制非常容易混淆。**
 | 
			
		||||
 | 
			
		||||
网络数据传输中,流量控制是指控制发送方和接收方的传输和接收速率在双方都可以接受的范围,通常使用的方法是滑动窗口;而拥塞控制是通过检测网络状况,随时疏通网络,避免网络中过多数据堆积,导致无法传输数据,包括慢启动与拥塞避免方法。如果你想深入了解拥塞控制的相关内容,可以自行查阅计算机网络的相关书籍。
 | 
			
		||||
 | 
			
		||||
## 总结
 | 
			
		||||
 | 
			
		||||
今天,我主要带你学习了分布式高可靠技术中的流量控制。
 | 
			
		||||
 | 
			
		||||
首先,我以网络传输中的流量控制和电商系统的例子,和你引入了分布式系统中的流量控制,即控制每个服务器的请求数,以保证处理请求所需计算能力在服务器处理能力的上限之内,从而避免系统崩溃。
 | 
			
		||||
 | 
			
		||||
然后,我为你介绍了常见的流量控制策略,包括漏桶策略和令牌桶策略。其中,漏桶策略的核心是“宽进严出”,发送给服务器进行处理的请求速率固定,以避免超过服务器处理能力上限,导致系统崩溃,但这种方式不适合突发流量增加的场景。令牌桶策略的核心是,只要桶里有令牌,请求就可以被处理,只要在服务器处理能力内即可,所以适用于处理及时且处理速率非固定的场景。
 | 
			
		||||
 | 
			
		||||
最后,我和你分享了阿里开源的Sentinel流量控制,并介绍了通过并发线程数和通过QPS指标进行流量控制的两种方式。
 | 
			
		||||
 | 
			
		||||
最后,我再通过一张思维导图来归纳一下今天的核心知识点吧。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/1f/e7/1f4cb85a61786d8a2080e8a59768eae7.png" alt="">
 | 
			
		||||
 | 
			
		||||
加油,相信通过本讲的学习,你对分布式系统中的流量控制有了一定的理解,也可以进一步对电商系统中抢购、秒杀中的流量控制问题进行分析了。加油,行动起来吧!
 | 
			
		||||
 | 
			
		||||
## 思考题
 | 
			
		||||
 | 
			
		||||
除了漏桶策略和令牌桶策略,你还知道哪些流量控制策略吗?它们的原理是什么呢?
 | 
			
		||||
 | 
			
		||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										132
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/30 | 分布式高可用之故障隔离:当断不断,反受其乱.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/30 | 分布式高可用之故障隔离:当断不断,反受其乱.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
			
		||||
<audio id="audio" title="30 | 分布式高可用之故障隔离:当断不断,反受其乱" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/37/75/37fcf6cc3f54c4da6bbf71ec738a7375.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
 | 
			
		||||
 | 
			
		||||
在前面两篇文章中,我带你一起学习了分布式系统高可靠的关键技术,包括分布式负载均衡和流量控制。除了高可靠,在实际生产中,分布式系统的高可用问题也极其重要。
 | 
			
		||||
 | 
			
		||||
比如,在双十一的抢购高峰期,如果分布式系统不能满足高可用的特性,那么当大量用户同时抢购时就可能导致系统崩溃,无法提供服务,导致大量用户流失。
 | 
			
		||||
 | 
			
		||||
因此,在接下来的两篇文章,我将从故障隔离和恢复机制这两项关键技术入手,和你一起学习如何保证分布式系统的高可用。
 | 
			
		||||
 | 
			
		||||
今天,我们就先一起打卡分布式高可用中的故障隔离吧。
 | 
			
		||||
 | 
			
		||||
## 什么是故障隔离?
 | 
			
		||||
 | 
			
		||||
从字面意思来看,故障隔离就是,把故障通过某种方式与其他正常模块进行隔离,以保证某一模块出现故障后,不会影响其他模块。
 | 
			
		||||
 | 
			
		||||
其实,我们生活有很多故障隔离的例子,比如交通。一辆车就类似于分布式系统中的一个模块,当一辆车在高速公路上出现故障后,我们通常会将其停靠在紧急车道,或者在其前后设置故障指示牌,以防止其他车辆与其相撞,引起更大的交通事故。这种将故障车辆停靠在路边紧急车道或设置故障指标牌的方法,就是一种故障隔离。
 | 
			
		||||
 | 
			
		||||
现在我们回到分布式系统,故障隔离,就是采用一定的策略,以实现当某个模块故障时,不会影响其他模块继续提供服务,以保证整个系统的可用性。所以说,故障隔离,可以避免分布式系统出现大规模的故障,甚至是瘫痪,降低损失。
 | 
			
		||||
 | 
			
		||||
在分布式系统中,要实现故障隔离,通常需要在进行系统设计时,提前对可能出现的故障进行预防,以使得在出现故障后能实现故障隔离。此外,由于是提前设计预防的,因此故障隔离还可以帮助我们快速定位故障点。
 | 
			
		||||
 | 
			
		||||
也就是说,分布式系统中的故障隔离策略是在系统设计时就进行考虑,从预防的角度来实现故障发生时,该模块故障不会影响其他模块。因此,**我今天与你介绍的故障隔离策略,是整个系统设计时,从高可用这个维度进行设计的策略。**
 | 
			
		||||
 | 
			
		||||
好了,理解了故障隔离为什么可以提高分布式系统的可用性以后,我们再来看看实现故障隔离有哪些常见策略吧。
 | 
			
		||||
 | 
			
		||||
## 分布式故障隔离策略
 | 
			
		||||
 | 
			
		||||
分布式系统中的故障隔离策略有很多,大体上可以从两个维度来划分:
 | 
			
		||||
 | 
			
		||||
- 一类是以系统功能模块为粒度进行隔离。比如,通过系统功能/服务划分,将系统分为多个功能/服务模块,各个功能/服务模块之间实现松耦合,即一个功能/服务模块出现故障,不会影响其他功能/服务模块,根据功能模块或服务由线程执行还是进程执行,通常分为线程级隔离、进程级隔离。
 | 
			
		||||
- 另一类是,通过资源隔离来实现。比如,系统中各个模块拥有自己独立的资源,不会发生资源争抢,从而大大提升系统性能。根据资源所属粒度,通常包括进程级隔离(比如采用容器隔离)、虚拟机隔离、服务器隔离和机房隔离等。
 | 
			
		||||
 | 
			
		||||
基于这个分类,接下来,我将为你讲述三种比较常见的故障隔离策略,包括以功能模块为粒度进行隔离的线程级隔离和进程级隔离,以及以资源为隔离维度的资源隔离。
 | 
			
		||||
 | 
			
		||||
### 线程级隔离
 | 
			
		||||
 | 
			
		||||
线程级故障隔离,是指使用不同的线程池处理不同的请求任务。当某种请求任务出现故障时,负责其他请求任务的线程池不会受到影响,即会继续提供服务,从而实现故障的隔离。
 | 
			
		||||
 | 
			
		||||
如图所示,以电商购物平台为例,假设初期运行在单台机器的一个进程中,在这个进程中有三个线程池,分别负责订单任务、支付任务和配送任务。这样,当订单请求出现故障时,不会影响已下单用户的支付和仓库配送服务。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/43/85/43500cb5d70138a423e6d1fc2187a885.png" alt="">
 | 
			
		||||
 | 
			
		||||
**线程级的故障隔离策略,在生产环境中较为常用,尤其对于单体应用**(单进程多线程的应用)。在单体应用场景下,应用被单个进程执行,但单进程中包括多个线程,因此该场景下,只需要实现线程级隔离即可,实现简单、效果好,因此是一种很常用的方式。
 | 
			
		||||
 | 
			
		||||
系统实现线程级隔离后,线程间的通信通常使用**共享变量**来实现。简单地说,共享变量就是一个进程中的全局变量,在进程的各个线程间可以同时使用。这种通信方式,实现简单且效果明显。
 | 
			
		||||
 | 
			
		||||
### 进程级隔离
 | 
			
		||||
 | 
			
		||||
随着业务逐渐扩大,业务系统也会越来越复杂,单体应用可能无法满足公司与用户的需求,这时候就需要对系统进行拆分。
 | 
			
		||||
 | 
			
		||||
一种常用的方式就是,将系统按照功能分为不同的进程,分布到相同或不同的机器中。如果系统的进程分布到不同机器上的话,从资源的角度来看,也可以说成是主机级的故障隔离。因为从另一个层面看,系统确实分布到了不同机器上,当某个机器出现故障时,不会对其他机器造成影响。
 | 
			
		||||
 | 
			
		||||
如图所示,电商购物平台可以分为订单系统、支付系统和配送系统三部分。这三个子系统可以采用三个不同的进程来服务用户。
 | 
			
		||||
 | 
			
		||||
这就是一个进程级的故障隔离方案,即不同的子系统对应不同的进程,某一个子系统出现故障,都不会导致其他系统不可用。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/2b/c0/2b8b5ee8a2215c82552a13fcb92709c0.png" alt="">
 | 
			
		||||
 | 
			
		||||
系统实现进程级隔离后,进程间的协同必须通过**进程间通信**(IPC)来实现。进程间通信有很多方式,大体可以分为以下两类:
 | 
			
		||||
 | 
			
		||||
- 如果进程都在同一台机器上,则可以通过管道、消息队列、信号量、共享内存等方式,来实现;
 | 
			
		||||
- 如果进程分布在不同机器上,则可以通过远程调用来实现,你可以再回顾下[第19篇文章](https://time.geekbang.org/column/article/160408)中的相关内容。
 | 
			
		||||
 | 
			
		||||
进程级故障隔离,目前在分布式应用中应用广泛,比如常见的电商、火车票购买等业务都可以采用。
 | 
			
		||||
 | 
			
		||||
### 资源隔离
 | 
			
		||||
 | 
			
		||||
前面介绍的是以服务或功能模块为粒度进行隔离的,下面我们一起看下从资源角度进行隔离是怎么做的?
 | 
			
		||||
 | 
			
		||||
简单来说,资源隔离就是将分布式系统的所有资源分成几个部分,每部分资源负责一个模块,这样系统各个模块就不会争抢资源,即资源之间互不干扰。这种方式不仅可以提高硬件资源利用率,也便于系统的维护与管理,可以大幅提升系统性能。
 | 
			
		||||
 | 
			
		||||
微服务就是一个典型的例子。当前,很多公司都在将自己的业务系统微服务化,比如亚马逊、阿里、华为、微软等。在微服务的理念中,是尽可能将服务最小化,服务与服务之间进行解耦合,包括运行环境的相互隔离等。比如,现在通常采用容器进行隔离,我在[第9篇文章](https://time.geekbang.org/column/article/148187)中分享的Mesos、Kubernetes等可实现容器管理与调度,而Mesos和Kuberntes的上层应用很多都是微服务。
 | 
			
		||||
 | 
			
		||||
实际上,在微服务框架中,一个服务通常对应一个容器,而一个容器其实就是操作系统中一个进程,不同容器负责不同的服务,就类似于刚才所讲的:不同进程负责系统不同的功能模块。
 | 
			
		||||
 | 
			
		||||
如图所示,如果将电商购物平台微服务化,则可以启动三个容器,分别负责订单服务、支付服务和配送服务。一个容器对应一个进程,因此微服务框架本质上还是一种进程级故障隔离策略。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/55/b8/55e56fcffbdf4a8d67092e5172a134b8.png" alt="">
 | 
			
		||||
 | 
			
		||||
但与进程级隔离不同的是,微服务框架采用容器进行故障隔离。容器虽然本质上是操作系统的一个进程,但具备普通进程不具备的特性,比如资源隔离。
 | 
			
		||||
 | 
			
		||||
- 一个普通进程有很大的计算或内存需求时,可能会占满物理机上所有的CPU、内存资源,导致其他进程没有资源可用,引发进程间的资源争夺;
 | 
			
		||||
- 但容器可以实现资源限制,让每个容器占用的资源都有一个上限,比如CPU、内存,均会设置一个上限值,这个上限值限定了该容器的处理能力,就好比一台服务器具有资源上限值一样。因此,一个容器使用的资源不会影响其他容器的资源,从而避免资源争抢,提高性能。
 | 
			
		||||
 | 
			
		||||
那到底什么是容器呢? 容器是一种虚拟化技术,可以为应用提供一整套运行环境。容器通过限制自身使用的资源来实现资源隔离,从而让容器就像一个个的“集装箱”:容量固定,存放着任意的物品。
 | 
			
		||||
 | 
			
		||||
目前,比较常用的容器是Docker。Docker主要使用Linux内核中的Linux Cgroups模块来设置容器的资源上限,包括CPU、内存、磁盘、网络带宽等。通过Cgroups模块,容器间就形成了资源隔离,从而避免了容器间的资源争夺,提升了系统性能。
 | 
			
		||||
 | 
			
		||||
通过容器进行资源隔离后,需要容器进行网络配置来进行容器间的通信。比如,Docker默认是通过建立虚拟网桥来实现容器间通信的。如果你想深入了解容器网络配置相关的内容,可以自行查阅[Docker官方文档中网络部分](https://docs.docker.com/network/)的内容。
 | 
			
		||||
 | 
			
		||||
除了容器级别的资源隔离,虚拟机级别的隔离也是资源隔离的一种常用手段,一台物理机可以安装多个虚拟机,每个虚拟机都会分配一定的资源,即进行资源隔离。除此之外,主机级别的隔离也可以说是一种资源隔离,每台机器的资源是独享的,不会与其他机器发生资源争夺,从而做到资源隔离。
 | 
			
		||||
 | 
			
		||||
除了以上所讲到的故障隔离策略,其实还有一些更粗粒度的隔离策略,比如集群隔离、机房隔离等,这些策略主要是跨集群或跨地域的隔离策略。这些粗粒度的隔离策略,不仅可以根据系统功能/服务等维度对系统进行划分,比如每个功能/服务由一个集群或一个机房单独负责,而且也是一种资源隔离策略,即集群间或机房间资源互相隔离,不会发生资源争夺,互不影响。
 | 
			
		||||
 | 
			
		||||
### 故障隔离策略综合对比
 | 
			
		||||
 | 
			
		||||
以上,就是分布式应用中常用的几种故障隔离策略了。接下来,我再通过一个表格进行对比分析,以便于你理解和记忆。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/72/fc/7233f358e26052b4e9f58f67bb514afc.jpg" alt="">
 | 
			
		||||
 | 
			
		||||
## 知识扩展:从用户角度看,有哪些常用的故障隔离方案?
 | 
			
		||||
 | 
			
		||||
无论是按照功能/服务划分模块,实现进程级、虚拟机级等故障隔离,还是按照系统资源进行故障隔离,它们都是一种针对服务方的故障隔离手段。除此之外,还有一种故障隔离策略是,针对用户的,即用户级别的故障隔离。
 | 
			
		||||
 | 
			
		||||
用户级别的故障隔离是指,将不同用户分开,当系统出现故障时,只影响部分用户,而不是全体用户。比如,发布产品前大多会有一个“灰度发布”过程, 就是先发布给一小部分用户进行测试,如果没问题再大规模发布;如果有问题也只是影响一小部分用户。这就是一种典型的用户级别的故障隔离。
 | 
			
		||||
 | 
			
		||||
常用的用户级别故障隔离策略,有数据分片、负载均衡等。你可以再回顾下[第25篇文章](https://time.geekbang.org/column/article/168940)中关于数据分片,以及[第28篇文章](https://time.geekbang.org/column/article/173398)中关于负载均衡技术的相关内容。
 | 
			
		||||
 | 
			
		||||
以数据分片为例,系统可以将不同用户的数据存储到不同的数据库,即一个数据库只存储部分用户的信息。这样当某个数据库出现故障时,仅影响该故障数据库存储的用户,而不会影响全部用户。
 | 
			
		||||
 | 
			
		||||
负载均衡也是这个道理。当处理请求的某个服务器出现故障时,只影响该故障服务器负责的用户请求,而不会影响其他服务器负责的用户请求。
 | 
			
		||||
 | 
			
		||||
## 总结
 | 
			
		||||
 | 
			
		||||
今天,我主要带你学习了分布式高可用技术中的故障隔离技术。
 | 
			
		||||
 | 
			
		||||
首先,我以汽车故障为例,带你了解了故障隔离的概念,并引出分布式系统中的故障隔离。分布式系统中的故障隔离技术是,在进行分布式系统可用性设计时,考虑故障隔离的设计,也就是提前预防或避免出现故障后对整个系统造成影响。
 | 
			
		||||
 | 
			
		||||
然后,我与你介绍了常见的故障隔离策略,包括线程级隔离、进程级隔离和资源隔离策略。其中,线程级隔离和进程级隔离是从对功能/服务模块进行隔离的维度进行划分的,借助了系统本身对线程或进程的隔离机制实现故障隔离;资源隔离是从资源的维度进行隔离,主要通过容器、服务器、集群、机房等物理资源维度进行隔离。
 | 
			
		||||
 | 
			
		||||
最后,我再通过一张思维导图来归纳一下今天的核心知识点吧。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/b5/6b/b5e25369e1645514e9cf8c2d2a52866b.png" alt="">
 | 
			
		||||
 | 
			
		||||
加油,相信通过今天的学习,你对分布式系统中的故障隔离技术有了一定的理解,也可以进一步对容器、虚拟机等的隔离技术进行深入分析了。加油,行动起来吧!
 | 
			
		||||
 | 
			
		||||
## 思考题
 | 
			
		||||
 | 
			
		||||
分布式系统难免会发生故障,那么评判一个系统故障的指标有哪些呢?或者说通过哪些指标可以判断故障的健康度呢?
 | 
			
		||||
 | 
			
		||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
 | 
			
		||||
							
								
								
									
										177
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/31 | 分布式高可用之故障恢复:知错能改,善莫大焉.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/31 | 分布式高可用之故障恢复:知错能改,善莫大焉.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,177 @@
 | 
			
		||||
<audio id="audio" title="31 | 分布式高可用之故障恢复:知错能改,善莫大焉" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c2/2d/c2da8b22249b499a9fce0d08dc98fc2d.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
 | 
			
		||||
 | 
			
		||||
在上一篇文章,我带你学习了故障隔离。故障隔离的目的是,对故障组件进行隔离,以避免其影响系统中的其他组件,尽可能保证分布式系统的可用性。
 | 
			
		||||
 | 
			
		||||
在分布式系统中,故障在所难免,发生故障后仅仅进行隔离还远远不够,还需要进行故障恢复。比如,现在集群中有3个节点,节点1故障后,对节点1进行隔离,如果节点2、节点3紧接着故障了,又隔离了这两个节点。那么,整个集群就无法继续提供服务了,何谈分布式系统的高可用呢?
 | 
			
		||||
 | 
			
		||||
为了解决这种问题,分布式领域还有一个关键技术来保证系统的高可用,即故障恢复。
 | 
			
		||||
 | 
			
		||||
接下来,我们就一起打卡分布式系统的故障恢复技术吧。
 | 
			
		||||
 | 
			
		||||
## 分布式故障基础知识
 | 
			
		||||
 | 
			
		||||
在介绍故障恢复之前,我先与你说说分布式系统中会有哪些故障类型。
 | 
			
		||||
 | 
			
		||||
### 故障类型
 | 
			
		||||
 | 
			
		||||
在任何一个分布式系统中,故障都是不可避免的。这里的故障,通常包括两类:
 | 
			
		||||
 | 
			
		||||
- 一类是物理故障,比如硬盘损坏、断电断网、硬件升级等;
 | 
			
		||||
- 另一类是软件层故障,比如系统存在Bug导致系统崩溃、系统负载过高导致系统崩溃等。
 | 
			
		||||
 | 
			
		||||
在讨论分布式系统故障时,我们通常还会从是否是网络导致的故障的角度来进行故障划分,包括节点故障和网络故障,而这两类故障可能同时包括物理故障和软件层故障。由于软件层故障和具体的程序实现等相关,因此主要由开发者根据自己的实现去解决;而物理故障通常具有很多共同特征,因此**今天我主要针对物理故障导致软件不可用的情况进行讲解**。
 | 
			
		||||
 | 
			
		||||
首先,我们看一下**节点故障。**
 | 
			
		||||
 | 
			
		||||
简单地说,节点故障就是单个机器自身出现故障。比如,由机器A、B,……,Z构成的分布式集群中,机器A自身出现故障,而不是非机器之间的网络连接出现故障,就是节点故障。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/43/28/434bfe18caacd4be581722b589bac828.png" alt="">
 | 
			
		||||
 | 
			
		||||
节点故障有很多种,大体可以分为两类:
 | 
			
		||||
 | 
			
		||||
- 一类是硬件故障,比如机器硬盘损坏、内存接触不良等;
 | 
			
		||||
- 另一类是软件故障,比如由于请求过多,超过服务器处理能力上限,导致无法处理,又或者是机器被攻击,导致机器瘫痪等。
 | 
			
		||||
 | 
			
		||||
节点故障在软件层的表现结果是,该机器无法为用户提供服务。
 | 
			
		||||
 | 
			
		||||
其次,我们看一下**网络故障**。
 | 
			
		||||
 | 
			
		||||
简单地说,网络故障就是分布式集群中,节点之间无法完成通信。比如,由机器A,B,……,Z构成的分布式集群中,机器间比如机器A和B之间无法完成通信,就属于网络故障。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/fa/0d/fa5659ae83d610bd99be333f9353d40d.png" alt="">
 | 
			
		||||
 | 
			
		||||
网络故障也有很多种,比如路由器故障、DNS故障、网络线路断裂等。这些物理故障在软件层的表现结果是,机器间无法通信,影响分布式应用正常提供服务。
 | 
			
		||||
 | 
			
		||||
了解了故障的类型,我们还要搞明白如何检查到故障,也就是如何进行故障检测,因为这是故障恢复的前提。
 | 
			
		||||
 | 
			
		||||
### 故障检测
 | 
			
		||||
 | 
			
		||||
故障检测,就是指通过一定的方式识别或发现故障。就好比,我们把火灾、地震等危险事件看作是故障,采用火灾报警器、地震仪等来检测发现火灾或地震。
 | 
			
		||||
 | 
			
		||||
如果可以提前检测到事件的发生,就能将损失降到最小。**在分布式系统中,检测硬件故障通常比较麻烦,因此会通过查看软件层的表现结果来进行故障检测**。比如,网络故障导致服务器之间无法通信,因此就可以通过检测服务器之间是否可以通信(比如,服务器之间心跳包是否可以正常地发送和接收),来检测是否存在网络故障。
 | 
			
		||||
 | 
			
		||||
关于故障检测的具体策略,我会在后文与你展开。当检测到故障后,就需要进行故障恢复了。
 | 
			
		||||
 | 
			
		||||
### 故障恢复
 | 
			
		||||
 | 
			
		||||
故障恢复,就是指修复分布式系统中出现的故障,使系统恢复正常。简单来说,故障恢复就是故障发生之后的弥补方案,可以理解为对故障进行修正或修复,以保证服务正常运行,有点类似“知错能改,善莫大焉”。
 | 
			
		||||
 | 
			
		||||
接下来,我们就具体看看故障检测和故障恢复的原理或者说策略吧。
 | 
			
		||||
 | 
			
		||||
## 分布式故障检测原理
 | 
			
		||||
 | 
			
		||||
在分布式系统中,常见的故障检测方法是心跳机制。基于心跳进行故障检测的策略主要分为两类,固定心跳检测策略和根据历史心跳信息预测故障策略。
 | 
			
		||||
 | 
			
		||||
还记得我在[第22篇文章](https://time.geekbang.org/column/article/165314)中,与你介绍的通过心跳方式判断集中式架构和非集中式架构中节点是否存活的方法吗?其实,这里用到的就是固定心跳检测策略。具体的检测原理,你可以再回顾下这篇文章。
 | 
			
		||||
 | 
			
		||||
所以接下来,**我主要与你分享基于历史心跳消息预测故障的策略,也就是我们常说的$φ$值故障检测**。
 | 
			
		||||
 | 
			
		||||
$φ$值故障检测是基于心跳间隔符合正态分布的假设进行计算的。其中,$φ$值是用来评估心跳是否超时的概率,是对心跳间隔的概率求对数,将非整数转换为整数以便于理解。
 | 
			
		||||
 | 
			
		||||
$φ$值故障检测方法中,通常会设置一个阈值Ф,若当前心跳计算得到的$φ≥Ф$,则判断心跳超时,否则心跳未超时。
 | 
			
		||||
 | 
			
		||||
那么,**$φ$值是如何计算的呢?**
 | 
			
		||||
 | 
			
		||||
从流程上来讲,$φ$值的计算可以分为三步,即:
 | 
			
		||||
 | 
			
		||||
1. 采样窗口存储心跳到达的时间;
 | 
			
		||||
1. 通过样本计算出心跳到达时间间隔的分布;
 | 
			
		||||
1. 使用得到的正态分布计算当前的$φ$值。
 | 
			
		||||
 | 
			
		||||
接下来,我们就具体看看这三个阶段吧。
 | 
			
		||||
 | 
			
		||||
**第一步:采样窗口存储心跳到达的时间。**
 | 
			
		||||
 | 
			
		||||
采样窗口就是一个具有固定容量的容器,一般存储近k次的心跳信息,每次心跳到达时,会将到达时间存储到采样窗口,如果采样窗口已满,则会删除窗口中最旧的数据。
 | 
			
		||||
 | 
			
		||||
比如,采样窗口最多存储最近10次心跳到达的时间,t1,t2,……, t10,当第11次心跳到来时,采样窗口会将t1删除,存入t11。到达时间的间隔很容易得到,比如第11次心跳到来后,到达时间的间隔是t3 - t2,t4 – t3,……,t11 – t10。通过这些采样数据,可以计算出样本的平均值μ和方差 σ<sup>2</sup>,以便后面计算$φ$值。当然,随着新的心跳到来,这些数据会进行相应的更新。
 | 
			
		||||
 | 
			
		||||
**第二步:通过样本计算出心跳到达时间间隔的分布。**
 | 
			
		||||
 | 
			
		||||
$φ$值故障检测是假设心跳到达时间间隔的分布遵循正态分布,假设P<sub>later</sub>(t)表示接收到上一次心跳之后t个时间片能收到下一次心跳的概率,则通过第一步中得到的样本平均值µ和方差  σ<sup>2</sup>,得到P<sub>later</sub>(t)的计算结果如下:
 | 
			
		||||
 | 
			
		||||
$$P_{later}(t)=\frac{1}{\sigma\sqrt{2\pi}}\int_{t}^{+\infty}e^{-\frac{(x-u)^{2}}{2\sigma^{2}}}dx=1-F(t)$$
 | 
			
		||||
 | 
			
		||||
其中,F(t)是具有均值µ和方差 σ<sup>2</sup>的正态分布的累积分布函数。
 | 
			
		||||
 | 
			
		||||
**第三步:使用得到的正态分布计算当前的$φ$值。**
 | 
			
		||||
 | 
			
		||||
假设,T<sub>last</sub>表示最近一次接收到心跳的时间,t<sub>now</sub>表示当前时间。将T<sub>last</sub>、t<sub>now</sub>,和第二步求得的P<sub>later</sub>(t),带入以下公式即可求得$φ$值:
 | 
			
		||||
 | 
			
		||||
$$\varphi(t_{now})\xlongequal{def}-log_{10}(P_{later}(t_{now}-T_{last}))$$
 | 
			
		||||
 | 
			
		||||
求得$φ$值后,与阈值Ф进行比较,即可判断节点是否发生故障。
 | 
			
		||||
 | 
			
		||||
以上就是$φ$值故障检测策略的介绍,它的基本思想是利用了历史心跳信息,来降低误判的可能性。如果你想了解这个策略更详细的内容,可以参考“The $φ$ Accrual Failure Detector”这篇论文。
 | 
			
		||||
 | 
			
		||||
这篇论文中提到,当阈值Ф=1时,误判的可能性大约为10%;Ф=2时,误判的可能性大约为1%;Ф=3时,误判的可能性大约为0.1% ······
 | 
			
		||||
 | 
			
		||||
通过以上讲解,可以看出,**$φ$值故障检测策略可以根据历史心跳信息动态预测下一次心跳是否超时,并可以通过设置阈值来自由调控误判的可能性**。目前,该策略已被应用到一些框架中,比如我们熟悉的Akka集群的故障检测,便是采用了$φ$值故障检测策略。
 | 
			
		||||
 | 
			
		||||
当采用故障检测策略检测到故障后,故障如何恢复呢?接下来,我们就一起看看故障恢复策略吧。
 | 
			
		||||
 | 
			
		||||
## 故障恢复策略
 | 
			
		||||
 | 
			
		||||
关于故障恢复策略,我从单节点故障和网络故障两个维度展开。
 | 
			
		||||
 | 
			
		||||
**对于单节点故障问题,往往采取主备策略**,即当主节点故障后,从备节点中选出一个作为新的主节点,以继续提供服务。这种备升主的方式比较好理解。
 | 
			
		||||
 | 
			
		||||
如下图所示,用户A访问分布式集群时一直是与Master交互的,但当Master故障后,其他Slave会通过分布式选举算法选出一个新的主节点。
 | 
			
		||||
 | 
			
		||||
假设,从Slave 1、Slave 2和Slave 3中选举出Slave 2作为新的Master,则Slave 2需要承担原来Master的职责,继续为用户提供服务,因此当用户A再次访问集群时,提供服务的是新选出的Master,也就是Slave 2。这就是备升主的过程。
 | 
			
		||||
 | 
			
		||||
关于分布式选举算法的相关内容,你可以再回顾下[第4篇文章](https://time.geekbang.org/column/article/143329)。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/b4/f3/b44d27914ff2826c7fe1fe3bd780d3f3.png" alt="">
 | 
			
		||||
 | 
			
		||||
从用户A的角度来看,并不会感受到服务有什么异常,因为依旧可以正常访问集群。因此,**主备策略可以大大提高分布式系统的可用性,在分布式系统中随处可见**。比如,[第10篇文章](https://time.geekbang.org/column/article/149653)涉及的Redis集群、[第23篇文章](https://time.geekbang.org/column/article/166582)中讲到的ZooKeeper集群等,都是采用了这种主备策略来做故障恢复。
 | 
			
		||||
 | 
			
		||||
**而对于网络故障问题的解决方案,简单来说就是C、A、P选择的问题**,即在分布式系统的可用性和数据一致性之间做权衡。根据不同的应用场景,选择不同的解决方案。
 | 
			
		||||
 | 
			
		||||
当分布式系统中出现网络故障时,对于高可用性要求严格的系统,比如要求必须及时响应用户的场景,就需要采用保AP弃C的策略;对于数据一致性有严格要求的系统,比如银行、金融系统等场景,就需要采用保CP弃A的策略。具体内容,你可以再回顾下[第23篇文章](https://time.geekbang.org/column/article/166582)。
 | 
			
		||||
 | 
			
		||||
其实,网络故障恢复问题也可以看作数据复制的问题,即网络故障恢复后节点间数据同步的问题。还记得[第26篇文章](https://time.geekbang.org/column/article/168963)中的同步复制、异步复制和半同步复制技术吗?其中,半同步复制技术因为既能有效保证数据安全,又能满足系统高性能的要求,所以最受欢迎,被大多数分布式系统采用。
 | 
			
		||||
 | 
			
		||||
关于如何通过半同步复制技术,来进行网络故障恢复,你可以再回顾下第26篇文章的相关内容。
 | 
			
		||||
 | 
			
		||||
其实,**节点故障和网络故障也有交叉的地方**,比如网络故障产生的原因可能是节点故障,即因为节点故障导致节点间无法通信,而不是纯粹的网络链路问题。这种情况有两种可能性,一种是节点临时性故障,即一段时间后就会恢复;一种是节点永久性故障,即节点不会恢复。针对第一种情况,只需等到故障恢复后,数据进行同步即可;第二种情况则需要备升主策略来解决。
 | 
			
		||||
 | 
			
		||||
## 知识扩展:固定心跳检测和基于历史心跳信息预测故障的策略,各有什么特点呢?
 | 
			
		||||
 | 
			
		||||
首先,我们看一下固定心跳检测。
 | 
			
		||||
 | 
			
		||||
固定心跳检测的核心是,固定周期T秒发送心跳,若连续k次未收到心跳回复(时间T内),则判断心跳超时的时间为k*T秒。可以看出,k和T的设置非常重要。
 | 
			
		||||
 | 
			
		||||
比如,对于要求秒级故障检测的场景(时延敏感性场景),则k*T≤1s,因此需要将T设置为ms级,比如200ms,k设置为1000/200=5次。但,这样一来容易导致误判。因为判断超时的时间设置得太短,很可能是系统做内存回收或系统本身有高任务在运行导致心跳回复延后。
 | 
			
		||||
 | 
			
		||||
而对于时延不太敏感的场景,k或T可以设置得大一些,降低误判率,但却会增加发现故障的时间。
 | 
			
		||||
 | 
			
		||||
接下来,我们看一下$φ$值故障检测。$φ$值故障检测是基于心跳间隔符合正态分布的假设,通过对历史心跳数据采样来预测当前心跳是否超时的。也就是说,心跳间隔符合比较平稳或符合规律的情况下,比较适合,但对于具有突发情况或心跳间隔无规律的场景误判率比较高。
 | 
			
		||||
 | 
			
		||||
**在网络状况确定且比较稳定的场景下,大多数系统会采用固定心跳检测策略,因为其可以根据网络状况与业务场景自主设定合适的k和T值,简单有效;而当网络状况有所变化,且变化有规律的场景,则可以使用$φ$值故障检测策略。**
 | 
			
		||||
 | 
			
		||||
## 总结
 | 
			
		||||
 | 
			
		||||
今天,我主要带你学习了分布式高可用技术中的故障恢复技术。
 | 
			
		||||
 | 
			
		||||
首先,我为你介绍了分布式系统中的故障类型,主要包括物理故障和软件故障,软件故障主要是由于程序或软件Bug等导致,通常由开发者在开发或测试过程中解决,而物理故障导致软件不可用的故障类型主要分为两类,节点故障和网络故障。
 | 
			
		||||
 | 
			
		||||
其次,我为你介绍了故障检测方法。故障检测方法主要是心跳检测方法,包括固定心跳策略和基于历史信息的心跳策略。其中,固定心跳策略的具体原理见[第22篇文章](https://time.geekbang.org/column/article/165314)中的相关内容;基于历史心跳策略的核心是通过统计历史数据规律,以预测当前心跳是否超时以进行故障检测;
 | 
			
		||||
 | 
			
		||||
紧接着,我为你介绍了故障恢复策略,主要涉及备升主、数据复制等关键技术,相关关键技术原理,你可以再回顾[第4篇文章](https://time.geekbang.org/column/article/143329)中的分布式选举算法,以及[第26篇文章](https://time.geekbang.org/column/article/168963)中的数据复制技术。
 | 
			
		||||
 | 
			
		||||
最后,我再通过一张思维导图来归纳一下今天的核心知识点吧。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/55/c7/552788a0f75b7a529439fd9ba04dc0c7.png" alt="">
 | 
			
		||||
 | 
			
		||||
加油,相信通过今天的学习,分布式系统中的故障恢复技术对你来说不再陌生了,你可以根据业务场景设计出对应的故障检测和故障恢复策略了。行动起来吧,加油!
 | 
			
		||||
 | 
			
		||||
## 思考题
 | 
			
		||||
 | 
			
		||||
在分布式系统中,网络分区是一个非常重要的故障问题。那么如何判断网络分区呢?以及出现网络分区后,如何处理呢?
 | 
			
		||||
 | 
			
		||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										140
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/32 | 答疑篇:如何判断并解决网络分区问题?.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								极客时间专栏/分布式技术原理与算法解析/第六站:分布式高可靠/32 | 答疑篇:如何判断并解决网络分区问题?.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
<audio id="audio" title="32 | 答疑篇:如何判断并解决网络分区问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/45/65/451de0dd2fa10c68ae23ae3f669ea965.mp3"></audio>
 | 
			
		||||
 | 
			
		||||
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
 | 
			
		||||
 | 
			
		||||
到目前为止,“分布式技术原理与算法解析”专栏已经接近尾声了。在这里,我首先要感谢你坚持学习每一篇文章,以及对每一道思考题的积极思考与讨论,并在此基础上扩展了类似问题。
 | 
			
		||||
 | 
			
		||||
比如@Jackey、@Eternal、@leslie、@mt11912、@小白啊、@随心而至等同学,一直在跟着专栏的更新节奏学习,并非常积极地在留言区留言讨论、总结自己的理解,并查询相关资料补充文中未讲解到或没有深入展开的问题。
 | 
			
		||||
 | 
			
		||||
今天,我梳理了文后的留言,发现大家对最近几篇文章介绍的分布式高可靠问题特别感兴趣,特别是我没有详细展开的网络分区问题。
 | 
			
		||||
 | 
			
		||||
比如,在第4篇文章“[分布式选举:国不可一日无君](https://time.geekbang.org/column/article/143329)”中,我给你留下的思考题是集群中是否会存在双主的场景,很多同学提到双主是网络分区导致的。
 | 
			
		||||
 | 
			
		||||
再比如,在第31篇文章“[分布式高可用之故障恢复:知错能改,善莫大焉](https://time.geekbang.org/column/article/175545)”中,我给你留下的思考题是,如何判断以及处理网络分区。
 | 
			
		||||
 | 
			
		||||
因此,在今天这篇文章中,我将会与你深入探讨网络分区问题,以帮助你进一步理解并解决业务中的故障恢复问题。
 | 
			
		||||
 | 
			
		||||
## 什么是网络分区?
 | 
			
		||||
 | 
			
		||||
我们先来看看网络分区到底是什么吧。在[第31篇文章](https://time.geekbang.org/column/article/175545)分享故障恢复时,我与你介绍了故障类型中的网络故障,网络分区就是其中的一种故障类型。
 | 
			
		||||
 | 
			
		||||
通常情况下,网络分区指的是在分布式集群中,节点之间由于网络不通,导致集群中节点形成不同的子集,子集中节点间的网络相通,而子集和子集间网络不通。也可以说,网络分区是子集与子集之间在网络上相互隔离了。
 | 
			
		||||
 | 
			
		||||
那么,应该如何判断是否发生了网络分区呢?
 | 
			
		||||
 | 
			
		||||
## 如何判断是否发生了网络分区?
 | 
			
		||||
 | 
			
		||||
在分布式集群中,不同的集群架构网络分区的形态略有不同。所以,要判断是否发生了网络分区,我们需要弄清楚不同的分布式集群架构,即[集中式架构](https://time.geekbang.org/column/article/148187)和[非集中式架构](https://time.geekbang.org/column/article/149653)中的网络分区形态是什么样的。
 | 
			
		||||
 | 
			
		||||
首先,我们来看一下**集中式架构的网络分区形态**。
 | 
			
		||||
 | 
			
		||||
集中式架构中,Master节点通常以一主多备的形式部署,Slave节点与Master节点相连接,Master节点的主和备之间会通过心跳相互通信。
 | 
			
		||||
 | 
			
		||||
以Master节点主备部署为例,如下图所示,集中式架构中的网络分区主要是主节点与备节点之间网络不通,且一部分Slave节点只能与主Master节点连通,另一部分只能与备Master节点连通。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/25/b6/254574879fa04cb5194f4506cf7769b6.png" alt="">
 | 
			
		||||
 | 
			
		||||
然后,我们再来看看**非集中式架构中的网络分区形态**。
 | 
			
		||||
 | 
			
		||||
如下图所示,非集中式架构中,节点是对称的,因此网络分区的形态是形成不同子集,子集内节点间可互相通信,而子集之间的节点不可通信。比如,子集群1中Node1、Node2和Node4可以相互通信,子集群2中Node3和Node5也可以相互通信,但子集群1和子集群2之间网络不通。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/de/10/de9bc8d7c1ec8b7fa62eddcb2d9f5810.png" alt="">
 | 
			
		||||
 | 
			
		||||
从集中式和非集中式这两种分布式集群架构的网络分区形态可以看出,**要判断是否形成网络分区,最朴素的方法就是判断节点之间心跳是否超时,然后将心跳可达的节点归属到一个子集中**。
 | 
			
		||||
 | 
			
		||||
由于非集中式系统中,每个节点都是对等的、提供的服务相同,所以当多个子集群之间不可达,或部分节点出现故障后,尽管提供的服务质量(SLA)可能会下降,但并不影响这些剩余节点或子集群对外提供服务。所以,接下来我将与你重点讨论集中式系统的网络分区问题。
 | 
			
		||||
 | 
			
		||||
## 网络分区最微妙的地方在哪里?
 | 
			
		||||
 | 
			
		||||
在工作和生活中遇到一个问题,你的本能反应估计是,有问题就解决问题好了。而网络分区最微妙的地方在于,你很难通过程序去判断问题到底出在哪里,而只能通过心跳等手段知道部分节点的网络不可达了。
 | 
			
		||||
 | 
			
		||||
但,导致节点不可达的原因有很多,有可能是网络的原因,也有可能是节点本身的问题。也就是说,我们无法通过一些症状就判断出是否真的产生了分区。另外,你也很难通过程序去判断这个问题是不是很快就会被恢复。这也是应对网络分区问题最微妙的地方。
 | 
			
		||||
 | 
			
		||||
## 网络分区出现概率较高的场景是什么?
 | 
			
		||||
 | 
			
		||||
在[第31篇文章](https://time.geekbang.org/column/article/175545)留言区中有同学提到:
 | 
			
		||||
 | 
			
		||||
> 
 | 
			
		||||
个人理解,网络分区故障一般是针对不同集群来说的,单个集群一般在同一个网络中,集群内的单点故障并不会出现网络分区。当整个集群的网络出故障时,才会有分区的说法。能想到检测办法是,单独有机器对不同集群的主节点进行心跳检测来判断。
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
首先,我要澄清的是,**我们说的网络分区肯定是就同一个集群而言的**。对于不同集群来说,正是因为集群间本就没有太多的交互,才需要从逻辑上分割成不同的集群,这些逻辑上不同的集群本就是可以独立对外提供服务的。
 | 
			
		||||
 | 
			
		||||
当集群跨多个网络时,确实正如这位同学所说,从概率上讲相对容易出现网络分区的情况,比如一个业务集群部署在多个数据中心时。所以,集群跨多网络部署时,就是网络分区出现概率较高的场景。
 | 
			
		||||
 | 
			
		||||
接下来,我们看看如何处理网络分区吧。
 | 
			
		||||
 | 
			
		||||
## 网络分区有哪些常见的处理方法?
 | 
			
		||||
 | 
			
		||||
为了不影响分布式系统的高可用性,检测到网络分区后,我们就需要尽快地进行处理。
 | 
			
		||||
 | 
			
		||||
假如,**我们采用一种非常激进的方式去处理**,即一旦发现节点不可达,则将不可达节点从现有集群中剔除,并在这个新集群中选出新的主。
 | 
			
		||||
 | 
			
		||||
以图1所示集中式集群为例,当备Master、Slave3和Slave4节点检测到主Master、Slave1和Slave2节点不可达时,剔除这些不可达节点后,备Master升主,连同Slave3和Slave4节点组成一个新的集群。
 | 
			
		||||
 | 
			
		||||
如果不可达是由于节点故障导致的,那么这种策略没有任何问题。这些剩余节点组成的集群可以继续对外提供服务。但,如果不可达是因为网络故障引起的,那么集群中的另一个子集,即主Master、Slave1和Slave2,也会采用同样的策略,仍然对外提供服务。这时,集群就会出现[第22篇文章](https://time.geekbang.org/column/article/165314)中讲到的双主问题了。
 | 
			
		||||
 | 
			
		||||
假如,**我们采用一种保守的方式去处理**,即节点一旦发现某些节点不可达,则直接停止自己的服务。这样确实解决了双主的问题,但因为不同分区之间的不可达是相互的,且所有的分区都采取了这种停服策略,就会导致系统中所有的节点都停止服务,整个系统完全不可用。这显然也不是我们想看到的。
 | 
			
		||||
 | 
			
		||||
那么,当系统中出现节点不可达后,如何在不出现双主的情况下,尽可能地提升系统的可用性呢?或者说,有没有什么更均衡的策略呢?
 | 
			
		||||
 | 
			
		||||
接下来,我就与你分享四种均衡的网络分区处理方法,即Static Quorum、Keep Majority、设置仲裁机制和基于共享资源的方式。
 | 
			
		||||
 | 
			
		||||
### 方法一:通过Static Quorum处理网络分区
 | 
			
		||||
 | 
			
		||||
Static Quorum是一种固定票数的策略。在系统启动之前,先设置一个固定票数。当发生网络分区后,如果一个分区中的节点数大于等于这个固定票数,则该分区为活动分区。
 | 
			
		||||
 | 
			
		||||
为了保证发生分区后,不会出现多个活动分区,导致出现双主或多主的问题,需要对固定票数的取值进行一些约束,即:**固定票数≤ 总节点数≤2*固定票数 - 1**。
 | 
			
		||||
 | 
			
		||||
这个策略的优点是,简单、容易实现,但却存在两个问题:
 | 
			
		||||
 | 
			
		||||
- 一是,对于分区数比较少的时候,比方2个分区时,该策略很容易选出一个唯一的活动分区。但是,当活动分区非常多的时候,由于各个分区的票数分散,不容易找到一个满足条件的分区,没有活动分区也就意味着整个集群不可用了。
 | 
			
		||||
- 二是,由于这种策略里固定票数是固定不变的,所以不适用于集群中有动态节点加入的场景。
 | 
			
		||||
 | 
			
		||||
### 方法二:通过Keep Majority处理网络分区
 | 
			
		||||
 | 
			
		||||
顾名思义,Keep Majority就是保留具备大多数节点的子集群。由于不限定每个分区的节点数超过一个固定的票数,所以可以应用于动态节点加入的场景。
 | 
			
		||||
 | 
			
		||||
假设,集群数为n,出现网络分区后,保留的子集群为节点数w≥n/2的集群。为防止出现双主或两个集群同时工作的情况,通常将集群总节点数n设置为奇数。
 | 
			
		||||
 | 
			
		||||
可想而知,若集群总数为偶数,比如图1集中式架构的例子中,子集群1和2都包含2个Slave节点,就会导致两个子集群同时存活,在业务场景只允许一个主的情况下,会导致业务运行不正确。
 | 
			
		||||
 | 
			
		||||
那么,如果真的出现了集群总节点数为偶数,两个子集群节点数均为总数一半时,又应该如何解决分区问题呢?
 | 
			
		||||
 | 
			
		||||
这时,我们可以在Keep Majority的基础上,叠加一些策略,比如保留集群节点ID最小的节点所在的子集群。如图1所示,假设集群节点总数为6,现在因为网络故障形成网络分区子集群1{主Master,Slave1, Slave2}和子集群2{备Master,Slave3, Slave4},假设Slave1是ID最小的节点,那么此时要保留包含Slave1的子集群1。
 | 
			
		||||
 | 
			
		||||
虽然Keep Majority方法可以解决动态节点加入的问题,但也不适用于产生多分区的场景。因为随着分区数增多、节点分散,也难以在众多分区中出现一个节点数w≥n/2的分区。
 | 
			
		||||
 | 
			
		||||
前面我讲到集群跨多个网络部署时更容易产生网络分区,因此我不推荐采用Static Quorum和Keep Majority方法去处理跨多网络集群的网络分区问题。
 | 
			
		||||
 | 
			
		||||
### 方法三:通过设置仲裁机制处理网络分区
 | 
			
		||||
 | 
			
		||||
设置仲裁机制的核心是,引入一个第三方组件或节点作为仲裁者,该仲裁者可以与集群中的所有节点相连接,集群中所有节点将自己的心跳信息上报给这个中心节点。因此,该中心节点拥有全局心跳信息,可以根据全局心跳信息判断出有多少个分区。当出现网络分区后,由仲裁者确定保留哪个子集群,舍弃哪些子集群。
 | 
			
		||||
 | 
			
		||||
如下图所示,假设引入Node0作为第三个节点,该节点IP为10.12.24.35,当出现网络分区子集群1{Node1, Node3}和子集群2{Node2, Node4}时,每个子集群中选择一个Leader节点并ping一下Node0的IP,能ping通则保留,否则舍弃。比如下图中,子集群1可以ping通,子集群2 ping不通,因此保留子集群1。
 | 
			
		||||
 | 
			
		||||
<img src="https://static001.geekbang.org/resource/image/6e/1a/6e89db7d47b9b7c73122ffe0beea841a.png" alt="">
 | 
			
		||||
 | 
			
		||||
### 方法四:基于共享资源的方式处理网络分区
 | 
			
		||||
 | 
			
		||||
说到共享资源,我们需要先回顾下分布式锁的相关知识。分布式锁是实现多个进程有序、避免冲突地访问共享资源的一种方式。
 | 
			
		||||
 | 
			
		||||
基于共享资源处理网络分区的核心,其实就类似于分布式锁的机制。也就是,哪个子集群获得共享资源的锁,就保留该子集群。获得锁的集群提供服务,只有当该集群释放锁之后,其他集群才可以获取锁。关于锁的管理和获取,你可以再回顾下[第7篇文章](https://time.geekbang.org/column/article/145505)中的相关内容。
 | 
			
		||||
 | 
			
		||||
这种方式的问题是,如果获取锁的节点发生故障,但未释放锁,会导致其他子集群不可用。因此,这种方式适用于获取锁的节点可靠性有一定保证的场景。
 | 
			
		||||
 | 
			
		||||
基于仲裁和共享资源的网络分区处理方法,其实都是依赖一个三方的节点或组件,然后借助这个第三方来保证系统中同时只有一个活动分区。所以,这两种处理方法适用于有一个稳定可靠的三方节点或组件的场景。
 | 
			
		||||
 | 
			
		||||
## 总结
 | 
			
		||||
 | 
			
		||||
今天,我与你进一步展开了分布式系统中的网络分区问题,以加深你对网络分区问题的检测、处理方式的理解,并帮助你在实践应用中处理网络分区问题。
 | 
			
		||||
 | 
			
		||||
**关于网络分区的处理方法,其本质就是,在产生分区后,选出一个分区,保证同时最多有一个分区对外提供服务。**基于此,我为你梳理了四种常见的处理方法,包括Static Quorum、Keep Majority、设置仲裁机制和基于共享资源的方式。
 | 
			
		||||
 | 
			
		||||
其中,基于Static Quorum的方法,因为涉及固定票数策略,所以不适用于处理多个分区,以及有动态节点加入的场景;基于Keep Majority的方法,可以解决动态节点场景下分区问题,但因为要求子集群节点数≥1/2总节点数,所以也不适用于处理多个分区的情况;而基于仲裁和共享资源的网络分区处理方法,其实都是依赖一个三方的节点或组件,所以适用于有一个稳定可靠的三方节点或组件的场景。
 | 
			
		||||
 | 
			
		||||
如果还有哪些思考题或者留言问题,还没来得及扩展的话,你可以留言给我,后续我会再找机会进行解答。最后,我要和你说的是,和我一起打卡分布式核心技术,一起遇见更优秀的自己吧。
 | 
			
		||||
 | 
			
		||||
篇幅所限,留言区见。
 | 
			
		||||
 | 
			
		||||
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!
 | 
			
		||||
		Reference in New Issue
	
	Block a user