You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

121 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 02新的挑战分布式系统是银弹吗我看未必
你好,我是陈现麟。
通过上一节课的介绍,你已经了解了分布式系统出现的原因和引入的新问题,并且我们一起讨论了这些新问题的处理思路。你对分布式系统的全局已经有了一个初步的认识,这就为后面的学习打下了良好的基础。
下一步,我们要从根本上理解分布式系统的设计方法和原则,这就需要你时刻谨记单体系统和分布式系统之间的差别。从本质上来说,单体系统是以单进程的形式运行在一个计算机节点上,而分布式系统是以多进程的形式运行在多个计算机节点上,**二者的本质差别就导致了分布式系统面临着四个方面的新问题,分别是:故障处理、异步网络、时钟同步和共识协同**。
所以,在这节课中,我们会从上述的四个方面来比较单体系统和分布式系统的差别,一起来探讨分布式系统会面临哪些新的挑战,而这些挑战又是怎么影响分布式系统的架构和设计的。
## 全部失败与部分失败
故障处理是所有系统都必须考虑的关键问题,所以我们从故障处理的角度开始分析。
单体服务系统中,在硬件正常的时候,对于一个确定的输入,总会得到一个确定的输出。就算是在内存、磁盘损坏等硬件异常的时候,对于一个确定的输入,计算机也会直接出现无法启动或崩溃的情况,而不是给出一个模棱两可或不正确的结果。
**这种全部失败的处理逻辑,会大大减轻用户使用计算机的心智负担,让我们明确地知道,如果系统内部发生了故障,计算机不会给出错误的结果,而是会全部崩溃**。那么处理计算机系统崩溃的方法就非常明确和简单了,重启计算机,重新运行程序即可。
相反,如果计算机给出了一个错误的结果,这些错误的结果和正常的结果混在一起,我们是无法感知的。比如一次硬件故障,导致所有的 0 都变成了 1 ,并且写入到了数据库中这种情况,它的代码逻辑是正确的,那么想要识别出来,再运行处理的成本就非常大了。
曾经某知名支付公司的系统实现了异地冷备,也就是除主机房外,在外地还有一个备用机房,备用机房运行的程序和主机房一模一样,只是不处理用户请求而已。但是有一次主机房故障时,这家公司并没有将流量切到冷备的机房。
公司这样考虑的主要原因是,虽然主机房故障,会导致系统暂时不能对外服务,出现很大的损失,但是这样的损失是能够评估出来的;可是,如果将流量切到冷备的机房去服务用户,很可能会出现其他不可预知的错误,这样产生的损失就无法评估了,结果也是我们不能承受的。
分布式系统由多个计算机节点组成,虽然每一个计算机节点都是全部失败的模型,**但是如果系统中的某些节点出现宕机或者网络故障,整个分布式系统就会出现部分失败的情况**,也就是说单机计算机系统这个确定性的全部失败的模型,在分布式系统中就无效了。这个问题大大增加了分布式系统的复杂性,也给我们处理分布式问题提高了难度。
部分失败的情况是构建分布式系统的一个大挑战,也深刻影响着分布式系统的设计方式和原则。在分布式系统中,我们需要接受部分失败,接受系统中每一个部分可能出现故障的情况,在不可靠的硬件上通过软件来容错,构建高可用的分布式系统。
所以,在分布式系统中,故障处理是软件设计的一个重要组成部分。我们需要时刻谨记节点宕机、网络分区等各种问题出现时,系统应该怎么正确处理,比如分布式系统在设计的时候,每一个组件都必须是高可用的。
在网络出现分区的时候,系统必须能够正确处理,在网络分区恢复的时候,系统也必须正确处理,不能出现不可预知的错误,特别是在进行数据复制的时候。
## 本地调用与远程调用
接下来,我们还要从当前广泛使用的异步网络的角度来分析。单体系统和分布式系统对网络的依赖程度有非常明显的差别,单体内部几乎不依赖网络,但是网络却是架构分布式系统的根基。
在单机系统中,系统各个组件之间的调用方式非常简单,直接本地调用即可。但是在分布式系统中,不同的组件运行在不同的机器上,通过本地调用是不可行的,我们只能通过网络来进行调用,即远程调用。
**虽然两个调用的差别只在于远程调用多依赖了网络这个通道,但是这却给系统带来了非常大的复杂性,其实主要原因还是网络本身的复杂性所导致的**。所以接下来,我们再一起讨论一下网络带给系统的复杂性。单机系统的本地调用方式,我们可以理解为只要发起调用,调用操作就一定会执行,并且我们可以忽略调用方和被调用方之间的数据传递时间。
但是单机系统的本地调用模型在远程调用上是不成立的,因为远程调用是通过网络来发送数据的,而我们目前依赖的网络是异步的,从一个节点发送数据到另一个节点,不能保证在多少时间内到达,甚至不能保证一定能到达;如果网络是同步的,能够保证从一个节点发送到另一个节点的数据,最慢会在多长时间内一定能到达,这样就可以大大简化远程调用带来的复杂性了。
这个时候你一定在想,我们能不能实现一个同步网络呢?其实是可以实现的,如果从节点 A 发送数据到节点 B我们只需在节点 A 和节点 B 之间,建立一条带宽充足的专门链路,用于这一次数据的传递就可以达到要求。可是,这样会导致整个网络的效率太低,对于目前的互联网来说是不现实的。
比如,你开车从北京到上海,如果直接走目前的公路网,什么时候能到上海,这个时间是不能确定的,因为一路上很多地方会出现不同程度的堵车。但是如果指定一条专线给你,这条专线只能走你的车,其他人的车都不能走,这样就肯定不会堵车,你也可以准确计算出从北京到上海的开车时间。但是在这个例子中,你也能看到专线模式会导致整个公路网络的运行效率非常低,所以这种情况在我们的日常生活中是不可能实现的。
所以你会发现,远程调用在时间上有不确定性,那么我们就来讨论一下,服务 A 通过网络远程调用服务 B 可能会出现哪些不确定的情况。在我们使用极客时间 App 的过程中,如果出现加载不出来或者系统内部错误的问题,主要和远程调用模型面临的网络排队、丢包和请求服务崩溃等情况有关,具体的场景和示意图如下:
![](https://static001.geekbang.org/resource/image/26/b6/268914fe34cf7fbec845b25ab8c8deb6.jpg?wh=2284x1616)![](https://static001.geekbang.org/resource/image/41/06/41c1a6ed5216e8543d6e57a2d6e6e906.jpg?wh=2284x1024)
**在这样的情况下,通常的做法是采用超时机制,请求方在发起请求后,设置一个超时时间,这样能确保请求方在超时时间内,一定能得到一个响应**。如果在超时时间内,请求方得到了明确的响应,不论这个响应是被调用服务回复的,还是网络地址不可达等网络错误,调用方都可以根据响应结果一一来处理。
比如,极客时间的用户去购买一个课程,如果订单服务响应为购买成功或者余额不足,我们可以将回复反馈给用户;如果是网络地址不可达,我们就可以知道这个购买请求还没有被订单服务处理过,可以采取重试等其他的办法来处理。
但是,如果请求在超时时间内没有收到任何响应,即响应超时,那么调用方将无法区分下面四种情况:
![](https://static001.geekbang.org/resource/image/b0/4e/b05630a876cc40a587ef50102d104e4e.jpg?wh=2284x1427)![](https://static001.geekbang.org/resource/image/22/a2/22f8d4a8cccf782c84d84a789bd2d3a2.jpg?wh=2284x1024)
在响应超时的情况下,如果调用方想确保这个请求被执行,只能重新发送刚刚的请求。但是,如果之前的请求只是在网络中延迟或者响应丢失了,例如上面 2、3 和 4 中描述的情况,重试操作会导致这个请求被多次执行;如果之前的请求在网络中丢失了,例如上面 1 描述的情况,那么调用方不进行重试的话,这个请求就会出现一次都没有被执行过的情况。
所以,在分布式系统的设计中,我们要充分考虑通过网络进行远程调用导致的不确定性,比如在响应超时的情况下增加重试机制,确保请求能最少执行一次。在重试的时候增加幂等的机制,确保请求只被精确处理一次,并且对重试机制增加退避策略,确保系统不会因为重试导致雪崩。
## 全局时钟与多个时钟
我们在上面讨论了故障处理和异步网络带来的挑战,接下来我们再一起来探讨分布式系统多个时钟的问题,从时钟同步的角度继续分析分布式系统面临的挑战。
计算机系统一般是通过石英钟来计算时间的但是石英钟的振动频率会随着温度等原因变慢或变快所以在运行时间比较长后计算机系统的时间可能会发生比较大的误差所以人们又增加了一组专门的时间服务器。我们可以认为这些时间服务器的时间是准确的计算机系统通过网络定期获得时间服务器的时间来调整本地时间即网络时间协议NTP。我们可以通过下面的公式来计算当时的时间
**本地时间 = 时间服务器的返回时间 + 时间服务器响应的网络时延**
这里我们可以来回忆一下,刚才我们在远程调用的话题中,知道了网络时延是不可预测的,所以通过 NTP 我们依然无法获得准确的时间,一般的精度都是在几十毫秒的范围内。不过,这个精度对于单机系统来说是足够的,我们一起来看看这是为什么。
在计算机系统中,时间主要有两个作用,**第一个是记录事件发生的时间,这是一个绝对时间,是让我们来阅读和理解的**。我们平时使用时间的精度一般为分钟,对于几十毫秒的误差是毫无感知的,比如我们经常说今天 12 点 30 分做了什么事情,很少提到多少秒,毫秒就更不会提了。
**第二个是记录事件之间的发生顺序,这是一个相对时间**。我们平时只关心事件之间的顺序,对于精度是没有要求的,比如对同一个字段的两个修改操作,我们认为一定是后一个的修改操作覆盖前一个操作的数据。
在单机系统中,由于只有一个时钟,先执行的事件一定能获得更小的时间,通过本地时钟就可以确保全局事件之间顺序的正确性,所以单机系统是一个全局时钟的模型。
而分布式系统是由多台计算机节点组成的,每一个节点都有自己的时钟,并且计算机执行的速度非常快,在一个毫秒内可以做非常多的事情。在这种情况下,如果在每一个节点,我们都采用本地的时钟来记录事件的发生时间,然后基于多个节点上的事件按发生时间进行排序,就很容易出现时间穿越的问题。下面我举例来分析,你可以结合下图进行思考。
![](https://static001.geekbang.org/resource/image/e1/91/e145229fd5999134b4377e0c960e3191.jpg?wh=2284x1106)
比如在多主复制的情况下,客户端 A 在主副本 1 修改了 x = 5记录时间为 10 ms但是该副本时间慢了 10 ms所以实际时间为 20 ms。几乎同一时间客户端 B 在主副本 2 修改了 x = 10记录时间为 15 ms但是该副本时间快了 10 ms所以实际时间为 5 ms。
一般对于这种情况的处理策略是最后写入获胜LWW在数据合并的时候如果按照主副本 1、2 的记录时间来处理的话,最终 x = 10会导致主副本 1 的修改丢失。
所以,在分布式系统的设计中,我们一定要谨记系统中各个节点的本地时钟是存在误差的,不能依赖各自的时钟对事件进行排序。
一般对于这个问题的解决思路有两个,一个是回到单机系统的全局时钟的模式,所有节点对于需要排序的事件时间,不使用本地时钟的时间,而是去请求同一个时间服务器获得事件的发生时间,然后依据这个时间进行排序;另一个是 Google 在 Spanner 中使用的,通过 GPS 和原子钟实现 TrueTime API 来解决。对于这一块内容,在课程的“事务(三)”中有详细的介绍,这里就不再赘述了。
## 一言堂与共识
最后,我们不要忘记分布式系统内部,多个实例或服务之间的协同问题,所以,我们接下来从共识协调的角度来分析。
在计算机系统中,我们经常要面对这样的情况:同时只允许一个线程操作某一个数据,和同时只允许一个线程执行某一个操作,如果不遵守这个规则,就可能会导致数据错误等不可预料的后果,这就是线程之间的同步操作。
在单体系统中,需要协同的多个线程是属于同一个进程的,所以同步操作很简单,直接使用进程内的资源来做协同就可以了,比如锁、信号量等。对于这些线程来说,所有的同步操作都以进程的资源为准,就好像进程是一个一言堂的管理员,协同进程内部的所有线程之间的同步。
但是,单体系统的单进程、多线程的同步模型,对于分布式系统是不适用的,因为分布式系统中各个组件都是独立的进程,运行在不同的机器上。所以,**对于分布式系统来说,我们需要处理的是一个跨机器的多进程同步问题**。那么,我们应该怎么来解决呢?
聪明的你应该很快就能想到一个方法,我们选择一个服务来做同步操作的管理者(我们称为同步服务),在多个进程间需要同步时,就到同步服务来请求一个锁,获得锁的进程就可以操作,其他的进程就必须等待。
这样看似将问题解决了,但是结合我们前面讨论过的,在做分布式系统设计的时候,我们必须要考虑到故障的存在,所以同步服务不能只有一个实例,它需要多个实例来保障它的高可用,那么同步服务应该由哪一个实例,来处理其他进程的同步请求呢?
你可能会想通过配置直接指定一个,这确实解决了同步服务启动时的问题,但是如果被指定实例宕机了,接下来该由哪一个实例来继续处理同步请求呢?
所以,我们通过这些讨论,会发现问题依然没有解决,只是转移了,也就是将分布式系统的多进程同步问题变成了同步服务的选主问题。其实,**这是一个共识问题,需要分布式系统中参与同步的进程之间能达成共识,目前我们是通过 Paxos 或者 Raft 这样的共识算法来解决问题的**。对于这一块内容,在后面的“一致性与共识”课程中会有详细的介绍,这里就不再赘述了。
## 总结
到这里,我们一起讨论了在分布式系统场景下面临的新挑战,我们一起来总结一下这节课的主要内容:
![](https://static001.geekbang.org/resource/image/2c/92/2c6713b837eab9882e65d77f9d9c8592.jpg?wh=2284x1204)首先,分布式系统由多个单机计算机节点组成,在系统中的某些节点出现故障或者网络故障时,整个分布式系统都会出现部分失败的情况。所以我们需要接受部分失败,接受系统每一个部分可能出现故障的情况,在不可靠的硬件上通过软件来容错,构建高可用的分布式系统。
然后,在分布式系统中,不同的组件运行在不同的机器上,只能通过网络来进行远程调用。当远程调用由于网络原因异常失败时,我们无法区分当前的请求是否被执行过,于是**如何确保请求只被精确处理一次成为了分布式场景下新的挑战**。
接着,我们了解到分布式系统是由多台计算机节点组成的,每一个节点都有自己的时钟,并且由于硬件和网络的原因,系统中各个节点的本地时钟是存在误差的,不能依赖各自时钟对事件进行排序,这就导致**如何对系统中的事件进行排序,变成分布式场景下新的挑战**。
最后,分布式系统各个组件都是独立的进程,运行在不同的机器上,这就导致单体系统的单进程、多线程的同步模型变成了跨机器的多进程同步模型,要解决这个问题,就需要分布式系统中参与同步的进程之间能达成共识。
## 思考题
分布式系统面临故障处理(部分失败)、异步网络、时钟同步和共识协调,这四个新的挑战和 CAP 理论之间是什么关系呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。