first commit

This commit is contained in:
张乾
2024-10-16 09:22:22 +08:00
parent 206fad82a2
commit bf199f7d5e
538 changed files with 97223 additions and 2 deletions

View File

@ -0,0 +1,104 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 掌握好学习路径,分布式系统原来如此简单
你好,我是陈现麟,现在是伴鱼技术中台负责人。
欢迎你加入到分布式系统这门课程的学习中,说起分布式系统,它发展到今天,已经是互联网公司 IT 架构的事实标准,正在深刻地影响着 IT 基础建设的各个方面。在分布式系统中,站在不同的层面会看到不同的分布式系统,总有一个层面能找到 IT 工程师研发工作的场景。
如果站在单一应用层面,我们看到的是单体服务和多服务的分布式系统;如果站在公司层面,我们看到的是单机房、多机房或者跨洲际的分布式系统;如果站在整个互联网的层面,那么基于 TCP/IP 协议构建的整个互联网,就是一个超大规模的分布式系统。可以说,分布式系统已然是 IT 工程师的一门必修课了。
同时,因为分布式系统慢慢成为了互联网公司的标准架构,所以对于后端工程师来说,是否能系统性地理解分布式系统是初级工程师和资深工程师之间最显著的差别。总而言之,学习分布式系统是非常值得我们投入的一件事情。
分布式系统的学习误区
不过你也可能会感到疑惑,既然分布式系统已经覆盖了我们工作中的方方面面,而且发展得如此成熟,你也有过一些相关的实践,还花了不少的时间来学习,可为什么还是无法掌握分布式系统的命脉呢?
我们一起来分析一下,你是不是还在这样处理分布式问题。
在做架构设计和选型的时候,因为没有知识系统做支撑,所以不能系统性地思考,处理问题也没有十足的把握;在系统稳定性这种非常重要的问题上,只能通过 case by case 的方式来处理,不能从根本上直接解决由于分布式架构引入的问题。
再来看看你的学习路径,你可能读过一些分布式系统的论文,想通过这些论文来学习分布式系统架构设计的原则、思路和取舍,但是却因为内容枯燥、无趣,很难有共鸣感促使自己坚持下去;你还深入研究过一些分布式系统的源码实现,想从代码的维度去学习分布式系统的实现细节,但是经常学完就忘,效果一点也不好。
最后你花费了不少时间,将分布式系统中的知识点,在各个系统间进行横向地比较和总结,想要更系统、更深刻地理解分布式系统,但是却因为自己对分布式系统的认知和经验有限,导致学习的进展非常缓慢,了解知识的深度和广度都不太够。
如果你也有类似的感觉,不妨花点耐心听我继续给你分析分析。
从我的经验来看,初学者想要高效且系统性地掌握分布式系统,这本身就是一个悖论。不管你有多努力,要知道理论与实践之间是有一道巨大的鸿沟的。你很可能会被场景、时间、自身积累与理解程度等原因限制住,导致学习事倍功半,实践也效率低下。而对于这个主题来说,论文、源码等资料的学习仅仅是入门,我们还需要结合业务场景、工作经历和实践经验,再加上思考与时间去慢慢沉淀才可以。
为什么我会这样说,听完我的经历,也许你就明白了。
学习路径的重要性
时间回到六年前,那是我在小米工作的第四年。当时的我已经做过业务功能开发和基础架构开发,还在一些技术领域有了比较深入的尝试,比如做过单机 200+W 的长连接系统,也做过峰值 100+W QPS 的 Web 服务。这时的我已经有了分布式技术的一些实践和相对丰富的经验。
但是当我去做一个实时计算的项目,面对任务调度、消息确认和故障恢复等需要多方面分布式知识来指导架构设计的模块时,还是显得捉襟见肘。主要原因就是在初学阶段,我以为恶补了一些相关知识,就可以大刀阔斧地干一场了,然而却是只见树木,不见森林。
直到后来我到伴鱼负责运维、数据库、服务治理、基础服务、DevOps 和 AI 能力等相关的技术中台建设,同时还负责过一段时间的数据中台和业务中台的建设。在这期间,我带着团队从 0 到 1 搭建了一个企业级的分布式系统,经历了一个分布式系统从小到大,从简单到复杂的完整演进过程。
这段经历让我对分布式系统有了更全面、深刻的认知。我熟悉了分布式系统中,每一个组件的设计原则,这让我能够站在一个全局的角度,去思考分布式系统中各个组件之间的关联与取舍。
如果将分布式系统比作一片森林的话,那么 6 年前的我进入到森林中近距离地考察、调研和实践过,我知道这里为什么要种柏树和杨树,以及怎么种好它们,但是却不知道柏树和杨树应该怎么搭配,也不知道为什么不选择种柳树。
而现在的我已经亲手培育过一片森林,还能经常以航拍的角度来审视这一片森林。进一步来说就是,我不仅知道了分布式系统是什么样子的,还知道分布式系统为什么是这个样子的,其中的挑战和权衡是什么,对分布式系统也有了清晰的知识脉络与理解。
这里也附上一份参考资料,是我之前参加的一个知乎圆桌会议,里面记录了我对分布式系统的理解,希望能够帮到你。
总而言之,对于分布式系统这样一个庞大的知识网络,经过我 6 年的深入研究,更加笃定了一件事,就是一定要有一条明确的学习路径,从最根本的原因出发。对于初学者来说,要多问自己为什么,然后再思考怎么做。在技术理解与实践中反复横跳,才能由点成线;在总结中抓住技术实现的关键点和系统脉络,不迷失于细节,才能连线成网。而这些都将是我在这个专栏中交付给你的。
希望你也能透过我的实践、思考、经验与总结,形成自己的认知。
课程设计
本着明确学习路径的目标,历经用户调研、专家调研等多个环节的打磨,最终这门课的设计思路如下。
首先,找到分布式系统中稳定不变的知识、原理和解决思路。
比如注册发现的原理、故障处理的思路和 CAP 理论等等。为什么首选这些呢?
细数分布式系统,技术要点其实有很多,包括 MapReduce 之类的分布式批处理技术Flink 之类的分布式流计算技术和 Istio 之类的分布式在线业务技术。但万变不离其宗,只要我们掌握了上述这些“宗”,那么再去研究其他技术时,就会实现一通百通的效果。
其次,去繁从简。
我们不会讨论分布式批处理和流计算,只聚焦于日常工作中你接触最频繁的在线业务分布式系统,依据是否有状态将其分为“分布式计算”和“分布式存储”这两大部分,从简单到复杂依次给你介绍分布式系统的相关知识与原理,以及我对分布式系统的实践、思考与总结。
具体来说,整个专栏将分为四个递进的模块,学习计划如下:-
概述篇
学习一个知识应该先理解这个知识的来龙去脉,所以在一开始的概述篇中,我们先来讨论分布式系统产生的过程:它为什么会产生,产生后解决了什么问题,又带来了哪些新问题,遇到哪些方面的挑战。
通过“概述篇”的学习,你可以比较好地抓到分布式系统的脉络和关键点,有了很强的学习目标和路径,就不会迷失在各种系统和框架实现的细节中了。
分布式计算篇
分布式计算是你日常工作中接触最多的分布式技术,这部分看起来像是微服务相关的知识,但是我们不是从微服务的角度来讲解的。因为分布式系统有各种各样的实现形式,而微服务只是其中的一种具体的实现形式,所以,我们会从单机系统演进到分布式系统后,引入哪些新问题的角度,在技术原理层面一个一个讨论并解决这些问题。
你在学习之后,可以在各种系统和场景中理解和运用它,并且知道在系统设计层面应该如何取舍。由于这一部分的内容你平时都有接触,所以学习起来的难度相对会比较低,共鸣会比较强烈。
分布式存储篇
这一部分你可以理解为是分布式技术篇中的进阶篇,我们对计算进行分布式扩展后,再一起来讨论存储的分布式扩展。这里我们从简单到复杂,一起讨论数据分片、数据复制、分布式事务和一致性等相关的知识。
掌握之后,再做架构设计时,你会发现思维的深度和广度都得到了提升。
总结篇
待上面三个模块的内容学习完成后,你已经对分布式系统的重要原理有了系统性地理解,这个时候,我们再一起来看分布式系统的发展历程和未来趋势。
我们从分布式计算的角度,一起讨论分布式系统是怎么从单机系统演进到 Service Mesh 的;还会从分布式存储的角度,一起讨论分布式系统是怎么从单机系统的 ACID 演进到 NewSQL 的。最终,交给你一张继续深入学习的路线图。
最后,我想说的是,这个专栏最大的价值就是能够系统性地解决你的问题,不需要你花费大量的精力再进行一次低效的探索。
并且,在专栏中,我还分享了关于学习知识和解决问题的思考方式,以及我对分布式系统的一些经验和思考。希望通过这个专栏,不仅能帮你系统性地学习分布式系统,还能帮你掌握学习知识和解决问题的思维方法,让你的工作和生活变得越来越好!
在学习过程中,如果你有不懂的地方,欢迎你在留言区问我,我们一起讨论和交流。最后,你可以给自己立个 Flag每节课都在评论区分享思考题的思路通过主动性学习和思考来提高学习的效率和效果等到 3 个月后,我们再来一起验收。
我相信只要你肯坚持,就一定会有巨大的提升,希望到时候对于分布式系统这个领域,你能有“昨夜西风凋碧树,独上高楼,望尽天涯路”的感受。我们一起加油吧!

View File

@ -0,0 +1,115 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 导读:以前因后果为脉络,串起网状知识体系
你好,我是陈现麟。
在我们正式学习整个专栏之前,我特别设计了这个导读环节,让你在正式学习之前先拿到一份导航地图。
在开篇词里,我提到了我曾经参加过知乎圆桌会议,回答了“如何系统性地学习分布式系统”这个问题。在这节课的导读内容里,我会结合知乎中的回答,进一步提炼出分布式系统前因后果中的核心问题,帮你追本溯源,理清这个专栏的设计思路,总结深入学习的方法和路径。
学完这节课以后,你不仅能对课程思路心中有数,还能知道如何推导出一门技术的脉络,学会将零散的知识点连成网状的知识体系。
前因:分布式系统解决了什么问题
学习一个知识之前,我觉得比较好的方式是先理解它的前因后果:前因就是这个知识产生的过程中,它解决了什么问题,怎么样解决的。后果就是它的出现又带来了哪些新的问题,这样我们才能比较好地抓到它的脉络和关键点,不会一开始就迷失在细节中。
所以,学习分布式之前,我们要解决的第一个问题就是:分布式系统解决了什么问题,怎么样解决的?分布式系统的出现主要解决的是单体系统方面的不足,下面我们就具体来分析一下。
首先,分布式系统解决了单机性能瓶颈导致的成本问题。由于摩尔定律失效,廉价 PC 机的性能瓶颈无法继续突破,虽然小型机和大型机能实现更高的单机性能,但是成本太高,一般的公司很难承受。
比如 2008 年,阿里巴巴发起的“去 IOE ”运动(在 IT 建设过程中,去除 IBM 小型机、Oracle 数据库及 EMC 存储设备)的一个关键原因,就是这些商用小型机和存储设备的成本实在太高了。
然后,解决了用户量和数据量爆炸性地增大导致的成本问题。进入互联网时代,用户量爆炸性地增大,用户产生的数据量也随之增大。在 2015 年,全球数据总量达到 6 ZB1 ZB 等于 1 万亿 GB在 2020 年,全球数据总量已经达到了 44 ZB ,预计到 2030 年,全球数据总量将超过 2,500 ZB。
即使在这种情况下,单个用户或单条数据的价值,也是比软件时代(比如银行用户)的价值低很多的,所以人们必须寻找更经济的方案来处理和存储这些数据。并且,在用户量和数据量,大到一定程度之后,在单机系统的范围内,即使不计成本,可能也找不到好的解决方案。
接着,满足了业务高可用的要求。互联网产品基本都要求 7 * 24 小时提供服务,对于停止服务等故障是无法容忍的。如果想要提供高可用的服务,唯一的方式就是通过增加冗余来完成。那么就算是单机系统可以支撑的服务,因为要满足高可用的要求,也会变成一个分布式系统。
最后,分布式系统解决了大规模软件系统的迭代效率和成本的问题。如果一个大规模的软件系统是一个单体系统,那么大量的开发人员就只能将一个大规模软件,整体进行编译、测试和发布,这样一来开发语言和生态都比较单一,系统的迭代效率就会非常低。同时,对于系统的各个部分,我们也不能有针对性地进行成本优化,这会产生非常大的成本问题。
根据上面的四个原因,我们可以总结出在互联网时代,单机系统是无法解决成本、效率和高可用问题的,但是这三个问题,对所有的公司来说都是非常关键的问题,所以分布式系统就这样应运而生了,你也会发现,从单机系统到分布式系统的发展,是无法阻挡的技术大潮流。
那么,分布式系统又是怎么解决单机系统面临的成本、效率和高可用的问题呢?
其实很容易想到,就是将一些廉价的 PC 机,通过网络连接起来共同完成工作,并且在系统中提供冗余,来解决高可用的问题。其实我们从分布式系统的定义中,也能找到这个方法,分布式系统指的是由一组通过网络进行通信,为了完成共同的任务,而协调工作的计算机节点组成的系统。在这个定义中,可以看出分布式系统是通过多工作节点,来解决单机系统面临的成本、效率和高可用问题的。
刚才我们讨论了分布式系统解决了什么问题,怎么样解决的,由此得出,分布式系统的发展是无法阻挡的技术大潮流。下面我再进一步解释一下,如何理解分布式系统的出现,以及分布式系统在不同业务场景中的表现形式。
“旧时王谢堂前燕,飞入寻常百姓家”,用来形容 IT 技术最合适不过了。由于互联网的爆发性发展, IT 技术不再是实验室、科研机构和金融等相对前沿行业的专属技术,它快速地平民化了。如果一项技术能够平民化,那么一定是在成本、效率和稳定性方面都有非常突出的表现,也就是说必须物美价廉,所以我们可以把分布式系统看成是单体系统的平民化和物美价廉的版本。
目前分布式系统依然还在快速发展中,它不停地掀起一波又一波的浪潮,快速地席卷着 IT 技术的方方面面。我们可以看到它在不同的业务场景中有着不同的表现形式,比如流量路由策略加多副本部署(微服务是其中的一种架构形式)是无状态服务的分布式架构方案, Redis Cluster 和 Codis 等方案实现了缓存的分布式化,而 Kubernetes 则完成了操作系统的分布式进化。
NoSQL 掀起了数据库分布式化的第一波浪潮,而 NewSQL 则推动着支持 ACID 的关系数据库的分布式化,这是数据库分布式化的第二波浪潮,由此可见,分布式系统确实是无法避免的技术大潮流。
后果:如何思考和处理分布式系统引入的新问题
我们在上文中,分析了分布式系统的前因,知道了分布式系统是通过多工作节点,来解决单机系统面临的成本、效率和高可用问题的。但是有利就会有弊,它的出现也引入了分布式系统内部工作节点的协调问题,主要体现在分布式系统内部组件、实例之间,通过异步网络进行通信和协调的问题上。
所以在后果部分,我们主要来解决第二个问题:针对内部工作节点的协调问题,分布式系统是怎么做的?
我们先从简单的情况入手对于分布式计算无状态的情况系统内部的协调需要做哪些工作我们围绕7个子问题进行思考。
其一,怎么找到服务?在分布式系统内部,会有不同的服务(角色),服务 A 怎么找到服务 B 是需要解决的问题。一般来说,服务注册与发现机制是常用的思路,所以,我们可以了解一下服务注册发现机制的实现原理,并且思考一下服务注册发现,是选择做成 AP 系统还是 CP 系统更合理。
其二怎么找到实例在找到服务之后当前的请求应该发往服务的哪一个实例呢一般来说如果同一个服务的实例是完全对等的无状态那么按负载均衡的策略来处理就足够轮询、权重、Hash、一致性HashFAIR 等各种策略的适用场景)。
如果同一个服务的实例不对等(有状态),那么就需要通过路由服务(元数据服务等)先确定,当前要访问的请求数据做到哪一个实例上,然后再进行访问。
其三,怎么管理配置?在分布式系统内部,会有不同的服务(角色),每一个服务都有多个实例,并且还可能自动扩容和缩容。在这样的情况下,通过配置文件的方式,来管理配置是低效、易出错的,对于这个问题,一般的思路是通过一个中心化的存储来统一管理系统的配置,即配置中心。
其四,怎么进行协同?在单体系统中,所有的功能模块都在一个进程中,系统内部进行协同非常简单, 直接调用系统的API 加锁就可以了。但是在分布式系统中,由于不同的功能模块已经拆分为不同的服务,并且一般都运行在不同的机器上,这个时候系统中加锁相关 API 就不能使用了。对于这个问题,我们可以通过一个跨进程与机器的分布式锁来解决。
其五,怎么确保请求只执行一次?在分布式系统中,各个模块之间通过网络进行连接。如果出现了网络抖动等情况,会导致模块之间的调用失败,而调用失败就有可能触发重试策略,使得程序可能出现没有执行或者多次执行的情况。一般来说,重试加上幂等是分布式系统中,确保请求只执行一次的方法。
其六,怎么避免雪崩?系统雪崩是指由于正反馈循序导致不断扩大规则的故障。一次雪崩通常是由于整个系统中,一个很小的部分出现故障而引发,进而导致系统的其他部分也出现故障。比如,系统中某一个服务的一个实例出现故障,导致负载均衡将该实例摘除,从而引起其他实例负载升高,最终导致该服务的所有实例像多米诺骨牌一样,一个一个全部出现故障。
避免雪崩的策略比较简单,主要是两个思路,一个是快速失败和降级机制(熔断、降级、限流等),通过快速减少系统负载来避免雪崩的发生;另一个是弹性扩容机制,通过快速增加系统的服务能力来避免雪崩的发生。我们可以根据不同的场景做出不同的选择,或者两个策略都使用。
一般来说,快速失败会导致部分的请求失败,如果分布式系统内部,对一致性要求很高的话,快速失败会带来系统数据不一致的问题。这种情况下,弹性扩容会是一个比较好的选择,但是弹性扩容的实现成本比快速失败要大,响应时间也更长。
其七,怎么监控告警和故障恢复?对于一个分布式系统来说,如果我们不能清楚地了解内部的状态,那么系统的稳定性是没有办法完全保障的。所以我们一定要完善分布式系统的监控(比如接口的时延和可用性等信息),分布式追踪 Trace ,模拟故障的混沌工程以及相关的告警等机制。同时做好故障恢复预案,确保在故障发生的时候,能够快速恢复故障。
接下来我们再来围绕4个子问题思考一下分布式存储有状态的内部协调是怎么做的因为前面介绍的分布式计算的协调方式在分布式存储中同样适用就不再重复了。
首先,在 CAP 及其相关理论与权衡方面,需要了解 ACID 、 BASE 和 CAP 理论这三个主题。我推荐你阅读一篇文章以及文章后面相关的参考文献,读完后你就能很好地理解 CAP 理论中的取舍了这是英文版本https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/ 这是中文版本https://www.infoq.cn/article/cap-twelve-years-later-how-the-rules-have-changed/ 。
然后我们来思考怎么做数据分片。单机系统是不可能存储所有数据的所以需要解决怎么将数据按一定的规则分别存储到不同的机器上这个问题目前使用较多的方案是Hash 和 Region 分片的策略,但是要注意了解一下它们的优缺点和各自的应用场景。
接着,我们讨论一下怎么做数据复制这方面。为了满足系统的高可用要求,需要对数据做冗余处理,目前的方案主要为:中心化方案(主从复制、一致性协议,比如 Raft 和 Paxos 等)和去中心化的方案( Quorum 和 Vector Clock我们需要了解它们的优缺点各自的应用场景以及对系统外部表现出来的数据一致性级别线性一致性、顺序一致性、最终一致性等
最后,我们来看看怎么做分布式事务。对于分布式系统来说,要实现事务,首先需要对并发事务进行排序的能力,这样在事务冲突的时候,就可以确认哪个事务提交成功,哪个事务提交失败。
在单机系统中,这完全不是问题,简单地通过时间戳加序号的方式就可以实现了。但是对于分布式系统来说,系统中机器的时间不能完全同步,并且单台机器的序号也没有全局意义,所以,按时间戳加序号的方式是不行的。
如果整个系统选一台机器,按照单机的模式生产事务 ID 是可以的同城多中心和短距离的异地多中心也都没有问题。但是想做成全球分布式系统的话每一次事务都要去一个节点获取事务ID的成本太高比如中国杭州到美国东部的 RTT 为 200+ ms我们可以参考 Google 的 Spanner ,它是通过 GPS 和原子钟,实现 TrueTime API 来解决这个问题,从而实现全球分布式数据库的。
有了事务 ID 后,通过 2PC 或者 3PC 协议来实现分布式事务的原子性,其他部分和单机事务差别不大,就不再细说了。
到这里,我们已经对分布式系统的脉络有了基本的概念,接下来就可以进入细节学习的阶段了,对分布式系统的理解深入与否,细节的深入度是很重要的评价指标,毕竟“魔鬼在细节”,所以这也是非常辛苦的阶段。这里我们可以从两个方面进行系统地学习:
首先从实践出发研究目前比较常用的分布式系统的设计HDFS 或者 GFS分布式文件系统、Kafka 和 Pulsar分布式消息队列Redis Cluster 和 Codis分布式缓存MySQL 的分库分表传统关系型数据库的分布式方案MongoDB 的 Replica Set 和 Sharing 机制集NoSQL 数据库TiDBNewSQL以及一些微服务框架等。
然后从理论出发,研究分布式相关的论文,这里推荐一本书“ Designing Data-Intensive Applications ”(中文版本:数据密集型应用系统设计),推荐你先把书整体看一遍,然后找到比较感兴趣的章节,再仔细读一读该章节中涉及的相关参考文献。
最后,为了让你更好地理解,如何处理分布式系统引入的,内部工作节点的协调问题,我把它们总结为下面三类问题。
首先是路由问题,分布式系统由单体系统拆分而来,必然会导致分布式系统内部,出现复杂的路由问题。路由问题主要是解决分布式系统内部各服务和实例之间的通信,我们可以将“怎么找到服务”和“怎么找到实例”等服务注册发现和负载均衡的问题,理解为正常情况下的路由问题,将“怎么做数据分片”的问题,理解为带状态的路由问题,将“怎么避免雪崩”涉及的熔断、降级等快速失败和降级机制,理解为异常情况下的路由问题。
接下来是共识问题,分布式系统的各个组件是运行在不同机器上的不同进程,因为程序总是需要按一定的逻辑有序地执行,所以需要一个办法,来协调分布式系统内部,已经各自为政的服务和实例,而共识就是讨论并解决这一类问题的,例如“怎么做数据复制”、“怎么做分布式事务”和“怎么做分布式锁”里,都会涉及共识问题。
最后是运维问题,分布式系统相对于单体系统是非常碎片化的,如果还依靠人肉运维,在效率上是完全行不通的,所以催生了一系列自动化运维的工具和平台来解决这一类问题,例如“怎么管理配置”和“怎么监控告警和故障恢复”都涉及运维的问题。
总结
通过这样一篇导读,我想告诉你,在学习新知识的时候,只学点状的知识是非常容易忘记的,而只学线状的知识又很难触类旁通,只有点、线结合,形成网状的知识体系,如下图这样,才能举一反三、融会贯通。-
-
所以,在这个专栏中,我会以分布式系统的前因后果为脉络,打造一条由浅入深、从简单到复杂的学习路径,并且会从路由问题、共识问题和运维问题这三大角度,对分布式系统的知识点进行交叉串讲。当你按照这条线性的路径完成学习后,将会得到一个网状的知识体系。
在课程导读中,我分享了如何学习、思考分布式技术原理的方法和路径,最后我想特别邀请你也来分享一下自己学习分布式系统的好方法,或者你也可以说说你在学习、工作中遇到的具体问题和困惑,非常期待你的留言。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

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

View File

@ -0,0 +1,127 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 CAP 理论:分布式场景下我们真的只能三选二吗?
你好,我是陈现麟。
通过学习分布式场景下面临的新挑战,你已经了解了从集中式系统演进到分布式系统时,分布式系统在部分故障、异步网络、时钟同步和共识协调这四个方面的新挑战,以及它们对分布式系统设计原则的影响。了解了这些之后,当你在面对分布式系统各种实现的时候,能更深刻地思考这些系统的取舍与权衡了。
经过不断地思考,人们在实践分布式系统架构的时候,从系统可用性和数据一致性的权衡中总结出来了 CAP 理论它是指导人们在面对架构分布式系统时进行取舍的设计原则。同时CAP 理论深刻影响着分布式系统的设计与发展,是我们在学习分布式系统时不能绕过的知识。
所以在这节课中,我将和你一起来讨论什么是 CAP 理论以及它产生的影响,并且我们还会讨论在当前这个时间点,业界对于 CAP 理论的重新思考与理解。
什么是 CAP 理论
CAP 理论是加州理工大学伯克利分校的 Eric Brewer 教授在 2000 年 7 月的 ACM PODC 会议上首次提出的,它是 Eric Brewer 在 Inktomi 期间研发搜索引擎、分布式 Web 缓存时得出的关于数据一致性( CConsistency )、服务可用性( AAvailability )、分区容错性( PPartition-tolerance )的一个著名猜想:
It is impossible for a web service to provide the three following guarantees : Consistency, Availability and Partition-tolerance.
在这个猜想提出的 2 年以后,来自麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 Eric Brewer 教授的 CAP 猜想是成立的从此CAP 理论在学术上正式成为了分布式领域公认的定理,并深刻影响着分布式系统的发展。
CAP 理论告诉我们,一个分布式系统不可能同时满足数据一致性、服务可用性和分区容错性这三个基本需求,最多只能同时满足其中的两个。为什么会这样呢?我们先来了解一下 CAP 理论对于数据一致性、服务可用性和分区容错性是怎么定义的。
一致性( C
CAP 理论中的一致性是指强一致性( Strong Consistency ),又叫线性一致性( Linearizable Consistency ),它要求多节点组成的分布式系统,能像单节点一样运作,如果一个写操作返回成功,那么之后的读请求都必须读到这个新数据;如果返回失败,那么所有的读操作都不能读到这个数据。
一致性中除了强一致性之外,还有其他的一致性级别,比如序列一致性( Sequential Consistency )和最终一致性( Eventual Consistency )等,这个在后面的课程“一致性与共识(一)”中会有详细的介绍。
可用性( A
CAP 理论对可用性的定义,指的是要求系统提供的服务必须处于 100% 可用的状态,对于用户的每一个操作请求,系统总能够在有限的时间内返回结果。下面我们重点来讨论可用性定义中的三个关键点:“ 100% 可用”、“有限时间内”和“返回结果”。
第一点100% 可用,既不是 99% 可用也不是99.99% 可用,它说的是系统必须完全可用,不允许任何不可用的情况出现,这是一个非常理想的模型。
第二点,有限时间内,它指的是对于客户端的一个请求,系统必须在指定的时间内返回对应的请求结果,如果超过了这个时间,系统就被认为是不可用的。一般来说,“有限时间内”是系统在设计的时候,就设定好的系统运行指标,不同的系统之间会有非常大的差别。
例如,对于一个服务在线业务的 OLTP 数据库 MySQL ,它的“有限时间”一般不会超过 1 秒,但是对于服务离线分析的 OLAP 数据库 Hive ,它的“有限时间”可能会超过 30 秒,甚至更长。
虽然不同的系统对于“有限时间”的设定差别非常大,但是对于一个给定的系统来说,在设定了这个“有限时间”之后,只要对一个请求的响应超过了这个时间,我们就认为这个系统是不可用的。
第三点,返回结果,这是指系统在完成对客户端请求的处理后,必须返回一个正常的响应结果。客户端可以根据这个响应结果,来明确判断这个请求执行成功还是失败,而不是返回一个让用户无法判断的不正常的响应结果。
比如客户发起一个请求,从用户 A 的账户转 50 元到用户 B 的账户,“转账成功”和“余额不足”都是正常的响应结果,而“服务不可达”和“服务器内部错误”等就是不正常的响应结果。
分区容错性( P
分区指的是在整个分布式系统中,因为各种网络原因,系统被分隔成多个单独的部分,它不仅包含我们通常说的网络分区,也包含因为网络丢包导致的网络不通的情况。并且,这里说的因为网络丢包导致网络不通的情形,还包含节点宕机的场景,由于系统的其他机器不知道某个节点宕机了,只知道与宕机节点的网络是不通的,所以当节点宕机发生时,其他节点发往宕机节点的包也将丢失。
在现实的分布式系统中,我们面对的就是一个不可靠的网络和有一定概率宕机的设备,这两个因素都会导致分区出现,因此在分布式系统实现中,分区容错性 P 是一个必须项,而不是可选项。
在分布式系统中,如果我们的设计放弃分区容错性,就相当于我们认为节点之间的网络通信永远是好的,那么我们对节点之间的远程调用的结果,就不需要处理超时、网络地址不可达等网络层错误了。但是这样一来,看似是简化了系统设计,实际却忽视了超时等网络错误的情况。当它们出现后,系统的行为就是未定义的了,可能会出现崩溃,或者是脏数据的问题。
因此,对于分布式系统工程实践来说, CAP 理论更合适的描述是:在满足分区容错的前提下,没有算法能同时满足数据一致性和服务可用性。
CAP 理论产生的影响
关于数据一致性和可用性之间的争论由来已久,当时主要表现为 ACID 与 BASE 之间的争论。
基于 BASE 理论支撑的 NoSQL 运动坚持创造各种可用性优先、数据一致性其次的方案,而传统数据库则坚守 ACID 特性(原子性、一致性、隔离性、持久性),优先数据一致性,在必要的时候,可以放弃系统可用性。当时 BASE 理论还没有被广泛接受,人们还是不愿意放弃 ACID 的优点。
当 CAP 理论提出后,我们明白了在分布式系统中,只能在强一致性和 100% 的可用性之间二选一,不能两个都要。从此 BASE 理论也逐渐被人们所接受,在大规模存储的场景中广泛应用,并且开创了从 2000 年到 2010 年, NoSQL 运动的黄金十年。这十年里,工业界产生了大量优秀的 NoSQL 系统,比如 BigTable 、 HBase 、 MongoDB 、 Cassandra ,解决了人们当时遇到的大规模数据存储的问题。
CAP 理论的重新思考与理解
CAP 理论的出现是有历史使命的,让人们能够在分布式系统中,放弃以关系数据库为代表的 ACID 强一致性系统,接受以 NoSQL 为代表的 BASE 理论,并且暂时解决了人们在 2000 年前后对于分布式系统中,数据一致性和可用性之间的争论,让人们能够更加务实地解决当时由于互联网爆发式发展,产生的海量用户和数据的分布式计算与存储的问题。
一个有历史使命的事物,在使命完成后,要么就过时了,人们不再提起它,要么就会对它有新的解释,让它跟随时代一起发展下去。而 CAP 理论显然属于后者,因为直到现在,人们还在对它不断地重新思考与理解。
在 2000 年的时候CAP 理论通过一个简单但是精确定义的模型,论证了在一个满足分区容错的分布式系统中,当我们进行系统设计时,只能在数据一致性和服务可用性之间二选一。其中,数据一致性( C )指的是数据的强一致性,服务的可用性( A 指的是服务100 % 的可用性,这才是 CAP 理论论证模型的关键点。
对可用性的重新思考与理解
首先,我们对服务的可用性( A )进行分析,你会发现在我们的日常工作中,几乎没有见过 100% 可用的服务。可用性指标是在 0 到 100% 之间连续分布的,其实一个 100% 可用性的服务和一个 99.9999% 可用性的服务之间并没有多大的差别,如果我们的服务能实现 99.9999% 的可用性,哪怕它不符合 CAP 理论的可用性,也是符合我们工作中对可用性的要求的。
所以,在我们的系统选择了 CP 模型的时候,对于可用性( A ),我们永远无法达到 100%,但是按业务要求不断优化,是我们努力的目标。
关于具体的实践,我认为基于 Raft 算法实现的 etcd 就是一个非常好的,对可用性进行重新思考的实践。如果依据 CAP 理论来划分的话etcd 属于 CP 模型。
而在 etcd 系统的实现中,如果网络没有出现分区,整个系统是 100% 可用的就算网络出现分区了也不会有整个etcd 系统都不可用的情况。在这时,超过半数 etcd 实例所在的网络分区一侧,系统是正常可用的,虽然网络分区的另一侧是不可用的,但是整个 etcd 系统的可用性依然可能超过 50% 。
对一致性的重新思考与理解
对于数据的一致性( C ,除了 CAP 理论要求的强一致性外,还有单调一致性、会话一致性和最终一致性等。如果我们的系统设计选择了 AP 模型,在数据一致性方面,虽然我们无法实现强一致性,但是我们也不要全部放弃,可以努力去实现更高的一致性级别,为系统的服务提供更好的抽象。
这里我们通过一个例子来说明,假设我们设计一个 AP 模型的分布式系统,正常情况下,如果依据 CAP 理论,在系统设计时,我们需要放弃数据的一致性。但是,我们可以从另一个思路来设计,在系统没有出现网络分区的时候,这个分布式系统应该设计为强一致性的。
如果出现网络分区了,我们可以根据系统情况,有选择并且精心设计地降低系统的一致性级别。比如,从强一致性降低到单调一致性或会话一致性等,这样的设计,既符合 CAP 理论依据,也为系统提供了更好的一致性级别,特别是在网络分区的时候。
对分区容错性的重新思考与理解
最后,我们来分析一下分区容错性 P 的问题。在分布式系统中,节点之间必须通过网络来通信,可是网络可能会丢包和中断,节点也可能会宕机,这样的情况就要求我们在系统设计的时候,必须做好系统的分区容错处理。
但是,系统出现分区的情况非常少见,所以我们可以来试想一下,在网络不出现分区的时候,我们将数据强一致性和 100% 的可用性都选择,等到网络出现分区的时候,系统再选择放弃部分的可用性或者降低数据一致性的级别,这种处理方式是否可行呢?
其实这样的处理方式是可以的,在上面对可用性和一致性的重新思考与理解中,所举的例子都是按这个方式来处理的,它实际是将 CAP 理论的选择,推迟到出现网络分区的时候,而不是系统一启动就进行 CAP 的选择。这样可以大大提高系统的可用性和数据一致性,并且系统依然能容忍网络分区。
另外,关于 CAP 理论的重新思考,特别需要说明的一个例子是 Google 的 Spanner ,我们都知道 Spanner 是一个全球分布式数据库,但是 Google 却宣称 Spanner 是一个 CA 系统,这是不是和 CAP 理论的说法产生了矛盾呢?
其实并不矛盾Spanner 虽然是一个分布式系统,但是它运行在 Google 的内部网络中,并且拥有大量冗余的网络链路、处理相关故障的架构规划、以及非常细致的运维,以此来确保系统的可用性超过了 99.999%。虽然不能达到 100%,但是对于使用者来说,和可用性 100% 几乎没有任何区别所以Spanner 就是一个 CA 系统。
而且在网络出现分区的时候Spanner 会选择一致性而不是可用性,这个时候 CAP 理论依然会生效。所以对于 CAP 理论的重新思考总而言之就是一句话CAP 理论给我们定义了系统的设计边界,虽然想要设计出超过边界的系统是徒劳的,但是我们却可以无限逼近边界,并且把它作为我们设计系统的目标。
总结
到这里我们知道了在分布式系统场景下CAP 理论的相关知识,我们一起来总结一下这节课的主要内容:
首先,我们一起讨论了什么是 CAP 理论,它是指分布式系统中,在满足分区容错的前提下,没有算法能同时满足数据一致性和服务可用性,只能在数据一致性和服务可用性之间二选一。
然后,我们讨论了 CAP 理论产生的影响,可以说 CAP 理论的出现,让人们接受了 BASE 理论,并且推动了 NoSQL 运动的发展,开启了它的黄金十年。
最后,我们探讨了现在人们对于 CAP 理论的新理解,对于 CAP 理论,我们不会简单地三选二或者二选一。对于 AP 模型的系统,我们会努力去提升数据一致性的级别,而对于 CP 模型的系统,我们会努力去提升系统可用性的级别。
同时,由于系统分区的情况非常少见,我们可以在网络不出现分区的时候,将 A 和 C 都选择上;在网络出现分区的时候,再选择放弃部分的可用性,或者降低数据一致性的级别,通过推迟 CAP 选择来提高系统的可用性和数据一致性。
思考题
在分布式场景下,对于 CAP 理论,我们真的只能三选二吗?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,138 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 注册发现: AP 系统和 CP 系统哪个更合适?
你好,我是陈现麟。
在前面的“概述篇”里,我们介绍了分布式技术的来龙去脉,以及在构建一个分布式系统的时候,我们会面临的相关挑战。从这节课开始,我们将一起进入到“分布式技术篇”的学习当中。
在这个专栏里,我们会聚焦日常工作中接触最频繁的分布式在线业务技术。学完这部分内容,相信你会对分布式计算技术心中有数,同时不会迷失于实现的细节中。
当然,分布式计算是个非常大的技术体系,包括 MapReduce 之类的分布式批处理技术Flink 之类的分布式流计算技术和 Istio 之类的分布式在线业务技术。但是万变不离其宗,我们掌握了分布式计算技术中稳定不变的知识、原理和解决问题的思路,再研究这些技术的时候也会一通百通。
如果直接讨论技术知识和原理,可能会让你觉得非常枯燥和抽象。通过具体的场景案例来讨论技术是非常好的方式,所以我给你虚构了后面这个场景。
假设你是极客时间的一个研发工程师,负责极客时间 App 的后端开发工作。目前极客时间采用的是单体架构,服务端所有的功能、模块都耦合在一个服务里。由于现在用户数据和流量都在快速增长,经常会因为一次小的发布,导致全站都不可用,所以在白天的时候,你都不敢发布服务。
等到时间一长,凌晨流量低峰时的运维慢慢变成常态,你经常收到机器 CPU、内存的报警但是每一次都很难知道是什么业务功能导致的只能直接升级机器配置。慢慢的你发现工作中的问题和挑战越来越多但是不知道怎么处理。
你是不是也在面临这样的困境呢?我要告诉你的是别担心,在接下来的课程中,我们将会通过分布式技术来一一解决这些问题。
为什么需要服务注册发现
其实极客时间服务器采用的单体架构,在业务早期的快速迭代中,发挥了非常重要的作用。但随着用户数量和流量的快速上涨,这个单体架构就遇到了成本、效率和稳定性的问题。-
单体服务面临的问题
首先是成本方面。我们在做所有的事情时都会考虑投入产出比ROI所以成本是我们必须考虑的一个问题。对于单体服务在服务器硬件方面的成本我们需要特别注意异构工作负载和不同保障级别这两个方面的问题。
我们先来看异构工作负载方面的问题。单体服务会包含多种多样的功能模块,有一些是 IO 密集型的模块,比如主要对数据库进行 CRUD 的功能模块;另一些则是计算密集型的模块,比如图片、音频和视频转码相关的功能模块。如果能将 IO 密集型和 CPU 密集型的模块拆分成不同的服务,分开部署到更合适的硬件上,将可以节省大量的机器成本。比如 IO 密集型的模块,我们可以部署在 CPU 性能相对较低的机器上。
另一个问题是不同的保障级别。不同业务等级的保障级别也是不一样的:对于账号模块等核心模块,必须确保资源充足;但是对于非核心模块,保障的资源可以相对少一些。而对于一个单体服务来说,是没有办法对不同的模块实施不同的保障级别的。
其次,研发效率是我们能够高效、舒心工作的基本保障,所以必须要注意单体服务模式导致的串行的编译、测试和发布,以及研发团队只能选择单一的研发语言和生态(一般在进程内跨语言都会有限制)这两个限制。
串行的编译、测试和发布很好理解:多个研发团队会同时开发不同的功能,由于是单体服务,这些功能只能一起编译、测试和发布,非常浪费时间。如果还要进行灰度发布,那么效率将会更低。
另外还有单一的语言和生态限制。要知道,不同的业务需求可能会对应不同的编程语言和生态。如果是单体服务,则很难按业务需求来选择编程语言和相关的生态,这会大大影响研发效率。
最后,我们来讨论一下单体服务引发的稳定性问题。
一来局部风险会放大到全局,因为整个单体服务会包括非常多的功能,一个局部非核心功能的崩溃、死锁等各种异常情况,都会影响所有的业务。这样的风险非常大,而且我们没有办法将故障隔离开。
二来业务迭代周期差异大,一般来说,越底层核心的功能,需求就越稳定,因为它的迭代周期会比较长,比如 4 周迭代一次;而越上层的业务功能,需求变更就越频繁,因为它的迭代周期会比较短,比如 1 周迭代一次。由于单体服务不能分开发布,所以在业务功能迭代的时候,底层核心功能也必须频繁地发布,这对于稳定性来说是一个考验。
经过仔细分析,我们会发现上面三个方面的本质问题,都是因为我们的业务是一个单体应用,不能按资源类型进行分别扩容,不能按功能或者服务进行小范围的部署,也不能按业务的需求来选择更适合的研发语言和生态等,所以我们决定按资源和业务等维度对单体服务进行拆分。
服务注册发现的业务场景
这个时候,我们会遇到一个新的问题:之前所有的功能都在一个服务里面,不同模块和功能之间直接通过本地函数进行调用,拆分为多个服务后,怎么调用其他服务的函数呢?
你肯定能很快想到,通过 REST API 或者 RPC 来进行跨服务的调用。的确,这是个非常好的办法,但是通过 REST API 或者 RPC 都需要知道被调用服务的 IP 和 Port。所以我们还需要解决一个问题如果服务 A 需要调用服务 B那么服务 A 怎么获取被调用服务 B 的 IP 和 Port 呢?这个其实就是服务注册发现的业务场景。
服务注册发现的关键问题是什么
我们先一起来讨论一下可以尝试哪些可行的方式。
首先,最容易想到的方式是配置 IP 和 Port 列表,即直接在服务 A 的配置文件中配置服务 B 的 IP 和 Port如果服务 B 有多个实例,那么就配置一个列表。
这样的确解决了问题,但是如果服务 C、D、E 等非常多的服务,都需要调用服务 B那么这些服务都需要维护服务 B 的 IP 和 Port 列表。每一次当服务 B 增加、删除一个实例,或者一个实例的 IP 和 Port 发生改变时,所有调用服务 B 的服务都需要更新配置,这是一个非常繁杂并且容易出错的工作,那么怎么避免这个问题呢?
其实,我们可以将配置 IP 和 Port 列表的方式修改为配置域名和 Port即在服务 A 的配置文件中不再配置服务 B 的 IP 和 Port 列表,而是配置服务 B 的域名和 Port。这样可以通过域名解析获得所有服务 B 的 IP 列表,让所有的服务 B 都监听同一个 Port。
当服务 B 的实例有变更,不论有多少个服务调用服务 B只需要修改服务 B 的域名解析就行了,这样就解决了配置分散到各个调用服务,导致配置一致性的问题。
但是如果服务 B 的某个实例出现了崩溃、网络不通等情况时,服务 A 在对服务 B 的域名做 DNS 解析时,会因为我们不能实时感知服务实例的状态变更,依然获得该实例的 IP从而导致访问错误。
这里我们举一个租房中介的例子来说明一下。假设每一个要租 A 小区房子的人,都需要亲自去 A 小区获得租房的信息,同样,如果还想租 B 小区的房子,也需要亲自去 B 小区获得租房的信息,这是一个非常麻烦的事情。而更麻烦的是,一个小区的租房信息有变化了,之前获得信息的人都不会立刻知道,非常影响我们的租房效率和成功率。
这个时候,租房中介出现了,他每天去各个小区收集租房信息,我们需要租房的时候,直接联系中介就可以获得相关小区的租房信息,并且,中介会记录谁关心哪一个小区的租房信息。如果一个小区的租房信息有变化,中介会主动通知给关心这个小区的人,这样就让租房这件事情变得非常高效了。这里的租房中介,其实就是承担租房信息的注册和发现的功能。
所以,经过前面的讨论,我们可以得出服务注册发现需要解决的两个关键问题:
统一的中介存储:调用方在唯一的地方获得被调用服务的所有实例的信息。
状态更新与通知:服务实例的信息能够及时更新并且通知到服务调用方。
怎么实现服务注册发现
接下来我们就一起来讨论“统一的中介存储”和“状态更新与通知”这两个关键问题的解决办法。
如何选择适合的中介存储
“中介存储”这个问题,其实是我们在解决服务注册发现的时候,引入的一个中间层。“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”,这一经典论断又一次被验证了。
我们需要找一个外部存储来做解决问题的中间层,但是基于服务注册发现的场景,我认为这个存储需要有以下几个特点:
可用性要求非常高:因为服务注册发现是整个分布式系统的基石,如果它出现问题,整个分布式系统将不可用。
性能要求中等:只要设计得当,整体的性能要求还是可控的,不过需要注意的是性能要求会随分布式系统的实例数量变多而提高。
数据容量要求低:因为主要是存储实例的 IP 和 Port 等元数据,单个实例存储的数据量非常小。
API 友好程度:是否能很好支持服务注册发现场景的“发布/订阅”模式,将被调用服务实例的 IP 和 Port 信息同步给调用方。
基于上面对所需求存储系统特点的分析,我们一起来对常见的存储系统做一个系统性的比较:-
-
通过上面的分析我们可以看到这些存储系统几乎都能用来作为服务发现的中介存储系统但是基于整体考虑MySQL 和 Redis 在高可用性和 API 友好程度上不满足要求,所以更合适的存储系统为 etcd、ZooKeeper 和Eureka。如果你希望在系统出现网络分区的时候调用方一定不能获取过期的被调用服务实例信息那么就选择 etcd 和 ZooKeeper但是在被分区的部分网络中可能出现因为不能获取被调用服务实例信息而导致请求失败的情况。
如果你认为获取过期的实例信息,可能比完全不能获取被调用服务的实例信息要好,那么就选择 Eureka。毕竟大部分情况下信息并没有过期因为被调用服务的实例配置还没有发生变更并且就算获得的信息过期了也只是导致一次请求失败。
怎么做服务状态的更新与通知
对于“状态更新与通知”这个问题,我们可以将其分解为两个问题解决:-
-
首先是服务的状态更新,即服务注册:如上图中的 1服务的每一个实例每隔一段时间比如 30 秒,主动向中介存储上报一次自己的 IP 和 Port 信息,同时告诉中介存储这一信息的有效期,比如 90 秒。这样如果实例一直存活,那么每隔 30 秒,它都会将自己的状态信息更新到中介存储。如果实例崩溃或者被 Kill 了,那么 90 秒后,中介存储就会自动将该实例的信息清除,避免了实例信息的不一致。所以这里的数据同步是最终一致性的。
然后是服务的状态通知,即服务发现:如上图中的 2服务的调用方通过中介存储监听被调用服务的状态变更信息。这里可以采用“发布/订阅”模式也可以采用轮询模式比如每30秒去中介存储获取一次。所以这里的数据同步也是最终一致性的。
选择 AP 还是 CP
根据上面的讨论从服务注册发现的场景来说我认为Eureka 之类的 AP 系统更符合要求。因为服务发现是整个分布式系统的基石,所以可用性是最关键的设计目标。并且上面介绍的服务,在同步自己的状态到中介存储,以及调用方通过中介存储区获得服务的状态,这两个过程中的数据同步都是最终一致性的。既然服务注册发现系统整体是一个 AP 系统,那么将中介存储设计为 CP 系统,去放弃部分的可用性是不值得的。
到这里,服务注册发现的基本原理就介绍完了。当我们去研究各种各样服务发现的实现方式时,就会发现其实它们都是在解决“如何选择适合的中介存储”和“怎么做服务状态的更新与通知”的问题。当然由于服务发现是非常基础和重要的功能,所以其中的各种实现都是在高性能、高可用性的基础上解决上面的两个问题,做着各自的优化与权衡。
总结
到这里,我们一起完整地讨论了分布式系统中,一个非常关键的组件“服务注册发现”。我们一起来总结一下这节课的主要内容。-
-
首先,我们一起讨论了为什么会对单体服务进行拆分,主要有成本、效率和稳定性三个维度的原因。
然后,在将单体服务拆分后,之前很方便的本地函数调用变成了跨实例或者跨机器的远程调用。这个时候,调用方需要知道被调用方的 IP 和 Port 等信息。
接着,我们发现 IP 和 Port 信息列表手动配置存在配置分散,无法统一管理的问题,在调用方变多之后,将会变得无法维护。域名 和 Port 信息的手动配置需要解决了配置统一管理的问题,但是如果实例出现突发的异常情况,将无法通知到调用方,导致故障。
最后,我们讨论了通过中介存储做服务发现的方式,其中最关键的是对于中介存储的选择问题。而且在服务发现的场景里面,高可用性是最应该去考虑的设计指标,所以选择 AP 系统做中介存储是一个不错的选择。
思考题
如果将整个互联网看成是一个非常庞大的分布式系统,那么这个分布式系统的服务注册发现系统是怎么实现的?它是一个 AP 系统还是一个 CP 系统?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,144 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 负载均衡:从状态的角度重新思考负载均衡
你好,我是陈现麟。
通过学习“注册发现”的内容,你已经明白了分布式系统为什么需要注册发现组件,也知道了在实现注册发现时要注意的两个关键点,并且理解了从 CAP 理论的角度来说,注册发现是一个 AP 模型。如果我们想把极客时间这个单体服务,改造成一个分布式系统,那么这些内容都将为我们打下一个良好的基础。
同时,极客时间为了实现系统的高可用和高性能,它所有的服务都会部署多个实例,那么这就会导致在极客时间的后端系统,调用方通过注册发现组件,去获得被调用服务实例的网络地址时,获取到包含多个服务实例的网络地址列表。这时你将面临一个新的问题,那就是调用方应该将请求,发向被调用服务的哪一个服务实例呢?
在本节课里,我们就一起来解决分布式系统中,多个被调用服务实例的选择问题,即负载均衡策略。我们会先从负载均衡在架构设计中需要考虑的关键点出发,根据负载均衡策略是否关心请求中携带的信息,即请求是否有状态,将负载均衡策略分为无状态的负载均衡、半状态的负载均衡和全状态的负载均衡,从状态的角度来重新思考。
负载均衡的关键点
每一个被调用服务(后面简称为后端服务)都会有多个实例,那么服务的调用方应该将请求,发向被调用服务的哪一个服务实例,这就是负载均衡的业务场景。
关于如何解决这个问题,我们可以换一个角度,站在被调用服务实例(后面简称为后端实例)的角度理解负载均衡。对于后端实例组来说,负载均衡就是一个调度器,它将发送给被调用服务的每一个请求,按一定的策略分配给后端实例组中的一个实例,确保能高效、正确地提供服务。
根据上面的讨论,我们可以得出,负载均衡需要达到的目的是“确保能高效、正确地提供服务”,同时从这个目的中,我们还可以分析出负载均衡的两个关键点。
首先,我们结合“高效地提供服务”这个目的来分析。如何高效地提供服务,我认为可以理解为后端实例组多个实例的资源运行效率问题。负载均衡需要考虑到各个实例性能差异的情况,让每一个实例都能充分发挥它的能力,不要出现一些实例负载比较高,而另一些实例的负载却非常低的情况,这样会造成资源浪费。
所以,我们从中可以得出,负载均衡的第一个关键点是公平性,即负载均衡需要关注被调用服务实例组之间的公平性,不要出现旱的旱死,涝的涝死的情况。
接着,我们来讨论一下“正确地提供服务”这个目的。如何正确地提供服务,我认为这是后端服务对外表现出的整体结果。负载均衡需要确保外部对后端服务的请求,一定能被路由到可以提供正确服务的实例上。如果后端实例是有状态的,比如需要利用本地缓存和存储来处理请求的,我们就需要考虑每个请求携带的状态,然后依据状态信息,将请求正确路由到后端的实例上。
从这里我们可以得出,负载均衡的第二个关键点是正确性,即对于有状态的服务来说,负载均衡需要关心请求的状态,将请求调度到能处理它的后端实例上,不要出现不能处理和错误处理的情况。
我们已经讨论出了负载均衡的两个关键点:公平性和正确性。所以,在后面讨论负载均衡各种不同的策略时,我们将采用公平性和正确性这两个维度,来评价每一种负载均衡策略的具体情况。
为了更好地实现负载均衡的公平性和正确性,针对各种不同的业务场景,出现了多种不同的策略。在这些不同的业务场景中,我认为对负载均衡策略的设计,影响最大的因素是后端实例是否存在状态,后端实例有状态,负载均衡就需要关心请求的状态。
如果一个有状态的请求,被路由到错误的后端实例上,将会导致请求无法处理或者获得错误的结果。比如一个查询用户年龄的请求,如果负载均衡策略将该请求,错误地路由到一个没有存储该用户年龄数据的实例上,那么这个实例就只能返回 not found 。对于有状态的请求,如果路由错误,就会影响负载均衡的正确性。
因此我们会在下文中,依据负载均衡是否关心请求的状态,将负载均衡策略分为无状态的负载均衡、半状态的负责均衡和全状态的负载均衡,结合负载均衡的两个关键点一一进行分析。
无状态的负载均衡
无状态的负载均衡是我们日常工作中接触最多的负载均衡模型,它指的是参与负载均衡的后端实例是无状态的,所有的后端实例都是对等的,一个请求不论发向哪一个实例,都会得到相同的并且正确的处理结果,所以无状态的负载均衡策略不需要关心请求的状态。
到这里,你可能会有一个疑问,这些无状态实例难道不能处理像存储数据这样的状态吗?如果需要处理状态应该怎么办呢?这是一个很好的问题,答案也非常简单。
实例将这些状态信息的处理都交给一个中心存储来负责,比如 MySQL 数据库和 Redis 缓存等,实例不在本地机器的磁盘或者内存中,存储任何状态信息。这是一个非常好的设计原则,让专业的中心存储来处理状态信息,大大简化了系统的设计。
下面我们以轮询和权重轮询来举例,先讲一讲它们的负载均衡策略,再结合公平性和正确性这两个关键点,评价无状态的负载均衡策略的具体情况。
轮询
轮询的负载均衡策略非常简单,只需要将请求按顺序分配给多个实例,不用再做其他的处理。例如,轮询策略会将第一个请求分配给第一个实例,然后将下一个请求分配给第二个实例,这样依次分配下去,分配完一轮之后,再回到开头分配给第一个实例,再依次分配。
轮询在路由时,不利用请求的状态信息,属于无状态的负载均衡策略,所以它不能用于有状态实例的负载均衡器,否则正确性会出现问题。在公平性方面,因为轮询策略只是按顺序分配请求,所以适用于请求的工作负载和实例的处理能力差异都较小的情况。
权重轮询
权重轮询的负载均衡策略是将每一个后端实例分配一个权重,分配请求的数量和实例的权重成正比轮询。例如有两个实例 AB假设我们设置 A 的权重为 20B 的权重为 80那么负载均衡会将 20% 的请求数量分配给 A80 % 的请求数量分配给 B。
权重轮询在路由时,不利用请求的状态信息,属于无状态的负载均衡策略,所以它也不能用于有状态实例的负载均衡器,否则正确性会出现问题。在公平性方面,因为权重策略会按实例的权重比例来分配请求数,所以,我们可以利用它解决实例的处理能力差异的问题,认为它的公平性比轮询策略要好。
无状态的负载均衡策略除了上面的两种外,还有 FAIR 、随机、权重随机和最少链接数等策略,你可以从两个关键点出发对这些负载均衡策略进行分析。
半状态的负载均衡
半状态的负载均衡指的是,虽然负载均衡策略利用请求的状态信息进行路由,但是仅仅进行简单的规则处理,比如 Hash 运算加求模来路由请求,它不保证路由的正确性,这个正确性由后端实例来保证。
另外,一些实例会在内存中缓存一些状态数据,用于提升系统的性能,如果一个请求被路由到错误的实例中,该实例可以立即通过中心存储,读取出所需要的数据,然后在内存中重建并缓存正确的处理请求,不会导致请求出现错误。
而对于路由错误,后端实例不能恢复状态数据的场景,后端节点需要适应路由策略来保证数据的正确性,例如基于 Hash 策略路由的 MySQL 集群,如果集群的数目发生变更,我们需要通过数据迁移来保证路由的正确性。
所以,我们可以看出,半状态的负载均衡将请求按一定的策略进行路由,后端实例可以利用路由规则来进行优化。假设后端实例在进程里面缓存用户的信息,如果我们能将同一个用户的多个请求,都路由到同一个实例上,相对于轮询策略,单个实例不需要缓存全部的用户信息,可以大大减少缓存的内存容量。
为了评价半状态的负载均衡策略的具体情况,我们以 Hash 和一致性 Hash 来举例。
Hash
Hash 负载均衡策略是指将请求的状态信息,按一定的 Hash 算法固定分配到一个实例上,例如,按请求的来源 IP 地址或者用户的 ID将同一个来源 IP 地址或者用户 ID 的请求固定到一个实例上。
我们来举个例子,如果有两个实例,我们想将相同用户 ID 的请求,固定分配到一个实例上面,那么按如下的方法来计算:
\[-
\\text { i }=\\operatorname{MD5}\\left(\\text {ID)} \\%2\\right.-
\]这里要说明一下,公式中的 2 为实例的数量,除了 MD5 外,我们还可以使用不同的 Hash 算法。我们将实例从 0 开始编号,上面公式的计算结果 i 为负载均衡将要分配实例的编号。
从这个计算公式中,我们可以看出 Hash 负载均衡策略,在机器实例数量发生变化的时候,几乎所有请求的分配实例都会发送变化。如果后端实例依赖 Hash 负载均衡策略来保证正确性,那么当实例数发生变化的时候,正确性将会出现问题。对于 Hash 策略是如何保证正确性的具体内容,在后面“数据分片”的课程中,我们将会继续讨论。
公平性方面,在不考虑 Hash 算法均匀性的情况下Hash 策略会按 Hash 值按模等分,它和轮询策略类似,不能解决请求的工作负载和实例的处理能力差异的问题。
一致性 Hash
Hash 的负载均衡策略中,最大的一个问题是基于机器数量求模,如果机器数量发生变化,请求和实例的分配关系机会将全部变化,这会影响它的正确性,而一致性 Hash 就可以用来解决这个问题,你可以结合下图来理解:-
假设我们定义 Hash 环的空间大小为\(2^{32}\),那么我们先将 0 ~ \(2^{32}\)均匀地分配到上图的 Hash 环上,将所有的实例按其唯一标识(例如名字的字符串 “ Node A ”)计算在环上的位置:
\[-
\\text { iNode }=\\operatorname{hash}\\left(\\text { Node ID) } \\%2^{32}\\right.-
\]然后,对于每一个请求,我们也按上面的方法计算其在环上的位置:
\[-
\\text { iRequest }=\\operatorname{hash}\\left(\\text {Request ID) } \\%2^{32}\\right.-
\]最后,按请求在环上的位置沿环顺时针“行走”,遇到的第一个服务器节点,就是该请求负载均衡分配的节点。这里要注意的是,“键 5 ”沿环顺时针“行走”到环的结尾,如果还没有找到服务器节点,将从环的开头继续找,直到找到 Node A 。
你可以看到,一致性 Hash 和 Hash 策略最大的区别在于,一致性 Hash 是对固定值\(2^{32}\)求模,不会随着机器数量的变化而变化,所以对于同一个 Request ID iRequest 是始终稳定不变的,这样就解决了 Hash 的策略在实例数量发送变化后,几乎所有的分配关系都会发生变化的问题。
如果一致性 Hash 的机器数量发生变化后,会出现什么问题呢?其实就是发生变化的实例节点逆时针方向的一些请求的路由实例会发生改变,例如 Node A 下线了,那么“键 5 ”将被路由到 Node B ,如果在“键 5 ”和 Node B 之间新增了一个节点,那么“键 5 ”将路由到新增的节点。那么关于一致性 Hash 策略如何保证正确性的问题,我们也是在后面的“数据分片”课程中详细讨论。
到这里,你是不是觉得一致性 Hash 能在后端实例数量变化的时候,依然保持比较好的正确性,已经很完善了呢?
其实还有一个问题,那就是公平性,这里有两点需要我们注意。首先,如果后端实例数非常少,公平性将会出现问题,假设上图中只有 Node B 和 Node C ,那么 Node B 将要承担 70% 以上的请求;其次,如果各个节点的性能差异比较大,这样的情况我们会希望能按权重来进行分配。
关于一致性 Hash 策略公平性的问题,一致性 Hash 是通过增加虚拟节点的方法来解决的,在 Hash 环中路由到虚拟实例的请求,会被路由到它的真实实例上,比如下图中“键 1”和“键 3”的请求将路由到 Node A。-
对于实例数过少导致的公平性问题,一致性 Hash 策略让每一个实例都生成多个虚拟实例,使分配更加均衡;对于实例之间性能差异的问题,一致性 Hash 策略通过让实例生成虚拟实例的数量,与该实例的权重成正比的策略来解决。
全状态的负载均衡
全状态的负载均衡是指,负载均衡策略不仅利用请求的状态信息进行路由,并且在后端实例有状态的情况下,依然会保证路由的正确性。那它是怎么做到的呢?下面我们就来讨论一下全状态负载均衡的实现。
全状态的负载均衡一般以路由服务的形式存在,在路由服务里面,都会存储后端实例 ID 和状态信息的索引,在进行请求路由的时候,路由服务从请求的状态信息中获得索引的标识,通过查询索引获得后端实例的 ID然后再进行路由。
如果你了解过“数据分片”机制,你就会发现它和全状态的负载均衡非常类似,其实它们就是一个事情,只是我们讨论的角度不同。如果我们从请求调度的角度来讨论,这就是一个全状态服务的负载均衡问题,如果我们从后端实例数据分布的角度来讨论,这就是一个数据分片的问题。
那么关于全状态的负载均衡策略,我们将放到后面的“数据分片”课程中进行讨论,这里就先不再赘述了。
总结
到这里,我们一起讨论了分布式系统场景下的负载均衡问题,一起来总结一下这节课的主要内容:
首先,我们通过对负载均衡业务场景的讨论,确定了评价负载均衡策略的关键点:公平性和正确性,以后当我们碰到负载均衡策略选型的时候,可以通过公平性和正确性来进行讨论。
然后,我们讨论了后端为无状态实例,常用的无状态的负载均衡策略:轮询、权重和 FAIR 等,学完这部分,你可以为无状态实例来选择合适的负载均衡策略。
接着我们讨论了后端实例有状态但是正确性不需要由负载均衡策略来保证的半状态负载均衡策略常用的半状态的负载均衡策略有Hash 和一致性 Hash 等,这里我们就知道了,怎么利用负载均衡策略的特点,优化后端服务的性能。
最后,我们讨论了全状态的负载均衡策略,其实全状态的负载均衡和数据分片是同一件事情,只是我们讨论的角度不一样而已,你会发现负载均衡和数据分片之间是有非常多的交集的。
思考题
我们利用 Hash 策略路由的 MySQL 集群,如果需要对集群进行扩容,我们怎么做才能在最少迁移数据的情况下,依然保证路由的正确性呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,147 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 配置中心:如何确保配置的强一致性呢?
你好,我是陈现麟。
通过学习“负载均衡”的内容,你知道了怎么评价一个负载均衡策略,以及针对不同的业务场景,应该怎么选择合适的负载均衡策略。现在,你已经能够顺利地解决分布式系统中,服务实例的选择问题,恭喜你又前进了一大步。
但是,随着极客时间分布式架构的逐渐演进,之前的单体服务慢慢被拆分为越来越多的服务,虽然拆分后的架构对公司研发的成本、效率和稳定性方面有着非常大的改进,可是你在系统运维的时候,特别是管理系统配置的时候,却发现效率越来越低了,并且还经常会出现因为配置问题导致的故障。
可能你很快就能想到这个问题产生的原因,因为在目前的分布式架构迭代过程中,极客时间的后端系统由之前单体架构的一个服务,被拆分成了多个服务,并且服务的数量还在继续增加。我们管理 1 个服务的配置是很轻松的,但是用管理 1 个服务配置的方法,来管理 10 个、20个甚至更多的服务配置效率一定是非常低的并且也避免不了出错。
虽然能想到原因,但是真正处理时,却不知道怎么做,你是不是也有这样的疑问呢?不要担心,在这节课中,我将和你一起来讨论在分布式系统中,我们应该怎么管理服务配置信息?
我们从分布式场景下,手动管理配置的问题出发,思考为什么需要配置中心,然后进一步讨论配置中心需要具备的功能,接着从存储系统的选择,配置信息的同步这两个方面,来结合业务场景实际讨论,解决如何实现配置中心的问题,最后再探讨一下,需要配置同时生效的场景下,如何确保配置信息的强一致性。
为什么需要配置中心
在思考配置问题之前,我们先讨论一下单体服务架构是怎么管理配置的,如果直接使用单体服务的方式来处理分布式系统的配置,将会出现什么样的问题,从而引出解决配置管理问题的高效方法——配置中心。
单体服务架构的场景下,一般是将配置信息视为代码的一部分,工程师会像编辑代码一样,编辑好配置,然后通过发布系统,将配置发布到服务程序所在的机器上,接下来,程序会通过加载本地存储上的配置文件,使配置生效。在单体架构下,这个配置即代码的方法能够很好地运行,但是在分布式架构下则会出现以下几个问题。
首先,这种方法缺乏整体的配置管理平台,会使配置管理的效率变得很低。单体服务的架构只有一个服务,不需要用全局视角来管理配置,但是在分布式系统中,如果将配置信息视为代码的一部分,会导致不同服务的配置文件,出现在不同的代码仓库中。当我们需要检索和查看多个服务的配置时,需要在一个个代码仓库中查找,效率会非常低。
其次,这种方法会导致实例之间的配置出现不一致的情况。其实在单体架构下,也会有这个问题,不过整个单体系统只有一个服务,通过人工来保证实例之间配置一致是比较简单的。但是在分布式系统中,随着服务的增加,想要靠人工来保障是不可能的。
因为配置是随着程序一起发布的,每一个实例都会加载本地机器上存储的配置信息,如果配置文件有人为修改或其他故障时,会因为多实例之间的配置信息不相同,出现实例之间的行为不一致性的情况,进而出现各种奇怪的问题。
最后,配置即代码的方法会使配置修改的操作,变得非常冗余和低效,这个问题同样存在于单体架构中。由于每一次的配置修改,都需要走一次完整的代码发布流程,所以工程师都需要从服务的代码仓库中找到配置文件,在对配置文件进行修改后,提交修改到代码仓库,然后通过发布系统进行发布,最后程序会通过重启或者热更新的方式加载配置。其中,只有修改配置文件和发布配置文件这两个操作是必须的,其他的流程都和配置修改无关。
结合上面的分析你会发现,配置即代码的配置管理方式有非常多的问题,那么我们能不能直接手动管理配置呢?其实从操作上来说是可以的,比如你登录到每一台机器上手动修改,然后再让程序加载,重新配置文件即可。
但是这样一来,每一次服务的配置修改,都需要修改该服务的所有实例的配置,效率又低又容易出错。如果你的操作稍微有一点失误,就会导致同一个服务中,多个实例的配置信息直接不一致了。而且这样的操作,还会导致配置文件的修改没有历史记录,如果出现了当前配置文件错误的问题,需要回滚到上一个版本的时候,就麻烦了。
那么,到底怎么能更高效、更准确地解决分布式系统的配置管理问题呢?一般来说,在分布式系统中,如果一个问题的影响半径超出单一服务的范围,就可以考虑通过引入一个中间层的方法来解决,即“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”这个经典论断。它会经常出现在我们的课程中,帮助你培养解决问题的高效思路。
所以,在解决分布式系统的配置管理问题时,我们也来引入一个中间层,把这个中间层称之为配置中心。引入配置中心这个高效的解决方法之后,我们可以进一步地讨论一个理想的配置中心应该是什么样子的,这样你就知道在建设配置中心时,我们需要注意什么样的关键点。
配置中心需要具备哪些功能
在解决问题之前,应该先定义好问题。所以我们在讨论配置中心的具体实现之前,先来定义一下,什么是配置中心,具体来说就是配置中心应该要具备哪些功能?
我们可以结合上文中,配置即代码的方法在分布式系统中面临的三个问题,推导出在分布式系统的架构下,一个理想的配置中心应该具备哪些特点。
首先,这个配置中心,能够统一管理分布式系统所有服务的配置信息。那么研发工程师就可以在配置中心上,便捷地全局搜索和查看每一个服务的配置信息,而不是看到所有服务的配置信息都散落在不同的地方。更进一步来说,配置中心需要能统一存储和管理整个分布式系统的所有配置文件。
其次,配置中心里,同一个服务实例之间的配置应该保持一致。也就是说,配置中心需要保证一个服务所有的实例,都加载同一份配置文件,而不是每一个实例维护一个配置文件的副本。这就需要配置中心统一去管理,服务当前版本的配置,并且服务的实例通过网络去配置中心,获得当前的配置信息,确保 Single Source of Truth ( SSOT )。
最后,这个配置中心应该能高效地修改配置。研发工程师只需要关心,并且高效地完成配置的修改、发布和回滚操作,而其他的就不需要研发工程师手动来操作了,比如配置文件的版本管理等,这些都由配置中心来自动完成。
经过前面的讨论,我们结合这节课开头提到的配置中心的业务场景,可以总结出配置中心需要解决的两个关键问题:
统一的配置存储:一个带版本管理的存储系统,按服务的维度,存储和管理整个分布式系统的配置信息,这样可以很方便地对服务的配置信息,进行搜索、查询和修改。
配置信息的同步:所有的实例,本地都不存储配置信息,实例能够从配置中心获得服务的配置信息,在配置修改后,能够及时将最新的配置,同步给服务的每一个实例。
那么到这里,你会发现配置中心和服务的注册发现机制是非常类似的,唯一不同的地方是服务注册发现所存储的服务实例的 IP 和 Port 等信息,是服务实例自己注册的,并且会设置过期时间,随着实例上线时主动写入,下线后会因为过期而被删除。但是配置中心的配置信息是研发工程师主动写入的,并且不会设置过期时间。
如何实现配置中心
我们确定了“统一的配置存储”和“配置的更新与同步”这两个关键问题,并且还发现了配置中心与服务的注册发现机制之间的相似性,掌握了这些信息,我们接下来就可以思考,如何实现配置中心的解决方法了。
关于如何实现配置中心,我们首先结合“统一的配置存储”这个关键点来分析,可以从“如何选择合适的存储系统”的角度来思考解决方法;然后再从“如何做配置信息的同步”的角度,讨论“配置的更新与同步”这个关键点。
如何选择合适的存储系统
与服务注册发现类似,实现配置中心也需要找一个外部存储,来做配置中心的统一存储。通过对配置中心的场景分析,我认为配置中心对存储系统的要求主要为以下几点:
可用性要求非常高:因为配置中心和服务注册发现一样,是整个分布式系统的基石,如果配置中心出现问题,整个分布式系统都将出现非常严重的问题。
性能要求中等:只要设计得当,整体的性能要求还是可控的,不过需要注意的是,性能要求会随分布式系统的实例数量变多而提高。
数据容量要求低:配置中心是用来存储服务的配置信息的,一般来说,服务的配置信息都非常小,如果出现非常大的配置,一般也不当成配置来处理,可以将它放到外部存储上,在配置中配置下载的链接。
API 友好程度:是否能很好地支持配置中心场景的“发布/订阅”模式,将服务的配置信息及时同步给服务的实例。
基于上面对所需求存储系统特点的分析,我们一起来对常见的存储系统做一个系统性的比较,由于注册发现和配置中心类似,所以我们使用第 4 节课“注册发现”中的这张图片,从配置中心的角度进一步分析:
-
通过上面的分析我们可以看到MySQL 和 Redis 在高可用性和 API 友好程度上不满足要求,而 etcd、ZooKeeper 和 Eureka 这三个存储系统中,更适合的是 Eureka。下面我们来讨论一下为什么 Eureka 这样的 AP 系统要比 etcd 和 ZooKeeper 这样的 CP 系统更合适。
如果我们选择 etcd 和 ZooKeeper那么出现网络分区的时候在网络分区的少数派节点一侧配置中心将不能提供服务这一侧的服务实例也就不能通过配置中心获取配置这时如果有实例的重启等操作就一定会发生故障。
如果选择 Eureka那么配置中心这个整体依然可以正常提供服务唯一的问题是如果这时有配置的更新那么同一个服务中不同实例的配置可能会不一致但是这个问题并不是最关键的主要原因有两个。
首先,即使配置中心内部是强一致性的,但是配置中心和服务实例之间是通过网络同步配置的,而网络的时延是不确定的,这会导致配置信息同步到实例的时间有先有后,不能同时到达,使得配置中心和同一服务多实例之间的配置,同步退化到最终一致性。
其次,配置修改的频率是非常低的,而且因为是人工操作,所以在出现网络分区的时候,如果我们不去修改配置,那么 Eureka 上多个副本的数据就是一致的。
如何做配置信息的同步
讨论完“如何选择合适的存储系统”,我们接着讨论配置中心的另一个关键点“如何做配置信息的同步”,对于这个问题,我们可以将其分解为两个问题解决,具体操作如下图:-
-
首先,实例刚启动的时候,主动去配置中心获取完整的配置信息,即首次同步:如上图中的 1服务的每一个实例启动后通过服务的唯一标识去配置中心获取服务的所有配置然后加载配置完成实例的启动流程。
然后,在实例的运行过程中,如果服务的配置有修改,配置中心需要及时同步到实例,即变更同步:如上图中的 2 和 3服务的配置信息有变更后配置中心监听到服务的配置修改了需要及时通知到服务的所有实例。这里可以采用“发布/订阅”的模式,也可以采用轮询模式,比如每 30 秒去配置中心查询一下,配置是否有变更。这里的数据同步是最终一致性的。
如何确保配置的强一致性
通过上面的讨论,我们知道了怎么来实现一个配置中心,并且知道了配置中心和服务实例之间的配置同步是最终一致性的。这时候你可能会有一个疑问,有没有一些业务场景必须要求,同一服务的多个实例之间的配置信息同时生效呢?如果有的话,应该怎么来保证呢?所以,我们最后来讨论一下,在需要配置同时生效的场景下,如何确保配置信息的强一致性。
确实有这样的场景,我们通过一个例子来分析一下。因为这部分只讨论配置强一致性的问题,所以这个数据迁移的例子,不会涉及整个数据迁移的完整流程。假设有一个分布式存储系统,如下图所示,我们现在需要通过配置信息,发送数据迁移指令,将数据集 2 从存储节点 1 迁移到节点 2 上。
在这个例子中,如果 Proxy 实例之间,对数据迁移的配置信息没有同时生效,将会导致什么样的异常情况呢?
从上图可以看出在进行数据迁移前Proxy 对数据集 2 的读写请求,都会路由到存储系统 1 上。我们通过配置中心,配置好数据迁移的配置后,如果 Proxy 1 已经加载了数据迁移的配置Proxy 2 还没有接收到数据迁移的配置,那么在处理数据集 2 的请求时,就会出现 Proxy 1 读写存储节点 2Proxy 2 读写存储节点 1 的情况,导致数据不一致的问题,反过来也是一样的。
那么我们应该怎么来解决这个问题呢?其实这是一个共识问题,需要所有的 Proxy 实例对数据迁移的配置达成共识后,才能进行迁移。而配置中心和多实例的配置同步,是通过网络来完成的,不是一个强一致性的模型,所以,我们不能简单依赖配置中心的配置同步来解决。
我们可以使用这样的解决思路,配置信息不能按上面讨论的方式直接通过网络进行同步,而需要通过类似两阶段提交的方式来解决这个问题。这里我们主要讨论处理这个问题的思路,不展开故障处理的情况,有了这个思路,后面你就可以处理多节点数据一致性和共识相关的问题了。
首先,从配置中心的存储节点中选择一个实例作为协调者 A。
在投票阶段,协调者 A 向所有的 Proxy 节点发送 Prepare 消息即数据迁移的配置信息Proxy 节点在收到数据迁移配置后,确认自己当前的状态是否可以执行数据迁移工作。如果可以,那么就阻塞当前节点所有的读写操作,进入 Prepare 状态,并回复协调者 A 同意执行数据迁移,否则回复不同意执行数据迁移。
那么这里要注意一点为了数据的一致性我们放弃了一定的可用性Prepare 状态下的 Proxy 节点相当于被锁住,不能进行读写操作了。
在执行阶段,协调者 A 收集所有的 Proxy 节点的反馈,如果所有的 Proxy 都同意执行数据迁移,那么协调者 A 向所有的 Proxy 节点发送 Commit 消息Proxy 节点收到 Commit 消息后,就应用数据迁移的配置信息,按最新的配置信息,接受读写请求,进行数据迁移。上文的例子,就是对于数据集 2 的读写请求,都路由到节点 2 来处理,否则就发送 Rollback 消息Proxy 节点收到后,回滚状态,重新接受读写请求。-
总结
到这里,我们一起完整地讨论了分布式系统中,一个非常关键的组件“配置中心”,我们一起来总结一下这节课的主要内容。
首先,我们一起讨论了为什么需要配置中心,主要有统一配置管理、同一个服务实例之间的配置一致性和配置修改效率这三个方面的原因。
然后,我们分析了一个理想的配置中心,应该具备什么功能,从中总结出配置中心的两个关键点:统一的配置存储和配置信息的同步。
接着,讨论了对于配置中心的业务场景来说,选择一个 AP 模型的存储系统是最优的方案,并且知道了应该如何做配置信息的同步。
最后,我们通过配置信息需要强一致性的例子,介绍了一个类似两阶段提交的方式,来实现强一致性的配置发布。
思考题
结合“如何处理强一致性的配置”的处理流程中的第二点为了数据的一致性放弃了可用性Prepare 状态的 Proxy 节点相当于被锁住,不能进行读写操作。
请你思考一下,如果允许 Prepare 状态的 Proxy 节点读,会出现什么问题?如果允许 Prepare 状态的 Proxy 节点写,又会出现什么问题?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,129 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 分布式锁:所有的分布式锁都是错误的?
你好,我是陈现麟。
通过学习“配置中心”的内容,你已经理解了在分布式系统中,为什么需要配置中心,以及怎么去实现一个设计良好的配置中心,现在,你终于不用再为管理极客时间后端各种服务的配置而烦恼了,这是一件值得高兴的事情。
但是,在极客时间后端系统快速迭代的过程中,你发现了一个服务中的代码逻辑问题:在有些场景下,你并不想让所有的实例都一起运行,只需要一个实例运行就够了,比如在用户生日当天,给用户发的祝福短信等类似定时通知的情况。
目前同一个服务的所有实例都是对等的,只能每一个实例都运行。如果将这个服务运行的实例修改为一个,虽然能解决刚才讨论的问题,但是这个实例就变成了一个单点,会面临性能瓶颈和单点故障的风险。
这真是一个两难的问题,我们应该如何解决呢?其实,这个问题的本质在于,我们希望同一个服务的多个实例,按照一定的逻辑来进行协同,比如刚才讨论的定时任务的逻辑。那么多个实例在同一时刻只能有一个实例运行,它就是一个典型的分布式锁的场景。
所以,在本节课中,我们将从“为什么需要分布式锁”,“怎么实现分布式锁”和“分布式锁的挑战”这三个层次依次递进,和你一起来讨论分布式锁相关的内容,解决你的困惑。
为什么需要分布式锁
在探讨分布式锁之前,我们先来了解一下锁的定义:锁是操作系统的基于原语,它是用于并发控制的,能够确保在多 CPU 、多个线程的环境中,某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。
在我们日常的研发工作中,经常会在进程内部缓存一些状态信息,通过锁可以很方便地控制、修改这些内部状态信息的临界区代码,确保不会出现多个线程同时修改临界区的资源这种情况,防止异常问题的发生。所以,锁是我们研发工作中一个非常重要的工具。
其实,我们将锁的定义推广到分布式系统的场景中,也是依然成立的。只不过锁控制的对象从一个进程内部的多个线程,变成了分布式场景下的多个进程,同时,临界区的资源也从进程内多个线程共享的资源,变成了分布式系统内部共享的中心存储上的资源。但是,锁的定义在本质上没有任何的改变,只有持有锁的线程或进程才能执行临界区的代码。
这句话如何理解呢?我们来看看这个例子。在进程内部,多个线程同时修改一个变量,可能会出现多个线程每个都写一部分,导致变量写入冲突的情况发生。那么在分布式系统中,如果多个进程,同时往一个中心存储的同一个位置写入一个文件,同样也会出现文件写入冲突的情况。所以,锁的定义在本质上没有任何的改变。
另外,我们从课程开头提到的定时任务代码的例子里,可以知道在同一时间内,临界区只能由一个进程来执行,而只有持有锁的线程或进程才能执行临界区的代码。
所以我们可以这样理解,分布式锁是一个跨进程的锁,是一个更高维度的锁。我们在进程内部碰到的临界区问题,在分布式系统中依然存在,我们需要通过分布式锁,来解决分布式系统中的多进程的临界区问题。
怎么实现分布式锁
我认为锁可以分为三个不同的层次,除了我们上面讨论过的,进程内部的锁和跨进程、跨机器之间的分布式锁外,还有介于它们之间的,同一台机器上的多进程之间的锁。
进程内的锁,是操作系统直接提供的,它本质上是内存中的一个整数,用不同的数值表示不同的状态,比如用 0 表示空闲状态。加锁时,判断锁是否空闲,如果空闲,修改为加锁态 1并且返回成功如果已经是加锁状态则返回失败而解锁时则将锁状态修改为空闲状态 0。整个加锁或者解锁的过程操作系统保证它的原子性。
对于同一台机器上的多进程之间,我们可以直接通过操作系统的锁来实现,只不过由于协调的是多个进程,需要将锁存放在所有进程都可以访问的共享内存中,所有进程通过共享内存中的锁来进行加锁和解锁。
到这里,你应该明白了,对于跨进程、跨机器之间的分布式锁的实现也是同样的思路,通过一个状态来表示加锁和解锁,只不过要让所有需要锁的服务,都能访问到状态存放的位置。在分布式系统中,一个非常自然的方案就是,将锁的状态信息存放在一个存储服务,即锁服务中,其他的服务再通过网络去访问锁服务来修改状态信息,最后进行加锁和解锁。
上面讨论的就是分布式锁最核心的原理,不过从分布式锁的场景出发,如果我们想实现一把完备的分布式锁,需要满足以下几个特性,接下来我们就一起来讨论具体怎么实现。
第一个特性就是互斥,即保证不同节点、不同线程的互斥访问,这部分知识我们在上面已经讨论过,就不再赘述了。
第二个特性是超时机制,即超时设置,防止死锁,分布式锁才有这个特性。在概述篇的第二节课“新的挑战”中,我们讨论过部分失败和异步网络的问题,而这个问题在分布式锁的场景下就会出现。因为锁服务和请求锁的服务分散在不同的机器上面,它们之间是通过网络来通信的,所以我们需要用超时机制,来避免获得锁的节点故障或者网络异常,导致它持有的锁不能归还,出现死锁的情况。
同时,我们还要考虑,持有锁的节点需要处理的临界区代码非常耗时这种问题,我们可以通过另一个线程或者协程不断延长超时时间,避免出现锁操作还没有处理完,锁就被释放,之后其他的节点再获得锁,导致锁的互斥失败这种情况。
对于超时机制,我们可以在每一次成功获得锁的时候,为锁设置一个超时时间,获得锁的节点与锁服务保持心跳,锁服务每一次收到心跳,就延长锁的超时时间,这样就可以解决上面的两个问题了。
第三个特性是完备的锁接口,即阻塞接口 Lock 和非阻塞接口 tryLock。通过阻塞 Lock 接口获取锁,如果当前锁已经被其他节点获得了,锁服务将获取锁的请求挂起,直到获得锁为止,才响应获取锁的请求;通过 tryLock 接口获取锁,如果当前锁已经被其他节点获得了,锁服务直接返回失败,不会挂起当前锁的请求。
第四个特性是可重入性,即一个节点的一个线程已经获取了锁,那么该节点持有锁的这个线程可以再次成功获取锁。我们只需在锁服务处理加锁请求的时候,记录好当前获取锁的节点 + 线程组合的唯一标识,然后在后续的加锁请求时,如果当前请求的节点 + 线程的唯一标识和当前持有锁的相同,那么就直接返回加锁成功,如果不相同,则按正常加锁流程处理。
最后是公平性,即对于 Lock 接口获取锁失败被阻塞等待的加锁请求,在锁被释放后,如果按先来后到的顺序,将锁颁发给等待时间最长的一个加锁请求,那么就是公平锁,否则就是非公平锁。锁的公平性的实现也非常简单,对于被阻塞的加锁请求,我们只要先记录好它们的顺序,在锁被释放后,按顺序颁发就可以了。
分布式锁的挑战
通过上面的学习,你已经学会了分布式锁的基本原理,不过在分布式系统中,由于部分失败和异步网络的问题,分布式锁会面临正确性、高可用和高性能这三点的权衡问题的挑战。所以,我们接下来讨论一下分布式锁的挑战问题,这样你在以后的工作中,就可以依据业务场景来实现合适的分布式锁了。
分布式锁的正确性
首先,我们一起来讨论分布式锁的正确性问题。我们在使用分布式锁的情况下,是否有办法做到,不论出现怎样的异常情况,都能保证分布式锁互斥语义的正确性呢?
那么这里,我们将从进程内的锁如何保证互斥语义的正确性出发,分析在分布式锁的场景中,部分失败和异步网络同时存在的情况下,是否能确保分布式锁互斥语义正确性的问题。
对于进程内的锁,如果一个线程持有锁,只要它不释放,就只有它能操作临界区的资源。同时,因为进程内锁的场景中,不会出现部分失败的情况,所以在它崩溃时,虽然没有去做解锁操作,但是整个进程都会崩溃,不会出现死锁的情况。
这里要说明一下,我们讨论出现死锁的情况,不包括业务逻辑层面出现死锁,因为这个与锁本身的正确性没有关系。我们讨论的是与业务逻辑无关的原因,导致的死锁问题,这个是锁自身的问题,需要锁自己来解决。
另一个方面,进程内锁的解锁操作是进程内部的函数调用,这个过程是同步的。不论是硬件或者其他方面的原因,只要发起解锁操作就一定会成功,如果出现失败的情况,整个进程或者机器都会挂掉。所以,因为整体失败和同步通信这两点,我们可以保证进程内的锁有绝对的正确性。
接下来,我们再来用同样的思路,讨论一下同一台机器上多进程锁的正确性问题。在这个情况下,由于锁是存放在多进程的共享内存中,所以进程和锁之间的通信,依然是同步的函数调用,不会出现解锁后信息丢失,导致死锁的情况。
但是,因为是多个进程来使用锁,所以会出现一个进程获取锁后崩溃,导致死锁的情况,这个就是部分失败导致的。
不过,在单机情况下,我们可以非常方便地通过操作系统提供的机制,来正确判断一个进程是否存活,比如,父进程在获得进程挂掉的信号后,可以去查看当前挂掉的进程是否持有锁,如果持有就进行释放,这可以当作是进程崩溃后清理工作的一部分。
讨论完进程内的锁和同一台机器上多进程锁的正确性问题后,我们还需要考虑到,在分布式锁的场景中,部分失败和异步网络这两个问题是同时存在的。如果一个进程获得了锁,但是这个进程与锁服务之间的网络出现了问题,导致无法通信,那么这个情况下,如果锁服务让它一直持有锁,就会导致死锁的发生。
一般在这种情况下,锁服务在进程加锁成功后,会设置一个超时时间,如果进程持有锁超时后,将锁再颁发给其他的进程,就会导致一把锁被两个进程持有的情况出现,使锁的互斥语义被破坏。那么出现这个问题的根本原因是超时后,锁的服务自动释放锁的操作,它是建立在这样一个假设之上的:
锁的超时时间 >> 获取锁的时延 + 执行临界区代码的时间 + 各种进程的暂停(比如 GC
对于这个假设,我们暂且认为“执行临界区代码的时间 + 各种进程的暂停”是非常小的,而“获取锁的时延”在一个异步网络环境中是不确定的,它的时间从非常小,到很大,再到因为网络隔离变得无穷大都是有可能的,所以这个假设不成立。
如果你计划让客户端在“获取锁的时延”上加心跳和超时机制,这是一个聪明的想法,但是这可能会导致锁服务给客户端颁发了锁,但是因为响应超时,客户端以为自己没有获取锁的情况发生。这样一来,依然会在一定程度上,影响锁的互斥语义的正确性,并且会在某些场景下,影响系统的可用性。
对于这些问题,如果我们获得锁是为了写一个共享存储,那么有一种方案可以解决上面的问题,那就是在获得锁的时候,锁服务生成一个全局递增的版本号,在写数据的时候,需要带上版本号。共享存储在写入数据的时候,会检查版本号,如果版本号回退了,就说明当前锁的互斥语义出现了问题,那么就拒绝当前请求的写入,如果版本号相同或者增加了,就写入数据和当前操作的版本号。
但是这个方案其实只是将问题转移了如果一个存储系统能通过版本号来检测写入冲突那么它已经支持多版本并发控制MVCC这本身是乐观锁的实现原理。那么我们相当于是用共享存储自身的乐观锁来解决分布式锁在异常情况下互斥语义失败的问题这就和我们设计分布式锁的初衷背道而驰了。
所以,我认为对于在共享存储中写入数据等等,完全不能容忍分布式锁互斥语义失败的情况,不应该借助分布式锁从外部来实现,而是应该在共享存储内部来解决。比如,在数据库的实现中,隔离性就是专门来解决这个问题的。分布式锁的设计,应该多关注高可用与性能,以及怎么提高正确性,而不是追求绝对的正确性。
分布式锁的权衡
接下来,我们一起来讨论关于分布式锁的高可用、高性能与正确性之间的权衡问题。
关于正确性的问题,我们从上面的讨论中,明白了在分布式锁的场景下,没有办法保证 100% 的正确性,所以,我们要避免通过外部分布式锁,来保证需要 100% 正确性的场景,将分布式锁定位为,可以容忍非常小概率互斥语义失效场景下的锁服务。一般来说,一个分布式锁服务,它的正确性要求越高,性能可能就会越低。
对于高可用的问题,我认为它是在设计分布式锁时,需要考虑的关键因素。我们必须提供非常高的 SLA ,因为分布式锁是一个非常底层的服务组件,是整个分布式系统的基石之一,所以一般来说,越底层、越基础的组件,依赖它的功能和服务就会越多,那么它的影响面就会越大。如果它出现了故障,必然会导致整个分布式系统大面积出现故障。
对于高性能的问题,这是一个由业务场景来决定的因素,我们需要通过业务场景,来决定提供什么性能的分布式锁服务。一般来说,我们可以在成本可接受的范围内,提供性能最好的分布式锁服务。如果我们提供的分布式锁服务的性能不佳,一定要在文档甚至接口的名字中体现出来,否则如果被误用的话,可能会导致分布式锁服务故障,系统将出现非常大的事故。
基于以上三点权衡,我们就可以根据业务情况,来实现或者选择自己的分布式锁服务了。其中关于分布式锁服务的存储的选择问题,因为对于主流存储系统的选择与对比,已经在第 4 讲“注册发现”和第 6 讲“配置中心”中讨论过,所以这里就不再赘述了。
总结
到这里,我们一起讨论了分布式系统场景下的分布式锁的相关问题,接下来我们一起来总结一下这节课的主要内容:
首先,我们讨论了单进程内和单节点内进程的临界区问题,并且这个问题在分布式系统中依然存在,那么对于分布式场景下的临界区问题,我们需要用分布式锁服务来解决。
其次,我们一起讨论了,怎么实现分布式锁服务的互斥、超时机制、完备的锁接口、可重入和公平性等特性,基于这些知识和原理,我们就可以很轻松地实现自己的分布式锁服务了。
最后,我们一起探讨了在分布式场景下的正确性问题,发现分布式场景下,锁服务没有办法保证 100% 的正确性,并且,我们认为可用性是设计分布式锁服务非常关键的一个目标。这样,我们就可以依据不同的业务场景,来设计和权衡我们的分布式锁服务了。
思考题
根据本节课讨论的情况,在实现分布式锁服务的时候,你认为应该以什么样的原则来选择我们的存储系统呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 重试幂等:让程序 Exactly-once 很难吗?
你好,我是陈现麟。
通过学习“分布式锁”的内容,你已经了解了如何实现一个分布式锁服务,并且知道了在分布式锁的场景下,我们应该如何在正确性、高可用和高性能之间做取舍。那么对于分布式场景下,实例或服务之间的协调问题,我们就心中有数了,你可以根据业务场景,做出最合适的选择,我们又一起往前走了一大步。
但是,在极客时间的开发过程中,你又面临了一个新的问题。在通过 RPC 远程调用极客时间的课程购买接口的过程中,你可能是这样处理 RPC 的响应结果的,先是将“请求超时”的响应结果解释为“课程购买失败”,返回给用户,可是这会影响到用户的正常购买,导致一部分用户放弃。
后来为了尽可能让用户购买成功你对“请求超时”响应的请求进行了重试发现用户的购买成功率确实提高了但是却有少量的用户反馈说他只点击了1 次购买,页面却出现了 2 笔支付成功的订单。
这确实是一个两难的问题,要么让一部分用户放弃购买,要么让少量的用户重复购买,难道没有一个好的办法吗?这里我们可以先来分析一下这个问题的根本原因,在请求的响应结果为“请求超时”的时候,我们不知道这个请求是否已经被远端的服务执行了,进一步来说就是请求的消息,是否精确一次发送到远端服务的问题,即 Exactly-once。
所以在这节课中,我们将从“为什么不能保证 Exactly-once”、“如何保证 Exactly-once ”和“Exactly-once 的挑战”这三个方面,一起来讨论如何让程序 Exactly-once。
为什么不能保证 Exactly-once
在单机系统中,模块之间的通信都是进程内的本地函数调用,在这个整体失败和同步通信的模型中,要么进程整体崩溃,要么调用完成,不会存在其他的情况,但是在分布式系统中,程序不能保证 Exactly-once 的原因主要有以下两个:
第一个是网络方面的原因。在分布式系统中,服务和服务之间都是通过网络来进行通信的,而这个网络是一个异步网络。在这个网络中,经过中间的路由器等网络设备的时候,会出现排队等待或者因为缓冲区溢出,导致消息被丢弃的情况,那么将一个消息从一个节点发送到另一个节点的时延是没有上界的,有可能非常快,比如 1 ms也有可能是 1 分钟,甚至无穷大,这个时候就是出现消息丢失的情况了。
在服务间进行远程调用的时候,如果迟迟没有收到响应结果,为了系统整体的可用性,我们不能无限等待下去,只能通过超时机制来快速获得一个结果。其实这样做是将无界时延的异步网络模型,通过超时机制转化成了有界时延,这个方式大大减轻了我们在写程序时的心智负担。
但是,计算机的世界里没有银弹,我们在收到响应为“请求超时”的时候,无法判断是请求发送的过程中延迟了,远端服务没有收到请求;还是远端服务收到请求并且正确处理了,却在响应发送的过程中延迟了。
第二个原因是远端服务发生了故障。如果远端服务在收到请求之前发生了故障,我们会收到“网络地址不可达”的错误,对于这个错误,我们能明确判断请求没有被远端服务执行过。但是,如果远端服务是在收到请求之后发生了故障,导致无法响应而引起“请求超时”,我们无法判断请求是否被远端服务执行过,或者被部分执行过。
通过上文提到的两个原因,我们可以知道,当请求方收到“请求超时”的时候,我们无法判断远端服务是否处理过这个请求。这个时候就出现了本课开头的问题:如果我们认为这是一个临时的故障,对请求进行重试,那么可能会出现多次执行的情况,即 At-least-once如果不进行重试就可能会出现一次都没有执行的情况即 At-most-once。
关于这个问题,在之前的课程“新的挑战”中“本地调用与远程调用”这部分,也有过深入的讨论,你可以参照着一起来看。
如何保证 Exactly-once
通过上面的分析,我们知道了导致消息传递,不能保证 Exactly-once 的原因主要有两个,一个是网络出现丢包或者分区等故障,另一个是远端服务发生了故障。因为这两点在分布式系统中是永远存在的,所以我们必须去直面这两个问题,通过上层的容错机制来解决它们。
一般来说,在分布式系统中,实现消息的 Exactly-once 传递,主要有三种方式:一种是至少一次消息传递加消息幂等性,一种是分布式快照加状态回滚,还有一种是整体重做,下面我们来一一介绍。
至少一次消息传递加消息幂等性
至少一次消息传递加消息幂等性的思路特别简单,我们可以结合本课开始提到的场景来分析,如果调用方在课程购买的 RPC 接口返回网络层错误,比如请求超时以及网络地址不可达等,对于这样的情况,调用方就进行重试,直到响应结果为成功或业务错误等非网络层错误。
当然,这里的请求超时也有可能是远端服务的执行时间太长导致的,为了简化讨论中的语言描述,后面我们统一归类为网络错误。
但是,我们同样要考虑到,重试会让用户对当前的课程重复购买,对于这个情况,我们可以在远端服务对课程购买接口的实现上,对请求进行去重,确保远端服务对同一个购买请求处理一次和多次的结果是完全相同的,对于这样的接口,我们称之为幂等的。
其实这个去重的思路也非常简单,你可以结合下图理解。我们只需要对用户发起的每一次课程购买的请求,生成一个唯一的 ID ,然后在课程购买的 RPC 请求中带上这个唯一的 ID ,在首次调用和重试的时候,这个唯一的 ID 都保持不变。
接着,课程购买服务在接收到请求后,先查询当前的 ID 是否已经处理过,如果是已经处理过的请求,就直接返回结果,不重复执行购买相关的逻辑了。-
分布式快照加状态回滚
分布式快照加状态回滚指的是,在整个分布式系统运行的过程中,定期对整个系统的状态做快照,在系统运行时,不论系统的哪个地方出现故障,就将整个系统回滚到上一个快照状态,然后再重放上一个快照状态之后的情况,直到所有的消息都被正常处理,你可以结合下图理解具体操作:
-
可是很明显,分布式快照加状态回滚的方式并不适合在线业务的情况。首先,要对在线业务的所有状态做快照是非常难的一件事情,因为在线业务的状态一般都是在数据库中,如果要对整个系统的数据库都定期做快照,这将消耗非常大的资源。
其次,在通过快照进行状态回滚的时候,整个系统不能处理当前的业务请求,当前的业务请求需要进行排队等待,等系统通过快照将状态回滚完,并且重放了上一个快照状态之后的所有请求,才能开始正常处理当前业务。这个过程可能很长,这对于在线业务系统是不能接受的。
最后,如果出现任何一个小的问题或者故障,就要对整个分布式系统进行状态回滚,这也是不能接受的。
所以,分布式快照加状态回滚的方式,一般不会应用于在线业务架构中,它的主要应用场景是例如 Flink 之类的流式计算。因为在流式计算中,系统状态的存储也是系统设计的一部分,我们可以在系统设计的时候,就考虑支持快照和回滚功能。并且,在流式计算中,消息来源一般都是 Kafka 之类的消息系统,这样对消息进行重放就非常方便了。
整体重做
整体重做的 Exactly-once 的方式,可以看成是分布式快照加状态回滚的一种特殊情况。在执行任务的过程中,如果系统出现故障,就将整个任务的状态删除,然后再进行重做。整体重做的方案,一般的使用场景为批处理任务的情况,比如 MapReduce 之类的批处理计算引擎。
Exactly-once 的挑战
因为这个专栏主要讨论的是在线业务架构的分布式系统,所以接下来,我们只讨论分布式在线业务架构系统中,对于解决 Exactly-once 问题,常用的“最少一次消息传递加消息幂等性机制”面临的挑战。
重试面临的挑战
通过“最少一次消息传递加消息幂等性机制”来确保消息的 Exactly-once我们首先要采用重试策略来确保消息最少传递一次但是在执行重试策略的过程中我们要避免重试导致的系统雪崩的问题。
在系统快要接近性能瓶颈的时候,某些节点可能会因为负载过高而响应超时,如果这个时候再无限制地重试,就会进一步放大系统的请求量,将一个局部节点的性能问题,放大到整个系统,造成雪崩效应。
一般情况下,重试策略都会有两个限制,第一个是限制重试的次数,比如,如果重试 3 次都失败了,就直接返回请求失败,不再继续重试;第二个是控制重试的间隔,一般采取指数退避的策略,比如重试 3 次,第一次请求失败后,等待 1 秒再进行重试,如果再次失败,就等待 3 秒再进行重试,仍然失败的话,就等待 9 秒后再进行重试。
幂等面临的挑战
对于请求的幂等问题,首先,我们要讨论能否通过对操作进行改写,将一个非幂等操作变成一个幂等操作,然后,我们再讨论如何将一个非幂等操作变成一个幂等操作,最后,我们讨论在有外部系统的情况下,如何保证请求的幂等性。
操作的幂等性讨论
对于请求的幂等处理,如果请求本身就是幂等的,比如请求只是查询数据,没有任何的状态修改,或者是像更新头像这样简单的重置操作,那么我们可以什么都不用做。这里我们要注意一个情况,假设有一个请求是为用户的余额增加 5 元,如果采用下面的 SQL 进行处理,我们都知道它不是幂等的:
UPDATE table SET balance = balance + 5 WHERE UID = 用户ID
但是,如果我们将上面的 SQL 改写为下面的三个操作,你可以思考一下,这个时候我们的请求是否为幂等的呢?
在数据库中查询用户的余额SELECT balance FROM table WHERE UID = 用户 ID
在内存中计算用户的余额balance = balance + 5 ,假设计算结果为 10 。
更新用户的余额到数据库UPDATE table SET balance = 10 WHERE UID = 用户 ID
在上面的操作中,虽然对数据库的两个操作都是幂等的,但是整体的操作却不是幂等的,因为第 2 步的操作不是幂等的,上面的改写只是将这个计算操作,从数据库中迁移到内存中,并不会改变这个请求的幂等性。
如何确保操作的幂等性
如果是一个非幂等操作的请求,我们如何将其变成一个幂等的请求呢?一个常用的方法就是我们在上面课程购买的例子中介绍的,在请求中增加唯一 ID ,然后在处理请求时,通过 ID 进行去重,确保对相同 ID 的请求只处理一次。
这里要特别注意的是,将请求处理结果写入数据库的操作,以及标记请求已处理的操作,也就是将请求唯一的 ID 写入数据库,它们都必须在同一个事务中,让事务来保证这两个操作的原子性。
否则,如果在写入处理结果后,请求唯一的 ID 写入数据库之前,服务发生崩溃的话,重试的时候就会使请求被执行多次;如果在请求唯一的 ID 写入数据库后,写入处理结果之前,服务发生崩溃,那么后面的重试请求都将因为去重而丢弃,导致请求一次都没有执行。
外部系统的幂等性保障
另外,还有一种情况,如果我们请求的操作会影响外部系统的状态,比如在一个请求中,我们需要给用户发送一条 IM 消息,因为发送 IM 消息是由外部的 IM 服务来提供的,我们可以通过下面两种方案,来保证请求操作整体的幂等性:
第一个方案,由 IM 服务提供幂等的消息发送接口。在这种情况下,我们采用全局唯一的 ID 作为请求的 ID这样当前请求在调用 IM 消息发送接口时,我们只需要传入当前请求的唯一 ID 作为消息发送的 ID 即可,由 IM 服务内部根据消息发送 ID 来进行去重操作,确保 IM 消息发送的幂等性。
第二个方案IM 服务提供 2PC 的消息发送接口,然后我们在当前请求的内部通过 2PC 的机制,确保该请求的内部状态修改逻辑, IM 消息的发送和请求唯一的 ID 写入数据库,这三个操作整体是一个原子操作。
到这里可以看出,如果我们请求的操作会影响到外部系统的状态,要保证请求的幂等性是需要依赖外部系统的支持才能实现的。
总结
本节课,我们一起讨论了分布式系统场景下的重试和幂等的相关问题,接下来一起来总结一下这节课的主要内容:
首先,我们讨论了在分布式场景下,由于不可靠的网络和随时都有可能出现的故障,导致在单体服务上非常容易保证的 Exactly-once ,在分布式系统中却非常困难。
其次,我们一起讨论了保证 Exactly-once 的三种方式:至少一次消息传递加消息幂等性、分布式快照加状态回滚和整体重做。这样,以后你再碰到需要 Exactly-once 的业务场景,就可以依据业务场景来进行选择了。
最后,我们一起讨论了在分布式系统中,确保 Exactly-once 面临的挑战:第一是重试的时候需要限制重试的间隔和次数,确保系统不会受到局部故障的影响,导致整体雪崩;第二是保障接口的幂等性,特别是对于涉及外部系统的情况下,如何保障接口整体的幂等性。通过这些讨论,以后对于 Exactly-once 你就心中有数了。
思考题
在 IM 系统中,我们如何实现幂等的消息发送接口?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,128 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 雪崩(一):熔断,让故障自适应地恢复
你好,我是陈现麟。
通过学习重试幂等的内容,让我们在网络故障和部分失败的分布式系统中,也有办法确保程序之间的调用实现 Exactly-once 的效果,这样当系统出现临时故障的时候,用户依然能正常购买,我们的系统又健壮了一点。
在日常运维极客时间服务端系统的过程中,你一定碰到过大规模故障的情况,可是事后复盘时,却发现故障的起因,大多都是一些局部的小问题引起的,比如因为一个接口响应时间变长,使相关实例的协程或线程数暴涨,让系统的负载进一步增加,最终导致实例所有接口的响应时间都变长,这就从一个接口的局部故障演变成了一个全局的故障。
在一个分布式系统中,局部故障是不可避免的,但是如果不能将局部故障控制好,导致其演变成一个全局的系统故障,这对我们来说是不可以接受的,那么我们应该如何解决这个问题呢?
其实这就是分布式系统中的雪崩场景问题,那么从这节课开始,我们将用四节课的时间来解决,如何让一个分布式系统避免发生雪崩的问题。这一节课,我们先讨论雪崩现象出现的原因,然后再分析如何通过熔断机制来避免雪崩,最后一起总结熔断机制应该注意的关键点。
为什么会出现雪崩
雪崩是由于局部故障被正反馈循环,从而导致的不断放大的连锁故障,正如我们上文的例子所说,雪崩通常是由于整个系统中,一个很小的部分出现故障,进而导致系统其他部分也出现故障而引发的。但是,一个正常运行的服务为什么会发生雪崩呢?我认为在实际工作中,出现雪崩一般会经历以下三个阶段,如下图。
首先,服务的处理能力开始出现过载。服务过载是指服务器只能处理一定 QPS 的请求,当发往该服务器的 QPS 超出后,由于资源不够等原因,会出现超时、内存增加等各种异常情况,使服务的请求处理能力进一步降低,过载情况更加严重。
服务处理能力出现过载有多种原因,比如服务可能由于 Bug 导致性能下降,或者由于崩溃导致过载,也有可能就是突发的流量超过了服务的设计目标,或者是机器宕机导致可提供服务的实例数量减少等原因。
然后,服务由于资源耗尽而不可用。当服务严重过载后,会出现大量请求的积压,这会导致服务消耗更多的内存、 CPU 、线程和文件描述符等资源,待这些资源被消耗尽后,服务将出现严重超时和崩溃等异常情况,最终对外表现为不可用。当服务的某一个实例崩溃后,负载均衡器会将请求发送给其他的实例,导致其他的实例也出现过载的情况,从而造成整个服务过载的故障。
最后,由于服务内部出现严重的过载,导致响应严重超时,服务的调用方同样会出现大量请求的积压使资源耗尽,这样正反馈循环就形成了,故障沿着调用链路逆向传播,导致整个系统出现雪崩。
通过上面的讨论,我们可以看出雪崩的根本原因是系统过载,如果在系统过载的情况下,不进行任何控制,异常情况就会急剧扩散,导致雪崩情况出现。所以,想要避免系统雪崩,要么通过快速减少系统负载,即熔断、降级、限流等快速失败和降级机制;要么通过快速增加系统的服务能力来避免雪崩的发生,即弹性扩容机制。
在本节课中,我们先来讨论如何通过熔断来避免系统发生雪崩。
利用熔断机制避免雪崩
其实对于熔断机制,我们并不陌生。在日常生活中,电路保险丝的熔断就是我们最常见的熔断机制,它指的是在电路系统中,当电路超过负荷运行时,保险丝会自动断开,从而保证电路中的电器不受损害。
那么我们就借鉴这个原理来讨论熔断机制。当服务之间发起调用的时候,如果被调用方返回的指定错误码的比例超过一定的阈值,那么后续的请求将不会真正发起,而是由调用方直接返回错误。
我们知道电路在工作的时候,有两种工作状态,分别是通路和开路,计算机的熔断机制则略有不同,在熔断机制的模式下,服务调用方需要为每一个调用对象,可以是服务、实例和接口,维护一个状态机,在这个状态机中有三种状态。
首先,是闭合状态( Closed )。在这种状态下,我们需要一个计数器来记录调用失败的次数和总的请求次数,如果在一个时间窗口内,请求的特定错误码的比例达到预设的阈值,就切换到断开状态。
其次,是断开状态( Open )。在该状态下,发起请求时会立即返回错误,也可以返回一个降级的结果,我们会在后面的课程“降级”中再详细讨论。在断开状态下,会启动一个超时计时器,当计时器超时后,状态切换到半打开状态。
最后,是半打开状态( Half-Open )。在该状态下,允许应用程序将一定数量的请求发往被调用服务,如果这些调用正常,那么就可以认为被调用服务已经恢复正常,此时熔断器切换到闭合状态,同时需要重置计数。如果这部分仍有调用失败的情况,我们就认为被调用方仍然没有恢复,熔断器会切换到断开状态,然后重置计数器。所以半打开状态能够有效防止正在恢复中的服务,被突然出现的大量请求再次打垮的情况。
通过上文对熔断机制的讨论,我们将服务由于过载原因导致的错误比例,作为熔断器断开的阈值,当被调用服务出现过载的时候,熔断器通过错误比例感知到被调用服务过载后,就立即将调用请求返回错误,这样可以减少被调用服务的请求数量,也可以减少调用服务由于等待请求响应而积压的请求,完美切断了正反馈循环,确保了雪崩不会发生。
熔断机制的关键点
到这里,我们已经明白了什么是熔断机制,以及如何利用熔断机制来避免雪崩,但是在熔断机制的具体实现上,还会面临熔断的粒度选择和过载判断等关键的问题,所以接下来我们一起从“粒度控制”、“错误类型”、“存活与过载的区别”、“重试和熔断的关系”和“熔断机制的适应范围”这五个角度来讨论熔断机制的关键点。
粒度控制
对于熔断的粒度控制问题,进一步来说,就是我们想将监控资源过载的粒度控制在一个什么样的范围内,这个范围可以由服务、实例和接口这三个维度的组合来得到,具体见下表。-
结合我的工作经验,在实现熔断机制的时候,更建议你选择“实例的接口”这个熔断粒度,主要有以下三个原因。
首先,熔断的敏感度高。假设有一个服务部署了 10 个实例,并且这 10 个实例都是均匀接受请求流量的。在这种情况下,只有一个实例的一个接口负载过高时,即使它的每一次请求都超时,但由于其他实例的这个接口都是正常的,所以基于“接口”粒度统计到的请求错误率不会超过 10 %,而基于“服务”和“实例”粒度的熔断器统计到的错误率将更低。
如果熔断器的阈值大于 10 %,那么将不能识别到这个实例接口过载的情况,只有等这个接口的过载慢慢被放大,才能被基于“服务”、“实例”和“接口”粒度的熔断器感知到,但是这个结果明显不是我们期待的。
其次,熔断的误伤范围小。当同一服务的不同实例,所分配的资源不相同时,“实例的接口”粒度的熔断机制,能够正确识别有问题实例的接口进行熔断,而不是将这个服务所有实例的这个接口进行熔断,更不是对实例和服务进行熔断,这样就提升了系统的可用性水平。
最后,虽然实现粒度越细的熔断机制,需要维护更多的熔断状态机,导致更多的资源消耗,但是设计优良的熔断机制所消耗的资源是非常少的,“实例的接口”粒度的熔断机制所消耗的资源,完全在系统可以承受的范围之内。
错误类型
由于熔断机制是用来消除系统过载的,所以,我们需要识别出与系统过载相关的错误,来进行熔断处理,一般来说,主要有下面两个错误类型。
第一,系统被动对外表现出来的过载错误,一般来说,如果一个接口过载了,那么它的响应时间就会变长,熔断器捕获到的错误类型就是“响应超时”之类的超时错误。
第二,系统主动对外表现出来的过载错误,对于这种情况,一般是请求的流量触发了限流等机制返回的错误码,这个是我们在程序开发过程中主动设计的。
另外,我们要记住,熔断机制一定不要关心应用层的错误,比如余额不足之类的错误,因为这一类型的错误和系统的过载没有关系。
过载与存活的区别
熔断机制关心的是服务是否过载,而判断一个服务是否过载,最好的方式是依据请求在队列中的平均等待时间来计算服务的负载。之所以不选择请求的平均处理时间,是为了去除下游服务调用的影响,有时处理时间的增加并不代表当前的服务过载了,而是代表请求依赖的下游服务过载了,并且请求的处理时间增加到一定程度,当前服务的资源也会逐渐耗尽,最终反映在等待时间的增加上。
但是在熔断场景中,我们对服务的过载判断进行了简化,直接通过服务接口请求的结果来进行判断。我们执行这个接口的逻辑,如果请求发生错误,并且错误为超时或者限流等错误的比例超过一定的阈值时,我们可以认为该接口是过载的,然后进行熔断。
而存活一般是指机器或者服务是否存活,对于机器是否存活,一般是通过定期 ping 机器的 IP ,如果超过一定时间不能 ping 通,则认为该机器不存活了。对于服务是否存活,一般是由服务来提供一个专门用于探活的、逻辑非常简单的接口,之后定期请求这个接口,如果超过一定时间不能请求成功,则认为该服务不存活了。
当然,服务严重过载会导致服务的存活性出现问题,不过总体来说,过载更关心服务当前的状态好不好,而存活只关心服务是否能活着,这是一个更低的要求。
熔断与重试的关系
熔断和重试都会对服务之间的调用请求进行额外的处理,但不同的是,重试是指在一个请求失败后,如果我们认为这次请求失败是因为系统的临时错误导致的,那么为了提高系统的可用性,我们会重新发起请求。
而熔断则认为当前系统的这一个接口已经出现过载的情况,为了确保系统不会出现雪崩,而对当前接口的请求进行快速失败,直接返回失败,而不是真正地发起请求,以此来减少系统当前的过载情况。
所以,我们可以认为熔断和重试是两个层面的操作,它们之间是相互独立的,不需要相互干扰。我们在需要重试的业务场景中进行重试操作,来提高系统的可用性,而熔断一般会内置到系统的框架中,并且默认开启,作为系统稳定性的最后一道保险丝,来确保系统不会因为过载而雪崩。至于因为熔断被迫进行快速失败的这个请求,它是首次的还是重试的请求,我们并不关心。
熔断机制的适应范围
通过前面的讨论,我们知道了熔断机制是用来解决过载问题的,所以只要是过载问题的场景,我们都可以考虑利用熔断机制来解决,不论是分布式系统中服务之间的调用,还是服务与数据库之间等其他场景的调用。
比如伴鱼开源的数据库中间件 Weir项目地址https://github.com/tidb-incubator/weir它就实现了 SQL 粒度的熔断机制,在后端数据库过载的情况下,通过熔断机制来快速减少数据库的请求压力,确保数据库的稳定性。
同时,一般来说,如果系统出现熔断,都是出现了一定的故障,所以熔断机制状态的变化都是系统非常关键的状态信息,可以通过报警之类的形式通知相关的负责人,来一起观察系统的状态,在必要的时候可以人工介入。
总结
到这里,我们一起讨论了分布式系统中为什么会出现雪崩,以及如何通过熔断机制来避免系统出现雪崩,我们一起来总结一下这节课的主要内容。
首先,我们知道了因为局部故障被正反馈循环导致不断放大,会使系统出现雪崩,这就是为什么一些非常大的故障,其根本原因都是非常小的问题。
在了解了什么是熔断机制,并且如何利用熔断机制来避免系统出现雪崩后,你就能自己实现一个熔断器,来避免你负责的系统雪崩了。
最后,通过了解熔断机制的 5 个关键点,我们正确理解了熔断机制和实现熔断机器的核心问题,从此就能彻底掌握熔断机制了。
思考题
如果我们想判断一个服务是否过载,除了请求在队列中的平均等待时间这个指标之外,还有什么其他的好方法吗?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,153 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 雪崩(二):限流,抛弃超过设计容量的请求
你好,我是陈现麟。
通过上一节课的学习,我们了解了因为局部故障的正反馈循环而导致的雪崩,可以通过熔断来阻断,这样我们就为极客时间的后端系统,加上了熔断这一根保险丝,再也不用担心小故障被放大成一个全局的故障了,这让极客时间的后端系统,在稳定性上又向前跨进了一大步。
但是有的时候,我们明明知道一个服务的最高处理能力为 10 w QPS ,并且我们也知道这一次活动,这个服务的请求会超过 10 w QPS 。这个时候,如果只有熔断机制,我们就需要等待服务由于过载出现故障后触发熔断,然后再恢复正常,那么系统就是在被动地应对服务请求过载的问题。
其实这是一个典型的限流场景,那么,我们应该如何优雅地处理这个问题呢?在这节课中,我们将一起讨论,保障分布式系统稳定性的另一个方法——限流,从限流的原因入手,分析如何实现限流,再一起讨论限流机制要注意的关键问题,从这三个方面来分析,如何通过限流机制主动处理服务流程过载。
为什么需要限流
限流和熔断是经常一起出现的两个概念,都是用来解决服务过载问题的,那么在有了熔断机制后,为什么还需要限流呢?我认为主要有以下几个方面的原因。
首先,熔断的处理方式不够优雅。回到课程开始的例子,虽然在服务过载的时候,熔断可以避免雪崩的发生,但是熔断机制是被动感知故障,然后再进行处理的,它需要先让过载发生,等系统出现故障后,才会介入处理,让系统恢复到正常。
这样的处理方式会让系统产生不必要的抖动,如果是处理意料之外的过载问题,我们是可以接受的。但是,在明知道服务的服务能力的情况下,依然让故障发生,然后在事后进行被动处理,这个处理思路就不够优雅了。
其次,熔断机制是最后底线。虽然熔断可以解决雪崩问题,但是它应该作为系统稳定性保障的最后一道防线,我们没有必要时刻把它亮出来。正确使用熔断的思路应该是,在其他方法用尽之后,如果过载问题依旧存在,这时熔断才会被动触发。
所以,我们的系统虽然有熔断机制,保障雪崩不会出现,但是当熔断出现的时候,依然代表着我们的系统已经失控了。我们需要更主动地解决问题,防患于未然,而限流就可以达到这个目的。
再次,在快速失败的时候,需要能考虑调用方的重要程度。熔断是调用方依据响应结果自适应来触发的,在被调用方出现过载的时候,所有的调用方都将受到影响。但是很多时候,不同调用方的重要程度是不一样的,比如同样是查询用户信息的接口,在用户详情页面调用这个接口的重要程度,会高于评论列表页面,如果查询用户信息的接口出现过载了,我们要优先保障用户详情页面的调用是正常的。
最后,在多租户的情况下,不能让一个租户的问题影响到其他的租户,我们需要对每一个租户分配一定的配额,谁超过了就对谁进行限流,保证租户之间的隔离性。
如何实现限流
通过上面的讨论,我们了解到限流机制是熔断等其他机制无法替代的,是必须的,那么我们该如何实现限流机制呢?这里我们先介绍一下常见的限流算法,然后讨论单节点限流机制需要注意的问题,最后再讨论分布式场景下限流机制的权衡。
限流算法
限流算法是限流机制的基础和核心,并且后续关于限流机制的讨论,都会涉及相关的限流算法,所以我们先介绍最常用的四个限流算法:固定窗口、滑动窗口、漏桶和令牌桶算法,把它们两两结合来进行分析。
固定窗口和滑动窗口
固定窗口就是定义一个“固定”的统计周期,比如 10 秒、30 秒或者 1 分钟,然后在每个周期里,统计当前周期中被接收到的请求数量,经过计数器累加后,如果超过设定的阈值就触发限流,直到进入下一个周期后,计数器清零,流量接收再恢复正常状态,如下图所示。-
假设我们现在设置的是 2 秒内不能超过 100 次请求,但是因为流量的进入往往都不是均匀的,所以固定窗口会出现以下两个问题。
第一,抗抖动性差。由于流量突增使请求超过预期,导致流量可能在一个统计周期的前 10 ms 内就达到了 100 次,给服务的处理能力造成一定压力,同时后面的 1990 ms 将会触发限流。这个问题虽然可以通过减小统计周期来改善,但是因为统计周期变小,每个周期的阈值也会变小,一个小的流量抖动就会导致限流的发生,所以系统的抗抖动能力就变得更差了。
第二,如果上一个统计周期的流量集中在最后 10 ms ,而现在这个统计周期的流量集中在前 10 ms ,那么这 20 ms 的时间内会出现 200 次调用,这就超过了我们预期的 2 秒内不能超过 100 次请求的目的了。这时候,我们就需要使用“滑动窗口”算法来改善这个问题了。
其实,滑动窗口就是固定窗口的优化,它对固定窗口做了进一步切分,将统计周期的粒度切分得更细,比如 1 分钟的固定窗口,切分为 60 个 1 秒的滑动窗口,然后统计的时间范围随着时间的推移同步后移,如下图所示。-
但是这里要注意一个问题,如果滑动窗口的统计窗口切分得过细,会增加系统性能和资源损耗的压力。同时,滑动窗口和固定窗口一样面临抗抖动性差的问题,“漏桶”算法可以进一步改进它们的问题。
漏桶和令牌桶
我们可以在图中看到,“漏桶”就像一个漏斗,进来的水量就像访问流量一样,而出去的水量就像是我们的系统处理请求一样。当访问流量过大时,这个漏斗中就会积水,如果水太多了就会溢出。
相对于滑动窗口和固定窗口来说,漏桶有两个改进点,第一,增加了一个桶来缓存请求,在流量突增的时候,可以先缓存起来,直到超过桶的容量才触发限流;第二,对出口的流量上限做了限制,使上游流量的抖动不会扩散到下游服务。这两个改进大大提高了系统的抗抖动能力,使漏桶有了流量整形的能力。
但是,漏桶提供流量整形能力有一定的代价,超过漏桶流出速率的请求,需要先在漏桶中排队等待,其中流出速率是漏桶限流的防线,一般会设置得相对保守,可是这样就无法完全利用系统的性能,就增加了请求的排队时间。
那么从资源利用率的角度来讲,有没有更好的限流方式呢?我们可以继续看下面介绍的“令牌桶”算法。
如图,我们可以看到,令牌桶算法的核心是固定“进口”速率,限流器在一个一定容量的桶内,按照一定的速率放入 Token ,然后在处理程序去处理请求的时候,需要拿到 Token 才能处理;如果拿不到,就进行限流。因此,当大量的流量进入时,只要令牌的生成速度大于等于请求被处理的速度,那么此时系统处理能力就是极限的。
根据漏桶和令牌桶的特点,我们可以看出,这两种算法都有一个“恒定”的速率和“可变”的速率。令牌桶以“恒定”的速率生产令牌,但是请求获取令牌的速率是“可变”的,桶里只要有令牌就直接发,令牌没了就触发限流;而漏桶只要桶非空,就以“恒定”的速率处理请求,但是请求流入桶的速率是“可变”的,只要桶还有容量,就可以流入,桶满了就触发限流。
这里我们也需要注意到,“令牌桶”算法相对于“漏桶”,虽然提高了系统的资源利用率,但是却放弃了一定的流量整形能力,也就是当请求流量突增的时候,上游流量的抖动可能会扩散到下游服务。
所以,计算机的世界没有银弹,一个方案总是有得必有失,一般来说折中的方案可能是使用最广泛的,这就是没有完美的架构,只有完美的 trade-off 的原因。
单节点限流
由于只有一个节点,不需要和其他的节点共享限流的状态信息,所以单节点限流的实现是比较简单的,我们可以基于内存来实现限流算法,让需要限流的请求先经历一遍限流算法,由限流算法来决定是正常执行,还是触发限流,这里需要注意两个问题。
首先,限流机制作用的位置是客户端还是服务端,即选择客户端限流还是服务端限流。一般来说,熔断机制作用的位置是客户端,限流机制作用的位置更多是服务端,因为熔断更强调自适应,让作用点分散在客户端是没有问题的,而限流机制则更强调控制,它的作用点在服务端的控制能力会更强。
但是,将作用点放置在服务端,会给服务端带来性能压力。如果将作用点放置在客户端,这就是一个天然的分布式模式,每一个调用方的客户端执行自己的限流逻辑,这部分我们会在下面的分布式限流中继续讨论。而将作用点放置在服务端时,服务端要执行所有请求的限流逻辑,就需要更多的内存来缓存请求,以及更多的 CPU 来执行限流逻辑。
我们可以考虑的一个策略是,在客户端实现限流策略的底线,比如,一个客户端对一个接口的调用不能超过 10000 并发,这是一个正常情况下完全不会达到的阈值,如果超过就进行客户端限流,避免客户端的异常流量对服务端造成压力。同时,因为这是一个非常粗粒度的阈值,设置好默认值后,几乎不会去修改,所以就缓解了客户端限流带来的阈值管理问题,之后就可以在服务端实现更精细和复杂的限流机制了。
其次,如果触发限流后,我们应该直接抛弃请求还是阻塞等待,即否决式限流和阻塞式限流。一般来说,如果我们可以控制流量产生的速率,那么阻塞式限流就是一个更好的选择,因为它既可以实现限流的目的,又不会抛弃请求;如果我们不能控制流量产生的速率,那么阻塞式限流将会因为请求积压,出现大量系统资源占用的情况,很容易引发雪崩,这时否决式限流将是更好的选择。
所以,对于在线业务的服务端场景来说,服务之间相互调用的请求流量主要是用户行为产生的,不论是客户端限流还是服务端限流,限流的作用点都处于流量的接收方,因为接收方不能控制流量产生的速率,所以超出阈值后通常直接丢弃,进行否决式限流。
而对于像消费 MQ 消息或者发送 Push 时,为了避免打挂所依赖的下游服务,我们可以通过对 MQ 消费或者发送 Push 的行为进行限速,来控制流量产生的速率,在这种情况下,如果超出阈值了,我们一般选择阻塞等待,进行阻塞式限流。
分布式限流
讨论完单节点限流后,我们还需要重点关注分布式限流,即为了系统高可用,每一个服务都会运行多个实例,所以我们在对某一服务进行限流的时候,就需要协调该服务的多个实例,统一进行限流。因为上文中对于单节点限流讨论的问题,在分布式限流场景同样适用,这里就不再赘述了。下面我们主要来讨论,在实现分布式场景下,如何来协同多个节点进行统一的限流。
首先,最容易想到的一个方案是进行集中式限流。单节点限流是在进程内的内存中实现限流器的,而对于分布式限流来说,我们可以借助一个外部存储来实现限流器,比如 Redis 。在分布式限流的场景下,我们一般选择令牌桶算法,但是这个方法的缺点是,每一次请求都需要先访问外部的限流器获取令牌,这将带来三个问题。
第一,限流器会成为系统的性能瓶颈,如果在系统的 QPS 非常高的情况下,限流器的压力是非常大的。虽然我们可以将请求,通过 Hash 策略扩展到多个限流器实例上,但是这也增加了系统的复杂性。复杂性是系统架构最大的敌人,我们一定要保持敏感。
第二,限流器的故障将会影响所有接入限流器的服务。不过,我们可以在限流器故障的情况下,进行降级处理,例如,如果服务访问限流器获取令牌出现了错误时,可以降级为直接进行调用,而不是抛弃请求。
第三,增加了调用的时延。每一次调用前,都需要先通过网络访问一次限流器,这是一个毫秒级别的时延。
其次,另一个方案是将分布式限流进行本地化处理。限流器在获得一个服务限额的总阈值后,将这个总阈值按一定的策略分配给服务的实例,每一个实例依据分配的阈值进行单节点限流。这里要注意的是,如果服务实例的性能不一样,在负载均衡层面,我们会考虑性能差异进行流量分配。在限流层面,我们也需要考虑这个问题,性能不同的实例,限流的阈值也不一样,性能好的节点,限流的阈值会更高。
但是,这个方式也有一个问题,该模式的分配比例模型,是依据统计意义来进行分配的,而现实中,具体到一个限流策略上,它的精确性可能会出现问题。比如有两个实例的服务,对一个用户限流为 10 QPS ,假设这两个实例的性能相同,每个实例限流的阈值为 5 QPS ,但是如果这个用户的流量,都被路由到其中的一个实例上,这就会导致该用户的流量,在 5 QPS 的时候就触发了限流,和我们的设计预期不一致了。
最后,我们来讨论一个折中的方案,这个方案建立在集中式限流的基础上,为了解决每次请求都需要,通过网络访问限流器获取令牌的问题,客户端只有在令牌数不足时,才会通过限流器获取令牌,并且一次获取一批令牌。这个方案的令牌是由集中式限流器来生成的,但是具体限流是在本地化处理的,所以在限流的性能和精确性之间,就有了一个比较好的平衡。
限流机制的关键问题
了解完限流的实现原理之后,我们就知道如何去实现一个限流器了,但是,在限流器实际落地的过程中,我们需要去配置限流的阈值,同时还要确保系统,不会因为触发了不必要的限流而导致故障,所以我们还需要思考下面两个关键问题。
如何确定限流的阈值
当我们对服务进行限流的时候,首先要面临的第一个问题是,确定服务触发限流的阈值。
一个最简单的方案是,根据经验设置一个比较保守,并且满足系统负载要求的阈值,在之后的使用中慢慢进行调整。但是这个方案会出现一个问题,我们预测的限流阈值不够准确,甚至会出现比较大的偏差,对于限流的阈值来说,不论过高还是过低都会出现问题,阈值过高则限流不会起作用,阈值过低则无法发挥出服务的性能。
另外,我们可以通过压力测试来决定限流的阈值。但是,压测的环境很难和线上环境保持一致,特别是在涉及缓存和存储的情况下,并且单个接口的压力测试不能反映出,正常运行情况下系统的状态。虽然全链路压测可以通过流量回放,一定程度上模拟线上真实流量的比例,但是它也只是用历史的流量比例来预测未来,并且这个工作量是非常大的。
同时,我们的系统在一个持续的迭代过程中,系统的性能可能会随着迭代而发生变化,所以限流的阈值设置好之后,还需要付出一定的维护成本。
限流可能会引入脆弱性
我们引入限流,本来是为了提高系统的稳定性,达到“反脆弱”的目的,但是,如果我们在分布式系统的复杂拓扑调用中,遍布限流功能,那么以后对每个服务的扩容,新功能的上线,以及调用拓扑结构的变更,就都有可能会导致局部服务流量的骤增,从而引发限流使业务有损。所以,限流可能会引入脆弱性,这是一个很值得讨论的问题。
限流机制的“反脆弱”也有可能会导致“脆弱”的出现,它的本质原因是,在限流的阈值设置后,我们很难适应调用拓扑、机器性能等等的变化,但是,在熔断的阈值里是可以自适应这些变化的,也就没有这个问题了。
所以,当我们决定对系统进行大规模限流设置时,需要谨慎地审视系统的限流能力和成熟度,判断它们是否能支撑起如此大规模的应用。
最后,通过这两个讨论,我认为使用限流机制比较好的一个方式是,在系统的核心链路和核心服务上,默认启用限流机制,比如,像网关这样的流量入口和账号这样的核心服务,不论是限流阈值的设定,还是脆弱性的判断,我们都可以通过减少限流引入的范围,来简化使用限流的复杂度;而对于其他的位置和服务,则默认不启用限流机制,在出现故障的时候,通过手动设置阈值再启用,把它作为处理系统故障的一个手段。
总结
到这里,我们一起讨论了需要限流机制的原因,限流机制的算法、实现原理以及关键问题,下面一起来总结一下这节课的主要内容。
通过了解有了熔断之后,还需要限流机制的原因,你在后续的工作中,如果碰到这样的限流场景,就可以引入限流机制了。
另外,在掌握了限流的算法、单节点限流和分布式限流的技术原理之后,你就可以为你现在的系统实现一个限流器了。
最后,我们从限流机制的关键问题:限流阈值的设置和引入的脆弱性中,得出在核心链路和核心服务上,默认启用限流机制,在其他位置上,手动启用限流机制,把它作为处理系统故障的一个手段。
思考题
在日常工作中,你在哪些场景会选择漏桶,哪些场景会选择令牌桶呢?欢迎你举例来分享。
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,106 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 雪崩(三):降级,无奈的丢车保帅之举
你好,我是陈现麟。
通过学习限流的内容,我们掌握了限流机制的应用场景、实现原理和关键问题,这样我们就可以为极客时间后端的分布式系统,在关键路径和核心服务上,去引入限流机制,进一步提高系统的稳定性。
但是,在系统因为过载而出现故障的时候,虽然熔断机制可以确保系统不会雪崩,限流可以确保,被保护的服务不会因为过载而出现故障,可是这时候,系统的可用性或多或少都会受到一定的影响,并且这个影响不会区分核心业务和非核心业务。
那么你的脑海里一定会出现一个想法,是否可以在故障出现的时候,通过减少或停掉非核心业务,来降低系统的负载,让核心业务不会受到,或者少受到影响呢?其实是可以的,这就是一个典型的降级场景问题。
在这节课中,我们将一起讨论保障分布式系统稳定性的第三个方法——降级,分析如何通过降级机制,来保障系统的核心服务稳定运行。这节课我依然会按照需要降级的原因,如何实现降级,以及降级机制应该注意的关键问题这一条思路来为你讲解。
为什么需要降级
为什么有了熔断和限流之后,我们依然需要降级机制呢?在分布式系统中,熔断、限流和降级是保障系统稳定性的三板斧,缺一不可,并且在保障系统的稳定性方面,降级有着熔断和限流所没有的优点,因此它们之间相互配合和补充,能够最大限度地保障系统的稳定性水平。
首先,降级机制能从全局角度对资源进行调配,通过牺牲非核心服务来保障核心服务的稳定性。比如,在当前极客时间的后端系统出现了过载问题的时候,或者我们预计到由于运营活动会出现突发流量的时候,我们有账号、支付和评论三个服务,停掉任意一个服务都可以让系统正常运行,那么相对于账号和支付这两个非常核心的服务,毫无疑问,我们会选择停掉评论服务来丢车保帅,降低系统故障对外的影响,这其实就是降级的核心思路。
你可能会想到,通过限流机制也可以出现降级的效果,比如,直接将评论服务的请求 QPS 限制为 0但是本质上来说限流和降级机制的思维方式还是不一样的。限流一般是通过对请求流量控制来保证被限流服务的正常运行而降级却恰恰相反它是通过牺牲被降级的接口或者服务来保障其他的接口和服务正常运行的。
其次,降级可以提高系统的用户体验性和可用性。在分布式系统中,如果接口的正常调用出现非业务层错误后,在某些情况下,我们可以不用直接返回错误,而是执行这个接口的“ B 计划”进行降级。虽然降级后的执行结果没有正常调用那么完美,但是和直接返回调用错误相比,这对系统的用户体验和可用性来说,却是一个不小的提升。
在这个场景下,降级可以和熔断、限流机制配合使用,在系统触发熔断和限流的时候,我们可以不直接返回错误,而是执行预先准备好的降级结果。降级需要提前设计,并且降级的逻辑也要消耗系统资源,所以一般来说,对于核心的接口或服务,我们可以通过缓存或者其他的方法来提供一些,一致性等方面较差,但是业务可以接受的返回结果;而对于非核心的接口和服务,我们可以考虑通过友好的提示等低成本的方式,来提升用户的体验。
这里一定要注意,降级在和熔断、限流机制配合使用时,一定要评估降级逻辑的性能,千万不能因为降级逻辑,再次导致系统雪崩。
如何实现降级
通过上面的讨论,我们了解到在故障出现的时候,降级机制可以从全局角度,提高系统资源使用的效率,进一步提升系统的稳定性和用户体验,而且这一点是熔断和限流机制都无法替代的。那么我们该如何实现降级机制呢?下面我们根据降级操作是否由人工触发,将降级机制分为手动降级和自动降级,来一一介绍。
手动降级
手动降级是指在分布式系统中提前设置好降级开关,然后通过类似配置中心的集中式降级平台,来管理降级开关的配置信息,在系统需要降级的时候,通过降级平台手动启动降级开关,对系统进行降级处理。
手动降级由人工操作,有可控性强的优点,但是一般来说,一个分布式系统中,会有成百上千的服务和成千上万的实例,如果在出现故障的时候,一个接口、一个服务地去手动启动降级开关是非常低效的。
对于这个问题,有一个可行的方案是,通过对降级分级,利用服务的等级信息和业务信息进行批量降级,具体的思路如下。
首先,将系统中的所有服务,按照对业务的重要程度进行分级,这里,我分享一个服务定级的标准,具体定义见下表。这个标准从高到低按重要程度分为 P0 ~ P3 这 4 个级别,你可以作为参考,依据自己的业务形态进行调整。
然后,根据服务的等级信息、业务信息和调用链路的依赖关系,对非核心服务建立分级降级机制。这里以服务为粒度进行分级,实际工作中,如果有需要也可以以接口为粒度进行分级。假设 P0 为核心业务,其他的为非核心业务,我们可以简单地将降级分为以下 3 个级别。
一级降级:会对 P1、P2、P3 的服务同时进行降级。
二级降级:会对 P2、P3 的服务同时进行降级。
三级降级:会对 P3 的服务同时进行降级。
这样在需要降级的时候,我们就可以根据系统当时的情况,按接口、服务和降级级别进行手动降级。当然在实际操作中,你还可以综合业务场景来设置降级级别,并且根据业务需要来设置更多的降级级别。这里要注意,不论是服务分级还是降级分级,都是需要谨慎对待的一件事情,如果出错将会导致人为的故障发生。
自动降级
自动降级是指在分布式系统中,当系统的某些指标或者接口调用出现错误时,直接启动降级逻辑,但是因为自动降级不能通过开关来控制,所以需要认真评估。一般来说,系统关键链路上的“ B 计划”可以进行自动降级,否则业务将无法正常提供服务。
这里我们来看一个鉴权接口自动降级的例子。假设我们在网关中调用鉴权服务进行鉴权,每一个调用鉴权服务的鉴权接口,需要执行如下的两个校验逻辑,不论哪一个失败,都会导致鉴权失败。
1. 校验 Token 是否合法。-
2. 校验 UID 是否被管理员封禁。
在这个情况下,我们可以将 Token 设计为可以自校验的,在鉴权服务出现故障的时候,则启动降级逻辑,直接在网关中校验 Token 是否合法如果合法就返回鉴权成功。因为在大多数业务场景中Token 被管理员封禁是小概率事件,所以相对于所有用户都不能正常鉴权的情况,我们认为个别被管理员封禁的用户也可以鉴权成功,是完全可以接受的。
其实,我们可以将自动降级理解为手动降级的特殊情况,即降级开关为启用的手动降级。所以,还有一个思路就是,不提供自动降级,在需要自动降级的场景下,通过降级开关为启用的手动降级来实现,这样还可以进一步提高降级的灵活性。
降级机制的关键问题
学习完降级的实现原理后,我们就知道了如何在自己的系统中引入降级机制了。但是一般来说,我们使用降级都是在系统已经出现过载的场景下,这时我们需要考虑,降级的配置信息是否能正常下发。并且,降级通常会与熔断和限流一起出现,我们应该如何处理它们三者之间的关系。基于这两点,在降级机制实际使用的过程中,我们还需要思考下面两个关键问题。
配置信息下发的问题
对于熔断和限流来说,其阈值相关的配置信息在系统正常运行的时候,就已经下发到实例上了,所以在系统出现故障的时候,这些配置信息会直接生效。但是对于降级机制来说,如果采用了手动降级的机制,并且默认设置为关闭,在系统出现故障的时候,我们需要通过降级平台下发配置来启动降级。
但是在系统出现故障的时候,有可能会出现降级配置无法正常下发的情况,这时我们将不能启动降级策略。我们可以考虑,由服务直接暴露出修改降级配置的 HTTP 接口,在必要的时候,可以手动通过 HTTP 接口,来启动服务的降级逻辑。
熔断、限流和降级之间的关系
在分布式系统中,熔断、限流和降级是保障系统稳定性的三板斧,经常一起出现,很容易导致混淆,所以,下面我们就对熔断、限流和降级机制之间的关系进行比较和总结:
首先,因为熔断机制是系统稳定性保障的最后一道防线,并且它是自适应的,所以我们应该在系统全局默认启用;其次,限流是用来保障被限流服务稳定性的,所以我们建议,一般在系统的核心链路和核心服务上,默认启用限流机制;最后,降级是通过牺牲被降级的接口或者服务,来保障其他的接口和服务正常运行的,所以我们可以通过降级直接停用非核心服务,然后对于核心接口和服务,在必要的时候,可以提供一个“ B 计划”。
其实,从整个系统的角度来看,不论是熔断还是限流,一旦触发了规则,都是通过抛弃一些请求,来保障系统的稳定性的,所以,如果更广泛地定义降级的话,可以说熔断和限流都是降级的一种特殊情况。
总结
我们掌握了需要降级机制的原因,以及实现原理和关键问题,一起来总结一下这节课的主要内容。
通过讨论有了熔断和限流机制之后,依然需要降级机制的原因,我们了解了限流的作用和应用场景,在后续的工作中碰到相关的问题时,可以引入降级机制。
另外,我们一起分析了如何实现降级机制,从操作的角度来讲,降级分为手动降级和自动降级,掌握了这些知识和原理后,你就能为你现在的系统实现一个降级机制了。
我们还一起探讨了限流机制的关键问题:配置信息下发的问题,以及熔断、限流和降级机制之间的关系,这样一来,你不仅能实现一个健壮的降级机制,并且还能更好地理解熔断、限流和降级三者之间的关系。
思考题
保障分布式系统稳定性的三板斧,熔断、限流和降级都已经讨论完了,欢迎你来分享一下自己对熔断、限流和降级的理解。
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,116 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 雪崩(四):扩容,没有用钱解决不了的问题
你好,我是陈现麟。
在降级的学习中,我们掌握了降级机制的应用场景,手动降级和自动降级的实现原理,以及降级机制值得注意的一些关键问题,这样我们就可以引入分级降级策略,来快速降低系统的负载,确保核心服务的可用性了。现在我们已经学习完了分布式系统稳定性的三板斧:熔断、限流和降级,以后对维护后端系统的稳定性就更有信心了。
虽然熔断、限流和降级,很大程度上保障了系统的稳定性,但是从结果来看,它们都是通过放弃一定的用户体验和可用性,来确保系统在过载情况下依然正常运行的,这是一种通过有损节流,来降级系统负载的思路,那么有没有一种无损的方式,可以保障系统在过载下依然正常运行呢?
其实,这个问题就引出了一个典型的扩容场景,在这节课中,我们将一起讨论保障分布式系统稳定性的最后一个方法——扩容,了解需要扩容的原因,讨论如何实现扩容,最后再一起分析扩容机制与云原生的关系。这里要说明一点,因为缩容是扩容的逆向操作,所涉及的思路,原理和扩容一致,所以在课程中就不分开说明了。
为什么需要扩容
在“雪崩”系列的前三课中,我们分别介绍了解决分布式系统稳定性的三板斧:熔断、限流和降级,它们从系统底线的保障、核心服务的保障和非核心服务的牺牲这三个角度,全方位地保障着分布式系统的正常运行。但是,正如课程开始提到的,这些方法本质上都是对系统进行降级,通过有损的方式来保障系统不会雪崩。
究其根本原因,熔断、限流和降级都是一种静态思维模式,当系统过载了,就通过各种方式来放弃一部分请求,降低系统负载,从而让系统恢复正常。我们在降级这节课中也提到过,从更广义上来讲,熔断和限流都是降级的一种特殊情况,都在做丢车保帅的事情。
而扩容则是一种动态的思维模式,当系统过载了,就增加资源让系统重新恢复正常,而不是对系统进行降级处理,所以扩容是一种无损的系统过载恢复手段。
但是,扩容也会带来问题,我们需要用更多的资源来应对系统过载,也就是需要花费更多的钱。这是一个投入产出比( ROI )的问题,是通过有损降级恢复系统,导致用户的体验和可用性,以及用户口碑、品牌等方面的损失,与扩容资源投入的价值之间的比较。不过对于我们来说,这也不是一个二选一的问题,正常的情况下两个方式都会需要,我们在有损降级和扩容之间,找到适合自己的平衡点即可。
一般对于一个公司来说,在不同的阶段,对于平衡点的选择会有不同的倾向,早期公司会更倾向于使用有损降级的方向,而成熟公司会更倾向于使用扩容的方向,这其实就是由系统稳定性保障的 ROI 来决定的。
那么,在拥有扩容机制之后,我们的雪崩处理策略也会发生变化,不论是像运营活动等计划内的流量突增场景,还是计划外的系统过载问题,我们都会先投入一定的资源对系统进行扩容,来应对系统的过载问题。如果扩容后,系统依然处于过载状态,那么就通过熔断、限流和降级等有损机制,对系统的稳定性进行兜底。而对于扩容应该投入多少资源,每个公司根据自己的情况来设置这个平衡点。
如何实现扩容
通过上面的讨论,我们知道除了熔断、限流和降级之类的有损策略外,还可以通过扩容这样的无损策略,将系统恢复到正常的情况,这对于用户规模大、品牌价值强的公司来说,无疑多了一个非常好的选择。
在对系统进行扩容的时候,首先我们需要评估出需要扩容的服务,以及需要扩容到什么样的容量,然后才能进行扩容。一般来说,对于运营活动之类的计划内的扩容,我们通过历史数据和经验来评估,而对于线上计划外的系统过载触发的扩容,我们就需要通过监控,来捕捉系统的过载服务和程度,然后才能进行扩容操作。一般来说,动态扩容的流程如下图。
那么接下来,我们先介绍如何通过自适应的方式,来判断服务是否出现过载问题,然后从自动扩容的角度讨论如何实现扩容机制。
过载判断
过载判断是一件复杂的事情,如果我们打算通过基准测量,来确定服务过载指标,这将是一件无法持续的事情。因为服务会持续迭代,服务运行的硬件随时都有可能发生变化,这就会导致一种情况,即付出了巨大的工作量,但测量出的过载指标可能还是无法匹配线上运行,那么最终就会让过载判断出现错误,人为引入了故障。所以,我们需要寻找可以自适应的过载判断标准。
对于这个问题,有一个可行的方案,是我们在熔断这节课中介绍的,我们可以依据请求在队列中的平均等待时间来计算服务的负载。比如,一个服务在 1 分钟之内的平均等待时间超过 3 秒,我们就认为该服务进入过载状态。这里的“ 1 分钟之内的平均等待时间超过 3 秒”是一个自适应的指标,不论服务是否进行优化和迭代,以及服务运行在什么样的硬件上,我们通过这个指标来进行判断都是成立的。
但是,这个方式需要入侵到每一个服务的实现逻辑中,所有的服务都需要在实现时,暴露出接口请求的排队时间。如果有一个服务没有暴露,我们将无法捕捉到这个服务的过载状态,从而导致故障的发生。并且有些服务的实现,不会对接口请求进行排队,在这样的情况下,我们也就无法通过排队时间,来判断服务的过载情况了。
所以在熔断场景下,我们对服务的过载判断进行了简化,直接对服务接口请求的结果来进行判断,如果请求发生了过载原因导致的错误,并且超过一定的阈值时,我们就可以认为该接口是过载的。
经过上面的讨论,可能你会觉得对服务的过载判断还是比较难的,其实从本质上来说,过载判断是非常简单的,我们只需要知道服务的满载指标,接近或者超过这个指标就是过载。但是依据这个思路确定服务过载指标时,会有 2 个问题。
初始满载指标测量的工作量大:服务非常多,并且还会快速增长,需要持续测量每一个服务的满载指标。
服务的满载指标是会变的:服务持续迭代,并且会运行在不相同的硬件上,导致满载指标是不稳定的。
而上述的 2 个问题,对于物理机器和 K8S 上的 Pod 这样的节点来说,都是非常容易解决的。
初始满载指标是硬件指标,不需要测量,可以直接从操作系统中准确获取,比如 CPU 32 核,内存 64G 等。一般为了避免出现过载情况,我们会相对保守,将满载指标按硬件指标的百分比来设置,比如 60% 之类的。
满载指标是硬件的指标,是不会变的。
所以,另一个判断服务过载的方案是,将服务和节点一一绑定,一个节点上只运行一个服务,如果节点的系统指标过载,则说明该服务出现了过载,需要扩容。在一台物理机器上只运行一个服务,资源浪费会比较严重,而 K8S 上的 Pod 则是一个非常好的方案。
当然,在某些业务场景下,我们认为服务每秒的 QPS 之类的指标,是决定系统过载最好的指标,我们也可以使用这个指标,来判断服务是否需要扩容。只不过我们要记住这个指标不是自适应的,在服务及其部署节点的性能发生变化后,我们需要再次评估好指标的阈值。
自动扩容
判断出系统过载的服务以及过载的程度之后,对系统进行扩容就是一个自动化部署的事情了。自动扩容分为两个层面,一个是容器的层面,另一个是机器节点的层面。
首先,对于容器层面的扩容有两个维度,一个是水平扩容,即通过增加服务的实例数量对系统进行扩容;另一个是垂直扩容,即通过升级服务部署节点的资源对系统进行扩容。在 K8S 中Horizontal Pod Autoscaler HPA 对应水平扩展Vertical Pod Autoscaler VPA )对应垂直扩展,具体的策略如下图。
一般来说,水平扩容不受单机硬件的限制,我们可以优先考虑,但是对于有状态服务,在水平扩容的时候,会涉及数据迁移。如果这个有状态服务,对数据的自动迁移原生支持不好的话,会给系统增加复杂度,这时垂直扩容是一个不错的选择。
其次,当我们进行容器层面的扩容后,整个集群的资源也会发生变化,如果集群的资源不足或者比较空闲,这时就需要进行机器节点层面的扩缩容了。对于节点层面的自动缩放涉及 Cluster Autoscaler CA ),它会在以下情况中自动调整集群的大小。
由于集群中的容量不足,任何 Pod 都无法运行并进入挂起状态在这种情况下CA 将向上扩展集群的容量。
集群中的节点在一段时间内未得到充分利用,并且节点上的 Pod 是可以迁移的在这种情况下CA 将缩小集群容量。
CA 进行例行检查来确定是否有任何 Pod ,因为等待额外资源处于待定状态;或者集群节点是否未得到充分利用,如果需要更多资源,就会相应地调整 Cluster 节点的数量。 CA 通过与云提供商交互,来请求其他节点或关闭空闲节点,并确保按比例放大或者缩小的集群,保持在用户设置的限制范围内。
扩容机制与云原生的关系
我认为自动扩容和缩容是云原生时代软件的标志之一,即利用云的能力来实现软件能力的弹性变化。
你会发现,在云原生时代之前,所有的系统都部署在自己运维的 IDC 机房中,由于机房的成本是一次性投入的,不能按需使用,所以当时的扩容是一件笨重和昂贵的事情。
当时我们需要有计划地做容量预计,然后购买机器,再进行扩容。如果计划中,有流量巨大的运营活动,就需要提前进行扩容处理,并且在运营活动过去之后,流量降下来了,也没有办法进行缩容,这就会导致我们要为系统的峰值付费,是巨大的成本浪费。
所以,如果那时系统出现了计划外的过载问题,熔断、限流和降级是更常用的方案。虽然一般来说, IDC 机房中会准备一定的备用机器,但是这些资源还没有弹性利用的机制,需要人工介入,效率非常低。
而现在则完全不一样了K8S 与公有云结合,通过 Cluster Autoscaler CA )请求增加节点或关闭空闲节点,可以为我们提供按需付费的弹性资源,这样一来,不论是在成本还是效率方面都有了非常大的改进,扩容和缩容将会变成一个自生而来的事情。所以,我认为系统能否利用公有云或私有云进行弹性扩容,是云原生系统的核心标志。并且在以后,扩容将是解决系统过载问题最常用的方法。
总结
在这节课中,我们先从故障恢复手段,对系统的用户体验性和可用性影响的角度,讨论了在有了三板斧之后,需要扩容机制的原因。通过这个讨论,你知道了扩容的作用和应用场景,在后续的工作中碰到相关的问题时,可以引入扩容机制。
另外在如何实现扩容机制的讨论中,我们知道了如何判断一个服务是否过载,以及自动扩容的两个方式:水平扩容和垂直扩容,掌握了这些知识和原理后,你就能为你现在的系统引入一个扩容机制了。
最后,我们一起探讨了扩容机制与云原生之间的关系,并且了解了云原生系统的核心标志是,能否利用公有云或私有云进行弹性扩容,在以后,扩容将是解决系统过载问题最常用的方法。
思考题
在云原生时代,除了按需付费(即扩容、缩容的弹性能力)之外,你觉得还有哪些趋势呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,167 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 可观测性(一):如何监控一个复杂的分布式系统?
你好,我是陈现麟。
通过学习“雪崩”系列的内容,我们掌握了构建一个稳定的分布式系统所需的四大方法:熔断、限流、降级和扩容,再也不用担心由于一个局部的小问题,导致整个系统出现重大的故障了。
在“雪崩”系列课程中,我们曾经提到需要基于系统内部的运行状态,来进行相应的降级和扩容操作,特别是在扩容机制中,需要通过服务过载的信息来进行相应的扩容,可是我们应该如何来获得系统内部的运行状态呢?
其实这就是分布式系统中的可观测性问题,那么从这节课开始,我们将用 2 节课的时间来讨论,如何通过分布式系统的可观测性,来解决系统监控与告警的问题。在这一节课中,我们先讨论需要监控的原因,然后分析监控与可观测性之间的关系,接着介绍搭建一个可观测性系统涉及的开源组件,最后,重点讨论对于一个大规模的分布式系统,设计监控系统应该遵循的经验和原则。
为什么需要监控
如果一辆汽车没有仪表盘,我们就不知道汽车当前的速度,只能凭着感觉开,很容易出现超速甚至意外,另外由于不知道当前还有多少汽油或者电量,一不小心就会因为能源耗尽抛锚在路上。监控之于分布式系统,更甚于仪表盘之于汽车,因为分布式系统的内部更加复杂,更容易出现意外的情况。那么对于“为什么需要监控”的这个问题,我们就从监控有哪些作用的角度来回答。
第一,从规则角度,监控信息是扩容、缩容和报警等规则的数据来源。只有通过监控了解了系统的状态信息,才能基于状态信息设置一定的规则,当规则满足后,就触发扩容、缩容和报警等相关处理。
第二,从全局角度,基于监控信息,我们才能构建监控大盘。监控大盘能让我们快速地了解当前系统的情况,并且能回答当前系统表现的一些基本问题。
第三,从长期角度,通过监控信息,可以分析系统的长期趋势。比如从系统当前磁盘的使用情况和增长速率,我们可以推测出什么时候需要进行扩容。
第四,从实时角度,在系统出现变更的时候,可以通过监控系统,迅速了解最新的变更是否异常。比如缓存命中率是否下降,请求时延是否变长。
第五,从调试角度,当系统出现报警信息的时候,通过监控系统能帮我们快速定位问题。在前面的“雪崩”系列中,虽然我们已经知道如何保障系统的稳定性了,但是既然故障出现了,就一定要定位到根本原因,然后彻底去解决。
监控和可观测性之间的关系
在 2018 年以前, IT 领域一直使用监控这个术语,来表示通过采集系统、服务和网络的内部信息,诊断和预测系统的行为。到了 2018 年, CNCF Landscape 率先出现了 Observability 的概念,将可观测性( Observability )从控制论( Cybernetics )中引入到 IT 领域。
在控制论中,可观测性是指系统可以由其外部输出,来推断其内部状态的程度,系统的可观察性越强,我们对系统的可控制性就越强。自此以后,“可观测性”逐渐取代了“监控”,成为云原生技术领域最热门的话题之一。
那么,在 IT 领域中,为什么会用“可观测性”逐渐取代“监控”呢?我们先来感性认识一下,一般来说,监控主要告诉我们以下的信息,这些信息主要表现为结果。
CPU 超过 80% 。
系统负载超过 200% 。
机器宕机了,服务崩溃了。
而一个可观测系统,除了告诉我们这些结果信息之外,还需要能回答出导致这些结果的原因。
性能问题是由什么原因导致的?瓶颈在哪里?
请求执行过程都需要经过哪些服务?请求失败的原因是什么?
每个服务如何处理请求?服务之间的依赖关系是什么样的?
从上面的对比中,我们可以初步了解到监控和可观测性之间的区别,接下来,我们再进一步从基本概念的角度,来讨论监控和可观测性概念的差别。
如下图,在 IT 建设中,我们将“可观测性”能力划分为 5 个层级,其中告警( Alerting )与应用概览( Overview )都属于传统监控的概念范畴,因为触发告警的往往是明显的症状与表象。但在云原生时代,架构与应用部署方式的变化是非常频繁的,不告警并非意味着一切正常,因此,通过获取系统内部的信息,来主动发现( Preactive )问题就显得非常重要了。
可观测性通过排错、剖析与依赖分析,这三个部分来主动发现故障,具体如下。
排错( Debugging ),即运用数据和信息去诊断故障出现的原因。
剖析( Profiling ),即运用数据和信息进行性能分析。
依赖分析( Dependency Analysis ),即运用数据信息追踪系统模块的依赖关系,进行关联分析。
并且,这三部分的逻辑关系是:首先,无论是否发生告警,运用主动发现能力,都能对系统运行情况进行诊断,通过指标呈现系统运行的实时状态;其次,一旦发现异常,逐层下钻,进行性能分析,调取详细信息,建立深入洞察;最后,调取模块与模块间的交互状态,通过链路追踪构建“上帝视角”。
因此,主动发现能力的目的,并不仅仅是为了告警与排障,而是通过获取最全面的数据与信息,构建对系统、应用架构最深入的认知,而这种认知可以帮助我们提前预测与防范故障的发生。
通过上面的讨论,我们可以看出可观测性是监控的扩展和进化,监控是建立在可观测性收集的数据之上的,我们可以结合下图,从三个维度进行理解。
首先,监控到可观测性是从黑盒往白盒方向的进化。监控更注重结果,即当前出现了什么问题,或者将要出现什么问题;而可观测性也同等关注问题出现的原因,即通过内部状态的展示来回答这个问题。
其次,监控到可观测性是从资源往服务方向的进化。监控的使用主体主要是运维,监控的对象主要为系统资源相关,而在云原生时代,分布式系统越来越复杂,仅仅通过系统层面监控是远远不够的,所以可观测性使用的主体引入了研发。通过研发的加入,增加了服务内部状态和服务之间调用关系的暴露,大大增强了我们对系统的控制力。
最后,监控到可观测性是处理方式从被动监控到主动分析的进化。在监控为黑盒的情况下,我们完全不了解系统内部的情况,只能等待监控信息触发报警后,再进行处理;而当监控为白盒的情况下,研发人员可以方便地了解系统内部运行的情况,并且进行分析,主动发现问题。
可观测性系统的开源组件
搭建一个可观测性平台,主要通过对日志( Logs )、链路( Traces )与指标( Metrics )这三类数据进行采集、计算和展示,它们的具体信息如下。
日志信息( Logs ),即记录处理的离散事件。它展现的是应用运行而产生的信息,或者程序在执行任务过程中产生的信息,可以详细解释系统的运行状态。虽然日志数据很丰富,但是不做进一步处理,就会变得难以查询和分析。目前,我们主要通过 ELK 来处理。
追踪链路( Traces ),处理请求范围内的信息,可以绑定到系统中,单个事务对象的生命周期的任何数据。在很大程度上, Traces 可以帮助人们了解请求的生命周期中,系统的哪些组件减慢了响应等。目前,我们主要通过分布式调用链跟踪系统 Jaeger 来处理。
指标信息( Metrics ),它作为可聚合性数据,通常为一段时间内可度量的数据指标,透过它,我们可以观察系统的状态与趋势。目前,我们主要通过 Prometheus 进行采集和存储,通过 Grafana 进行展示来解决。
基于上面的开源组件,我们可以很方便地搭建一个可观测性平台,来提升极客时间后端分布式系统的可观测性。
同时,我们也可以看出,当前对于可观测性的三类数据 Logs 、 Traces 和 Metrics 的处理系统是割裂的,这会导致它们相互之间的数据不通、标准不统一、组件繁杂。所以 CNCF 社区推出了 OpenTelemetry 项目,旨在统一 Logs、 Traces 和 Metrics 三种数据,实现可观测性大一统,这是一个非常有雄心的计划,目前正在推进中,我们敬请期待。
监控系统的设计经验
在可观测性的五个层次中, Overview 和 Alerting 这两个部分和业务结合得非常紧密,完全是依照业务场景来定制的。并且 Overview 是我们使用可观测性系统主要的入口, Alerting 是可观测性系统将故障通知到工程师的通道,在可观测性系统中,它们是和工程师日常工作非常紧密的两个部分。
因此,一个设计良好的 Overview 和 Alerting 非常影响可观测性系统整体的使用效率和体验,那么接下来,我就将我在工作过程中,对设计 Overview 总结的一些经验分享给你, Alerting 将会在下一课中介绍。
分层设计,每一层都有自己的 Overview
在公司中,每一个层级的工程师,所关心的目标是不一样的,所以, Overview 应该分层设计,你关心的目标是什么,你的 Overview 就应该展示什么。一般来说,层级越高,关心的事情越偏业务,不要将各层的关注点混合在一起。
首先,对于整个研发部门来说, Overview 展示的是,能够实时体现公司业务状态最核心的指标,例如 Amazon 和 eBay 会跟踪销售量, Google 和 Facebook 会跟踪广告曝光次数等与收入直接相关的实时指标;而 Netflix 由于是订阅制,销售数据不实时反映业务的情况,则通过反映用户满意度的指标——播放按钮的点击率来替代,即视频每秒开始播放数,简称为 SPS Starts Per Sencond )。
其次,对于运维研发、 DBA 、基础服务之类的各级研发团队,同样需要有自己的 Overview ,总体原则依然是,团队的目标是什么, Overview 就应该展示什么,并且整个团队的人都需要关心这个 Overview。
最后,最低一层的 Overview 对象不是工程师,而是为每一个服务、机器节点建立 Overview 。因为相对于部门组织来说,工程师的数量大,并且变化比较频繁, Overview 的维护成本非常大。并且,个人查看自己负责的服务和机器的 Overview 需求,可以通过团队、服务和机器节点的 Overview 来解决。
通过对 Overview 分层设计,保证了每一层的专注力都能聚焦在核心指标上,如果上层指标出现异常,我们可以查看下一层的指标,进一步诊断问题,并且结合 Logs 和 Traces ,直到找到问题发生的原因。
少就是多, Overview 不要超过一个页面
在分层设计中,我们已经知道对 Overview 进行分层设计的方法了,这里我们主要来讨论 Overview 内容的设计经验。
除了上文提到的“ Overview 应该分层设计,你关心的目标是什么,你的 Overview 就应该展示什么”之外,还有一个非常重要的原则是,一个 Overview 不应该超过一个页面。
首先,在使用上,我们经常要持续观察系统的情况,如果一个 Overview 有多个页面,我们就需要不停地切换,这非常影响我们的专注力,效率非常低。
其次,在内容上, Overview 的信息应该越精简越好,精简到不能再精简为止,如果超过了一个页面,就说明信噪比比较低,聚合和精简做得不彻底。
最后在日常维护上Overview 不超过一个页面,相当于规定了指标的最大数量,也就避免了我们只增加指标,从来不去清理的问题。
四个黄金指标
Google SRE 团队在介绍它的监控系统时,明确说明监控系统的四个黄金指标是延迟、流量、错误和饱和度,如果系统只能监控四个指标,那么就应该监控这四个指标,具体介绍如下。
延迟是指服务处理某个请求所需要的时间。我们在计算延迟的时候,应该区分成功的请求和失败的请求,比如,某一个接口的一个实例触发了熔断,被熔断的请求时延非常低,如果将熔断的请求时延和正常请求一起统计的话,就会产生误导性的结论。
流量是用来度量系统负载的。对于 API 接口来说是请求的 QPS ;对于音视频流媒体来说,是并发数或者网络 I/O 速率。
错误是指请求失败的速率,包含显示错误,比如 HTTP 500 以及隐式错误,比如 HTTP 200 回复中包含的错误。
饱和度描述的是,系统当前的负载占满载的百分比,一般来说,以整个系统最受限的资源的指标来表示,比如,对于 Redis 这样的内存系统,内存就是它的饱和度指标,假设当前机器使用内存为 16 G ,机器总内存为 32 G ,那么当前系统的饱和度为 50%。
选择合适的度量方法和采样频率
分位值是指把所有的数值从小到大排序,取前 n% 位置的值,即为该分位的值。它更加能描述数据的分布情况,比如,请求时延 60% 的分位置为 3 s ,说明有 60% 的请求时延小于或者等于 3 s 40% 的请求时延大于或者等于 3 s ,依据这个信息,我们就能判断出,当前接口时延大于 3 s 的比例。
由于平均值容易受到少数极值的影响,所以,当请求时延的平均值为 3 s 时,我们不能判断出接口时延的真实情况。比如, 100 个请求的时延为 10 ms有 1 个请求的时延为 102 s 的情况下,虽然平均时延很大,但是 99% 的请求时延都很快,在 10 ms 以内。
一般情况下,我们经常会通过计算多个分位值,来进一步了解数据的分布情况,比如,请求的时延情况,我们经常会通过 80% 、90% 、99% 、99.9% 多个分位值来度量。
而对于采样频率,我们需要对系统的不同部分,选择不同的频率。比如 CPU 使用率的变化是非常大的,我们可以选择 1 s 采集一次,而对于变化非常缓慢的磁盘容量, 1 分钟一次的频率可能都高了。
并且,我们可以通过降低历史数据的采样频率,来降低存储空间,提高访问速度。比如,对于 CPU 使用率,超过一个月的数据,可以从 1 s 一个采样点,聚合为 1 分钟内只保留 3 个数据:最大值、最小值和平均值。
总结
本节课,我们先从规则、全局、长期、实时和调试的角度,分析了需要监控的原因,这样你就明白了监控的重要性。
通过讨论两个很容易混淆的概念,即监控和可观测性之间的关系,我们将多个维度进行对比,得出监控是可观测性的一部分,可观测性是监控的扩展和进化这个结论。
为了了解开源社区主流的实现方式,我们介绍了可观测系统需要收集的关键数据:日志( Logs )、链路( Trace )与指标( Metrics ),这样你就知道如何搭建一个可观测性平台了。
最后,我们讨论了监控系统的设计经验,依据这些设计经验,你在设计一个监控系统的时候,就能游刃有余了。
思考题
你平时在使用监控系统的过程中,碰到最大的痛点是什么呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,116 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 可观测性(二):如何设计一个高效的告警系统?
你好,我是陈现麟。
通过上节课的学习,我们掌握了在可观测性体系中,监控的位置和重要性,以及设计一个监控系统的基本原则,这样我们就可以为极客时间搭建一个可观测体系,并且设计一个简洁有效的监控系统了。
但是,只有监控还是不够的,因为我们不能一直盯着监控系统,所以需要通过一些规则,自动从监控的信息中发现问题,实时通知给负责的工程师,让工程师实时接入来处理。
那么解决这个问题的有效方法就是告警,你作为工程师,应该收到过各种各样的告警信息,并且及时解决了很多线上问题。但是,你也一定收到过很多无效的报警信息,这些信息浪费了我们的精力;有时线上故障真的发生了,反而会出现收不到告警信息的情况,导致我们错过了最佳的修复时间。
其实这是因为告警系统的设计不够高效,那么在本节课中,我们将一起来解决这个问题。我们会先讨论一个告警系统的评价指标,然后基于我的亲身经历,来讨论如何进行告警的治理,最后再来总结告警系统的设计经验。
告警系统的评价指标
告警系统的作用是把线上已经出现,或即将出现的故障及时通知给我们,所以一个理想的告警系统应该是不多报,不漏报,报对人。即所有的通知都是有效的,是需要立即处理的;所有的故障或即将出现的故障,都有告警通知;所有通知的接受对象,应该是处理这个问题的最佳人选。
从上面的讨论中,我们可以得出如下的三个指标,来评价一个告警系统。
信噪比:指有效告警通知数和无效告警通知数的比例,信噪比越高越好,是用来评估“多报”问题的。
覆盖率:指被告警系统通知的故障占全部线上故障的比例,同样,覆盖率也是越高越好,是用来评估“漏报”问题的。
转交率:指被转交的告警通知数占全部告警通知数的比例,转交率越低越好,是用来评估“对比人”问题的。
综上,依据这三个指标,我们能够评估一个告警系统的设计是否高效。
告警治理案例
关于如何告警治理,在我的工作中,有过一个非常有意思的经历,我在这里分享给你。
我们的告警系统会自动分析每个服务实例的日志,通过日志等级和数量自动生成告警通知,例如下表所示的规则。
案例背景
因为公司业务发展非常快,工程师的数量快速增长,所以业务需求也快速地迭代,我们都无暇对报警信息进行及时清理和处理。记得那时候,一天当中告警系统发出的告警通知有几千条,我们已经进入了恶性循环当中,告警通知越多,越不关注告警以及处理报警信息,导致告警通知变得更多了。
其实我们很清楚,这是一个非常不好的问题,整个告警系统的信噪比太低了,告警系统已经形同虚设,很容易因为漏过告警通知,而错过问题处理的最佳时间,导致更大的故障出现。于是,我们决定采取一些行动来解决这个问题。
首先,我们通过开会来强调这个问题,希望提高所有人的重视度,让工程师们及时处理告警通知,并且清理不需要的告警通知,提升系统的信噪比,使整件事情进入一个正循环。
在会后一段时间内情况有好转,但是后来又慢慢恢复到了之前的状态,其中一个主要原因是研发的工作比较忙碌。虽然所有人都明白清理好报警的长期受益,但是这个事情需要持续去优化才能显示出效果,我们坚持做几天问题不大,可是持续坚持就非常难了。这个现象在减肥、跑步、健身等场景里太司空见惯了,做一件事情很简单,但是长期坚持做一件简单的事情却非常难。
然后,我们开始思考既然开会不行,那么就通过统计数据,做一个服务的告警排名,并且每天都公开发布,让所有人了解自己负责服务的情况,基于排名开始竞争起来。 Leader 们也通过为告警少的服务点赞,推动告警多的服务做清理。刚刚开始实行时,效果很不错,但是不久后,所有人就很难坚持下去了,工程师们对告警排名麻木了,效果越来越差。
在上面的两个方法依然不见收效后,我们仔细分析了出现这个问题的原因。
通知机制太弱:当时的告警通知都发在一个群里面,然后 @ 相关的负责人,人们很容易忽略掉。其实当时也可以通过规则来设置电话报警,但是这个需要你主动来配置。
推动清理的粒度太大:不论是开会强调,还是告警排名,推动的频率都是以天来计算的,可是经过一天的时间,已经积累了很多告警,处理的压力增加,动力就会差很多。
解决方法
进一步找出问题后,我们对告警通知采取了下面的处理机制。
基于服务等级(服务等级在第 11 讲雪崩(三)中有详细介绍)对所有的告警通知,启用电话报警规则,并且严格执行,具体如下表所示。
我们这样设计的原因,主要建立在这两个认识上:
第一,相信工程师,并且让工程师自己负责起来。这里主要指的是,相信工程师对它负责的服务发出的告警通知都是有效的,并且他是有能力和义务来做好这一点的,所以,所有的告警通知都需要经过严格的电话告警规则来认真对待。
第二,通过分级机制来提高处理效率,避免频繁骚扰。虽然告警通知都是有效的,但是故障越大,告警通知就一定会越多。那么当频率非常低的时候,大概率是偶发性的问题,我们可以让告警信息进入工单,后续再处理,不需要立即打断工程师的工作。同时,利用服务等级信息来唯一确定电话告警的阈值,在工程师的工作效率和故障处理的实时性之间,找到一个平衡点。
在方案评估会议时,工程师们纷纷表示,如果电话报警太多,能不能自己来调整服务的电话报警阈值,我们给出的回复是不行的,具体的解释如下。
自定义阈值很容易出现阈值设置不合理的情况,导致覆盖率降低;同时自定义报警阈值会让工程师们,对于报警通知重要程度的理解不一致,增加沟通的成本。
如果一定要调整阈值,只能通过调整服务等级的形式来实现。但是如果调低服务等级,该服务在运维层面的资源保障也要跟着降低,我们主要通过这个机制来进行制衡,通过降低服务等级来提高报警阈值的问题。
电话报警多,说明需要清理服务告警的时候到了。
后来,这个告警方案实行的第一周,所有人确实都比较辛苦,报警电话很多,经常需要打断工作进行处理。但是 2 周后,电话报警就非常少了,我们开始进入了一个正常的状态,并且这个机制是持续、实时生效的,这样服务的告警问题就彻底解决了。
告警系统的设计经验
讨论完上面的告警系统治理案例,接下来我会结合工作中对告警系统的设计,分享如下的设计经验。
首先,对于告警系统,“相信工程师,并且责任到人”和“利用服务等级信息,来建立告警规则”,这两点在我们的案例中讨论得比较多,就不再赘述了。不过,这里要特别强调一点,服务的等级信息,是我们对分布式系统,或服务运维、治理中最重要的元数据,是其他系统可以依靠的、非常关键的一个分级依据。
其次,为了避免告警通知的单点问题,如果服务的负责人没有及时处理,我们就可以依照组织架构逐级上升。比较推荐的一个告警信息的处理流程是,负责人在收到告警的电话通知后,在告警信息的通知群里面,点击“正在处理”,这样该类型的告警会自动抑制一小段时间,避免告警信息的过度骚扰。
再次,告警规则应该简单易懂,工程师看到告警信息,就能知道触发的原因,告警规则的可解释性对于告警的处理非常重要。这一点 Google 也说明过,他们当前还没有使用基于 AI 的告警规则,就是为了确保告警规则的可解释性。
最后,比故障更严重的问题是告警缺失。告警缺失会使我们错过处理故障的最佳时机,导致故障被放大。总之,作为一个工程师,如果让用户来告诉你系统出现故障了,是一个非常羞愧的事情,所以我们一定要比用户先知道。
我们都明白,故障是无法 100% 避免的,但是告警却可以保证不缺失。因为告警是多层网状覆盖的,其中一个地方的故障,往往会导致多个层面出现报警信息,除非所有层面都告警缺失了,才会出现问题。
比如,一台机器突然崩溃了,虽然我们的系统可以自愈,但是在应用层面,当时正在调用这台机器上服务的调用方,会由于调用失败而告警;在机器层面,监控机器存活性的程序会告警。所以,在故障复盘中,如果故障发生时,告警缺失了,这就是一个必须严肃讨论的问题,我们需要思考,出现告警缺失的原因,以及还有没有类似的报警缺失。
对于告警缺失的问题,我们还需要注意一点,就是告警系统自身的问题,比如告警系统出现故障了怎么办,它的告警通知由谁来发送。我们可以考虑做一个独立的程序,来监控告警系统,并且,这个程序有独立发送告警信息的通道,通过这个独立的程序与告警系统,相互监控和告警来解决这个问题。
总结
在这节课中,我们总结出了告警系统的三个评价指标:信噪比、覆盖率和转交率,这样你就有了评估一个告警系统是否高效的依据,可以利用它来评估你现在使用的告警系统。
接着,我们通过一个告警治理的真实案例,了解了告警治理的难点,以及如何一步步分析、权衡,最后解决这个问题,你可以把这个案例作为对照,去思考你当前告警治理的情况。
最后,依据告警系统的设计经验,你在设计一个监控系统时,就能游刃有余,少走很多弯路了。
思考题
你是否遇到过告警通知的信噪比非常低的情况?后来解决了吗?如果解决了,是如何解决的呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,134 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 故障(一):预案管理竟然能让被动故障自动恢复?
你好,我是陈现麟。
通过学习“监控”与“告警”这两节课的内容,你已经学会如何利用 Metric、Trace 和 Log 搭建一个可观测系统,去监控极客时间这样的分布式系统。并且知道了在系统出现故障时,职责明确的告警机制,可以在第一时间通知到相关的工程师。
但是,我们现在还不能掉以轻心,因为极客时间是 7 * 24 小时无间断为用户提供服务的,能掌控和发现故障还不够,如果故障出现了,我们还必须能快速恢复故障。所以本课我们一起来讨论另外一个非常重要的问题:如果系统发生故障了,我们应该怎么来快速恢复故障?
故障恢复是一个非常复杂的问题,这里我们首先要讨论的是:怎么理解故障,以及它的评估标准是什么?只有定义好问题,并且确定好标准,我们才能明确解决问题的方向。
如何理解故障及其评估标准
对于如何理解故障和故障评估标准的问题,我认为可以从两个方面去理解和思考。
首先,评价故障的标准一定不是有或没有。虽然我们不希望有故障发生,但这却是所有的工程师必须面对的问题。同时,我们不能出现故障就处理,没有故障我们就什么也不做,我们要积极地应对故障。在系统设计的时候,应该充分考虑到故障的存在,并且做好充分的预案,才能在故障发生时,将系统的影响降到最低。
既然我们必须面对故障,那么应该如何评估一个故障的水平呢?我认为有两个指标非常重要。
平均出现故障的频率:指平均多少时间出现一次故障,这个频率越低越好。
平均故障恢复的时间:指出现故障后,系统在多长时间恢复到正常状态,这个时间越短越好,并且,我认为这是一个更关键的指标。
我们可以这样来思考上面的两个指标,假设我们的系统可用性 SLA 是 99.99%,那么只要全年故障时间不超过 52 分钟,就是符合要求的。但是,对于这个 52 分钟的故障时间却有不同的情况如下表所示如果是在线用户数等其他情况一样的前提下1 个持续 52 分钟的故障和 52 个持续 1 分钟的故障,还有 3120 个持续 1 秒的故障,你认为哪一种情况对用户的影响最小呢?-
聪明的你一定会选择 3120 个持续 1 秒的故障,因为这样的故障只会导致用户的某几个请求失败,用户自己或者系统内部自动重试一下就好了,对体验的影响非常小。并且,这种情况用户一般是能接受的,因为在进出电梯等情况下,也会经常出现偶发的网络错误。
如果是 52 个持续 1 分钟的故障,那么用户会明确感知到系统出现故障。而如果是 1 次 52 分钟的故障,那么故障期间,所有的用户都不能使用系统提供的服务,其中的损失和影响是无法估计的。当然,这里并不是说平均出现故障频率这个指标不重要,如果频繁出现短暂的故障也会影响到用户的体验。
所以,我们通过上面的讨论,就得到了两个关键的结论:一方面我们应该在事前做好故障避免,降低平均出现故障的频率;另一方面,如果出现故障了,降低平均故障恢复的时间是非常关键的指标。
到这里,我们就知道了出现故障后,快速恢复是非常关键的指标。那么,具体应该如何快速恢复故障呢?
对于这个问题,根据我长期在系统稳定性建设方面的经验,我认为可以采用分治法。根据故障是由于宕机等被动原因,还是由于系统迭代过程中,人为引入等主动原因导致的,将故障分为被动故障和主动故障,然后我们再一一来讨论如何快速恢复。
由于故障恢复涉及的内容比较多,所以我们将用两节课的时间来讨论这个问题,这节课我将先和你一起讨论如何快速恢复被动故障。通过分析被动故障的来源,我们可以从中归纳出被动故障的特点,推导出处理被动故障的方法——预案调度,然后再通过一个案例,来讨论预案调度具体的实施方法,最后梳理被动故障的预案,和你分享我的经验。
被动故障的分析与思考
被动故障的来源
我们先一起来分析一下都有哪些原因会引发被动故障。依据我们对被动故障的定义,从用户的 App 发起请求到系统提供服务的过程中,被动故障的来源主要出现在以下四个地方。
DNS 解析问题:用户本地网络的 DNS 服务不能将我们的域名正确解析到 IP 地址。
网络连通性问题:用户已经解析到正确的 IP 地址,但是从用户网络到我们服务器的 IP 地址之间的网络慢或者不通。
系统内部的硬件设施故障:比如机器突然宕机,内部网线中断等。
系统依赖的各种第三方服务:比如 CDN 服务、短信网关、语音识别等第三方服务故障。
当我们结合平时工作中的案例去分析上述问题时,会发现被动故障有一些特点,首先是它每一次出现的原因都各不相同。比如 DNS 解析的问题,有可能是用户本地的 DNS 服务配置错误,也有可能是 DNS 服务器网络的问题,还有可能是 DNS 服务器的问题,并且这些原因都不受我们的控制,这也是被动故障的一个特点。
处理被动故障的思维方式
对于各不相同以及完全不受我们控制的原因,应该如何处理呢?其实这个问题在计算机领域有非常明确的答案:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。
由于出故障的地方不受我们控制,并且每一次故障的原因可能都不相同,通过 case by case 的方式,来一一来解决是不可能的。但是也正是因为不受我们的控制,所以这些出问题的地方都有相对标准的服务和方案,不论是服务还是硬件,不会随着业务的变化而快速变化,并且故障来源的数量是非常有限的。
那我们就可以从细节中跳出来,从更高层次的角度来思考这些问题,对于每一个故障来源,我们都可以准备多个预案,然后通过增加一个中间层来进行自动调度,对外屏蔽这些问题,从而达到快速恢复故障的目的,我们将这个方法称之为预案调度。
通过预案调度来恢复故障
接下来,我们一起来讨论在实际工作中,如何通过预案调度来处理被动故障。就拿我亲身经历的一个真实故事来说吧,当时我们就是用了预案调度,解决了第三方服务 CDN 的下载故障,以后你处理类似问题的时候,也可以借鉴其中的思路。
公司在刚开拓东南亚业务的时候,因为那边的基础设施比国内要差不少,所以用户经常出现图片加载失败的情况,非常影响他们的体验。
当时,研发工程师们将国内、国外所有的 CDN 供应商都换了一遍,但每一家 CDN 供应商都或多或少出现过问题,比如 CDN 供应商 A 在越南的服务质量不错但是在泰国很不好CDN 供应商 B 则是在泰国的服务质量不错,但是到了马来西亚却很不好。并且工程师们还没有任何的办法来优化 CDN 的问题,因为这些都是第三方 CDN 供应商自身的问题。
那时候,我们也走了一些弯路,花了不少的时间去定位问题,通过客户端的日志分析出用户 CDN 图片加载失败的时间点、 URL 和当时域名解析的 IP 地址,然后推送给 CDN 供应商来优化。
这样做有一定的效果,但是需要注意两个问题,一是,我们只能在故障出现后去解决问题,这时用户已经被影响了,体验很不好;二是,导致故障的问题没有办法收敛。因为网络本身是动态的, CDN 供应商将一个接入点的网络质量优化好,等到下次外部网络环境发生变化,就又会出现新的问题。
结合上述思考,最后我们采用如下的方法解决了问题:对每一张图片都提供两个以上供应商的 CDN 链接返回给客户端,并且根据之前的网络访问数据,统计其网络质量,按照质量从优到低排序;客户端则依据返回的 CDN 列表,从优到低下载图片,直到下载成功为止。
现在我们来总结一下,通过预案调度解决被动故障的思路。首先,使用“对每一张图片都提供两个以上供应商的 CDN 链接,并且按质量从优到低排序”的方法,其实就是为每一张图片的 CDN 服务准备了多个下载预案,并且这些预案是有优先级的。
然后,使用“客户端依据返回的 CDN 列表,从优到低下载图片,直到下载成功为止”的方法,就是在客户端实现了一个,通过 CDN 链接下载图片的预案调度层,它依据当时的网络情况,择优选择 CDN 供应商,来提供图片的下载服务。就这样,这个问题就彻底解决了,一个核心思考点是:虽然每一家 CDN 供应商都有下载失败的情况,但是一张图片几乎不会出现,所有 CDN 供应商都下载失败的情况。
通过上面的例子,我们将预案调度的思路总结为下图。之前,我们的业务系统直接调用标准服务,在增加调度层之后,业务系统直接调用调度层,不需要关心具体的标准服务(即预案)。所以我们可以通过增加调度层的方式,来屏蔽各种预案之间的差异,并且可以在不同的预案之间进行自动最优的调度。
被动故障的预案梳理
通过上面的学习,你已经掌握了通过预案调度来解决被动故障的思路,这个时候,你一定想知道,有哪些场景适合通过预案调度,来快速恢复被动故障呢?
我从被动故障来源的维度,给你总结了解决每一类被动故障的预案列表,你可以先通读,理解一下预案列表的设计思路,在有需要的时候,可以直接查询这个列表,具体的预案细节如下。
第一,对于 DNS 解析问题,因为是解析服务失败导致的,所以我们可以通过提供不同的域名解决服务来解决问题,依据可靠性从高到低依次为。
第二,网络连通性的问题与网络链路有关,我们可以通过提供不同接入的网络链路来解决问题,依据实现成本从低到高依次为。
第三,对于系统内部的硬件设施故障的问题,因为设备是我们可以控制的,所以处理方式比较简单,直接通过冗余设备的方式来解决。
第四,对于系统依赖的各种第三方服务,我们可以通过提高服务供应商的质量和数量来解决问题,依据实现成本从低到高依次为。
并且对于“DNS 解析预案”和“网络连通性预案”这前两个预案来说,它们都是内置在客户端的,我认为比较好的方案是,将这两个层面实现一个统一的调度层,这一个调度层不仅用来快速恢复故障,还可以通过 App 端对网络性能数据的统计,实时提供当前网络性能最好的接入服务和 IP 地址。
对于“系统内部的硬件设施预案”,它的调度层可能不需要我们再次实现,因为系统的高可用已经覆盖了这个部分。比如机器宕机会导致上面跑的服务实例都挂掉,系统的服务注册发现模块会实时摘除这些实例的 IP 和 Port 信息,并且通知给相关的调用方。
对于“第三方服务的预案”,它的调度层可以实现为,通过实时的数据统计,来为我们的系统选择质量最好的第三方服务并且使用其服务。
通过上面被动故障的预案梳理,我们掌握了不同故障的预案都有哪些,这个不一定全面,如果你有更好的方案,也可以继续补充,这节课更重要的目标是讨论处理故障的思维方式。
总结
本节课,我们先讨论了应该如何理解故障,以及故障的评估标准:平均出现故障的频率和平均故障恢复时间。在 SLA 一定的情况下,平均故障恢复时间越短,对用户体验的影响就越小,所以快速恢复故障是一个非常关键的目标。
接着,我们分析了被动故障的来源都有哪些,我们发现这些故障完全不受我们的控制,可能每一个 case 的原因都不相同,我们很难通过穷举来消除故障,因此我们决定对可能出现故障的地方增加多个预案,通过增加一个中间层来进行调度,对外屏蔽这些问题,从而达到快速恢复故障的目的。
我们还详细讨论了被动故障各个来源的预案,在调度层通过对各个预案的实时数据统计,不仅能提供可用性非常高的服务,还可以为系统提供最优质的服务,这个是提供高质量服务非常关键的优化点。
思考题
在工作中,你是否用过“通过增加一个中间层来解决复杂问题”的思路,你用它解决了哪些问题?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,103 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 故障(二):变更管理,解决主动故障的高效思维方式
你好,我是陈现麟。
通过上一节课的学习,你已经理解了系统故障的评估标准,并且明白了在 SLA 一定的情况下,平均故障恢复的时间越短,对用户体验的影响就越小,所以快速恢复故障是一个非常关键的目标。接着,我们采用分治法,将故障分为被动故障和主动故障,讨论了如何通过预案调度快速恢复被动故障的策略。
相信你已经对被动故障如何处理心中有数了,但是,我们对于故障恢复的处理还远远没有结束。根据极客时间以往的故障报告进行分析,我们会发现很多故障都是在系统迭代过程中,人为引入的主动故障,比如发布新版本服务引入的 Bug 和崩溃等。所以,在这节课中,我们就继续来学习,如何处理由于主动原因导致的系统故障。
主动故障的分析与思考
首先,我们一起来思考一下,主动故障是否也可以通过预案调度的形式来快速恢复呢?答案一定是不可以的。我们来回忆一下被动故障的特点,虽然出现被动故障的地方,不受我们的控制,但是它有相对标准的服务和方案,不会随着业务的迭代而快速变化,所以处理被动故障时,我们准备多预案的成本是可控的。
而主动故障是工程师们在业务迭代过程中,人为引入的故障,如配置错误、代码 Bug 等,它来自于我们的业务系统,我们不可能为了做预案,同时组织两个不同的研发团队,分别开发同一个业务系统,这个多预案的成本实在是太高了。
如果预案调度的形式不可用,那么我们应该如何快速恢复主动故障呢?
当我们去分析主动故障时,会发现每一次发生主动故障的原因都不太相同,比如需求理解错误、逻辑考虑不全面这些不可穷举的问题。但是我们可以通过分析问题的根源确认一点,主动故障主要是工程师们在业务迭代过程中引入的,也就说明如果业务系统没有发生迭代变化,就不会发生主动故障。
主动故障的来源与处理方法
通过对根本原因的分析,下面我们就可以从工程师的哪些行为,会导致系统发生变化的角度去思考了,这样问题就能很快收敛了。你可以想一想,在日常工作中,我们碰到的主动故障来源是什么,是否几乎包含在下面几类中。
程序发布变更指服务器、App 和 Web 等发布了新版本的程序和服务。
实例数目变更:指服务器新增实例和下线实例。
配置发布变更:指发布了新版本的配置。
运营策略变更:指举办了导致用户流量增长的运营活动,比如购买了新的推广广告等。
虽然发生主动故障的具体原因各不相同,但是它的来源却只有这几个。所以对于这种情况,我认为快速恢复主动故障,可以从变更入手:出现主动故障的时候,如果我们没有足够的信息,去判断当前的故障出自什么原因,我们就应该第一时间定位故障可能存在的范围,比如某一个服务或者数据库,然后我们就去看这个服务或者数据库最近是否有相关的变更,依据变更信息来确定故障恢复方案。
但是,在突发线上故障这种高压力、争分夺秒的情况下,我们应该如何准确、快速获得主动故障相关的服务和数据库的最新变更呢?
如果只是在故障现场去询问工程师显然是不行的,可能会出现询问的人不全、回答的信息缺失或者不正确等问题,这会对故障的快速恢复造成非常严重的影响,甚至还可能出现更大的故障。这一类信息的收集、展示和查询需求是非常适合用管理系统来解决的,所以,一个自动化的变更管理系统是非常有必要的。
变更信息的管理
上文中的讨论,让我们明确了变更管理系统可以收集,整个分布式系统所有的变更信息,为工程师提供变更信息展示和查询服务。它的实现相对比较简单,我们只需要在发布系统、配置中心和运营中心等,可能导致系统变更的运营和运维系统中,将每一次变更的信息丢入消息队列,变更管理系统就会消费消息队列的信息,然后做好展示和查询。具体的架构设计图如下。
变更管理系统有两个值得我们关注的地方。首先,变更信息最少要包括 4 个“什么”的内容:什么人在什么时间和什么地点,做了什么事情。如果还需要其他的信息,可以自行增加。
其次,变更信息最少要包含,时间维度的视图和支持按服务或系统维度的查询。因为一般来说,故障能提供给我们最关键的信息就是这两个:发生故障的时间和位置,所以我们需要通过这两个信息来定位相关的变更信息。
稳定版本的概念
有了变更管理系统后,我们就能基于变更信息快速处理故障了吗?其实,还有一个问题会影响我们对故障的判断和处理效率。
例如,你负责的某一个服务,在今天白天的低峰期有 10 个变更,系统一直正常,但是到了晚上的高峰期突然出现了故障,这个时候,你应该如何定位,是哪一个或哪几个变更导致的故障呢?如果要通过回滚来恢复故障,那么你应该回滚到哪一个变更呢?
你可以按时间倒序一个一个变更回滚,然后观察系统是否正常,但是这样非常低效。首先,每一次回滚都需要重新发布,其次,有一些系统故障就算已经回滚到正常版本了,它的恢复也是需要一些时间的,并且我们不能确定这个时间的长短,一般无状态的服务恢复时间会很快,有状态的服务则慢得多。所以,在每一次回滚后,你都需要等待一段时间,来确定是否恢复到正常版本了,有时,甚至需要回滚很多个版本,才能让系统恢复到稳定版本。
那么你一定在想,有没有更高效的故障处理方式呢?
更高效的处理方式是有的,在我的实践经验中,一个比较好的方法就是引入稳定版本的概念,出现故障的时候,如果定位到了引起故障的服务,首先回滚到上一个版本,因为最后一次变更导致故障出现的概率是非常大的,如果系统还没有恢复,就可以直接回滚到这个服务的稳定版本了。
对于稳定版本的定义,我们可以先基于公司的业务流量情况,定义出公司业务的高峰时段,然后将经历整个完整高峰时段的变更,标记为稳定版本,这个功能可以设计为变更管理系统的一部分。
例如,公司的业务高峰期是 19 点 - 22 点,那么所有在 19 点前发布,并且持续到 22 点,依然在提供服务的变更就是稳定版本,变更管理系统通过分析每一个变更的上线时间和下线时间,自动标记变更是否为稳定版本。
这里要注意一个关键点,一定要关注新的变更是否持续了整个高峰期,否则很有可能会出现在高峰期的时候,故障被回滚的变更版本,依然标记为稳定版本的情况。例如,在一个业务高峰期为 19 点 - 22 点的系统中,如果有一个变更是 16 点发布的,到了 20 点出现了故障,因为这个变更版本没有持续运行到 22 点,没有经历一个完整的高峰期,那么它就不能被标记为稳定版本。
故障恢复流程
有了变更管理系统和稳定版本这两个工具,再结合可观测性的监控系统,整个故障恢复的流程就变得简单和高效了,如下图所示。
通过可观测性的信息快速确定导致故障的服务。
回滚到上一个版本,观察故障是否恢复,如果恢复,结束流程,否则执行 3。
回滚到最新的稳定版本,观察故障是否恢复,如果恢复,结束流程,否则执行 4。
确认之前故障定位的服务是否准确,如果不准确,重新定位,然后执行 2如果准确则需要考虑该服务是否被其他的因素影响了比如网络、机器等这个需要具体问题具体分析。
这里还要特别强调一点,一般来说,我们的服务和系统等变更都是要求可回滚的,即向前兼容。当然,我们也可以容忍回滚的时候,新功能失效,但是老功能和数据不能因为回滚出现问题,这样在发布出现故障的时候,我们才能够通过回滚快速恢复。
其中,有一些变更设计成可回滚的成本非常高,那么对于这一类变更,如果选择不向前兼容的设计,那么上线前就要经过更严格的评估和测试,确保不会出现问题。
总结
本节课,我们先讨论了通过预案调度来快速恢复主动故障的办法是不可行的,因为我们不能对同一个业务开发两套系统,它的研发和协调成本实在太高了。
然后,我们通过分析主动故障的来源,将主动故障分为四个原因:程序发布变更、实例数目变更、配置发布变更和运营策略变更,这样主动故障的问题就收敛了。
最后,我们一起探讨了如何设计一个变更管理系统,如何来定义一个变更的版本为稳定版本,并且分析了基于变更管理和稳定版本,如何快速恢复主动故障的流程。
到这里,“分布式计算篇”的学习就结束了,提前预告一下,下一周我为你准备了期中测试,你可以抓住时间,好好复习下学完的知识,预祝你取得一个好成绩。
思考题
日常工作中,你碰到过最严重的一次故障是什么原因导致的?当时你是通过什么办法修复故障的?后来又采取了哪些方法,来避免类似的故障再次发生呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,99 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 分片(一):如何选择最适合的水平分片方式?
你好,我是陈现麟。
结束了“分布式计算篇”的系列学习,我们掌握了如何解决分布式系统中,无状态节点或服务之间内部的协调问题,利用这些知识和技术原理,你就可以轻松地构建、运维一个大规模无状态的分布式系统了,恭喜你,取得了一个值得庆祝的学习成果。
接下来我们乘胜追击,继续了解有状态分布式系统的相关知识和技术原理。在“分布式存储篇”,我们先解决单机存储和性能瓶颈的“分片”,再解决数据高可用的“复制”,然后讨论如何在已经“分片”和“复制”的数据集上实现 ACID 事务,最后从实践回归到理论,讨论分布式系统最核心、最重要的两个问题:一致性和共识,进一步提高你对分布式技术的理解和认识。
从这节课开始,我们将用两节课的时间来讨论,如何通过“分片”技术,突破单机存储和性能瓶颈,让分布式系统的计算和存储能力可以线性扩展。本节课,我们先梳理常用的分片策略,然后讨论水平分片的算法,并对其优缺点进行比较,最后从理论到实践,分析这些分片策略在实际工作中的应用场景。
分片策略讨论与梳理
在 2000 年左右,由于互联网的快速发展,用户数据爆炸性增长,如何存储和管理这些海量的用户数据成为了一道难题,当时摆在工程师面前主要有两条道路。
第一条是垂直扩容,即 PC 机扛不住换小型机,小型机扛不住换大型机,大型机扛不住换超级计算机,通过不断提高机器的配置来应对数据的增长。但是,这条道路会受到材料的物理极限、制造的工艺水平和使用成本的限制,不是一条可持续的道路。
另一条是水平扩容,即通过将数据进行分片,分散到不同的 PC 机上,每一个 PC 节点负责一部分数据的存储和计算,来应对数据和成本的增长。这一条道路是由 Google 在 2000 年代的三篇论文 GFS 、MapReduce 和 BigTable 开启的,并且成为了解决数据激增问题的事实标准。
那么对数据进行分片的策略,主要有三种:水平分片、垂直分片和混合分片,具体如下图所示。
我们从图中可以看到,水平分片和垂直分片是通过数据切分的操作方向来区分的,而混合分片是它们的组合体。为了帮助你更好地理解,本节课我们将详细讨论水平分片的知识、原理和应用,下一节课我们再讨论垂直分片和混合分片的内容。
水平分片策略介绍
结合水平分片的原理,你是不是也联想到了负载均衡,其实我们在第 5 讲负载均衡中也讨论过这个问题。对于有状态服务,水平分片和负载均衡是解决单机存储与性能瓶颈问题中,相辅相成的两件事情,从流量角度来看,是负载均衡,从数据存储角度来看,是水平分片。
水平分片算法有两个最关键的因素,一是,如何对数据进行划分,即数据划分,二是,分片是否支持动态分裂与合并,即数据平衡。所以接下来,我们将从这两个维度来讨论水平分片策略。
数据划分
数据划分要解决的问题是,将整个数据空间划分为多个分片空间,它主要有两种方式,基于模运算划分和基于范围划分。基于模运算的划分,在“负载均衡”篇中 Hash 负载均衡策略的部分充分介绍过了,这里不再重复。下面我们重点介绍基于范围的划分,如下图所示,它分为基于关键词划分和基于关键词的 Hash 值划分两种方式。
这两种划分方式都是给每一个分片,分配一个确定的数据范围,在这个数据范围内的所有数据,都属于这个分片。基于关键词划分和基于关键词的 Hash 值划分,二者唯一的区别在于,前者是直接利用关键词进行划分,而后者是利用关键词的 Hash 值进行划分。虽然只有这一个区别,但是却会深刻地影响数据的分布规律,所以我们接下来将重点讨论。
基于关键词划分的好处是,分片后数据的分布依然保留了关键词的顺序,我们可以方便地进行区间查询。假如我们在设计一个中国的公民数据库,将地址信息作为分片的关键词进行划分。如果我们需要查询“北京市海淀区”的所有公民,将查询区间设置为 [北京市海淀区 , 北京市海淀区] 即可,因为所有“北京市海淀区”的公民信息是连续存储在一起的。
但是基于关键词划分也会带来问题,即数据分布不均匀和访问的热度不均匀。比如在上文公民数据库的例子中,如果我们按省级行政单位进行划分,每一个省一个分片的时候,你会发现存储西藏数据的分片只有 300 多万条数据,而广东分片则有 1 亿 2000 多万条数据,这就会导致数据分布不均匀。
而数据分布不均匀也会导致访问的热度不均匀,比如,在对数据的访问频率相差不大的情况下,访问广东分片的热度要远远高于西藏分片的热度。并且,如果基于自增 ID 或者时间等关键词对数据进行分片的时候,即使数据是均匀分布的,对于一般的业务场景来说,往往新产生数据的访问热度,也是远远大于历史数据的,这也会导致访问的热度不均匀。
为了解决基于关键词划分带来的问题,我们可以对它的分布规律再进行一些调整。比如,可以对广东分片的数据进一步分片,分为“广东广州”、“广东中山”等多个分片,西藏分片可以与周边的分片合并为一个分片。而对于基于自增 ID 或者时间戳等原因,导致的访问冷热不均匀的关键词,则避免作为数据划分的关键词。
到这里,你会发现基于关键词划分,很明显会使数据分布和关键词自身的分布保持一致。在我们不了解数据分布的情况下,选择哪一个字段作为关键词是一个难题,有没有一种好方法来解决呢?
其实基于关键词的 Hash 值划分就可以解决这些问题,它通过对关键词进行 Hash 运算,然后基于计算后的 Hash 值范围对数据进行划分,一个好的 Hash 算法可以处理数据倾斜并让它均匀分布。这里我们可以理解为通过 Hash 运算,去除了关键词数据分布的业务属性,从而解决了数据分布和访问的热度不均匀的问题。
但是这里依旧没有银弹,基于关键词的 Hash 值划分,带来了数据分布和访问热度更均匀的优点,但同时,它也失去了基于关键词的顺序性,不能方便地通过关键词进行区间查询了。并且,在极端情况下,如果一个关键词的访问热度非常大,那么基于关键词的 Hash 值划分也完全不起作用了。
这里要特别说明一点,我们可以将一致性 Hash 算法理解为基于关键词的 Hash 值划分的一种实现。
数据平衡
根据数据分片是否支持动态的分裂与合并,我们可以将水平分片的数据平衡方式分为静态分片和动态分片。其中静态分片是指在系统设计之初,数据分片的数目和区间就预估好了,数据划分后不能再变化,而动态分片则可以在运行时,根据分片的负载和容量做调整。
对于静态分片,由于分片区间在运行时不能再调整,所以数据划分时一定要谨慎考虑。如果我们对数据的分布有足够的了解,并且数据的分布是比较稳定的,就可以采用基于关键词的方式,通过选择合适的关键词对数据进行划分。例如上文中提到的中国公民数据库的例子中,对于中国各省市的人口分布,因为我们有统计数据支撑,并且人口分布的数据非常稳定,所以就可以基于地址信息,并且结合数据的分布进行划分了。
在我们对数据的分布不了解,或者数据的分布不稳定的情况下,如果要采用静态分片的话,比较稳妥的方式是,采用基于关键词的 Hash 值的方式对数据进行划分,通过 Hash 算法解决数据分布和访问的热度不均匀的问题。
而对于动态分片,因为在运行时,分片区间是可以进行分裂和合并的,所以我们不用担心不了解数据分布,而导致分片区间划分不合理的情况,也不用担心在分片区间划分后,数据的分布发生变化,使分片区间不合适的问题。总而言之,动态分片与基于关键词的划分,往往是一个比较好的组合方式,它避免了基于关键词划分的问题,还保留了数据基于关键词有序的优点。
但是,在基于关键词的划分中,基于自增 ID 或者时间戳等原因,导致的访问冷热不均匀的问题,即使是在动态分片中也不能很好地解决,因为数据的热点往往集中在最新的一个分片区间上。而基于关键词的 Hash 值划分的方式,则可以很方便地将最新的热点数据分布到多个分片上,很好地解决这个问题。
另外,动态分片存在冷启动的问题。当一个基于动态分片的存储系统启动时,通常是从一个分片开始,当数据量不断增长后,再动态进行分裂。在第一次进行分裂前,所有的读写请求都由第一个分片来进行处理,而其他的节点则都属于空闲状态。关于这个问题,一个比较好的解决方式是,动态分片在冷启动时,预分裂为多个分片来缓解。
这里还要特别强调一点,像 Codis、Redis Cluster 这样,预先分配固定数据量 slot slot 不能合并和分裂,但是可以通过将 slot 迁移到新增的节点上,进行水平扩容。比如预先分配 1024 个 slot在 3 副本的情况下,刚开始运行的时候,可能是 3 个节点,每个节点上分布全部的 1024 个 slot 。在数据量增大的情况下,可以增加新的节点,将一部分 slot 迁移到新的节点上,实现水平扩展。
在课程中,由于预先分配 slot 后,就不能再进行合并和分裂了,所以我们将预先分配固定数据量 slot 归类为静态平衡方案。它能提供有限的水平扩容能力,最大程度是一个节点运行一个 slot ,但是当一个 slot 出现非常极端的数据热度和访问热度时,不能再进行分裂和水平扩容。
水平分片策略分析
了解了水平分片的两个维度,数据划分策略和数据平衡策略后,我们将常见的数据划分策略和动态、静态的数据平衡策略交叉组合,一一来讨论它们的优缺点和应用场景,具体如下表。
总结
在这节课中,我们先讨论了数据分片的原因,了解了数据分片策略有三个类型,水平分片、垂直分片和混合分片,这样你就对整个数据分片有了一个全局的了解。
接着,我们介绍了水平分片策略的两个关键维度:数据划分和数据平衡,通过对这两个维度的讨论和分析,你可以基于业务特点,清晰地选择适合你的水平分片策略。
最后,我们对水平分片策略的所有算法和应用场景进行了全面的总结和对比,你可以进一步地理解水平分片策略了。同时,它也是一个非常有价值的结论,一个非常方便的知识库,在有需要的时候,你可以直接查看。
思考题
在极端情况下,如果一个关键词的访问热度非常大,我们有什么办法对这个关键词进行负载均衡呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,100 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 分片(二):垂直分片和混合分片的 trade-off
你好,我是陈现麟。
通过了解水平分片策略中,关于数据划分和数据平衡的原理和知识,我们可以基于极客时间的业务场景,选择合适的数据划分和数据平衡的方式,组合出最佳的水平分片策略。
而在一些数据分析的场景中,一行数据往往有非常多的字段,我们在计算时,却只需要一列或者几列的数据。这时基于水平分片策略,虽然能解决数据容量的问题,但是却没有充分利用数据分析场景的业务特点进行优化。那么是否有针对这个场景设计的数据分片方式呢?
答案是肯定的,数据的垂直分片与混合分片,比起水平分片来说,能更好地满足数据分析场景。所以在本节课中,我们将一起来讨论数据分片的另外两种方式:垂直分片与混合分片,思考一下垂直分片与混合分片,是如何利用数据分析场景的业务特点,来做数据存储优化的。
我们会先讨论垂直分片策略的应用场景和技术原理,接着分析混合分片策略是如何结合垂直分片与混合分片,在读写和水平扩展之间达到最优平衡的,最后再对讨论垂直分片时,引入的两种存储方式:行式存储和列式存储,进行对比和总结。
垂直分片策略
垂直分片策略和水平分片策略都是对数据进行分片,但是它们的思路却截然不同。水平分片策略将整个数据集的条数作为划分的对象,每一个分片负责处理一定的数据条数。而垂直分片策略则是将数据 Schema 的字段集个数作为划分的对象,每一个分片负责处理一个或几个字段的全部数据,具体如下图所示。
从上文的描述中不难看出,对于数据的水平扩展能力,垂直分片策略是很有限的。因为数据 Schema 的字段个数是非常有限的,常见的字段个数从几个到几百个不等,即使一个字段一个分片,在字段数少的数据集上,水平扩展能力也是非常差的。关于这个问题,可以将垂直分片与水平分片策略组合起来解决,我们会在下一部分的“混合分片策略”中讨论。
这里你会发现一个很有意思的地方,如果垂直分片策略的处理方式为一个字段一个分片,那么垂直分片策略就等价于列式存储了,所以列式存储是垂直分片策略的一种特殊情况,也是最常见的情况。接下来,我们就以列式存储为例,从它应用最广泛的大数据分析场景,来讨论垂直分片策略的特点,当然这些特点在垂直分片策略中依然生效。
我们先来解释下大数据分析场景,它是指从用户的行为数据中获得新的洞见,来改进我们的产品和运营方式。大数据分析场景的数据处理一般有以下的特点:
宽表存储,按列读取:数据往往以宽表的形式存储,一个表上百列,但是一次分析往往只关心一列或者几列。
读多写少:一次写入,多次读取,几乎不更新。
数据量大:大数据会存储全站的所有数据,包括日志和数据库内的数据,并且会持续增加。
查询无规律,不能索引覆盖:在分析场景中,我们会通过各种维度和组合,来统计和分析数据,所以这些查询方式是无规律的,不可能全部通过索引来覆盖。
由于大数据场景存储和计算的数据非常大,所以存储成本和计算性能是非常核心的设计指标,现在我们就来分析一下,列式存储是如何利用数据分析场景的特点,来达到低成本、高性能的。
第一,对于宽表存储,按列读取的场景,如果采用行式存储,当我们只需要读取一列数据的时候,可以按行顺序读取整个宽表所有列的数据,但是这会导致读取的数据量放大上百倍;或者我们可以跳着只读取所需列的数据,这样读取的数据量不会放大,但是读取数据的方式就从顺序读取变成随机读取了,这会增加非常多的寻址操作。并且,因为不能充分预读,在很大程度上,会降低磁盘的读性能。特别是对于机械磁盘来说,随机读取导致的寻址操作是毫秒级别的时延。
第二,读多写少的场景,会减少列式存储对写性能的影响。一般来说,数据写入存储系统是以行的形式写入的,而列式存储会导致一行数据的写入操作,按字段拆分为多个写入操作,使写入放大。不过,这个问题可以进行一定的优化,并且由于分析场景的数据写入模式是读多写少,所以不会影响整个系统性能的设计目标。
第三,因为数据量大,并且会持续增加的特点,要求存储系统能进行非常高效的压缩,降低存储数据的容量。那么我们先来分析下,列式存储是如何利用业务特性,进行数据压缩和提升性能的。
首先,在列式存储中相邻的数据类型是一致性的,并且通常会出现前缀一样,甚至完全相同的数据的特点,比如在用户的地址信息中,同一个地方的用户,省市县都是完全相同的,这非常适合使用 RLE 压缩、前缀压缩和字典压缩等压缩算法去压缩。
这里我们介绍一下字典压缩算法,其他的算法也是类似的思路,就不再一一介绍了。字典压缩算法的思路是,在数据重复度比较高的情况下,对数据采用字典重新编码,来减少数据的大小,具体见下图。
其次,虽然列式存储通过数据压缩大大提高了存储效率,节省了存储成本,但是与原始数据的存储相比,在写入和读取数据时,需要进行压缩和解压的操作,这需要消耗 CPU 来进行计算,所以,数据压缩其实是利用 CPU 资源来换取 IO 资源。
不过,在数据分析场景中,这是一个非常值得的选择,因为压缩算法在减少数据大小的同时,也减少了磁盘的寻道时间,提高了 I/O 性能,因为减少了数据的传输时间,并且提高了缓冲区的命中率,导致这些环节中得到的收益,能轻易地补偿压缩数据带来的额外 CPU 开销。
第四,如果熟悉数据库索引设计,你应该知道,数据库虽然有 Hash 索引或位图索引,但是最常见的索引模型是,将被索引的一个或多个关键词作为 Key 按一定规则进行排序Value 为行数据主键的指针,然后我们可以通过二分查询或 B+ Tree 进行查询,查到索引的关键词后,通过主键的指针找到行数据。
而对于大数据场景来说,经常需要读取一列或者几列中的大量数据、全表数据,那么列式存储通过按列顺序存储、按需读取和高效压缩,可以使按列读取的性能大大提高。其中,主键所在的列是有序的,其他列的读取性能也非常不错,可以理解为数据即索引,所以一般来说,列式存储系统对二级索引依赖不大,列式存储可以方便地应对查询无规律,不能预先建立索引的情况。
到这里,我们会发现,架构设计总是依赖业务场景的特点来做取舍,所以我们说,没有完美的架构,只有完美的 trade-off列式存储其实是牺牲了按行写入的性能去换取按列读取性能的 trade-off。
混合分片策略
在上文中,我们分析出了数据 Schema 的字段个数是非常有限的,特别是在字段数少的数据集上,完全依赖垂直分片策略,解决数据的水平扩展是不现实的,所以我们可以将垂直分片策略和水平分片策略结合起来解决这个问题。
根据这两种策略的组合顺序,可以将它们分为垂直水平分片策略和水平垂直分片策略。前者先进行垂直分片,再进行水平分片,而后者先进行水平分片,然后再进行垂直分片,具体方法如下图所示。
我们可以从图中看出,垂直水平分片策略就是垂直分片策略的水平扩容版本。对于水平分片策略,我们通常会选择主键进行水平分片,这样主键的列在整个存储系统中是有序的,垂直水平分片策略的数据分布特性和优缺点,与垂直分片策略完全相同,这里就不再重复讨论了。
而水平垂直分片策略更像是,水平分片策略和垂直分片策略的结合体,它对于整个数据集来说,一般是主键先利用基于关键词划分的水平分片策略,将数据集成不同的分片,然后对一个分片内的数据再进行垂直分片。
这样带来的好处是在一个水平分片内,依然按列式存储来存储数据,所以它有列式存储按列读取数据,高效和压缩比高的优点。在按行写入和读取多列的时候,都在一个数据分片上,大大地减少了网络 IO ,要知道在大规模的数据处理系统中,网络 IO 有可能是整个系统的瓶颈,同时,也能将一些分布式事务变成本地事务,提升系统的处理效率。
总体来说,水平垂直分片策略不仅保留了列式存储的优点,而且将多列操作控制在一个数据分片上,减少了网络 IO 和分布式事务是混合分片策略常见的方式Google 的 Dremel 数据库就采用了这种分片策略。
行列存储比较
在介绍水平分片、垂直分片和混合分片这三种分片策略的过程中,我们引入了行式存储和列式存储的讨论,并且我们发现这是存储引擎非常关键的一个选择,所以,最后我们来总结和分析一下,它们的优缺点以及应用场景,具体如下表。
总结
本节课,我们讨论了垂直分片策略在按列读取时的 IO 优势、数据压缩方面的存储优势和数据自动索引的查询优势,当然这些优势都是付出了代价的。这样你就深入了解了垂直分片策略的特点,并且掌握了在架构设计时,如何根据业务场景进行取舍。
接着,我们解决了垂直分片模式水平扩容差的问题,了解了混合分片策略的两种模式:垂直水平分片策略和水平垂直分片策略,其中垂直水平分片策略是垂直分片策略的水平扩容版本,而水平垂直分片策略,是水平分片策略和垂直分片策略的结合版本。
然后,我们对行式存储和列式存储,进行了全面的对比和总结,这样你就进一步地理解了行式存储和列式存储的优缺点以及适用场景。同时,它也是一个非常有价值的结论,一个非常方便的知识库,在有需要的时候,你也可以直接查看。
通过对各种策略不同优缺点的讨论和对比,希望你能明白,架构设计总是依赖业务场景的特点来做取舍,没有完美的架构,只有完美的 trade-off。
思考题
你在工作中经常接触的数据库系统,是行式存储还是列式存储呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,135 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 复制(一):主从复制从副本的数据可以读吗?
你好,我是陈现麟。
通过学习“分片”的内容,我们使用分片技术,让数据按一定的策略分布到多台机器上,解决了极客时间用户量快速增长,导致存储或处理的用户数据量超过单台机器限制的问题。
但是,我们还不能高兴得太早,如果现在有一台提供数据服务的机器,由于宕机、网络不通等原因不可用了,那这一台机器上的所有数据分片就都不能被访问,这对于极客时间要求 7 * 24 小时提供服务的系统来说是不能接受的,而这就是我们工作中经常会涉及的高可用问题的场景。
那么从这节课开始,我们将一起花三节课的时间来解决这个问题。这一节课,我们先讨论让存储服务高可用的思路,接着讨论具体的解决方法,即数据复制的三种方案,最后学习第一种主从复制的基本原理,另外两种数据复制的方案在后面的两节课程中我们再来讨论。
如何让存储服务高可用
通过分析上文提到的场景,我们可以迅速锁定这是存储服务高可用的问题,解决高可用问题通常有两个思路:
第一种思路是避免故障出现。我们通过深入细节,一个一个去消除可能导致故障的原因,从而避免故障的出现,比如停电会导致宕机,那么我们就增加备用电源。但是这样,我们会遇到两个无法确定的问题:
我们可能无法穷尽所有的可能性,如果一个意料之外的问题出现了,故障就会发生。
我们虽然知道某些故障的原因,但是无法控制,比如机房会因为海啸、台风等自然不可抗力原因而宕机,在一定成本范围内,我们没有好的办法来防范。
而第二种思路则恰恰相反,接受故障随时可能出现的事实,通过冗余的方法让系统在故障发生时,也能够正常提供服务。这也正是第 15 讲“被动故障恢复”中,通过多预案冗余来解决问题的思路。其实,只要我们能够接受提供的冗余机器和人力成本,那么冗余就是值得优先考虑的方案。
数据复制的三种方案
通过上述的思考和讨论,本课开头提出的极客时间存储服务的高可用问题,就变成了下面两个子问题:
1. 我们有没有办法让提供数据服务的机器永远可用?-
2. 我们将每一份数据都复制到多台机器上,让它们都能提供相关的数据服务,这个成本我们能接受吗?
对于目前的计算机系统来说,第 1 个子问题的答案显然是否定的。
而第 2 个子问题的解决其实也非常困难,虽然我们能接受数据复制的成本,但是这个成本真的非常大。首先,数据复制导致的相关硬件成本是成倍增长的。其次,由于数据是持续变化的,导致复制操作不能一次完成,我们必须持续将这些变化复制到它所有的节点上,这就给分布式存储带来一个非常大的麻烦:在数据复制的过程中,由于节点可用性和网络中断等各种原因,可能会导致不同节点的数据不完全相同,这就是数据的一致性问题。
数据一致性问题是伴随整个分布式存储发展的技术与理论,它是分布式存储的核心问题,在后面“一致性与共识”的课程中会有深入的介绍。
虽然数据复制的成本和复杂度非常大,但是为了让存储服务高可用,我们也别无选择。下面我们先介绍一下数据复制的一些基本概念。
对于一个数据集来说,每个保持完整数据集的节点我们称为副本。如果一个副本接受外部客户端的写请求,并且这个副本在新数据写入本地存储后,通过复制日志和更改流将新数据发送给所有的副本,那么我们就将接受写请求的副本称为主副本,其他的副本称为从副本,从副本可以接受读请求。
于是,基于是否有主副本,有一个还是多个主副本,我们可以将数据复制的方案分为以下三种:
主从复制:整个系统中只有一个主副本,其他的都为从副本。
多主复制:系统中存在多个主副本,客户端将写请求发送给其中的一个主副本,该主副本负责将数据变更发送到其他所有的主副本。
无主复制:系统中不存在主副本,每一个副本都能接受客户端的写请求,接受写请求的副本不会将数据变更同步到其他的副本。
主从复制的工作流程
这节课我们先来了解主从复制,主从复制就像是一个主人带一堆的仆从,主人能接收外面的信息,仆从不能接触外面的信息,主人在接收到外面的信息后,按照一定的策略将外面的信息完整分享给仆从们。
主从复制是我们工作中最常见、最容易理解的复制方案,比如我们接触最频繁的缓存系统 Redis、关系数据库 MySQL 和 PostgreSQL、非关系数据库 MongoDB 和消息队列 Kafka 都内置支持主从复制,它的工作流程如下图。
关键选择:同步复制 OR 异步复制
主从复制的工作流程,主要就是将变更数据从主副本复制到从副本,但是这里有一个非常关键的选择:主副本在接受外部客户端的写请求,将新数据写入本地存储后,是同步等待从副本也将新数据写入本地存储后,才回复客户端写入成功,即同步复制;还是立即向客户端回复写入成功,即异步复制呢?具体的过程如下图。
我们可以从图中看到,主副本在处理写请求的时候,会等待配置同步复制的从副本 1 确认成功后,才返回客户端。而对于配置异步复制的从副本 2主副本不会等待它的确认就直接向客户端返回写入成功了。
这里我们来举一个例子,比如我在极客时间上更新了头像后,如果你立即去查看我的头像,那么可能会出现以下三种情况:
读主副本:你从主副本去读取我的头像,因为头像就是从这个副本写入的,所以你查询到的是我刚刚更新的头像。
读同步复制的从副本:你从从副本 1 去读取我的头像,因为头像的写入请求会同步等待这个从副本复制完成,所以你查询到的是我刚刚更新的头像。
读异步复制的从副本:你从从副本 2 去读取我的头像,因为头像的写入请求和这个从副本的复制操作是异步的,写入请求成功不能保证该副本数据复制完成,所以你有可能查询到我刚刚更新的头像,也有可能是上一次更新的头像。
了解完同步复制和异步复制的工作流程后,你可能觉得它们之间差别也不太大,只是在主副本处理客户端写请求时,是否等待从副本同步完成后再返回客户端。但是,这两种不同的复制方案却对系统可用性的设计有着非常大的影响。
如果我们选择同步复制,那么在数据更新的时候,主副本都需要等待从副本写入成功。正常情况下这个时间非常短,在 1 秒钟内就可以完成,但是由于主、从副本分别运行在不同的机器上,可能出现网络延迟、中断和机器故障的情况。因此数据从主副本复制到从副本的时延就变得不可预测,可能数秒或者数十秒,甚至写入失败。
例如,一个用户在更新头像时,如果一个从副本突然宕机,那主副本就会迟迟收不到这个从副本同步完成的通知。由于是同步复制,系统就不能向用户返回更新成功的提示,待等待超时后,只能提示用户头像更新失败,这样会非常影响系统可用性的设计。
但是它也有优点,由于所有的数据都是同步从主副本复制到从副本的,所以主、从副本都有最新的数据版本,它们都能对外提供读服务,并且数据都是正确的,即系统的数据是强一致性的。
如果我们选择异步复制,在数据更新的时候,主副本写入成功就会返回成功,不会同步等待从副本是否写入成功,数据变更后通过异步的方式进行复制。由于数据的更新操作不依赖从副本,所以不受网络和从副本机器故障的影响,写入性能和系统可用性会大大提高。
但是由于主、从副本间的数据变更不是同步复制的,所以从副本上的数据可能不是最新的版本,那么就会有两个问题。
首先,当主副本突然故障时,主副本上写入成功,但是还没有复制到从副本的变更就会丢失,这种情况在数据正确性要求高的场景里是不可以接受的。比如你在极客时间的 App 中充值了 100 元,充值请求就会将余额增加 100 元的变更写入主副本。在数据更新还未同步到所有从副本的时候,主副本突然宕机了,这个时候,我们会将其中的一个从副本切换为主副本,以便正常对外提供服务。
但是由于主、从副本间的数据变更不是同步复制的,现在所有的副本都没有收到余额增加 100 元的数据变更,那么你就会发现刚刚的 100 元已经花出去了,而余额中并没有增加 100 元,这个时候你一定会找客服投诉的。
其次,我们可能会通过从副本读到老版本的数据,在正确性要求高的场景下,就不能通过从副本来提供读服务了。在异步复制的场景中,如果要通过从副本读取数据,要么我们能接受旧版本的数据,要么我们在读数据的时候给定一个版本号,要求读取小于或者等于这个版本号最新的数据。然后处理读请求的从副本,通过等待或者主动向主副本同步数据的方式,确保本地数据的版本超过读请求的版本号后,再按要求返回数据。
从上面的讨论中,可以看到 CAP 理论的权衡,同步复制模式选择了 C ,而放弃了 A ,是 CP 模型;而异步复制模式选择了 A ,而放弃了 C ,是 AP 模型。为了让你更好地理解,我总结了同步复制和异步复制的优缺点,具体见下表:
通过这些讨论,你会发现同步复制和异步复制的优缺点都非常明显,所以我们很自然会想到混合的复制方式,比如有一个主副本,一个同步复制的从副本,其他都是异步复制的从副本。
这样如果主副本故障,由于有一个同步复制的从副本,所以不会出现数据丢失的严重问题,并且这个从副本也能提供数据完全一致的读服务。另外其他从副本可能会读到旧版本数据,但是由于只有一个同步复制的从副本,对系统的写性能和可用性的影响也相对较少。
主从复制的粒度更小一点
我们上面讨论的主从复制模型,是基于每一个副本都有全量的数据集的,如果我们将这个主从复制的粒度变小一点,比如可以指定每一个副本最大为 128 M对于全量数据集按 128 M 拆分成多个副本,在每一个主从复制副本集内部做同步复制,这其实就是水平分片和主从复制的组合方式,也是当前分布式存储系统中非常流行的数据复制方案。
比如,我们有 4 台存储机器,每台机器可以存储 3 * 128 M 数据,当前我们的数据集合总量为 4 * 128 M那么我们可以将这个数据集拆分为 4 个分片,每个 128 M然后将这些分片和分片的副本分布到这 4 台机器上,具体的方法见下图。
这样将主从复制的粒度变小一点的方法,可以带来一些显著优点:
首先,系统中的每一台机器都可以负责一部分主副本,提升了系统的写入性能和可用性。
其次,可以让主从复制的副本数量不再和机器数量强绑定。在前面讨论的每一个副本都有全量数据集的方案中,每增加一台机器,都会导致副本集的数目增加 1 个,给系统带来了更多数据副本的性能开销。
但是,当主从复制的副本数量不再和机器数量强绑定,比如指定副本数量为 3 个,那么我们需要同步的从副本数量就是 2 个,不论集群的机器的数量如何增加,副本的数量都不会改变,这样我们就可以通过增加机器,来提升整个系统整体的读写性能。
总结
在分布式系统中,为了实现数据的高可用性,我们只能通过数据复制将数据保存多个副本。那么基于是否有主副本,有一个还是多个主副本,我们可以将数据复制的方案分为以下三种:主从复制、多主复制、无主复制。
接下来,我们重点讨论了主从复制的工作流程和主从复制的一个关键选择:同步复制 OR 异步复制,讨论了它们相关的优缺点,以及它们对系统设计的影响。并且,为了在系统的写性能和可用性之间取得更好地平衡,我们进一步讨论了同步、异步复制的混合使用方式。
最后,我们通过将主从复制的粒度变小一点的方法,得到了当前分布式存储系统中非常流行的数据复制方案。
思考题
请你根据标题思考一下,在主从复制的数据同步模式中,从副本的数据可以读吗?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,125 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 复制(二):多主复制的多主副本同时修改了怎么办?
你好,我是陈现麟。
通过上节课的学习,我们掌握了主从复制中,同步复制和异步复制的原理与知识,这样我们就可以根据业务场景,为极客时间后端的缓存系统 Redis 、关系数据库 MySQL 和 PostgreSQL 选择合适的数据复制方式,确保存储系统的高可用了。
但是,随着极客时间业务的快速发展,我们对产品的可用性和用户体验会提出更高的要求,那么在异地建立多个数据中心就是一个不错的思路,它可以让系统容忍地区性的灾害,并且用户也可以就近接入数据中心来优化网络时延。
不过,如果我们在多个数据中心之间,依然通过主从复制来同步数据,那么所有的写请求都需要经过主副本所在的数据中心,容灾能力和网络时延的问题并没有彻底改善,这个问题如何解决呢?
其实通过多主复制的方式进行数据复制,就可以避免主从复制,不能发挥多数据中心优势的问题了,所以本节课,我们将通过多主复制的技术原理解决这个问题。
为什么需要多主复制
我们都知道,数据复制是指将同一份数据复制到多个机器上,来避免机器故障时数据丢失的问题,它主要是用于保障数据高可用的。可一旦我们有了多个数据副本,为了提供更好的容灾能力,数据的多个副本应该分布得足够远,分布在多个机房或者多个城市中。
接下来,我们很自然就会想到,既然数据已经分布在多个机房或者城市中了,那么是否允许用户直接读写离自己最近的数据中心的数据呢?
在主从复制的情况下,将多个从副本分别部署到不同的数据中心上,对于读请求来说,如果是对一致性要求不高,或者主从之间是同步复制的情况,用户可以就近读取离自己最近的数据中心副本的数据;但是对于写请求来说,由于必须通过主副本写入,就导致所有的写请求必须经过主副本所在的数据中心写入。
而多主复制和无主复制,允许多个副本写入,就可以避免上面的问题了,那么在本节课中,我们主要讨论多主复制,下节课再介绍无主复制。
其实除了上面讨论的,在多数据中心提供就近读写的应用场景之外,多主复制还有在线文档和在线日历之类的客户端本地修改场景。在这个场景中,每一个可以本地修改的客户端,都可以视为一个主副本,它们与远端服务器进行异步复制变更信息,只不过这个异步复制在离线的场景下,可能是几分钟、几天甚至更长。
如何实现多主复制
基于主从复制模式,我们来介绍一下多主复制模式。它是指在一个数据系统中,存在多个主从复制单元,每一个主从复制单元都可以处理读写请求,一个主从复制单元的主副本处理了写请求后,需要复制到其他的主从复制单元的主副本,具体的流程见下图。
在实现多主复制的时有几个值得注意的地方,首先,每一个主从复制单元内部是一个常规的主从复制模式,这里的主副本、从副本之间的复制可以是同步的,也可以是异步的,具体的讨论可以查看第 19 讲“主从复制”。
其次,多个主从复制单元之间,每一个主副本都会将自己的修改复制到其他的主副本,主副本之间的复制可以是同步的,也可以是异步的。
如果主副本之间的复制是同步的,那么一个主副本的写入,需要等待复制到其他的主副本成功后,才能返回给用户,这样当写入出现冲突时,可以返回失败或由用户来解决冲突。但是,它却失去了多主复制最重要的一个优点,即多个主副本都可以独立处理写入,这就导致整个模式退化为主从复制的形式。所以一般来说,多主复制的主副本之间,大多采用异步模式,我们本课中讨论的多主复制也都是异步模式。
如果主副本之间的复制是异步的,那么一个主副本待自己写入成功后,就立即返回给用户,然后再异步地将修改复制给其他的主副本。这时也会出现一个问题,如果多个主副本同时成功修改一个数据,当主副本之间复制这个数据的修改时,会出现冲突,我们就不知道以哪一个主副本的写入结果为准了。所以接下来,我们就一起讨论对于异步模式的多主复制,如何解决多个主副本写入冲突的问题。
冲突解决
写入冲突是由于多个主副本同时接受写入,并且主副本之间异步复制导致的,那么依据这个定义,我们可以推导出写入冲突的两种主要形式。
首先是由于更新导致的冲突,多个主副本同时更新了一个数据,导致这个数据的版本是非线性的,出现了分叉,具体见下图。
其次,由于新增导致的冲突,多个主副本同时新增了一个含有唯一性约束的数据,导致数据的唯一性约束被破坏。例如,在酒店预订业务中,一个时段内一个房间只能预订给一个用户,如果多个用户在多个主副本上,同时发起预订操作,就可能出现同一个时段内,一个房间被多个用户预定成功的情况。
避免冲突
基于冲突的定义,我们应该怎么解决呢?有一个很自然的思路是,既然冲突是多个主副本同时修改了一个数据,或者破坏了数据的唯一性约束导致的,那么我们就对数据进行分片,让不同的主数据负责不同的数据分片,具体分片策略可以查看“分片”系列课程。这个方式确实可以在一定程度上避免冲突,但是会出现两个问题。
首先,一个修改操作可能会修改多个分片数据,这样我们就没有办法通过分片来隔离修改了。比如,我们将修改用户余额的操作进行水平分片, ID 为 0-10 的用户在主副本 1 写入, ID 为 11-20 的用户在主副本 2 写入。当 ID 6 的用户给 ID 16 的用户转账时,如果在主副本 1 上执行,那么同一时间, ID 16 的用户在主副本 2 上也有修改时,就会出现写入冲突。
其次由于就近接入和故障等原因我们会将出现故障的主副本流量切换到其他的主副本这时也会出现写入冲突的情况。我们继续按刚才的例子分析ID 为 0-10 的用户在主副本 1 写入ID 为 11-20 的用户在主副本 2 写入。
假设 ID 8 的用户在主副本 1 写入成功,但是数据的变更还没有同步到主副本 2 ,这时如果 ID 8 的用户到主副本 1 的网络出现问题,我们会立即将 ID 为 0-10 的用户的写入流量切换到主副本 2 ,那么在主副本 2 上,再对 ID 8 的数据进行修改就会导致冲突发生。
写时解决冲突
对于异步模式的多主复制,写入冲突是不可避免的,那么我们可以考虑,在数据写入一个主副本后,在主副本间进行复制时,检测是否有冲突,如果有冲突,就立即解决,这种方式称为写时解决冲突。它有两种实现方式,预定义解决冲突和自定义解决冲突,下面我们来一一讨论。
预定义解决冲突,是指由存储系统预先定义好规则,在冲突发生时依据预先定义好的规则,自动来解决冲突,它的规则主要有以下几种。
一是,从操作维度来处理,最后写入获胜。也就是为每一个写操作分配一个时间戳,如果发生冲突,只保留时间戳最大的版本数据,其他的修改都丢弃,但是这个方法会导致修改丢失。
二是,从副本维度来处理,最高优先级写入获胜。也就是为每一个副本都排好优先级,如果发生冲突,只保留优先级最高的副本修改数据,其他的修改都丢弃。例如,为每一个副本分配一个 ID ID 越大的副本,修改的优先级就越高,在发生冲突时,只保留 ID 最大的副本数据。同样,这个方法也会导致修改丢失。
三是,从数据结构和算法的维度来处理,通过研究一些可以自动解决冲突的数据结构来解决问题。比如 Google Doc 利用“操作转换”Operational transformation作为协作、编辑的冲突解决算法但是目前这种方式还不太成熟所以应用的范围比较少。
第二种实现方式是自定义解决冲突,它是由业务系统来定义冲突的解决方式,如果发生冲突了,存储系统就依据业务系统定义的方式执行。
自定义冲突解决的处理逻辑是,在主副本之间复制变更日志时,如果检测到冲突,就调用用户自定义的冲突处理程序来进行处理。由于主副本之间的数据复制是异步的,所以一般都是后台执行,不会提示用户。
一般来说,正确解决冲突是需要理解业务的,因此由业务来定义解决冲突的逻辑是非常合理的,所以大多数支持多主复制的存储系统,都会以用户自定义的逻辑,来提供解决冲突的入口。
读时解决冲突
读时解决冲突的思路和写时解决冲突的思路正好相反,即在写入数据时,如果检测到冲突,不用立即进行处理,只需要将所有冲突的写入版本都记录下来。当下一次读取数据时,会将所有的数据版本都返回给业务层,在业务层解决冲突,那么读时解决冲突的方式有下面两种。
第一种方式是由用户来解决冲突。毕竟用户才是最知道如何处理冲突的人,业务层将冲突提示给用户,让用户来解决。
另一个方式是自定义解决冲突。业务层先依据业务情况,自定义好解决冲突的处理程序,当检测到冲突时,直接调用处理程序来解决,你会发现它和写时解决冲突的第二种实现方式一样,只不过一个在写入时解决冲突,一个在读取时解决冲突。
多主复制的关键问题
多主复制虽然有多个主副本独立写入的优点,但是在一致性方面,多主复制的存储系统却面临着三个关键问题。
首先,正确解决冲突的难度非常大。从上文讨论的复杂情况中不难看出,解决冲突是一件非常难的事情,如果解决不当,就会出现修改丢失或错误的问题。
其次,异步模式的多主复制会存在数据一致性的问题。为了获得多个主副本可以独立写入的优点,多主副本之间,通常是通过异步的方式来复制数据的,这就会出现读取到陈旧版本数据的问题,影响整个系统的一致性。这里要特别说明一点,在多副本之间进行数据复制,如果你期望数据强一致性,那么目前最好的方案是 Paxos 和 Raft 之类的分布式一致性算法。
最后是多个主副本之间的复制拓扑结构问题。一般来说,多主复制的主副本之间的复制拓扑结构主要有三种:环形拓扑、星形拓扑以及全部至全部拓扑,具体见下图:
我们从图中可以看出,采用环形拓扑和星形拓扑结构时,如果一个主副本出现故障,可能会导致其他的主副本,也不能正常复制变更,甚至整个复制拓扑都会出现中断的情况。这时我们必须修复好出问题的主副本节点,或者重新调整复制的拓扑结构,才能恢复到正常状态。一般来说,这个过程需要人工参与且不能自愈,这会进一步延迟系统的恢复时间,使系统的可用性降低,同时降低系统的一致性。
在采用全部至全部拓扑结构时,虽然一个主副本的故障,不会影响其他主副本之间的数据复制,但是却会出现一个问题,那就是由于副本之间的网络时延各不相同,会使数据复制出现乱序,更新相互覆盖,变更丢失等错误情况,也会影响系统的一致性。
总而言之,虽然异步模式的多主复制有多个主副本可以独立写入的优点,但是也会在一定程度上降低系统的一致性,所以我们在使用时,需要评估业务特点,对一致性要求容忍度高的业务,可以使用多主复制,而对于一致性要求高的业务,则需要慎重考虑。
总结
这节课中,我们先讨论了多主复制的优点,即在多数据中心的场景下,每个数据中心的主副本可以单独写入,提高了系统的写入性能,并且用户可以实现就近读写,降低了系统的延迟。如果你的业务要实施多数据中心部署,也可以考虑是否采用多主复制的模式。
接着,我们讨论了如何实现多主复制,这里要注意一个关键点,如果要发挥多主复制的优点,就需要采用异步模式的多主复制,但是异步模式的多主复制还会有写入冲突的情况。
关于如何解决冲突,我们讨论了避免冲突、写时解决冲突和读时解决冲突的思路,当你在实施多主复制的时候,也可以通过这些方法,解决多主复制的写入冲突问题。
最后,因为异步模式的多主复制会在一定程度上,降低系统的一致性,所以我们在使用时,需要评估业务特点,对于一致性要求高的业务,需要慎重考虑。
思考题
本课中我们讨论了通过水平分片的方式避免写入冲突时,会出现一些不能解决的问题,那么请你思考一下,通过垂直分片的方式避免写入冲突时,会出现什么问题呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,104 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 复制(三):最早的数据复制方式竟然是无主复制?
你好,我是陈现麟。
通过上节课,我们掌握了在部署多数据中心的时候,可以用多主复制的方式,让用户直接读写离自己最近的数据中心的数据,减少用户与数据中心之间的网络延迟,提升用户体验。
当我们的业务面向全球的用户时,这个优点将会变得尤为重要,比如一个北京的用户,访问北京的数据中心,网络时延为毫秒级别,但是当他访问美国的数据中心时,网络时延就是百毫秒级别了,这是影响用户体验的关键点。所以,当极客时间启动全球业务的时候,多主复制是一个可以考虑的方案。
但不论是主从复制还是多主复制,所有的写入操作都必须依赖主节点,如果主节点出现故障,则必须再选举出一个新的主节点后,才能继续提供写服务,否则就将大大影响系统的可用性。那么是否有办法可以让单节点故障时,系统的可用性完全不受到影响呢?
我们可以这样思考一下,既然系统的可用性是由主节点的故障导致的,那么我们是否能去掉主节点和从节点的角色,也就是让系统中所有节点的角色都是对等的,这样是否可以解决问题呢?
其实这就是无主复制的数据复制方式,它确实可以解决由主节点故障,导致的系统可用性问题。虽然无主复制是“复制”系列课程的最后一节,但其实它才是最早出现的数据复制方式。无主复制又称为去中心复制,只不过在关系数据库出现并且主导后,由于要确保各副本写入顺序的一致性,主从复制开始流行起来,无主复制被大家慢慢遗忘了。
本节课中,我们将按无主复制的实现方案,面临的问题,以及如何解决这个思路来学习,最后对主从复制、多主复制和无主复制,这三种数据复制的方式进行比较和总结。
如何实现无主复制
无主复制顾名思义,即集群中没有主节点和从节点之分,所有节点的角色都是对等的,每个节点负责存储和处理一定范围的数据,并且由于高可用的要求,每一份数据都需要在多个节点上存储,那么一种常见的处理方式就如下图所示。
从图中可以看到,每一份数据按顺序存储多个副本,每一个节点都会负责多个范围数据的存储,节点 B 存储 Key Range (HB) 的数据,节点 C 存储 Key Range (IC) 的数据,节点 E 存储 Key Range (AE) 的数据。
这里要特别注意,主从复制和无主复制有一个非常大的区别,主从复制先写主节点,然后由主节点将数据变更同步到所有的从副本,从副本数据的变更顺序由主节点的写入顺序决定;但是,无主复制是由客户端或代理程序,直接负责将数据写入多个存储节点,这些存储节点之间是不会直接进行数据同步的。
数据读写
从刚才的讨论中可以看出,无主复制写入数据时,为了数据的高可用,会向多个节点写入多份数据,那么它是等所有的节点都写入成功,客户端才返回成功呢?还是有一个节点写入成功,客户端就返回成功呢?
同样地,读取数据也存在这个问题,每一份数据都有多个副本,那么它是等所有的节点都读取成功,客户端才返回成功呢?还是有一个节点读取成功,客户端就返回成功呢?
这里我们举个例子来讨论一下,假设现在有 3 个副本,如果数据成功写入 1 个副本,那么要确保读请求一定能读取到最新写入的数据,就需要成功读取 3 个副本的数据;如果数据成功写入 2 个副本,则需要成功读取 2 个副本的数据;如果数据成功写入 3 个副本,那么成功读取 1 个副本的数据即可。
这样就可以得出一个结论,如果要确保读取到最新的数据,读取的副本和写入的副本之间的交集不能为空,只要存在交集,就必定有一个写入的最新副本被读取到,那么我们就可以按如下的方式来定义这个问题。
假设对于每一份数据,我们保存 n 个副本,客户端写入成功的副本数为 w ,读取成功的副本数为 r ,那么只需要满足仲裁条件 w + r > n 成立,读副本和写副本之间的交集就一定不为空,即一定能读取到最新的写入。
我们将满足仲裁条件 w + r > n 的 w 和 r 称之为法定票数写和读这就是Quorum 机制,你也一定能发现它其实就是抽屉原理的应用。那么对于 w、r 和 n 的值,通常是可以配置的,一个常见的配置选择为,设置 n 为奇数(通常为 3 或 5 w = r = (n + 1)/2 向上取整。这个配置的读写比较均衡,比如 n = 5那么 w = r = 3读和写都保证 3 个副本成功即可,能容忍 2 个节点故障。
在实际的读多写少的业务场景中,我们假设 n = 5 ,如果想要读性能最高,可以设置 w = n = 5 r = 1 ,在读取的时候,只需等待一个节点读取成功即可。但是在写入的时候,需要所有的副本都写入成功,因此它不能容忍节点故障,如果有一个节点不可用,将会导致写入失败。如果 w = 4 r = 2 ,那么读性能依然比较高,并且能容忍一个节点不可用,这就是读性能、写性能和可用性之间的权衡。
反之也是同样的思路,对于写多读少的业务场景,我们假设 n = 5 ,如果想要写性能最高,那么可以设置 r = n = 5 w = 1 ,在写入的时候,只需等待一个节点读取成功即可。但是在读取的时候,需要所有的副本都读取成功,因此它不能容忍节点故障,如果有一个节点不可用,将会导致读取失败。如果 r = 4 w = 2 ,那么读性能依然比较高,并且能容忍一个节点不可用。
现在我们可以看出Quorum 机制通过参数的调整,能够非常方便地适应业务的特点,在读性能、写性能和可用性之间达到平衡。
数据修复
我们知道一个复制模型,应该确保数据最终都能成功复制到所有的副本上,主从复制和多主复制是通过主节点接受数据写入,并且由主节点负责将数据副本,成功复制到所有的从副本来保证的。但是在上文“数据读写”的讨论中,我们了解了当 w < n 并不能保证数据成功写入所有的副本中那么无主复制的这个问题应该如何解决呢一般来说有如下的两种方式来实现数据的修复
首先是读修复当客户端并行读取多个副本时如果检测到某一副本上的数据是过期的那么在读取数据成功后就异步将新值写入到所有过期的副本上进行数据修复具体如下图所示
其次是反熵过程由存储系统启动后台进程不断去查找副本之间数据的差异将数据从新的副本上复制到旧的副本上这里要注意反熵过程在同步数据的时候不能保证以数据写入的顺序复制到其他的副本这和主从复制有着非常大的差异同时由于数据同步是后台异步复制的会有明显的同步滞后
总体来看读修复对于读取频繁的数据修复会非常及时但它只有在数据被读取时才会发生那么如果系统只支持读修复不支持反熵过程的话有一些很少访问的数据在还没有发生读修复时会因为副本节点的不可用而更新丢失影响系统的持久性所以将读修复和反熵过程结合是一种更全面的策略
一个关键的选择
到这里我们已经掌握了如何实现一个无主复制的数据系统不过在这个系统中还有一个非常关键的选择如果系统的某些节点发生故障导致读或写的时候无法等到系统配置的 w r 个客户端成功返回我们应该如何处理呢这里我们可以依据 2 个方案来思考
当读写无法到达Quorum要求的 w r 直接返回失败并且明确地将错误返回给客户端
在读写的时候依然是等待 w r 个客户端成功返回只不过有一些节点不在事先指定的 n 个节点的集合内比如本课第一幅图中的 Key K它指定的存储副本集合应该是 BCD E假设 D 出现故障了那么它的存储集合可以临时修改为 BCE F
你会发现第一个方案即当系统的故障已经导致仲裁条件不成立时就返回失败并且明确地将错误返回给客户端的选择是一致性和可用性之间的权衡是为了数据的一致性而放弃了系统的可用性
对于第二个方案在数据读写时当我们在规定的 n 个节点的集合内无法达到 w r 就按照一定的规则再读写一定的节点这些法定集合之外的数据读写的节点可以设置一些简单的规则比如对于一致性 Hash 环来说可以将读写顺延到下一个节点作为临时节点进行读写当故障恢复时临时节点需要将这些接收到的数据全部复制到原来的节点上即进行数据的回传
通过这个方式我们可以确保在数据读写时系统只需要有任意 w r 个节点可用就能读写成功这将大大提升系统的可用性但是这也说明即使系统的读写能满足仲裁条件 w + r > n ,我们依然无法保证,一定能读取到最新的值,因为新值写入的节点并不包含在这 n 个节点之中。
那么这个方案叫 Sloppy Quorum ,相比于传统的 Quorum ,它为了系统的可用性而牺牲了数据的一致性。目前,几乎所有无主复制的存储系统都支持 Sloppy Quorum但是它在 Cassandra 中是默认关闭的,而在 Riak 中则是默认启用的,所以我们在使用时,可以根据业务情况进行选择。
三种数据复制模式总结
目前,我们已经学习了三种数据复制模式:主从复制、多主复制和无主复制,因为在我们进行存储系统设计时,数据复制是一个非常关键的选择,所以我们再来总结和分析一下,它们的优缺点以及应用场景,具体如下表。
总结
无主复制由于写入不依赖主节点,所以在主节点故障时,不会出现不可用的情况。但是,也是由于写入不依赖主节点,可能导致副本之间的写入顺序不相同,会影响数据的一致性。
在实现无主复制时,有两个关键问题:数据读写和数据修复。数据读写是通过仲裁条件 w + r > n 来保证的,如果满足 w + r > n ,那么读副本和写副本之间就一定有交集,即一定能读取到最新的写入。而数据修复是通过读修复和反熵过程实现的,这两个方法在数据的持久性和一致性方面存在一定的问题,如果对数据有强一致性的要求,就要谨慎采用无主复制。
然后,我们了解了 Sloppy Quorum ,它相比于传统的 Quorum ,为了系统的可用性而牺牲了数据的一致性,这里我们可以进一步得出,无主复制是一个可用性优先的复制模型。
最后,我们对比了“复制”系列中,三种数据复制模型的优缺点和应用场景,你可以通过这些对比,更加深刻地理解数据复制,并且依据业务场景做出最佳的选择。
思考题
如果现在有这样的一个业务场景:数据需要有 7 个副本,读写都能容忍一个节点失败,并且读请求远远大于写请求,那么 w 和 r 为多少最合适呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,106 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 事务(一):一致性,事务的集大成者
你好,我是陈现麟。
通过学习“数据复制”系列的内容,我们使用数据复制,将同一份数据按一定的策略复制到多台机器上,解决了存储服务由于宕机等故障,不能为用户提供服务和数据丢失的问题,恭喜你又攻克了一个难关。
但是,由于极客时间用户量增多,每一天课程购买的订单数都在急剧增加,你开始接到用户这样的投诉,他在购买课程时出现了错误,课程没有购买成功,但是余额却被扣了。同时,财务的同事也开始向你反馈,他们在算账时,发现收入和支出的数目对不上。在遇到这样的问题时,你是不是一时抓不到头绪呢?其实这些都是我们工作中经常碰到的事务场景问题。
那么从这节课开始,我们将一起花四节课的时间来解决分布式场景下的事务问题。这一节课,我们先通过分析业务场景来讨论事务是什么,以及它可以解决的问题,然后学习它的四个特性:一致性、原子性、隔离性和持久性,最后再一起来讨论如何实现事务的一致性。
事务是什么
在本课开头,我们列举了工作中经常碰到的两个事务场景问题,那么我们先来了解一下事务是什么?事务可以看成是一个或者多个操作的组合操作,并且它对这个组合操作提供一个保证,如果这个组合操作之前的数据是一致的(即正确的),那么操作之后的数据也应该是一致的。不论这个组合操作执行的过程中,是否发生系统故障,还是在这个组合操作执行的过程中,是否与其他事务一起执行。
为了让你更好地理解事务的定义,我们结合开头提到的两个具体的事务场景问题来讨论一下。
第一个问题,用户在极客时间购买课程时出现了错误,课程没有购买成功,但是余额却被扣了。这里我们先分析一下,用户在购买课程时,在我们的服务器程序里,需要实现三个操作:
余额检查:确认用户的余额是否大于课程的价格,如果余额足够,则可以购买,否则不可以购买。
余额扣费:从用户的余额中扣除购买课程的金额。
权益发放:给用户发放购买课程的学习权益。
正常情况下,上述三个操作完成后,用户的余额被扣除,也获得了学习权益。但是如果在这三个操作执行的过程中,出现了发放学习权益的服务崩溃或者课程下线等情况,那么这三个操作就无法全部执行成功。此时,用户的余额已经被扣除,但是在 App 上却收到了购买失败的提示,用户一定是不认可的。
我们仔细分析就会发现,这个问题的根本原因是余额扣费和权益发放不是一个整体操作,出现了部分执行成功的情况。这里可以结合事务来思考,如果将课程购买通过一个事务来执行的话,这个事务就会包括余额检查、余额扣费和权益发放 3 个操作,并且它对这个组合操作提供了一个保证,保证不论出现什么故障,这 3 个操作要么都执行成功,要么都不执行。所以当我们结合事务来思考,如下图所示,这个问题就迎刃而解了。
第二个问题,财务的同事反馈他们在算账时,发现收入和支出的数目对不上。这里我们还是拿用户购买课程来分析,假设用户余额为 100 元,课程价格为 60 元,此时因为余额大于课程价格,所以余额检查是通过的。
如果在这时,用户同时还在买另一个价格为 80 元的课程,因为上笔订单没有付款,用户的余额依旧为 100 元,大于课程价格 80 元,所以这笔订单的余额检查也是通过的。接下来,当这两个课程都购买成功时,就相当于用户用 100 元的余额,分别购买了 60 元和 80 元的两个课程,所以最后用户的余额就变成了负 40 元,这个结果显然是不符合财务同事预期的。
那么它的根本原因就是用户的两个购买操作并发执行。我们也结合事务来思考一下,如下图,如果这两个课程购买的操作通过事务来执行的话,事务会对这个组合操作提供一个保证,保证它和一个一个串行执行的操作一样,不会出现由于并发执行而导致数据不正确的问题。
通过分析具体的事务场景问题,我们会发现事务为日常的研发工作,提供了一个非常优雅的抽象,让我们可以将一组操作过程中的内部状态处理等细节交给事务来处理,而我们只需要去关心这一组操作是否成功就可以了,这大大简化了研发的工作负担。
事务的四个特性
事务是一个非常实用的工具,它为我们的研发提供了非常友好的保证,但是,你心里一定会有一个问题,它是通过什么具体方法来实现的呢?
事务主要是通过提供以下四个特性来实现的:
一致性C一个事务能够正确地将数据从一个一致性的状态变换到另一个一致性的状态。
原子性A一个事务所有的操作要么全部执行要么就一个都不执行即 all-or nothing。它可以让事务在出现故障等原因导致不能全部执行成功时将已经执行的部分操作回滚到事务前的状态。
隔离性I如果多个事务并发执行那么执行结果和一个一个串行执行是一样的。它可以使事务在执行时不会受到其他事务的影响。不过在实践中由于考虑到性能的问题一般都使用较弱一点的保证我们在后续的课程中会专门讨论。
持久性D如果一个事务已经提交不论什么原因它产生的结果都是永久存在的它保证了事务的结果不会丢失。
从上面的分析中,我们了解了事务是如何通过四个特性来达成它的目标的。在四个特性中,一致性是对事务执行最终结果正确性的保证,它需要依赖事务的其他特性来协助完成,我们可以将它看成是事务操作的一个概览。
所以,本节课我们会先来讨论事务的一致性。并且这里要特别说明一下,事务的四个特性不是孤立的,它们之间是相互联系的。在学习事务一致性时,我们需要思考事务原子性、隔离性、持久性与一致性之间的联系。
一致性是怎么实现的
上文提到了一致性的定义,即一个事务能够正确地将数据从一个一致性的状态,变换到另一个一致性的状态。也就是在事务执行的过程中,不能出现任何不一致的问题,如果一个事务执行前的数据是正确的,那么执行后的数据也必须是正确的,所以,事务的一致性其实就是正确性。
事务的一致性需要保证一个事务在执行时,不论出现停电、宕机等任何问题,最终的执行结果都是正确的,这是一个非常高的要求,接下来我们就来分析一下,在高要求下事务是如何实现一致性的?
首先,我们从数据复制的角度来看,为了保障系统的高可用,每一份数据都复制了多个副本,事务执行后,这多个副本的数据需要完全一致,即数据的多副本必须通过强一致性的策略进行复制。这个问题我们在“数据复制”的课程中已经有过讨论,并且在“事务(四)”的课程中会继续讨论,这里就不再赘述了。
然后,我们可以从事务的原子性、隔离性和持久性方面,来讨论事务的一致性是如何实现的。在事务的执行过程中,不能因为系统故障等原因,出现部分操作执行成功的情况,比如我们前面提到的课程购买例子中,余额扣费成功,但是权益却发放失败的情况,这个部分需要事务的原子性来保证。
同时,也不能出现因为事务并发导致执行后状态错误的情况,比如两个课程购买的事务并发执行的例子中,当时余额检查都成功,但是到了后面扣费时,由于用户余额不足出现了负数。为了让事务在执行时,不会受到其他事务的影响,事务的隔离性也需要注意。
另外,在事务执行的过程中,也要考虑到因为数据丢失,导致执行后的结果错误的情况,这个部分需要事务的持久性来保证。
虽然有了底层存储多副本数据强一致性的支持,以及事务三个特性的保驾护航,但我们还是要考虑事务执行的最终结果,是否满足数据库层以及业务层的约束规则,所以最后我们要做好约束检测。这里又分为如下两个层面来讨论。
第一个是数据库层面,数据库内部需要基于一些约束规则,来检测数据是否违反了一致性的约束,比如外键约束和唯一性约束等。
另一个是应用层的业务逻辑,它需要结合业务场景做一些约束检测,这样做是为了保障数据的一致性,比如用户课程购买的场景,从用户账号扣掉的钱,应该和收款方的数目是相等的。如果应用层的处理逻辑出现 Bug导致用户账号扣掉的钱比收款方的多这样的一致性问题在数据库的事务层面是无法约束检测的它需要应用层的业务逻辑来保证。
所以,通过上面的分析,我们可以了解到,事务一致性的实现需要多维度来保证,比如底层存储的多副本数据强一致性,事务原子性、隔离性和持久性的一起协作,以及数据库层和应用层的约束检测等各方面来保障,它不单单是事务层面的一致性问题。
这也是事务的一致性和其他三个特性不一样的地方,事务的原子性、隔离性和持久性这三个特性可以通过各自的实现机制来保障,而一致性则是应用层通过运用事务的原子性、隔离性和持久性的特性,加上数据库层的约束检测,并且在应用层开发中做好相关的约束检测才能达成,所以,我们说一致性是事务的集大成者。
总结
这节课中,我们讨论了事务的概念,事务是一个或多个操作的组合操作,并且它对这个组合操作提供一个保证,如果这个组合操作之前的数据是一致的,那么操作之后的数据也应该是一致的。
然后,我们通过分析极客时间 App 出现的课程购买问题,引出了事务的具体业务场景。如果我们期望多个操作同时成功或者失败,并且期望多组操作之间不能相互影响,就需要通过一个事务来执行。而且事务为我们日常的研发工作,提供了一个非常优雅的抽象,大大简化了研发的工作负担。
在学习了事务的四个特性,一致性、原子性、隔离性和持久性后,我们了解到事务的四个特性之间是相互联系和影响的。
最后,我们探讨了事务的一致性是如何实现的,它是通过底层存储的多副本数据强一致性,事务的原子性、隔离性和持久性一起协作,以及数据库层和应用层的约束检测等各方面来保障的,不单单是事务层面的一致性问题,这也正说明了事务的四个特性有着直接的联系与影响。
思考题
事务的一致性和数据的一致性是一个概念还是两个概念?如果是两个概念,它们之间有什么联系吗?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,119 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 事务(二):原子性,对应用层提供的完美抽象
你好,我是陈现麟。
通过上节课的学习,我们理解了事务的一致性的定义,并且知道了事务一致性的实现,是通过底层存储的多副本数据强一致性,事务的原子性、隔离性和持久性一起协作,以及数据库层和应用层的约束检测等各方面来保障的,那么本节课,我们就继续来讨论事务中,另一个非常重要的特性:原子性。我们从原子性的定义出发,一起分析在分布式系统中,原子性的实现方法,最后再对原子性的关键问题进行讨论。
当我们对事务的原子性进行讨论和学习后,你就能明白原子性是一个非常完美的抽象,因为它对应用程序,屏蔽了分布式系统中部分失败的问题,这可以大大减少我们在编程时的心智负担。
原子性的定义
一般来说,我们在计算机领域第一次接触“原子”这一概念,都来源于操作系统的“原子操作”。在操作系统中,原子操作的定义是指,不可被中断的一个或者一系列操作,它包含了两个层面的意思。
首先,是整体的不可分割性。一个原子操作的所有操作,要么全部执行,要么就一个都不执行,即 all-or-nothing 。
其次,是可串行化的隔离性,即线程安全。原子操作是在单核 CPU 时代定义的,由于原子操作是不可中断的,那么系统在执行原子操作的过程中,唯一的 CPU 就被占用了,这就确保了原子操作的临界区,不会出现竞争的情况。原子操作自带了线程安全的保证,即最严格的隔离级别的可串行化,所以我们在编程的时候,就不需要对原子操作加锁,来保护它的临界区了。
但是,我们上节课提到了事务中原子性的定义,一个事务所有的操作,要么全部执行,要么就一个都不执行,即 all-or-nothing 。它可以让事务在执行的过程中,当遇到故障等原因,不能全部执行成功的时候,将已经执行的操作,回滚到事务前的状态。
你会发现事务中对原子性的定义,只保留了原子操作的不可分割性,并没有关注可串行化的隔离性。其实这也很好理解,主要是基于性能的考虑,如果事务的原子性同时定义了不可分割性和可串行化的隔离性,那么对数据库性能的影响将会非常大,因为数据库需要频繁地操作,相对于内存来说非常慢的磁盘,而可串行化地去操作磁盘,在很多业务场景下的性能是我们不可以接受的。
因此,在事务的定义中,就将原子操作的不可分割性和隔离性,分别定义出了两个特性,即原子性和隔离性。其中隔离性为了在性能和正确性之间权衡,定义了多种隔离级别,我们可以依据自己的业务情况进行选择,具体的隔离性讨论,我们将在下一节课进行。
怎么实现原子性
通过上面的讨论,我们知道了事务的原子性只关注整体的不可分割性,一个事务所有的操作,要么全部执行,要么就一个都不执行。那么我们应该如何实现事务的原子性呢?
从不可分割性的角度来思考,实现一个事务需要解决两个维度上的操作分割:
第一个维度是单节点事务,即单节点上操作的不可分割性。在单节点上,一个事务在执行的过程中出现崩溃等问题,它的一部分操作已经执行完成,而另一部分操作则无法继续执行,这时就会出现整个事务操作无法继续完成,仅仅部分操作完成的情况。
第二个维度是分布式事务,即多节点之间的操作不可分割性。在多节点上,一个事务操作需要在多个节点上运行,如果某些节点检测到违反约束、冲突、网络故障或者崩溃等问题,事务将无法继续执行,而其他节点的事务却已经顺利完成了,这时就会出现部分节点操作完成的情况。
下面我们就从单节点事务和分布式事务的维度,来一一讨论事务原子性的实现。
单节点事务
对于单节点上运行的事务(单节点事务)来说,在执行过程中,不需要与其他的节点交互,也就不会出现部分节点失败导致的操作分割,我们只需要考虑当前节点整体失败导致的操作分割即可。对于单节点事务,一般是在存储引擎上,通过 Undo Log 、 Redo Log 和 Commit 记录来实现,具体流程如下图。
我们从图中不难看出,对于单节点事务来说,一个非常关键的顺序就是在磁盘上持久化数据的顺序:先写入 Undo Log 和 Redo Log ,然后再写入 Commit 记录。其中事务的提交或中止由 Commit 记录来决定,如果在写入 Commit 记录之前发生崩溃,那么事务就需要中止,通过 Undo Log 回滚已执行的操作;如果事务已经写入了 Commit 记录,就表明事务已经安全提交,后面发生了崩溃的话,就要等待系统重启后,通过 Redo Log 恢复事务,然后再提交。
接下来,我们通过举例来简单描述下这个过程,注意这里简化了 Undo Log 和 Redo Log 的格式。假设一个事务操作 A、B 两个数据,他们的初值分别为 1 和 2 ,事务的操作内容为将 A 修改为 3 B 修改为 4 ,那么事务的执行流程如下图。
通过这些讨论,我们可以看出, Redo Log 保证了事务的持久性, Undo Log 保证了事务的原子性,而写入 Commit 记录了事务的提交点,它来决定事务是否应该安全提交。通过提交点,我们就可以将事务中多个操作的提交,绑定在一个提交点上,实现事务的原子提交。
分布式事务
对于多节点上运行的事务(分布式事务)来说,除了当前节点整体失败导致的操作分割之外,还存在部分节点失败导致的操作分割。我们知道当前节点整体失败导致的操作分割,可以按单节点事务来处理,而对于部分节点失败导致的操作分割,一个常见的思路是通过两阶段提交( 2PC )来解决,实现 2PC 的思路如下图所示。
选择一个协调者,这个协调者可以是分布式事务的参与节点,也可以是一个单独的进程。
阶段 1
协调者发送事务请求Prepare到所有的参与节点并询问它们是否可以提交。
如果所有的参与节点都回复“是”,那么接下来协调者在阶段 2 发出提交Commit请求。
如果任何的参与节点都回复“否”,那么接下来协调者在阶段 2 发出放弃Rollback请求。
阶段 2
依据阶段 1 返回的结果决定事务最终是提交Commit还是放弃Rollback
关于 2PC ,在实现的时候,要特别注意 2 个关键点。
一是,两个关键承诺。第一个承诺在阶段 1 ,当事务的参与节点回复“是”的时候,对于当前事务,这个参与节点一定是能够安全提交的,它不仅要保障事务在提交时,不会出现冲突和约束违规之类的问题,还要保障即使出现系统崩溃、电源故障和磁盘空间不足等系统问题时,事务依然能够正常提交成功。
第二个承诺在阶段 2 ,当协调者基于参与者的投票,做出提交或者中止的决定后,这个决定是不可以撤销的。对于协调者来说,如果协调者通知参与者失败,那么协调者必须一直重试,直到所有的参与节点都通知成功为止;而对于参与者来说,不论协调者通知的结果是提交还是中止,参与者都必须严格执行,不能反悔。即使出现了故障,在故障恢复后,还是必须要执行,直到成功为止。
第二个关键点是2PC 的提交点。当协调者通过参与者的投票,做出提交或者中止事务的决定后,需要先将决定写入事务日志,然后再通知事务的参与者。如果协调者在事务执行过程中崩溃了,那么等到协调者恢复后,在事务日志中如果没有发现未解决的事务,就中止事务;反之,就会继续执行事务。
所以,协调者将阶段 1 的决定写入事务日志就是 2PC 中事务的提交点,通过这个提交点,将多个节点的事务操作绑定在一个提交点上,然后像单节点事务一样,利用这个提交点来保证事务的原子性。
2PC 面临的问题
通过上面的讨论,我们知道了 2PC 可以解决分布式事务的原子性问题,但是要正确使用 2PC还需要了解以下几个方面的问题。
第一2PC 是一个阻塞式协议。当 2PC 的一个参与者,在阶段 1 做出了“是”的回复后,参与者将不能单方面放弃,它必须等待协调者的决定,也就意味着参与者所有占用的资源都不能释放。如果协调者出现故障,不能将决定通知给参与者,那么这个参与者只能无限等待,直到协调者恢复后,成功收到协调者的决定为止。
因为 2PC 有阻塞问题所以后来又提出了3PC 协议,它在 2PC 的两个阶段之间插入了一个阶段,增加了一个相互协商的过程,并且还引入了超时机制来防止阻塞。虽然 3PC 能解决 2PC 由于协调者崩溃而无限等待的问题,但是它却有着超高的延迟,并且在网络分区时,还可能会出现不一致的问题,这些原因导致它在实际应用中的效果并不好,所以目前普遍使用的依然是 2PC 。
第二, 2PC 是一个逆可用性协议。如果在阶段 1 ,任何一个参与者发生故障,使准备请求失败或者超时,协调者都将中止操作;如果在阶段 2 ,协调者发生故障,也会导致参与者只能等待,无法完成操作。
你是否感觉很奇怪同样是共识算法Raft 和 Paxos 等共识算法都能容忍少数节点失败的情况,那为什么 2PC 则完全不能容忍节点的失败呢?其实,这个差异的出现是因为 2PC 是一个原子提交协议,为了 all-or-nothing ,在操作过程中就需要与所有的节点达成共识;而 Raft 和 Paxos 则只需要与大部分节点达成一致,确保共识成立即可,它可以容忍少数节点不可用,当故障恢复的时候,之前不可用的节点可以向其他正常的节点同步之前达成的共识。
第三,虽然 2PC 能保证事务的原子性,即一个事务所有的操作,要么都成功,要么都失败,但是它并不能保证多个节点的事务操作会同时提交。如果没有同时提交,即一部分节点已经提交成功,而另一部分节点还没有提交的时候,就将使事务的可见性出现问题,这部分知识,我们将在课程“事务(三)”中继续讨论。
总而言之,虽然 2PC 在性能、可用性和可见性方面都存在问题,但是目前分布式事务中,使用最广泛的还是 2PC 。
总结
在这节课中,我们先讨论了原子性的定义,了解了事务的原子性,以及操作系统的原子操作是两个不同的概念,事务的原子性只要求 all-or-nothing ,而操作系统的原子操作除了要求 all-or-nothing 之外,还需要可串行化的隔离级别。
然后,我们从单节点事务和分布式事务的角度,讨论了如何实现事务的原子性。对于单节点事务来说,我们将事务的多个操作绑定到,事务提交信息写入的一个提交点上,如果提交信息写入成功,那么事务提交,否则事务回滚。
而对于分布式事务来说,它在单节点事务的基础上,进一步地要求事务的多个参与者做出两个关键承诺,第一个承诺在阶段 1 ,当事务的参与节点回复“是”的时候,该参与者是一定可以提交的;第二个承诺在阶段 2 ,当协调者基于参与者的投票,做出提交或者中止的决定后,这个决定是不可以撤销的。
最后,我们讨论了 2PC 在性能、可用性和可见性方面有着一些问题,但是 2PC 依然是当前分布式事务场景中,使用最多的原子提交协议。
思考题
在学习 2PC 协议的过程中,我们提到了 3PC 协议,它在 2PC 的两个阶段之间插入一个阶段,从而增加了一个相互协商的过程,并且还引入了超时机制来防止阻塞,你知道这是怎么做的吗?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,127 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 事务(三):隔离性,正确与性能之间权衡的艺术
你好,我是陈现麟。
通过上节课的学习,我们掌握了通过 2PC 实现分布式事务原子性的技术原理,并且也明白了 2PC 在可用性等方面存在的问题,这些知识能够帮助我们在极客时间的架构选型中,做出正确的选择。
同时,我们还讨论了事务原子性的定义,区分出了事务的原子性并不等价于操作系统里面的原子操作,事务的原子性只定义了操作的不可分割性,而不关心多个事务是否由于并发相互竞争而出现错误,那么在本节课中,我们就一起来讨论事务并发执行的问题,即事务的隔离性。
我们先一起来讨论隔离性的级别和各个隔离级别可能出现的异常情况,然后分析在业务代码中,如何避免异常情况的出现,最后通过讨论隔离性的实现方式,让你进一步理解隔离级别。
什么是隔离性
隔离性定义的是,如果多个事务并发执行时,事务之间不应该出现相互影响的情况,它其实就是数据库的并发控制。可能你对隔离性还有点陌生,其实在编程的过程中,隔离性是我们经常会碰到的一个概念,下面我们就具体讨论一下。
在应用程序的开发中,我们通常会利用锁进行并发控制,确保临界区的资源不会出现多个线程同时进行读写的情况,这其实就对应了事务的最高隔离级别:可串行化,它能保证多个并发事务的执行结果和一个一个串行执行是一样的。
现在你就会发现,隔离级别是我们日常开发中经常碰到的一个概念,那么你肯定会有一个疑问,为什么应用程序中可以提供可串行化的隔离级别,而数据库却不能呢?
其实根本原因就是应用程序对临界区大多是内存操作而数据库要保证持久性即ACID 中的 Durability需要把临界区的数据持久化到磁盘可是磁盘操作比内存操作要慢好几个数量级一次随机访问内存、 SSD 磁盘和 SATA 磁盘,对应的操作时间分别为几十纳秒、几十微秒和几十毫秒,这会导致临界区持有的时间变长,对临界区资源的竞争将会变得异常激烈,数据库的性能则会大大降低。
所以,数据库的研究者就对事务定义了隔离级别这个概念,也就是在高性能与正确性之间提供了一个缓冲地带,相当于明确地告诉使用者,我们提供了正确性差一点但是性能好一点的模式,以及正确性好一点但是性能差一点的模式,使用者可以按照自己的业务场景来选择。
隔离级别与异常情况
通过对隔离性定义的讨论,我们知道了隔离性是高性能与正确性之间的一个权衡,那么它都提供了哪些权衡呢?
首先这个权衡是由隔离级别Isolation Level来定义的 SQL-92 标准定义了 4 种事务的隔离级别读未提交Read Uncommitted、读已提交Read Committed、可重复读Repeatable Read和串行化Serializable在后面的发展过程中又增加了快照隔离级别Snapshot Isolation
由于我们在讨论事务隔离级别的时候,经常通过是否避免某一些异常情况来定义,所以在具体讨论每一个隔离级别之前,我们先来看看事务并发时可能会出现的异常情况,具体有以下几种。
其一脏写Dirty Write即有两个事务 T1 和 T2 T1 更改了 x ,在 T1 提交之前, T2 随之也更改了 x ,这就是脏写,这时因为 T1 还没有提交,所以 T2 更改的就是 T1 的中间状态。假如现在 T2 提交了, T1 就要回滚,如果回滚到 T1 开始前的状态,已经提交的 T2 对 x 的操作就丢失了;假如不回滚到 T1 开始前的状态,已经 Roll Back 的 T1 的影响就还存在于数据库中。能够允许这种现象的数据库基本是不可用的,因为它已经不能完成事务的 Roll Back 了。
其二脏读Dirty Read即有两个事务 T1 和 T2 T1 更改了 x ,将 x 从 0 修改为 5 ,在 T1 提交之前, T2 对 x 进行了读取操作,读到 T1 的中间状态 x = 5 ,这就是脏读。假设最终 T1 Roll Back 了,而 T2 却根据 T1 的中间状态 x = 5 做了一些操作,那么最终就会出现不一致的结果。
其三不可重复读Nonrepeatable read/ 读倾斜Read Skew即有两个事务 T1 和 T2 T1 先读了 x = 0 ,然后 T2 更改了 x = 5 ,接着提交成功,这时如果 T1 再次读取 x = 5 ,就是不可重复读。不可重复读会出现在一个事务内,两次读同一个数据而结果不一样的情况。
其四丢失更新Loss of Update即有两个事务 T1 和 T2 T1 先读 x = 0 ,然后 T2 读 x = 0 ,接着 T1 将 x 加 3 后提交, T2 将 x 加 4 后提交,这时 x 的值最终为 4 T1 的更新丢失了,如果 T1 和 T2 是串行的话,最终结果为 7 。
其五幻读Phantom Read即有两个事务 T1 和 T2 T1 根据条件 1 从表中查询满足条件的行,随后 T2 往这个表中插入满足条件 1 的行或者更新不满足条件 1 的行,使其满足条件 1 后提交,这时如果 T1 再次通过条件 1 查询,则会出现在一个事务内,两次按同一条件查询的结果却不一样的情况。
其六写倾斜Write Skew即假如 x y 需要满足约束 x + y >= 0 ,初始时 x = -3 y = 5 ,事务 T1 先读 x 和 y ,然后事务 T2 读 x 和 y ,接着事务 T2 将 y 更新为 3 后提交,事务 T1 将 x 改为 -5 后提交,最终 x = -5 y = 3 不满足约束 x + y >= 0 。
讨论完这些异常情况后,我们再通过一个表格来看看,事务的隔离级别与这些异常情况的关系。
我们从表格中可以看到,在隔离级别的一致性强度上,读未提交 < 读已提交 < 可重复读 <> 快照 < 串行化可重复度和快照隔离级别之间是不可以比较的
这里要特别注意由于 SQL 标准对隔离级别的定义还存在不够精确的地方并且标准的定义有时还与实现有关系而各个数据库对隔离级别的具体实现又各不相同所以上面的表格只是对常见的隔离级别异常情况的定义你可以把它当成一个通用的标准参考当你使用某一个数据库时需要读一下它的文档确定好它的每一个隔离级别具体的异常情况
如何避免异常情况
现在我们已经知道了每一个隔离级别可能会出现的异常情况如果当前数据库使用了某一个隔离级别我们也知道它有哪些异常情况是否有办法来避免呢
其实这是一个非常好的问题不过有些异常情况只能通过提升隔离级别来避免那么接下来我们就针对每一种异常情况来一一讨论一下
其一对于脏写几乎所有的数据库都可以防止异常的出现并且我们可以理解为出现脏写的数据库是不可用的所以这里就不讨论脏写的情况了
其二对于脏读提供读已提交隔离级别及以上的数据库都可以防止异常的出现如果业务中不能接受脏读那么隔离级别最少在读已提交隔离级别或者以上
其三对于不可重复读或读倾斜,“可重复读隔离级别及以上的数据库都可以防止问题的出现如果业务中不能接受不可重复读和读倾斜那么隔离级别最少在可重复读隔离级别或者以上
其四对于丢失更新如果数据库的隔离级别不能达到可重复读隔离级别或者以上那么我们可以考虑以下的几种方法来避免
首先如果数据库提供了原子写操作那么一定要避免在应用层代码中进行修改操作应该直接通过数据库的原子操作执行避免更新丢失的问题例如关系数据库中的 udpate table set value value 1 where key MongoDB 中的 \(set\)unset 等操作
数据库的原子操作一般通过独占锁来实现相当于可串行化的隔离级别所以不会有问题不过在使用 ORM 框架时很容易在应用层代码中完成修改的操作导致无法使用数据库的原子操作
其次如果数据库不支持原子操作或者在某些场景中原子操作不能处理时可以通过对查询结果显式加锁来解决对于 MySQL 来说就是 select for update 通过 for update 告诉数据库查询出来的数据行过一会是需要更新的需要加锁防止其他的事务对同一块数据也进行读取加更新操作从而导致更新丢失的情况
最后我们还可以通过原子比较和设置来实现例如 update table set value newvalue where id and value oldvalue 但是这个方式有一个问题如果 where 条件的判断是基于某一个旧快照来执行的那么 where 的判断就是没有意义的所以要是采用原子比较和设置避免更新丢失的话一定要确认数据库比较设置操作的安全运行条件
我们把第五点和第六点合在一起讨论对于幻读和写倾斜如果数据库的隔离级别不能达到可串行化的隔离级别我们就可以考虑通过显式加锁来避免幻读和写倾斜通过对事务利用 select for update 显式加锁可以确保事务以可串行化的隔离级别运行所以这个方案是可以避免幻读和写倾斜的但不是在所有的情况下都适用比如 select for update 如果在 select 时不能查询到数据那么这时的数据库将无法对数据进行加锁
例如在订阅会议室时多个事务先通过 select for update 查询会议室某一时段的订阅记录当该会议室在这个时间点还没有被订阅时就都查询不到订阅记录select for update 也就无法进行显式加锁如果后面多个事务都会订阅成功就会导致一个会议室在某一时段只能订阅一次的约束被破坏
所以显式加锁对于写倾斜不能适用的情况就是如果在 select 阶段没有查询到临界区的数据就会导致无法加锁这种情况下我们可以人为引入用于加锁的数据然后通过显式加锁来避免写倾斜的问题比如在订阅会议室时我们为所有会议室的所有时间都创建好数据每一个时间会议室一条数据这个数据没有其他的意义只是在 select for update 数据库可以 select 查询到数据来进行加锁操作
如何来实现隔离性
到这里我们已经讨论完事务的隔离级别每一个隔离级别可能遇到的异常情况以及避免这些异常情况的具体技术方案最后我们一起来讨论一下事务的隔离性是如何实现的
既然事务的隔离性是用来确保多个事务并发执行时的正确性的那么我们就可以依据应用程序开发中经常使用的并发控制策略来思考事务的隔离性如何实现这样就可以轻松得出如下的几个方法
首先最容易想到的是通过锁来实现事务的隔离性对于锁的方案最简单的策略是整个数据库只有一把互斥锁持有锁的事务可以执行其他的事务只能等待但是这个策略有很明显的问题那就是锁的粒度太粗会导致整个数据库的并发度变为 1
不过我们可以进行优化为事务所操作的每一块数据都分配一把锁通过降低锁的粒度来增加事务的并发度同时相对于互斥锁来说读写锁是一个更好的选择通过读写锁多个事务对同一块数据的读写和写写操作会相互阻塞但却能允许多个读操作并发进行
这样我们就得到了一个事务的并发模型但是一个事务通常由多个操作组成那么一个事务在持有锁修改某一个数据后不能立即释放锁如果立即释放锁在其他的事务读到这个修改或者基于这个修改进行写入操作当前事务却因为后续操作出现问题而回滚的时候就会出现脏读或脏写的情况
对于这个问题有一个解决方法即事务对于它持有的锁在当前的数据操作完成后不能立即释放需要等事务结束提交或者回滚完成后才能释放锁这个加锁的方式就是两阶段锁2PL第一阶段当事务正在执行时获取锁第二阶段在事务结束时释放所有的锁
那么现在是否就得到了可串行化的隔离性呢其实还不是的我们现在还没有解决幻读和写倾斜的问题幻读指的是其他的事务改变了当前事务的查询结果在幻读的情况下可能会导致写倾斜比如前面提到的例子当订阅会议室的事务进行 select 操作时由于会议室还没有被订阅所以数据库没有办法对订阅记录加锁这样多个事务同时操作就会导致一个会议室在同一个时间内出现多个订阅记录的异常情况
关于这个问题我们可以通过谓词锁Predicate Lock来解决它类似于前面描述的读写/互斥锁但是它的加锁对象不属于特定的对象例如表中的一行它属于所有符合某些搜索条件的对象如果对符合下面 SELECT 条件的对象加锁
SELECT * FROM bookings WHERE room_id = 888 AND start_time < 2022-02-02 13:00 AND end_time > 2022-02-02 12:00;
这样就可以避免一个会议室在同一个时间内被订阅多次的情况了。同时间隙锁Next-Key Locking也可以解决这个问题它是关于谓词锁的简化以及性能更好的一个实现。
其次我们可以通过多版本并发控制MVCC , Multi-Version Concurrency Control实现隔离性。数据库为每一个写操作创建了一个新的版本同时给每一个对象保留了多个不同的提交版本读操作读取历史提交的版本这样对同一个数据来说只有写写事务会发生冲突读读事务和读写事务是不会发生冲突的。对于写写冲突的问题可以通过加锁的方式来解决不过对于 MVCC 来说,相对于悲观锁,乐观锁是一个更常见的选择。
另外,通过 MVCC 来实现隔离性,由于读操作都是读取旧版本的数据,所以数据库需要知道哪些读取结果可能已经改变了,然后中止事务,不然就会导致写倾斜的问题出现。这需要数据库能够检测出异常情况,然后中止事务,而实现这个异常检测机制的 MVCC 我们称为可序列化快照隔离SSI , Serializable Snapshot Isolation这是一个比较新的研究方向目前还处于快速发展中。
最后是一个最简单的方式,通过避免并发的情况出现,在单个线程上按顺序一次只执行一个事务。这个方式避免了并发的出现,但是也失去了并发带来的多机多核的计算能力提升,目前在一些基于内存的数据库上使用过,比如 Redis ,同时它也在研发和发展中。
总结
本节课中,我们先掌握了有哪些隔离级别,以及每一个隔离级别可能出现的异常情况,这样在业务开发的过程中,我们对程序可能出现的异常情况就心中有数了。
其次,我们一起学习了如何避免异常情况的出现,在以后的业务选型过程中,我们不仅知道如何来选择数据库的隔离级别,也知道了当数据库的隔离级别不能调整时,如何通过应用开发手段来避免一些异常情况。
最后,我们讨论了如何实现数据库的隔离级别,这个过程能帮我们更深刻地理解隔离性的知识和原理。
思考题
你能在银行转账的业务场景下,举一个出现写倾斜的例子吗?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,112 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 事务(四):持久性,吃一碗粉就付一碗粉的钱
你好,我是陈现麟。
通过上节课的学习,我们掌握了通过 MVCC 和 2PL 实现分布式事务隔离性的技术原理,并且也明白了隔离级别是事务在正确性和性能之间的一个权衡,以后在极客时间的业务研发中,我们就可以根据业务特点,对事情的隔离级别做出正确的选择了。
虽然事务在有了一致性、原子性和隔离性的保障后,已经可以很好地保障业务在各种使用场景下的正确性了,但是如果机器突然断电或者崩溃,导致已经提交成功的事务数据丢失了,最终也就功亏一篑了。
所以在这节课中,我们将一起来讨论如何确保机器在突然断电、崩溃等异常情况下,不会将已经成功的事务数据丢失掉的问题,即事务的持久性。
我们首先会通过持久性的定义分析出它面临的挑战,然后再一起讨论一下如何通过非易失性存储来保障事务的持久性,最后一起讨论在分布式系统中,如何通过数据复制来进一步提高事务的持久性。
持久性的挑战
持久性在事务中的定义是,如果一个事务已经提交,不论什么原因,它产生的结果都是永久存在的,这保证了事务的结果不会丢失。通过持久性的定义,我们会发现要保证事务的持久性,一个显而易见的思路就是,将事务的结果立即写入到非易失性存储设备中,比如 SSD 硬盘和 SATA 硬盘等,并且写入的副本数越多,持久性就越高。
但是理想很完美,现实却很骨感,将数据写入到硬盘中其实是非常消耗性能的。如果将每一个事务所有的操作结果,都实时写入到持久化的存储设备中,这样的数据库几乎就是不可用的,更不用说多副本的写入了,那么我们如何来解决存储设备的写入性能和事务持久性之间的矛盾呢?
如何通过非易失性存储保障持久性
关于这个问题,我们需要从磁盘设备的特性开始说起。对于 SATA 硬盘来说,可以将它简单理解为一个有很多同心圆的圆盘,在写入数据的时候,会经历以下几个步骤:
寻道,找到数据所在的同心圆,这个时间是毫秒级别的;
寻址,找到数据所在的同心圆的位置,这个时间也是毫秒级别的;
开始读写数据,每秒可以读写的数据量为 100M 级别的数据,这个是非常快的。
我们可以从上面看出,如果没有寻道和寻址这两个步骤, SATA 硬盘的性能其实是非常不错的。那么如何避免寻道和寻址呢?如果第一次寻道和寻址后,持续对数据进行大量的读写,即顺序读写,是可以忽略寻道和寻址的时间消耗的。而对应顺序读写的是随机读写,它每一次读写的数据量很小,并且数据位置不相邻,都需要先寻道、寻址,然后才能进行数据读写,所以随机读写的性能是非常差的。
对于 SSD 硬盘,寻址的情况则大大改观,不需要像 SATA 硬盘一样机械地寻道、寻址它可以通过电路直接获得读写的地址。但是SSD 硬盘与传统的 SATA 硬盘有一点不同,即它不能够覆盖写,所以对于已经存在数据的 SSD 磁盘来说,一次数据的写入需要分为 2 个步骤:
擦除 SSD 上已有的数据;
写入新的数据。
但是对于 SSD 来说,一般每次写入的最小单位为 Page ,一个 Page 的大小为 4KB而每次擦除的大小单位为 Block Block 通常由 64 或 128 个 Page 组成。
由此看出, SSD 的写入与擦除的单位大小不匹配,那么如果仅仅是要修改一个 Page 的数据,在单个 Block 之中没有了空余的 Page 时,需要先读取 Block 的内容,然后擦除一个 Block 的数据,再将 Block 的内容和修改的内容进行合并,写入一个 Block 的数据。而这就会导致原本只需要写入 4KB 的数据,最终却写入了 64 倍甚至是 128 倍的数据,出现写放大的问题。
从上面的讨论中,我们发现对于 SSD 磁盘来说,写放大是无法避免的,相比于顺序写入,随机写入会大大加剧写放大的问题。
总而言之,不论是 SATA 硬盘还是 SSD 硬盘,从硬盘自身的特点来说,顺序读写的性能都要远远高于随机读写。另外从系统的角度来看,顺序读写在预读和缓存命中率等方面也要大大优于随机读写。
现在,我们就可以回答存储设备的写入性能和事务持久性之间矛盾的问题了:由于事务的持久性是必须的,如果一个事务已经提交,不论什么原因,它产生的结果都是永久存在的,所以对于单节点来说,我们可以先在内存中将事务的操作完成,然后将处理的结果顺序写入日志文件中,这就避免了事务操作结果随机写入存储的性能问题了。
然后我们再提交事务,这样一来,哪怕事务提交后,机器立即崩溃了,在机器故障恢复后,系统依然能通过日志文件,恢复已经提交的事务。
所以通过顺序写入日志的形式避免了非易失性存储设备随机写入性能差的问题达到了事务提交时所有事务操作结果都写入存储设备的目的。在这个时候即使系统崩溃事务的持久性也是有保障的。我们把这种通过顺序写入日志的形式称之为重做日志RedoLog或预写日志Write Ahead Log
如何通过数据冗余保障持久性
通过 Redo Log 或 WAL是否可以完美地解决事务持久性的问题呢其实还是不够的。虽然 Redo Log 能保证系统在崩溃、重启等问题出现时的持久性,但是当存储设备出现了故障,比如数据都不可读的时候,还是会出现即使事务已经提交成功,但是事务结果却丢失的情况。那么这个问题应该如何处理呢?
有一种思路是,通过磁盘阵列,从磁盘内部通过冗余数据来解决。比如 RAID 1 ,我们将多块硬盘组成一个磁盘阵列,磁盘阵列中每块磁盘都有一个或多个是副本磁盘。事务的每一次写入都同时写入所有的副本硬盘,这样只要不是所有的副本磁盘同时出现故障的情况,我们就都可以正常从磁盘上读到数据,不会影响事务的持久性。还有一种磁盘阵列的方式是 RAID 5 ,它是通过冗余校验数据的方法来保障持久性。
磁盘阵列的方法确实可以解决事务的持久性问题,但是由于磁盘阵列上多块硬盘的地理位置通常都是在一起的,这样如果出现地震、火灾和洪水等自然灾害时,可能会导致整个磁盘阵列上的硬盘都不可用,那么事务的持久性就不能被保证了。
而另外一个思路是通过增加副本,通过网络复制数据来解决。其实这个问题在“复制”系列课程中已经详细讨论过了,但是对于事务的场景来说,由于数据的复制必须是线性一致性的,所以我们只能采用同步的主从复制,但是这个方式在性能和可用性方面都存在问题。
性能问题:一次写入必须等所有的节点都写入成功,整体的写入性能取决于最慢的节点的写入性能,并且网络的不确定性会加剧性能问题。
可用性问题:对于同步复制来说,如果一个节点出现故障,就会导致写入失败,非常影响系统整体的可用性。
对于事务场景,如果我们不采用同步的主从复制,是否有其他的办法来解决呢?
其实我们可以通过 Raft 或者 Paxos 之类的共识算法来解决。对于数据复制到多个副本来说,其实就是多个副本对写入的结果达成共识,利用 Raft 或者 Paxos 之类的共识算法进行数据的复制,可以实现线性一致性,同时共识算法可以避免同步主从复制在性能、可用性问题和磁盘阵列多副本地理位置相近的问题。
性能问题:一次写入只需要等大多数的节点都写入成功即可,整体的写入性能取决于最快的大多数节点的写入性能。
可用性问题:只要出现故障的节点数不超过大多数,系统就会写入成功,它能容忍少数节点的故障。
地理位置相近的问题:数据通过网络复制,可以将副本分布到不同的数据中心、城市或者大洲,进一步提高事务的持久性。
对于共识算法,我们会在后面的课程“一致性与共识”中详细讨论,在这节课里,你只要知道对于存储系统内部的多节点数据副本,一般都通过共识算法来解决即可。
到这里,我们就学习完了“事务”序列的课程,这里从事务的四个特性的角度,总结一下:
一致性C是指事务的正确性需要底层数据的线性一致性事务层的原子性、隔离性、持久性以及数据库层面的约束检测和应用层的约束检测来保证。
原子性A是指事务操作的不可分割性一般通过原子提交协议 2PC 或 3PC 来保障。
隔离性I是指事务操作在并发控制一般数据库都提供弱隔离性是数据库在性能和正确性之间的衡权一般通过 MVCC 或者 2PL 来实现。
持久性D是指已提交的事务结果不可丢失一般在单机上通过非易失性存储来保障在分布式场景下通过数据冗余来保障。
总结
本节课,我们知道了事务在实现持久性的过程中,会面临性能和可用性这两个方面的挑战。
首先,要保障事务在系统宕机情况下的持久性,必须保证事务的操作结果能够立即保存到硬盘之类的非易失性存储中,但是不论是 SATA 硬盘还是 SSD 硬盘对于这一类随机读写操作都会面临严重的性能问题目前我们主要是通过重做日志RedoLog或预写日志Write Ahead Log将随机读写转化为顺序读写来提高事务的性能。
其次,要保障事务在磁盘故障情况下的持久性,必须将数据复制到多块磁盘上,这节课我们介绍了两种思路:一是通过磁盘阵列,从磁盘内部复制数据来解决;另一种是通过外部的数据复制来解决。
其中,磁盘阵列的多块硬盘的地理位置通常都是在一起的,地震、火灾和洪水等自然灾害,可能会导致整个磁盘阵列同时毁坏;而外部的数据复制方法需要保证数据的强一致性,它会面临性能和可用性的问题,这里我们主要通过 Raft 或者 Paxos 之类的共识算法来解决。
最后,我们可以看到,事务的持久性会让事务的结果被持久保存下来,不会出现因为数据丢失、毁坏而导致不认账的情况,这也就是本节课标题提到的“吃一碗粉就付一碗粉的钱”的真正含义。
思考题
通过课程的学习我们知道了存储设备的特性会影响存储引擎的设计同样业务存储的数据特点也会影响存储引擎的设计请你来思考一下如果我们的业务需要存储很多非常小的文件比如平均几十K应该怎么来设计存储引擎呢
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,155 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
26 一致性与共识(一):数据一致性都有哪些级别?
你好,我是陈现麟。
通过学习“事务”序列的内容,我们从事务的四个特性 ACID 的角度讨论了相关的知识与技术原理,这样在以后的工作中,事务对我们来说就不再是一个陌生和难懂的概念,而是越发清晰了。我们能清楚地知道事务能提供哪些保障,我们的代码逻辑可能会出现什么样的异常情况,以及怎么避免这些异常情况的出现。恭喜你在学习分布式的道路上又前进了一大步!
不过,在前面课程的学习中,我们经常会碰到两个概念:多副本数据的一致性和多节点的共识,比如在分布式锁、事务的原子性等场景中。其实在分布式系统中,一致性和共识是两个绕不过的话题,现在各种各样的分布式系统都是建立在一致性和共识之上的,可以说没有一致性和共识,就没有可用的分布式系统。
既然一致性和共识对于分布式系统来说这么关键,那么我们一定要好好掌握。可是,通过前面课程中对一致性和共识场景的讨论,你现在虽然对二者有了很多的感性认识,知道在什么场景下会遇到一致性和共识方面的问题,也知道一些具体的解决方案,但是如果要你具体介绍一致性和共识的话,心里是不是不太有底呢?
所以,从这节课开始,我们将一起花三节课的时间来解决这个问题。这一节课,我们先介绍一致性问题的来源,然后我们从一致性模型从强到弱的角度,来介绍几种经典的一致性的模型,并且一起讨论和对比各个一致性模型之间的差异。
一致性问题的来源
虽然数据一致性是分布式系统的基石,但是其实最早研究一致性的场景并不是分布式系统,而是多路处理器。不过我们可以将多路处理器理解为单机计算机系统内部的分布式场景,它有多个执行单元,每一个执行单元都有自己的存储(缓存),一个执行单元修改了自己存储中的一个数据后,这个数据在其他执行单元里面的副本就面临数据一致的问题。
当时间走到 1990 年代时,由于互联网公司的快速发展,单机系统在计算和存储方面都面临瓶颈,分布式是一个必然的选择,但是这也进一步放大了数据一致性面临的问题。对于数据的一致性,最理想的模型当然是表现得和一份数据完全一样,修改没有延迟,即所有的数据修改后立即被同步,但是这在现实世界中,数据的传播是需要时间的,所以理想的一致性模型是不存在的。
不过从应用层的角度来看,我们并不需要理想的一致性模型,只需要一致性模型能满足业务场景的需求就足够了,比如在一些统计点赞数的场景中,是能容忍一定的误差的,而评论之类的场景中,可能只要有因果关系的操作顺序一致就可以了。
同时由于一致性要求越高,实现的难度和性能消耗就越大,所以我们可以通过评估业务场景来降低数据一致性的要求,这样人们就定义了不同的一致性模型来满足不同的需求。是不是发现了这里的思考逻辑和事务的隔离级别一样了?都是正确性和性能之前的衡权。
讨论完了一致性问题的来源后,接下来我们从客户端读写操作的维度来讨论一致性模型。由于一致性模型的定义大多是基于数学语言来定义的,理解起来有一定的难度,所以在课程中,我们尽量用简单的语言来讨论。
接下来,我们将讨论四个经典且常见的一致性模型:线性一致性、顺序一致性、因果一致性和最终一致性。
线性一致性
线性一致性模型Linearizability是 Herlihy 和 Wing 等于 1987 年在论文 “Axioms for Concurrent Objects” 中提出的线性一致性也被称为原子一致性Atomic Consistency、强一致性Strong Consistency、立即一致性Immediate Consistency和外部一致性External Consistency
线性一致性是非常重要的一个一致性模型在分布性锁、Leader 选举、唯一性约束等很多场景都可以看到它的身影。对于线性一致性的描述,我们可以从读写操作的维度来描述。
对于写操作来说,任意两个写操作 x1 和 x2
如果写 x1 操作和写 x2 操作有重叠,那么可能 x1 覆盖 x2也可能 x2 覆盖 x1
如果写 x1 操作在写 x2 开始前完成,那么 x2 一定覆盖 x1。
对于读操作来说:
写操作完成后,所有的客户端都能立即观察到;
对于多个客户端来说,必须读取到一样的顺序。
我们可以看到,线性一致性保证了所有的读取都可以读到最新写入的值,即一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。在线性一致性模型中不论是数据的覆盖顺序还是读取顺序,都是按时间线从旧值向新值移动,而不会出现旧值反转的情况。
顺序一致性
顺序一致性模型Sequential Consistency是 Leslie Lamport 在 1979 年发表的论文 “How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Program” 中提出的,在论文中具体的定义如下:
A multiprocessor is said to be sequentially consistent if the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program.-
如果任何执行的结果与所有处理器的操作都以某种顺序执行的结果相同,并且每个单独的处理器的操作按照其程序顺序出现在该序列中,则称多处理器是顺序一致的。
对于顺序一致性,论文中的定义虽然严谨,但是理解起来也是有难度的,它需要掌握一些前置的定义,比如 “program order”。不过在这里我们依然可以用简单的语言来描述。
对于写操作来说,任意两个写操作 x1 和 x2
如果写 x1 操作和写 x2 操作有重叠,那么可能 x1 覆盖 x2也可能 x2 覆盖 x1
当写 x1 操作在写 x2 开始前完成,如果两个写操作没有因果关系,当写 x1 操作在写 x2 开始前完成,那么有可能 x1 覆盖 x2也有可能 x2 覆盖 x1如果两个写操作有因果关系即同一台机器节点先写 x1或者先看到 x1 然后再写 x2则所有节点必须用 x2 覆盖 x1。
对于读操作来说:
如果写操作 x2 覆盖 x1 完成,那么如果一个客户端到 x2 后,它就无法读取到 x1 了,但是这个时候,其他的客户端还可以观察到 x1
对于多个客户端来说,必须观察到一样的顺序。
相对于线性一致性来说,顺序一致性在一致性方面有两点放松:
对于写操作,对没有因果关系的非并发写入操作,不要求严格按时间排序;
对于读操作,只要求所有的客户端观察到的顺序一致性,不要求写入后,所有的客户端都必须读取新值。
因果一致性
因果一致性模型Causal Consistency是 Mustaque Ahamad, Gil Neiger, James E. Burns, Prince Kohli, Phillip W. Hutto 在 1991 年发表的论文 “Causal memory: definitions, implementation, and programming” 中提出的一种一致性强度低于顺序一致性的模型。在这里,我们依然从读写操作的维度来进行描述。
对于写操作来说,任意两个写操作 x1 和 x2
如果两个写操作没有因果关系,那么写 x1 操作在写 x2 开始前完成,有的节点是 x1 覆盖 x2有的节点则 x2 可能覆盖 x1
如果两个写操作有因果关系,即同一台机器节点先写 x1或者先看到 x1 然后再写 x2则所有节点必须用 x2 覆盖 x1。
对于读操作来说:
如果写操作 x2 覆盖 x1 完成,那么如果一个客户端到 x2 后,它就无法读取到 x1 了,但是这个时候,其他的客户端还可以观察到 x1。
相对于顺序一致性来说,因果一致性在一致性方面有两点放松:
对于写操作,对没有因果关系的非并发写入操作,不仅不要求按时间排序,还不再要求节点之间的写入顺序一致了;
对于读操作,由于对非并发写入顺序不再要求一致性,所以自然也无法要求多个客户端必须观察到一样的顺序。
最终一致性
最终一致性模型Eventual Consistency是 Amazon 的 CTO Werner Vogels 在 2009 年发表的一篇论文 “Eventual Consistency” 里提出的,它是 Amazon 基于 Dynamo 等系统的实战经验所总结的一种很务实的实现,它不同于前面几种由大学计算机科学的教授提出的一致性模型,所以也没有非常学院派清晰的定义,但是我们依然可以从读写操作的维度来描述它。
对于同一台机器的两个写操作 x1 和 x2 来说:
如果写 x1 操作在写 x2 开始前完成,那么所有节点在最终某时间点后,都会用 x2 覆盖 x1。
对于读操作来说:
在数据达到最终一致性的过程中,客户端的多次观察可以看到的结果是 x1 和 x2 中的任意值;
在数据达到最终一致性的过程后,所有客户端都将只能观察到 x2。
我们可以看出来“最终”是一个模糊的、不确定的概念它是没有明确上限的Vogels 提出这个不一致的时间窗口可能是由通信延迟、负载和复制次数造成的,但是最终所有进程的观点都一致,这个不一致的时间窗口可能是几秒也可能是几天。
所以,最终一致性是一个一致性非常低的模型,但是它能非常高性能地实现,在一些业务量非常大,但是对一致性要求不高的场景,是非常推荐使用的。
总结
到这里,我们已经讨论完了几种最经典也最常见的一致性模型,现在我们来对这节课的内容做一个总结。
首先,我们讨论了一致性问题最早出现在多路处理器的场景,现在在分布式系统中广泛出现。同时,我们还得出了一个结论:对一致性模型进行分级是正确性和性能之间的一个权衡。
接着,我们从一致性模型强弱的维度,讨论了四个经典一致性模型的定义与差异,这里我们再从其他的维度描述一下,让你对一致性模型有一个更立体的理解。
第一,现在可以实现的一致性级别最强的是线性一致性,它是指所有进程看到的事件历史一致有序,并符合时间先后顺序, 单个进程遵守 program order并且有 total order。
第二,是顺序一致性,它是指所有进程看到的事件历史一致有序,但不需要符合时间先后顺序, 单个进程遵守 program order也有 total order。
第三,是因果一致性,它是指所有进程看到的因果事件历史一致有序,单个进程遵守 program order不对没有因果关系的并发排序。
第四,是最终一致性,它是指所有进程互相看到的写无序,但最终一致。不对跨进程的消息排序。在课程“复制(三):最早的数据复制方式竟然是无主复制?”中讨论的 Quorum 机制就是最终一致性。
思考题
通过对一致性模型的学习,你可以通过读写操作序列,分别举出线性一致性、顺序一致性、因果一致性和最终一致性的例子吗?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,104 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
27 一致性与共识(二):它们是鸡生蛋还是蛋生鸡?
你好,我是陈现麟。
通过上节课的学习,我们了解了一致性模型的发展历史,同时还掌握了各个一致性模型之间的强弱差异,这样在极客时间后端技术的选型和演进过程中,你就能够做出最适合业务场景的选择了,这对于我们搭建分布式系统是非常关键的一个权衡。
其实一致性和共识是两个如影随形的概念,我们在讨论一致性的时候,总是会提到共识,同时我们在研究共识的时候,一致性也是不能绕过的话题。那么,你一定会很好奇它们之间的关系是什么?一致性和共识是像鸡生蛋和蛋生鸡这种非常紧密的关系呢?还是其他的比较弱的关系呢?
在这节课中,我们主要来讨论一致性与共识之间的关系,一方面解开你的疑问,另一方面通过探讨它们之间的关系,让你能够进一步理解一致性和共识。我们先一起来了解共识问题的场景与定义,然后分析达成共识所面临的挑战,最后再来探讨一致性和共识的关系。
共识问题的定义
在分布式系统中共识Consensus问题是最重要也是最基本的问题之一简单来说它就是多个节点进程对某一个事情达成一致的结果。在分布式系统中我们经常碰到这样的场景比如在主从复制的模型中需要在多个节点选举出 Leader 节点。由于有且只能有一个 Leader 节点,所以多个节点必须就哪一个节点是 Leader 这个决定达成一致。那么共识算法经常用于像选举 Leader 、分布式锁服务这样,有且只有一个能胜出的场景。
在讨论共识问题的时候我们通常会做这样的形式化定义一个或多个节点可以提议Propose某些值而共识算法决定Decide采用其中某一个节点提议的某个值。比如在 Leader 选举的例子中,每一个节点都可以提议自己为 Leader 节点,而共识算法会让所有的节点对某一个节点为 Leader 达成一致。
所以,通过上面的讨论,我们可以得出共识算法必须满足的四个条件,具体如下。
一致同意Uniform Agreement所有协议的节点必须接受相同的决议。
诚实性Integrity所有节点不能反悔即对一项提议一个节点不能做两次决定。
合法性Validity如果决定了值 v ,则 v 一定是由某个节点所提议的。
可终止性Termination如果节点不崩溃则一定可以达成决议。
其中,一致同意和诚实性定义了共识的核心思想:所有人都决定了相同的结果,并且一旦决定了,就不能再改变。
合法性主要是为了排除没有意义的解决方案。例如无论节点提议了什么值都可以让所有节点始终以某一个固定值如nil达成共识的算法这个算法满足一致同意和诚实性但是由于达成共识的值是固定的不是由某一个节点提出的所以不满足合法性。
可终止性确保了,共识算法在部分节点故障的情况下,其他的节点也能达成一致,可终止性让共识算法能够容错。如果共识算法不需要容错是很容易实现的,比如将某一个节点指定为共识算法的“独裁者”,其他的节点必须同意该节点做出的所有决定。不过这个算法的问题是如果“独裁者”节点出现故障,系统就将无法达成共识了。
其实 2PC 协议就是不满足可终止性的共识协议。在 2PC 中,协调者节点就是“独裁者”节点,它在第一阶段通过收集参与者节点 Prepare 的响应做出决定,但是当协调者故障时,参与者就无法决定提交还是中止了。
到这里,你是否觉得共识问题非常简单呢?其实不然,共识问题是一个非常难的问题,如果处理不好共识,很有可能会出现各种问题或故障,比如在分布式锁服务 Leader 选举的场景中,如果出现两个 Leader那么整个分布式锁服务就进入了脑裂的状态锁的互斥性将会被破坏使业务上出现不可预期的情况。
达成共识的挑战
我们已经知道共识处理不好,可能会出现各种问题或故障,那么接下来,我们就从共识理论出发,分析达成共识面临的挑战,提前发现问题,解决问题。
第一个挑战是,在异步网络模型中,如果一个节点出现崩溃,那么共识就将无法达成,这就是大名鼎鼎的 “ FLP 不可能”。但是在分布式系统中,节点的故障是我们必须要面对的问题,如果以 Leader 选举的场景来讨论,需要达成共识的一个主要场景就是, Leader 节点崩溃了,需要重新选择一个新的 Leader ,选择新的 Leader 需要达成共识,但是因为 “ FLP 不可能”,所以共识不能在节点崩溃的时候达成。
这样看来问题就无解了,但是在实际应用中,我们是可以通过 Raft 或者 Paxos 之类的共识算法来解决这一类问题的,这是否和 “FLP 不可能” 冲突了呢?
其实出现这个问题的根本原因是,在异步网络模型的定义中,网络中消息的传递延迟和节点的处理延迟是无上限的,所以对于消息是不能使用任何时钟或超时的,这样就导致在节点出现崩溃的时候,我们无法判断是否有节点崩溃,只能无限等待下去,使共识算法不能满足“可终止性”;但是在真实的环境中,我们可以允许共识算法使用超时或其他方法,来识别可疑的崩溃节点(即使有时怀疑是错误的),这样就避免了无限等待,使达成共识成为一个可行的事情。
第二个挑战与我们对分布式系统的故障模型定义有关。一般来说,在分布式系统中,我们对故障模型的定义是“崩溃-恢复失败”Crash-Recovery Failure模型。简单来说就是在一个节点很长时间没有返回消息时我们不能确定它是因为崩溃还是因为网络或者计算速度过慢等原因导致的。其中网络或者计算速度过慢等原因都是可以恢复的这个模型和我们现在的分布式模型是最匹配的。
而像 Raft 和 Paxos 之类的共识算法,我们可以在“崩溃-恢复失败”Crash-Recovery Failure模型上通过超时来识别可疑的崩溃节点这就解决了一个问题一个或多个节点可以提议Propose某些值而共识算法决定Decide采用其中某一个节点提议的某个值。
除此之外还有“拜占庭失败”Byzantine Failure和“崩溃-停止失败”Crash-Stop Failure等模型。其中“拜占庭失败”Byzantine Failure模型在“崩溃-恢复失败”Crash-Recovery Failure模型上增加了节点会主动伪造和发布虚假消息的情况由于这个情况在内网的分布式环境中几乎不会出现并且要解决它的代价非常高所以一般的共识算法不会考虑解决“拜占庭失败”Byzantine Failure 模型下的共识问题。
但是,在公网的分布式环境中,是需要解决这个问题的,例如比特币是通过“工作量证明”这样的算法,利用经济学原理,让节点造假的成本高于收益,来避免节点发布虚假消息的。
而“崩溃-停止失败” Crash-Stop Failure模型在“崩溃-恢复失败”Crash-Recovery Failure模型上去掉了节点崩溃后的不确定性如果一个节点很长时间没有返回消息那么它就是崩溃了不会再回复什么消息即崩溃后就立即停止。
但是,在实际的分布式场景中,由于网络或者计算太慢而故障的节点,待恢复后,很久之前响应的消息是会正常出现的。所以,如果共识算法只能处理“崩溃-停止失败”Crash-Stop Failure模型就不能适应我们实际的网络环境了。接下来我们总结一下课程中提到的三种故障模型如下表所示。
最后,还要特别强调一点,我们应该尽量选择像 ZooKeeper 和 etcd 这样,开源并且经过了广泛应用而被验证的程序,来为我们的应用提供共识能力,而不是自己再依据 Raft 或 Paxos 算法实现一个共识算法。因为相对于实现一个共识算法,证明共识算法实现的正确性是一个更难的问题。
一致性和共识的关系
通过学习共识问题的定义和挑战,我们对共识问题有了一定的了解,接下来,我们将一致性和共识结合,讨论一下它们之间的关系,这里的一致性我们定义为一致性最强的线性一致性。
在本专栏第 19 讲“主从复制”的课程中,我们讨论过主从复制:主节点承接所有的写入操作,然后以相同的顺序将它们应用到从节点,从而使主、从副本节点的数据保持最终一致性。
如果在主节点或同步副本的从节点上读取数据,那么就是线性一致性的。当然如果数据库的读为快照读,由于不能读到最新版本的数据,这个情况下就不是线性一致性的。
到这里,你是否觉得线性一致性非常容易实现,而且和共识算法也没有什么关系呢?其实不然,在主从复制的模型中,如果主节点不出现故障,那么一切都非常美好,但是如果主节点发生崩溃了,应该怎么办呢?
首先,最简单的办法是等待主节点修复,如果主节点无法快速修复或者无法修复,那么系统的高可用就名存实亡了。对于等待主节点恢复的方式,我们可以理解为系统对之前达成主节点的共识是不可改变的。
其次,人工切换主节点,这个方案是可行的,不过它的时间不确定,或长或短。如果出故障的时候,找不到合适的人来操作,就会严重影响系统的可用性。对于这个方式,我们可以理解为,系统对于主节点的共识是由操作人员来提供的,这是一个来自“上帝”视角的共识。
最后,让程序自动切换主节点,这就需要其余正常运行的节点,来选择一个新的主节点,这样就回到了 Leader 选举的场景,分布式系统中的共识问题就出现了。这个方式是通过共识算法,让系统对一个新 Leader 节点达成共识,避免多个 Leader 节点出现,导致脑裂的情况发生。
到这里,我们就明白了,线性一致性是数据存储系统对外表现的一种形式,即好像只有一个数据副本,但是在实现数据一致性,实现容错的时候,我们需要共识算法的帮助。
当然,这里要特别注意,我们通过共识算法,除了可以实现线性一致性,也可以实现顺序一致性等其他的数据一致性,共识算法是用来满足线性一致性的容错性的。同时,不使用共识算法,我们也可以实现数据的线性一致性,比如 ABD 和 SCD broadcast 之类的非共识算法,也可以实现线性一致性。
总而言之,我们通过共识算法,可以实现高可用的线性一致性,以及其他的一致性存储系统,在这种情况下,共识算法是手段,一致性是目的,先有共识算法,后有高可用的线性一致性系统。同时,不通过共识算法,我们也可以用其他的方法,来实现线性一致性等其他的一致性,在这种情况下,共识和一致性就没有关系了。不过,目前通过共识算法,来实现高可用的线性一致性模型,是一个最常见的选择。
总结
本节课中,我们通过 Leader 选举的业务场景,讨论了共识问题的定义,并且得出了一个共识算法需要满足四个要求:一致同意、诚实性、合法性和可终止性。现在,你不仅可以识别出业务场景中的共识问题,还能深刻理解这些场景需要引入共识的原因。
接着,我们一起分析了达成共识所面临的挑战,其中让人震惊的是“ FLP 不可能”原理竟然证明了,在异步网络中,如果一个节点出现故障,共识就不可能达成。不过这种理论上的不可能,我们可以在现实中通过超时等机制解决。同时,我们还讨论了分布式系统中的几种故障模型,这让我们可以更好地理解分布式理论的研究对象,以及现实的分布式系统所面临的问题。
最后,我们讨论了一致性和共识的关系,得出了具体结论:通过共识算法,我们可以实现高可用的线性一致性,但是共识算法不是线性一致性的必要条件。到这里,你一定对一致性和共识有了清晰的认识。
思考题
本课中,我们明白了一致性和共识的关系,请你继续思考一下,共识和高可用之间有什么关系呢?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,77 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
28 一致性与共识(三):共识与事务之间道不明的关系
你好,我是陈现麟。
通过上节课的学习,我们知道了共识问题的使用场景、定义和经典的算法,并且从共识的角度深入探讨了一致性和共识的关系,这让我们对一致性和共识的理解更进了一步。
你应该还记得,在课程第 23 讲“原子性”中提到过,当我们在实现事务的原子性时,采用的是 2PC 或 3PC 这样的共识协议;同时,在课程第 25 讲“持久性”中我们也讲过,通过线性一致性算法来复制数据,可以提高事务的持久性。另外,最显而易见的就是,事务的 ACID 中C 就是一致性。
那么,你一定在想,在分布式事务中,共识与事务之间是什么关系呢?是不是像共识和线性一致性一样,共识是方法和手段,事务的一致性是目的呢?
在这节课中,我们就一起来讨论一下共识与事务之间的关系。我们先从事务的特性 ACID 的维度,一一来分析事务与共识的关系,然后以它们的关系为基础,探讨事务的本质问题,让你深入理解事务与共识、一致性之间的联系,从根本上理解分布式事务,为以后的工作打下一个坚实的基础。
事务与共识的关系
通过课程第 22 讲“一致性”的学习,我们知道了事务的最终目的是实现一致性,即确保事务正确地将数据从一个一致性的状态,变换到另一个一致性的状态。为了达成这个目标,除了需要应用层的逻辑保证外,在事务层面还需要通过原子性、隔离性和持久性这三个特性一起协作。很有意思的一件事情是,在分布式事务中,事务这三个特性都与共识有一定的关系,下面我们来一一讨论一下。
首先,对于原子性来说,在分布式系统中,需要通过 2PC 或 3PC 之类的原子提交协议来实现。以 2PC 为例,协调者在第一阶段通过接收所有参与者对 Prepare 请求的响应,才能最终确定当前的事务是提交还是中止,而这就是典型的共识场景:所有的参与者都同意,就提交事务;如果有参与者不同意,就中止事务。所以,我们认为 2PC 或 3PC 之类的原子提交协议是共识协议。
另外,还要特别注意一点,我们在上节课讨论过, 2PC 不是一个完备的共识算法,它满足共识算法的一致同意、诚实性以及合法性,但是在协调者出现故障的时候,并不能满足共识算法的可终止性。
其次,对于隔离性来说,我们一般通过 2PL 或 MVCC 的方式来实现,可是它们能正确实现隔离性的前提条件,建立在底层数据为单副本的基础之上。但是在分布式系统中,为了系统的高可用,底层存储的数据是多副本,为了对事务操作表现出单副本的状态,数据的复制协议必须是线性一致性的,而线性一致性的数据复制协议,通常都是通过共识算法来实现的。学到这里,你会发现特别有意思,我们从事务的隔离性深层次去探索,就会触碰到共识这个话题。
最后,对于持久性来说,我们在课程第 25 讲中讨论过,在分布式系统中,为了进一步提高事务的持久性,我们会对数据进行复制,通过冗余来提高持久性。虽然数据复制可以不需要共识,但是就像上一段的讨论那样,为了保障事务的隔离性,数据的复制必须是线性一致性的。所以我们可以得出,事务为了持久性而引入了数据复制,但是为了保障隔离性,只能选择线性一致性的数据复制算法,而一旦涉及线性一致性,就说明我们又回到共识了。
通过上面的讨论,你是否会感觉到在分布式系统中,当我们为了实现一个确定性正确的程序,一步一步深挖下去,就一定会碰到共识问题呢?其实这一点很好理解,比如在现实生活中,多人合作完成一件事情,如果人们的意见不能达成一致,是很难将事情正确完成的。想要使他们的意见达成一致,就是共识问题了,人们通过沟通来达成共识,计算机节点之间通过交换信息来达成共识,本质上都是一样的。
事务的本质是什么
在本专栏中,我们特别用四节课的时间做了一个“事务”系列课程,主要有两方面原因。一方面为了说明在分布式系统中,事务占有非常重要的位置,另一方面是为了让你学习到与分布式事务相关的技术原理。
但是,这些知识都是从外向内来解释事务是什么,会让我们感觉到分布式事务涉及的技术原理非常繁多,但是正因为有了这些知识的铺垫,现在我们就可以从更深的维度去探讨事务,让分布式事务变得更加简单和清晰了。那么接下来,我们就来探讨一个问题,事务的本质是什么?
首先,我们简单回忆一下事务的隔离级别:读未提交 (Read Uncommitted)、读已提交 (Read Committed)、可重复读 (Repeatable Read)、快照隔离级别 (Snapshot Isolation) 和串行化 (Serializable) ,从隔离级别的名称和异常情况中,我们都不难发现,隔离级别都是从读异常情况的角度来定义的(其中,脏写和写倾斜也可以看成是,由于脏读和幻读导致的写异常),那么这是为什么呢?
其实这是由于事务面对的数据存储,是单副本数据或线性一致的多副本,单个写操作完成后,读操作都是可以立即读取到的,所以在单个写操作的层面,事务是不会出现异常情况的。但是,由于事务一般都涉及对多个数据对象的读写操作,为了避免并发事务的相互影响,事务需要将还未提交的写操作结果,与其他并发事务进行隔离处理,那么如何实现隔离呢?
既然写操作已经实际发生了,那就只能通过读操作进行隔离了,即将一个事务时间内多个离散的写操作,通过对读操作在并发事务之间隔离的方式,使事务的多个操作对外表现为一个原子操作一样。
接着,我们再来梳理一下数据一致性的模型。从课程第 26 讲“数据一致性都有哪些级别”的定义与讨论中,我们不难看出线性一致性、顺序一致性、因果一致性和最终一致性,这四种线性一致性模型讨论的都是,对单个数据对象操作时,单节点或多节点的多个写操作的顺序,以及复制时延的问题。在数据一致性的模型中,读异常都是由于对单个数据对象的写操作,在多个副本之间的不同原子同步导致的。
到这里,我们会发现事务和数据一致性是非常类似的,它们本质上都是期望它的一个完整操作是原子操作,研究的本质问题都是数据的一致性问题。
只不过事务对一个完整操作的定义是,一个事务内,对一个或多个数据对象的一个或多个读写操作,它需要解决的是对多个数据对象操作的一致性问题;而数据一致性对一个完整操作的定义是,在多个数据副本上对一个数据对象的写操作,它要解决的是单个数据操作,复制到多个副本上的一致性问题。
“一致性与共识”系列小结
到这里,“一致性与共识”系列课程就结束了,为了让你对这部分知识有一个整体的把握,以及充分的理解,接下来我们分别从一致性、共识以及它们之间的关系出发,做一个小结。
首先,数据的一致性模型定义了,一个数据对象在多个节点上有多个副本时,对外部读写表现出来的现象。数据一致性模型从强至弱分别为:线性一致性、顺序一致性、因果一致性和最终一致性。其中线性一致性是我们目前可以实现的一致性最强的模型,对于线性一致性的数据复制模型,我们可以认为它和操作单副本是一样的结果,基于它搭建的数据系统一般都是 CP 系统。
而一致性级别最弱的最终一致性,它只能确保数据最终会一致,并不能明确这个时间有多长。最终一致性牺牲了数据一定程度上的正确性,换取了高性能和高可用,在高并发的互联网场景中经常被使用,基于它搭建的数据系统一般都是 AP 系统。
其次,共识是指多个节点(进程)对某一个事情达成一致的结果,一个完备的共识算法需要满足四个要求:一致同意、诚实性、合法性和可终止性。共识算法主要用于解决 Leader 选举和分布式锁服务等分布式场景中,最底层、最基础的问题,所以基于 Leader 的线性一致性算法,通常都需要依赖共识算法来实现选举。
最后,通过讨论共识与分布式事务之间的关系,我们发现在事务的原子性、隔离性和持久性的实现中,都可以看到共识的身影,并且当我们对事务与数据的一致性进行比较后,发现事务是多个数据操作的一致性问题,而数据一致性则可以理解为,对多个副本的单个数据对象的事务问题。
总结
本节课中,我们先讨论了事务与共识的关系,发现它们之间有着非常密切的关系,世界的尽头在哪里我不知道,但是我可以明确地告诉你,分布式的尽头就是共识。
然后,我们通过分析分布式事务,并且与数据一致性做对比,发现事务可以理解为对多个数据操作的一致性问题,这样我们对分布式事务的理解就又多了一个维度。其实深入理解事务,是学习好分布式存储的基石,也会为你以后的工作打下一个坚实的基础。
最后,我们对“一致性与共识”系列课程进行了总结和梳理,相信你对于这些知识已经有了非常深入和系统的理解,恭喜你,在学习分布式系统的道路上,跨过了“一致性与共识”这一道坎。
思考题
在课程总结中,有这样一句话,“世界的尽头在哪里我不知道,但是我可以明确地告诉你,分布式的尽头就是共识”,欢迎你来分享一下对这句话的理解。
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,113 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
29 分布式计算技术的发展史:从单进程服务到 Service Mesh
你好,我是陈现麟。
通过学习“一致性与共识”系列的内容,我们掌握了一致性模型之间的差异,这让我们能够在工作中,依据自己的业务特点来做最合适的选择。并且我们也明白了什么是共识问题,以及在分布式系统中,共识为什么这么重要。最后,我们深入讨论了一致性、共识和事务之间的联系,通过比较和关联的方法,让你对这些知识建立了网状和系统性的认知。
同时,学习完“一致性与共识”系列课程,也意味着你已经完成了专栏中关于技术原理方面的学习,一路坚持到现在不是一件容易的事情,但是你一定感受到了学习与成长的乐趣,恭喜你!
接下来,我们将开始一段较为轻松,但是非常重要的学习历程。说它轻松是因为,我们不会再深入讨论分布式相关的技术原理,只会系统性地叙述分布式系统的发展历史;而说它重要是因为,虽然我们已经由浅入深地学习,并且网状地分析了分布式系统的技术原理,但是我们构建的知识网络还差最后一个维度,即时间或历史维度,那么在接下来的课程中,我们就一起来完成这画龙点睛的一笔。
从这节课开始,我们将一起花 2 节课的时间,讨论分布式系统的发展历史。这一节课,我们先介绍分布式业务系统的演进历史:从单进程服务到 Service Mesh 。为了让你更好地记忆和理解,我会将这一段演进历史,梳理为 5 个阶段去讨论和总结,具体分别为:史前期、探索期、萌芽期、爆发期和云原生期。
史前期
在分布式系统的史前期,最简单的形式是单进程系统:整个系统只有一个进程,并且运行在一个节点上。单进程系统是非常符合我们直觉的分布式系统的史前期形式,除此之外,还有一种情况也可以归类为分布式系统的史前期,下面我们接着来讨论一下。
对于服务端系统来说,高可用和高性能是无法回避的两个要求,而要达到这两个要求,最简单的方式就是通过多副本来实现:将单进程的程序复制到多台机器上,然后通过负载均衡将流量分发至多台机器上。
但是在这个系统中,多个副本的进程之间是不需要任何通信的,彼此之间也不会感知对方的存在,并且在架构层面,你会发现单进程系统和简单复制的多副本系统,它们都是单体架构,差异只在部署方式上。从严格定义来说,我们可以将这个系统称为集群,但它不是分布式系统。
所以在本课中,我们将单体架构的系统定义为史前期,史前期的时间大约从有计算机程序开始,到 1990 年代之前。
探索期与萌芽期
在史前期,人们通过对单体架构的系统进行集群化部署,解决了业务对高可用和高性能的需求,但是在互联网公司快速发展的过程中,单体架构逐渐在成本和效率方面,暴露出了很多的问题,这部分内容,我们在第 4 讲课程“注册发现”中详细讨论过,这里就不再重复了。
于是,在 1990 年代左右,人们开始探索一种新的架构——分布式业务系统架构,来解决这个问题。在探索的过程中,许许多多的科学家和工程师都贡献了自己的聪明才智,在 1990 年代为分布式业务系统打下了坚实的理论基础,特别是 1996 年 Gartner 公司提出了 SOA 的概念。
基于上述讨论,我认为分布式业务系统架构的探索期为 1990 年度,在这一时期,主要是对 SOA 架构进行探索。
到了 2002 年, Gartner 公司正式推出了 SOA 概念,从此单体架构快速向 SOA 架构迁移,所以在课程中,我们将 2000 年代定义为分布式业务系统架构的萌芽期。
相比于史前期的单体架构来说,探索与萌芽期的 SOA 架构有如下的特点:
单体架构所有的逻辑都在一个进程中,而 SOA 架构要求面向服务对业务的逻辑进行拆分。
被拆分的多个服务,需要通过 ESB 进行通信。
从此,单体架构慢慢退出了历史的舞台,面向服务进行拆分则变成了一个理所当然的常识。
爆发期
SOA 架构推广后,越来越多的人和公司开始使用 SOA 架构,在使用的过程中,服务的粒度慢慢变得更细,并且慢慢倾向于让不同的服务之间直接通信,而不需要借助 ESB 这样中心化的组件。
终于在 2014 年, Martin Fowler 和 James Lewis 在 SOA 的基础上,提出了微服务的架构。它相比于 SOA 架构来说,具体的差异如下。
服务的粒度拆分得更细,更加强调一个服务只做一个事情,并且做到最好。
去中心化,服务之间的通信不走 ESB 这样中心化的组件,而是由服务之间直接通信。
更加强调复用性,服务化和组件化更加彻底。
微服务架构更强调数据是服务私有的,其他服务不能直接访问服务的私有数据,只能通过服务提供的接口来获取。
通过上面的描述,我们可以看到微服务架构比 SOA 架构更加复杂,一个微服务中少则几百个服务,多则上千或更多的服务,所以通过人工运维一个微服务是低效并且不可能的,从此服务治理就开始变成了微服务的标配。关于服务治理相关的技术原理,在本专栏的 “分布式计算”中有非常详细的讨论,这里就不再重复了。
基于上述讨论,我认为分布式业务系统架构的爆发期为 2010 - 2015 年度,在这一时期, SOA 架构逐步被微服务架构所取代,分布式业务系统的架构开始进入微服务时代。 2016 年,开源 Spring Cloud 就是微服务架构的一个经典实现。
云原生期
微服务架构由于在工程上的成本和效率方面,能满足互联网公司快速迭代的需求,所以很快便风靡起来。
但是,从架构上来看,微服务的框架层(比如服务注册发现、熔断降级和负载均衡等)是以 SDK 的形式集成在服务代码中的,而框架层的 SDK 和服务的业务代码,在公司中通常都是由两个团队来开发和维护的:框架层的 SDK 由基础架构团队来开发和维护,业务代码由业务研发团队来开发和维护。
而服务的发布权限在服务的 Owner 业务研发手中,这就使基础架构团队和业务研发团队在程序发布的时候耦合了,基础架构团队想上线新的功能,只能去和业务研发团队沟通,可是业务研发团队的目标在业务上,就导致两个需要紧密协作的团队,出现目标不一致的情况,这是非常影响工作效率的。
所以,在 2016 年 Buoyant 公司提出了 Service Mesh 架构,它在微服务的基础上,做了下面的架构设计优化。
不再基于机器进行架构设计,而是直接在云原生基础设施 K8S 的基础上进行架构,这样能直接利用云的弹性能力。
将微服务的框架层拆分出来,以一个 Sidecar 的形式,独立部署在服务运行的节点上,通过这个方式来解耦基础架构团队和业务研发团队。
最终目标是将微服务的框架层所做的服务治理相关的功能,都抽象到 Sidecar 上,通过 Sidecar 建立云原生时代的 Service Mesh 。
另外,我们将 Service Mesh 定位为云原生时代的 TCP / IP 协议,为了帮助你更好地理解,下面我们就对 TCP / IP 和 Service Mesh 进行一个比较。
首先,是路由能力层面。 TCP / IP 协议在发送数据的时候,通过路由协议,利用网络唯一标识 IP 地址找到所属的计算机节点,而 Service Mesh 通过服务注册发现机制,利用服务的唯一标识来找到服务实例的 IP 列表。
其次,是控制能力层面。 TCP / IP 协议通过慢启动、拥塞控制等一系列的手段,确保网络能够正常运行,而 Service Mesh 通过服务治理中的熔断、降级和限流等机制,确保整个分布式系统正常运行。
通过上面对 TCP / IP 和 Service Mesh 的比较你会发现它们虽然做的事情不一样工作的层次不相同但是工作原理是一样的TCP / IP 负责将数据包通过网络发送给指定 IP 的主机, Service Mesh 负责将请求通过网络发送给指定的服务,并且它们都会进行流量控制,关心整个网络运行的效率。
对于分布式业务系统架构的云原生期,我认为是从 2015 年 - 至今。在这一时期, Service Mesh 从概念刚刚出现,发展到许多的公司都开始在生产环境中使用,并且出现了许多优秀的开源框架,比如 2016 年 Buoyant 的 Linkerd 和 Lyft 的 Envoy 2017 年由 IBM 、 Google 和 Lyft 共同推出的 Istio 。
总结
本节课中,我们讨论了分布式业务系统的演进历史,现在我们一起来总结一下。
首先,是 1990 年代以前的史前期,这个时期主要的架构形式是单体架构,为了高可用和高性能,部署形式为集群部署。
然后,是 1990 年代的探索期,为了解决单体架构在研发效率和成本方面的不足,人们开始对分布式系统进行探索,其中的标志性事件是 1996 年 Gartner 公司提出了 SOA 的概念。之后是 2000 年代的萌芽期在这一时期SOA 架构正式推出并且在工业界广泛实践。
接着,是 2010 年代 - 2015 年代的爆发期,在这一时期, SOA 架构已经深入人心,同时在 2014 年,基于 SOA 架构进化的微服务架构,被 Martin Fowler 和 James Lewis 提出并推广。
最后,是 2015 年代到现在的云原生期,在云原生期,人们希望微服务架构中的服务治理变成像 TCP / IP 协议一样的网络基础设施,其中的标志性事件是 2016 年 Buoyant 公司提出了 Service Mesh 架构。
到这里,你会发现从历史的发展脉络中,我们可以看到未来的方向,你自然也就明白为什么 Service Mesh 是分布式业务系统中代表未来的架构了。同时,通过增加分布式业务系统中,时间维度的学习后,你对于单体架构、 SOA 、微服务和 Service Mesh 一定也有了更深刻的认识,也就知道如何选择适合公司业务特点的架构了。
思考题
结合本节课对分布式业务系统的演进分析,请你来分享一下,你的公司所使用的架构,并且说一说使用这一架构时遇到了哪些问题?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,103 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
30 分布式存储技术的发展史:从 ACID 到 NewSQL
你好,我是陈现麟。
通过上节课的学习我们明白了分布式在线业务系统是如何一步步从单体架构、SOA、微服务到 Service Mesh 的,这对于帮助我们理解 Service Mesh 为什么被设计为现在这个样子,并且为什么 Service Mesh 是一种更好的架构,给出了一个清晰的结论。
接下来,我们开始讨论本专栏另一个重点对象——分布式存储系统的演进历史。通过对这段历史的讨论和研究,从时间和历史的维度上,帮助你建立网状、立体的知识体系。
这一节课,我们主要讨论分布式存储系统中,分布式在线数据库的演进历史:从 ACID 到 NewSQL。与分布式业务系统的演进历史一样我们也将分布式在线数据库的演进历史梳理为史前期、探索期、萌芽期、爆发期和云原生期这 5 个阶段来讨论和总结。
史前期与探索期
在 1990 年以前,互联网还没有被广泛使用,能连网的用户和设备非常有限,存储的数据量还在单机的承受范围之内。同时,由于 1970 年发布的 SQL 有表达能力强、面向集合和声明式等优良设计,所以在数据库中被广泛地使用,使得当时的在线数据库主要为单机的关系数据库,其中最著名的是 1979 年甲骨文发布的 Oracle 和 1983 年 IBM 发布的 DB2。所以我们可以认为 1990 年以前为分布式在线数据库的史前期。
单机数据库非常大的一个优点是提供了一个非常完美的抽象,即 ACID 事务,让业务层可以专心去处理业务逻辑。关于事务,我们在第 22- 25 讲“事务”系列课程中非常详细地讨论过,这里就不再重复了。
但是,随着互联网的快速发展,用户量快速增长,单机数据库在存储容量和并发性能方面面临非常大的挑战,于是人们开始探索新的解决方案。
一种方案是从业务层面来解决单机数据库的问题,具体有如下两个操作方式。
一是,将业务垂直拆分为不同的逻辑单元,然后将不同逻辑单元上的数据库表,按数据容量和并发量等规则拆分到不同的数据库实例上。这种方法虽然可以大大扩展单机数据库的容量和性能,但是如果单个数据库表的数据非常大,那么分库就无法解决了。
二是,为了解决单表数据量非常大的问题,在业务上从一个表中,选择某一个字段为分片键,将其水平拆分为多个子表,这样每一个子表负责原表的一部分数据存储和读写。这种方法可以从更小的粒度对数据库进行扩容,但是对于非分片键的查询等操作是非常麻烦的。
对于这种方案,多个数据库实例之间并不需要相互感知,分库分表都是由业务来进行处理,所以只能称为单机数据库的集群模式,不能称之为分布式数据库。
另一种方案是从数据库层面解决单机数据库面临的问题,通过将数据库扩展为一个分布式数据库提升存储容量和性能,并且对业务来说,它依然和使用单机数据库一样使用分布式数据库。
正常来说,分布式数据库的解决方案需要提供和单机数据库一样的 ACID 事务,但是在分布式数据库中,数据被分片存储到数据库的多个节点上,事务操作不能在一个节点上完成,需要支持跨节点的分布式事务。
而这对于当时的计算机工程与理论水平来说,是一个非常大的挑战,于是在 1990 年代,工业界和学术界都进行了深入探索和实践,其中最有影响力的成果如下。
首先是 1990 年,著名的分布式理论科学家 Leslie Lamport 提出了 Paxos 算法。Paxos 是一个可以容错的共识算法,后来为分布式存储技术的发展提供了底层的共识基础,但是,在当时这是一颗被遗弃的明珠,并没有受到人们的重视。
接着是 1997 年的 BASE 理论和 2000 年的 CAP 理论这两个理论直接将分布式数据库的发展推向了另一条道路NoSQL ,在线数据库为了水平扩展能力而放弃了 ACID 事务。关于 BASE 理论和 CAP 理论,在课程第 3 讲“CAP 理论”中有详细的讨论,这里就不再重复了。
我们可以看出1990 年代是分布式数据库的理论探索期在这期间Leslie Lamport 提出了一个可以容错的共识算法 Paxos 算法,这个算法是后来 NewSQL 的理论基础。而同在这一时期的 BASE 理论和 CAP 理论,则提供了另外的一个新选择,放弃 ACID 事务,选择了 NoSQL。
萌芽期
通过 1990 年理论上的探索后BASE 理论和 CAP 理论深入人心,当时人们通过它们确定了分布式数据库的理论边界,于是放弃 ACID 事务的 NoSQL 数据库一时大放光彩。所以在 2000 年代这一时期,出现了非常多而且优秀的 NoSQL 数据库,下面我们来介绍几个著名的数据库。
首先是 2006 年Google 发表了论文 “Bigtable: A Distributed Storage System for Structured Data” 在这篇论文中Google 对外分享了公司内部的分布式存储系统 Bigtable 的实现原理。Bigtable 在设计上有一个妥协,即只支持单行事务,不支持跨行事务。
接着在 2007 年AWS 发表了论文“Dynamo: Amazons Highly Available Key-value Store”。这篇论文中AWS 陈述了它们发现自己的很多业务场景,比如购入车场景,对数据库的关系型能力需求并不频繁,大约 70% 的操作都是键-值类操作,即仅使用一个主键,返回一个单行数据;大约 20% 的操作会返回一组行数据,但是也仍然位于单个表上,所以 AWS 重新设计了一个 Key-value 数据库 Dynamo。
同时由于 AWS 业务规模巨大,对系统的可扩展性和可用性有非常高的要求,所以 AWS 特别在论文中指出“可靠性是我们最重要的需求之一,因为即使是最微小的故障也会造成巨大的经济损失,而且会降低客户对我们的信任。”
基于上面的设计目标Dynamo 采用了无主复制的数据复制策略,并且通过 Quorum 机制让业务根据自身的特点在读性能、写性能和可用性之间达成平衡。关于无主复制和Quorum 机制,在课程第 21 讲“无主复制”中有详细的讨论,这里就不再重复了。
总而言之Dynamo 整体是一个非常优秀的技术方案2008 年 Facebook 推出的 Cassandra 是 Dynamo 的一个开源实现。后来在 2009 年10gen 公司(后改名为 MongoDB Inc )推出了 MongoDB它是一个文档型的数据库简单来说是一个 Schemaless数据即文档的数据库。
爆发期与云原生期
在 2010 年代,由于互联网公司数据的快速增长,人们接受了 BASE 理论和 CAP 理论,在数据库的架构设计方面,为了水平扩展能力和高可用性,放弃了数据的一致性,也就是在 CAP 里面选择了 AP 模型。可是我们在课程第 28 讲“共识与事务”中讨论过,要实现事务,底层多副本数据的复制模型必须是线性一致性的,所以 NoSQL 选择了 AP 模型,也就相当于放弃了事务。
在课程第 22 讲“事务的一致性”中我们已经详细讨论过,对于业务逻辑来说,事务提供了原子性、隔离性、持久性和一致性,这是一个非常好的抽象,所以工程师们非常希望自己使用的数据库是支持事务的。如果不支持事务的话,工程师为了保障业务逻辑的正确性,需要自己在业务逻辑层实现事务本身应该提供的保障。
比如,由于 Google 的 Bigtable 数据库只支持单行事务,不支持跨行事务,而业务中的跨行事务是很正常的逻辑,所以在 Google 里面使用 Bigtable 的工程师们,就只能在 Bigtable 之上构建自己的事务,这个过程是非常浪费时间并且很容易出现错误的。
而且在这一时期人们对于分布式理论的认识、存储硬件和工程经验方面都有了长足的发展。在理论层面Google 在 2006 年发布了分布式锁 Chubby 的论文 “The Chubby Lock Service for Loosely-Coupled Distributed Systems”在论文中可以看到人们已经充分认识到共识算法 Paxos 对于构建分布式系统的重要性了。
在硬件层面SSD 磁盘已经普及,随机读写能力几乎高出 SATA 磁盘 3 个数量级。在工程经验层面,工程师们在 2000 年代就构建了大量的 NoSQL 系统,已经积累了关于构建一个分布式存储系统的丰富经验。
在这样的背景下Google 于 2012 年发布了 Spanner 的论文 “Spanner: Googles Globally-Distributed Database”于 2013 年发布了 F1 的论文“F1: A Distributed SQL Database That Scales”这两篇论文介绍了一个 Google 内部开发的支持外部一致性External Consistency的全球分布式关系数据库直接宣告数据库行业进入了 NewSQL 时代。
Google 发布论文,开源界进行跟进是最近多年的一个规律,于是在 2015 年,开源界陆续推出了 NewSQL 数据库 CockroachDB 和 TiDB所以我们认为 2010 年 - 2015 年为分布式数据库 NewSQL 的爆发时期。
从 2015 年开始DBaaS DB as a Service )的趋势越来越明显,据 AWS 的数据显示,用户在 2019 年迁移到 AWS 云上数据库的数量,超过了 2015 年到 2018 年的总数。DBaaS 也给分布式数据库提出了新的要求:分布式数据库需要能利用云的弹性等能力,来动态扩展自己的服务能力。从此,开启了分布式数据库的云原生时代。
总结
本节课中,我们讨论了分布式在线数据库的演进历史,现在一起来总结一下。
首先,是 1990 年以前的史前期,这个时期主要的架构形式是支持 ACID 事务的单机数据库。
然后,在 1990 年代的探索期,由于互联网的快速发展,单机数据库在存储容量和性能方面都面临非常大的挑战,所以人们开始探索新的解决方案:分库分表的集群方案和分布式数据库。
接着,是 2000 年代的萌芽期,人们接受了 BASE 理论和 CAP 理论,为了应对互联网的海量数据,人们为了水平扩展能力而放弃了 ACID 事务,这一时期出现了大量的 NoSQL 数据库。
由于业务层对事务的需求非常强烈,并且人们在工程能力和理论水平方面都在不断进步,所以在 2010 年 - 2015 年,分布式支持 ACID 事务的 NewSQL 数据库诞生,从此进入了 NewSQL 时代。
最后,是 2015 年 - 至今的云原生时代,这个时期最鲜明的特点是 DBaaS 和分布式数据库,能够利用云的弹性能力进行动态扩展。
到这里,我们可以看到,技术的发展是曲折前进的,在 1990 年以前的单机关系数据库就支持了 ACID 事务,但是到了 2000 年代,由于当时理论和工程水平的原因,许多 NoSQL 数据库为了水平扩展能力,而放弃了 ACID 事务,这就是一个非常大的权衡。
后来随着技术的发展,在 NewSQL 时代,既支持水平扩展,又支持 ACID 事务的分布式数据库终于出现了。所以,在技术的发展过程中,没有完美的架构,只有完美的 trade-off取舍永远是最关键的因素。
思考题
结合本节课对分布式在线数据库的演进分析,请你来分享一下,你的公司所使用的数据库,并且说一说使用这一数据库时遇到了哪些问题?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,85 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
春节加餐 技术债如房贷,是否借贷怎样取舍?
你好,我是陈现麟。我要先给假期还在坚持学习的你点个赞。
在我们日常的研发工作中,有时候会出现这种情况,因为业务方希望产品能立即上线,所以提出了“这个需求很简单,怎么实现我不管,明天上线”的特殊要求。虽然不至于总是出现这种情况,但是,希望产品能立即上线和研发投入成本之间的矛盾是一直存在的。
在充分评估的请求下,工程师只有通过借技术债务的方式来达成目标,所以技术债务是我们日常研发工作经常需要面对的一个问题。
但是根据我的观察,人们对于技术债务的理解是众说纷纭的,有的人认为必须要借技术债务,这是没有选择的办法,而有的人却认为技术债务要尽量避免,避免架构设计、代码逻辑被技术债务污染,那么工程师们到底应该怎么看待技术债务呢?
基于人们的不同讨论,在这期春节加餐中,我想和你聊一聊技术债务的事情。我们一般说的技术债务指的是,将一些技术方案通过简单、粗暴的方式来实现,以减少研发资源和研发时间的投入。
但是从本质上来说,技术债务也是一种借贷行为,相当于在我们现在的项目中通过技术债务的形式,向未来借贷了研发资源和研发时间,那么当我们在未来的一个时间点,通过重构的方式优化项目中的技术债务时,其实就是在用研发资源和研发时间来偿还技术债务了。
因为技术债务是一种抽象的债务,直接讨论可能比较枯燥,也不好理解,所以,在本期课程中,我将结合你在日常生活中,经常会接触到的贷款买房的模型来类比思考,通过你熟悉的贷款买房,来分析技术债务的各个方面。
首先我会结合贷款买房的不同情况,讨论我对技术债务的看法,接着根据重要的利息问题,为你总结一份借贷清单,最后再来聊聊不偿还的危害,让你意识到不断偿还技术债务的重要性,从而对技术债务有一个理性和清晰的认识。
技术债务是生产力
我们应该怎么来看待技术债务呢?它是我们架构中的洪水猛兽,会影响整个工程的成败,还是生产力,能推动工程的快速发展和迭代呢?
这里我们先从生活场景入手,分析一下我们会因为什么原因用贷款的方式买房。
首先,是钱不够的情况。不贷款买不起房子,但是我们又担心房子会涨价,在这种情况下,我们没法等到钱挣够了再买,必须贷款。其次,是钱勉强够的情况。虽然我们可以全款购买,但是不想因为买房子的事情去影响生活质量,所以我们会通过贷款来支付一部分的购房款。
最后,是钱完全够的情况。这时候,我们可以轻松地全款支付,并且不会影响日常的生活质量,但是我们对自己的理财能力有预期,相信自己理财的收益,会超过银行贷款的利率,所以在这种情况下,我们也会选择贷款。
那么下面我们就可以将这三种情况,分别映射到技术债务中进行讨论。
首先,钱不够的情况,一般出现在项目快速发展的初期。这个时候,内部和外部的环境都在剧烈变化,快速交付是非常重要的,我们需要通过借技术债务来融资,快速完成我们的项目,确保在竞争中不会失败,不然可能会出现钱越挣越多,房价也越涨越高,最后我们依然买不起的情况。
其次,钱勉强够的情况,一般出现在项目发展的中期。在这时,我们经常会碰到一些技术决策,需要思考到底应该很完善、系统地完成,还是借一点技术债务,让实现变得简单一点。很多情况下,如果能控制技术债务的风险,我们都会希望工程师的工作张弛有度,不要经常出现加班的情况,影响到他们的生活质量。
最后钱完全够的情况一般出现在项目发展的后期。这个时期资源很充足对于每一个技术设计我们都能申请到资源将它实现得非常完善但是如果我们还有一些其他的更高投入产出比ROI的事情我们就可以选择借一些技术债务来完成项目的工作然后将空余的资源投入到更高 ROI 的事情,达到全局最优的效果。
所以,我认为技术债务是生产力,合理利用技术债务会大大提高我们的研发效率,提高项目的成功率。
技术债务应该是深思熟虑的结果
同时,我们在合理利用技术债务来提高短期研发效率时,也要充分考虑到技术债务对我们长期研发效率的影响。
关于影响,我们同样结合买房贷款来思考。除了本金之外,贷款还会涉及利息的问题,一般会提供非常多家的银行,每家银行之间会有差异,并且还有公积金贷款这样利息更低的贷款产品,我们一般都会选择利率最低的贷款产品或银行。
既然技术债务是债务,那么借技术债务也是有利息的,所以我们在借技术债务的时候,要深思熟虑,区分哪些地方的技术债务是高利息的,甚至是复利,而哪些地方的技术债务是低利息的,甚至有些地方的技术债务可能都是不需要还的。
那么我们应该如何选择技术债务的利息,将整体价值最大化呢?一般来说,技术债务影响的范围越大,它的利息就越高,所以,对于技术债务的利息高低,我们可以通过它的影响范围来判断。下面我们就来具体分析一下技术债务的利息,你可以结合下面的表格来理解。-
-
首先,系统的接口和协议的利息是非常高的,因为系统的接口和协议是对外提供服务的,就导致它的影响范围非常大,并且还会随着接入方的增加,而自动放大技术债务,所以,这样的利息是复利,我们一定要避免。
然后,系统架构的技术债务的利息一般也是很高的,因为系统的架构会从全局影响系统的设计,它的影响范围会非常大,并且会随着系统的迭代而增加,所以,这样的利息是非常高的,我们要尽量避免。
接着,局部的功能和逻辑之类的实现的利息是比较低的,因为它只会影响到局部的代码实现,比如一个函数的具体实现、写死的配置和策略等影响范围不大的地方,这样的技术债务利息比较低,在我们有需要的时候,可以多借一些。
最后,非常边缘的功能和一些尝试型的功能实现的利息是非常低的,因为边缘功能后续的迭代不会很多,它在时间维度上的影响范围是非常小的,而尝试型的功能在后面是有一定的可能性被放弃的。虽然我们希望尝试都成功,但是如果被放弃后,从技术债务的角度来看的话,我们甚至连本金都不需要还。所以,这样的利息债务可以根据需要多借一些。
技术债务是需要不断去偿还的
俗话说欠债还钱,天经地义,在我们贷款买房之后,肯定是需要每个月还贷款的。如果其中有一个月我们没还,就会影响我们在征信中的评分,从而影响到社会信用,也就是整个社会对你的评价。最坏的情况下,如果一个人借了太多的贷款后来还不起了,就会被贷款机构告上法庭,面临强制执行的风险。
其实对于技术债务来说,也是一样的。如果我们借了技术债务后,在资源充足的时候,就需要不断地去偿还,确保我们整体的技术债务是可控的。虽然在现实世界中,我们都认为欠债还钱是天经地义的,但是在面对技术债务的时候,有可能会迷失。那么如果不重视偿还,会出现什么问题呢?
首先,这会让我们系统的技术债务恶化,我们每一次的迭代都需要付出不少的利息,这个利息包括迭代的工程效率低、上线的故障等。如果技术债务积累到一定的程度,甚至会影响这个业务的成本,例如曾经和 Facebook 齐名的社交平台网站 MySpace这个网站会失败其中一个原因就是技术债务太重了。
其次,所有的债务都是需要信用来担保的,在贷款买房的过程中,用来担保的是我们的社会信用,在借技术债务的过程中,用来担保的是我们的技术信用,即我们的技术影响力。技术影响力对于我们职业生涯的发展是非常关键的,如果我们在自己负责的项目中,积累了非常多的技术债务,那么在其他人了解这个项目的情况后,将会影响我们的技术影响力。
所以,我们需要通过不断地偿还,合理控制技术债务,让技术债务变成我们的杠杆,而不是负担。
总结
到这里,我们已经对技术债务有了比较清晰的认识,对于是否需要借技术债务,以及应该在什么时候借技术债务,可以总结为一句话:技术债务和贷款买房的思维模式一样,如果借技术债务的收益大于利息的时候,你就大胆地去借吧!
思考题
作为一个研发工程师,你一定也借过不少技术债务,你可以分享一下,你借过最大的技术债务的亲身经历,还有你后面是如何偿还的。
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,103 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
春节加餐 深入聊一聊计算机系统的时间
你好,我是陈现麟。
在专栏“概述篇”第二节课“新的挑战”里,我们讨论过分布式系统在时钟上面临的挑战,今天这期春节加餐,我还会和你深入地聊一聊计算机系统的时间。
在计算机系统中,时间是一个非常重要的概念,首先它深刻影响着分布式系统的设计。如果我们想要了解如何简化分布式系统的设计,要先从单机系统的时间问题出发。举个例子来说,在构建分布式系统的时候,如果我们能在每个单机系统中,都获得精确的时间点或时间范围,就能大大简化分布式事务等相关的设计。
其次,在时间方面,分布式系统存在多时钟的问题,理解这个问题之前,也需要先了解单机系统的时间问题。所以,为了让你深入地了解计算机系统的时间,我们就从单机计算机系统的层面来讨论时间,等你理解以后,再学习分布式系统的时候,就会事半功倍了。
在计算机系统内部,主要有两种时钟:墙上时钟和单调时钟,它们都可以衡量时间,但却有本质的区别。在这节课中,我将带你了解两种时钟的相关知识,其中的墙上时钟是本节课的重点部分,然后我们再一起探讨如何对两种时钟进行管理。
墙上时钟
学习墙上时钟的相关知识我们要先从墙上时钟的同步入手了解时间同步出现误差的原因以及现有的解决方案之后再分析闰秒出现的原因以及闰秒的处理方式最后我们会根据处理方式中的“跳跃式调整”的处理逻辑来分析2012年一个 Linux 服务器宕机的案例。
墙上时钟又叫钟表时间,顾名思义,和我们平时使用的钟表的时间一样,表示形式为日期与时间。在 Linux 系统中,墙上时钟的表示形式为 UTC 时间,记录的是自公元 1970 年 1 月 1 日 0 时 0 分 0 秒以来的秒数和毫秒数(不含闰秒)。
Linux 系统需要处理闰秒的逻辑就是因为 Linux 系统使用 UTC 时间,但是系统中记录的 UTC 时间是不含闰秒的。
墙上时钟的同步
根据墙上时钟的定义,我们可以发现,墙上时钟的标准是在计算机外部定义的,所以确保墙上时钟的准确性就变成了一个问题。计算机内部的计时器为石英钟,但是它不够精确,随着机器的温度波动,会存在过快或者过慢的问题,所以依靠计算机自身,来维持墙上时钟的准确性是不可能的,这就是计算机系统内的时间需要与外部时间进行同步的原因。
目前普遍采取的一种方式为:计算机与 NTP 时间服务器定期通过网络同步。很明显,这个方式受限于网络时延的影响,一般来说,至少会有 35 毫秒的偏差,最大的时候可能会超过 1 秒。
在一些对时间精度要求很高的系统中,通过 NTP 进行同步是远远不够的,这时我们可以通过 GPS 接收机,接收标准的墙上时钟,然后在机房内部通过精确时间协议( PTP )进行同步。 PTP 是一种高精度时间同步协议,可以达到亚微秒级精度,有资料说可达到 30 纳秒左右的偏差精度,但是它需要网络的节点(交换机)支持 PTP 协议,才能实现纳秒量级的同步。
在时间同步这个问题上, Google 的做法更酷,通过 GPS 接收机,接收标准的墙上时钟,然后通过机房内部去部署原子钟,使得它的精度可以达到每 2000 万年才误差 1 秒,用这种方式来防止 GPS 接收机的故障。
接着,再把这些时间协调装置连接到特定数量的主服务器,最后再由主服务器,向整个谷歌网络中运行的其他计算机传输时间读数,即 TrueTime API 。 Google 正是基于上面的时间精度保证,在此基础上实现了第一个可扩展的、全球分布式的数据库 Spanner。
闰秒出现的原因
从上述的讨论中,我们可以知道计算机的墙上时钟通过同步机制,确保时间的误差会保持在一个范围以内。虽然它保证了时间精度,但是因为 Linux 系统中,墙上时钟的表示形式为 UTC 时间,而 UTC 时间是不含闰秒的,所以如何处理闰秒就成为了一个重要的问题,那么我们先来想想闰秒出现的原因。
因为地球自转速率变慢,所以目前的两种时间计量系统:世界时和原子时,它们之间发生了误差,这就是闰秒出现的根本原因,下面我们就从世界时和原子时这两方面,具体来分析一下。
世界时( UT1 )以地球自转运动来计量时间,它定义地球自转一周为一天,绕太阳公转一周为一年,这对人们的日常生活非常重要。但是,因为地球自转速率正在变慢,世界时的秒长就会有微小的变化,每天会长千分之几秒,也就是说,后一天的 24 小时会比前一天的 24 小时要长千分之几秒,所以用世界时来度量时间,会出现均匀性非常不好的问题。
原子时取微观世界的铯原子中,两个超精细能级间的跃迁辐射频率来度量时间,精确度非常高,每天快慢不超过千万分之一秒。所以,原子时的均匀性非常好,是度量时间的理想尺度。
可是,原子时与地球空间位置无关,由于地球自转速率正在变慢,如果在某地区使用原子时,从今天开始计时,那么原子时到了明天凌晨 000 的时候,地球还需要等千分之几秒才自转完一周。这样一天一天地累积,就会出现原子时到了凌晨 000 这个时候,太阳还在地球正上空的情况,这显然是不符合常识的。
所以,为了统一原子时与世界时之间的差距,协调世界时( UTC )就产生了。从 1972 年 1 月 1 日 0 时起,协调世界时秒长采用原子时秒长,时刻与世界时的时刻之差保持在正负 0.9 秒之内,必要时用阶跃 1 整秒的方式来调整。
这个 1 整秒的调整,叫做闰秒,如果增加 1 秒就是正闰秒,减少 1 秒就是负闰秒。 UTC 从 1972 年 1 月起正式成为国际标准时间,它是原子时和世界时这两种时间尺度的结合。
闰秒的处理
因为 Linux 系统记录着,自公元 1970 年 1 月 1 日 0 时 0 分 0 秒以来的秒数和毫秒数,但是不含闰秒这种情况,导致了在 Linux 系统中每分钟有 60 秒,每天有 86400 秒是系统定义死的。
所以 Linux 系统需要额外的逻辑来处理闰秒。目前处理闰秒的方式主要有两种,一种是在 Linux 系统上进行跳跃式调整,另一种是在 NTP 服务上进行渐进式调整的 Slew 模型,下面我们具体讲一讲这两种处理逻辑。
跳跃式调整
首先是在 Linux 系统上进行跳跃式调整,当 UTC 时间插入一个正闰秒后Linux 系统需要跳过 1 秒,即这一秒时间过去后,在 Linux 的时间管理程序中不应该去计时,因为闰秒的这一秒钟在 Linux 系统中不能被表示。
但是,当 UTC 时间插入一个负闰秒后Linux 系统就需要插入 1 秒,即 Linux 的时间管理程序中要增加 1 秒钟的计时。虽然并没有过去 1 秒钟的时间,但是闰秒的这一秒钟在 Linux 系统中是不存在的。
目前 Linux 系统就是采用这种方式来处理闰秒的,所以在 2012 年 6 月 30 日, UTC 时间插入一个正闰秒的时候Linux 系统会启动相应的逻辑来处理这个插入的正闰秒,这样就使某些版本的闰秒处理逻辑,触发了一个死锁的 bug造成了大规模的 Linux 服务器内核死锁而宕机的情况。
Slew 模式
NTP 服务的 Slew 模式并不使用跳跃式修改时间,而是渐进式地调整。比如,当 UTC 时间需要插入一个正闰秒时, NTP 服务就会每秒调整一定的 ms 来缓慢修正时间。这样 Linux 系统从 NTP 服务同步时间的时候,就不会感知闰秒的存在了,内核也就不需要启动闰秒相关的逻辑了。
单调时钟
关于墙上时钟,我们主要讨论的是如何进行时间同步,确保时间精度的问题,而计算机系统中的第二种时钟,单调时钟是一个相对时钟,不需要与外部的时钟进行同步,较墙上时钟要简单很多,所以这里我们就简单地分析一下。
单调时钟总是保证时间是向前的,不会出现墙上时钟的回拨问题,所以它非常适合用来测量持续时间段,比如在一个时间点读取单调时钟的值,完成某项工作后再次获得单调时钟的值,时钟值之差就是两次检测之间的时间间隔。
到这里,我们可以看出,墙上时钟是绝对时钟,不同计算机节点上的墙上时间可以进行比较,但是它是有误差的,导致比较的结果不可信;而单调时钟是相对时钟,它的绝对值没有任何意义,有可能是计算机自启动以后经历的纳秒数等,所以,比较不同计算机节点上的单调时钟的值是没有意义的。这两点正是分布式系统面临时钟的问题。
时间的管理
前面我们讨论了墙上时钟和单调时钟,你一定很好奇操作系统内部是如何处理时间的,这里你可以先思考两个问题,我们带着问题再具体讨论。第一个问题是:计算机系统是没有时间概念的机器,那么它怎么来计算与管理时间呢?另一个问题是:计算机系统可以提供微秒甚至纳秒,那么它怎么处理这么高精度的时间呢?
首先,时间的概念对于计算机来说有些模糊,计算机必须在硬件的帮助下才能计算和管理时间。前面说的石英钟就是用来做计算机的系统定时器的,系统定时器以某种固定的频率自行触发时钟中断。由于时钟中断的频率是编程预定的,所以内核知道连续两次时钟中断的间隔时间,这个间隔时间就叫做节拍。通过时钟中断,内核周期性地更新系统的墙上时钟和单调时钟,从而计算和管理好时间。
其次,目前系统定时器的中断频率为 1000 HZ那么计算机能处理的时间精度为 1 ms。然而很多时候需要更加精确的时间比如 1 微秒,计算机是怎么来解决这个问题的呢?
其实解决的方式非常简单,在每一次计算机启动的时候,计算机都会计算一次 BogoMIPS 的值,这个值的意义是,处理器在给定的时间内执行指令数,通过 BogoMIPS 值,计算机就可以得到非常小的精度了。
比如在 1 秒内,计算机执行了 N 条指令,那么计算机的精度就可以达到 N 分之一秒。很明显 N 是一个非常大的数目,因此计算机可以得到非常精确的时间了。
总结
在这节课中,我们从计算机系统内部的两种时钟出发,深入地讨论了时间相关的话题。在墙上时钟这部分,我们讨论了计算机系统时间同步的方式,分析了闰秒产生的原因,以及 Linux 系统应对闰秒的办法,然后概览性地讲了 Linux 系统是通过时钟中断进行时间的计算与管理的,最后分析了 Linux 系统可以提高时间精度的方法。
思考题
计算机历史上的“千年虫”问题你了解过吗?它和时间有关系吗?
欢迎你在留言区发表你的看法。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,63 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
春节加餐 系统性思维,高效学习和工作的利器
你好,我是陈现麟。
今天是除夕,首先在这里祝你新春快乐、虎年虎虎生威,在新的一年,学业有成,工作顺利,身体健康,疫情之下一定要做好防护。
学习和工作是我们人生中非常关键的两个部分,它们占据了我们大部分的时间,并且它们的结果也在很大程度上,决定了我们生活的质量和幸福感。那么在同样的时间里,如何让学习和工作变得更高效,就是我们自我提升的关键了。
在以往的学习和工作经历中,因为没有好的思维方法,导致我在学习一些新知识时,出现过不能理解也记不住的情况,在工作中也不能举一反三地处理好问题,经历过一些挫折。不过,随着一次次的经验教训,我慢慢总结出了对我影响最大的一个思维方式,即系统性的思维方式,它很好地提高了我学习和工作的效率和质量。
所以在这一期春节加餐中,我想先和你分享,我是如何使用系统性的思维方式在学习和工作中提效的,学完这节课程以后,你可以在学习中建立起自己的知识体系,在工作中形成高效解决问题的方法,让快乐学习和高效工作常伴左右。
对于学习,从深度和广度上运用系统性思维方式
在学习过程中,特别是计算机技术方面的学习时,我们经常会面临两个问题:一个是学习之前,觉得知识太抽象了,不好理解;另一个是学习之后,很容易就忘记了,记不住这个知识。其实这两个问题都是我们的学习方式不够系统导致的,下面就结合我的具体学习经历,从这两个问题出发,讲一讲如何通过系统性思维,高效地理解和掌握知识。
一方面,学习之前觉得知识太抽象,不好理解,很多时候是因为我们在学习一个知识时,直接面对的是这一个知识的结论,是高度总结和抽象的结果,所以在我们不了解这个知识相关的时代背景和原因的情况下,直接去进行学习,肯定会一知半解,甚至毫无头绪。
在 Spark 刚出来的时候,我就打算赶快学习一下,根本没有思考和设计怎么去学,就直接找了一本源码剖析的书啃起来,结果看完毫无头绪。后来静下心来思考,意识到自己的学习方法不对,我都不知道 Spark 的出现是为了解决什么问题的,自然就不知道 Spark 这个系统为什么要这么设计了。
如果跳过了这个知识产生的时代背景和原因,在深度的学习上就缺乏了系统性。正确的学习思路应该是:我们要知道一个知识是为了解决什么问题而产生的,后面又经过了什么样的迭代和优化,最终演变成了什么样子。
想通了这点以后,我立刻修改了我的学习方法,先暂停了源码剖析,去了解了这个项目的背景和设计,找到 Spark 作者 Matei Zaharia 的博士论文来学习,通过阅读论文,我明白了 Spark 是在计算引擎 MapReduce 因为存在大量磁盘读写的问题,导致性能不高的背景下提出的,并且掌握了 Spark 是怎么通过 RDD 来实现高效内存计算的。补充完 Spark 的背景知识之后,我再去看源码时,就非常清晰了。
另一方面,学习之后,容易忘记,记不住这个知识,是因为在广度的学习上缺乏了系统性。我们学习了很多零散的知识,但是却没有将知识点之间建立起联系,形成一个相互依赖的网状知识体系。
我在读大学的时候,用系统性的思维方法,高效地阅读了一本鸿篇巨著《百年孤独》。书中描述了一个家族七代人的传奇故事,有复杂的人物关系,又长又复杂的名字,并且这些名字之间经常重用,比如孙子重用了爷爷的名字,这导致我在看书的过程中非常崩溃,经常看着看着就忘记谁是谁了。
后来,我将这个家族的族谱网络画下来,族谱网络让人物之间的相互联系变得一目了然,而我只需要记住中间核心人物的名字及关系,就能简单推导出与核心人物有关的其他人物了。在建立好人物关系的知识网络后,记不住人物名字和人物之间关系的问题就被高效地解决了,当然这只是一个非常简单的构建网状知识体系的例子,但是我们却可以以小见大,把这个方法应用到更复杂的学习中去。
同时,《深入浅出分布式技术原理》这个专栏的设计思路也是如此,在学习中你会发现系统性思维方式一直贯穿其中,让你在学习知识的同时,还可以掌握高效的思维方法。
对于工作,通过系统性思维从根本上解决问题
上文中,我们提到学习中有很多零散的知识点,在工作中,需要处理的问题也是如此,我们经常会面临一个又一个独立的问题。如果我们只是见招拆招地解决,就会发现问题永远都解决不过来,并且还有可能会越来越多,直到我们被问题的黑洞所湮没。
对于这些问题,我认为系统性的思维方式是解决它们的根本。我们在每一次面对独立的问题的时候,应该跳过问题表层现象,深度思考这个问题的本质原因,系统性地解决。
为了让你更好地理解这种方式怎么运用于工作中,这里我举例带你分析。运维数据库的一个核心指标是数据库的稳定性,但是影响到这个指标的原因实在是太多了,对于技术中台内部可以控制的问题,我们通过一些高可用方案将其解决,比如网络故障、机器故障之类的。
但是业务研发侧引起的问题多种多样,比如没有建索引、索引建立不合理,请求的量急增了等等,并且这些问题在每一个表上都可能出现,我们没有办法通过穷举来解决。
那么这个时候,就要系统性地分析了,我们会发现这个问题的根本原因是,数据库的请求数量超过了它的负荷,比如对于没有建立索引请求来说,可能它的最大负荷就是 10 个并发;对于已经建立好索引的请求来说,可能它的最大负荷是 1000 个并发,索引建立不合理也是类似的情况。
所以我们就可以从数据库的请求数量超过它的负荷,这个根本原因上来解决,而不是一个表、一个索引进行梳理和优化。虽然一个表、一个索引进行梳理和优化也是非常有必要的,它可以大大地提高系统整体的性能,但是这些现象无法穷尽,问题也就无法完全解决。
那么具体应该如何解决,数据库的请求数量超过它的负荷这个根本原因呢?我们最终的解决思路是引入一个中间层,这个中间层是一个有数据库治理功能的 Proxy ,它能提供发现、熔断、降级、监控等服务治理功能,同时能够保证不论任何时候出现了任何问题,这个 Proxy 都能快速发现哪些请求导致的并发,超过了数据库的负荷,然后控制这些请求的并发或者完成抛弃,确保数据库快速恢复。
同样,当你学习专栏时,也会发现用系统性思维方式,解决问题的方法经常出现,你也可以运用于你的工作中。
总结
到这里,我们就从学习和工作这两个方面,讨论了系统性的思维方法,最后我们来总结一下。在学习中,从深度上系统性学习,我们可以了解一个知识的来龙去脉;在广度上系统性学习,我们可以明白知识之间的关系,并且建立好知识网络;在工作中,使用系统性思维解决问题,可以让我们找到问题的本源,从根本上解决问题。
有的人觉得学习非常痛苦,是因为学习效率不高,而且没有掌握好方法,在学习完这节课之后,希望你能使用系统性思维去搭建自己的分布式知识网络,让学习高效、快乐起来。如果这节课对你有帮助,也推荐你分享给更多的同事、朋友。

View File

@ -0,0 +1,81 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 在分布式技术的大潮流中自由冲浪吧!
你好,我是陈现麟。
时间一晃三个月就过了,经过这一段时间的学习,你对分布式系统一定有了自己的知识体系和认识,如果现在让你回答一个问题:分布式系统是简单还是复杂呢?你的脑海里可能会出现两种矛盾的声音,无法给出确定的答案。其实出现这个情况是非常正常的,有了专栏的知识积淀,我们是时候重新认识一下分布式系统了。
重新认识分布式系统
分布式系统其实很简单和分布式系统其实很复杂,这两个回答没有对错之分,它们是基于宏观和细节角度的不同思考。
在这个专栏里,我们将分布式技术的知识体系做了一个全面的讨论和梳理,下面我们从简单和复杂这两个不同角度,带你跳出知识体系,对分布式系统有一个全新的理解。
分布式系统其实很简单
分布式系统简单来说就是一句话,由多个计算机节点通过网络组成一个系统,对它的使用者来说和单机系统一样。通过网络组成一个系统是非常简单的事情,而让使用者以为是一个单机系统,则需要解决多个计算机节点内部协调的问题。这些问题在本专栏中,我们都一一讨论过了,具体见下图。
同时,如果我们从分布式系统演进历史的角度来看,分布式系统的发展历史差不多为 30 年,可以简单梳理为史前期、探索期、萌芽期、爆发期和云原生期这 5 个历史阶段。史前期为单机时代,探索期主要是理论边界的思考,萌芽期诞生了第一代分布式系统,爆发期则开始快速发展和不断突破,现在我们处于云原生期,一个新的发展方向是利用云的能力来架构分布式系统,具体见下图。
从上面的讨论中,我们可以看到理解分布式系统是有迹可循的,它解决的问题和演进的历史都简单清晰,很好理解和掌握,所以从宏观的知识角度出发,我们认为分布式系统其实很简单。
分布式系统其实很复杂
但是,分布式系统又是非常广泛的一个概念,它包含了非常多的领域,并且每一个领域都有它特别的领域问题和解决方案。下面我将其分为 6 个领域分别为分布式文件系统、Stream、NoSQL、OLAP、OLTP 和业务系统,每一个领域都在快速发展中,并且出现了非常不错的进展。这里我进行了简单的梳理,具体见下图。
从上面的图中,我们看到了非常多的分布式系统,它们组成了一个非常复杂的概览图。其实这里的分布式系统只是冰山一角,还有非常多的分布式系统没有被收录进来。所以,分布式系统其实是非常复杂的一个体系。
总体来说,我认为这个问题如同“在战略上藐视敌人,在战术上重视敌人”一样,在宏观层面,分布式系统是简单的,但是在细节上,分布式系统又是复杂的。不过从宏观层面掌握了分布式系统,我们就不会害怕复杂的细节实现了。
开始自由冲浪吧
通过这个专栏的学习,你已经建立好分布式系统的知识体系,这就好比你已经学会了冲浪的技能,整个“分布式技术的大海”向你敞开了怀抱,现在你可以去自由冲浪了,这就达到了本专栏的目标。
在你自由冲浪之前,我还想和你分享一下我在学习方面的一些经验,将这些经验作为本专栏最后的一次谈心,也作为朋友之间的临别赠言,希望能为你今后的学习保驾护航。
时间都去哪儿了
我曾经有一段时间,被一个问题困扰:每天我都花很多的时间学习和阅读,通过手机、平板、电脑和书籍等渠道输入了很多的信息,但是回想起来又感觉没有学习到什么,很疑惑这些学习的时间都去哪儿了?
有上进心并且爱学习的你,是不是也有这方面的困扰呢?下面我来分享一下我是如何解决这个问题的,希望这些方法可以让你的自由冲浪之旅更加快乐。
被动学习 vs 主动学习
首先我们来思考下学习方式的问题。我们的学习方式可以分为两类,一类是主动学习,另一类是被动学习,每一种学习方式都有不同的学习效率,下图是美国国家训练实验室研究的结果,研究了不同学习方式的平均学习效率。
结合上面的图,你是不是很快就发现了问题,我们平时将大量的时间都投入到了被动学习中,所以学习效率是很低的,而改变自己的学习习惯,通过主动学习来提高学习效率是非常有效的。
就拿我的经历来说吧,当我发现我花在学习上的时间很多,但是吸收的效果不好,并且记不住也答不出的时候,我就意识到被动学习的效率太低了。然后我就开始有意识地采用做笔记、写博客和做分享的方式去学习,将被动学习转变为主动学习,学习效率和质量都有了非常大的提高。
看完我的学习经历,你可能会联想到费曼学习法,它其实也是一样的思路,通过自己学习,然后将学习结果教给完全不懂的人,将被动学习转变为主动学习。
泛读学习 vs 精读学习
另一个问题是学习精力投入的问题。我们平时进行的大量学习其实都是泛读的方式,但是通过这个方式只是获取了信息,并没有把它变成我们所掌握的知识,这样很容易就会出现学完就忘记的情况,而精读则是聚焦于某一个知识点、某一篇论文或某一本书上,集中大量的时间来 Close 一个问题,这种方式可以让我们把获取的信息变成自己掌握的知识或能力。
记得刚刚开始学习计算机网络的时候我在网络上看了各种各样的资料也花了一些时间来总结但是总感觉自己对于网络的体系化认识是非常不足的。不过在这些泛读中我发现有很多的资源都会提到KevinR.Fall 和 W.RichardStevens 写的书籍《TCP/IP详解》我在试读后感觉非常不错于是立即花了几天时间精读了这本书从此我对计算机网络基础方面的理解可以说是上了一个台阶。对于当时的我来说相当于 Close 掉计算机网络这个知识领域了。
所以,通过泛读确定精读的目标和内容,然后去精读是一个让我受益匪浅的学习方式,并且在精读的时候,我们还可以采用主动学习的方式或者费曼学习法,使学习效率提高,让效果更持久。
以上这些就是我的学习经验分享,可能这些分享并不是最新潮的,但却是实实在在帮助到我的,希望你也能将这些方法实践起来。
最后,送君千里,终须一别,你已经面朝分布式技术的大海,迎来了春暖花开的季节,现在就带上主动学习和精读学习的习惯,开启你自由冲浪的生活吧!同时,我也要告诉你温故知新非常重要,在后续的复习过程中,你依然可以在留言区提问,我依然会跟你保持交流。
为了让专栏的内容越来越好,我准备了一份结课问卷,希望你能用两分钟的时间填写一下,非常期待你对这个专栏的意见或建议,众人拾柴火焰高,专栏质量的提升离不开你的努力!