90-分布式技术原理与算法解析

开篇词 | 四纵四横，带你透彻理解分布式技术

你好，我是聂鹏程，智载云帆的 CTO。在接下来的三个月时间里，我将带你打卡分布式技术。

在众多计算机技术当中，分布式技术无疑是最璀璨的明珠之一。毫不夸张地说，没有分布式技术就没有互联网，也就没有现在的阿里巴巴、腾讯、亚马逊、Facebook、谷歌等科技巨头，更不会有以信息技术为核心的、对人类历史产生巨大变革的第三次工业革命。万维网、Email、DNS 等，都是分布式系统的典型代表。

随着分布式技术的不断发展，它也早已不再局限于传统的互联网等应用场景。在今年的两会期间，全国政协委员、360 董事长周鸿祎更是大唱 I’M ABCDE 字母歌。IMABCDE 这 7 个字母所代表的 IoT 物联网、Mobile 移动计算、AI 人工智能、Blockchain 区块链、Cloud 云计算、Data 大数据、Edge 边缘计算，也无不都是以分布式技术为基石。

无疑，谁更好地掌握了分布式技术，谁就更容易在新一轮技术浪潮中获得主动。在全球经济增速趋缓的大背景下，与许多应用业务大量裁人形成鲜明对比的是，各大巨头的中间件团队、实验室等基础部门，依然在大规模地招兵买马。随着业务扩展，以及 IMABCDE 等新兴技术领域的布局，分布式技术人才已然成为巨头们争夺的焦点。

一方面是各大厂商的求贤若渴，一方面是分布式专业技术人才的一将难求。在多年的面试中，我经常能体会到，有些面试者确实非常积极主动，但他们表现出来的水平却无法通过面试。分布式技术人才市场的供应与需求，俨然一首冰与火之歌。

2007 年，我在西安电子科技大学攻读博士期间，就开始研究并行与分布式计算；毕业后，在 IBM 做过 HPC 大规模负载管理系统 LSF 相关的设计和研发工作，在华为负责过分布式 IoT 相关项目的架构设计，以及电信级业务微服务框架、函数服务框架的设计工作，也从事过区块链相关的研究工作。现在，我在智载云帆负责技术体系的构建，专注于无服务器 Serverless 的架构实践。

从我深入研究分布式技术这十多年的经验来看，分布式技术概念繁多、知识庞杂、新兴技术层出不穷，令许多新手望而却步，而许多有一定年限工作经验的老手，虽然也能对一些概念滔滔不绝，但追问到实质性问题就变得磕磕巴巴，顾左右而言它。比如：

各种分布式概念、名词学了一大堆，但经常张冠李戴，傻傻分不清楚；

做了多年技术，也参与了很多分布式技术实践，却无法回答工作中各种分布式技术、组件、框架选型背后的根源；

在一个分布式技术配套的典型场景往往能驾轻就熟，但一旦稍微变更考察业务场景、业务目标后，就变得毫无头绪。

究其原因，主要是知识碎片化、不成体系、见树不见林。如果再追究更深层次的原因，那无外乎就是两点：

在计算机学科课程设置中，分布式技术尴尬如同中小学中的性教育，重要但不受重视。鲜有高校在计算机本科专业中设置分布式课程，即便是有些高校在研究生教育中设置了相关课程，也是如同高手过招点到为止。

信息碎片化与信息孤立。在信息泛滥的信息时代，各种经典教材、最新文章自然是唾手可得。但，教材固然经典但严谨有余浅出不够，最重要的是没有与时下最新的场景相结合，一方面是因为教材“年久失修”，另一方面确实是因为分布式领域新技术推出的速度令人叹为观止；而网上的各种技术文章虽然多，却鲜有体系化的说明，一个个概念如同一座座信息孤岛。如果不能体系化地理解这些概念，何谈掌握，更谈不上真正地去综合运用这些知识了。

因为工作太忙，这些年我整体而系统的输出比较少。偶然一次听到任正非的讲话，他提到了基础教育、孩子是一个社会的未来，这让我感触良多。我想，如果说一个社会的未来，离不开朝气蓬勃的孩子们，那么一个行业的兴盛，同样也离不开一个广泛的从业者基础。而我如果能做好分布式通识课这样的一个专栏，既可以对自己这些年的经验做一次系统梳理，温故而知新，又能帮助更多的人系统理解并掌握分布式核心技术，为整个行业的兴盛略尽绵薄之力，何乐而不为呢？

其实，在工作、面试、演讲等多种场合，也经常会有人问我：“聂博士，分布式领域的新概念繁多、各种框架五花八门、各种组件层出不穷，应该如何应对啊？”我回答说：“其实你已经有答案了。”

看着他们满脸疑惑，我笑着问：“RISC 芯片，程序设计中的封装、继承，还有现在提倡的中台战略，它们都在做一件什么事情呢？”他们答道：“莫非是重用？”

我说：“是的，既然指令可以重用，代码可以重用，技术、业务、数据等都可以重用，为什么知识体系不可以呢？学好分布式通识课，掌握了分布式的核心技术、体系，你就会发现很多新技术、新框架、新组件只不过是‘新瓶装旧酒’，将分布式核心技术进行了再包装、再组合，至多也就是做了一点延伸而已。”

那么，分布式通识课究竟该如何学呢？在接下来的三个月时间里，我会遵循以下 4 个思路带着你一起学习。

第一，分布式技术错综复杂，各种技术相互耦合，确实无法简单地像网络等技术一样划分层次，所以我会结合自己多年的积累和思考，首先为你梳理出一个脉络清晰、四纵四横的分布式核心技术知识体系，然后从这个纵横的技术体系中抽取最核心、最普适的技术思想以及概念，结合各种适用场景一一解析。这样的设计，旨在帮助你找到核心知识点，并将这些知识点联系起来，快速形成分布式核心技术的知识网络，从而形成自己的技术判断力，进而规划出自己的技术路线。

第二，从一个熟知的事物出发，进行浅出的讲解，帮助你从已有知识体系扩展到新的知识体系，从而迅速、牢固地掌握分布式技术的核心知识点。

第三，透过表象深入讲解技术本质，而不是 case by case 地讲解，帮助你知其然并知其所以然，真正做到理解与运用时的举一反三。

第四，针对同一分布式问题的不同方法，从多维度、多角度进行对比、分析，方便你在工作中灵活选型，避免重复“造轮子”。你甚至可以综合权衡各种方法的优缺点，提炼发明出新的方法，最终做到活学活用。

讲到这里，你是不是也有点摩拳擦掌、跃跃欲试了呢？“分布式世界这么大，我要去看看！”别慌，请先看完这份技术地图。

首先，按照业务的架构层次栈，我自底向上按照资源、通信、数据与计算的维度，梳理出了 4 个技术层次：分布式资源池化、分布式通信、分布式数据存储与管理、分布式计算。这样的划分符合业务架构设计的一般规律，即“在一定资源上，进行一定通信，通过一定计算，完成一定数据的加工和处理，从而对外提供特定的服务”。另一方面，这样的划分也整合了零散的知识点，具有完备性。

既然横向的 4 个层次都已经完备了，那为什么又多出了 4 个纵向的技术呢？如果我们把横向的 4 个层次比作派生类的话，那么纵向的 4 条技术线应该是它们的基类。因为，在分布式环境下，无论是资源、通信、数据还是计算，都需要去解决协同、调度、追踪高可用，还有部署的问题。因此，我从横向的技术层次中，提炼出分布式协同、分布式调度、分布式追踪与高可用、分布式部署 4 个纵向技术线。由于分布式追踪、分布式部署虽属于支撑技术，但并不会影响业务的构成，因此我不会在本专栏中进行讲解。

最后，如果说现在分布式领域里各种包装出来的、五花八门的新技术，像是令人高不可攀的女神、男神的话，那么这个分布式通识课程中所提炼出来的体系和核心知识点无疑就是女神、男神素颜的样子。我想说，等你看尽素颜，无论是女神、男神也好，还是各种高大上的技术也好，也就不会觉得那么高不可攀了。

既然你已经看到了这里，相信你也看到了学习分布式技术知识的迫切需求，那么不妨请你在留言区做个自我介绍，给我说说你的困惑，也说说你想通过这个专栏收获些什么，这样我后续也可以根据你的情况进行有针对性的讲解。

我是聂鹏程，今天的内容就到这里了，我们下一讲再会。

01 | 分布式缘何而起：从单兵，到游击队，到集团军

你好，我是聂鹏程。这是专栏的第一篇文章，我们先来聊聊什么是分布式。

与其直接用些抽象、晦涩的技术名词去给分布式下一个定义，还不如从理解分布式的发展驱动因素开始，我们一起去探寻它的本质，自然而然地也就清楚它的定义了。

在今天这篇文章中，我将带你了解分布式的起源，是如何从单台计算机发展到分布式的，进而帮助你深入理解什么是分布式。为了方便你更好地理解这个演进过程，我将不考虑多核、多处理器的情况，假定每台计算机都是单核、单处理器的。

分布式起源

单兵模式：单机模式

1946 年情人节发布的 ENIAC 是世界上的第一台通用计算机，它占地 170 平米重达 30 吨，每秒可进行 5000 次加法或者 400 次乘法运算，标志着单机模式的开始。

所谓单机模式是指，所有应用程序和数据均部署在一台电脑或服务器上，由一台计算机完成所有的处理。

以铁路售票系统为例，铁路售票系统包括用户管理、火车票管理和订单管理等模块，数据包括用户数据、火车票数据和订单数据等，如果使用单机模式，那么所有的模块和数据均会部署在同一台计算机上，也就是说数据存储、请求处理均由该计算机完成。这种模式的好处是功能、代码和数据集中，便于维护、管理和执行。

单机模式的示意图，如下所示：

这里需要注意的是，本文的所有示意图中，紫色虚线表示在一台计算机内。

事物均有两面性，我们再来看看单机模式的缺点。单个计算机的处理能力取决于 CPU 和内存等，但硬件的发展速度和性能是有限的，而且升级硬件的性价比也是我们要考虑的，由此决定了 CPU 和内存等硬件的性能将成为单机模式的瓶颈。

你有没有发现，单机模式和单兵作战模式非常相似，单台计算机能力再强，就好比特种兵以一敌百，但终归能力有限。此外，将所有任务都交给一台计算机，也会存在将所有鸡蛋放到一个篮子里的风险，也就是单点失效问题。

归纳一下，单机模式的主要问题是：性能受限、存在单点失效问题。

游击队模式：数据并行或数据分布式

既然单机模式存在性能和可用性的问题。那么，有没有什么更好的计算模式呢？答案是肯定的。

为解决单机模式的问题，并行计算得到了发展，进而出现了数据并行（也叫作数据分布式）模式。并行计算采用消息共享模式使用多台计算机并行运行或执行多项任务，核心原理是每台计算机上执行相同的程序，将数据进行拆分放到不同的计算机上进行计算。

请注意，并行计算强调的是对数据进行拆分，任务程序在每台机器上运行。要达到这个目的，我们必须首先把单机模式中的应用和数据分离，才可能实现对数据的拆分。这里的应用就是执行任务的程序，任务就是提交的请求。以铁路售票系统为例，运行在服务器上的用户管理、火车票管理和订单管理等程序就是应用，用户提交的查询火车票、购买火车票的请求就是任务。

在单机模式中，应用和数据均在一台计算机或服务器上，要实现数据的并行，首先必须将应用和数据分离以便将应用部署到不同的计算机或服务器上；然后，对同类型的数据进行拆分，比方说，不同计算机或服务器上的应用可以到不同的数据库上获取数据执行任务。

以铁路售票系统的数据并行为例，主要包括两个步骤，如下所示：

第一步，将应用与数据分离，分别部署到不同的服务器上：

第二步，对数据进行拆分，比如把同一类型的数据拆分到两个甚至更多的数据库中，这样应用服务器上的任务就可以针对不同数据并行执行了。

对于铁路售票系统来说，根据线路将用户、火车票和订单数据拆分到不同的数据库中，部署到不同的服务器上，比如京藏线的数据放在数据库服务器 1 上的数据库中，沪深线的数据放在数据库服务器 2 上的数据库中。

需要注意的是，为了更好地帮助你理解这个数据拆分的过程，我在这里选择拆分数据库的方式进行讲解。由于数据库服务器本身的并发特性，因此你也可以根据你的业务情况进行选择，比方说所有业务服务器共用一个数据库服务器，而不一定真的需要去进行数据库拆分。

可以看出，在数据并行或数据分布式模式中，每台计算机都是全量地从头到尾一条龙地执行一个程序，就像一个全能的铁道游击队战士。所以，你也可以将这种模式形象地理解成游击队模式，就和铁道游击队插曲的歌词有点类似：“我们扒飞车那个搞机枪，撞火车那个炸桥梁……”

这种模式的好处是，可以利用多台计算机并行处理多个请求，使得我们可以在相同的时间内完成更多的请求处理，解决了单机模式的计算效率瓶颈问题。但这种模式仍然存在如下几个问题，在实际应用中，我们需要对其进行相应的优化：

相同的应用部署到不同的服务器上，当大量用户请求过来时，如何能比较均衡地转发到不同的应用服务器上呢？解决这个问题的方法是设计一个负载均衡器，我会在”分布式高可靠“模块与你讲述负载均衡的相关原理。

当请求量较大时，对数据库的频繁读写操作，使得数据库的 IO 访问成为瓶颈。解决这个问题的方式是读写分离，读数据库只接收读请求，写数据库只接收写请求，当然读写数据库之间要进行数据同步，以保证数据一致性。

当有些数据成为热点数据时，会导致数据库访问频繁，压力增大。解决这个问题的方法是引入缓存机制，将热点数据加载到缓存中，一方面可以减轻数据库的压力，另一方面也可以提升查询效率。

从上面介绍可以看出，数据并行模式实现了多请求并行处理，但如果单个请求特别复杂，比方说需要几天甚至一周时间的时候，数据并行模式的整体计算效率还是不够高。

由此可见，数据并行模式的主要问题是：对提升单个任务的执行性能及降低时延无效。

集团军模式：任务并行或任务分布式

那么，有没有办法可以提高单个任务的执行性能，或者缩短单个任务的执行时间呢？答案是肯定的。任务并行（也叫作任务分布式）就是为解决这个问题而生的。那什么是任务并行呢？

任务并行指的是，将单个复杂的任务拆分为多个子任务，从而使得多个子任务可以在不同的计算机上并行执行。

我们仍以铁路售票系统为例，任务并行首先是对应用进行拆分，比如按照领域模型将用户管理、火车票管理、订单管理拆分成多个子系统分别运行在不同的计算机或服务器上。换句话说，原本包括用户管理、火车票管理和订单管理的一个复杂任务，被拆分成了多个子任务在不同计算机或服务器上执行，如下图所示：

可以看出，任务并行模式完成一项复杂任务主要有两个核心步骤：首先将单任务拆分成多个子任务，然后让多个子任务并行执行。这种模式和集团军模式很像，任务拆分者对应领导者，不同子系统对应不同兵种，不同子程序执行不同任务就像不同的兵种执行不同的命令一样，并且运行相同子系统或子任务的计算机又可以组成一个兵团。

在集团军模式中，由于多个子任务可以在多台计算机上运行，因此通过将同一任务待处理的数据分散到多个计算机上，在这些计算机上同时进行处理，就可以加快任务执行的速度。因为，只要一个复杂任务拆分出的任意子任务执行时间变短了，那么这个任务的整体执行时间就变短了。

当然，nothing is perfect。集团军模式在提供了更好的性能、扩展性、可维护性的同时，也带来了设计上的复杂性问题，毕竟对一个大型业务的拆分也是一个难题。不过，对于大型业务来讲，从长远收益来看，这个短期的设计阵痛是值得的。这也是许多大型互联网公司、高性能计算机构等竞相对业务进行拆分以及重构的一个重要原因。

分布式是什么？

讲了半天，那到底什么是分布式呢？

总结一下，分布式其实就是将相同或相关的程序运行在多台计算机上，从而实现特定目标的一种计算方式。

从这个定义来看，数据并行、任务并行其实都可以算作是分布式的一种形态。从这些计算方式的演变中不难看出，产生分布式的最主要驱动力量，是我们对于性能、可用性及可扩展性的不懈追求。

总结

在今天这篇文章中，我和你分享了分布式的起源，即从单机模式到数据并行（也叫作数据分布式）模式，再到任务并行（也叫作任务分布式）模式。

单机模式指的是，所有业务和数据均部署到同一台机器上。这种模式的好处是功能、代码和数据集中，便于维护、管理和执行，但计算效率是瓶颈。也就是说单机模式性能受限，也存在单点失效的问题。

数据并行（也叫作数据分布式）模式指的是，对数据进行拆分，利用多台计算机并行执行多个相同任务，通过在相同的时间内完成多个相同任务，从而缩短所有任务的总体执行时间，但对提升单个任务的执行性能及降低时延无效。

任务并行（也叫作任务分布式）模式指的是，单任务拆分成多个子任务，多个子任务并行执行，只要一个复杂任务中的任意子任务的执行时间变短了，那么这个业务的整体执行时间也就变短了。该模式在提高性能、扩展性、可维护性等的同时，也带来了设计上的复杂性问题，比如复杂任务的拆分。

在数据并行和任务并行这两个模式的使用上，用户通常会比较疑惑，到底是采用数据并行还是任务并行呢？一个简单的原则就是：任务执行时间短，数据规模大、类型相同且无依赖，则可采用数据并行；如果任务复杂、执行时间长，且任务可拆分为多个子任务，则考虑任务并行。在实际业务中，通常是这两种模式并用。赶紧行动起来，去分析一下你的业务到底应该采用哪种分布式模式吧，加油！

课后思考

你觉得分布式与传统的并行计算的区别是什么？你可以结合多核、多处理器的情况进行思考。

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再见！

02 | 分布式系统的指标：啥是分布式的三围

你好，我是聂鹏程。

在上一篇文章中，通过对分布式发展历程的学习，我们对分布式技术有了一个整体印象。接下来，我们就再来看看可以用哪些指标去具体地衡量一个分布式系统。如果你已经对分布式系统的指标了解得很清楚了，可以直接跳过这篇文章，学习下一讲的内容。

分布式系统的指标

从分布式技术的起源可以看出，分布式系统的出现就是为了用廉价的、普通的机器解决单个计算机处理复杂、大规模数据和任务时存在的性能问题、资源瓶颈问题，以及可用性和可扩展性问题。换句话说，分布式的目的是用更多的机器，处理更多的数据和更复杂的任务。

由此可以看出，性能、资源、可用性和可扩展性是分布式系统的重要指标。没错，它们就是分布式系统的“三围”。接下来，我们一起来看看这几个指标吧。

性能（Performance）

性能指标，主要用于衡量一个系统处理各种任务的能力。无论是分布式系统还是单机系统，都会对性能有所要求。

不同的系统、服务要达成的目的不同，关注的性能自然也不尽相同，甚至是相互矛盾。常见的性能指标，包括吞吐量（Throughput）、响应时间（Response Time）和完成时间（Turnaround Time）。

吞吐量指的是，系统在一定时间内可以处理的任务数。这个指标可以非常直接地体现一个系统的性能，就好比在客户非常多的情况下，要评判一个银行柜台职员的办事效率，你可以统计一下他在 1 个小时内接待了多少客户。常见的吞吐量指标有 QPS（Queries Per Second）、TPS（Transactions Per Second）和 BPS（Bits Per Second）。

QPS，即查询数每秒，用于衡量一个系统每秒处理的查询数。这个指标通常用于读操作，越高说明对读操作的支持越好。所以，我们在设计一个分布式系统的时候，如果应用主要是读操作，那么需要重点考虑如何提高 QPS，来支持高频的读操作。

TPS，即事务数每秒，用于衡量一个系统每秒处理的事务数。这个指标通常对应于写操作，越高说明对写操作的支持越好。我们在设计一个分布式系统的时候，如果应用主要是写操作，那么需要重点考虑如何提高 TPS，来支持高频写操作。

BPS，即比特数每秒，用于衡量一个系统每秒处理的数据量。对于一些网络系统、数据管理系统，我们不能简单地按照请求数或事务数来衡量其性能。因为请求与请求、事务与事务之间也存在着很大的差异，比方说，有的事务大需要写入更多的数据。那么在这种情况下，BPS 更能客观地反应系统的吞吐量。

响应时间指的是，系统响应一个请求或输入需要花费的时间。响应时间直接影响到用户体验，对于时延敏感的业务非常重要。比如用户搜索导航，特别是用户边开车边搜索的时候，如果响应时间很长，就会直接导致用户走错路。

完成时间指的是，系统真正完成一个请求或处理需要花费的时间。任务并行（也叫作任务分布式）模式出现的其中一个目的，就是缩短整个任务的完成时间。特别是需要计算海量数据或处理大规模任务时，用户对完成时间的感受非常明显。

资源占用（Resource Usage）

资源占用指的是，一个系统提供正常能力需要占用的硬件资源，比如 CPU、内存、硬盘等。

一个系统在没有任何负载时的资源占用，叫做空载资源占用，体现了这个系统自身的资源占用情况。比如，你在手机上安装一个 App，安装的时候通常会提示你有多少 KB，这就是该 App 的空载硬盘资源占用。对于同样的功能，空载资源占用越少，说明系统设计越优秀，越容易被用户接受。

一个系统满额负载时的资源占用，叫做满载资源占用，体现了这个系统全力运行时占用资源的情况，也体现了系统的处理能力。同样的硬件配置上，运行的业务越多，资源占用越少，说明这个系统设计得越好。

可用性（Availability）

可用性，通常指的是系统在面对各种异常时可以正确提供服务的能力。可用性是分布式系统的一项重要指标，衡量了系统的鲁棒性，是系统容错能力的体现。

系统的可用性可以用系统停止服务的时间与总的时间之比衡量。假设一个网站总的运行时间是 24 小时，在 24 小时内，如果网站故障导致不可用的时间是 4 个小时，那么系统的可用性就是 4/24=0.167，也就是 0.167 的比例不可用，或者说 0.833 的比例可用。

除此之外，系统的可用性还可以用某功能的失败次数与总的请求次数之比来衡量，比如对网站请求 1000 次，其中有 10 次请求失败，那么可用性就是 99%。

你可能经常在一个系统的宣传语中见到或听到 3 个 9（或 3N，3 Nines）、5 个 9（或 9N，9 Nines）。这些宣传语中所说的 3 个 9、5 个 9，实际上就是系统厂商对可用性的一种标榜，表明该系统可以在 99.9% 或 99.999% 的时间里能对外无故障地提供服务。

讲到了可用性，你可能还会想到一个非常近似的术语：可靠性（Reliability）。那可靠性和可用性有什么区别呢？

可靠性通常用来表示一个系统完全不出故障的概率，更多地用在硬件领域。而可用性则更多的是指在允许部分组件失效的情况下，一个系统对外仍能正常提供服务的概率。

杰夫 · 迪恩（Jeff Dean）曾在 Google I/O 大会上透露：谷歌一个基于 1000 台通用计算机的集群，一年之内就有 1000+ 硬盘会出现故障。由于现在比较常见的分布式系统基本上都是基于通用计算机的，这就意味着在这些系统中无法实现真正的可靠，所以我们也会在一些场合见到可靠性和可用性交换使用的情况。

可扩展性（Scalability）

可扩展性，指的是分布式系统通过扩展集群机器规模提高系统性能 (吞吐、响应时间、 完成时间)、存储容量、计算能力的特性，是分布式系统的特有性质。

分布式系统的设计初衷，就是利用集群多机的能力处理单机无法解决的问题。然而，完成某一具体任务所需要的机器数目，即集群规模，取决于单个机器的性能和任务的要求。

当任务的需求随着具体业务不断提高时，除了升级系统的性能做垂直 / 纵向扩展外，另一个做法就是通过增加机器的方式去水平 / 横向扩展系统规模。

这里垂直 / 纵向扩展指的是，增加单机的硬件能力，比如 CPU 增强、内存增大等；水平 / 横向扩展指的就是，增加计算机数量。好的分布式系统总在追求“线性扩展性”，也就是说系统的某一指标可以随着集群中的机器数量呈线性增长。

衡量系统可扩展性的常见指标是加速比（Speedup），也就是一个系统进行扩展后相对扩展前的性能提升。

如果你的扩展目标是为了提高系统吞吐量，则可以用扩展后和扩展前的系统吞吐量之比进行衡量。

如果你的目标是为了缩短完成时间，则可以用扩展前和扩展后的完成时间之比进行衡量。

不同场景下分布式系统的指标

我们都希望自己的分布式系统是高性能、高可用、高扩展和低资源占用的。但出于硬件成本、开发效率等因素的约束，我们无法在性能、可用性、可靠性和资源占用做到面面俱到。因此，在不同的业务场景中，设计者们需要有所取舍。

接下来，我带你一起看一下典型的电商、IoT、电信、HPC（高性能计算）、大数据、云计算、区块链等业务或系统对不同指标的诉求。

电商系统。对于一个电商系统而言，系统设计者最看重的是吞吐量，为了处理更多的用户访问或订单业务，甚至不惜牺牲一些硬件成本。

IoT。对于一个 IoT 系统而言，设计者最看重的是资源占用指标，因为在一些功能极简的 IoT 设备上 RAM、ROM 的可用资源通常都是 KB 级的。

电信业务。对于电信业务而言，最重要的无疑是响应时间、完成时间，以及可用性。因为，你在打电话时不希望你的声音半天才被对方听到，也不希望半天才听到对方的回应，更不希望你的电话无法拨出。

HPC。HPC 系统最显著的特点是任务执行时间极长，一个天体物理任务的分析和计算通常耗时数周甚至数月。因此，通过水平扩展来提高系统的加速比，是 HPC 系统设计者需要关注的。

大数据。大数据任务的处理时间可能相对 HPC 系统来讲比较短，但常见的完成时间也达到了小时级，所以扩展性也是大数据系统首先要考虑的。

云计算。对于一个云计算系统而言，常见任务是虚拟主机或容器的创建、资源调整、销毁等操作，如何减少这些操作的完成时间，从而提升用户体验是设计者们要重点关注的。另外，云计算系统本质上卖的是资源，那么降低系统本身的资源开销，也是系统设计的重中之重。

区块链。区块链的吞吐量比较低，比特币的 TPS 只有 7 次每秒，单平均一次交易的确认就需要 10 分钟左右，因此吞吐量和完成时间通常是区块链系统设计者的首要目标。

总结与思考

按照不同维度，分布式系统的指标可以分为性能、资源占用、可用性、可扩展性这四大类。我们自然希望自己的系统，是高性能、高可用、高扩展和低资源占用的，但考虑到硬件成本、开发效率等因素，必须要在设计不同的系统、业务时有所取舍。

所以，我又和你分析了典型的电商、IoT、电信、HPC（高性能计算）、大数据、云计算、区块链等业务或系统的不同诉求，进而得出了系统设计者需要关注哪些指标。你在设计其他类型的系统时，可以按照这个思路进行取舍。

我在文中提到了，分布式系统的指标之间会存在一些冲突或约束。那你不妨思考一下：我们今天讲解的指标中，哪些指标之间是相互制约、相互冲突的，它们又是如何制约的呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

03 | 分布式互斥：有你没我，有我没你

你好！我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

通过前面的两篇文章，相信你已经对此次的分布式世界之行有了一个初步了解，想必对此次旅行也是充满期待。今天，我就带你正式踏上第一站：分布式协调与同步。在这一站，我将带你学习如何让分布在不同计算机上的程序具有“团队精神”，换句话说就是如何让程序通过协作共同去达成一个业务目标。

在本站，我将带你打卡的第一个景点是分布式互斥。那，什么是分布式互斥呢？

什么是分布式互斥？

想象一下，你正在一家餐厅使用自助咖啡机泡制咖啡，突然有个人过来挪走了你的杯子，开始泡制他自己的咖啡。你耐着性子等他操作完，继续泡制自己的咖啡。结果你开始没多久，他又回来中断了你泡制咖啡的过程。相信要不了几个回合，你和他就会上演一场“有你没我，有我没你”的格斗了。

这样现实的问题也同样存在于分布式世界。就像我们使用自助咖啡机时不希望被打扰一样，对于同一共享资源，一个程序正在使用的时候也不希望被其他程序打扰。这，就要求同一时刻只能有一个程序能够访问这种资源。

在分布式系统里，这种排他性的资源访问方式，叫作分布式互斥（Distributed Mutual Exclusion），而这种被互斥访问的共享资源就叫作临界资源（Critical Resource）。

接下来，我带你一起看看如何才能让分布式系统里的程序互斥地访问临界资源。

霸道总裁：集中式算法

对于前面提到的咖啡机问题，我们首先想到的就是，增加一个“协调者”来约束大家使用自助咖啡机，解决强行插入打断别人的问题。

类似地，我们引入一个协调者程序，得到一个分布式互斥算法。每个程序在需要访问临界资源时，先给协调者发送一个请求。如果当前没有程序使用这个资源，协调者直接授权请求程序访问；否则，按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源，则通知协调者，协调者从“排号”的队列里取出排在最前面的请求，并给它发送授权消息。拿到授权消息的程序，可以直接去访问临界资源。

这个互斥算法，就是我们所说的集中式算法，也可以叫做中央服务器算法。之所以这么称呼，是因为协调者代表着集中程序或中央服务器。

集中式算法的示意图如下所示：

如图所示，程序 1、2、3、4 为普通运行程序，另一个程序为协调者。当程序 2 和程序 4 需要使用临界资源时，它们会向协调者发起申请，请求协调者授权。

不巧的是，程序 3 正在使用临界资源。这时，协调者根据程序 2 和 4 的申请时间顺序，依次将它们放入等待队列。在这个案例里，程序 4 的申请时间早于程序 2，因此排在程序 2 的前面。

程序 3 使用完临界资源后，通知协调者释放授权。此时，协调者从等待队列中取出程序 4，并给它发放授权。这时，程序 4 就可以使用临界资源了。

从上述流程可以看出，一个程序完成一次临界资源访问，需要如下几个流程和消息交互：

向协调者发送请求授权信息，1 次消息交互；

协调者向程序发放授权信息，1 次消息交互；

程序使用完临界资源后，向协调者发送释放授权，1 次消息交互。

因此，每个程序完成一次临界资源访问，需要进行 3 次消息交互。

不难看出，集中式算法的优点在于直观、简单、信息交互量少、易于实现，并且所有程序只需和协调者通信，程序之间无需通信。但是，这个算法的问题也出在了协调者身上。

一方面，协调者会成为系统的性能瓶颈。想象一下，如果有 100 个程序要访问临界资源，那么协调者要处理 100*3=300 条消息。也就是说，协调者处理的消息数量会随着需要访问临界资源的程序数量线性增加。

另一方面，容易引发单点故障问题。协调者故障，会导致所有的程序均无法访问临界资源，导致整个系统不可用。

因此，在使用集中式算法的时候，一定要选择性能好、可靠性高的服务器来运行协调者。

小结一下：集中式算法具有简单、易于实现的特点，但可用性、性能易受协调者影响。在可靠性和性能有一定保障的情况下，比如中央服务器计算能力强、性能高、故障率低，或者中央服务器进行了主备备份，主故障后备可以立马升为主，且数据可恢复的情况下，集中式算法可以适用于比较广泛的应用场景。

民主协商：分布式算法

既然引入协调者会带来一些问题，这时你可能就会想，不用协调者是否可以实现对临界资源的互斥访问呢？想象一下，当你需要使用自助咖啡机的时候，是不是可以先去征求其他人的意见，在确认其他人都没在使用也暂时不会使用咖啡机时，你就可以放心大胆地去泡制自己的咖啡了呢？

同理，我们可以把这套算法用于分布式系统。当一个程序要访问临界资源时，先向系统中的其他程序发送一条请求消息，在接收到所有程序返回的同意消息后，才可以访问临界资源。其中，请求消息需要包含所请求的资源、请求者的 ID，以及发起请求的时间。

这，就是民主协商法。在分布式领域中，我们称之为分布式算法，或者使用组播和逻辑时钟的算法。

如图所示，程序 1、2、3 需要访问共享资源 A。在时间戳为 8 的时刻，程序 1 想要使用资源 A，于是向程序 2 和 3 发起使用资源 A 的申请，希望得到它们的同意。在时间戳为 12 的时刻，程序 3 想要使用资源 A，于是向程序 1 和 2 发起访问资源 A 的请求。

如图所示，此时程序 2 暂时不访问资源 A，因此同意了程序 1 和 3 的资源访问请求。对于程序 3 来说，由于程序 1 提出请求的时间更早，因此同意程序 1 先使用资源，并等待程序 1 返回同意消息。

如图所示，程序 1 接收到其他所有程序的同意消息之后，开始使用资源 A。当程序 1 使用完资源 A 后，释放使用权限，向请求队列中需要使用资源 A 的程序 3 发送同意使用资源的消息，并将程序 3 从请求队列中删除。此时，程序 3 收到了其他所有程序的同意消息，获得了使用资源 A 的权限，开始使用临界资源 A 的旅程。

从上述流程可以看出，一个程序完成一次临界资源的访问，需要进行如下的信息交互：

向其他 n-1 个程序发送访问临界资源的请求，总共需要 n-1 次消息交互；

需要接收到其他 n-1 个程序回复的同意消息，方可访问资源，总共需要 n-1 次消息交互。

可以看出，一个程序要成功访问临界资源，至少需要 2*(n-1) 次消息交互。假设，现在系统中的 n 个程序都要访问临界资源，则会同时产生 2n(n-1) 条消息。总结来说，在大型系统中使用分布式算法，消息数量会随着需要访问临界资源的程序数量呈指数级增加，容易导致高昂的“沟通成本”。

从上述分析不难看出，分布式算法根据“先到先得”以及“投票全票通过”的机制，让每个程序按时间顺序公平地访问资源，简单粗暴、易于实现。但，这个算法可用性很低，主要包括两个方面的原因：

当系统内需要访问临界资源的程序增多时，容易产生“信令风暴”，也就是程序收到的请求完全超过了自己的处理能力，而导致自己正常的业务无法开展。

一旦某一程序发生故障，无法发送同意消息，那么其他程序均处在等待回复的状态中，使得整个系统处于停滞状态，导致整个系统不可用。所以，相对于集中式算法的协调者故障，分布式算法的可用性更低。

针对可用性低的一种改进办法是，如果检测到一个程序故障，则直接忽略这个程序，无需再等待它的同意消息。这就好比在自助餐厅，一个人离开餐厅了，那你在使用咖啡机前，也无需征得他的同意。但这样的话，每个程序都需要对其他程序进行故障检测，这无疑带来了更大的复杂性。

因此，分布式算法适合节点数目少且变动不频繁的系统，且由于每个程序均需通信交互，因此适合 P2P 结构的系统。比如，运行在局域网中的分布式文件系统，具有 P2P 结构的系统等。

那么，在我们工作中，什么样的场景适合采用分布式算法呢？

Hadoop 是我们非常熟悉的分布式系统，其中的分布式文件系统 HDFS 的文件修改就是一个典型的应用分布式算法的场景。

如下图所示，处于同一个局域网内的计算机 1、2、3 中都有同一份文件的备份信息，且它们可以相互通信。这个共享文件，就是临界资源。当计算机 1 想要修改共享的文件时，需要进行如下操作：

计算机 1 向计算机 2、3 发送文件修改请求；

计算机 2、3 发现自己不需要使用资源，因此同意计算机 1 的请求；

计算机 1 收到其他所有计算机的同意消息后，开始修改该文件；

计算机 1 修改完成后，向计算机 2、3 发送文件修改完成的消息，并发送修改后的文件数据；

计算机 2 和 3 收到计算机 1 的新文件数据后，更新本地的备份文件。

归纳一下：分布式算法是一个“先到先得”和“投票全票通过”的公平访问机制，但通信成本较高，可用性也比集中式算法低，适用于临界资源使用频度较低，且系统规模较小的场景。

轮值 CEO：令牌环算法

那么除了集中式算法、分布式算法以外，还有什么方法可以实现分布式互斥吗？答案是肯定的。毕竟，方法总比问题多。华为独创的轮值 CEO 其实就给了我们一个很好的启示。在华为的轮值 CEO 体系里，CEO 就是临界资源，同时只能有一个人担任，由多名高管轮流出任 CEO。

类似地，程序访问临界资源问题也可按照轮值 CEO 的思路实现。 如下图所示，所有程序构成一个环结构，令牌按照顺时针（或逆时针）方向在程序之间传递，收到令牌的程序有权访问临界资源，访问完成后将令牌传送到下一个程序；若该程序不需要访问临界资源，则直接把令牌传送给下一个程序。

在分布式领域，这个算法叫作令牌环算法，也可以叫作基于环的算法。为了便于理解与记忆，你完全可以把这个方法形象地理解为轮值 CEO 法。

因为在使用临界资源前，不需要像分布式算法那样挨个征求其他程序的意见了，所以相对而言，在令牌环算法里单个程序具有更高的通信效率。同时，在一个周期内，每个程序都能访问到临界资源，因此令牌环算法的公平性很好。

但是，不管环中的程序是否想要访问资源，都需要接收并传递令牌，所以也会带来一些无效通信。假设系统中有 100 个程序，那么程序 1 访问完资源后，即使其它 99 个程序不需要访问，也必须要等令牌在其他 99 个程序传递完后，才能重新访问资源，这就降低了系统的实时性。

综上，令牌环算法非常适合通信模式为令牌环方式的分布式系统，例如移动自组织网络系统。一个典型的应用场景就是无人机通信。

无人机在通信时，工作原理类似于对讲机，同一时刻只能发送信息或接收信息。因此，通信中的上行链路（即向外发送信息的通信渠道）是临界资源。

如下图所示，所有的无人机组成一个环，按照顺时针方向通信。每个无人机只知道其前一个发送信息的无人机，和后一个将要接收信息的无人机。拥有令牌的无人机可以向外发送信息，其他无人机只能接收数据。拥有令牌的无人机通信完成后，会将令牌传送给后一个无人机。

所有的无人机轮流通信并传输数据，从而消除了多个无人机对通信资源的争夺，使得每个无人机都能接收到其他无人机的信息，降低了通信碰撞导致的丢包率，保证了网络通信的稳定性，提高了多个无人机之间的协作效率。

令牌环算法是一种更加公平的算法，通常会与通信令牌结合，从而取得很好的效果。特别是当系统支持广播或组播通信模式时，该算法更加高效、可行。

对于集中式和分布式算法都存在的单点故障问题，在令牌环中，若某一个程序（例如上图的无人机 2）出现故障，则直接将令牌传递给故障程序的下一个程序（例如，上图中无人机 1 直接将令牌传送给无人机 3），从而很好地解决单点故障问题，提高系统的健壮性，带来更好的可用性。但，这就要求每个程序都要记住环中的参与者信息，这样才能知道在跳过一个参与者后令牌应该传递给谁。

小结一下：令牌环算法的公平性高，在改进单点故障后，稳定性也很高，适用于系统规模较小，并且系统中每个程序使用临界资源的频率高且使用时间比较短的场景。

知识扩展：有适合大规模系统中的分布式互斥算法吗？

可以看到，上面提到的集中式、分布式和令牌环 3 个互斥算法，都不适用于规模过大、节点数量过多的系统。那么，什么样的互斥算法适用于大规模系统呢？

由于大规模系统的复杂性，我们很自然地想到要用一个相对复杂的互斥算法。时下有一个很流行的互斥算法，两层结构的分布式令牌环算法，把整个广域网系统中的节点组织成两层结构，可以用于节点数量较多的系统，或者是广域网系统。

我们知道，广域网由多个局域网组成，因此在该算法中，局域网是较低的层次，广域网是较高的层次。每个局域网中包含若干个局部进程和一个协调进程。局部进程在逻辑上组成一个环形结构，在每个环形结构上有一个局部令牌 T 在局部进程间传递。局域网与局域网之间通过各自的协调进程进行通信，这些协调进程同样组成一个环结构，这个环就是广域网中的全局环。在这个全局环上，有一个全局令牌在多个协调进程间传递。

总结

我首先结合生活中的案例，带你剖析了什么是分布式互斥，以及为什么需要分布式互斥。然后，我和你介绍了 3 类典型的分布式互斥方法，即：集中式算法、分布式算法，以及令牌环算法，并列举了对应的适用场景，相信你通过今天的学习，一定可以为你的场景选择一个合适的分布式互斥算法了，加油！

接下来，我把今天的内容通过下面的一张思维导图再全面总结下。

课后问题

最后，我想请你思考以下两个问题：

你认为集中式算法、分布式算法和令牌环算法，还有什么可以改进的地方吗？

传统单机上的互斥方法，为什么不能用于分布式环境呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

04 | 分布式选举：国不可一日无君

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

相信你对集群的概念并不陌生。简单说，集群一般是由两个或两个以上的服务器组建而成，每个服务器都是一个节点。我们经常会听到数据库集群、管理集群等概念，也知道数据库集群提供了读写功能，管理集群提供了管理、故障恢复等功能。

接下来，你开始好奇了，对于一个集群来说，多个节点到底是怎么协同，怎么管理的呢。比如，数据库集群，如何保证写入的数据在每个节点上都一致呢？

也许你会说，这还不简单，选一个“领导”来负责调度和管理其他节点就可以了啊。

这个想法一点儿也没错。这个“领导”，在分布式中叫做主节点，而选“领导”的过程在分布式领域中叫作分布式选举。

然后，你可能还会问，怎么选主呢。那接下来，我们就一起去揭开这个谜底吧。

为什么要有分布式选举？

主节点，在一个分布式集群中负责对其他节点的协调和管理，也就是说，其他节点都必须听从主节点的安排。

主节点的存在，就可以保证其他节点的有序运行，以及数据库集群中的写入数据在每个节点上的一致性。这里的一致性是指，数据在每个集群节点中都是一样的，不存在不同的情况。

当然，如果主故障了，集群就会天下大乱，就好比一个国家的皇帝驾崩了，国家大乱一样。比如，数据库集群中主节点故障后，可能导致每个节点上的数据会不一致。

这，就应了那句话“国不可一日无君”，对应到分布式系统中就是“集群不可一刻无主”。总结来说，选举的作用就是选出一个主节点，由它来协调和管理其他节点，以保证集群有序运行和节点间数据的一致性。

分布式选举的算法

那么，如何在集群中选出一个合适的主呢？这是一个技术活儿，目前常见的选主方法有基于序号选举的算法（ 比如，Bully 算法）、多数派算法（比如，Raft 算法、ZAB 算法）等。接下来，就和我一起来看看这几种算法吧。

长者为大：Bully 算法

Bully 算法是一种霸道的集群选主算法，为什么说是霸道呢？因为它的选举原则是“长者”为大，即在所有活着的节点中，选取 ID 最大的节点作为主节点。

在 Bully 算法中，节点的角色有两种：普通节点和主节点。初始化时，所有节点都是平等的，都是普通节点，并且都有成为主的权利。但是，当选主成功后，有且仅有一个节点成为主节点，其他所有节点都是普通节点。当且仅当主节点故障或与其他节点失去联系后，才会重新选主。

Bully 算法在选举过程中，需要用到以下 3 种消息：

Election 消息，用于发起选举；

Alive 消息，对 Election 消息的应答；

Victory 消息，竞选成功的主节点向其他节点发送的宣誓主权的消息。

Bully 算法选举的原则是“长者为大”，意味着它的假设条件是，集群中每个节点均知道其他节点的 ID。在此前提下，其具体的选举过程是：

集群中每个节点判断自己的 ID 是否为当前活着的节点中 ID 最大的，如果是，则直接向其他节点发送 Victory 消息，宣誓自己的主权；

如果自己不是当前活着的节点中 ID 最大的，则向比自己 ID 大的所有节点发送 Election 消息，并等待其他节点的回复；

若在给定的时间范围内，本节点没有收到其他节点回复的 Alive 消息，则认为自己成为主节点，并向其他节点发送 Victory 消息，宣誓自己成为主节点；若接收到来自比自己 ID 大的节点的 Alive 消息，则等待其他节点发送 Victory 消息；

若本节点收到比自己 ID 小的节点发送的 Election 消息，则回复一个 Alive 消息，告知其他节点，我比你大，重新选举。

目前已经有很多开源软件采用了 Bully 算法进行选主，比如 MongoDB 的副本集故障转移功能。MongoDB 的分布式选举中，采用节点的最后操作时间戳来表示 ID，时间戳最新的节点其 ID 最大，也就是说时间戳最新的、活着的节点是主节点。

小结一下。Bully 算法的选择特别霸道和简单，谁活着且谁的 ID 最大谁就是主节点，其他节点必须无条件服从。这种算法的优点是，选举速度快、算法复杂度低、简单易实现。

但这种算法的缺点在于，需要每个节点有全局的节点信息，因此额外信息存储较多；其次，任意一个比当前主节点 ID 大的新节点或节点故障后恢复加入集群的时候，都可能会触发重新选举，成为新的主节点，如果该节点频繁退出、加入集群，就会导致频繁切主。

民主投票：Raft 算法

Raft 算法是典型的多数派投票选举算法，其选举机制与我们日常生活中的民主投票机制类似，核心思想是“少数服从多数”。也就是说，Raft 算法中，获得投票最多的节点成为主。

采用 Raft 算法选举，集群节点的角色有 3 种：

Leader，即主节点，同一时刻只有一个 Leader，负责协调和管理其他节点；

Candidate，即候选者，每一个节点都可以成为 Candidate，节点在该角色下才可以被选为新的 Leader；

Follower，Leader 的跟随者，不可以发起选举。

Raft 选举的流程，可以分为以下几步：

初始化时，所有节点均为 Follower 状态。

开始选主时，所有节点的状态由 Follower 转化为 Candidate，并向其他节点发送选举请求。

其他节点根据接收到的选举请求的先后顺序，回复是否同意成为主。这里需要注意的是，在每一轮选举中，一个节点只能投出一张票。

若发起选举请求的节点获得超过一半的投票，则成为主节点，其状态转化为 Leader，其他节点的状态则由 Candidate 降为 Follower。Leader 节点与 Follower 节点之间会定期发送心跳包，以检测主节点是否活着。

当 Leader 节点的任期到了，即发现其他服务器开始下一轮选主周期时，Leader 节点的状态由 Leader 降级为 Follower，进入新一轮选主。

节点的状态迁移如下所示（图中的 term 指的是选举周期）：

请注意，每一轮选举，每个节点只能投一次票。这种选举就类似人大代表选举，正常情况下每个人大代表都有一定的任期，任期到后会触发重新选举，且投票者只能将自己手里唯一的票投给其中一个候选者。对应到 Raft 算法中，选主是周期进行的，包括选主和任值两个时间段，选主阶段对应投票阶段，任值阶段对应节点成为主之后的任期。但也有例外的时候，如果主节点故障，会立马发起选举，重新选出一个主节点。

Google 开源的 Kubernetes，擅长容器管理与调度，为了保证可靠性，通常会部署 3 个节点用于数据备份。这 3 个节点中，有一个会被选为主，其他节点作为备。Kubernetes 的选主采用的是开源的 etcd 组件。而，etcd 的集群管理器 etcds，是一个高可用、强一致性的服务发现存储仓库，就是采用了 Raft 算法来实现选主和一致性的。

小结一下。Raft 算法具有选举速度快、算法复杂度低、易于实现的优点；缺点是，它要求系统内每个节点都可以相互通信，且需要获得过半的投票数才能选主成功，因此通信量大。该算法选举稳定性比 Bully 算法好，这是因为当有新节点加入或节点故障恢复后，会触发选主，但不一定会真正切主，除非新节点或故障后恢复的节点获得投票数过半，才会导致切主。

具有优先级的民主投票：ZAB 算法

ZAB（ZooKeeper Atomic Broadcast）选举算法是为 ZooKeeper 实现分布式协调功能而设计的。相较于 Raft 算法的投票机制，ZAB 算法增加了通过节点 ID 和数据 ID 作为参考进行选主，节点 ID 和数据 ID 越大，表示数据越新，优先成为主。相比较于 Raft 算法，ZAB 算法尽可能保证数据的最新性。所以，ZAB 算法可以说是对 Raft 算法的改进。

使用 ZAB 算法选举时，集群中每个节点拥有 3 种角色：

Leader，主节点；

Follower，跟随者节点；

Observer，观察者，无投票权。

选举过程中，集群中的节点拥有 4 个状态：

Looking 状态，即选举状态。当节点处于该状态时，它会认为当前集群中没有 Leader，因此自己进入选举状态。

Leading 状态，即领导者状态，表示已经选出主，且当前节点为 Leader。

Following 状态，即跟随者状态，集群中已经选出主后，其他非主节点状态更新为 Following，表示对 Leader 的追随。

Observing 状态，即观察者状态，表示当前节点为 Observer，持观望态度，没有投票权和选举权。

投票过程中，每个节点都有一个唯一的三元组 (server_id, server_zxID, epoch)，其中 server_id 表示本节点的唯一 ID；server_zxID 表示本节点存放的数据 ID，数据 ID 越大表示数据越新，选举权重越大；epoch 表示当前选取轮数，一般用逻辑时钟表示。

ZAB 选举算法的核心是“少数服从多数，ID 大的节点优先成为主”，因此选举过程中通过 (vote_id, vote_zxID) 来表明投票给哪个节点，其中 vote_id 表示被投票节点的 ID，vote_zxID 表示被投票节点的服务器 zxID。ZAB 算法选主的原则是：server_zxID 最大者成为 Leader；若 server_zxID 相同，则 server_id 最大者成为 Leader。

接下来，我以 3 个 Server 的集群为例，此处每个 Server 代表一个节点，与你介绍 ZAB 选主的过程。

第一步：当系统刚启动时，3 个服务器当前投票均为第一轮投票，即 epoch=1，且 zxID 均为 0。此时每个服务器都推选自己，并将选票信息 <epoch, vote_id, vote_zxID> 广播出去。

第二步：根据判断规则，由于 3 个 Server 的 epoch、zxID 都相同，因此比较 server_id，较大者即为推选对象，因此 Server 1 和 Server 2 将 vote_id 改为 3，更新自己的投票箱并重新广播自己的投票。

第三步：此时系统内所有服务器都推选了 Server 3，因此 Server 3 当选 Leader，处于 Leading 状态，向其他服务器发送心跳包并维护连接；Server1 和 Server2 处于 Following 状态。

小结一下。ZAB 算法性能高，对系统无特殊要求，采用广播方式发送信息，若节点中有 n 个节点，每个节点同时广播，则集群中信息量为 n*(n-1) 个消息，容易出现广播风暴；且除了投票，还增加了对比节点 ID 和数据 ID，这就意味着还需要知道所有节点的 ID 和数据 ID，所以选举时间相对较长。但该算法选举稳定性比较好，当有新节点加入或节点故障恢复后，会触发选主，但不一定会真正切主，除非新节点或故障后恢复的节点数据 ID 和节点 ID 最大，且获得投票数过半，才会导致切主。

三种选举算法的对比分析

好了，我已经带你理解了分布式选举的 3 种经典算法，即 Bully 算法、Raft 算法和 ZAB 算法。那么接下来，我就从消息传递内容、选举机制和选举过程的维度，对这 3 种算法进行一个对比分析，以帮助你理解记忆。

知识扩展：为什么“多数派”选主算法通常采用奇数节点，而不是偶数节点呢？

多数派选主算法的核心是少数服从多数，获得投票多的节点胜出。想象一下，如果现在采用偶数节点集群，当两个节点均获得一半投票时，到底应该选谁为主呢？

答案是，在这种情况下，无法选出主，必须重新投票选举。但即使重新投票选举，两个节点拥有相同投票数的概率也会很大。因此，多数派选主算法通常采用奇数节点。

这，也是大家通常看到 ZooKeeper、 etcd、Kubernetes 等开源软件选主均采用奇数节点的一个关键原因。

总结

今天，我首先与你讲述了什么是分布式选举，以及为什么需要分布式选举。然后，我和你介绍了实现分布式选举的 3 种方法，即：Bully 算法、Raft 算法，以及 ZooKeeper 中的 ZAB 算法，并通过实例与你展示了各类方法的选举流程。

我将今天的主要内容总结为了如下所示的思维导图，来帮助你加深理解与记忆。

思考题

分布式选举和一致性的关系是什么？

你是否见到过一个集群中存在双主的场景呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

05 | 分布式共识：存异求同

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

首先，我们来回忆下上篇文章的分布式选举。分布式选举问题，是从多个节点中选出一个主节点，相关的选举方法几乎都有一个共同特点：每个节点都有选举权和被选举权。大部分选举方法采用多数策略，也就是说一个节点只有得到了大部分节点的同意或认可才能成为主节点，然后主节点向其他节点宣告主权。

其实，这个选主过程就是一个分布式共识问题，因为每个节点在选出主节点之前都可以认为自己会成为主节点，也就是说集群节点“存异”；而通过选举的过程选出主节点，让所有的节点都认可该主节点，这叫“求同”。由此可见，分布式共识的本质就是“存异求同”。

所以，从本质上看，分布式选举问题，其实就是传统的分布式共识方法，主要是基于多数投票策略实现的。基于多数投票策略的分布式选举方法，如果用于分布式在线记账一致性问题中，那么记账权通常会完全掌握到主节点的手里，这使得主节点非常容易造假，且存在性能瓶颈。因此，分布式选举不适用于分布式在线记账的一致性问题。在今天这篇文章中，我就带你了解另外一种用于解决分布式在线记账一致性问题的分布式共识技术。

这里所说的分布式在线记账，是指在没有集中的发行方，也就是没有银行参与的情况下，任意一台接入互联网的电脑都能参与买卖，所有看到该交易的服务器都可以记录这笔交易，并且记录信息最终都是一致的，以保证交易的准确性。而如何保证交易的一致性，就是该场景下的分布式共识问题。

接下来，我们就一起学习下分布式共识技术吧。

什么是分布式共识？

假设，现在有 5 台服务器，分散在美国华盛顿、英国伦敦、法国巴黎、中国北京、中国上海，分别对应着用户{A,B,C,D,E}。现在，用户 A 给用户 B 转了 100 元。

在传统方法中，我们通过银行进行转账并记录该笔交易。但分布式在线记账方法中，没有银行这样的一个集中方，而是由上述 5 台服务器来记录该笔交易。但是，这 5 台服务器均是有各自想法的个体，都可以自主操作或记录，那么如何保证记录的交易是一致的呢？这，就是分布式共识技术要解决的问题。

可以看出，分布式共识就是在多个节点均可独自操作或记录的情况下，使得所有节点针对某个状态达成一致的过程。通过共识机制，我们可以使得分布式系统中的多个节点的数据达成一致。

看到这里，相信你已经看出来了，我在这里说的分布式在线记账，就是近几年比较火的区块链技术解决的问题。而分布式共识技术，就是区块链技术共识机制的核心。

接下来，请和我一起看看分布式共识是如何实现的，有哪些方法吧。

分布式共识方法

为了不影响你理解分布式共识的核心技术，我会先和你分享区块链中的一个核心概念：挖矿。

在传统的交易方式中，用户 A 给用户 B 转账，需要银行来实行具体的转账操作并记录交易，银行会从中收取相应的手续费。而采用分布式在线记账的话，参与记录这笔交易的服务器，也可以从中获得一些奖励（这些奖励，在区块链技术中可以换成钱）。所有服务器帮助记录交易并达成一致的过程，就是区块链中的“挖矿”。

区块链是由包含交易信息的区块从后向前有序链接起来的数据结构，其中区块是指很多交易数据的集合，每个区块包括区块头和区块体，区块头包括前一区块的哈希值、本区块的哈希值和时间戳；区块体用来存储交易数据。如果你对区块链技术的其他概念感兴趣的话，可以自行查阅更多资料。

接下来，我将与你介绍 3 种主流的解决分布式在线记账一致性问题的共识技术，即：PoW（Proof-of-Work，工作量证明）、PoS（Proof-of-Stake，权益证明）和 DPoS（Delegated Proof of Stake，委托权益证明）。

PoW

从分布式选举问题可以看出，同一轮选举中有且仅有一个节点成为主节点。同理，在分布式在线记账问题中，针对同一笔交易，有且仅有一个节点或服务器可以获得记账权，然后其他节点或服务器同意该节点或服务器的记账结果，达成一致。

也就是说，分布式共识包括两个关键点，获得记账权和所有节点或服务器达成一致。

PoW 算法，是以每个节点或服务器的计算能力（即“算力”）来竞争记账权的机制，因此是一种使用工作量证明机制的共识算法。也就是说，谁的计算力强、工作能力强，谁获得记账权的可能性就越大。

那么，如何体现节点的“算力”呢？答案就是，每个节点都去解一道题，谁能先解决谁的能力就强。

假设每个节点会划分多个区块用于记录用户交易，PoW 算法获取记账权的原理是：利用区块的 index、前一个区块的哈希值、交易的时间戳、区块数据和 nonce 值，通过 SHA256 哈希算法计算出一个哈希值，并判断前 k 个值是否都为 0。如果不是，则递增 nonce 值，重新按照上述方法计算；如果是，则本次计算的哈希值为要解决的题目的正确答案。谁最先计算出正确答案，谁就获得这个区块的记账权。

请注意：nonce 值是用来找到一个满足哈希值的数字；k 为哈希值前导零的个数，标记了计算的难度，0 越多计算难度越大。

达成共识的过程，就是获得记账权的节点将该区块信息广播给其他节点，其他节点判断该节点找到的区块中的所有交易都是有效且之前未存在过的，则认为该区块有效，并接受该区块，达成一致。

接下来，我以上文提到的分散在世界各地的 5 台服务器为例，和你说明基于 PoW 的共识记账过程。

假设客户端 A 产生一个新的交易，基于 PoW 的共识记账过程为：

客户端 A 产生新的交易，向全网进行广播，要求对交易进行记账。

每个记账节点接收到这个请求后，将收到的交易信息放入一个区块中。

每个节点通过 PoW 算法，计算本节点的区块的哈希值，尝试找到一个具有足够工作量难度的工作量证明。

若节点 D 找到了一个工作量证明向全网广播。当然，当且仅当包含在该区块中的交易都是有效且之前未存在过的，其他节点才会认同该区块的有效性。

其他节点接收到广播信息后，若该区块有效，接受该区块，并跟随在该区块的末尾，制造新区块延长该链条，将被接受的区块的随机哈希值视为新区块的随机哈希值。

可以看出，PoW 算法中，谁的计算能力强，获得记账权的可能性就越大。但必须保证其记账的区块是有效的，并在之前未存在过，才能获得其他节点的认可。

目前，比特币平台采用了 PoW 算法，属于区块链 1.0 阶段，其重心在于货币，比特币大约 10min 才会产生一个区块，区块的大小也只有 1MB，仅能够包含 3000～4000 笔交易，平均每秒只能够处理 5~7（个位数）笔交易。

PoW 通过“挖矿”的方式发行新币，把比特币分散给个人，实现了相对的公平。PoW 的容错机制，允许全网 50% 的节点出错，因此，如果要破坏系统，则需要投入极大成本（若你有全球 51% 的算力，则可尝试攻击比特币）。

但，PoW 机制每次达成共识需要全网共同参与运算，增加了每个节点的计算量，并且如果题目过难，会导致计算时间长、资源消耗多；而如果题目过于简单，会导致大量节点同时获得记账权，冲突多。这些问题，都会增加达成共识的时间。

所以，PoW 机制的缺点也很明显，共识达成的周期长、效率低，资源消耗大。

PoS

为了解决 PoW 算法的问题，引入了 PoS 算法。它的核心原理是，由系统权益代替算力来决定区块记账权，拥有的权益越大获得记账权的概率就越大。

这里所谓的权益，就是每个节点占有货币的数量和时间，而货币就是节点所获得的奖励。PoW 算法充分利用了分布式在线记账中的奖励，鼓励“利滚利”。

在股权证明 PoS 模式下，根据你持有货币的数量和时间，给你发利息。每个币每天产生 1 币龄，比如你持有 100 个币，总共持有了 50 天，那么，你的币龄就为 5000。这个时候，如果你发现了一个 PoS 区块，你的币龄就会被减少 365。每被减少 365 币龄，你就可以从区块中获得 0.05 个币的利息 (可理解为年利率 5%)。

在这个案例中，利息 = （5000*5% ）/365 = 0.68 个币。这下就有意思了，持币有利息。

基于 PoS 算法获得区块记账权的方法与基于 PoW 的方法类似，不同之处在于：节点计算获取记账权的方法不一样，PoW 是利用区块的 index、前一个区块的哈希值、交易的时间戳、区块数据和 nonce 值，通过 SHA256 哈希算法计算出一个哈希值，并判断前 k 个值是否都为 0，而 PoS 是根据节点拥有的股权或权益进行计算的。

接下来，我们看一个具体的案例。假设一个公链网络中，共有 3 个节点，A 、B 和 C。其中 A 节点拥有 10000 个币，总共持有 30 天，而 B 和 C 节点分别有 1000 和 2000 个币，分别持有 15 和 20 天。

通过 PoS 算法决定区块记账权的流程和 PoW 算法类似，唯一不同的就是，每个节点在计算自己记账权的时候，通过计算自己的股权或权益来评估，如果发现自己权益最大，则将自己的区块广播给其他节点，当然必须保证该区块的有效性。

以太坊平台属于区块链 2.0 阶段，在区块链 1.0 的基础上进一步强调了合约，采用了 PoS 算法。12 年发布的点点币（PPC），综合了 PoW 工作量证明及 PoS 权益证明方式，从而在安全和节能方面实现了创新。

可以看出，PoS 将算力竞争转变成权益竞争。与 PoW 相比，PoS 不需要消耗大量的电力就能够保证区块链网络的安全性，同时也不需要在每个区块中创建新的货币来激励记账者参与当前网络的运行，这也就在一定程度上缩短了达成共识所需要的时间。所以，基于 PoS 算法的以太坊每秒大概能处理 30 笔左右的交易。

但，PoS 算法中持币越多或持币越久，币龄就会越高，持币人就越容易挖到区块并得到激励，而持币少的人基本没有机会，这样整个系统的安全性实际上会被持币数量较大的一部分人掌握，容易出现垄断现象。

DPoS

为了解决 PoS 算法的垄断问题，2014 年比特股（BitShares）的首席开发者丹尼尔 · 拉里默（Dan Larimer）提出了委托权益证明法，也就是 DPoS 算法。

DPoS 算法的原理，类似股份制公司的董事会制度，普通股民虽然拥有股权，但进不了董事会，他们可以投票选举代表（受托人）代他们做决策。DPoS 是由被社区选举的可信帐户（受托人，比如得票数排行前 101 位）来拥有记账权。

为了成为正式受托人，用户要去社区拉票，获得足够多的信任。用户根据自己持有的货币数量占总量的百分比来投票，好比公司股票机制，假设总的发行股票为 1000，现在股东 A 持股 10，那么股东 A 投票权为 10/1000=1/100。如下图所示，根据自己拥有的权益，投票选出可代表自己的受托节点，受托节点之间竞争记账权。

在 DPos 算法中，通常会选出 k(比如 101) 个受托节点，它们的权利是完全相等的。受托节点之间争取记账权也是根据算力进行竞争的。只要受托节点提供的算力不稳定，计算机宕机或者利用手中的权力作恶，随时可以被握着货币的普通节点投票踢出整个系统，而后备的受托节点可以随时顶上去。

DPoS 在比特股和 Steem 上已运行多年，整个网络中选举出的多个节点能够在 1s 之内对 99.9% 的交易进行确认。此外，DPoS 在 EOS（Enterprise Operation System，为商用分布式应用设计的一款区块链操作系统）中也有广泛应用，被称为区块链 3.0 阶段。

DPoS 是在 PoW 和 PoS 的基础上进行改进的，相比于 PoS 算法，DPoS 引入了受托人，优点主要表现在：

由投票选举出的若干信誉度更高的受托人记账，解决了所有节点均参与竞争导致消息量大、达成一致的周期长的问题。也就是说，DPoS 能耗更低，具有更快的交易速度。

每隔一定周期会调整受托人，避免受托人造假和独权。

但是，在 DPoS 中，由于大多数持币人通过受托人参与投票，投票的积极性并不高；且一旦出现故障节点，DPoS 无法及时做出应对，导致安全隐患。

三种分布式共识算法对比分析

好了，现在我们已经理解了 PoW、PoS 和 DPoS 这 3 种分布式共识算法。接下来，为了方便你理解与记忆，我把这三种算法放在一起做下对比，如下图所示。

知识扩展：一致性与共识的区别是什么？

在平常使用中，我们通常会混淆一致性和共识这两个概念，接下来我就为你分析下这两个概念吧。

一致性是指，分布式系统中的多个节点之间，给定一系列的操作，在约定协议的保障下，对外界呈现的数据或状态是一致的。

共识是指，分布式系统中多个节点之间，彼此对某个状态达成一致结果的过程。

也就是说，一致性强调的是结果，共识强调的是达成一致的过程，共识算法是保障系统满足不同程度一致性的核心技术。

总结

今天，我和你介绍了分布式在线记账问题中的 3 种常见共识算法，即：PoW、PoS 和 DPoS。

PoW 算法， 以每个节点或服务器的计算能力，即“算力”，来竞争记账权的机制。类似于按劳分配，谁工作量大，谁拿的多。其实竞争的就是挖矿设备，看谁的挖矿设备的 CPU、GPU 等更厉害，缺点就是费电、污染环境。

PoS 算法，由系统权益代替算力来决定区块记账权，拥有的权益越大，获得记账权的概率就越大。这种方法的优点是节能，不需要挖矿了，但缺点是容易形成垄断。

DPoS 算法，是一种委托权益证明算法。持有币的人可以通过投票选举出一些节点，来作为代表去记账，类似于全国人民代表大会制度。

讲到这里，我还希望你明确，区块链中的共识技术并没那么难和神秘，常用的算法就是 PoW、PoS 和 DPoS。希望通过这篇文章，你能对共识技术有一定的了解，能勇敢、自信地去探索分布式共识技术和区块链技术。

最后，我再用思维导图概括一下今天的内容。

思考题

你能描述出拜占庭将军问题是什么吗？你认为可以如何解决拜占庭将军的容错问题呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

06 | 分布式事务：All or nothing

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

对于网上购物的每一笔订单来说，电商平台一般都会有两个核心步骤：一是订单业务采取下订单操作，二是库存业务采取减库存操作。

通常，这两个业务会运行在不同的机器上，甚至是运行在不同区域的机器上。针对同一笔订单，当且仅当订单操作和减库存操作一致时，才能保证交易的正确性。也就是说一笔订单，只有这两个操作都完成，才能算做处理成功，否则处理失败，充分体现了“All or nothing”的思想。

在分布式领域中，这个问题就是分布式事务问题。那么今天，我们就一起打卡分布式事务吧。

什么是分布式事务？

要想理解分布式事务，我们首先来看一下什么是事务。

事务，其实是包含一系列操作的、一个有边界的工作序列，有明确的开始和结束标志，且要么被完全执行，要么完全失败，即 all or nothing。通常情况下，我们所说的事务指的都是本地事务，也就是在单机上的事务。

而分布式事务，就是在分布式系统中运行的事务，由多个本地事务组合而成。在分布式场景下，对事务的处理操作可能来自不同的机器，甚至是来自不同的操作系统。文章开头提到的电商处理订单问题，就是典型的分布式事务。

要深入理解分布式事务，我们首先需要了解它的特征。分布式事务是多个事务的组合，那么事务的特征 ACID，也是分布式事务的基本特征，其中 ACID 具体含义如下：

原子性（Atomicity），即事务最终的状态只有两种，全部执行成功和全部不执行。若处理事务的任何一项操作不成功，就会导致整个事务失败。一旦操作失败，所有操作都会被取消（即回滚），使得事务仿佛没有被执行过一样。

一致性（Consistency），是指事务操作前和操作后，数据的完整性保持一致或满足完整性约束。比如，用户 A 和用户 B 在银行分别有 800 元和 600 元，总共 1400 元，用户 A 给用户 B 转账 200 元，分为两个步骤，从 A 的账户扣除 200 元和对 B 的账户增加 200 元 ; 一致性就是要求上述步骤操作后，最后的结果是用户 A 还有 600 元，用户 B 有 800 元，总共 1400 元，而不会出现用户 A 扣除了 200 元，但用户 B 未增加的情况 (该情况，用户 A 和 B 均为 600 元，总共 1200 元)。

隔离性（Isolation），是指当系统内有多个事务并发执行时，多个事务不会相互干扰，即一个事务内部的操作及使用的数据，对其他并发事务是隔离的。

持久性（Durability），也被称为永久性，是指一个事务完成了，那么它对数据库所做的更新就被永久保存下来了。即使发生系统崩溃或宕机等故障，只要数据库能够重新被访问，那么一定能够将其恢复到事务完成时的状态。

分布式事务基本能够满足 ACID，其中的 C 是强一致性，也就是所有操作均执行成功，才提交最终结果，以保证数据一致性或完整性。但随着分布式系统规模不断扩大，复杂度急剧上升，达成强一致性所需时间周期较长，限定了复杂业务的处理。为了适应复杂业务，出现了 BASE 理论，该理论的一个关键点就是采用最终一致性代替强一致性。我会在“知识扩展”模块与你详细展开 BASE 理论这部分内容。

介绍完什么是分布式事务，以及事务的基本特征后，就进入“怎么做”的阶段啦。所以接下来，我们就看看如何实现分布式事务吧。

如何实现分布式事务？

实际上，分布式事务主要是解决在分布式环境下，组合事务的一致性问题。实现分布式事务有以下 3 种基本方法：

基于 XA 协议的二阶段提交协议方法；

三阶段提交协议方法；

基于消息的最终一致性方法。

其中，基于 XA 协议的二阶段提交协议方法和三阶段提交协议方法，采用了强一致性，遵从 ACID，基于消息的最终一致性方法，采用了最终一致性，遵从 BASE 理论。

基于 XA 协议的二阶段提交方法

XA 是一个分布式事务协议，规定了事务管理器和资源管理器接口。因此，XA 协议可以分为两部分，即事务管理器和本地资源管理器。

XA 实现分布式事务的原理，就类似于我在第 3 篇文章中与你介绍的集中式算法：事务管理器作为协调者，负责各个本地资源的提交和回滚；而资源管理器就是分布式事务的参与者，通常由数据库实现，比如 Oracle、DB2 等商业数据库都实现了 XA 接口。

基于 XA 协议的二阶段提交方法中，二阶段提交协议（The two-phase commit protocol，2PC），用于保证分布式系统中事务提交时的数据一致性，是 XA 在全局事务中用于协调多个资源的机制。

那么，两阶段提交协议如何保证分布在不同节点上的分布式事务的一致性呢？为了保证它们的一致性，我们需要引入一个协调者来管理所有的节点，并确保这些节点正确提交操作结果，若提交失败则放弃事务。接下来，我们看看两阶段提交协议的具体过程。

两阶段提交协议的执行过程，分为投票（voting）和提交（commit）两个阶段。

投票为第一阶段，协调者（Coordinator，即事务管理器）会向事务的参与者（Cohort，即本地资源管理器）发起执行操作的 CanCommit 请求，并等待参与者的响应。参与者接收到请求后，会执行请求中的事务操作，记录日志信息但不提交，待参与者执行成功，则向协调者发送“Yes”消息，表示同意操作；若不成功，则发送“No”消息，表示终止操作。

当所有的参与者都返回了操作结果（Yes 或 No 消息）后，系统进入了提交阶段。在提交阶段，协调者会根据所有参与者返回的信息向参与者发送 DoCommit 或 DoAbort 指令：

若协调者收到的都是“Yes”消息，则向参与者发送“DoCommit”消息，参与者会完成剩余的操作并释放资源，然后向协调者返回“HaveCommitted”消息；

如果协调者收到的消息中包含“No”消息，则向所有参与者发送“DoAbort”消息，此时发送“Yes”的参与者则会根据之前执行操作时的回滚日志对操作进行回滚，然后所有参与者会向协调者发送“HaveCommitted”消息；

协调者接收到“HaveCommitted”消息，就意味着整个事务结束了。

接下来，我以用户 A 要在网上下单购买 100 件 T 恤为例，重点与你介绍下单操作和减库存操作这两个操作，帮助你加深对二阶段提交协议的理解。

第一阶段：订单系统中将与用户 A 有关的订单数据库锁住，准备好增加一条关于用户 A 购买 100 件 T 恤的信息，并将同意消息“Yes”回复给协调者。而库存系统由于 T 恤库存不足，出货失败，因此向协调者回复了一个终止消息“No”。

第二阶段：由于库存系统操作不成功，因此，协调者就会向订单系统和库存系统发送“DoAbort”消息。订单系统接收到“DoAbort”消息后，将系统内的数据退回到没有用户 A 购买 100 件 T 恤的版本，并释放锁住的数据库资源。订单系统和库存系统完成操作后，向协调者发送“HaveCommitted”消息，表示完成了事务的撤销操作。

至此，用户 A 购买 100 件 T 恤这一事务已经结束，用户 A 购买失败。

由上述流程可以看出，二阶段提交的算法思路可以概括为：协调者下发请求事务操作，参与者将操作结果通知协调者，协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作。

虽然基于 XA 的二阶段提交算法基本满足了事务的 ACID 特性，但依然有些不足。

同步阻塞问题：二阶段提交算法在执行过程中，所有参与节点都是事务阻塞型的。也就是说，当本地资源管理器占有临界资源时，其他资源管理器如果要访问同一临界资源，会处于阻塞状态。

单点故障问题：基于 XA 的二阶段提交算法类似于集中式算法，一旦事务管理器发生故障，整个系统都处于停滞状态。尤其是在提交阶段，一旦事务管理器发生故障，资源管理器会由于等待管理器的消息，而一直锁定事务资源，导致整个系统被阻塞。

数据不一致问题：在提交阶段，当协调者向参与者发送 DoCommit 请求之后，如果发生了局部网络异常，或者在发送提交请求的过程中协调者发生了故障，就会导致只有一部分参与者接收到了提交请求并执行提交操作，但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。

三阶段提交方法

三阶段提交协议（Three-phase commit protocol，3PC），是对二阶段提交（2PC）的改进。为了解决两阶段提交的同步阻塞和数据不一致问题，三阶段提交引入了超时机制和准备阶段。

同时在协调者和参与者中引入超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应，就会根据当前的状态选择提交或者终止整个事务。

在第一阶段和第二阶段中间引入了一个准备阶段，也就是在提交阶段之前，加入了一个预提交阶段。在预提交阶段排除一些不一致的情况，保证在最后提交之前各参与节点的状态是一致的。

也就是说，除了引入超时机制之外，3PC 把 2PC 的提交阶段一分为二，这样三阶段提交协议就有 CanCommit、PreCommit、DoCommit 三个阶段。

第一，CanCommit 阶段。

CanCommit 阶段与 2PC 的投票阶段类似：协调者向参与者发送请求操作（CanCommit 请求），询问参与者是否可以执行事务提交操作，然后等待参与者的响应；参与者收到 CanCommit 请求之后，回复 Yes，表示可以顺利执行事务；否则回复 No。

CanCommit 阶段不同节点之间的事务请求成功和失败的流程，如下所示。

第二，PreCommit 阶段。

协调者根据参与者的回复情况，来决定是否可以进行 PreCommit 操作。

如果所有参与者回复的都是“Yes”，那么协调者就会执行事务的预执行：

发送预提交请求。协调者向参与者发送 PreCommit 请求，进入预提交阶段。

事务预提交。参与者接收到 PreCommit 请求后执行事务操作，并将 Undo 和 Redo 信息记录到事务日志中。

响应反馈。如果参与者成功执行了事务操作，则返回 ACK 响应，同时开始等待最终指令。

假如任何一个参与者向协调者发送了“No”消息，或者等待超时之后，协调者都没有收到参与者的响应，就执行中断事务的操作：

发送中断请求。协调者向所有参与者发送“Abort”消息。

终断事务。参与者收到“Abort”消息之后，或超时后仍未收到协调者的消息，执行事务的终断操作。

预执行阶段，不同节点上事务执行成功和失败的流程，如下所示。

第三，DoCommit 阶段。

DoCmmit 阶段进行真正的事务提交，根据 PreCommit 阶段协调者发送的消息，进入执行提交阶段或事务中断阶段。

执行提交阶段：

发送提交请求。协调者接收到所有参与者发送的 Ack 响应，从预提交状态进入到提交状态，并向所有参与者发送 DoCommit 消息。

事务提交。参与者接收到 DoCommit 消息之后，正式提交事务。完成事务提交之后，释放所有锁住的资源。

响应反馈。参与者提交完事务之后，向协调者发送 Ack 响应。

完成事务。协调者接收到所有参与者的 Ack 响应之后，完成事务。

事务中断阶段：

发送中断请求。协调者向所有参与者发送 Abort 请求。

事务回滚。参与者接收到 Abort 消息之后，利用其在 PreCommit 阶段记录的 Undo 信息执行事务的回滚操作，并释放所有锁住的资源。

反馈结果。参与者完成事务回滚之后，向协调者发送 Ack 消息。

终断事务。协调者接收到参与者反馈的 Ack 消息之后，执行事务的终断，并结束事务。

执行阶段不同节点上事务执行成功和失败 (事务终断) 的流程，如下所示。

在 DoCommit 阶段，当参与者向协调者发送 Ack 消息后，如果长时间没有得到协调者的响应，在默认情况下，参与者会自动将超时的事务进行提交，不会像两阶段提交那样被阻塞住。

基于分布式消息的最终一致性方案

2PC 和 3PC 这两种方法，有两个共同的缺点，一是都需要锁定资源，降低系统性能；二是，没有解决数据不一致的问题。因此，便有了通过分布式消息来确保事务最终一致性的方案。

在 eBay 的分布式系统架构中，架构师解决一致性问题的核心思想就是：将需要分布式处理的事务通过消息或者日志的方式异步执行，消息或日志可以存到本地文件、数据库或消息队列中，再通过业务规则进行失败重试。这个案例，就是使用基于分布式消息的最终一致性方案解决了分布式事务的问题。

基于分布式消息的最终一致性方案的事务处理，引入了一个消息中间件（Message Queue，MQ），用于在多个应用之间进行消息传递。基于消息中间件协商多个节点分布式事务执行操作的示意图，如下所示。

仍然以网上购物为例。假设用户 A 在某电商平台下了一个订单，需要支付 50 元，发现自己的账户余额共 150 元，就使用余额支付，支付成功之后，订单状态修改为支付成功，然后通知仓库发货。

在该事件中，涉及到了订单系统、支付系统、仓库系统，这三个系统是相互独立的应用，通过远程服务进行调用。

根据基于分布式消息的最终一致性方案，用户 A 通过终端手机首先在订单系统上操作，然后整个购物的流程如下所示。

订单系统把订单消息发给消息中间件，消息状态标记为“待确认”。

消息中间件收到消息后，进行消息持久化操作，即在消息存储系统中新增一条状态为“待发送”的消息。

消息中间件返回消息持久化结果（成功 / 失败），订单系统根据返回结果判断如何进行业务操作。失败，放弃订单，结束（必要时向上层返回失败结果）；成功，则创建订单。

订单操作完成后，把操作结果（成功 / 失败）发送给消息中间件。

消息中间件收到业务操作结果后，根据结果进行处理：失败，删除消息存储中的消息，结束；成功，则更新消息存储中的消息状态为“待发送（可发送）”，并执行消息投递。

如果消息状态为“可发送”，则 MQ 会将消息发送给支付系统，表示已经创建好订单，需要对订单进行支付。支付系统也按照上述方式进行订单支付操作。

订单系统支付完成后，会将支付消息返回给消息中间件，中间件将消息传送给订单系统。订单系统再调用库存系统，进行出货操作。

可以看出，分布式事务中，当且仅当所有的事务均成功时整个流程才成功。所以，分布式事务的一致性是实现分布式事务的关键问题，目前来看还没有一种很简单、完美的方案可以应对所有场景。

三种实现方式对比

现在，为了方便你理解并记忆这三种方法，我总结了一张表格，从算法一致性、执行方式、性能等角度进行了对比：

知识扩展：刚性事务与柔性事务

在讨论事务的时候，我们经常会提到刚性事务与柔性事务，但却很难区分这两种事务。所以，今天的知识扩展内容，我就来和你说说什么是刚性事务、柔性事务，以及两者之间有何区别？

刚性事务，遵循 ACID 原则，具有强一致性。比如，数据库事务。

柔性事务，其实就是根据不同的业务场景使用不同的方法实现最终一致性，也就是说我们可以根据业务的特性做部分取舍，容忍一定时间内的数据不一致。

总结来讲，与刚性事务不同，柔性事务允许一定时间内，不同节点的数据不一致，但要求最终一致。而柔性事务的最终一致性，遵循的是 BASE 理论。

那，什么是 BASE 理论呢？

BASE 理论包括基本可用（Basically Available）、柔性状态（Soft State）和最终一致性（Eventual Consistency）。

基本可用：分布式系统出现故障的时候，允许损失一部分功能的可用性。比如，某些电商 618 大促的时候，会对一些非核心链路的功能进行降级处理。

柔性状态：在柔性事务中，允许系统存在中间状态，且这个中间状态不会影响系统整体可用性。比如，数据库读写分离，写库同步到读库（主库同步到从库）会有一个延时，其实就是一种柔性状态。

最终一致性：事务在操作过程中可能会由于同步延迟等问题导致不一致，但最终状态下，数据都是一致的。

可见，BASE 理论为了支持大型分布式系统，通过牺牲强一致性，保证最终一致性，来获得高可用性，是对 ACID 原则的弱化。具体到今天的三种分布式事务实现方式，二阶段提交、三阶段提交方法，遵循的是 ACID 原则，而消息最终一致性方案遵循的就是 BASE 理论。

总结

我从事务的 ACID 特性出发，介绍了分布式事务的概念、特征，以及如何实现分布式事务。在关于如何实现分布式的部分，我以网购为例，与你介绍了常见的三种实现方式，即基于 XA 协议的二阶段提交方法，三阶段方法以及基于分布式消息的最终一致性方法。

二阶段和三阶段方法是维护强一致性的算法，它们针对刚性事务，实现的是事务的 ACID 特性。而基于分布式消息的最终一致性方案更适用于大规模分布式系统，它维护的是事务的最终一致性，遵循的是 BASE 理论，因此适用于柔性事务。

在分布式系统的设计与实现中，分布式事务是不可或缺的一部分。可以说，没有实现分布式事务的分布式系统，不是一个完整的分布式系统。分布式事务的实现过程看似复杂，但将方法分解剖析后，你就会发现分布式事务的实现是有章可循的。

我将实现分布式事务常用的三个算法整理为了一张思维导图，以帮助你加深理解与记忆。

思考题

你觉得分布式互斥与分布式事务之间的关系是什么呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

07 | 分布式锁：关键重地，非请勿入

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

我在第 3 篇文章中，与你一起学习了分布式互斥，领悟了其“有你没我，有我没你”的精髓，为你解释了同一临界资源同一时刻只能被一个程序访问的问题，并介绍了解决分布式互斥的算法。

不知道你有没有发现一个细节，在之前介绍的算法中，我主要讲了如何协调多个进程获取权限和根据权限有序访问共享资源，“获得访问权限的进程可以访问共享资源，其他进程必须等待拥有该权限的进程释放权限”。但是，我并没有介绍在访问共享资源时，这个权限是如何设置或产生的，以及设置或产生这个权限的工作原理是什么。

那么，在本讲，我就将带你一起打卡分布式锁，去学习分布式锁是如何解决这个问题的。

为什么要使用分布锁？

首先，我先带你认识一下什么是锁。

在单机多线程环境中，我们经常遇到多个线程访问同一个共享资源（这里需要注意的是：在很多地方，这种资源会称为临界资源，但在今天这篇文章中，我们统一称之为共享资源）的情况。为了维护数据的一致性，我们需要某种机制来保证只有满足某个条件的线程才能访问资源，不满足条件的线程只能等待，在下一轮竞争中重新满足条件时才能访问资源。

这个机制指的是，为了实现分布式互斥，在某个地方做个标记，这个标记每个线程都能看到，到标记不存在时可以设置该标记，当标记被设置后，其他线程只能等待拥有该标记的线程执行完成，并释放该标记后，才能去设置该标记和访问共享资源。这里的标记，就是我们常说的锁。

也就是说，锁是实现多线程同时访问同一共享资源，保证同一时刻只有一个线程可访问共享资源所做的一种标记。

与普通锁不同的是，分布式锁是指分布式环境下，系统部署在多个机器中，实现多进程分布式互斥的一种锁。为了保证多个进程能看到锁，锁被存在公共存储（比如 Redis、Memcache、数据库等三方存储中），以实现多个进程并发访问同一个临界资源，同一时刻只有一个进程可访问共享资源，确保数据的一致性。

那什么场景下需要使用分布式锁呢？

比如，现在某电商要售卖某大牌吹风机（以下简称“吹风机”），库存只有 2 个，但有 5 个来自不同地区的用户{A,B,C,D,E}几乎同时下单，那么这 2 个吹风机到底会花落谁家呢？

你可能会想，这还不简单，谁先提交订单请求，谁就购买成功呗。但实际业务中，为了高并发地接受大量用户订单请求，很少有电商网站真正实施这么简单的措施。

此外，对于订单的优先级，不同电商往往采取不同的策略，比如有些电商根据下单时间判断谁可以购买成功，而有些电商则是根据付款时间来判断。但，无论采用什么样的规则去判断谁能购买成功，都必须要保证吹风机售出时，数据库中更新的库存是正确的。为了便于理解，我在下面的讲述中，以下单时间作为购买成功的判断依据。

我们能想到的最简单方案就是，给吹风机的库存数加一个锁。当有一个用户提交订单后，后台服务器给库存数加一个锁，根据该用户的订单修改库存。而其他用户必须等到锁释放以后，才能重新获取库存数，继续购买。

在这里，吹风机的库存就是共享资源，不同的购买者对应着多个进程，后台服务器对共享资源加的锁就是告诉其他进程“关键重地，非请勿入”。

但问题就这样解决了吗？当然没这么简单。

想象一下，用户 A 想买 1 个吹风机，用户 B 想买 2 个吹风机。在理想状态下，用户 A 网速好先买走了 1 个，库存还剩下 1 个，此时应该提示用户 B 库存不足，用户 B 购买失败。但实际情况是，用户 A 和用户 B 同时获取到商品库存还剩 2 个，用户 A 买走 1 个，在用户 A 更新库存之前，用户 B 又买走了 2 个，此时用户 B 更新库存，商品还剩 0 个。这时，电商就头大了，总共 2 个吹风机，却卖出去了 3 个。

不难看出，如果只使用单机锁将会出现不可预知的后果。因此，在高并发场景下，为了保证临界资源同一时间只能被一个进程使用，从而确保数据的一致性，我们就需要引入分布式锁了。

此外，在大规模分布式系统中，单个机器的线程锁无法管控多个机器对同一资源的访问，这时使用分布式锁，就可以把整个集群当作一个应用一样去处理，实用性和扩展性更好。

分布式锁的三种实现方法及对比

接下来，我带你看看实现分布式锁的 3 种主流方法，即：

基于数据库实现分布式锁，这里的数据库指的是关系型数据库；

基于缓存实现分布式锁；

基于 ZooKeeper 实现分布式锁。

基于数据库实现分布式锁

要实现分布式锁，最简单的方式就是创建一张锁表，然后通过操作该表中的数据来实现。

当我们要锁住某个资源时，就在该表中增加一条记录，想要释放锁的时候就删除这条记录。数据库对共享资源做了唯一性约束，如果有多个请求被同时提交到数据库的话，数据库会保证只有一个操作可以成功，操作成功的那个线程就获得了访问共享资源的锁，可以进行操作。

基于数据库实现的分布式锁，是最容易理解的。但是，因为数据库需要落到硬盘上，频繁读取数据库会导致 IO 开销大，因此这种分布式锁适用于并发量低，对性能要求低的场景。对于双 11、双 12 等需求量激增的场景，数据库锁是无法满足其性能要求的。而在平日的购物中，我们可以在局部场景中使用数据库锁实现对资源的互斥访问。

下面，我们还是以电商售卖吹风机的场景为例。吹风机库存是 2 个，有 5 个来自不同地区的用户{A,B,C,D,E}想要购买，其中用户 A 想买 1 个，用户 B 想买 2 个，用户 C 想买 1 个。

用户 A 和用户 B 几乎同时下单，但用户 A 的下单请求最先到达服务器。因此，该商家的产品数据库中增加了一条关于用户 A 的记录，用户 A 获得了锁，他的订单请求被处理，服务器修改吹风机库存数，减去 1 后还剩下 1 个。

当用户 A 的订单请求处理完成后，有关用户 A 的记录被删除，服务器开始处理用户 B 的订单请求。这时，库存只有 1 个了，无法满足用户 B 的订单需求，因此用户 B 购买失败。

从数据库中，删除用户 B 的记录，服务器开始处理用户 C 的订单请求，库存中 1 个吹风机满足用户 C 的订单需求。所以，数据库中增加了一条关于用户 C 的记录，用户 C 获得了锁，他的订单请求被处理，服务器修改吹风机数量，减去 1 后还剩下 0 个。

可以看出，基于数据库实现分布式锁比较简单，绝招在于创建一张锁表，为申请者在锁表里建立一条记录，记录建立成功则获得锁，消除记录则释放锁。该方法依赖于数据库，主要有两个缺点：

单点故障问题。一旦数据库不可用，会导致整个系统崩溃。

死锁问题。数据库锁没有失效时间，未获得锁的进程只能一直等待已获得锁的进程主动释放锁。一旦已获得锁的进程挂掉或者解锁操作失败，会导致锁记录一直存在数据库中，其他进程无法获得锁。

基于缓存实现分布式锁

数据库的性能限制了业务的并发量，那么对于双 11、双 12 等需求量激增的场景是否有解决方法呢？

基于缓存实现分布式锁的方式，非常适合解决这种场景下的问题。所谓基于缓存，也就是说把数据存放在计算机内存中，不需要写入磁盘，减少了 IO 读写。接下来，我以 Redis 为例与你展开这部分内容。

Redis 通常可以使用 setnx(key, value) 函数来实现分布式锁。key 和 value 就是基于缓存的分布式锁的两个属性，其中 key 表示锁 id，value = currentTime + timeOut，表示当前时间 + 超时时间。也就是说，某个进程获得 key 这把锁后，如果在 value 的时间内未释放锁，系统就会主动释放锁。

setnx 函数的返回值有 0 和 1：

返回 1，说明该服务器获得锁，setnx 将 key 对应的 value 设置为当前时间 + 锁的有效时间。

返回 0，说明其他服务器已经获得了锁，进程不能进入临界区。该服务器可以不断尝试 setnx 操作，以获得锁。

我还是以电商售卖吹风机的场景为例，和你说明基于缓存实现的分布式锁，假设现在库存数量是足够的。

用户 A 的请求因为网速快，最先到达 Server2，setnx 操作返回 1，并获取到购买吹风机的锁；用户 B 和用户 C 的请求，几乎同时到达了 Server1 和 Server3，但因为这时 Server2 获取到了吹风机数据的锁，所以只能加入等待队列。

Server2 获取到锁后，负责管理吹风机的服务器执行业务逻辑，只用了 1s 就完成了订单。订单请求完成后，删除锁的 key，从而释放锁。此时，排在第二顺位的 Server1 获得了锁，可以访问吹风机的数据资源。但不巧的是，Server1 在完成订单后发生了故障，无法主动释放锁。

于是，排在第三顺位的 Server3 只能等设定的有效时间（比如 30 分钟）到期，锁自动释放后，才能访问吹风机的数据资源，也就是说用户 C 只能到 00:30:01 以后才能继续抢购。

总结来说，Redis 通过队列来维持进程访问共享资源的先后顺序。Redis 锁主要基于 setnx 函数实现分布式锁，当进程通过 setnx<key,value> 函数返回 1 时，表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁，或者等到时间超时才能获得锁。

相对于基于数据库实现分布式锁的方案来说，基于缓存实现的分布式锁的优势表现在以下几个方面：

性能更好。数据被存放在内存，而不是磁盘，避免了频繁的 IO 操作。

很多缓存可以跨集群部署，避免了单点故障问题。

很多缓存服务都提供了可以用来实现分布式锁的方法，比如 Redis 的 setnx 方法等。

可以直接设置超时时间来控制锁的释放，因为这些缓存服务器一般支持自动删除过期数据。

这个方案的不足是，通过超时时间来控制锁的失效时间，并不是十分靠谱，因为一个进程执行时间可能比较长，或受系统进程做内存回收等影响，导致时间超时，从而不正确地释放了锁。

为了解决基于缓存实现的分布式锁的这些问题，我们再来看看基于 ZooKeeper 实现的分布式锁吧。

基于 ZooKeeper 实现分布式锁

ZooKeeper 基于树形数据存储结构实现分布式锁，来解决多个进程同时访问同一临界资源时，数据的一致性问题。ZooKeeper 的树形数据存储结构主要由 4 种节点构成：

持久节点。这是默认的节点类型，一直存在于 ZooKeeper 中。

持久顺序节点。也就是说，在创建节点时，ZooKeeper 根据节点创建的时间顺序对节点进行编号。

临时节点。与持久节点不同，当客户端与 ZooKeeper 断开连接后，该进程创建的临时节点就会被删除。

临时顺序节点，就是按时间顺序编号的临时节点。

根据它们的特征，ZooKeeper 基于临时顺序节点实现了分布锁。

还是以电商售卖吹风机的场景为例。假设用户 A、B、C 同时在 11 月 11 日的零点整提交了购买吹风机的请求，ZooKeeper 会采用如下方法来实现分布式锁：

在与该方法对应的持久节点 shared_lock 的目录下，为每个进程创建一个临时顺序节点。如下图所示，吹风机就是一个拥有 shared_lock 的目录，当有人买吹风机时，会为他创建一个临时顺序节点。

每个进程获取 shared_lock 目录下的所有临时节点列表，注册子节点变更的 Watcher，并监听节点。

每个节点确定自己的编号是否是 shared_lock 下所有子节点中最小的，若最小，则获得锁。例如，用户 A 的订单最先到服务器，因此创建了编号为 1 的临时顺序节点 LockNode1。该节点的编号是持久节点目录下最小的，因此获取到分布式锁，可以访问临界资源，从而可以购买吹风机。

若本进程对应的临时节点编号不是最小的，则分为两种情况：

a. 本进程为读请求，如果比自己序号小的节点中有写请求，则等待；
b. 本进程为写请求，如果比自己序号小的节点中有读请求，则等待。

例如，用户 B 也想要买吹风机，但在他之前，用户 C 想看看吹风机的库存量。因此，用户 B 只能等用户 A 买完吹风机、用户 C 查询完库存量后，才能购买吹风机。

可以看到，使用 ZooKeeper 可以完美解决设计分布式锁时遇到的各种问题，比如单点故障、不可重入、死锁等问题。虽然 ZooKeeper 实现的分布式锁，几乎能涵盖所有分布式锁的特性，且易于实现，但需要频繁地添加和删除节点，所以性能不如基于缓存实现的分布式锁。

三种实现方式对比

我通过一张表格来对比一下这三种方式的特点，以方便你理解、记忆。

总结来说，ZooKeeper 分布式锁的可靠性最高，有封装好的框架，很容易实现分布式锁的功能，并且几乎解决了数据库锁和缓存式锁的不足，因此是实现分布式锁的首选方法。

从上述分析可看出，为了确保分布式锁的可用性，我们在设计时应考虑到以下几点：

互斥性，即在分布式系统环境下，分布式锁应该能保证一个资源或一个方法在同一时间只能被一个机器的一个线程或进程操作。

具备锁失效机制，防止死锁。即使有一个进程在持有锁的期间因为崩溃而没有主动解锁，也能保证后续其他进程可以获得锁。

可重入性，即进程未释放锁时，可以多次访问临界资源。

有高可用的获取锁和释放锁的功能，且性能要好。

知识扩展：如何解决分布式锁的羊群效应问题？

在分布式锁问题中，会经常遇到羊群效应。所谓羊群效应，就是在整个分布式锁的竞争过程中，大量的“Watcher 通知”和“子节点列表的获取”操作重复运行，并且大多数节点的运行结果都是判断出自己当前并不是编号最小的节点，继续等待下一次通知，而不是执行业务逻辑。

这，就会对 ZooKeeper 服务器造成巨大的性能影响和网络冲击。更甚的是，如果同一时间多个节点对应的客户端完成事务或事务中断引起节点消失，ZooKeeper 服务器就会在短时间内向其他客户端发送大量的事件通知。

那如何解决这个问题呢？具体方法可以分为以下三步。

在与该方法对应的持久节点的目录下，为每个进程创建一个临时顺序节点。

每个进程获取所有临时节点列表，对比自己的编号是否最小，若最小，则获得锁。

若本进程对应的临时节点编号不是最小的，则继续判断：

若本进程为读请求，则向比自己序号小的最后一个写请求节点注册 watch 监听，当监听到该节点释放锁后，则获取锁；

若本进程为写请求，则向比自己序号小的最后一个读请求节点注册 watch 监听，当监听到该节点释放锁后，获取锁。

总结

我以电商购物为例，首先带你剖析了什么是分布式锁，以及为什么需要分布式锁；然后，与你介绍了三种实现分布式锁的方法，包括基于数据库实现、基于缓存实现（以 Redis 为例），以及基于 ZooKeeper 实现。

分布式锁是解决多个进程同时访问临界资源的常用方法，在分布式系统中非常常见，比如开源的 ZooKeeper、Redis 中就有所涉及。通过今天这篇文章对分布式锁原理及方法的讲解，我相信你会发现分布式锁不再那么神秘、难懂，然后以此为基础对分布式锁进行更深入的学习和应用。

接下来，我把今天的内容通过下面的一张思维导图再全面总结下。

思考题

分布式锁与分布式互斥的关系是什么呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

08 | 答疑篇：分布式技术是如何引爆人工智能的？

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

通过前面课程，相信你已经对分布式的起源以及什么是分布式有了一定的了解，从用户留言来看很多同学对分布式技术、分布式技术的应用以及分布式技术的重要性非常感兴趣。所以，在今天这篇答疑文章中，我将以人工智能技术为例，带你了解分布式技术的应用及其重要性。

什么是人工智能？

2016 年 3 月，Google AlphaGo 与围棋世界冠军李世石进行围棋人机大战，以 4 比 1 的总分获胜。至此，人工智能技术被推向了高潮。现在，人工智能已经广泛渗入到了我们的生活中，比如手机拍照美化、人脸识别、平安城市、自然语言处理、语音识别等。

那么，到底什么是人工智能呢？

顾名思义，人工智能就是机器模拟人的思维，像人那样智能呗。目前，对人工智能的定义大多可划分为四类，即机器“像人一样思考”“像人一样行动”“理性地思考”和“理性地行动”。这里的行动，指的是采取行动或制定行动的决策。

那人工智能是如何让机器像人那样智能呢？人并不是天生就会解决问题的，我们经常会听到一句经典的话“见多识广”，人遇到新的问题，是通过学习新知识，然后结合自己的经验去解决问题的。比如，人并不是生来就认识香蕉，而是通过后天的学习（包括学习香蕉的形状、颜色、口味等）来获取识别香蕉的经验，当下次再看到香蕉时，就知道这是香蕉了。

人工智能要模拟人的智能也类似，需要通过大量的数据进行学习和分析获得规律（即建立一个模型），然后利用该规律或模型对未知数据进行预测，以判断是否与建模数据具有相同特征。

从人工智能的定义可以看出，数据、模型（也叫作算法）、算力是人工智能的三大核心。可以说，在一定程度上数据决定了机器学习的上限，而模型为逼近这个上限提供方法，因此数据处理和模型训练是人工智能的关键技术，算力决定了数据处理和模型训练的实用性能，而分布式技术就是解决算力的不二妙招。

接下来，我就对数据处理和模型训练进行具体分析，来帮助你了解人工智能中需要用到哪些分布式技术来解决算力问题。

数据处理

数据处理又称数据预处理，是指通过数据统计、数据集成、数据清理、数据规约、数据变换等方法，对数据缺失、数据噪声、数据冗余、多数据源等问题进行处理以得到高质量数据，为模型训练提供高质量输入，是人工智能不可缺少的环节。

其实，数据处理类似于我们的知识整理过程。一个精心打造的、体系化梳理过的专栏文章，可以帮助我们在学习一门课程时，少走弯路、避免踩雷、达到事半功倍的效果。同样地，一个精心处理过的数据集，对于人工智能的模型训练也能起到事半功倍的效果，一方面可以缩短机器学习的周期，另一方面也可以提高机器学习的质量。

所以接下来，我们就一起看看数据预处理的方法吧。

数据统计（Data Statistics）。数据统计是数据预处理的第一步，其范围、规模、方式等会直接影响数据分析的结果。常见的统计特征有最大值、最小值、均值、中位数、方差、标准差等。

数据集成（Data Integration）。数据的收集有多种途径，比如文件数据、数据库数据、问卷数据等，而不同的数据源，其数据的存储方式、命名规则、单位等不尽相同，所以我们需要数据集成来将多个数据源的数据整合到一起，以保证数据结构、属性的一致性，并去除冗余数据，方便后续分析。

数据清理（Data Cleaning）。由于用户忘记或设备损坏，经常会造成部分数据缺失；由于仪器故障或用户填写错误，经常会出现数据错误（噪声数据）等。如果不对这些数据做任何处理，后面的模型训练过程将产生严重偏差。数据清理过程就是用来解决这个问题的，它可以通过平均值或众数等来填充丢失值或修改这些噪声值。

数据规约（Data Reduction）。由于机器学习中的数据量很大，因此会导致很多重复的特征，或者很多不重要的特征（比如 ID 号等）。数据规约的目的就是去除重复特征及不重要的特征，从而减少数据的维度或者数据量，降低问题复杂度，同时不影响后面训练的结果。数据规约的方法有主成分分析法 (Principal Component Analysis，PCA)、小波变换 (Wavelet Transform，WT) 等。

数据变换（Data Conversion）。数据经过集成、清理与规约等步骤后，要将数据进行标准化、离散化、分层化，使得数据更加一致、更加容易被模型处理。数据变换方法主要有数据标准化、数据离散化和数据泛化三类。

可以看出，数据预处理虽然很复杂，但可以拆分成多个步骤进行。对于小样本数据处理时，单台机器的处理能力就足够了，所以采用单台机器进行处理即可。但是对于大规模数据来说，单台机器的处理能力已成为瓶颈，此时，不得不需要分布式数据处理了。

目前，业界已经有很多大数据处理软件，比如分布式计算框架 MapReduce、Spark，分布式存储框架 HDFS、HBASE 等，来进行分布式数据处理。

备注：我会在专栏的“第三站：分布式计算技术”和“第五站：分布式数据存储”与你详细讲述这些框架。

接下来，我们再一起看看分布式如何助力模型训练。

分布式模型训练

在了解什么是分布式模型训练之前，我们先看一下什么是模型训练。

什么是分布式模型训练？

模型训练就是从已知数据中找到规律。具体来说就是，不断通过已有数据进行验证增强，最终给出最适合的模型参数，以此来预测给定的未知数据。

比如有一堆橘子和西瓜，可以通过模型训练得到：大的、绿色的判定为西瓜，小的、黄色的判定为橘子。那么当给出一个未知数据时，我们通过它的大小及颜色信息就可以判断该水果是橘子还是西瓜。这就是模型训练。其中，大小和颜色属于预测的两个特征，而它们的具体数值 (比如，大于 10 厘米等，颜色 RGB 的数值范围）就是模型参数。

随着大数据时代的到来，人工智能技术逐渐向大规模训练数据、大模型训练等方向发展。比如，百度的 Deep Speech 2 系统使用了 11940 小时的语音数据以及超过 200 万句表述来训练英语的语音识别模型；2011 年谷歌训练出拥有十亿个参数的超大神经网络模型。很明显，单台计算机的存储能力、计算能力已经不能满足了，因此分布式模型训练诞生了。

研究表明，在一台具有一个现代 GPU 的单台机器上完成一次基于 ImageNet 数据集的训练要耗费多达一周的时间。这还仅仅只是一次训练迭代的时间，如果是比较严格的生产级业务，至少需要数十次迭代，训练累计时间将会达到数十周。试想一下，如果一个业务仅仅是模型训练就花费数十周，那么等到真正上线，恐怕最佳时间窗口也已经过去了。

在多台机器上的分布式训练无疑能极大减少训练时间，近期的一项研究通过分布式技术，使用一个包含 2048 个 GPU 的集群将 ImageNet 的训练时间降低到了 4 分钟。TensorFlow 是由 Google 首创且在业内非常流行的开源机器学习框架，它的分布式版本利用了 GPU 加速服务器的虚拟化集群，将深度学习的训练时间从数周缩短到数小时。

总结来讲，分布式训练可以大大提升训练效率，大幅缩短训练时间，从而缩短业务面市周期，所以各大公司都在研究分布式训练，比如华为、IBM、阿里巴巴等。

那，什么是分布式模型训练呢？

分布式模型训练是利用分布式集群，将多个计算机的存储能力、计算能力等进行统一管理和调度，从而实现模型训练。

可以看到，分布式模型训练的前提是有一个分布式集群，因此一个高效、可靠的分布式集群是基础。而这个分布式集群的架构、选主、调度、可靠性等关键技术，便奠定了分布式模型训练的基础。

备注：关于分布集群的架构和调度，我会在本专栏的“第二站：分布式资源管理与负载调度”进行详细讲解；关于集群的选主，我已经在本专栏的“第一站：分布式协调与同步”中进行了讲解；而关于可靠性，我会在在本专栏的“第六站：分布式高可靠”进行讲解。

好了，有了分布式集群作为基础，接下来，我们要考虑的就是如何进行分布式模型训练了。不同的场景，采用的分布式模型训练的方法也不一致，主要包括数据分布式训练、模型分布式训练和混合模型训练三类。

接下来，我将带你了解这三种分布式模型训练模式，并带你了解其中涉及的分布式技术。

数据分布式训练

数据分布式训练主要是针对大规模训练数据的场景。如下图所示，数据分布式训练是在每个节点（假设，一台服务器代表一个节点）上都存储或运行一个完整的模型训练程序的基础上，将大规模数据进行划分，然后将划分后的数据子集分配到多个节点上，每个节点根据自己接收到的数据进行训练。

每个节点会根据自己拥有的数据子集训练出一个子模型，并按照一定的规则与其他节点通信，比如交互子模型参数或参数更新等信息，以保证最终可以有效整合来自各个节点的训练结果以得到全局的机器学习模型。比如，每个节点训练一个子模型得到自己的参数，最终的模型为多个节点的参数取平均值。

可以看出，数据分布式有如下两个重要信息：

数据需拆分存储到不同的节点进行训练，因此涉及了数据的拆分方法、数据的分布式存储和管理，其中数据拆分方法主要有两类，对训练样本进行划分和对每个样本的维度进行划分，这是非常基础的方法。目前，市面上大部分的书籍均有介绍，如果你感兴趣的话可以自行学习。

节点之间需要通信交互信息。分布式通信是实现任何分布式技术的底座，没有分布式通信技术，分布式模型训练犹如纸上谈兵。

备注：数据的分布式存储和管理，是数据分布式的基础，我会在“第五站：分布式数据存储”中与你详细讲述；而关于分布式通信的相关技术，我会在“第四站：分布式通信技术”与你介绍。

模型分布式训练

了解了数据分布式训练，我们再来看一下模型分布式训练。它针对的主要是大模型训练场景，在分布式领域中也被称为任务并行或任务分布式。

如下图所示，模型分布式训练是指将大模型进行拆分，然后将拆分后的子模型分配到不同的节点上进行训练。与数据分布式训练不同的是，首先每个节点上只存储和运行部分模型训练程序，而不是完整的模型训练程序；其次，各个子模型之间存在较强的依赖关系，比如节点 1 的输出是节点 2 和节点 3 子模型的输入，因此节点之间需要进行中间计算结果的通信。

可以看出，模型分布式训练包含如下两个关键信息：

大模型拆分为多个小模型，其本质是将大任务拆分为多个子任务，这其实就是分而治之策略。而子任务之间的拆分，需要运用包括流水线、MapReduce 等在内的多种分布式计算模式。

不同节点上的子任务之间，需要通过通信交互中间计算结果，涉及分布式通信技术。

备注：任务拆分和流水线等分布式计算模式是模型分布式训练不可缺少的技术，我会在“第三站：分布式计算技术”与你详细介绍；而关于分布式通信的相关技术，你可以移步“第四站：分布式通信技术”中进行了解。

混合模型训练

混合模型训练，主要是针对大规模训练数据和大模型训练共存的场景。

所谓混合模型训练，就是将数据分布式训练和模型分布式训练结合起来。如下图所示，假设有一个多 GPU 集群系统，首先对模型进行拆分，将子模型分配到单节点上不同的 GPU，然后对数据进行划分，每个节点负责训练一部分数据，最后进行模型参数同步得到全局参数和全局模型。

从混合模型训练的流程可以看出：

单节点或多节点实现模型并行或模型分布式训练，涉及模型拆分、并行与分布式计算模式等；

多节点之间实现了数据分布式训练，涉及数据的拆分方法和数据的分布式存储和管理等技术；

单节点之间的模型分布式训练，需要单节点上多进程之间通信；多节点之间的分布式训练需要跨节点跨进程通信。

备注：我会在“第三站：分布式计算技术”“第四站：分布式通信技术”和“第五站：分布式数据存储”模块中，与你讲述这其中涉及的分布式技术。

总结

讲完分布式模型训练，本节课就告一段落了。分布式训练可以将深度学习的训练时间从数周缩短到数小时，极大提升了训练效率，这充分说明了分布式技术的重要性。

接下来，我们一起总结下今天的主要内容吧。

首先，我与你介绍了什么是人工智能，让你先对其有了一个整体的理解。

然后，我与你介绍了人工智能中的数据预处理方法，包括数据清理、数据统计、数据集成、数据规约、数据变换等，在这其中分布式数据预处理是处理大规模数据的一个很好的方式。

最后，我与你介绍了分布式模型训练，包括数据分布式训练、模型分布式训练和混合模型训练 3 种方法，并介绍了其中涉及的关键分布式技术，比如数据的分布式存储和管理、分布式通信等。没有这些关键的分布式技术，分布式模型训练其实就是空谈了。

现在，我将人工智能中涉及的关键分布式技术整理为了一张表格，以方便你学习。

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

09 | 分布式体系结构之集中式结构：一人在上，万人在下

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

云这个话题对我们来说已经非常熟悉了。可以说，云在我们的生活中无处不在，比如我们平时看的视频通常就是放在云上的。当我们要播放一段视频时，请求会先转发到云上，从云上下载数据到本地，然后播放。在这里，你肯定会疑惑，云上资源那么丰富吗，可以存放这么多东西吗？

云上的资源确实丰富，因为它可以尽可能地把更多的服务器组织起来，作为一个统一的资源，为多个用户提供服务。这里的重点是，把多个服务器管理起来，作为一个统一的资源提供服务。而如何组织，就是分布式体系结构的范畴了。

你会发现，很多场景下，我们的请求都会汇总到一台服务器上，由这台服务器统一协调我们的请求和其他服务器之间的关系。这种由一台服务器统一管理其他服务器的方式，就是分布式体系结构中的集中式结构（也称为 Master/Slave 架构），其中统一管理其他服务器的服务器是主，其他服务器是从，可以形象地比喻为“一人在上，万人在下”。

接下来，我就带你一起打卡分布式体系结构中的集中式结构吧。

什么是集中式结构？

集中式结构就是，由一台或多台服务器组成中央服务器，系统内的所有数据都存储在中央服务器中，系统内所有的业务也均先由中央服务器处理。多个节点服务器与中央服务器连接，并将自己的信息汇报给中央服务器，由中央服务器统一进行资源和任务调度：中央服务器根据这些信息，将任务下达给节点服务器；节点服务器执行任务，并将结果反馈给中央服务器。

集中式结构最大的特点，就是部署结构简单。这是因为，集中式系统的中央服务器往往是多个具有较强计算能力和存储能力的计算机，为此中央服务器进行统一管理和调度任务时，无需考虑对任务的多节点部署，而节点服务器之间无需通信和协作，只要与中央服务器通信协作即可，具体示意图如下所示：

经典集中式结构

现在，我们理解了什么是集中式结构，为了加深理解，接下来我以 Google Borg、Kubernetes 和 Apache Mesos 三个经典的集群管理系统为例，带你深入学习集中式结构的原理。

Google Borg

Borg 是 Google 内部使用的集群管理系统，采用了典型的集中式结构，负责提交、调度、开始、重启和管理 Google 运行在其上的所有应用。

在 Borg 中，一个集群称为一个 Cell，每个 Cell 里面有一个 Leader，称为 BorgMaster，即为中央服务器；其他服务器为节点服务器或从服务器，被称为 Borglet。

首先，我们一起看看BorgMaster。它由两个进程组成，一个是 Borgmaster 主进程，一个是独立的 scheduler 进程：

主进程处理客户端的 RPC 请求，比如任务的执行状态更新或者查询等；同时，管理系统中所有实体的状态（比如，服务器、任务等），并负责和 Borglet 通信。

scheduler 进程负责任务调度，通过任务对资源的需求以及当前 Borglet 所在服务器的资源情况进行匹配，为任务寻找一个合适的节点服务器执行。我会在第 11 篇文章“分布式调度之单体调度：物质文明、精神文明一手抓”中与你详细讲述具体的调度流程。

接下来，我们一起看看 Borglet。它是运行在每个节点机器的一个 agent，负责任务的拉起、停止、重启等，并管理和搜集本服务器资源，将任务的状态、服务器状态等信息上报给 BorgMaster。而 BorgMaster 会周期性地轮询每个 Borglet，以获取节点服务器的状态和资源信息等。

Borg 的整体架构示意图如下所示：

备注：此图引自Borg 论文。

Borg 的主要用户是 Google 的开发者以及运行 Google 应用和服务的系统管理员（网站可靠性工程师，简称 SRE）。用户以 Job 的形式向 Borg 提交工作，每个 Job 由运行一个或多个运行相同程序的 Task 组成。每个 Job 运行在一个 Borg Cell 中，并将一组机器当做一个单元进行管理。

Borg 可以运行各种各样的任务，这些任务主要分为两类：

第一类是长服务，即长时间运行不停止的服务，并且要求能够处理短暂的、延迟敏感的请求（延迟要求在几微秒到几百毫秒之间）。这些服务主要用于面向终端用户的服务（比如 Gmail、Google Docs、Web 搜索），以及内部的一些基础设施服务（比如 BigTable）。

第二类是批处理任务。通常需要几秒到几天的时间来完成的批处理 Job，这些 Job 对短时间的性能波动并不是非常敏感。

这些负载通常在 Cell 之间混合分布，每个 Cell 随着主要租户以及时间的不同会运行各种不同的应用：批处理类型的 Job 来了又走，而许多面向终端用户的 Job 又期望一个能长时间使用的模式。

对于这些不同的服务，要求 Borg 能很好地处理所有的情况。Borg 主要有三大优点：

开发者只需关注应用，不需要关注底层资源管理。它隐藏了资源管理以及错误处理，因此用户能集中精力开发应用。

高可靠性和可用性，支持多种应用。

支持上千级服务器的管理和运行。

Borg 并不是第一个解决这些问题的系统，但却是少数能在这么大规模处理这些问题的同时，还能实现这样的弹性和完整性的系统之一。

Kubernetes

Kubernetes 是 Google 开源的容器集群管理系统，是 Borg 的一个开源版本。Kubernetes 是用于自动部署、扩展和管理容器化应用程序的开源系统。其核心是，在集群的节点上运行容器化应用，可以进行自动化容器操作，包括部署、调度和在节点间弹性伸缩等。

Kubernetes 也是典型的集中式结构，一个 Kubernetes 集群，主要由 Master 节点和 Worker 节点组成，以及客户端命令行工具 kubectl 和其他附加项。

我们先来看看 Master 节点。它运行在中心服务器上，Master 节点由 API Server、Scheduler、Cluster State Store 和 Control Manger Server 组成，负责对集群进行调度管理。

API Server：是所有 REST 命令的入口，负责处理 REST 的操作，确保它们生效，并执行相关业务逻辑。

Scheduler：根据容器需要的资源以及当前 Worker 节点所在节点服务器的资源信息，自动为容器选择合适的节点服务器。

Cluster State Store：集群状态存储，默认采用 etcd，etcd 是一个分布式 key-value 存储，主要用来做共享配置和服务发现。

Control Manager：用于执行大部分的集群层次的功能，比如执行生命周期功能（命名空间创建和生命周期、事件垃圾收集、已终止垃圾收集、级联删除垃圾收集等）和 API 业务逻辑。

接下来，我们看看 Worker 节点吧。它作为真正的工作节点，运行在从节点服务器，包括 kubelet 和 kube-proxy 核心组件，负责运行业务应用的容器。

kubelet：用于通过命令行与 API Server 进行交互，根据接收到的请求对 Worker 节点进行操作。也就是说，通过与 API Server 进行通信，接收 Master 节点根据调度策略发出的请求或命令，在 Worker 节点上管控容器（Pod），并管控容器的运行状态（比如，重新启动出现故障的 Pod）等。Pod 是 Kubernetes 的最小工作单元，每个 Pod 包含一个或多个容器。

kube-proxy：负责为容器（Pod）创建网络代理 / 负载平衡服务，从 API Server 获取所有 Server 信息，并根据 Server 信息创建代理服务，这种代理服务称之为 Service。Kube-proxy 主要负责管理 Service 的访问入口，即实现集群内的 Pod 客户端访问 Service，或者是集群外访问 Service，具有相同服务的一组 Pod 可抽象为一个 Service。每个 Service 都有一个虚拟 IP 地址（VIP）和端口号供客户端访问。

Kubernetes 架构示意图如下所示：

备注：此图引自https://www.padok.fr/en/blog/kubernetes-architecture-clusters

图中， Kube DNS 负责为整个集群提供 DNS 服务；CNI 是 Container Network Interface 的一个标准的通用接口，用于连接容器管理系统和网络插件。

与 Borg 不同的是，Kubernetes 主要是一个容器编排引擎，不仅支持 Docker，还支持 Rocket(另一种容器技术)。

Kubernetes 也已经被很多公司采用，比如网易云、华为在需要使用容器进行资源隔离以运行相关业务的场景下，采用了大规模 Kubernetes 集群。

在容器管理方面，Kubernetes 有很多优势。

自动化容器的部署和复制。Kubernetes 执行容器编排，因此不必人工编写这些任务的脚本。

将容器组织为组，弹性伸缩。Kubernetes 引入 Pod 机制，Pod 代表着能够作为单一应用程序加以控制的一组容器集合。通过 Pod 机制，Kubernetes 实现了多个容器的协作，能够有效避免将太多功能集中到单一容器镜像这样的错误实践中。此外，软件可以向外扩展跨越多个 Pods 实现初步部署，且相关部署可随时进行规模伸缩。

容器间负载均衡。Services 用于将具备类似功能的多个 Pod 整合为一组，可轻松进行配置以实现其可发现性、可观察性、横向扩展以及负载均衡。

易于版本控制与滚动更新。Kubernetes 采取“滚动方式”实现编排，且可跨越部署范围内的全部 Pod。这些滚动更新可进行编排，并以预定义方式配合当前可能尚不可用的 Pods 数量，以及暂时存在的闲置 Pods 数量。Kubernetes 利用新的应用程序镜像版本对已部署 Pods 进行更新，并在发现当前版本存在不稳定问题时回滚至早期部署版本。

Mesos

理解了 Google Borg 和 Kubernetes 的集中式结构，接下来我们再看看 Apache 旗下的开源分布式资源管理框架 Mesos 吧。它被称为是分布式系统的内核，最初由加州大学伯克利分校的 AMPLab 开发，后在 Twitter 得到广泛使用。

Mesos 的开发受到了 Borg 系统的启发，也是采用的典型的集中式架构。Mesos 与 Borg 不同之处在于，Borg 的 Master 直接对接用户应用，也就是说用户可以向 Borg 的 Master 直接请求任务。但 Mesos 不可以，Mesos 只负责底层资源的管理和分配，并不涉及存储、 任务调度等功能，因此 Mesos Master 对接的是 Spark、Hadoop、Marathon 等框架，用户的任务需要提交到这些框架上。也正因为此，Mesos 的任务调度框架是双层结构。

在 Mesos 中，一个集群包括 Mesos Master 和多个 Mesos Agent。其中，Mesos Master 运行在中央服务器，Mesos Agent 运行在节点服务器上。

Mesos Master 负责收集和管理所有 Agent 所在服务器的资源和状态，并且对接 Spark、Hadoop 等框架，将集群中服务器的资源信息告知给这些框架，以便这些框架进行任务资源匹配和调度。Mesos Agent 负责任务的拉起、停止、重启等，并负责收集所在服务器的资源 (比如 CPU、内存等) 信息和状态，上报给 Mesos Master。

Mesos Master 通常采用一主两备的方式，以方便故障处理和恢复。而 Mesos Master 的选主策略，采用的就是我们在第 4 篇文章“分布式选举：国不可一日无君”中介绍的 ZAB 算法。

Mesos 架构示意图如下所示：

备注：此图引自《Mesos 架构 · Mesos 中文手册》

如上所述，Mesos 对接的是框架，并且可以同时对接多个框架，目前已经被很多公司使用。比如，国外的 Twitter、Apple、Airbnb、Uber 等，国内的爱奇艺、去哪儿、携程、当当等。

这些公司选择 Mesos，主要是因为它具有如下优势：

效率。Mesos 对物理资源进行了逻辑抽象，在应用层而不是物理层分配资源，通过容器而不是虚拟机（VM）分配任务。因为应用程序的调度器知道如何最有效地利用资源，所以在应用层分配资源能够为每个应用程序的特殊需求做考量 ; 而通过容器分配任务则能更好地进行“装箱”。

可扩展性。Mesos 可扩展设计的关键是两级调度架构，其中 Framework 进行任务调度，Mesos Master 进行资源分配。由于 Master 不必知道每种类型的应用程序背后复杂的调度逻辑，不必为每个任务做调度，因此可以用非常轻量级的代码实现，更易于扩展集群规模。

模块化。每接入一种新的框架，Master 无需增加新的代码，并且 Agent 模块可以复用，为此开发者可以专注于应用和框架的选择。这，就使得 Mesos 可以支持多种框架，适应不同的应用场景。

随着分布式应用程序和微服务的流行，越来越多的用户正在寻找一种技术，来帮助他们管理这些复杂的应用程序。而 Mesos 为数据中心带来的这些好处，就使得越来越多的人关注 Mesos 及其相关项目。

分析对比

Borg、Kubernetes 和 Mesos 采用的都是集中式结构，要理解它们的实现原理，就要清楚其架构。所以，虽然这部分内容理解起来有难度，但希望你可以深入进去探其本质，这样在实际操作中，就可以从用途出发选择合适的集群管理架构。

接下来，我将这 3 种集群管理系统的特点梳理为了一张表格，以方便你理解与记忆。

知识扩展：Mesos 是如何支持容器部署的？

目前，容器技术十分热门，解决了服务打包发布、资源隔离的问题。我们知道，Kubernetes 的设计主要针对的就是容器，那么 Mesos 又是如何支持容器部署呢？

Mesos 本身只负责资源管理，不负责任务调度。但 Mesos 可以对接不同的框架，Mesos+Marathon 可以支持容器调度和部署。Marathon 支持容器的调度，将容器部署请求发给 Mesos Master，Mesos Master 再将请求转发给 Mesos Agent，Mesos Agent 的执行器会将容器拉起。

目前，Mesos+Marathon 支持的容器，主要包括 Docker 和 cgroups。

总结

今天，我主要与你分享了分布式系统中的集中式架构，并以 Borg、Kubernetes、Mesos 这三款知名的集群管理系统为例，与你描述了集中式架构的设计目的、框架结构，以及各组件模块的功能等。

Borg 是 Google 公司内部使用的集群管理系统，既可以执行长服务，也可以执行批处理任务，是一个具有强大功能的、复杂的集群管理系统。

Kubernetes 是 Borg 的简化开源版，是一个正在兴起的集群管理系统。Mesos 和 Kubernetes 都是为帮助应用程序在群集环境中运行而创建的，Kubernetes 更加专注于运行容器群集，具有更多功能。

Mesos 是非常典型的开源集群管理系统。在 Mesos 之上，可以搭载诸如 Spark、Hadoop 等框架，甚至可以在 Mesos 上集成 Kubernetes，扩展性强。

可以发现，这三种集群管理系统虽然具有不同的功能组件，但整体框架采用的都是集中式架构。因此，你只要理解了一个集群管理系统的架构，再去理解其他集中式的集群管理架构就会很容易了。

Kubernetes 由于其成熟的社区、丰富的文档，所以如果你是一个新手的话，Kubernetes 就是一个很棒的开始。加油，赶紧开启你的集群管理之旅吧。

好了，到最后，我再以一个思维导图为你总结一下本讲的内容，以方便你理解记忆。

思考题

在集中式架构中，Master 如何判断 Slave 是否存活呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

10 | 分布式体系结构之非集中式结构：众生平等

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章中，我带你了解了分布式体系结构中的集中式结构。虽然很多云上的管理都采用了集中式结构，但是这种结构对中心服务器性能要求很高，而且存在单点瓶颈和单点故障问题。

为了解决这个问题，分布式领域中又出现了另一经典的系统结构，即非集中式结构，也叫作分布式结构。那什么是非集中式结构呢，它的原理是什么样的，又有哪些集群采用了这种结构呢？

今天，我们就一起打卡非集中式结构，揭开它的神秘面纱吧。

什么是非集中式结构？

在非集中式结构中，服务的执行和数据的存储被分散到不同的服务器集群，服务器集群间通过消息传递进行通信和协调。

也就是说，在非集中式结构中，没有中央服务器和节点服务器之分，所有的服务器地位都是平等（对等）的，也就是我们常说的“众生平等”。这样一来，相比于集中式结构，非集中式结构就降低了某一个或者某一簇计算机集群的压力，在解决了单点瓶颈和单点故障问题的同时，还提升了系统的并发度，比较适合大规模集群的管理。

所以近几年来，Google、 Amazon、Facebook、阿里巴巴、腾讯等互联网公司在一些业务中也相继采用了非集中式结构。

接下来，我将为你介绍 3 种典型的非集中式架构系统，包括 Akka 集群、Redis 集群和 Cassandra 集群，来帮助你深入理解非集中式架构。

Akka 集群

在介绍 Akka 集群的结构之前，我带你了解一下什么是 Akka 框架吧。

Akka 框架基于 Actor 模型，提供了一个用于构建可扩展的、弹性的、快速响应的应用程序的平台。其中，Actor 是一个封装了状态和行为的对象，它接收消息并基于该消息执行计算。Actor 之间互相隔离，不共享内存，但 Actor 之间可通过交换消息（mail）进行通信（每个 Actor 都有自己的 MailBox）。

比如，在分布式系统中，一个服务器或一个节点可以视为一个 Actor，Actor 与 Actor 之间采用 mail 进行通信，如下图所示：

可以看到，Actor 发送的 Mail 消息会存储在接收方的 MailBox 中。默认情况下，接收方按照 mail 到达的先后顺序，从 MailBox 中提取 mail 消息，并进行相应的计算处理。

备注：关于 Actor 模型更详细的内容，我会在第 17 篇文章中会与你讲述。

显然，Actor 模型为系统并发度提供了非常好的解决方案，且是一个异步的、非阻塞的、高性能的事件驱动编程模型。Akka 集群充分利用了 Actor 模型的优势，提供了一个非集中式架构的集群管理模块，用来构建可扩展的、弹性的分布式应用程序。

Akka 集群负责 Actor 模型底层的节点管理，包括故障检测、节点加入 / 退出集群等。也就是说，Akka 集群为 Actor 模型提供了一个可容错、去中心化的节点集群管理系统，来保证 Actor 的运行和 Actor 之间的通信。

如下图所示，Akka 集群是一个完全去中心化的分布式集群管理系统。一个集群由多个节点组成，每个节点都可以进行数据处理和任务执行，节点之间均可进行通信。节点有 Leader 节点和非 Leader 节点之分。与非 Leader 节点相比，Leader 节点只是增加了负责节点的加入和移除集群的功能，所以并不会影响非集中式结构中节点的平等关系。

可以看到，Akka 集群的两个重点是数据传输和集群组建及管理，所以接下来我将从这两个方面与你介绍 Akka 集群。

首先，我们看一下数据传输。在 Akka 集群中，节点是对等的，也就是说每个节点是可以并发处理的，因此必然存在数据传输和一致性的问题。

比如，我们要针对数据进行操作，将 X=1 修改为 X=2。现在集群中节点 1 进行了修改使得 X=2，但其他节点上还是 X=1，因此节点 1 需要将 X=2 的消息告知其他节点，以保证最终集群中所有节点上均为 X=2。

其实，这个问题就是分布式共识问题。我已经在第 5 篇文章“分布式共识：存异求同”中，与你介绍了 PoW、PoS 和 DPoS 三种达成共识的方法，你可以再复习下相关内容。

Akka 集群主要采用的是谁的时间戳最新（也就是数据最新），就以谁为准的原则。在这里我要重点与你讲述的是，如何将 X=2 这个消息传输给集群中的每个节点。

Akka 集群采用了Gossip 协议，该协议是最终一致性协议。它的原理是每个节点周期性地从自己维护的集群节点列表中，随机选择 k 个节点，将自己存储的数据信息发给这 k 个节点，接收到该信息的节点采用前面讲的共识原则，对收到的数据和本地数据进行合并，这样迭代几个周期后，集群中所有节点上的数据信息就一致了。

这就好比我们生活中的“谣言传播”一样，用户 A 告诉用户 B“商场新开了一家火锅店”，用户 B 收到信息后再告诉用户 C，然后用户 C 再告诉用户 D。这样，用户 A、B、C、D 最终都知道了这个消息。

接下来，我们看一下集群组建及管理。下图展示了 Akka 集群的创建过程。在创建集群时，节点被分为三种类型，即：

种子节点。使用静态配置文件方式或者系统运行时指定方式，可以生成种子节点；种子节点是普通节点加入集群的联系点，可以自动接收新加入集群的节点的信息。

首种子节点。首种子节点是配置文件中的第一个种子节点，其功能是集群第一次启动时，首种子节点启动起来，集群才能组建成功，保证集群第一次创建时只有一个集群。如下图 A 节点，就是 Akka 集群的首种子节点。

普通节点。可以向种子节点或集群中的任意节点发送 Join 消息，请求加入集群。如下图的 B 和 C 节点，通过向 A 节点发送 Join 消息，从而加入到 Akka 集群。

备注：图片引自https://getakka.net/articles/clustering/cluster-overview.html

Akka 集群的每个节点启动后，读取配置文件获取种子节点列表，然后开始组建集群：

如果本节点为首种子节点，则把自己加入到集群列表中，即以自己为中心构建集群；

如果本节点为种子节点，则向首种子节点请求加入集群，当首种子节点回复同意消息后，可以加入集群，否则不可加入集群；

如果本节点为普通节点，则可以向任一种子节点（包括首种子节点）请求加入集群，收到同意后，则加入集群，否则不可加入集群。

加入首种子节点或种子节点的节点信息，会通过 Gossip 协议的传播方式传播给当前已加入的所有节点，以完成集群组建。当集群组建完成后，就不存在种子节点与普通节点之分了，每个节点均可执行 Actor 应用程序。

Akka 集群可以构建可扩展的、弹性的分布式应用程序，因此在 JVM 中应用了 Akka 框架，从而实现并发编程。目前，豌豆荚、蘑菇街等公司采用了 Akka 集群。

到这里，我们小结一下吧。Akka 集群是一个完全去中心化的集群管理系统，当集群组建完成后，每个节点均可执行 Actor 应用程序，因此支持并发操作。但，这个并发操作引入了数据同步和一致性问题，所以 Akka 集群采用了 Gossip 协议进行数据同步，通过谁的时间戳最新就以谁为准，来解决一致性问题。

在实际业务场景中，除了面向应用程序平台的分布式集群管理之外，分布式数据存储也是一个非常重要的话题。在这其中，分布式数据存储中的集群管理便是一个关键因素。那么接下来，我就以开源数据库 Redis 的集群管理系统为例，与你展开介绍吧。

Redis 集群

Redis 是一个开源的高性能分布式 key-value 数据库，应用广泛，其特征主要表现为：

支持数据的持久化，可以将内存中的数据保存在磁盘中，重启时可以再次加载并使用；

支持多种数据结构，不仅支持简单的 key-value 类型的数据，同时还提供 list、set、hash 等数据结构的存储；

支持数据的备份，即 Master/Slave 模式的数据备份。

Redis 的这些特征均是为数据存储进行服务的，数据可分片存储在不同的 Redis 节点上，多个 Redis 节点间可共享数据，而提供这项能力的就是 Redis 集群。

Redis 集群中不存在中央节点，是典型的去中心化结构，每个节点均可与其他节点通信。所有节点均可负责存储数据、记录集群的状态（包括键值到正确节点的映射），客户端可以访问或连接到任一节点上。集群节点同样能自动发现其他节点，检测故障的节点，并在需要的时候在从节点中推选出主节点。Redis 集群的架构图如下所示。

当然，节点之间的数据传输仍采用了 Gossip 协议，来保证集群中数据的最终一致性。

Redis 集群中的节点用于数据存储，所以在设计时，需要考虑数据的可靠性和分片存储问题。

对于可靠性的问题，集群中每个节点均存在主备，也就是说每台服务器上都运行两个 Redis 服务，分别为主备，主故障后，备升主。

而对于数据的分片存储问题，Redis 集群引入了哈希槽的概念。Redis 集群内置了 16384 个哈希槽，每个节点负责一部分哈希槽。当客户端要存储一个数据或对象时，对该对象的 key 通过 CRC16 校验后对 16384 取模，也就是 HASH_SLOT = CRC16(key) mod 16384 来决定哈希槽，从而确定存储在哪个节点上。

比如，当前集群有 3 个节点，那么:

节点 A 包含 0 到 5500 号哈希槽；

节点 B 包含 5501 到 11000 号哈希槽；

节点 C 包含 11001 到 16383 号哈希槽。

Redis 集群采用集群分片方式实现了数据的分片存储，从而将 Redis 的写操作分摊到了多个节点上，提高了写并发能力。

到这里，我们小结一下。Redis 集群是一个非集中式集群管理系统，没有中心节点，不会因为某个节点造成性能瓶颈，每个节点均支持数据存储，且采用分片存储方式，提高了写的并发能力。同时，每个节点的设计采用主备设计，提高了数据的可靠性。

鉴于这些优点，Redis 已被 Twitter、Uber、GitHub、Instagaram 等公司采用。

除了 Redis 外，还有一个开源分布式 key-value 数据库系统 Cassandra。接下来，我就再与你分享下 Cassandra 集群的设计，以加深你对非集中式架构的理解。

Cassandra 集群

与 Redis 类似，Cassandra 也支持数据的分布式存储和操作。因此，Cassandra 的集群架构与数据分片存储方案，与 Redis 集群类似。

如下图所示，Cassandra 集群的系统架构是基于一致性哈希的完全 P2P 结构，没有 Master 的概念，所有节点都是同样的角色，彻底避免了因为单点问题导致的系统不稳定。Cassandra 集群节点间的状态同步，也是通过 Gossip 协议来进行 P2P 通信的。

集群中的每个节点，都可以存储数据，并接收来自客户端的请求。Cassandra 集群数据存储与 Redis 的不同之处是，Redis 集群每个节点代表一部分哈希槽，一个哈希槽代表一个哈希值区间，而 Cassandra 集群中每个节点代表一个哈希值。

在 Cassandra 集群中，每次客户端随机选择集群中的一个节点来请求数据，对应接收请求的节点将对应的 key 在一致性哈希环上定位出是哪些节点应该存储这个数据，然后将请求转发到对应的节点上，并将对应若干节点的查询反馈返回给客户端。

目前，Cassandra 集群因为完全去中心化的结构模式，已经被 Apple、Comcast、Instagram、Spotify、eBay、Netflix 等公司使用。

到这里，我们小结一下吧。Cassandra 采用去中心化的架构，解决了集中式结构的单点故障问题，同时因为数据基于哈希值分区存储，提高了读写数据的并发能力。在 Cassandra 集群中，没有 Master 的概念，每个节点代表一个哈希值，通过哈希映射的方式决定数据存储的位置。集群间的状态同步通过 Gossip 协议来进行 P2P 的通信。

对比分析

好了，以上就是 Akka 集群、Redis 集群和 Cassandra 集群的主要内容了。为了便于理解与记忆，我将这 3 个集群的主要特征梳理为了一张表格，如下所示：

知识扩展：如何优化 Gossip 协议中的重复消息问题？

非集中式结构的通信协议采用了 Gossip 协议。而 Gossip 是一种谣言传播协议，每个节点周期性地从节点列表中选择 k 个节点，将本节点存储的信息传播出去，直到所有节点信息一致，即算法收敛了。

这里有个问题，如果每次都是随机选择 k 个节点的话，势必会存在重复选择同样节点的可能，增加消息量。你觉得这个问题是否可以优化，又应该如何优化呢？

首先，这个问题肯定是可以优化的。解决方案是，每个节点记录当前传输的消息且还未达到收敛的时候，已经发送给了哪些节点，然后每次选择时从没有发送过的节点列表中随机选择 k 个节点，直到所有节点均被传输或集群收敛为止。这样，一方面减少了重复消息量，另一方面加快了收敛速度。

总结

集中式结构虽然易于理解，但容易出现单点瓶颈和单点故障等问题，而非集中结构才是超大规模分布式系统的首选结构。所以今天，我以 Akka 集群、Redis 集群和 Cassandra 集群的结构为例，与你详细介绍了非集中式架构。

Akka 集群是一个完全去中心化的集群管理系统，节点之间都是 P2P 的连接模式，通过 Gossip 协议来进行通信，节点之间有角色划分，负责数据存储的节点会进行存储数据。

Redis 集群也是 P2P 的网状连接模式，但是基于 key-value 的数据库模型，每个节点都可以执行数据的计算和存储。此外，Redis 集群引入了哈希槽的概念，来解决数据的分片存储问题。

Cassandra 集群的结构是一致性哈希的 P2P，节点会构成一个环结构，通过哈希映射来选择对应的节点。

好了，到最后，我再以一个思维导图为你总结一下这三个集群核心知识点，以方便你理解与记忆。

虽然这三种集群的节点组织结构各有不同，但节点之间都是通过 Gossip 协议来传递信息的。因此，在实现过程中，集群的消息传输、节点的功能等，在不同的分布式系统中都是类似的，而难点主要在于集群结构的设计。

由于 Akka 集群、Redis 集群和 Cassandra 集群都是典型的非集中式集群组织结构，目前应用已经非常广泛了，所以有很多的实现案例可供你借鉴了。对于具体集群使用配置可参考相应的官网手册，讲得比较全和细。

相信你通过对今天的学习以后，很快就可以上手非集中式架构的集群管理应用和实践了。加油，挑战一下自己吧！

思考题

边缘计算中边缘设备的管理，你认为适合非集中式结构还是集中式结构呢，原因又是什么呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

11 | 分布式调度架构之单体调度：物质文明、精神文明一手抓

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在前两篇文章中，我和你分析了云资源管理的集中式架构和非集中式架构。可以看出，分布式系统架构的目的是，将多个服务器资源管理起来，寻找合适的服务器去执行用户任务。

那，什么是合适的服务器呢？衡量一个服务器是否合适会涉及很多条件或约束，比如在一些场景下，任务存在优先级，那当我们需要执行多个任务的时候，通常需要满足优先级高的任务优先执行的条件。但在这些条件中，服务器资源能够满足用户任务对资源的诉求是必须的。

而为用户任务寻找合适的服务器这个过程，在分布式领域中叫作调度。在分布式系统架构中，调度器就是一个非常重要的组件。它通常会提供多种调度策略，负责完成具体的调度工作。

当然，不同的分布式架构的调度器原理也不一样，最常见或最直观的是单体调度，就是任务和分布式系统中的空闲资源直接进行匹配调度。也就是说，调度器同时管理任务和资源，如果把资源比作“物质文明”，把任务比作“精神文明”，那么单体调度就是“物质文明和精神文明一手抓”。

接下来，我带你一起打卡分布式调度架构之单体调度。

首先，让我们先了解一下什么是单体调度。

什么是单体调度？

分布式系统中的单体调度是指，一个集群中只有一个节点运行调度进程，该节点对集群中的其他节点具有访问权限，可以搜集其他节点的资源信息、节点状态等进行统一管理，同时根据用户下发的任务对资源的需求，在调度器中进行任务与资源匹配，然后根据匹配结果将任务指派给其他节点。

单体调度器拥有全局资源视图和全局任务，可以很容易地实现对任务的约束并实施全局性的调度策略。目前很多集群管理系统采用了单体调度设计，比如我们第 9 篇文章中讲到的 Google Borg、Kubernetes 等。

如下图所示，图中展示了一个典型的单体调度框架。Master 节点上运行了调度进程（负责资源管理、Tasks 和资源匹配）；Node 1，Node 2，…， Node N 对应着我们在第 9 篇文章中讲的 Master/Slave 架构中的 Slave 节点。

Slave 节点会将 Node State 上报给 Master 节点的 Cluster State 模块，Cluster State 模块用于管理集群中节点的资源等状态，并将节点的资源状态传送给 Scheduling Logic 模块，以便 Scheduling Logic 模块进行 Tasks 与资源匹配，并根据匹配结果将 Task 发送给匹配到的节点。

单体调度设计

在集群管理中，单体调度模块称为“Scheduler”或“单体调度器”。单体调度器也叫作集中式调度器，指的是使用中心化的方式去管理资源和调度任务。

也就是说，调度器本身在系统中以单实例形式存在，所有的资源请求和任务调度都通过这个实例进行。集中式调度器的常见模型，如下图所示。可以看到，在这一模型中，资源的使用状态和任务的执行状态都由调度器进行管理。

在 Borg 和 Kubernetes 这两个集群管理系统中，Scheduler 是它们的核心。而 Kubernetes 又是 Borg 的开源版本。所以接下来，我就以 Borg 为例，与你讲述它的调度器是如何设计的，才能保证在上万台机器规模的集群上，运行来自几千个不同应用的几十万个作业。

Borg 调度设计

调度的初衷是为作业或任务寻找合适的资源，也就是说作业或任务是调度的对象。那么作业和任务到底是什么呢？下面，我带你先了解一下作业和任务的概念以及关系。

我们先来看看作业和任务的定义分别是什么吧。

一个 Borg 作业的属性包括名称、拥有者和任务个数。作业可以有一些约束来强制其任务运行在有特定属性的机器上，比如处理器架构、操作系统版本、是否有外网 IP 地址等。这些约束可以是硬性的也可以是柔性的，其中柔性约束表示偏好，而非需求。一个作业只在一个集群中运行。

而一个任务对应的是一组 Linux 进程，运行在一台机器上的一个容器内或直接运行在节点上。任务也有一些属性，比如资源需求量、在作业中的序号等。

那么，作业和任务是什么关系呢？

概括来说，一个作业可以包含多个任务。作业类似于用户在一次事务处理或计算过程中要求计算机所做工作的总和，而任务就是一项项具体的工作，二者属于包含关系。

一个作业中的任务大多有相同的属性，但也可以被覆盖 ，比如特定任务的命令行参数、各维度的资源（比如，CPU 核、内存、硬盘空间、硬盘访问速度、TCP 端口等）。

多个任务可以在多台机器上同时执行，从而加快作业的完成速度，提高系统的并行程度。而具体将哪个任务分配给哪个机器去完成，就是调度器要做的事儿了。

接下来，我就与你讲述下 Borg 的 Scheduler 组件，来帮助你理解 Borg 内部的任务调度流程，以加深你对单体调度的理解。其实，很多框架比如 Hadoop、Spark 等都是采用了单体调度设计，它们整体的思想类似，所以我希望通过对 Borg 调度的讲解，能够帮助你理解你所在业务中的调度逻辑。

我们先来回忆下 Borg 的系统架构图吧。

Scheduler 负责任务的调度，当用户提交一个作业给 BorgMaster 后，BorgMaster 会把该作业保存到 Paxos 仓库中，并将这个作业的所有任务加入等待队列中。调度器异步地扫描等待队列，将任务分配到满足作业约束且有足够资源的计算节点上。

这里我要再强调一下，调度是以任务为单位的，而不是以作业为单位。调度器在扫描队列时，按照任务的优先级从高到低进行选择，同优先级的任务则以轮询的方式处理，以保证用户间的公平，并避免队首的大型作业阻塞队列。

接下来，我们再看看调度器的核心部分，也就是调度算法吧。

Borg 调度算法

Borg 调度算法的核心思想是“筛选可行，评分取优”，具体包括两个阶段：

可行性检查，找到一组可以运行任务的机器（Borglet）；

评分，从可行的机器中选择一个合适的机器（Borglet）。

首先，我们看一下可行性检查。在可行性检查阶段，调度器会找到一组满足任务约束，且有足够可用资源的机器。比如，现在有一个任务 A 要求能部署的节点是节点 1、节点 3 和节点 5，并且任务资源需求为 0.5 个 CPU、2MB 内存。根据任务 A 的约束条件，可以先筛选出节点 1、节点 3 和节点 5，然后根据任务 A 的资源需求，再从这 3 个节点中寻找满足任务资源需求的节点。

这里需要注意的是，每个节点上的可用资源，包括已经分配给低优先级任务但可以抢占的资源。

然后，我们看看评分阶段。

在评分阶段，调度器确定每台可行机器的适宜性。Borg 根据某一评分机制，对可行性检查阶段中筛选出的机器进行打分，选出最适合调度的一台机器。

在评分过程中，我们可以制定多种评价指标，比如考虑如何最小化被抢占的任务数、尽量选择已经下载了相同 package 的机器、目标任务是否跨域部署、在目标机器上是否进行高低优先级任务的混合部署等。 根据不同的考虑因素，可以定制不同的评分算法。

其中，常见的评分算法，包括“最差匹配”和“最佳匹配”两种。

Borg 早期使用修改过的 E-PVM 算法来评分，该算法的核心是将任务尽量分散到不同的机器上。该算法的问题在于，它会导致每个机器都有少量的无法使用的剩余资源，因此有时称其为“最差匹配”（worst fit）。

比如，现在有两个机器，机器 A 的空闲资源为 1 个 CPU 和 1G 内存、机器 B 的空闲资源为 0.8 个 CPU 和 1.2G 内存；同时有两个任务，Task1 的资源需求为 0.4 个 CPU 和 0.3G 内存、Task2 的资源需求为 0.3CPU 和 0.5G 内存。按照最差匹配算法思想，Task1 和 Task2 会分别分配到机器 A 和机器 B 上，导致机器 A 和机器 B 都存在一些资源碎片，可能无法再运行其他 Task。

与之相反的是“最佳匹配”（best fit），即把机器上的任务塞得越满越好。这样就可以“空”出一些没有用户作业的机器（它们仍运行存储服务），来直接放置大型任务。

比如，在上面的例子中，按照最佳匹配算法的思想，Task1 和 Task2 会被一起部署到机器 A 或机器 B 上，这样未被部署的机器就可以用于执行其他大型任务了。

但是，如果用户或 Borg 错误估计了资源需求，紧凑的装箱操作会对性能造成巨大的影响。比如，用户估计它的任务 A 需要 0.5 个 CPU 和 1G 内存，运行该任务的服务器上由于部署了其他任务，现在还剩 0.2 个 CPU 和 1.5G 内存，但用户的任务 A 突发峰值时（比如电商抢购），需要 1 个 CPU 和 3G 内存，很明显，初始资源估计错误，此时服务器资源不满足峰值需求，导致任务 A 不能正常运行。

所以说，最佳匹配策略不利于有突发负载的应用，而且对申请少量 CPU 的批处理作业也不友好，因为这些作业申请少量 CPU 本来就是为了更快速地被调度执行，并可以使用碎片资源。还有一个问题，这种策略有点类似“把所有鸡蛋放到一个篮子里面”，当这台服务器故障后，运行在这台服务器上的作业都会故障，对业务造成较大的影响。

因此，这两个评分算法各有利弊。在实践过程中，我们往往会根据实际情况来选择更适宜的评分算法。比如，对于资源比较紧缺，且业务流量比较规律，基本不会出现突发情况的场景，可以选择最佳匹配算法；如果资源比较丰富，且业务流量会经常出现突发情况的场景，可以选择最差匹配算法。

Borg 的设计是支持高优先级抢占低优先级任务的，也就是说如果评分后选中的机器上没有足够的资源来运行新任务，Borg 会抢占低优先级的任务，从最低优先级逐级向上抢占，直到可用资源足够运行该任务。被抢占的任务放回到调度器的等待队列里，而不会被迁移或使其休眠。

当然有很多调度框架是支持用户根据自己的场景自定义调度策略的，比如优先级策略、亲和性策略、反亲和性策略等。

知识扩展：多个集群 / 数据中心如何实现单体调度呢？

今天这篇文章中，我与你讲述的单体调度，其实是针对一个集群或一个数据中心的，那么多个集群或多个数据中心，能不能基于单体调度实现呢？

答案是肯定的，这就是集群联邦的概念了。

所谓集群联邦，就是将多个集群联合起来工作，核心思想是增加一个控制中心，由它提供统一对外接口，多个集群的 Master 向这个控制中心进行注册，控制中心会管理所有注册集群的状态和资源信息，控制中心接收到任务后会根据任务和集群信息进行调度匹配，选择到合适的集群后，将任务发送给相应的集群去执行。

集群联邦的概念，其实就是单体调度的分层实现。如果你对集群联邦感兴趣的话，推荐你看一下 Kubernetes 的集群联邦设计和工作原理。

总结

今天，我以 Borg 为例，与你讲述了单体调度架构的设计及调度算法。

单体调度是指一个集群中只有一个节点运行调度进程，该调度进程负责集群资源管理和任务调度，也就是说单体调度器拥有全局资源视图和全局任务。

单体调度的特征，可以总结为以下四点：

单体调度器可以很容易实现对作业的约束并实施全局性的调度策略，因此适合批处理任务和吞吐量较大、运行时间较长的任务。

单体调度系统的状态同步比较容易且稳定，这是因为资源使用和任务执行的状态被统一管理，降低了状态同步和并发控制的难度。

调度算法只能全部内置在核心调度器当中，因此调度框架的灵活性和策略的可扩展性不高。

单体调度存在单点故障的可能性。

现在，我再用一个思维导图为你总结一下今天的主要内容，以方便你理解记忆。

单体调度器虽然具有单点瓶颈或单点故障问题，但因为其具有全局资源视图和全局任务，简单易维护，被很多公司广泛采用，比如 Google、阿里、腾讯等公司。另外，我们今天介绍的 Borg 集群管理系统，以及其开源版 Kubernetes 集群管理系统，使用的都是单体调度结构。

单体调度结构虽然结构单一，但是其调度算法可以扩展甚至自定义，也就是说你可以根据业务特征，自定义调度策略，比如优先级策略、亲和性策略等。

学完了关于单体调度的知识后，赶紧上手试试，定制一个独特的调度算法或设计一个特定的单体调度器吧。如果你在这个过程中遇到了什么问题，就留言给我吧。

思考题

你能和我分享下，Google Borg 是采用什么技术实现的资源隔离吗？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

12 | 分布式调度架构之两层调度：物质文明、精神文明两手抓

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

我在上一篇文章中，与你分享了单体调度。单体调度的核心是，所有节点的资源以及用户的任务均由中央服务器统一管理和调度。因此，中央服务器很容易成为单点瓶颈，会直接导致其支持的调度规模和服务类型受限。

于是两层调度就出现了。那么，到底什么是两层调度呢，它是如何设计的，又有哪些调度算法呢？接下来，就和我一起打卡分布式调度架构的两层调度，去探寻这些问题的答案吧。

什么是两层调度？

在单体调度架构中，中央服务器的单点瓶颈问题，会限制调度的效率和支持的任务类型。中央服务器的性能会限制调度的效率，很好理解，但为什么会限制支持的任务类型呢？

简单地说，这是因为不同的服务具有不同的特征，对调度框架和计算的要求都不一样。比如说，你的业务最开始时只有批处理任务，后来发展到同时还包括流数据任务，但批处理任务是处理静态数据，流数据任务却是处理实时数据。显然，单体调度框架会随着任务类型增加而变得越来越复杂，最终出现扩展瓶颈。

那么，为了提升调度效率并支持多种类型的任务，最直接的一个想法就是，能不能把资源和任务分开调度，也就是说一层调度器只负责资源管理和分配，另外一层调度器负责任务与资源的匹配呢。

很显然，这个解决方案是可以的。这种调度架构，就是我们通常所说的两层调度。如果我们还是把资源比作物质文明、把任务比作精神文明的话，两层调度就可以理解为“物质文明与精神文明两手抓”。

两层调度结构对应的就是两层调度器，资源的使用状态同时由中央调度器和第二层调度器管理，中央调度器从整体上进行资源的管理与分配，将资源分配到第二层调度器；再由第二层调度器负责将资源与具体的任务配对，因此第二层调度可以有多个调度器，以支持不同的任务类型。

如下图所示，Scheduler-1 表示第一层调度，负责收集和管理集群中的资源信息；Scheduler-2 表示第二层调度，Scheduler-1 会将集群资源发送给 Scheduler-2，然后 Scheduler-2 根据任务的资源需求和 Scheduler-1 发送的资源信息进行任务匹配和调度。

两层调度器中的第一层调度器仍是一个经简化的中央调度器，通常放在分布式集群管理系统中，而第二层调度则是由各个应用程序框架完成。两层调度器的职责分别是：第一层调度器负责管理资源并向框架分配资源，第二层调度器接收分布式集群管理系统中第一层调度器分配的资源，然后根据任务和接收到的资源进行匹配。

采用两层调度结构的集群管理系统有很多，典型代表是 Apache Mesos 和 Hadoop YARN。我在第 9 篇文章中讲述 Mesos 的体系结构时，和你分析了它采用的是典型的两层调度。那么今天，我就继续以 Mesos 为例，带你学习两层调度的架构设计和对应的分配算法吧。

两层调度设计

由于 Mesos 只负责底层资源的管理和分配，并不涉及存储、 任务调度等功能，因此 Mesos 要实现类似 Borg 那样的资源与任务管理，还需要上层框架的配合。

具体到两层调度架构上，Mesos 本身实现的调度器为第一层调度，负责资源管理，然后将第二层任务调度交给了框架完成。接下来，我们就具体看看吧。

两层调度架构

以 Mesos 为基础的分布式资源管理与调度框架包括两部分，即 Mesos 资源管理集群和框架。

资源管理集群是由一个 Master 节点和多个 Slave 节点组成的集中式系统。每个集群有且仅有一个 Master 节点，负责管理 Slave 节点，并对接上层框架；Slave 节点向 Master 节点周期汇报资源状态信息，并执行框架提交的任务。

框架（Framework）运行在 Mesos 上，是负责应用管理与调度的“组件”，比如 Hadoop、Spark、MPI 和 Marathon 等，不同的框架用于完成不同的任务，比如批处理任务、实时分析任务等。框架主要由调度器（Scheduler）和执行器（Executor）组成，调度器可以从 Master 节点获取集群节点的信息 ，执行器在 Slave 节点上执行任务。

从上述的架构描述可以看出，Mesos 是一个典型的双层调度框架。Mesos Master 上有一个调度器（也就是 Allocation Module），负责管理并分配集群中的所有资源，是第一层调度。框架上负责任务的管理与调度的调度器，是第二层调度，如下图所示。

接下来，我们再看看 Mesos 两层调度的基本原理吧。

框架向 Mesos Master 注册；

Mesos Slave 节点定期或周期向 Mesos Master 上报本节点的空闲资源；

Mesos Master 的 Scheduler 进程收集所有节点的空闲资源信息，并以 Resource Offer 的方式将空闲资源发送给注册的框架；

框架的 Scheduler 接收到 Mesos 发送的资源后，进行任务调度与匹配，匹配成功后，将匹配结果下发给 Mesos Master，并由 Mesos Master 转发给相应节点的执行器执行任务。

可以看出，Mesos 实现双层调度时，采用 Resource Offer 机制衔接了第一层和第二层调度。Resource Offer 机制指的是，Mesos Master 主动将节点空闲资源，以类似发放（Offer）的方式发给每个框架，如果框架需要则使用，不需要则还回。

也就是说，通过 Resource Offer 机制，第一层调度将资源主动告知第二层调度，然后第二层调度进行具体的任务匹配，从而实现了任务调度与资源管理的分离，Mesos Master 通过资源分配算法决定给各个 Framework 提供多少资源，而 Framework 则决定接受哪些资源，以及哪些任务使用这些资源运行。这样一来，一个两层调度架构就实现了。

在 Mesos 的两层调度中，Framework 第二层调度器中的任务与资源匹配的调度策略很常见，也有很多文章做了比较深入的分析了，所以如果你想要深入研究的话，可以参考下 Hadoop、Spark 等的调度策略，这里我就不多说了。

接下来，我们重点看下 Mesos 第一层调度算法，理解其如何为框架分配资源，以支持多用户多框架。

资源分配算法

Mesos 的资源分配算法解决的问题是，决策需要将当前可用资源分配给哪些框架以及分配多少。接下来，我将重点与你介绍两种主要的资源分配算法，即：最大最小公平算法（Max-min Fairness，MMF）和主导资源公平算法（Dominant Resource Fairness，DRF）。

首先，我们看看最大最小公平算法。这是一种在兼顾公平的前提下，尽可能让更多人满意的资源分配算法。为什么这么说呢？因为这个算法有 3 个主要原则：

按照用户对资源需求量递增的顺序进行空闲资源分配；

不存在用户得到的资源超过自己需求的情况；

对于分配的资源不满足需求的用户，所获得的资源是相等的。

在执行资源分配时，最大最小公平算法按照上述 3 条原则进行多次迭代，每次迭代中资源均平均分配，如果还有剩余资源，就进入下一次迭代，一直到所有用户资源得到满足或集群资源分配完毕，迭代结束。

接下来，我们通过一个具体的例子来看看最大最小公平算法的资源分配流程吧。

假设，现在有总量为 100 的空闲资源，有 4 个用户 A、B、C、D 对该资源的需求量分别为（35，10，25，45），分配流程如下所示：

按照用户对资源的需求量升序排列，则 4 个用户的需求量为（B:10，C:25，A:35，D:45）。

平均分配空闲资源。资源空闲总量 100，除以用户数 4，则平均空闲资源量为 25；按照第一步中需求量分配后，用户资源需求量为（0，0，10，20），且用户 B 由于资源需求量小于 25，因此会剩余资源。此时空闲资源量为 15，资源需求人数为 2。

重复第二步，平均分配资源，15/2=7.5，即分别为用户 A 和 D 分配 7.5 份资源，此时用户资源需求量为（0，0，2.5，12.5），空闲资源量为 0，资源需求人数为 2。

所有资源已分配完，算法终止。

最大最小公平算法的执行流程，如下图所示。

在这个案例中，最大最小公平算法是由于所有资源全部分配完才终止的。至此，对于需求量为（10，25，35，45）的用户们来说，分配到的资源是（10，25，32.5，32.5）。这个算法的另外一个结束条件是，资源分配满足了所有用户的资源需求，即当没有用户有资源需求时，算法也会终止。

接下来，我们再看看主导资源公平算法。

最大最小公平算法采用了绝对公平的方式分配资源，会导致大量的资源浪费，比如用户需求量为 35 和 45 的用户 A 和用户 D，均分配了 32.5 的空闲资源，但由于资源不满足需求，这两个用户均无法使用。

而主导资源公平算法在考虑用户公平性的前提下，还考虑了用户对不同资源类型的需求，以尽可能地合理分配资源。也就是说，同样的资源量，主导资源公平算法可以尽可能地满足更多的用户。

在 Mesos 中，框架对资源的需求往往包括对 CPU、内存等多种类型资源的需求。针对多种资源的需求，主导资源公平算法首先计算已经分配给用户的每一种资源的占用率（Resource Share），比如已经分配的 CPU 占总资源量的多少，已经分配的内存占总资源量的多少。所有资源占用率中的最大值称作该用户的主导资源占用率，而主导资源占用率对应的资源就是用户的主导资源。

我们通过一个具体的案例，看看如何判断用户的主导资源吧。如下图所示，假设系统中的资源共包括 18 个 CPU 和 36 GB 内存，有两个 Framework（Framework A 和 Framework B）分别运行了两种任务，假设 Framework A 运行内存密集型任务，Framework B 运行 CPU 密集型任务，且每个任务所需要的资源量是一致的，分别是 <2 CPU, 8 GB> 和 <6 CPU, 2 GB>。

第一步：计算资源分配量。

假设 x 和 y 分别是 Framework A 和 Framework B 分配的任务数，那么 Framework A 消耗的资源为{2x CPU，8x GB}，Framework B 消耗的资源数为{6y CPU，2y GB}，分配给两个 Framework 的总资源量为（2x+6y）个 CPU 和（8x+2y）GB 内存。

第二步：确定主导资源。

对于 Framework A 来说，每个任务要消耗总 CPU 资源的 2/18，总内存资源的 8/36，所以 Framework A 的主导资源为内存；对于 Framework B 来说，每个任务要消耗总 CPU 资源的 6/18 和总内存资源的 2/36，因而 Framework B 的主导资源为 CPU。

第三步：DRF 算法的核心是平衡所有用户的主导资源占用率，尽可能试图最大化所有用户中最小的主导资源占用率。通过求解下列公式，可以计算出 Framework A 和 Framework B 分配的任务数，并且要在满足公式的条件下，使得 x 和 y 越大越好。

2x+6y≤18

8x+2y≤36

8x/36=6y/18

通过求解可以得出：x=3，即 Framework A 可以运行 3 个任务；y=2，即 Framework B 可以运行 2 个任务。这样分配的话，每个 Framework 获取了相同比例的主导资源，即：A 获取了 2/3 的内存，B 获取了 2/3 的 CPU，从而在主导资源上体现了调度算法的公平性。

在实际任务分配过程中，主导资源率是根据已经分配给 Framework 的资源，占集群中总资源量的多少进行计算的，并且在每次分配过程中，会选择主导资源最小的 Framework 进行分配，也就是试图最大化所有用户中最小的主导资源占用率。

如果你想深入研究主导资源公平算法的话，可参考“Dominant Resource Fairness: Fair Allocation of Multiple Resource Types”这篇论文。

现在，我来对比下这两种调度算法吧。

最大最小公平算法适用于单一类型的资源分配场景，而主导资源公平算法适用于多种类型资源混合的场景。并且，最大最小公平算法从公平的角度出发，为每个用户分配不多于需求量的资源；而主导资源公平算法从任务出发，目的在于尽量充分利用资源使得能够执行的任务越多越好。

知识扩展：两层调度如何保证不同的业务不会互相干扰？

类似 Mesos 这样的两层调度机制，可以同时支持多个框架和多种类型的业务，那么如何保证这些业务运行时不会互相干扰呢？

首先，我们思考一下什么情况下会存在业务运行时相互干扰呢。答案就是，当多个业务运行在同一台机器上，共同使用 CPU、内存，以及系统环境时会存在相互干扰。

要解决这个问题，我想你肯定会问，不同的业务能在独立的环境中运行吗？也就是说，隔离不同的业务资源和环境，应该就不会存在相互干扰了吧。不错，解决这个问题的办法就是资源隔离，就好比我们现在接触的虚拟机一样，在同样的服务器上安装多个虚拟机，不同的用户在不同的虚拟机上运行，这些用户互不干扰。在 Mesos 中，实现这种资源隔离的是容器。

容器的实质是进程，该进程运行于属于自己的独立的命名空间，可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间，甚至是自己的用户 ID 空间。Mesos 支持的容器，包括 Linux 自带的 cgroups 和 Docker。

所以说，Mesos 正是用容器隔离开了不同的业务，使得它们运行时不会互相干扰。

总结

今天，我以 Mesos 为例，与你讲述了两层调度的架构设计和资源分配算法。我们一起总结下今天的核心内容吧。

两层调度是一种资源和任务分开调度的设计，也就是说一层调度器只负责资源的管理和分配，另外一层调度器负责任务与资源的匹配。

在 Mesos 中，第一层资源调度由 Mesos 提供，第二层任务调度由框架提供，Mesos 将资源以 Resource Offer 的形式发放给框架调度器，框架调度器根据任务需求和得到的资源信息进行任务匹配调度，为此提高了调度的并发性。

而关于第一层的调度算法，通常有最大最小公平算法和主导资源公平算法等。

两层调度的一个问题是，由于第二层调度只能获得部分资源视图，因此无法实现全局最优调度。

最后，我将今天这篇文章的核心知识点梳理为了一张思维导图，以方便你理解与记忆。

两层调度提供了多租户多框架的支持，如果你的业务类型比较多或者面向的是不同的租户的话，建议你采用两层调度框架。

相信你通过这篇文章可以看到，在分布式领域中，同时支持多种框架、支持多种类型任务调度的调度机制，并没有那么神秘，只要你静下心来弄明白这篇文章的调度机制，以后遇到类似的调度机制，就可以做到心中有数了。

不得不说，Mesos 的两层调度设计得非常巧妙，并且 Mesos 支持你自己写一个调度器注册到 Mesos 作为第二层调度。赶快动手实践一下吧，Mesos 的官网提供了相应的案例，方便你入门，加油，相信你一定可以！

思考题

你觉得，Mesos 双层调度机制容易导致资源碎片问题吗？原因又是什么呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

13 | 分布式调度架构之共享状态调度：物质文明、精神文明多手协商抓

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章中，我们一起学习了两层调度。在两层调度架构中，第二层调度只知道集群中的部分资源，无法进行全局最优调度。那么，是否有办法解决全局最优调度的问题呢？

答案是肯定的，解决办法就是我今天要带你打卡的共享状态调度。

接下来，我们就一起看看共享状态调度到底是什么，以及它的架构和工作原理吧。

什么是共享状态调度？

通过我们前两篇文章的讲述，不难发现，集群中需要管理的对象主要包括两种：

一是，资源的分配和使用状态；

二是，任务的调度和执行状态；

在单体调度中，这两种对象都是由单体调度器管理的，因此可以比较容易地保证全局状态的一致性，但问题是可扩展性较差（支持业务类型受限），且存在单点瓶颈问题。

而在两层调度中，这两种对象分别由第一层中央调度器和第二层 Framework 调度器管理，由于 Framwork 调度器只能看到部分资源，因此不能保证全局状态的一致性，也不容易实现全局最优的调度。

为了解决这些问题，一种新的调度器架构被设计了出来。这种架构基本上沿袭了单体调度器的模式，通过将单体调度器分解为多个调度器，每个调度器都有全局的资源状态信息，从而实现最优的任务调度，提供了更好的可扩展性。

也就是说，这种调度架构在支持多种任务类型的同时，还能拥有全局的资源状态信息。要做到这一点，这种调度架构的多个调度器需要共享集群状态，包括资源状态和任务状态等。因此，这种调度架构，我们称之为共享状态调度器。

如果我们继续把资源比作物质文明、把任务比作精神文明的话，相对于单体调度和两层调度来说，共享状态调度就是“物质文明与精神文明多手协商抓”。

共享状态调度架构的示意图，如下所示：

可以看出，共享状态调度架构为了提供高可用性和可扩展性，将集群状态之外的功能抽出来作为独立的服务。具体来说就是：

State Storage 模块（资源维护模块）负责存储和维护资源及任务状态，以便 Scheduler 查询资源状态和调度任务；

Resource Pool 即为多个节点集群，接收并执行 Scheduler 调度的任务；

而 Scheduler 只包含任务调度操作，而不是像单体调度器那样还需要管理集群资源等。

共享状态调度也支持多种任务类型，但与两层调度架构相比，主要有两个不同之处：

存在多个调度器，每个调度器都可以拥有集群全局的资源状态信息，可以根据该信息进行任务调度；

共享状态调度是乐观并发调度，在执行了任务匹配算法后，调度器将其调度结果提交给 State Storage，由其决定是否进行本次调度，从而解决竞争同一种资源而引起的冲突问题，实现全局最优调度。而，两层调度是悲观并发调度，在执行任务之前避免冲突，无法实现全局最优匹配。

看到这里，我再和你说说乐观并发调度和悲观并发调度的区别吧。

乐观并发调度，强调事后检测，在事务提交时检查是否避免了冲突：若避免，则提交；否则回滚并自动重新执行。也就是说，它是在执行任务匹配调度算法后，待计算出结果后再进行冲突检测。

悲观并发调度，强调事前预防，在事务执行时检查是否会存在冲突。不存在，则继续执行；否则等待或回滚。也就是说，在执行任务匹配调度算法前，通过给不同的 Framework 发送不同的资源，以避免冲突。

现在，我们已经对共享状态调度有了一个整体印象，知道了它可以解决什么问题。那么接下来，我们再看看这种调度架构是如何设计的吧。

共享状态调度设计

共享状态调度的理念最早是 Google 针对两层调度器的不足，提出的一种调度架构。这种调度结构的典型代表有 Google 的 Omega、微软的 Apollo，以及 Hashicorp 的 Nomad 容器调度器。

作为 Google 公司的第二代集群管理系统，Omega 在设计时参考了 Borg 的设计，吸收了 Borg 的优点，并改进了其不足之处。所以接下来，我就以 Omega 为例和你讲述共享状态调度的架构和工作原理吧。这样一来，你可以对照着第 11 篇文章中 Borg 的调度设计一起理解。

Omega 调度架构

Omega 集群中有一个“Cell”的概念，每个 Cell 管理着部分物理集群，一个集群有多个 Cell。实际上，你可以直接将这里的“Cell”理解为一个集群的子集群或子节点的集合。

Omega 集群的调度架构示意图，如下所示。

我在介绍共享状态调度的架构时提到，State Storage 模块负责存储和维护资源及任务状态，里面有一个 Cell State 文件，记录着全局共享的集群状态。实际上，State Storage 组件中的集群资源状态信息，就是主本，而 Cell State 就是以主副本的形式存在的。每个调度器都包含一个私有的 Cell State 副本，也就是拥有了一个集群资源状态信息的副本，进而达到了共享集群资源状态信息的目的。

在 Omega 系统中，没有中央资源分配器，所有资源分配决策都在调度器（Scheduler）中进行。每个调度器都可以根据私有的 Cell State 副本，来制定调度决策。

调度器可以查看 Cell 的整个状态，并申请任何可用的集群资源。一旦调度器做出资源调度决策，它就会在原子提交中更新本地的 Cell State 的资源状态副本。若同时有多个调度器申请同一份资源，State Storage 模块可以根据任务的优先级，选择优先级最高的那个任务进行调度。

可以看出，在 Omega 系统中的每个调度器，都具有对整个集群资源的访问权限，从而允许多个调度器自由地竞争空闲资源，并在更新集群状态时使用乐观并发控制来调解资源冲突问题。

这样一来，Omega 就有效地解决了两层调度中 Framework 只拥有局部资源，无法实现全局最优的问题。

接下来，我们看一下 Omega 共享调度的工作原理吧。

Omega 共享调度工作原理

Omega 使用事务管理状态的设计思想，将集群中资源的使用和任务的调度类似于基于数据库中的一条条事务（Transaction）一样进行管理。显然，数据库是一个共享的状态，对应 Omega 中的 Cell State，而每个调度器都要根据数据库的信息（即集群的资源信息）去独立完成自己的任务调度策略。

接下来，我们就具体看看吧。

如下图所示，在一个应用执行的过程中，调度器会将一个 Job 中的所有 Task 与 Resource 进行匹配，可以说 Task 与 Resource 之间是进行多对多匹配的。其间，调度器会设置多个 Checkpoint 来检测 Resource 是否都已经被占用，只有这个 Job 的所有 Task 可以匹配到可用资源时，这个 Job 才可以被调度。

这里的 Job 相当于一个事务，也就是说，当所有 Task 匹配成功后，这个事务就会被成功 Commit，如果存在 Task 匹配不到可用资源，那么这个事务需要执行回滚操作，Job 调度失败。

无论事务是否执行成功，调度器都会在事务执行之后，重新从主本那里同步更新本地 Cell State 的资源状态副本，以保证本地集群信息状态的有效性。若事务未成功执行，则调度器会在必要时重新运行其调度算法并再次尝试申请资源。

也就是说，调度器对 Job 的调度是具有原子性的，一个 Job 的所有 Task 都是一起调度的，即使部分 Task 调度失败了，调度器再次调度时也必须再次调度整个 Job。多个调度器可以并行调度，无需等待其他调度器调度结果，若存在冲突时，进行冲突处理，比如根据 Job 的优先级，优先级高则获得资源。

由此我们可以看到，Omega 涉及了 Job 并发调度。针对这一点，Omega 采用了传统数据库中的乐观锁（MVCC，Multi-Version Concurrency Control，基于多版本的并发访问控制），即每一个应用都发放了所有的可用资源，在更新集群状态时使用乐观并发控制来解决资源冲突问题，来提高 Omega 的并发度。

不同的 Omega 调度器可以实现不同的策略，但有一些调度规则是所有调度器必须达成一致的，比如哪些资源是允许分配的、如何评估作业的优先级等。

因此，Omega 调度器将两层调度器中的集中式资源调度模块简化成了一些持久化的共享数据（状态）和针对这些数据的验证代码。而这里的“共享数据”，实际上就是整个集群的实时资源状态信息，而验证代码就是解决调度冲突的调度规则。

知识扩展：单体调度、两层调度和共享调度的区别是什么？

现在，我已经带你学习了单体调度、双层调度和共享调度，那么这三种调度的区别是什么呢？接下来，我们就一起回忆并对比下吧。

我把这三种调度的架构示意图放到一起，先帮你有一个整体认识。

单体调度，是由一个中央调度器去管理整个集群的资源信息和任务调度，也就是说所有任务只能通过中央调度器进行调度。

这种调度架构的优点是，中央调度器拥有整个集群的节点资源信息，可以实现全局最优调度。但它的缺点是，无调度并发性，且中央服务器存在单点瓶颈问题，导致支持的调度规模和服务类型受限，同时会限制集群的调度效率。因此，单体调度适用于小规模集群。

两层调度，是将资源管理和任务调度分为两层来调度。其中，第一层调度器负责集群资源管理，并将可用资源发送给第二层调度；第二层调度接收到第一层调度发送的资源，进行任务调度。

这种调度架构的优点是，避免了单体调度的单点瓶颈问题，可以支持更大的服务规模和更多的服务类型。但其缺点是，第二层调度器往往只对全局资源信息有部分可观察性，因此任务匹配算法无法实现全局最优。双层调度适用于中等规模集群。

共享状态调度，多个调度器，每个调度器都可以看到集群的全局资源信息，并根据这些信息进行任务调度。相较于其他两个调度架构来说，共享状态调度架构适用的集群规模最大。

这种调度架构的优点是，每个调度器都可以获取集群中的全局资源信息，因此任务匹配算法可以实现全局最优性。但，也因为每个调度器都可以在全局范围内进行任务匹配，所以多个调度器同时调度时，很可能会匹配到同一个节点，从而造成资源竞争和冲突。

虽然 Omega 的论文宣称可以通过乐观锁机制，避免冲突。但在工程实践中，如果没有妥善处理资源竞争的问题，则很可能会产生资源冲突，从而导致任务调度失败。这时，用户就需要对调度失败的任务进行处理，比如重新调度、任务调度状态维护等，从而进一步增加了任务调度操作的复杂度。

我将单体调度、两层调度、共享状态调度总结在了一张表格中：

总结

今天，我主要与你分享了分布式调度架构设计中的共享状态调度。我们一起来总结下今天的核心知识点吧。

首先，我讲述了什么是共享状态调度。概括地说，共享状态调度是将单体调度器分解为多个服务，由多个服务共享集群状态，包括资源状态和任务状态等。

接下来，我以 Google 的 Omega 集群管理系统为例，和你分享了共享状态调度的架构和工作原理。共享状态调度包含多个调度器，每个调度器都可以看到集群的全局资源信息，并根据这些信息进行任务调度。

最后，我要和你说明的是，共享状态调度是乐观并发调度，调度器将其调度的结果以原子的方式提交给资源维护模块，由其决定是否进行本次调度。

接下来，我整理一张思维导图来帮助你巩固今天的核心知识点。

我想让你知道的是，在分布式领域中，共享状态调度，是 Google 号称的下一代集群管理系统 Omega 的调度机制，可以解决双层调度无法实现全局最优的问题，同时也避免了单体调度的单点瓶颈问题。

但，说到这儿你可能会回想起曾经看到的两句话：

为了达到设计目标，Omega 的实现逻辑变得越来越复杂。 在原有的 Borg 共享状态模型已经能满足绝大部分需要的情况下，Omega 的前景似乎没有那么乐观。

Omega 系统缺点是，在小集群下没有优势。

这里，我再与你解释下，为什么说 Omega 是 Google 准备打造的下一代集群管理系统。

从调度架构方面来看，Borg 无法支持同时存在多种业务类型的场景，并且存在单点瓶颈问题。而 Omega 解决了 Borg 的这两个问题，但是当多个调度器并行调度时，可能存在资源冲突，当资源申请产生冲突时，会导致大量任务或任务多次调度失败，增加了任务调度失败的故障处理的复杂度，比如需要进行作业回滚、任务状态维护等。

因此，设计一个好的冲突避免策略是共享状态调度的关键。对于小规模集群来说，其集群规模、任务数量等都不大，使用单体调度就可以满足其任务调度的需求，避免了考虑复杂的冲突避免策略。也就是说，共享状态调度比较适合大规模、同时存在多种业务类型的场景，不太适合小规模集群。

思考题

共享状态调度的核心是解决并发冲突，那你认为有没有什么好的方法可以解决冲突呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

14 | 答疑篇：分布式事务与分布式锁相关问题

你好，我是聂鹏程。

到目前为止，“分布式技术原理与算法解析”专栏已经更新 13 篇文章了，主要与你介绍了“分布式起源”“分布式协调与同步”和“分布式资源管理与负载调度”。

在这里，我首先要感谢你们在评论区留下的一条条精彩留言。这让我感觉到，你们对分布式技术的浓厚兴趣，以及对我和对这个专栏的支持。这不但活跃了整个专栏的氛围、丰富了专栏的内容，也让我备受鼓舞，干劲儿十足地去交付更高质量的文章。

比如，@xfan、@Jackey 等同学积极思考问题，并对文中有疑惑的地方结合其他资料给出了自己的思考和建议；再比如，@zhaozp 、@静水流深等同学，每次更新后都在坚持打卡学习；再比如，@约书亚等同学针对分布式事务提出了非常好的问题，@每天晒白牙等同学对文中内容进行了非常好的总结。

这些同学有很多，我就不再一一点名了。感谢你们的同时，我也相信，积极参与并留言也会帮助你更深入地理解每一个知识点。所以我希望，你接下来可以多多留言给我，让我们一起学习，共同进步。

留言涉及的问题有很多，但我经过进一步地分析和总结后，发现针对分布式事务和分布式锁的问题比较多，同学们的疑惑也比较多。

确实，这两大问题属于分布式技术的关键问题。因此，今天的这篇答疑文章，我就围绕这两大问题来进行一次集中的分析和回答吧。

我们先来看一下分布式事务的相关问题。

分布式事务的相关问题

在第 6 篇文章“分布式事务：All or Nothing”中，我介绍了两阶段提交协议和三阶段提交协议。有很多同学提出了疑问：两阶段提交协议（2PC）和三阶段提交协议（3PC）的区别，到底是什么？

在回答这个问题前，我建议你先回到第 6 篇文章，去回忆一下它们的流程。然后，我们看看2PC 和 3PC 的第一步到底是不是“类似”的？

2PC 的第一步投票（voting）阶段中，参与者收到事务执行询问请求时，就执行事务但不提交；而 3PC 却写着在 PreCommit 阶段执行事务不提交。2PC 和 3PC 的第一步，是非常不类似吧？

其实，我说它们类似，是指它们均是通过协调者，来询问参与者是否可以正常执行事务操作，参与者也都会给协调者回复。

在 2PC 中，如果所有参与者都返回结果后，会进入第二阶段，也就是提交阶段，也可以说是执行阶段，根据第一阶段的投票结果，进行提交或取消。

在 3PC 中，进入真正的提交阶段前，还会有一个预提交阶段，这个预提交阶段不会做真正的提交，而是会将相关信息记录到事务日志中，当所有参与者都返回 Yes 消息后，才会真正进入提交阶段。

这样说明后，相信你对这个问题的疑惑应该解决了吧。

现在，我们继续延展一下这个问题吧。

追问 1：3PC 在预提交阶段，才开始执行事务操作，那协调者发送 CanCommit 给参与者的时候，参与者根据什么返回 Yes 或者 No 消息呢?

3PC 在投票阶段（CanCommit 阶段），协调者发送 CanCommit 询问后，参与者会根据自身情况，比如自身空闲资源是否足以支撑事务、是否会存在故障等，预估自己是否可以执行事务，但不会执行事务，参与者根据预估结果给协调者反回 Yes 或者 No 消息。

追问 2：3PC 出现的目的是，解决 2PC 的同步阻塞和数据不一致性问题。那么，我们不可以在 2PC 中直接去解决这些问题吗？3PC 多了预提交和超时机制，就真的解决这些问题了吗？

我们先来看看同步阻塞的问题。

在 2PC 中，参与者必须等待协调者发送的事务操作指令，才会执行事务，比如提交事务或回滚等操作，如果协调者故障，也就是说参与者无法收到协调者的指令了，那么参与者只能一直等待下去。这就好比在一个班级里面，班主任是协调者，学生是参与者，班主任告诉学生今天下午 6 点组织一个比赛，但班主任今天生病了，根本到不了学校，并且也无法发送信息告诉学生，那么学生们就只能一直等待。

3PC 在协调者和参与者中都引入了超时机制（2PC 只是在协调者引入了超时），也就是说当参与者在一定时间内，没有接收到协调者的通知时，会执行默认的操作，从而减少了整个集群的阻塞时间。这就好比班主任生病了，学生默认等待半个小时，如果班主任还没有任何通知，那么默认比赛取消，学生可以自由安排，做自己的事情去了。

但其实，阻塞在实际业务中是不可能完全避免的。在上面的例子中，学生等待超时的半个小时中，其实还是阻塞的，只是阻塞的时间缩短了。所以，相对于 2PC 来说，3PC 只是在一定程度上减少（或者说减弱）了阻塞问题。

接下来，我们再看看数据不一致的问题吧。

通过上面的分析可以看到，同步阻塞的根本原因是协调者发生故障，想象一下，比如现在有 10 个参与者，协调者在发送事务操作信息的时候，假设在发送给了 5 个参与者之后发生了故障。在这种情况下，未收到信息的 5 个参与者会发生阻塞，收到信息的 5 个参与者会执行事务，以至于这 10 个参与者的数据信息不一致。

3PC 中引入了预提交阶段，相对于 2PC 来讲是增加了一个预判断，如果在预判断阶段协调者出现故障，那就不会执行事务。这样，可以在一定程度上减少故障导致的数据不一致问题，尽可能保证在最后提交阶段之前，各参与节点的状态是一致的。

所以说，3PC 是研究者们针对 2PC 中存在的问题做的一个改进，虽然没能完全解决这些问题，但也起到了一定的效果。

在实际使用中，通常采用多数投票策略来代替第一阶段的全票策略，比如 Raft 算法等。关于 Raft 算法的具体原理，你可以再回顾下第 4 篇文章“分布式选举：国不可一日无君”中的相关内容。

追问 3：3PC 也是只有一个协调者，为什么就不会有单点故障问题了？

首先，我先明确下这里所说的单点故障问题。

因为系统中只有一个协调者，那么协调者所在服务器出现故障时，系统肯定是无法正常运行的。所以说，2PC 和 3PC 都会有单点故障问题。

但是，3PC 因为在协调者和参与者中都引入了超时机制，可以减弱单点故障对整个系统造成的影响。为什么这么说呢？

因为引入的超时机制，参与者可以在长时间没有得到协调者响应的情况下，自动将超时的事务进行提交，不会像 2PC 那样被阻塞住。

好了，以上就是关于分布式事务中的 2PC 和 3PC 的相关问题了，相信你对这两个提交协议有了更深刻的认识。接下来，我们再看一下分布式锁的相关问题吧。

分布式锁的相关问题

在第 7 篇文章“分布式锁：关键重地，非请勿入”后的留言中，我看到很多同学都问到了分布式互斥和分布式锁的关系是什么。

我们先来回顾下，分布式互斥和分布式锁分别是什么吧。

在分布式系统中，某些资源（即临界资源）同一时刻只有一个程序能够访问，这种排他性的资源访问方式，就叫作分布式互斥。这里，你可以再回顾下第 3 篇文章中的相关内容。

分布式锁指的是，在分布式环境下，系统部署在多个机器中，实现多进程分布式互斥的一种锁。这里，你可以再回顾下第 7 篇文章中的相关内容。

分布式锁的目的是，保证多个进程访问临界资源时，同一时刻只有一个进程可以访问，以保证数据的正确性。因此，我们可以说分布式锁是实现分布式互斥的一种手段或方法。

除了分布式互斥和分布式锁的关系外，很多同学都针对基于 ZooKeeper 和基于 Redis 实现分布式锁，提出了不少好问题。我们具体看看这些问题吧。

首先，我们来看一下基于 ZooKeeper 实现分布式锁的问题。有同学问，ZooKeeper 分布式锁，可能存在多个节点对应的客户端在同一时间完成事务的情况吗？

这里，我需要先澄清一下，ZooKeeper 不是分布式锁，而是一个分布式的、提供分布式应用协调服务的组件。基于 ZooKeeper 的分布式锁是基于 ZooKeeper 的数据结构中的临时顺序节点来实现的。

请注意，这里提到了 ZooKeeper 是一个分布式应用协调服务的组件。比如，在一个集中式集群中，以 Mesos 为例，Mesos 包括 master 节点和 slave 节点，slave 节点启动后是主动去和 master 节点建立连接的，但建立连接的条件是，需要知道 master 节点的 IP 地址和状态等。而 master 节点启动后会将自己的 IP 地址和状态等写入 ZooKeeper 中，这样每个 slave 节点启动后都可以去找 ZooKeeper 获取 master 的信息。而每个 slave 节点与 ZooKeeper 进行交互的时候，均需要一个对应的客户端。

这个例子，说明了存在多个节点对应的客户端与 ZooKeeper 进行交互。同时，由于每个节点之间并未进行通信协商，且它们都是独立自主的，启动时间、与 ZooKeeper 交互的时间、事务完成时间都是独立的，因此存在多个节点对应的客户端在同一时间完成事务的这种情况。

接下来，我们看一下基于 Resis 实现分布式锁的问题。Redis 为什么需要通过队列来维持进程访问共享资源的先后顺序？

在我看来，这是一个很好的问题。

@开心小毛认为，Redis 的分布式锁根本没有队列，收到 setnx 返回为 0 的进程会不断地重试，直到某一次的重试成为 DEL 命令后第一个到达的 setnx 从而获得锁，至于此进程在等待获得锁的众多进程中是不是第一个发出 setnx 的，Redis 并不关心。

其实，客观地说，这个理解是合情合理的，是我们第一反应所能想到的最直接、最简单的解决方法。可以说，这是一种简单、粗暴的方法，也就是获取不到锁，就不停尝试，直到获取到锁为止。

但你有没有想过，当多个进程频繁去访问 Redis 时，Redis 会不会成为瓶颈，性能会不会受影响。带着这个疑惑，我们来具体看看基于 Redis 实现的分布式锁到底需不需要队列吧。

如果没有队列维护多进程请求，那我们可以想到的解决方式，就是我刚刚和你分析过的，通过多进程反复尝试以获取锁。

但，这种方式有三个问题：

一是，反复尝试会增加通信成本和性能开销；

二是，到底过多久再重新尝试；

三是，如果每次都是众多进程进行竞争的话，有可能会导致有些进程永远获取不到锁。

在实际的业务场景中，尝试时间的设置，是一个比较难的问题，与节点规模、事务类型均有关系。

比如，节点规模大的情况下，如果设置的时间周期较短，多个节点频繁访问 Redis，会给 Redis 带来性能冲击，甚至导致 Redis 崩溃；对于节点规模小、事务执行时间短的情况，若设置的重试时间周期过长，会导致节点执行事务的整体时间变长。

基于队列来维持进程访问共享资源先后顺序的方法中，当一个进程释放锁之后，队列里第一个进程可以访问共享资源。也就说，这样一来就解决了上面提到的三个问题。

总结

我针对前面 13 篇文章留言涉及的问题，进行了归纳总结，从中摘取了分布式事务和分布式锁这两个知识点，串成了今天这篇答疑文章。

今天没来得及和你扩展的问题，后续我会再找机会进行解答。最后，我要和你说的是，和我一起打卡分布式核心技术，一起遇见更优秀的自己吧。

篇幅所限，留言区见。

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

15 | 分布式计算模式之MR：一门同流合污的艺术

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

我在第 12 篇文章中与你介绍两层调度时提到，Mesos 的第二层调度是由 Framework 完成的。这里的 Framework 通常就是计算框架，比如 Hadoop、Spark 等。用户基于这些计算框架，可以完成不同类型和规模的计算。

那么，在接下来的 4 篇文章，我们就要进入“第三站：分布式计算技术”了。在这一站，我将与你详细介绍分布式领域中的 4 种计算模式，包括 MapReduce、Stream、Actor 和流水线。而今天这篇文章，我们就先从 MR 模式开始吧。

Hadoop 这个框架主要用于解决海量数据的计算问题。那么，它是如何做到海量数据计算的呢？你可能会想，既然是海量数据，规模这么大，那就分成多个进程，每个进程计算一部分，然后汇总一下结果，就可以提升运算速度了。其实，整个计算流程，我们可以很形象地用一个词来解释，就是“同流合污“。

没错，就是这种想法，在分布式领域中就叫作 MR 模式，即 Map Reduce 模式。接下来，我们就一起揭开 MR 模式的神秘面纱吧。

什么是分而治之？

分而治之（Divide-and-Conquer），是计算机处理问题的一个很重要的思想，简称为分治法。

顾名思义，分治法就是将一个复杂的、难以直接解决的大问题，分割成一些规模较小的、可以比较简单的或直接求解的子问题，这些子问题之间相互独立且与原问题形式相同，递归地求解这些子问题，然后将子问题的解合并得到原问题的解。

比如，现在要统计全中国的人口数，由于中国的人口规模很大，如果让工作人员依次统计每个省市的人口数，工作量会非常大。在实际统计中，我们通常会按照省分别统计，比如湖南省的工作人员统计湖南省的人口数，湖北省的工作人员统计湖北省的人口数等，然后汇总各个省的人口数，即可得到全国人口数。

这，就是一个非常好的分而治之的例子。

当然，这种分治的思想还广泛应用于计算机科学的各个领域中，分布式领域中的很多场景和问题也非常适合采用这种思想解决，并为此设计出了很多计算框架。比如，Hadoop 中的 MapReduce。

那么，在分布式领域，具体有哪些问题适合采用分治法呢？要回答这个问题，我们先看下适合分治法的问题具有哪些特征吧。

问题规模比较大或复杂，且问题可以分解为几个规模较小的、简单的同类型问题进行求解；

子问题之间相互独立，不包含公共子问题；

子问题的解可以合并得到原问题的解。

根据这些特征，我们可以想到，诸如电商统计全国商品数量时，按区域或省市进行统计，然后将统计结果合并得到最终结果等大数据处理场景，均可以采用分治法。

同时，根据这些特征，我们可以推导出，采用分治法解决问题的核心步骤是：

分解原问题。将原问题分解为若干个规模较小，相互独立，且与原问题形式相同的子问题。

求解子问题。若子问题规模较小且容易被解决则直接求解，否则递归地求解各个子问题。

合并解，就是将各个子问题的解合并为原问题的解。

接下来，我们就一起看看分布式系统中分治法的原理和应用吧。

分治法的原理

分布式原本就是为处理大规模应用而生的，所以基于分布式系统，如何分而治之地处理海量数据就是分布式领域中的一个核心问题。

Google 提出的 MapReduce 分布式计算模型（Hadoop MapReduce 是 Google 的开源实现），作为分治法的典型代表，最开始用于搜索领域，后来被广泛用于解决各种海量数据的计算问题。下面，我将以 MapReduce 为例，带你了解分治法的抽象模型、工作原理和实践应用。

抽象模型

如下图所示，MapReduce 分为 Map 和 Reduce 两个核心阶段，其中 Map 对应“分”，即把复杂的任务分解为若干个“简单的任务”执行；Reduce 对应着“合”，即对 Map 阶段的结果进行汇总。

在第一阶段，也就是 Map 阶段，将大数据计算任务拆分为多个子任务，拆分后的子任务通常具有如下特征：

相对于原始任务来说，划分后的子任务与原任务是同质的，比如原任务是统计全国人口数，拆分为统计省的人口数子任务时，都是统计人口数；并且，子任务的数据规模和计算规模会小很多。

多个子任务之间没有依赖，可以独立运行、并行计算，比如按照省统计人口数，统计河北省的人口数和统计湖南省的人口数之间没有依赖关系，可以独立、并行的统计。

第二阶段，也就是 Reduce 阶段，第一阶段拆分的子任务计算完成后，汇总所有子任务的计算结果，以得到最终结果。也就是，汇总各个省统计的人口数，得到全国的总人口数。

MapReduce 工作原理

那么，在 MapReduce 里，各个组件是如何分工完成一个复杂任务的呢？为了解答这个问题，我先带你了解一下 MapReduce 的组件结构。

如上图所示，MapReduce 主要包括以下三种组件：

Master，也就是 MRAppMaster，该模块像一个大总管一样，独掌大权，负责分配任务，协调任务的运行，并为 Mapper 分配 map() 函数操作、为 Reducer 分配 reduce() 函数操作。

Mapper worker，负责 Map 函数功能，即负责执行子任务。

Reducer worker，负责 Reduce 函数功能，即负责汇总各个子任务的结果。

基于这三种组件，MapReduce 的工作流程如下所示：

程序从 User Program 开始进入 MapReduce 操作流程。其中图中的“step1，step2，…，step6”表示操作步骤。

step1：User Program 将任务下发到 MRAppMaster 中。然后，MRAppMaster 执行任务拆分步骤，把 User Program 下发的任务划分成 M 个子任务（M 是用户自定义的数值）。假设，MapReduce 函数将任务划分成了 5 个，其中 Map 作业有 3 个，Reduce 作业有 2 个；集群内的 MRAppMaster 以及 Worker 节点都有任务的副本。

step2：MRAppMaster 分别为 Mapper 和 Reducer 分配相应的 Map 和 Reduce 作业。Map 作业的数量就是划分后的子任务数量，也就是 3 个；Reduce 作业是 2 个。

step3：被分配了 Map 作业的 Worker，开始读取子任务的输入数据，并从输入数据中抽取出 <key, value> 键值对，每一个键值对都作为参数传递给 map() 函数。

step4：map() 函数的输出结果存储在环形缓冲区 kvBuffer 中，这些 Map 结果会被定期写入本地磁盘中，被存储在 R 个不同的磁盘区。这里的 R 表示 Reduce 作业的数量，也是由用户定义的。在这个案例中，R=2。此外，每个 Map 结果的存储位置都会上报给 MRAppMaster。

step5：MRAppMaster 通知 Reducer 它负责的作业在哪一个分区，Reducer 远程读取相应的 Map 结果，即中间键值对。当 Reducer 把它负责的所有中间键值对都读过来后，首先根据键值对的 key 值对中间键值对进行排序，将相同 key 值的键值对聚集在一起，从而有利于 Reducer 对 Map 结果进行统计。

step6：Reducer 遍历排序后的中间键值对，将具有相同 key 值的键值对合并，并将统计结果作为输出文件存入负责的分区中。

从上述流程可以看出，整个 MapReduce 的工作流程主要可以概括为 5 个阶段，即：Input（输入）、Splitting（拆分）、Mapping（映射）、Reducing（化简）以及 Final Result（输出）。

所有 MapReduce 操作执行完毕后，MRAppMaster 将 R 个分区的输出文件结果返回给 User Program，用户可以根据实际需要进行操作。比如，通常并不需要合并这 R 个输出文件，而是将其作为输入交给另一个 MapReduce 程序处理。

MapReduce 实践应用

通过上述的流程描述，你大概已经知道 MapReduce 的工作流程了。接下来，我和你分享一个电商统计用户消费记录的例子，再帮你巩固一下 MapReduce 的功能吧。

需要注意的是，为了方便理解，我对下面用的数据做了一定的处理，并不完全是真实场景中的数据。

每隔一段时间，电商都会统计该时期平台的订单记录，从而分析用户的消费倾向。在不考虑国外消费记录的前提下，全国范围内的订单记录已经是一个很大规模的工程了。

在前面的文章中我也提到过，电商往往会在每个省份、多个城市分布式地部署多个服务器，用于管理某一地区的平台数据。因此，针对全国范围内的消费统计，可以拆分成对多个省份的消费统计，并再一次细化到统计每一个城市的消费记录。

为方便描述，假设我们现在要统计苏锡常地区第二季度手机订单数量 Top3 的品牌。我们来看看具体的统计步骤吧。

任务拆分（Splitting 阶段）。根据地理位置，分别统计苏州、无锡、常州第二季度手机订单 Top3 品牌，从而将大规模任务划分为 3 个子任务。

通过循环调用 map() 函数，统计每个品牌手机的订单数量。其中，key 为手机品牌，value 为手机购买数量（单位：万台）。如下图 Mapping 阶段所示（为简化描述，图中直接列出了统计结果）。

与前面讲到的计算流程不同的是，Mapping 阶段和 Reducing 阶段中间多了一步 Shuffling 操作。Shuffling 阶段主要是读取 Mapping 阶段的结果，并将不同的结果划分到不同的区。在大多数参考文档中，Mapping 和 Reducing 阶段的任务分别定义为映射以及归约。但是，在映射之后，要对映射后的结果进行排序整合，然后才能执行归约操作，因此往往将这一排序整合的操作单独放出来，称之为 Shuffling 阶段。

Reducing 阶段，归并同一个品牌的购买次数。

得到苏锡常地区第二季度 Top3 品牌手机的购买记录。

由上述流程可以看出，Map/Reduce 作业和 map()/reduce() 函数是有区别的：

Map 阶段由一定数量的 Map 作业组成，这些 Map 作业是并发任务，可以同时运行，且操作重复。Map 阶段的功能主要由 map() 函数实现。每个 Map 作业处理一个子任务（比如一个城市的手机消费统计），需要调用多次 map() 函数来处理（因为城市内不同的居民倾向于不同的手机）。

Reduce 阶段执行的是汇总任务结果，遍历 Map 阶段的结果从而返回一个综合结果。与 Reduce 阶段相关的是 reduce() 函数，它的输入是一个键（key）和与之对应的一组数据（values），其功能是将具有相同 key 值的数据进行合并。Reduce 作业处理一个分区的中间键值对，期间要对每个不同的 key 值调用一次 reduce() 函数。在完成 Map 作业后，每个分区中会存在多个临时文件；而执行完 Reduce 操作后，一个分区最终只有一个输出文件。

知识扩展：Fork-Join 计算模式是什么意思呢？

MapReduce 是一种分而治之的计算模式，在分布式领域中，除了典型的 Hadoop 的 MapReduce(Google MapReduce 的开源实现)，还有 Fork-Join。你知道 Fork-join 是什么吗？

Fork-Join 是 Java 等语言或库提供的原生多线程并行处理框架，采用线程级的分而治之计算模式。它充分利用多核 CPU 的优势，以递归的方式把一个任务拆分成多个“小任务”，把多个“小任务”放到多个处理器上并行执行，即 Fork 操作。当多个“小任务”执行完成之后，再将这些执行结果合并起来即可得到原始任务的结果，即 Join 操作。

虽然 MapReduce 是进程级的分而治之计算模式，但与 Fork-Join 的核心思想是一致的。因此，Fork-Join 又被称为 Java 版的 MapReduce 框架。

但，MapReduce 和 Fork-Join 之间有一个本质的区别：

Fork-Join 不能大规模扩展，只适用于在单个 Java 虚拟机上运行，多个小任务虽然运行在不同的处理器上，但可以相互通信，甚至一个线程可以“窃取”其他线程上的子任务。

MapReduce 可以大规模扩展，适用于大型计算机集群。通过 MapReduce 拆分后的任务，可以跨多个计算机去执行，且各个小任务之间不会相互通信。

总结

所谓分而治之，就是将一个复杂的、难以直接解决的大问题，分割成一些规模较小的、可以直接求解的子问题，这些子问题互相独立且与原问题形式相同，递归地解这些子问题，然后将子问题的解合并以后就是原问题的解。

分布式计算模型 MapReduce 就运用了分而治之的思想，通过 Map 操作将大任务分成多个较小的任务去执行，得到的多个结果再通过 Reduce 操作整合成一个完整的结果。所以，今天我就以 MapReduce 为例，与你讲述了分布式领域中分治法的模型、原理与应用。

最后，我将今天涉及的核心知识点梳理为了一张思维导图，以方便你理解与记忆。

分而治之的思想，是简单且实用的处理复杂问题的方法。所以无论是计算机领域还是其他研究领域亦或日常生活中，我们都可以用分治法去处理很多复杂庞大的问题，将大问题划分成多个小问题，化繁为简、化整为零。

其实，很多算法并不是凭空创造出来的，都是源于生活并服务于生活的。在日常工作学习中，我们对眼前的问题一筹莫展时，就可以将其化繁为简，从最简单的小问题出发，逐渐增加问题的规模，进而解决这个复杂的问题。同样的道理，我们也可以借鉴生活中的例子去解决专业问题。

思考题

MapReduce 属于批量处理任务类型吗？你能说说其中的原因吗？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

16 | 分布式计算模式之Stream：一门背锅的艺术

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章中，我与你介绍了分布式计算模式中的 MapReduce 模式。这种模式的核心思想是，将大任务拆分成多个小任务，针对这些小任务分别计算后，再合并各小任务的结果以得到大任务的计算结果。

这种模式下任务运行完成之后，整个任务进程就结束了，属于短任务模式。但，任务进程的启动和停止是一件很耗时的事儿，因此 MapReduce 对处理实时性的任务就不太合适了。

实时性任务主要是针对流数据的处理，对处理时延要求很高，通常需要有常驻服务进程，等待数据的随时到来随时处理，以保证低时延。处理流数据任务的计算模式，在分布式领域中叫作 Stream。

今天，我将针对流数据的处理展开分享，和你一起打卡 Stream 这种计算模式。

什么是 Stream？

近年来，由于网络监控、传感监测、AR/VR 等实时性应用的兴起，一类需要处理流数据的业务发展了起来。比如各种直播平台中，我们需要处理直播产生的音视频数据流等。这种如流水般持续涌现，且需要实时处理的数据，我们称之为流数据。

总结来讲，流数据的特征主要包括以下 4 点：

数据如流水般持续、快速地到达；

海量数据规模，数据量可达到 TB 级甚至 PB 级；

对实时性要求高，随着时间流逝，数据的价值会大幅降低；

数据顺序无法保证，也就是说系统无法控制将要处理的数据元素的顺序。

在分布式领域中，处理流数据的计算模式，就是流计算，也叫作 Stream。这个名字是不是非常形象呢？

流计算的职责是实时获取来自不同数据源的海量数据，进行实时分析处理，获得有价值的信息。

它是一个对实时性要求非常高的计算形式，如果数据处理不及时，很容易导致过时、没用的结果，这时就需要对造成的后果进行“背锅”。从这个角度来说，Stream 可谓“一门背锅的艺术”。

类比于水流的持续不断且变幻莫测，流数据也是以大量、快速、时变的流形式持续在应用中产生，因此流计算一般用于处理数据密集型应用。

比如，百度、淘宝等大型网站中，每天都会产生大量的流数据，这些数据包括用户的搜索内容、用户的浏览记录等。实时采集用户数据，并通过流计算进行实时数据分析，可以了解每个时刻数据流的变化情况，甚至可以分析用户的实时浏览轨迹，从而进行个性化内容实时推荐，提高用户体验。

此外，我们常用的爱奇艺、腾讯等音视频平台，对电影、电视剧等数据的处理，也是采用了流计算模式。

那么，这种实时的流计算到底是如何运行的呢？接下来，我们就一起看看流计算的工作原理吧。

Stream 工作原理

我在上一篇文章中与你介绍的 MapReduce，是一种批量计算的形式。这种模式下，会先收集数据并将其缓存起来，等到缓存写满时才开始处理数据。因此，批量计算的一个缺点就是，从数据采集到得到计算结果之间经历的时间很长。

而流计算强调的是实时性，数据一旦产生就会被立即处理，当一条数据被处理完成后，会序列化存储到缓存中，然后立刻通过网络传输到下一个节点，由下一个节点继续处理，而不是像 MapReduce 那样，等到缓存写满才开始处理、传输。为了保证数据的实时性，在流计算中，不会存储任何数据，就像水流一样滚滚向前。

所以说，流计算属于持续性、低时延、事件驱动型的计算作业。

从这些分析中可以看出，使用流计算进行数据处理，一般包括 3 个步骤，如下图所示。

第一步，提交流式计算作业。流式计算作业是一种常驻计算服务，比如实时交通监测服务、实时天气预报服务等。对于流式计算作业，首先必须预先定义计算逻辑，并提交到流计算系统中，使得流计算系统知道自己该如何处理数据。

系统在整个运行期间，由于收集的是同一类型的数据、执行的是同一种服务，因此流式计算作业的处理逻辑不可更改。如果用户停止当前作业运行后再次提交作业，由于流计算不提供数据存储服务，因此之前已经计算完成的数据无法重新再次计算。

第二步，加载流式数据进行流计算。流式计算作业一旦启动将一直处于等待事件触发的状态，一旦有小批量数据进入流式数据存储，系统会立刻执行计算逻辑并迅速得到结果。

从上图中我们可以看出，在流计算系统中，有多个流处理节点，流处理节点会对数据进行预定义的处理操作，并在处理完后按照某种规则转发给后续节点继续处理。此外，流计算系统中还存在管理节点，主要负责管理处理节点以及数据的流动规则。其中，处理节点的个数以及数据转发的规则，都在第一步作业提交时定义。

第三步，持续输出计算结果。流式计算作业在得到小批量数据的计算结果后，可以立刻将结果数据写入在线 / 批量系统，无需等待整体数据的计算结果，以进一步做到实时计算结果的实时展现。

到这里，我们小结一下吧。流计算不提供流式数据的存储服务，数据是持续流动的，在计算完成后就会立刻丢弃。流计算适用于需要处理持续到达的流数据、对数据处理有较高实时性要求的场景。为了及时处理流数据，流计算框架必须是低延迟、可扩展、高可靠的。

流计算的应用场景有很多，比如它是网络监控、传感监测、AR/VR、音视频流等实时应用的发展的基础。所以，目前流计算相关的框架和平台也有很多了，主流的划分方式是将其分为如下 3 类：

商业级的流计算平台，比如 IBM 的 InfoSphere Streams 和 TIBCO 的 StreamBase。InfoSphere Streams 支持同时分析多种数据类型并实时执行复杂计算。StreamBase 是一个用于实时分析的软件，可以快速构建分析系统，即时做出决策。StreamBase 可以为投资银行、对冲基金、政府机构等提供实时数据分析服务。

开源流计算框架，典型代表是 Apache Storm（由 Twitter 开源）和 S4（由 Yahoo 开源）。Storm 是一个分布式的、容错的实时计算系统，可以持续进行实时数据流处理，也可以用于分布式 RPC。S4 是一个通用的、分区容错的、可扩展的、可插拔的分布式流式系统。这些开源的分布式流计算系统由于具备开源代码，因此比较适合开发人员将其搭建在自身业务系统中。

各大公司根据自身业务特点而开发的流计算框架，比如 Facebook 的 Puma、百度的 Dstream（旨在处理有向无环的数据流）、淘宝的银河流数据处理平台（一个通用的、低延迟、高吞吐、可复用的流数据实时计算系统）。

除了这些框架外，我们还会经常听到 Spark、Flink 等。Spark 和 Flink 与 Storm 框架的不同之处在于，Spark 和 Flink 除了支持流计算，还支持批量计算，因此我没有直接将它们列入上述的流计算框架中。如果你的业务中需要用到或者需要参考某种计算框架或者平台的话，可以再参考其官方文档或者相关的技术文章。

接下来，我就以 Storm 这个开源的流计算框架为例，通过介绍 Storm 的工作原理，以加深你对流计算模式的进一步理解，进而帮助你将其运用到实际业务中。

Storm 的工作原理

说到 Storm 的工作原理，我们先来对比下 Storm 与 MapReduce 的区别吧。Hadoop 上运行的是“MapReduce 作业”，而 Storm 上运行的是“计算拓扑（Topologies）”。 “作业”和“拓扑”的一个关键区别是：MapReduce 的一个作业在得到结果之后总会结束；而拓扑描述的是计算逻辑，该计算逻辑会永远在集群中运行（除非你杀死该进程）。

如下图所示，Storm 集群上有两种节点，即主节点（Master Node）和工作节点（Worker Nodes）。

主节点上运行着一个名为“Nimbus”的守护进程。 Nimbus 负责为集群分发代码，为工作节点分配任务以及进行故障监控。一个 Storm 集群在工作过程中，只有一个 Nimbus 进程工作。

每个工作节点上都运行着一个名为“Supervisor”的守护进程。 Supervisor 负责监听分配给它所在的机器上的工作，负责接收 Nimbus 分配的任务，并根据需要启动和停止工作进程，其中每个工作进程都执行一个子任务。因此，一个正在运行的拓扑任务，是由分布在许多计算机上的许多工作进程组成。

前面我介绍了 Nimbus 是负责分发任务或代码的，Supervisor 是负责接收任务，并启动和停止工作进程以执行任务的。那么 Nimbus 和 Supervisors 之间，具体是怎么协同的呢？下面我们一起看一下。

如果所有数据和信息均存储在 Master Node 上，Master Node 故障后，会导致整个集群信息丢失，因此引入了 ZooKeeper 集群来加强可靠性。为此 Master Node 与 Worker Node 之间的交互通过 ZooKeeper 完成，由于 Nimbus 和 Supervisors 是 Master Node 和 Worker Node 之间负责交互的进程，因此 Nimbus 和 Supervisors 之间的所有协调都是通过 ZooKeeper 集群完成的，比如 Nimbus 会将任务的分配情况或信息发送给 ZooKeeper 集群，然后 Supervisors 向 ZooKeeper 集群获取任务，并启动工作进程以执行任务。

当 Supervisor 接收到分配的任务后，会启动工作节点的工作进程 (Worker) 去执行任务。我们知道，一个计算任务可以分成任务数据的读取以及任务执行两部分。Worker 提供了两个组件 Spout 和 Bolt，分别进行数据读取和任务执行。

在详细介绍 Worker 组件之前，我首先介绍一下 Storm 的核心抽象：数据流。数据流是一个无界序列，是在分布式环境中并行创建、处理的一组元组（tuple）。数据流可以由一种能够表述数据流中元组的域（fields）的模式来定义。

Storm 为进行数据流转换提供了基本组件 Spout 和 Bolt。 Spout 和 Bolt 有用户自定义的接口，用于运行特定应用程序的逻辑。如下图所示，Storm 上运行的计算拓扑其实是由一系列 Spout 和 Bolt 组成的有向无环图，这个有向无环图代表了计算逻辑。

备注：

图中箭头，表示数据元组的传递方向。

此图引自“Twitter Analysis with Apache Storm”。

接下来，我们看看Spout 和 Bolt 的含义吧。

Spout 用于接收源数据。通常情况下，Spout 会从一个外部的数据源读取数据元组，然后将它们发送到拓扑中。例如，Spout 从 Twitter API 读取推文并将其发布到拓扑中。

Bolt 负责处理输入的数据流，比如数据过滤（filtering）、函数处理（functions）、聚合（aggregations）、联结（joins）、数据库交互等。数据处理后可能输出新的流作为下一个 Bolt 的输入。每个 Bolt 往往只具备单一的计算逻辑。当我们执行简单的数据流转换时，比如仅进行数据过滤，则通常一个 Bolt 可以实现；而复杂的数据流转换通常需要使用多个 Bolt 并通过多个步骤完成，比如在神经网络中，对原始数据进行特征转换，需要经过数据过滤、清洗、聚类、正则化等操作。

知识扩展：流计算和批量计算的区别是什么？

MapReduce 可以说是一种批量计算，与我们今天介绍的用于实时数据处理的流计算，是什么关系呢？

虽然流计算和批量计算属于两种不同的计算模式，但并不是非此即彼的关系，只是适用于不同的计算场景。

在流计算中，数据具有时效性，因此在 5G 以及人工智能应用的驱动下，专注于实时处理的流计算越来越得到广泛的关注。流计算的低延时、易扩展等性能非常适用于对时延要求高的终端应用（比如直播中音视频的处理等），从而极大提高用户的服务体验。而批量计算适用于对时延要求低的任务。

在实际运用中，可以根据计算要求，选择不同的计算模式。我将这两种计算模式的特点，总结为了一张表格，以帮助你理解、记忆，以及选择适合自己业务场景的计算模式。

总结

今天，我与你介绍了分布式计算模式中的流计算。流数据的价值会随时间的流逝而降低，“时间就是金钱”在流计算中体现得淋漓尽致。这就要求流计算框架必须是低延迟、可扩展、高可靠的。

在介绍流计算的工作原理时，我首先通过一个流程图，与你介绍了它的 3 个步骤，即提交流式计算作业、加载流式数据进行流计算和持续输出计算结果。

然后，我以流计算开源框架中的 Storm 为例，与你讲述了 Storm 的核心组件以及通过 Spout 和 Bolt 构建有向无环图代表流计算逻辑，以实现流计算，以加深你对流计算原理的理解。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

思考题

离线计算和批量计算，实时计算和流式计算是等价的吗？你能和我说说你做出判断的原因吗？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

17 | 分布式计算模式之Actor：一门甩锅的艺术

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

我在前两篇文章中，带你一起学习了 MapReduce 和 Stream 计算模式，相信你对批处理和流计算也有了一定的了解。虽然这两种计算模式对数据的处理方式不同，但都是以特定数据类型（分别对应静态数据和动态数据）作为计算维度。

在接下来两篇文章中，我将从计算过程或处理过程的维度，与你介绍另外两种分布式计算模式，即 Actor 和流水线。分布式计算的本质就是在分布式环境下，多个进程协同完成一件复杂的事情，但每个进程各司其职，完成自己的工作后，再交给其他进程去完成其他工作。当然，对于没有依赖的工作，进程间是可以并行执行的。

你是不是想说，分布式进程那么多，如果需要开发者自己去维护每个进程之间的数据、状态等信息，这个开发量可不是一般得大，而且特别容易出错。那么，有没有什么办法可以让开发者只关注自己的逻辑呢？

答案是肯定的，Actor 计算模式就能满足你的需求。也就是说，你可以把数据、状态等都扔给 Actor。这是不是“一门甩锅的艺术”呢？

接下来，我们就一起打卡分布式计算模式中的 Actor 模式。

什么是 Actor？

在第 10 篇文章“分布式体系结构之非集中式结构：众生平等”中，我曾提到 Akka 框架基于 Actor 模型，提供了一个用于构建可扩展的、弹性的、快速响应的应用程序的平台。

其中，Actor 类似于一个“黑盒”对象，封装了自己的状态和行为，使得其他 Actor 无法直接观察到它的状态，调用它的行为。多个 Actor 之间通过消息进行通信，这种消息类似于电子邮箱中的邮件。Actor 接收到消息之后，才会根据消息去执行计算操作。

那么，Actor 模型又是什么呢？Actor 模型，代表一种分布式并行计算模型。这种模型有自己的一套规则，规定了 Actor 的内部计算逻辑，以及多个 Actor 之间的通信规则。在 Actor 模型里，每个 Actor 相当于系统中的一个组件，都是基本的计算单元。

Actor 模型的计算方式与传统面向对象编程模型（Object-Oriented Programming，OOP）类似，一个对象接收到一个方法的调用请求（类似于一个消息），从而去执行该方法。

但是，OOP 因为数据封装在一个对象中，不能被外部访问，当多个外部对象通过方法调用方式，即同步方式进行访问时，会存在死锁、竞争等问题，无法满足分布式系统的高并发性需求。而 Actor 模型通过消息通信，采用的是异步方式，克服了 OOP 的局限性，适用于高并发的分布式系统。

举一个最简单的例子，假如你现在定义了三个对象 A、B 和 C，对象 C 中有一个函数 Function，现在对象 A 和对象 B 同时调用对象 C 中的 Function，此时对象 C 中的 Function 就成为了我们在第 3 篇文章“分布式互斥：有你没我，有我没你”中提到的共享资源，有可能会存在竞争、死锁等问题。

而对于 Actor 模式，对象 A、B 和 C 对应着 Actor A、Actor B 和 Actor C，当 Actor A 和 Actor B 需要执行 Actor C 中的 Function 逻辑时，Actor A 和 Actor B 会将消息发送给 Actor C， Actor C 的消息队列存储着 Actor A 和 Actor B 的消息，然后根据消息的先后顺序，执行 Function 即可。

也就是说，Actor 模式采用了异步模式，并且每个 Actor 封装了自己的数据、方法等，解决了 OOP 存在的死锁、竞争等问题。

Actor 计算模式

接下来，我们再一起看看 Actor 计算模式吧。如下图所示，描述了具有 3 个 Actor 的 Actor 模型。

可以看到，Actor 模型的三要素是状态、行为和消息，有一个很流行的等式：Actor 模型 =（状态 + 行为）+ 消息。

接下来，我们一起看看这三要素的具体含义吧。

状态（State）。Actor 的状态指的是，Actor 组件本身的信息，相当于 OOP 对象中的属性。Actor 的状态会受 Actor 自身行为的影响，且只能被自己修改。

行为（Behavior）。Actor 的行为指的是，Actor 的计算处理操作，相当于 OOP 对象中的成员函数。Actor 之间不能直接调用其他 Actor 的计算逻辑。Actor 只有收到消息才会触发自身的计算行为。

消息（Mail）。Actor 的消息以邮件形式在多个 Actor 之间通信传递，每个 Actor 会有一个自己的邮箱（MailBox），用于接收来自其他 Actor 的消息，因此 Actor 模型中的消息也称为邮件。一般情况下，对于邮箱里面的消息，Actor 是按照消息达到的先后顺序（FIFO）进行读取和处理的。

了解了 Actor 的三要素后，我们再一起看下 Actor 的工作原理吧。

Actor 工作原理

为了方便你理解 Actor 的工作原理，我会通过讲述 3 个 Actor 之间基于消息和消息队列的工作流程进行说明。这 3 个 Actor 的工作流程，如下所示。

Actor1 和 Actor3 先后向 Actor2 发送消息，消息被依次放入 Actor2 的 MailBox 队列的队尾 ;

Actor2 从 MailBox 队列的队首依次取出消息执行相应的操作，由于 Actor1 先把消息发送给 Actor2，因此 Actor2 先处理 Actor1 的消息；

Actor2 处理完 Actor1 的消息后，更新内部状态，并且向其他 Actor 发送消息，然后处理 Actor3 发送的消息。

了解了 Actor 之间的消息交互和处理流程，我再以一个具体案例和你详细解读一下 Actor 之间的消息传递过程吧。

我们已经知道，在系统中，不同的组件 / 模块可以视为不同的 Actor。现在有一个执行神经网络的应用，其中有两个组件 A 和 B，分别表示数据处理模块和模型训练模块。假设，我们可以将组件 A 和 B 看作两个 Actor，训练过程中的数据可以通过消息进行传递。如上图所示，完整的消息传输过程为：

组件 A 创建一个 Actor System，用来创建并管理多个 Actor。

组件 A 产生 QuoteRequest 消息（即 mail 消息，比如数据处理后的数据），并将其发送给 ActorRef。ActorRef 是 Actor System 创建的组件 B 对应 Actor 的一个代理。

ActorRef 将消息（经过数据处理后的数据）传输给 Message Dispatcher 模块。Message Dispatcher 类似于快递的中转站，负责接收和转发消息。

Message Dispatcher 将消息（数据处理后的数据）加入组件 B 的 MailBox 队列的队尾。

Message Dispatcher 将 MailBox 加入线程。需要注意的是，只有当 MailBox 是线程时，才能处理 MailBox 中的消息。

组件 B 的 MailBox 将队首消息（数据）取出并删除，队首消息交给组件 B 处理，进行模型训练。

Actor 关键特征

通过上面的描述，可以看出 Actor 的通信机制与日常的邮件通信非常类似。因此，我们可以进一步总结出 Actor 模型的一些特点：

实现了更高级的抽象。我在前面提到过，Actor 与 OOP 对象类似，封装了状态和行为。但是，Actor 之间是异步通信的，多个 Actor 可以独立运行且不会被干扰，解决了 OOP 存在的竞争问题。

非阻塞性。在 Actor 模型中，Actor 之间是异步通信的，所以当一个 Actor 发送信息给另外一个 Actor 之后，无需等待响应，发送完信息之后可以在本地继续运行其他任务。也就是说，Actor 模型通过引入消息传递机制，从而避免了阻塞。

无需使用锁。Actor 从 MailBox 中一次只能读取一个消息，也就是说，Actor 内部只能同时处理一个消息，是一个天然的互斥锁，所以无需额外对代码加锁。

并发度高。每个 Actor 只需处理本地 MailBox 的消息，因此多个 Actor 可以并行地工作，从而提高整个分布式系统的并行处理能力。

易扩展。每个 Actor 都可以创建多个 Actor，从而减轻单个 Actor 的工作负载。当本地 Actor 处理不过来的时候，可以在远程节点上启动 Actor 然后转发消息过去。

虽然 Actor 模型有上述的诸多优点，但它并不适用于分布式领域中所有的应用平台或计算框架。因为，Actor 模型还存在如下一些不足之处：

Actor 提供了模块和封装，但缺少继承和分层，这使得即使多个 Actor 之间有公共逻辑或代码部分，都必须在每个 Actor 中重写这部分代码，也就是说重用性小，业务逻辑的改变会导致整体代码的重写。

Actor 可以动态创建多个 Actor，使得整个 Actor 模型的行为不断变化，因此在工程中不易实现 Actor 模型。此外，增加 Actor 的同时，也会增加系统开销。

Actor 模型不适用于对消息处理顺序有严格要求的系统。因为在 Actor 模型中，消息均为异步消息，无法确定每个消息的执行顺序。虽然可以通过阻塞 Actor 去解决顺序问题，但显然，会严重影响 Actor 模型的任务处理效率。

尽管 Actor 模型在需要同步处理的应用等场景具有局限性，但它在异步场景中应用还是比较广泛的。接下来，我们就一起看看 Actor 目前都应用在哪些地方吧。

Actor 模型的应用

Actor 模型在 1973 年被提出，已广泛应用在多种框架和语言中。可以说，很多框架或语言支持 Actor 编程模型，是为了给开发者提供一个通用的编程框架，让用户可以聚焦到自己的业务逻辑上，而不用像面向对象等编程模型那样需要关心死锁、竞争等问题。

那么，到底有哪些框架或语言支持 Actor 编程模型呢？将下来，我就和你列举几个典型的框架或语言吧，以方便你参考。

Erlang/OTP。Erlang 是一种通用的、面向并发的编程语言，使用 Erlang 编写分布式应用比较简单，而 OTP 就是 Erlang 技术栈中的标准库。Actor 模型在 Erlang 语言中得到广泛支持和应用，其他语言的 Actor 逻辑实现在一定程度上都是参照了 Erlang 的模式。实现了 Actor 模型逻辑的 Erlang/OTP，可以用于构建一个开发和运行时环境，从而实现分布式、实时的、高可用性的系统。

Akka。Akka 是一个为 Java 和 Scala 构建高度并发、分布式和弹性的消息驱动应用程序的工具包。Akka 框架基于 Actor 模型，提供了一个用于构建可扩展的、弹性的、快速响应的应用程序的平台。通过使用 Actors 和 Streams 技术， Akka 为用户提供了多个服务器，使用户更有效地使用服务器资源并构建可扩展的系统。

Quasar (Java) 。Quasar 是一个开源的 JVM 库，极大地简化了高度并发软件的创建。Quasar 在线程实现时，参考了 Actor 模型，采用异步编程逻辑，从而为 JVM 提供了高性能、轻量级的线程，可以用在 Java 和 Kotlin 编程语言中。

知识扩展：Akka 中 Actor 之间的通信可靠性是通过 Akka 集群来保证的，那么 Akka 集群是如何检测节点故障的呢？

在第 10 篇文章“分布式体系结构之非集中式结构：众生平等”中，我与你介绍了 Akka 集群是一个去中心化的架构，比如现在集群中有 n 个节点，这 n 个节点之间的关系是对等的。节点之间采用心跳的方式判断该节点是否故障，但未采用集中式架构中的心跳检测方法。

Akka 集群中的故障检测方法是，集群中每个节点被 k 个节点通过心跳进行监控，比如 k = 3，节点 1 被节点 2、节点 3 和节点 4 通过心跳监控，当节点 2 发现节点 1 心跳不可达时，就会标记节点 1 为不可达（unreachable），并且将节点 1 为不可达的信息通过 Gossip 传递给集群中的其他节点，这样集群中所有节点均可知道节点 1 不可达。

其中，k 个节点的选择方式是，将集群中每个节点计算一个哈希值，然后基于哈希值，将所有节点组成一个哈希环（比如，从小到大的顺序），最后根据哈希环，针对每个节点逆时针或顺时针选择 k 个临近节点作为监控节点。

总结

接下来，我们小结一下吧。今天，我与你介绍了分布式计算中，一门甩锅的计算模型，即 Actor 模型。

首先，我介绍了什么是 Actor 模型以及 Actor 模型的三要素，包括状态、行为和消息。

其次，我介绍了 Actor 的工作原理，并通过实例介绍了 Actor 之间通过消息及消息队列进行异步通信的流程，以便于你进一步理解 Actor 的工作原理。

最后，我为你介绍了几个当前支持 Actor 编程模型的框架和语言，以便于你在需要采用 Actor 模型编程时做一个参考。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

著名的 Erlang 并发编程语言，以及 Akka 这一分布式计算框架都实现了 Actor 模型的计算逻辑。因此，即使你在之前未曾接触过 Actor 模型，学习了这篇文章后，你也可以根据开源的 Erlang 或 Akka 项目，去更深刻地理解 Actor 模型了，加油！

思考题

Actor 是否可以采用阻塞方式去运行呢，原因是什么呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

18 | 分布式计算模式之流水线：你方唱罢我登场

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

通过前面几篇文章，我们一起学习了分布式计算模式中的 MapReduce、Stream 和 Actor，它们各显神通解决了很多实际问题。

但是，在现实生活中，经常还会出现这样的情况，前一个任务的结果是另外一个任务的输入。比如工厂生产一瓶饮料，首先需要往瓶子里装上饮料，待饮料装满后，再封口。如果装饮料和封口分别为子任务，那么前一个任务（装饮料）结束后才可以开始第二个任务（封口）。类似这样的作业，就是我们常说的流水线作业。

在分布式领域中解决类似具有依赖关系的流水线作业的计算模式，叫作流水线计算模式。其实，流水线计算模式是我们在第 1 篇文章中提到的数据并行计算的一种形式，就是将一个任务拆分为多个步骤（子任务），然后多个这样的任务通过对步骤（子任务）的重叠执行，以实现数据并行处理的场景。

这种流水线模式在计算机领域中最先用于 CPU 指令设计，后来推广到机器学习领域进行数据处理、模型训练等。在流水线计算模式中，由于前一个子任务执行后，会扔给下一个子任务，由下一个子任务去展现自己的能力，因此可以形象地比喻为“你方唱罢我登场”。

接下来，我们就一起打卡分布式计算模式中的流水线模式吧。

什么是流水线模式？

其实，分布式领域的流水线计算模式，就是参考了工业生产中的流水作业模式，将一个任务分为多个步骤执行，使得不同任务可以并行执行。此外，你肯定还会想到计算机技术中的流水线计算吧。

计算机中的流水线（Pipeline）技术是一种将每条指令拆分为多个步骤，多条指令的不同步骤重叠操作，从而实现几条指令并行处理的技术。现代 CPU 指令采用了流水线设计，将一条 CPU 指令分为取指（IF）、译码（ID）、执行（EX）、访存（MEM）、回写（WB）五级流水线来执行。

如下图所示，在第一条指令执行译码操作时，第二条指令就可以执行取指操作了，从而实现了多条指令的并行操作。

在分布式领域中，流水线计算模式也类似，它是将一个大任务拆分为多个步骤执行，不同的步骤可以采用不同的进程执行。这，使得不同任务可以并行执行，从而提高了系统效率。

以机器学习中的数据预处理为例，假设现在有 5 个样本数据，每个样本数据进行数据预处理的流程，包括数据去重、数据缺失值处理、数据归一化 3 个步骤，且需要按照顺序执行。也就是说，数据预处理这个任务可拆分为数据去重—> 数据缺失值处理—> 数据归一化 3 个子任务。

如果现在有 3 个节点，节点 1 执行数据去重，节点 2 执行数据缺失值处理，节点 3 执行数据归一化。那么，节点 1 处理完样本 1 的数据，将处理后的数据发送节点 2 后，则节点 1 可以继续处理样本 2 的数据，同时节点 2 处理样本 1 的数据，以此类推，就实现了多任务的并行执行。

接下来，我们再具体看看分布式领域中的流水线计算模式吧。

流水线计算模式

流水线计算模式的应用非常广泛，在 AI 技术中也非常常见。对流水线计算模式的学习，将有助于你学习 AI 技术，因此我接下来会以机器学习为例，为你介绍流水线计算模式。

当然，流水线计算模式的原理是通用的，也可以应用到其他领域，比如通信领域中使用 HTTP 流水线传输、计算机图形学中的图流水线等。

随着神经网络、深度学习在全世界掀起了 All in AI 的热潮，用于加速的 GPU 和 TPU 也被越来越多的人使用。虽然诸如 GPU、TPU 之类的加速器可以从根本上减少执行单个训练步骤所需的时间，但为了达到最佳性能，我们仍然需要高效的输入流水线机制。

比如，在流水线模式中数据预处理与 GPU/TPU 进行模型训练可以重叠进行；再比如，第 N 个样本进行模型训练时，第 N+1 个样本可以进行数据预处理，也就是说在第 N+1 个样本进行预处理前，已经将第 N 个样本处理后的数据提供给了模型训练，进一步减少了整体的数据处理和模型训练时间。

Tensorflow 是 Google 开源的一个分布式机器学习框架，已被各大公司采用，比如网易、eBay、Intel 等公司。接下来，我就以 TensorFlow 的输入流水线模式为例，与你介绍流水线技术模式的原理，并带你了解如何构建机器学习的流水线。

流水线计算模式的原理

TensorFlow 运用了流水线模式对输入数据进行预处理，因此称为输入流水线（TensorFlow Training Input Pipelines）。其数据输入流水线主要包含 3 个步骤：

提取（Extract）。通过多种途径读取数据，比如内存、本地的 HDD 或 SSD、远程的 HDFS、GCS 等。数据的种类也有很多，比如图像数据、文本数据、视频数据等。

转换（Transform）。使用 CPU 处理器对输入的数据进行解析以及预处理操作，包括混合重排（shuffling）、批处理（batching）, 以及一些特定的转换。比如图像解压缩和扩充、文本矢量化、视频时序采样等。

加载（Load）。将转换后的数据加载到执行机器学习模型的加速器设备上，比如 GPU 或 TPU。

由于输入流水线包含了提取、转换、加载 3 个步骤，因此 TensorFlow 的数据输入流水线也称为 ETL 流水线。TensorFlow 提供了一个官方 API 也就是 tf.data，利用简单、可重用的数据片段构建复杂的输入流水线。

没错，在加速模型训练方面，输入流水线是非常重要的一个模块。由上述流程可知，要执行训练步骤，首先需要提取并使用 CPU 转换数据，然后将其提供给在加速器上运行的模型。

如果不引入流水线模型的话，当 CPU 正在预处理数据时，加速器处于空闲状态。同样，当 GPU/TPU 正在训练模型时，CPU 处于空闲状态。因此，训练的用时是 CPU 预处理时间和加速器训练时间的总和。

为了帮助你理解，我们一起看下TensorFlow 官网给出的一个示例吧。这个例子展示了一个不使用流水线技术和使用流水线技术时，CPU、GPU/TPU 的训练过程对比。

我们先看看不使用流水线技术的训练过程。如下图所示，Prepare 1 表示 CPU 正在对第 1 个样本数据进行预处理操作，Train 1 表示 GPU/TPU 正在训练第 1 个样本数据。

备注：图片来源为www.tensorflow.org/guide。

图中的“idle”指的是空闲时间。可以看出，如果不使用流水线，CPU 和 GPU/TPU 运作的时间没有重叠，因此在大部分时间都可能处于空闲状态。

接下来，我们再看看使用流水线技术的训练过程。流水线模型可以将训练步骤的数据预处理和数据训练过程重叠到一起。比如，当 GPU/TPU 正在训练第 N 个样本数据时，CPU 可以预处理第 N+1 个样本数据。这样做不仅可以最大限度地缩短训练的单步用时，还可以缩短提取和转换数据所需的时间，如下图所示：

图片来源：www.tensorflow.org/guide

很明显，采用流水线的设计可以充分利用 CPU 和 GPU/TPU，从而避免资源闲置，加速训练过程。

到这里，我们来小结一下吧。

TensorFlow 的输入流水线模式将对数据的操作拆分为提取、转换、加载 3 个不重叠的部分。当 CPU 对第 N 个样本的数据完成预处理之后，会将预处理后的数据发送给 GPU/TPU，然后 CPU 继续对第 N+1 个样本的数据进行预处理，同时 GPU/TPU 对第 N 个样本数据进行模型训练。也就是说，这种计算模式实现了多样本数据处理和模型训练的并行执行。

可以看出，在模型训练中引入流水线模式，可以提高 CPU、GPU/TPU 的利用率，还可以加速训练过程。

实践: 构建机器学习流水线

前面提到在 TensorFlow 中，流水线模式主要运用在数据读取阶段。那么，对于一个复杂的机器学习任务，是否也可以构建一套流水线作业呢？

答案是肯定的。接下来，我们就一起看看，如何构建机器学习流水线。

一个典型的机器学习训练模型按照流水线计算模式拆分，可以包括如下所示的 5 个步骤：

数据输入，指的是从不同的数据源中导入数据。

数据转换，主要是要把输入的无结构数据转换成合适的格式，以便特征提取。

特征提取，指的是从数据集中提取特征数据。

模型训练，包括提供一个算法，并提供一些训练数据让模型可以学习。学习算法会从训练数据中发现模型，并生成输出模型。

模型验证，指的是通过训练得到的结果，对模型进行错误率验证。比如，图像分类中分类结果的验证，预测中的准确度验证，从而提高模型的准确性。

值得注意的是，在数据输入和数据转换之间，有时需要进行数据清洗。数据清洗主要是剔除错误数据和不重要的数据，从而降低模型训练的错误率。

接下来，我以图像分类为例，带你了解机器学习流水线的流程。关于图像分类的详细知识点，你可以自行查阅相关资料。

如下图所示，假如现在有 10000 张小狗照片，需要训练出一个关于小狗的预测模型。

假设这 10000 张照片中，8000 张作为训练集，2000 张作为测试集，采用 CNN 进行模型训练。CNN 包括输入层、卷积层、池化层、全连接层，其中输入层为数据输入，卷积层和池化层为特征提取，全连接层是连接所有特征，输出数据到分类器中，以得到训练结果。

如上图所示，生成小狗预测模型的流水线可以分为数据输入、数据转换、特征提取、模型训练、模型验证 5 部分。具体流程如下：

输入数据，也就是输入图像数据，即 8000 张图片，其中图像以像素表示。比如，图片的大小是 480480，那么图像输入数据格式可以是 480480*3 的数组。3 代表的是，RGB 的维度。

数据转换，也就是对输入的图像数据进行解析、正则化处理，消除一些噪声数据，得到格式化的数据。

特征提取，指的是得到格式化的数据之后，就可以对输入图像进行特征提取，通过卷积操作提取小狗的一些轮廓特征，比如耳朵、尾巴、身体等，然后通过池化层识别出主要特征，比如小狗的耳朵、眼睛、舌头等，对特征进行精简。

模型训练。在 CNN 中模型训练其实和特征提取是相辅相成的，也就是特征提取后，实现特征提取的那些参数就是模型参数，而训练过程，会根据梯度下降法等对参数进行调整，以使得在模型验证阶段预测结果逼近真实结果。也就是说，特征提取和模型训练这两步，在 CNN 中是放到一起的，这里我为了方便你理解，才显式地把这两步划分了出来。

模型验证。将带有标签的测试数据集的图像（2000 张）输入到小狗预测模型，将预测结果与实际结果进行对比，如果误差比较大，则对模型参数进行优化并进入下一次迭代训练；如果误差较小，那么得到的结果就是最终的小狗预测模型。

知识扩展：流水线模式和 MapReduce 模式中，都有将大任务拆分为多个子任务，两者的区别是什么？

如题目所述，流水线计算模式与分而治之的 MapReduce 计算模式（你可以再回顾下第 15 篇文章中的相关知识点）有相似之处，都是将一个完整的、大的任务进行划分，但它们划分的模式不一样：

MapReduce 以任务为粒度，将大的任务划分成多个小任务，每个任务都需要执行完整的、相同的步骤，同一任务能被并行执行，可以说是任务并行的一种计算模式；

而流水线计算模式以步骤为粒度，一个任务拆分为多个步骤，每个步骤执行的是不同的逻辑，多个同类型任务通过步骤重叠以实现不同任务的并行计算，可说是数据并行的一种模式。

此外，它们的子任务（步骤）间的关系不同：

在 MapReduce 中，各个子任务可以独立执行，互不干扰，多个子任务执行完后，进行结果合并得到整个任务的结果，因此要求子任务之间是没有依赖关系的；

而在流水线模式中，多个子任务之间是具有依赖关系的，前一个子任务的输出是后一个子任务的输入。

所以，综合来讲，MapReduce 计算模式适合任务并行的场景，而流水线计算模式适合同类型任务数据并行处理的场景。

总结

首先，我与你介绍了什么是分布式计算模式中的流水线模式。它参考了工业生产中的流水作业模式，将一个任务分为多个步骤执行，不同任务之间的步骤可以重叠执行，这使得多个不同任务可以并行执行。

然后，我以典型的机器学习流程为例，介绍了机器学习流水线处理流程，以加深你对分布式流水线计算模型的理解。

最后，我以 CNN 进行小狗分类模型训练为例，通过讲述数据输入、数据处理、特征提取（卷积、池化等操作）、模型训练、模型验证等过程，带你进一步理解了流水线计算模式在实际应用中的原理。

现在，我再通过一张思维导图来归纳一下今天的核心知识点吧。

流水线计算模式适合同类型任务，且每个任务可以拆分为多个步骤（子任务）进行执行的场景，通过重叠执行多个不同任务间的不同步骤实现数据并行。在实际应用场景中，有很多例子，最常见的就是机器学习。相信你在理解了本文的计算原理之后，一定可以将这种研究方法运用在你的工作中，加油！

思考题

流水线计算模式和流计算的区别是什么？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

19 | 分布式通信之远程调用：我是你的千里眼

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在前面三个模块中，我带你学习了分布式领域中的分布式协调与同步、分布式资源管理与负载调度，以及分布式计算技术，相信你对分布式技术已经有了一定的了解。

通过前面的学习，不知道你有没有发现分布式的本质就是多进程协作，共同完成任务。要协作，自然免不了通信。那么，多个进程之间是如何通信的呢？这也就是在“第四站：分布式通信技术”模块中，我将要为你讲解的问题。

话不多说，接下来我们就一起进入分布式通信的世界吧。今天，我首先带你打卡的是，分布式通信中的远程调用。

什么是远程调用？

首先，我通过一个例子，来让你对远程调用和本地调用有一个直观了解。

以电商购物平台为例，每一笔交易都涉及订单系统、支付系统及库存系统，假设三个系统分别部署在三台机器 A、B、C 中独立运行，订单交易流程如下所示：

用户下单时，调用本地（机器 A）的订单系统进行下单；

下单完成后，会远程调用机器 B 上的支付系统进行支付，待支付完成后返回结果，之后在本地更新订单状态；

在本地远程调用机器 C 上的仓库系统出货，出货完成后返回出货结果。

在整个过程中，“下单”和“订单状态更新”两个操作属于本地调用，而“支付”和“出货”这两个操作是通过本地的订单系统调用其他两个机器上的函数（方法）实现的，属于远程调用。

整个订单交易流程如下图所示。

通过这个例子，你应该对本地调用和远程调用有了一个初步的认识了。那到底什么是本地调用，什么是远程调用呢？

本地调用通常指的是，进程内函数之间的相互调用；而远程调用，是进程间函数的相互调用，是进程间通信 IPC（Inter-Process Communication）的一种方式。通过远程调用，一个进程可以看到其他进程的函数、方法等，这是不是与我们通常所说的“千里眼”有点类似呢？

在分布式领域中，一个系统由很多服务组成，不同的服务由各自的进程单独负责。因此，远程调用在分布式通信中尤为重要。

根据进程是否部署在同一台机器上，远程调用可以分为如下两类：

本地过程调用（Local Procedure Call，LPC），是指运行在同一台机器上的进程之间的互相通信，即在多进程操作系统中，运行的不同进程之间可以通过 LPC 进行函数调用。

远程过程调用（Remote Procedure Call，RPC），是指不同机器中运行的进程之间的相互通信，某一机器上运行的进程在不知道底层通信细节的情况下，就像访问本地服务一样，去调用远程机器上的服务。

在这两种远程调用中，RPC 中的不同进程是跨机器的，适用于分布式场景。因此，在今天这篇文章中，我主要针对 RPC 进行详细讲解。接下来，我再提到远程调用时，主要指的就是 RPC 了。

远程调用的原理及应用

我们经常会听别人提起 B/S ( Browser/Server，浏览器 / 服务器) 架构。在这种架构中，被调用方（服务器）有一个开放的接口，然后调用方（用户）通过 Browser 使用这个接口，来间接调用被调用方相应的服务，从而实现远程调用。

比如，用户 A 在自己的电脑上通过浏览器查询北京今天的天气， 浏览器会将用户查询请求通过远程调用方式调用远程服务器相应的服务，然后为用户返回北京今天的天气预报。

但是，B/S 架构是基于 HTTP 协议实现的，每次调用接口时，都需要先进行 HTTP 请求。这样既繁琐又浪费时间，不适用于有低时延要求的大规模分布式系统，所以远程调用的实现大多采用更底层的网络通信协议。

接下来，我将为你介绍两种常用的远程调用机制：远程过程调用 RPC(Remote Procedure Call) 和远程方法调用 RMI(Remote Method Invocation)。

首先，我们一起看一下 RPC 的原理和应用吧。

RPC 的原理及应用

简单地说，RPC 就是调用方采用参数传递的方式，通过调用本机器上的一个函数或方法，去执行远程机器上的函数或方法（可以统称为服务），并返回结果。在整个过程中，RPC 会隐藏具体的通信细节。

如下图所示，我们以刚才电商购物平台例子中的“支付”操作为例，来详细看看一次 RPC 调用的完整流程吧：

本地服务器也就是机器 A 中的订单系统，调用本地服务器上的支付系统中的支付操作 Pay(Order)，该方法会直接调用 Client Stub（其中，Stub 是用于转换 RPC 过程中在订单系统和支付系统所在机器之间传递的参数），这是一次正常的本地调用。

Client Stub 将方法 Pay、参数 Order 等打包成一个适合网络传输的消息，通过执行一次系统调用（也就是调用操作系统中的函数）来发送消息。

订单系统所在机器 A 的本地操作系统通过底层网络通信，将打包好的消息根据支付系统所在机器 B 的地址发送出去。

机器 B 上的操作系统接收到消息后，将消息传递给 Server Stub。

机器 B 上的 Server Stub 将接收到的消息进行解包，获得里面的参数，然后调用本地的支付订单的操作 Pay(Order)。

机器 B 上的支付操作 Pay(Order) 完成后，将结果发送给 Server Stub，其中结果可使用 XDR（External Data Representation，一种可以在不同计算机系统间传输的数据格式）语言表示。

机器 B 上的 Server Stub 将结果数据打包成适合网络传输的消息，然后进行一次系统调用发送消息。

机器 B 的本地操作系统通过底层网络将打包好的消息发送回机器 A。

机器 A 的操作系统接收到来自机器 B 的消息，并将消息发送给本地的 Client Stub。

本地的 Client Stub 将消息解包，然后将解包得到的结果返回给本地的订单系统。

到此，整个 RPC 过程结束。

从整个流程可以看出，机器 A 上的 Pay(Order)、 Client Stub 和网络调用之间的交互属于本地调用，机器 B 上的 Pay(Order)、Server Stub 和网络调用之间的交互也属于本地调用。而机器 A 和机器 B 之间的远程调用的核心是，发生在机器 A 上的网络调用和机器 B 上的网络调用。

RPC 的目的，其实就是要将第 2 到第 8 步的几个过程封装起来，让用户看不到这些细节。从用户的角度看，订单系统的进程只是做了一次普通的本地调用，然后就得到了结果。

也就是说，订单系统进程并不需要知道底层是如何传输的，在用户眼里，远程过程调用和调用一次本地服务没什么不同。这，就是 RPC 的核心。

接下来，我再带你一起看一下 RPC 与本地调用（进程内函数调用）的区别吧，以加深你对 RPC 的理解。

你可以先想象一下，本地调用过程是怎样的。

简单来说，同一进程是共享内存空间的，用户可以通过{函数名 + 参数}直接进行函数调用。

而在 RPC 中，由于不同进程内存空间无法共享，且涉及网络传输，所以不像本地调用那么简单。所以，RPC 与本地调用主要有三点不同。

第一个区别是，调用 ID 和函数的映射。在本地调用中，进程内可共享内存地址空间，因此程序可直接通过函数名来调用函数。而函数名的本质就是一个函数指针，可以看成函数在内存中的地址。比如，调用函数 f()，编译器会帮我们找到函数 f() 相应的内存地址。但在 RPC 中，只通过函数名是不行的，因为不同进程的地址空间是不一样的。

所以在 RPC 中，所有的函数必须要有一个调用 ID 来唯一标识。一个机器上运行的进程在做远程过程调用时，必须附上这个调用 ID。

另外，我们还需要在通信的两台机器间，分别维护一个函数与调用 ID 的映射表。两台机器维护的表中，相同的函数对应的调用 ID 必须保持一致。

当一台机器 A 上运行的进程 P 需要远程调用时，它就先查一下机器 A 维护的映射表，找出对应的调用 ID，然后把它传到另一台机器 B 上，机器 B 通过查看它维护的映射表，从而确定进程 P 需要调用的函数，然后执行对应的代码，最后将执行结果返回到进程 P。

第二个区别是，序列化和反序列化。我们知道了调用方调用远程服务时，需要向被调用方传输调用 ID 和对应的函数参数，那调用方究竟是怎么把这些数据传给被调用方的呢？

在本地调用中，进程之间共享内存等，因此我们只需要把参数压到栈里，然后进程自己去栈里读取就行。但是在 RPC 中，两个进程分布在不同的机器上，使用的是不同机器的内存，因此不可能通过内存来传递参数。

而网络协议传输的内容是二进制流，无法直接传输参数的类型，因此这就需要调用方把参数先转成一个二进制流，传到被调用方后，被调用方再把二进制流转换成自己能读取的格式。这个过程，就叫作序列化和反序列化。

同理，被调用方返回的结果也需要有序列化和反序列化的过程，不然调用方无法获取到结果。也就是说，RPC 与本地调用相比，参数的传递需要进行序列化和反序列化操作。

第三个区别是，网络传输协议。序列化和反序列化解决了调用方和被调用方之间的数据传输格式问题，但要想序列化后的数据能在网络中顺利传输，还需要有相应的网络协议，比如 TCP、UDP 等，因此就需要有一个底层通信层。

调用方通过该通信层把调用 ID 和序列化后的参数传给被调用方，被调用方同样需要该通信层将序列化后的调用结果返回到调用方。

也就是说，只要调用方和被调用方可以互传数据，就可以作为这个底层通信层。因此，它所使用的网络协议可以有很多，只要能完成网络传输即可。目前来看，大部分 RPC 框架采用的是 TCP 协议。

说完 RPC 的核心原理，下面我以一个具有代表性的 RPC 框架 Apache Dubbo 为例，帮助你更加深入的了解 RPC。

在讲解 Dubbo 之前，你可以先想一下：如果你是一个 RPC 框架的设计者，你会如何设计呢？

首先必须得有服务的提供方和调用方。如下图所示，假设服务提供方 1～4 为调用方 1～4 提供服务，每个调用方都可以任意访问服务提供方。

当服务提供方和服务调用方越来越多时，服务调用关系会愈加复杂。假设服务提供方有 n 个， 服务调用方有 m 个，则调用关系可达 n*m，这会导致系统的通信量很大。此时，你可能会想到，为什么不使用一个服务注册中心来进行统一管理呢，这样调用方只需要到服务注册中心去查找相应的地址即可。

这个想法很好，如下图所示，我们在服务调用方和服务提供方之间增加一个服务注册中心，这样调用方通过服务注册中心去访问提供方相应的服务，这个服务注册中心相当于服务调用方和提供方的中心枢纽。

这样是不是好多了呢？

Dubbo 就是在引入服务注册中心的基础上，又加入了监控中心组件（用来监控服务的调用情况，以方便进行服务治理），实现了一个 RPC 框架。如下图所示，Dubbo 的架构主要包括 4 部分：

服务提供方。服务提供方会向服务注册中心注册自己提供的服务。

服务注册中心。服务注册与发现中心，负责存储和管理服务提供方注册的服务信息和服务调用方订阅的服务类型等。

服务调用方。根据服务注册中心返回的服务所在的地址列表，通过远程调用访问远程服务。

监控中心。统计服务的调用次数和调用时间等信息的监控中心，以方便进行服务管理或服务失败分析等。

可以看到，Dubbo 的大致工作流程如下：

服务提供方需要向服务注册中心注册自己提供的服务；

服务调用方需要向注册中心预订调用服务的提供方地址列表；

服务注册中心将服务对应的提供方地址列表返回给调用方；

服务调用方根据服务地址信息进行远程服务调用；

服务调用方和服务提供方定时向监控中心发送服务调用次数及调用时间等信息。

接下来，我再带你学习另一个远程调用机制 RMI。

RMI 的原理及应用

RMI 是一个基于 Java 环境的应用编程接口，能够让本地 Java 虚拟机上运行的对象，像调用本地对象一样调用远程 Java 虚拟机上的对象。

RMI 可以说是 RPC 的一种具体形式，其原理与 RPC 基本一致，唯一不同的是 RMI 是基于对象的，充分利用了面向对象的思想去实现整个过程，其本质就是一种基于对象的 RPC 实现。

RMI 的具体原理如下图所示：

RMI 的实现中，客户端的订单系统中的 Stub 是客户端的一个辅助对象，用于与服务端实现远程调用；服务端的支付系统中 Skeleton 是服务端的一个辅助对象，用于与客户端实现远程调用。

也就是说，客户端订单系统的 Pay(Order) 调用本地 Stub 对象上的方法，Stub 打包调用信息，比如变量、方法名等，通过网络发送给服务端的 Skeleton 对象，Skeleton 对象将收到的包进行解析，然后调用服务端 Pay(Order) 系统中的相应对象和方法进行计算，计算结果又会以类似的方式返回给客户端。

为此，我们可以看出，RMI 与 PRC 最大的不同在于调用方式和返回结果的形式，RMI 通过对象作为远程接口来进行远程方法的调用，返回的结果也是对象形式，可以是 Java 对象类型，也可以是基本数据类型。 RMI 的典型实现框架有 EJB（Enterprise JavaBean，企业级 JavaBean），如果你需要深入了解这个框架的话，可以参考其官方文档。

RPC 与 RMI 对比分析

好了，上面我带你学习了 RPC 和 RMI，接下来我通过一个表格来对比下它们的异同吧，以方便你进一步理解与记忆。

知识扩展：远程过程调用存在同步和异步吗？

分布式领域中，我们经常会听到同步和异步这两个词，那么远程过程调用存在同步和异步吗？

答案是肯定的。

远程过程调用包括同步调用和异步调用两种，它们的含义分别是：

同步调用，指的是调用方等待被调用方执行完成并返回结果。这就好比在现实生活中，用户 A 让用户 B 完成一篇文章，用户 A 就在那里等着，一直等用户 B 将写好的文章交给用户 A 后才离开，并对文章进行审核。

异步调用，指的是调用方调用后不用等待被调用方执行结果返回，并可以通过回调通知等方式获取返回结果。这就好比在现实生活中，用户 A 让用户 B 完成一篇文章，用户 A 告知用户 B 后，用户 A 离开去做其他事情，当用户 B 完成文章后反馈给用户 A，用户 A 收到反馈后开始审核文章。

也就是说，同步调用和异步调用的区别是，是否等待被调用方执行完成并返回结果。

因此，同步调用通常适用于需要关注被调用方计算结果的场景，比如用户查询天气预报，调用方需要直接返回结果；异步调用通常适用于对响应效率要求高、但对结果正确性要求相对较低的场景，比如用户下发部署一个任务，但真正执行该任务需要进行资源匹配和调度、进程拉起等过程，时间比较长，如果用户进程阻塞在那里，会导致体验很差，这种情况下可以采用异步调用。

总结

今天，我主要与你分享了分布式通信中的远程调用。

我以电商购物平台为例，首先让你对本地调用和远程调用有了一定的认识，然后分析了两种常用的远程调用机制 RPC 和 RMI，并对两者进行了比较。除此之外，我还介绍了 Dubbo 这个代表性的 RPC 框架。

接下来，我们再回顾下今天涉及的几个与远程调用相关的核心概念吧。

本地调用通常指的是同一台机器进程间函数的相互调用，而远程调用是指不同机器进程间函数的相互调用。

RPC 是指调用方通过参数传递的方式调用远程服务，并得到返回的结果。在整个过程中，RPC 会隐藏具体的通信细节，使调用方好像在调用本地函数或方法一样。

RMI 是一个基于 Java 环境的应用编程接口，能够让调用方 Java 虚拟机上运行的对象，像调用本地对象一样，调用其他机器 Java 虚拟机上的对象。可以说，RMI 是 RPC 的一种具体实现形式。

Dubbo 是一个代表性的 RPC 框架，服务提供方首先向注册中心注册自己提供的服务，调用方通过注册中心获取提供的相对应的服务地址列表，然后选择其中一个地址去调用相应的服务。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

现在，是不是觉得 RPC 没有之前那么神秘了呢？如果你对 RPC 感兴趣的话，Dubbo 就是一个很棒的出发点。加油，赶紧开启你的分布式通信之旅吧。

思考题

在 Dubbo 中引入了一个注册中心来存储服务提供方的地址列表，若服务消费方每次调用时都去注册中心查询地址列表。如果频繁查询，会导致效率比较低，你会如何解决这个问题呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

20 | 分布式通信之发布订阅：送货上门

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章中，我带你一起学习了分布式通信中的远程调用。远程调用的核心是在网络服务层封装了通信协议、序列化、传输等操作，让用户调用远程服务如同进行本地调用一样。

其实，这种方式就是通过网络服务层的封装实现了不同机器上不同进程之间的直接通信，因为是直接通信，所以通过线程阻塞的方式实现同步调用比较容易，因此通常被用于同步调用。比如，机器 1 上的进程 A 调用机器 2 上的进程 B，进程 A 被挂起，进程 B 开始执行，当进程 B 将值返回给 A 时，A 继续执行。

虽然这种方式也可以用于异步通信，但因为进程之间是直接交互的，所以当进程比较多时，会导致进程维护通信的复杂度非常高，且一个进程通信接口改变，与其通信的进程都会受到影响。

随着业务和分布式计算规模的逐渐增大和复杂化，远程调用模型有点心有余力而不足了，为此出现了专门的异步通信模式，也就是消息发布订阅模式和消息队列模式。在接下来的两篇文章中，我将与你详细讲述这两种通信模式。

话不多说，今天，我就带你一起打卡分布式通信中的发布订阅模式吧。

什么是发布订阅？

其实，发布订阅的思想在我们的生活中随处可见。

比如，学术届电子论文的订阅方式。通常，各个会议方或出版社会将学术论文发布到论文网站（或平台上，比如 ACM、知网等），然后学生或老师向论文网站订阅自己感兴趣的论文，比如分布式相关的、AI 相关的等。

当会议方或出版社将论文发布到论文网站后，论文网站会根据订阅信息，将相应的论文推送给订阅者（比如通过邮件的方式）。这里的会议方或出版社就相当于生产者，负责发布论文，学生或老师就相当于消费者，而论文网站就相当于一个消息中心。

由此可以看出，发布订阅的三要素是生产者、消费者和消息中心，生产者负责产生数据放到消息中心，消费者向消息中心订阅自己感兴趣的消息，当发布者推送数据到消息中心后，消息中心根据消费者订阅情况将相关数据推送给对应的订阅者。这种将数据送到消费者手里的行为，是不是和我们现在常说的“送货上门”一样呢？

发布订阅的原理及应用

这个论文订阅的例子，充分体现了发布订阅的思想。接下来，我就与你进一步分析下发布订阅的原理吧。

发布订阅的基本工作原理

在分布式通信领域中，消息系统一般有两种典型的模式。一种是点对点模式（P2P，Point to Point），另一种是发布订阅模式（Pub/Sub，Publish/Subscribe）。接下来，我们就一起看看这两种模式，以帮助你深入理解发布订阅模式的原理。

首先，我们一起看一下什么是点对点模式。

生产者将消息发送到消息中心，然后消费者从消息中心取出对应的消息进行消费。消息被消费后，消息中心不再存储该消息，因此其他消费者无法再消费该消息。也就是说，点对点模式虽然支持多个消费者，但一个消息只能被一个消费者消费，不允许重复消费。

这种模式就好比，限定了每篇论文只能被一个用户消费，比如现在有一篇分布式相关的论文，这篇论文推送给学生 A 之后，论文网站就必须将其删除或下架，也就是说其他用户无法再获取或阅读该论文了。（当然实际情况并不是这样的，这里只是为了方便你理解，我做了相应的假设。）

接下来，我们看一下发布订阅模式。

生产者可以发送消息到消息中心，而消息中心通常以主题（Topic）进行划分，每条消息都会有相应的主题，消息会被存储到自己所属的主题中，订阅该主题的所有消费者均可获得该消息进行消费。

比如图中假设生产者 1 发布一个 Topic 相关数据或消息，消费者 1～3 均订阅了该 Topic 消息，则该消息会推送消费者 1～3，也就是说同一个消息被 3 个消费者消费了。

这种模式就好比，不同的方向代表不同的主题，比如分布式领域代表一个主题，当会议方或出版社发布分布式相关的论文时，该论文会被存储到论文网站的分布式主题下，同时学生或老师也会根据自己感兴趣的主题进行订阅。如果学生 A 订阅了分布式主题，那么当会议方或出版社发布分布式相关的论文后，会议网站会将这些论文推送给学生 A。

与点对点模式相比，发布订阅模式中一个消息可以被多个消费者进行消费，这也是和点对点模式的本质区别。

以上就是发布订阅中的两种典型模式了。

在分布式系统中，通常会为多用户服务，而多个用户通常会关注相同类型的消息，因此发布订阅模式在分布式系统中非常常见。接下来，我再结合经典的分布式发布订阅消息系统 Kafka 的发布订阅原理及工作机制，来帮助你巩固对发布订阅的理解。

Kafka 发布订阅原理及工作机制

Kafka 是一种典型的发布订阅消息系统，其系统架构也是包括生产者、消费者和消息中心三部分。

生产者（Producer）负责发布消息到消息中心，比如电子论文的会议方或出版社；

消费者（Consumer）向消息中心订阅自己感兴趣的消息，获得数据后进行数据处理，比如订阅电子论文的老师或学生；

消息中心（Broker）负责存储生产者发布的消息和管理消费者订阅信息，根据消费者订阅信息，将消息推送给消费者，比如论文网站。在 Kafka 中，消息中心本质上就是一组服务器，也可以说是 Kafka 集群。

Kafka 的架构图，如下所示：

可以看到，Kafka 中除了 Producer、Broker、Consumer 之外，还有一个 ZooKeeper 集群。Zookeeper 集群用来协调和管理 Broker 和 Consumer，实现了 Broker 和 Consumer 的解耦，并为系统提供可靠性保证。

ZooKeeper 集群可以看作是一个提供了分布式服务协同能力的第三方组件，Consumer 和 Broker 启动时均会向 ZooKeeper 进行注册，由 ZooKeeper 进行统一管理和协调。

ZooKeeper 中会存储一些元数据信息，比如对于 Broker，会存储主题对应哪些分区（Partition），每个分区的存储位置等；对于 Consumer，会存储消费组（Consumer Group）中包含哪些 Consumer，每个 Consumer 会负责消费哪些分区等。

接下来，我们看看分区和消费组的原理和作用吧。

从上面的介绍可以看出，Broker 负责存储消息数据，Consumer 负责消费数据，Consumer 消费数据的能力会影响 Broker 数据存储是否溢出的问题。若 Consumer 消费太慢，会导致 Broker 存储溢出，Broker 就会丢弃一部分消息。

因此，Broker 和 Consumer 是 Kafka 的核心。接下来，我将带你进一步了解 Kafka 中 Broker 和 Consumer 的关键技术，如下图所示：

首先，我们看一下 Broker。

在 Kafka 中，为了解决消息存储的负载均衡和系统可靠性问题，所以引入了主题和分区的概念。其中，主题是一个逻辑概念，指的是消息类型或数据类型，就好比电子论文案例所讲的分布式是一个主题。

而分区是针对主题而言的，指的是一个主题的内容可以被划分成多个集合，分布在不同的 Broker 上，不同的 Broker 在不同的节点上。这里的集合就是分区，其中同一个分区只属于一个 Broker。

那么，分区有什么好处呢？

在我看来，分区的好处主要包括如下两点：

实现负载均衡，避免单个 Broker 上的负载过高。比如，Topic 0 被分为 Partiton-0、Partiton-1 和 Partiton-2 三个分区，分别分布在 Broker 0、Broker 1 和 Broker 2 上。这，就使得 Topic 0 的消息可以分布在这 3 个分区中，实现负载均衡。

实现消息的备份，从而保证系统的高可靠。比如，Topic 1 包含两个分区 Partiton-0、Partiton-1，每个分区内容一致，分别存储在 Broker 0 和 Broker 1 上，借此实现了数据备份。

接下来，我们再看看 Consumer 吧。

Kafka 中的消费组，指的是多个消费者的一个集合。一个消费组中的消费者共同消费主题消息，并且主题中每个消息只可以由消费组中的某一个消费者进行消费。

引入消费组的目的是什么呢？我们知道，在消息过多的情况下，单个消费者消费能力有限时，会导致消费效率过低，从而导致 Broker 存储溢出，丢弃一部分消息。Kafka 为了解决这个问题，所以引入了消费组。

这样一来，我们对发布订阅的基本工作机制就比较清楚了。接下来，我们再结合电商购物平台的例子，来看看发布订阅技术的具体应用吧。

发布订阅实践应用

假设在电商购物平台（为了方便理解，我对电商购物平台做了一定的简化）中，用户首先在订单系统下单，下单后库存系统会进行出货，通知系统则负责通知用户，整个流程可以用发布订阅的模式进行，如下图所示：

订单系统对应发布订阅模式中的生产者，消息中心有个主题专门存放下单信息，每次用户下单后，订单系统会向该主题写入数据；

库存系统和通知系统对应发布订阅模式中的消费者，它们会向消息中心订阅下单信息相关的主题；

订单系统向消息中心发布订单信息后，库存系统和通知系统都会获取到相应的下单信息，然后进行各自后续的操作，即库存系统进行出货，通知系统通过短信或邮件等方式通知用户。

接下来，我们总结下发布订阅模式的关键特征吧。

实现了系统解耦，易于维护。生产者 / 发布者只负责消息的发布，不需要知道订阅者 / 消费者的数量，也不需要知道订阅者 / 消费者获取消息用来做什么，而订阅者 / 消费者也不需要知道什么时候生产者 / 发布者会发布消息。

所以，生产者 / 发布者和订阅者 / 消费者互相独立，进而实现了系统解耦，每个部分可以单独维护，减少了因为生产者和消费者的耦合引入的一些相互影响。比如，如果两者耦合在一起，当生产者逻辑更改需要修改代码时，消费者部分的代码也受影响，因此每个部分单独维护降低了维护的复杂度。

实现了异步执行，避免高负载。生产者 / 发布者发布消息到消息中心，当消息超过消息中心可以存储的容量后，消息中心会丢弃掉超出的消息，这样系统就不会因为消息数量多而导致系统故障。

知识扩展：观察者模式和发布订阅模式的区别是什么？

我们还经常会听到一个概念，叫作观察者模式，也会在分布式系统中都会经常用到。那么，观察者模式和发布订阅模式的区别到底是什么呢？

首先，我们看一下观察者模式。顾名思义，观察者模式下有观察者，那么就有被观察者，两者之间的关系是什么呢？

观察者负责监控被观察者的状态变更，如果被观察者的状态发生了改变，那么观察者根据状态的变更执行相关操作。举个例子，现在进程 A 是被观察者，进程 B 和进程 C 是观察者，当进程 B 观察到进程 A 中变量 X 由 3 变为 4 时，执行 X+1 的操作；当进程 C 观察到进程 A 中变量 X 由 3 变为 4 时，执行 X-1 的操作。也就是说，观察者模式，定义了被观察者与观察者的直接交互或通信关系。

接下来，我们看一下发布订阅模式。发布订阅模式中存在发布者、订阅者和消息中心，订阅者需要向消息中心指定自己对哪些数据感兴趣，发布者推送的数据放入消息中心后，消息中心根据订阅者订阅信息推送数据。也就是说，发布者和订阅者之间引入了消息中心，实现的是间接通信。

总结来讲，观察者模式采用了直接通信，观察者和被观察者通信时延会低一些，但它们的依赖关系比较强，不管是被观察者还是观察者逻辑或接口有更改，另外一个均会受影响。而发布者和订阅者模式采用间接通信，引入了消息中心，相对比较厚重，且通信时延相对会高一点，但实现了订阅者与发布者的解耦。

总结

我首先通过论文订阅的案例，与你介绍了什么是发布订阅以及发布订阅的基本原理，然后介绍了一个经典的分布式发布订阅消息系统 Kafka，最后以一个电商购物平台的案例描述了发布订阅模式的应用场景。

我再和你总结下今天的核心知识点吧。

发布订阅就是生产者产生消息发布到消息中心，消费者订阅自己感兴趣的消息，消息中心根据消费者的订阅情况将相关消息发给对应的消费者。

Kafka 是一个经典的发布订阅消息系统，采用多分区实现了消息备份、负载均衡，并引入消费组提高了消费者的消费能力，防止 Broker 因为存储资源不够丢弃消息的情况，从而提高了 Kafka 系统的可靠性。

发布订阅模式可以使系统松耦合易于维护，也可异步执行解决高负载问题，适用于系统解耦、流量削峰等场景。

最后，我再通过一张思维导图梳理下今天的核心知识点，以帮助你理解与记忆。

发布订阅模式易于理解，与点对点模式很类似。不同的是，点对点模式中一个消息只能由一个消费者消费，而发布者订阅者模式中一个消息可以由多个消费者消费。

不同的通信模式适用于不同的分布式场景，其中发布订阅模式适合具备多个生产者、多个消费者且异步处理的场景，比如现在的视频 App，多个用户都可以通过同一款 App 看同一部电视剧，当然这个电视剧可以是被不同的生产者发布。点对点模式由于其局限性，一般适用于需要进行点对点通信的场景，比如近场投屏等。

相信你通过本讲的学习后，可以针对不同的分布式场景选择合适的通信模式，加油！

思考题

发布订阅模式下，当发布者消息量很大时，单个订阅者的处理能力是有限的，那么能否实现订阅者负载均衡消费呢？又该如何实现呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

21 | 分布式通信之消息队列：货物自取

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章，我带你学习了分布式通信技术中的发布订阅。总结来说，发布订阅就是发布者产生数据到消息中心，订阅者订阅自己感兴趣的消息，消息中心根据订阅者的订阅情况，将相关消息或数据发送给对应的订阅者。所以，我将其的思想，概括为“送货上门”。

在实际使用场景中，还有一种常用的通信方式，就是将消息或数据放到一个队列里，谁需要谁就去队列里面取。在分布式领域中，这种模式叫“消息队列”。与发布订阅相比，消息队列技术的核心思想可以概括为“货物自取”。

接下来，我们就一起打卡分布式通信技术中的消息队列吧。

什么是消息队列？

回想一下，在上一篇学术电子论文订阅的例子中，出版社或会议方将论文发布到论文网站（或平台）上，然后论文网站再将论文推送给订阅相关论文的老师或学生。这里的论文网站就是消息中心，负责根据订阅信息将论文送货上门，角色非常关键。

但其实，除了将论文送货上门外，我们还能想到另外一种模式，也就是出版社或会议方将论文发布到论文网站进行存储，老师或学生根据需要到论文网站按需购买文章。

这种思想，在分布式通信领域中称为消息队列模式，论文网站充当的就是消息队列的角色，也非常关键。接下来，我再通过一个具体的应用案例来帮助你更加深入地理解什么是消息队列吧。

比如，很多系统都提供了用户注册功能，注册完成后发送通知邮件。如下图所示，假设用户通过邮箱进行注册，填写完注册信息并点击提交后，系统的处理过程主要分为两步：

检查用户注册信息的合法性，如果合法则将注册信息写入数据库中，若不合法，直接返回，流程结束；

将用户注册信息写入数据库后，给用户发送通知邮件，以告知用户注册的相关信息，比如注册账号等信息。

假设，系统将注册信息写入数据库需要花费 400ms、给用户发送通知邮件需要花费 600ms。

这时，注册消息写入数据库和发送通知邮件这两个组件间是直接交互，且是同步通信方式。那么，从用户提交注册到收到响应，需要等系统完成这两个步骤。也就是说，如果不考虑通信延迟的话，注册系统对用户的响应时间是 1000ms，即 1s。

如下图所示，如果引入消息队列作为注册消息写入数据库和发送通知邮件这两个组件间的中间通信者，那么这两个组件就可以实现异步通信、异步执行。引入消息队列后，上述步骤可以分为三步：

检查用户注册信息的合法性，如果合法则将注册信息写入数据库中，若不合法则直接返回，流程结束；

注册消息写入消息数据库后，将消息写入消息队列的队尾；

发送通知邮件的组件去消息队列取出队首的消息，给用户发送通知邮件，告知用户注册的相关信息。

也就是说，采用消息队列模式，只需要第 2 步完成，即可给用户返回响应。第 3 步发送通知邮件可以在返回响应之后执行。

用户的注册信息写入数据库之后，通过数据库的可靠性设计来保证用户注册信息不会丢失，也就是说发送通知邮件的组件一定可以获取到用户注册信息，即保证会给注册用户发送通知邮件。也就是说，消息队列的引入不会影响用户注册网站，但会提升用户响应效率。

通常情况下，将消息写入消息队列的速度很快，假设需要 100ms。那么，引入消息队列后，发送通知邮件实现了异步读取，系统响应时间缩短为 500ms，响应速度提升了一倍，提升了用户体验。

讲完了用户注册这个例子，我们再来看消息队列的定义就比较容易理解了。

队列是一种具有先进先出特点的数据结构，消息队列是基于队列实现的，存储具有特定格式的消息数据，比如定义一个包含消息类型、标志消息唯一性的 ID、消息内容的一个结构体作为消息数据的特定格式。消息以特定格式放入这个队列的尾部后可以直接返回，并不需要系统马上处理，之后会有其他进程从队列头部开始读取消息，按照消息放入的顺序逐一处理。

从上面的例子中，我们也可以看出引入消息队列的好处是，提高响应速度，以及实现组件间的解耦。

消息队列的原理

现在，我把消息队列的工作原理从用户注册这个例子中剥离出来，给你一个更加直接的解释吧。

消息队列工作原理

消息队列的核心结构，如下图所示。与发布订阅模式类似，消息队列模式也是包括 3 个核心部分：

生产者。生产者会产生消息或数据，并将消息或数据插入到消息队列中。

消息队列。一种具有先进先出特点的数据结构，用于存储消息。

消费者。从消息队列中获取消息或数据，进行相关处理。

具体流程是，生产者将发送的消息插入消息队列，也就是入队，之后会有一个消费者从消息队列中逐次取出消息进行处理，完成出队。

了解了消息队列的工作原理，接下来我以阿里开源的 RocketMQ 为例，与你进一步介绍消息队列的原理、工作机制和实践应用。

RocketMQ 消息队列原理及工作机制

首先，我们看一下 RocketMQ 的架构图，形成一个整体认知。

RokcetMQ 共包括 NameServer Cluster、Producer Cluster、Broker Cluster 和 Consumer Cluster 共 4 部分。接下来，我们一起看看每部分的具体功能吧。

NameServer Cluster，指的是名字服务器集群。这个集群的功能与 Kafka 中引入的 ZooKeeper 类似，提供分布式服务的协同和管理功能，在 RocketMQ 中主要是管理 Broker 的信息，包括有哪些 Broker、Broker 的地址和状态等，以方便生产者获取 Broker 信息发布消息，以及订阅者根据 Broker 信息获取消息。

Producer Cluster，指的是生产者集群，负责接收用户数据，然后将数据发布到消息队列中心 Broker Cluster。那么，生产者按照集群的方式进行部署，好处是什么呢？在我看来，好处可以概括为以下两点：

一是，多个 Producer 可以并发接收用户的输入数据，提升业务处理效率；

二是，考虑到可靠性问题，如果只有一个 Producer 接收用户输入数据，当这个 Producer 故障后，整个业务就无法运行了。

Consumer Cluster，指的是消费者集群，负责从 Broker 中获取消息进行消费。Consumer 以集群方式进行部署的好处是，提升消费者的消费能力，以避免消息队列中心存储溢出，消息被丢弃。

Broker Cluster，指的是 Broker 集群，负责存储 Producer Cluster 发布的数据，以方便消费者进行消费。

Broker Cluster 中的每个 Broker 都进行了主从设计，即每个 Broker 分为 Broker Master 和 Broker Slave，Master 既可以写又可以读，Slave 不可以写只可以读。每次 Broker Master 会把接收到的消息同步给 Broker Slave，以实现数据备份。一旦 Broker Master 崩溃了，就可以切换到 Broker Slave 继续提供服务。这种设计的好处是，提高了系统的可靠性。

可以看出，Broker Cluster 就是我们今天要讲的核心“消息队列中心”，那么它到底是如何采用队列实现的呢？接下来，我们就一起看看 Broker Cluster 的实现方式吧。

如下图所示，在 Broker Cluster 中，消息的存储采用主题（Topic）+ 消息队列（Queue）的方式实现：

与 Kafka 一样，RocketMQ 中的主题也是一个逻辑概念。一个主题可以分区，分布在各个不同的 Broker 中，每个 Broker 上只有该主题的部分数据。每个主题分区中，队列的数量可以不同，由用户在创建主题时指定。队列是资源分配的基本单元，消息进行存储时会存放到相应主题的分区中。

上面我为你介绍了 RocketMQ 的关键组件。接下来，我们再看看 RocketMQ 的工作流程，如下图所示：

首先启动 NameServer，然后启动 Broker。Broker 启动后，会主动找 NameServer 建立连接，并将自己的信息注册到 NameServer 上。注册完毕后，Broker 会周期性地给 NameServer 发送心跳包，比如每隔 1s 发送一次，以告知 NameServer 自己还活着；心跳包里还可以包括 Broker 当前存储的数据信息，也就是说 Broker 可以周期性地向 NameServer 更新自己的数据信息，以保证 NameServer 上存储的数据是最新的。

创建主题，并确定这个主题的数据放入哪些 Broker 中。

当 Producer 生产消息发送到主题时，需要先到 NameServer 查询该主题存放在哪些 Broker 中，获取到相关 Broker 信息后，将消息发送给这些 Broker 进行存储。

Consumer 要从主题消费消息，也需要首先到 NameServer 查询一下该主题的消息存储在哪些 Broker 上，然后去相应的 Broker 获取消息进行消费。

通过对 RocketMQ 的介绍，相信你已经对消息队列有比较深刻的认识了。接下来，我们再看看消息队列模式适用于什么场景吧。

消息队列模式，是根据消费者需求到消息队列获取数据消费的，消费者只需要知道消息队列地址即可，消息队列中心也无需提前知道消费者信息。也就是说，这种模式对消费者没有特别需求，因此比较适合消费者为临时用户的场景。

比如目前，阿里内部将 RocketMQ 应用于购物交易、充值、消息推送等多个场景，因为在这些场景下，每个消费者不是常驻进程或服务，几乎都是临时存在。此外，滴滴、联想等公司也都有采用 RocketMQ。

知识扩展：发布订阅和消息队列模式都支持系统解耦，两者是否一致呢？

概括地说，发布订阅和消息队列模式虽然都支持系统解耦，但它们在实现时采用的数据结构和方式并不相同。

首先，我们看一下它们实现解耦的数据结构。

发布订阅模式采用了消息中心，消息队列模式采用了消息队列中心，它们均用来存储生产者发布的数据，并均有主题、Broker 等概念；

唯一不同之处，是消息队列模式中采用了具有先进先出特征的队列结构进行存储，而订阅发布采用了 map 或数组等方式存储。

然后，我们再看看它们实现解耦的方式。

消息队列模式中，生产者发布数据到消息队列中心，消息队列中心会存储数据，等待消费者按需获取数据。这样生产者就不需要和消费者进行直接通信了，实现了生产者和消费者的解耦。

而在发布订阅模式中，消费者需要提前向消息中心订阅自己感兴趣的数据，待生产者发布数据到消息中心后，消息中心根据订阅者订阅信息将数据主动推送给消费者，也实现了消费者和生产者的解耦。

对于消息队列模式，消息队列中心无需提前获取消费者信息，因此对消费者比较灵活，适合消费者为临时用户的场景；而发布订阅模式，需要消费者提前向消息中心订阅消息，也就是说消息中心需要提前获取消费者信息，比较适合消费者为长驻进程或服务的场景。

总结

今天，我主要与你分享的是分布式通信技术中的消息队列模式。

首先，我通过用户注册的案例，与你介绍了什么是消息队列模式，以及它的好处。其中，消息队列模式中的核心是以一种具有先进先出特点的队列结构来存储数据，实现组件间的解耦和异步执行。

然后，我与你介绍了消息队列的基本原理，并以 RocketMQ 为例对其架构、核心组件和工作原理做了更深入的讲解，以帮助你进一步了解消息队列模型。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

加油，行动起来，试着将发布订阅和消息队列这两种通信模式用到你的业务场景中吧，相信你可以的。如果你需要进一步了解这两种通信模式对应的产品源码的话，相信你结合这两篇文章中讲述的原理，可以比较容易地开启你的源码之旅了。

思考题

消息队列模型中，消费者是主动去消息队列获取消息的，而消息队列需要保证多个消费者可以获取到消息，也就是说一个消费者获取消息后并不会删除该消息，那么如何保证同一个消息不被同一个消费者重复消费呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

22 | 答疑篇：分布式体系架构与分布式计算相关问题

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

到目前为止，“分布式技术原理与算法解析”专栏已经更新 21 篇文章了，我已经为你介绍了分布式技术四纵四横知识体系中的三横，即“布式资源管理”“分布式计算技术”和“分布式通信”，以及四纵中的“分布式协同”和“分布式调度”。

在这里，我首先要感谢你们坚持学习每一篇文章，以及对每一道思考题的积极思考与讨论，并且还在此基础上对类似问题进行了扩展。

比如，@1024、@每天晒白牙、@游弋云端、@Jackey 和 @Dale 等同学，对双主问题展开了激烈的讨论；再比如，@xj_zh、@mt11912、@小白啊、@随心而至等同学，对 Master 如何判断 Slave 是否存活的问题进行了讨论，特别是 @小白啊还专门查询了 Kubernetes 的方法，在留言区进行了回复。

这样的同学还有很多，我就不再一一点名了。今天，我就针对前面文章涉及的与思考题有关的留言，做一次进一步地梳理与分析，以帮助你夯实前面所学的知识点。

留言涉及的问题有很多，但我经过进一步地分析和总结后，发现大家特别感兴趣和有疑惑的思考题主要分为两类：

分布式体系架构中，如何判断节点存活的问题；

分布式计算技术中，离线计算、批量计算、实时计算和流式计算的区别。

今天，我主要就对这两类思考题进行一下分析和讲解。

分布式体系架构相关问题

在第 9 篇文章“分布式体系结构之集中式结构：一人在上，万人在下”中，我给你留了一个思考题：在集中式架构中，Master 如何判断 Slave 是否存活呢？

首先，我先和你说说 Slave 故障的两类情况：一种是 Slave 进程退出，另一种是 Slave 所在服务器宕机或重启了。你可能会说，这两种情况的判断方法，难道还不一致吗？别着急，且听我慢慢道来。

如下图所示，假设 Master 节点与 3 个 Slave 节点相连。请注意，我在图中，Master 与 Slave 之间画了两条线，实线旁写的是 TCP 长连接，虚线旁写的是心跳。因为 Master 与 Slave 之间的监控关系是固定的，因此我用了两种机制协同来判断 Slave 是否存活。

其中，TCP 长连接就是针对 Slave 进程退出，但是 Slave 所在服务器未故障的情况。这种方式是借助 TCP 长连接的工作原理进行判断的。TCP 长连接中，TCP 会对对端的 Socket 进行检测，当发现对端 Socket 不可用时，比如不能发出探测包或探测包未收到响应，会返回 -1 的状态，表示连接断开。所以，这种方式可以快速检测到 Slave 进程的退出。

对于 Slave 所在服务器故障的情况，由于服务器宕机或重启，那么系统环境等均不工作了，这种情况 TCP 长连接也无法进行探测了，也就是说 TCP 长连接方法在这种场景下无法判断节点是否故障。

对于这种场景，现有的软件架构中，基本都采用了心跳方式。其核心策略是，Master 按照周期性（比如每隔 1s）的方式给 Slave 发送心跳包，正常情况下 Slave 收到 Master 发送的心跳包后，会立即回复一个心跳包，告知 Master 自己还活着。当某个 Slave（比如 Slave1）所在服务器故障后，由于 Slave 无法接收到 Master 的心跳包，也就无法回复了。

因此，Master 也无法接收到这个 Slave（比如 Slave1）的回复信息。通常情况下，系统会设置一个阈值（一般设置为与心跳周期一致），若超过这个阈值还未收到 Slave 节点的回复，Master 就会标记自己与该 Slave 心跳超时。

其中，设置阈值的目的是，解决 Slave 故障情况下，Master 一直收不到心跳信息而阻塞在那里等待心跳回复的问题。一般连续 k 次 Master 与 Slave 的心跳超时，Master 就会判断该 Slave 故障了。其中，设置连续 k 次的目的是，降低因为系统做垃圾回收或网络延迟导致误判的概率。

这里的 k，主要是根据业务场景进行设置的。如果 k 设置得太小，容易导致故障误判率过高，因为系统在做垃圾回收或系统进程正在占用资源时，会阻塞心跳，导致心跳包无法及时回复而超时，从而被误判。如果 k 设置得太大，会导致故障发现的时间过长，因为故障发现时间 =k* 心跳发送周期。

接下来，我们继续延展下这个问题吧。

追问 1：非集中式架构中，如何判断节点是否存活？

集中式架构中，采用了 TCP 连接和心跳协同判断节点是否存活，那么非集中式架构中是否也是这样判断的呢？

其实，在非集中式架构与集中式架构中，判断节点是否存活的原理有所不同。因为，非集中式架构中节点之间是对等的，没有 Master 与 Slave 之分。如果每个节点间都建立 TCP 长连接，假设集群中有 n 个节点，那么每个节点均需要与其他 n-1 个节点建长连接，这将导致每个节点的资源占用都会非常多。因此，非集中式架构是采用心跳的方式进行判断的。

这里你可能会问，如果像集中式架构那样，每个节点与其他 n-1 个节点都发送心跳的话，整个集群中同一时间心跳消息为 n*(n-1)，消息量也特别大，甚至会导致网络风暴，应该怎么办。

其实，与集中式架构中的心跳包不同，非集中式架构中采用的心跳方式的核心思想是，每个节点被 b（1≤b<n）个节点监控，以减少心跳信息量。

接下来，我们以 Akka 的原理为例，先来看看 b 的取值原则吧。

如果用户不设置 b 的值，那么 b 默认取值的原则是：若集群中节点总数 n 小于 6，b=n-1；若 n 大于等于 6，b=5。

若用户设置 b 的值，则 b 以用户设置的值为准。

接下来，我们再看看 Akka 集群中具体是如何通过心跳方式判断节点是否存活的。

Akka 中集群组建完成后，每个节点拥有整个集群中的节点列表。

每个节点根据集群节点列表，计算哈希值（比如根据节点 ID 计算一个哈希值），然后基于哈希值，将所有节点组成一个哈希环（比如，从小到大的顺序），如下图所示。由于每个节点上的计算方法一致，因此虽然每个节点独立计算，但每个节点上维护的哈希环是一致的。

根据哈希环，针对每个节点逆时针或顺时针方向选择 b（图中设置 b=2）个临近节点作为监控节点，比如图中 Node 2 和 Node3 监控 Node1，Node 3 和 Node4 监控 Node2，以此类推。由于每个节点被 b 个节点监控，反过来也可以说，在这个环上每个节点监控 b 个节点，因此具体的实现方式是每个节点按照逆时针或顺时针方向选择 b 个节点进行监控。

当某个节点发现自己监控的节点心跳超时时（比如 Node 2 发现 Node1 心跳超时），则标记该节点不可达（Node2 标记 Node1 不可达），并将该信息通过 Gossip 协议传播给集群中的其他节点。

如果某个节点被标记为不可达之后（比如 Node1 不可达），若不将该节点踢出集群，那么 Node2 和 Node3 仍然会给 Node1 发送心跳，若后面 Node2 又发现 Node1 心跳可达时，则重新将 Node1 更新为可达状态，然后同步给集群中其他节点。

这里的判断心跳超时机制，可采用集中式方法中的连续 k 次心跳超时的方法进行判断，也可以通过历史心跳信息进行预测。具体的预测方法，我将在第 31 篇文章“分布式高可用之故障恢复：知错能改，善莫大焉”中做进一步讲解。

追加 2: 一个集群为什么会存在双主的场景呢？

上面，我提到判断节点存活的方法主要是通过心跳的方式。如果是因为网络连接断开，那么节点之间就会被误判为对方故障了。在主备场景下，通常会出现双主的情况。这也就是第 4 篇文章“分布式选举：国不可一日无君”的课后思考题答案了。

在主备场景下，正常情况下，主节点提供服务，备节点是对主节点的数据、状态进行备份，以确保主故障后备升主后业务可以正常运行。主备节点之间通常会通过心跳的方式进行检测，目的是监控主节点是否故障，若故障则备升主，保证业务运行。

想象一下，如果主备节点之间的网络连接断开了，那么主节点与备节点之间心跳均不可达，因此主节点会认为备节点故障，此时主节点会继续提供服务，而备节点会认为主节点故障，备升主。所以，集群中就出现了双主的场景。

好了，以上就是关于分布式体系结构中如何判断节点是否存活的相关问题了，相信你对这几个问题有了比较深刻的理解。接下来，我们再看看分布计算技术的相关问题吧。

分布计算技术相关问题

在分布式计算技术中，我们经常会听到离线计算、批量计算、实时计算和流式计算这四个概念，也常常会弄混。那么，离线计算和批量计算，实时计算和流式计算到底是什么呢？离线计算和批量计算、实时计算和流式计算分别是等价的吗？

接下来，就请你带着问题，随我一起进入下文。

首先，我们来看一下离线计算。通常我们提到的离线计算，主要的应用场景是对时延要求不敏感、计算量大、需要计算很长时间（比如需要数天、数周甚至数月）的场景，比如大数据分析、复杂的 AI 模型训练（比如神经网络）等。

这种场景如果采用在线计算或实时计算的话，通常会存在数据量不够或大量计算影响正在运行的业务等问题，因此往往会采用离线计算的方式。

离线计算方式的核心思想是，先采集数据，并将这些数据存储起来，待数据达到一定量或规模时再进行计算，然后将计算结果（比如离线训练的模型）应用到实际业务场景中。

其次，我们看一下批量计算。批量计算通常是指，将原始数据集划分为多个数据子集，然后每个任务负责处理一个数据子集，多个任务并发执行，以加快整个数据的处理。比如，我在第 15 篇文章“分布式计算模式之 MR: 一门同流合污的艺术”中，讲 MR 计算模式时提到，MapReduce 中的 Map 其实就属于批量计算，Map 计算的结果会通过 Reduce 进行汇总。

接下来，我们再看一下实时计算。实时计算其实是和离线计算相对应的，离线计算对时延要求不敏感，相反，实时计算对时延的要求比较敏感。这种模式需要短时间执行完成并输出结果，比如秒级、分钟级，也就是说强调时效，通常用于秒杀、抢购等场景。实时计算由于时延要求低，因此计算量通常不大、数据量也不会太多，所计算的数据往往是 K、M 级别的。

最后，我们在看看流式计算。我在第 16 篇文章“分布式计算模式之 Stream: 一门背锅的艺术”中，与你讲述了流式计算。流计算强调的是实时性，数据一旦产生就会被立即处理，当一条数据被处理完成后，会立刻通过网络传输到下一个节点，由下一个节点继续处理。这种模式通常用于商业场景中每天的报表统计、持续多天的促销活动效果分析等。

为了便于你理解与记忆，我将这四种计算模式的特点，汇总为了一张表格，如下所示。

通过对这四种计算模式的讲解，相信你已经发现了，离线计算和批量计算对任务执行的时延不是特别敏感，而实时计算和流式计算对任务执行的时延敏感。但，离线计算和实时计算是从计算时延的维度进行分类的，而批量计算和流式计算是从计算方式的维度进行分类的，因此我们不能将离线计算和批量计算直接等同，也不能将实时计算和流式计算直接等同。

总结

我把前面 21 篇文章中，大家针对思考题的讨论和困惑，筛选出了分布式系统架构中如何判断节点是否存活，以及四种分布式计算模式的异同，做了进一步展开，梳理成了今天的这篇答疑文章。

如果还有哪些思考题或者留言问题，还没来得及扩展的话，你可以留言给我，后续我会再找机会进行解答。最后，我要和你说的是，和我一起打卡分布式核心技术，一起遇见更优秀的自己吧。

篇幅所限，留言区见。

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

23 | CAP理论：这顶帽子我不想要

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在开篇词中，我将分布式计算划分为了四横四纵。而在前面的文章中，我们已经一起学习了四横中的分布式计算、分布式通信和分布式资源池化三横的相关知识。比如，在分布式计算中，我们学习了分布计算模式，包括 MapReduce、Stream、Actor 和流计算的原理和实际应用；在分布式通信中，我们学习了远程调用、订阅发布和消息队列模式的原理和应用；在分布式资源池化中，我们学习了分布式系统架构和分布式调度架构。

相信通过对这些内容的学习，你已经对分布式技术有比较深刻的了解了。分布式系统处理的关键对象是数据，前面这些文章也都是为数据处理服务的。那么，数据本身相关的分布式技术有哪些呢？这就是接下来的几讲，我要带你学习的四横中的最后一横“分布式数据存储与管理”的相关技术。

在正式介绍分布式数据存储技术之前，我需要先带你了解一个基本理论，也就是 CAP 理论。前面提到，分布式系统处理的关键对象是数据，而数据其实是与用户息息相关的。CAP 理论指导分布式系统的设计，以保证系统的可用性、数据一致性等特征。比如电商系统中，保证用户可查询商品数据、保证不同地区访问不同服务器查询的数据是一致的等。

话不多说，接下来，我们就一起打卡 CAP 理论吧。

什么是 CAP？

如果你之前没有听说过 CAP 理论的话，看到这三个字母第一反应或许是“帽子”吧。那么，在分布式领域中，CAP 这顶“帽子”到底是什么呢？我们先来看看这三个字母分别指的是什么吧。

接下来，我结合电商的例子，带你理解 CAP 的含义。

假设某电商，在北京、杭州、上海三个城市建立了仓库，同时建立了对应的服务器{A, B, C}用于存储商品信息。比如，某电吹风在北京仓库有 20 个，在杭州仓库有 10 个，在上海仓库有 30 个。那么，CAP 这三个字母在这个例子中分别代表什么呢？

首先，我们来看一下 C。C 代表 Consistency，一致性，是指所有节点在同一时刻的数据是相同的，即更新操作执行结束并响应用户完成后，所有节点存储的数据会保持相同。

在电商系统中，A、B、C 中存储的该电吹风的数量应该是 20+10+30=60。假设，现在有一个北京用户买走一个电吹风，服务器 A 会更新数据为 60-1=59，与此同时要求 B 和 C 也更新为 59，以保证在同一时刻，无论访问 A、B、C 中的哪个服务器，得到的数据均是 59。

然后，看一下 A。A 代表 Availability，可用性，是指系统提供的服务一直处于可用状态，对于用户的请求可即时响应。

在电商系统中，用户在任一时刻向 A、B、C 中的任一服务器发出请求时，均可得到即时响应，比如查询商品信息等。

最后，我们看一下 P。P 代表 Partition Tolerance，分区容错性，是指在分布式系统遇到网络分区的情况下，仍然可以响应用户的请求。网络分区是指因为网络故障导致网络不连通，不同节点分布在不同的子网络中，各个子网络内网络正常。

在电商系统中，假设 C 与 A 和 B 的网络都不通了，A 和 B 是相通的。也就是说，形成了两个分区{A, B}和{C}，在这种情况下，系统仍能响应用户请求。

一致性、可用性和分区容错性，就是分布式系统的三个特征。那么，我们平时说的 CAP 理论又是什么呢？

CAP 理论指的就是，在分布式系统中 C、A、P 这三个特征不能同时满足，只能满足其中两个，如下图所示。这，是不是有点像分布式系统在说，这顶“帽子”我不想要呢？

接下来，我就通过一个例子和你进一步解释下，什么是 CAP 以及 CAP 为什么不能同时满足吧。

如下图所示，网络中有两台服务器 Server1 和 Server2，分别部署了数据库 DB1 和 DB2，这两台机器组成一个服务集群，DB1 和 DB2 两个数据库中的数据要保持一致，共同为用户提供服务。用户 User1 可以向 Server1 发起查询数据的请求，用户 User2 可以向服务器 Server2 发起查询数据的请求，它们共同组成了一个分布式系统。

对这个系统来说，分别满足 C、A 和 P 指的是：

在满足一致性 C 的情况下，Server1 和 Server2 中的数据库始终保持一致，即 DB1 和 DB2 内容要始终保持相同；

在满足可用性 A 的情况下，用户无论访问 Server1 还是 Server2，都会得到即时响应；

在满足分区容错性 P 的情况下，Server1 和 Server2 之间即使出现网络故障也不会影响 Server1 和 Server2 分别处理用户的请求。

当用户发起请求时，收到请求的服务器会及时响应，并将用户更新的数据同步到另一台服务器，保证数据一致性。具体的工作流程，如下所示：

用户 User1 向服务器 Server1 发起请求，将数据库 DB1 中的数据 a 由 1 改为 2；

系统会进行数据同步，即图中的 S 操作，将 Server1 中 DB1 的修改同步到服务器 Server2 中，使得 DB2 中的数据 a 也被修改为 2；

当 User2 向 Server2 发起读取数据 a 的请求时，会得到 a 最新的数据值 2。

这其实是在网络环境稳定、系统无故障的情况下的工作流程。但在实际场景中，网络环境不可能百分之百不出故障，比如网络拥塞、网卡故障等，会导致网络故障或不通，从而导致节点之间无法通信，或者集群中节点被划分为多个分区，分区中的节点之间可通信，分区间不可通信。

这种由网络故障导致的集群分区情况，通常被称为“网络分区”。

在分布式系统中，网络分区不可避免，因此分区容错性 P 必须满足。接下来，我们就来讨论一下在满足分区容错性 P 的情况下，一致性 C 和可用性 A 是否可以同时满足。

假设，Server1 和 Server2 之间网络出现故障，User1 向 Server1 发送请求，将数据库 DB1 中的数据 a 由 1 修改为 2，而 Server2 由于与 Server1 无法连接导致数据无法同步，所以 DB2 中 a 依旧是 1。这时，User2 向 Server2 发送读取数据 a 的请求时，Server2 无法给用户返回最新数据，那么该如何处理呢？

我们能想到的处理方式有如下两种。

第一种处理方式是，保证一致性 C，牺牲可用性 A：Server2 选择让 User2 的请求阻塞，一直等到网络恢复正常，Server1 被修改的数据同步更新到 Server2 之后，即 DB2 中数据 a 修改成最新值 2 后，再给用户 User2 响应。

第二种处理方式是，保证可用性 A，牺牲一致性 C：Server2 选择将旧的数据 a=1 返回给用户，等到网络恢复，再进行数据同步。

除了以上这两种方案，没有其他方案可以选择。可以看出：在满足分区容错性 P 的前提下，一致性 C 和可用性 A 只能选择一个，无法同时满足。

CAP 选择策略及应用

通过上面的分析，你已经知道了分布式系统无法同时满足 CAP 这三个特性，那该如何进行取舍呢？

其实，C、A 和 P，没有谁优谁劣，只是不同的分布式场景适合不同的策略。接下来，我就以一些具体场景为例，分别与你介绍保 CA 弃 P、保 CP 弃 A、保 AP 弃 C 这三种策略，以帮助你面对不同的分布式场景时，知道如何权衡这三个特征。

比如，对于涉及钱的交易时，数据的一致性至关重要，因此保 CP 弃 A 应该是最佳选择。2015 年发生的支付宝光纤被挖断的事件，就导致支付宝就出现了不可用的情况。显然，支付宝当时的处理策略就是，保证了 CP 而牺牲了 A。

而对于其他场景，大多数情况下的做法是选择 AP 而牺牲 C，因为很多情况下不需要太强的一致性（数据始终保持一致），只要满足最终一致性即可。

最终一致性指的是，不要求集群中节点数据每时每刻保持一致，在可接受的时间内最终能达到一致就可以了。不知道你是否还记得，在第 6 篇文章分布式事务中介绍的基于分布式消息的最终一致性方案？没错，这个方案对事务的处理，就是选择 AP 而牺牲 C 的例子。

这个方案中，在应用节点之间引入了消息中间件，不同节点之间通过消息中间件进行交互，比如主应用节点要执行修改数据的事务，只需要将信息推送到消息中间件，即可执行本地的事务，而不需要备应用节点同意修改数据才能真正执行本地事务，备应用节点可以从消息中间件获取数据。

保 CA 弃 P

首先，我们看一下保 CA 弃 P 的策略。

在分布式系统中，现在的网络基础设施无法做到始终保持稳定，网络分区（网络不连通）难以避免。牺牲分区容错性 P，就相当于放弃使用分布式系统。因此，在分布式系统中，这种策略不需要过多讨论。

既然分布式系统不能采用这种策略，那单点系统毫无疑问就需要满足 CA 特性了。比如关系型数据库 DBMS（比如 MySQL、Oracle）部署在单台机器上，因为不存在网络通信问题，所以保证 CA 就可以了。

保 CP 弃 A

如果一个分布式场景需要很强的数据一致性，或者该场景可以容忍系统长时间无响应的情况下，保 CP 弃 A 这个策略就比较适合。

一个保证 CP 而舍弃 A 的分布式系统，一旦发生网络分区会导致数据无法同步情况，就要牺牲系统的可用性，降低用户体验，直到节点数据达到一致后再响应用户。

我刚刚也提到了，这种策略通常用在涉及金钱交易的分布式场景下，因为它任何时候都不允许出现数据不一致的情况，否则就会给用户造成损失。因此，这种场景下必须保证 CP。

保证 CP 的系统有很多，典型的有 Redis、HBase、ZooKeeper 等。接下来，我就以 ZooKeeper 为例，带你了解它是如何保证 CP 的。

首先，我们看一下 ZooKeeper 架构图。

备注：此图引自ZooKeeper 官网。

ZooKeeper 集群包含多个节点（Server），这些节点会通过分布式选举算法选出一个 Leader 节点。在 ZooKeeper 中选举 Leader 节点采用的是 ZAB 算法，你可以再回顾下第 4 篇文章中的相关内容。

在 ZooKeeper 集群中，Leader 节点之外的节点被称为 Follower 节点，Leader 节点会专门负责处理用户的写请求：

当用户向节点发送写请求时，如果请求的节点刚好是 Leader，那就直接处理该请求；

如果请求的是 Follower 节点，那该节点会将请求转给 Leader，然后 Leader 会先向所有的 Follower 发出一个 Proposal，等超过一半的节点同意后，Leader 才会提交这次写操作，从而保证了数据的强一致性。

具体示意图如下所示：

当出现网络分区时，如果其中一个分区的节点数大于集群总节点数的一半，那么这个分区可以再选出一个 Leader，仍然对用户提供服务，但在选出 Leader 之前，不能正常为用户提供服务；如果形成的分区中，没有一个分区的节点数大于集群总节点数的一半，那么系统不能正常为用户提供服务，必须待网络恢复后，才能正常提供服务。

这种设计方式保证了分区容错性，但牺牲了一定的系统可用性。

保 AP 弃 C

如果一个分布式场景需要很高的可用性，或者说在网络状况不太好的情况下，该场景允许数据暂时不一致，那这种情况下就可以牺牲一定的一致性了。

网络分区出现后，各个节点之间数据无法马上同步，为了保证高可用，分布式系统需要即刻响应用户的请求。但，此时可能某些节点还没有拿到最新数据，只能将本地旧的数据返回给用户，从而导致数据不一致的情况。

适合保证 AP 放弃 C 的场景有很多。比如，很多查询网站、电商系统中的商品查询等，用户体验非常重要，所以大多会保证系统的可用性，而牺牲一定的数据一致性。

以电商购物系统为例，如下图所示，某电吹风在北京仓库有 20 个，在杭州仓库有 10 个，在上海仓库有 30 个。初始时，北京、杭州、上海分别建立的服务器{A, B, C}存储该电吹风的数量均为 60 个。

假如，上海的网络出现了问题，与北京和杭州网络均不通，此时北京的用户通过北京服务器 A 下单购买了一个电吹风，电吹风数量减少到 59，并且同步给了杭州服务器 B。也就是说，现在用户的查询请求如果是提交到服务器 A 和 B，那么查询到的数量为 59。但通过上海服务器 C 进行查询的结果，却是 60。

当然，待网络恢复后，服务器 A 和 B 的数据会同步到 C，C 更新数据为 59，最终三台服务器数据保持一致，用户刷新一下查询界面或重新提交一下查询，就可以得到最新的数据。而对用户来说，他们并不会感知到前后数据的差异，到底是因为其他用户购买导致的，还是因为网络故障导致数据不同步而产生的。

当然，你可能会说，为什么上海服务器不能等网络恢复后，再响应用户请求呢？可以想象一下，如果用户提交一个查询请求，需要等上几分钟、几小时才能得到反馈，那么用户早已离去了。

也就是说这种场景适合优先保证 AP，因为如果等到数据一致之后再给用户返回的话，用户的响应太慢，可能会造成严重的用户流失。

目前，采用保 AP 弃 C 的系统也有很多，比如 CoachDB、Eureka、Cassandra、DynamoDB 等。

对比分析

保 CA 弃 P、保 CP 弃 A 和保 AP 弃 C 这三种策略，以方便你记忆和理解。

知识扩展：CAP 和 ACID 的“C”“A”是一样的吗？

首先，我们看一下 CAP 中的 C 和 ACID 中的 C 是否一致。

CAP 中的 C 强调的是数据的一致性，也就是集群中节点之间通过复制技术保证每个节点上的数据在同一时刻是相同的。

ACID 中的 C 强调的是事务执行前后，数据的完整性保持一致或满足完整性约束。也就是不管在什么时候，不管并发事务有多少，事务在分布式系统中的状态始终保持一致。具体原理可参考第 6 篇文章“分布式事务：All or Nothing”。

其次，我们看一下 CAP 中的 A 和 ACID 中的 A。

CAP 中的 A 指的是可用性（Availability），也就是系统提供的服务一直处于可用状态，即对于用户的请求可即时响应。

ACID 中的 A 指的是原子性（Atomicity），强调的是事务要么执行成功，要么执行失败。

因此，CAP 和 ACID 中的“C”和“A”是不一样的，不能混为一谈。

总结

今天，我主要与你分享的是 CAP 理论。

首先，我通过电商的例子带你了解来 CAP 这三个字母在分布式系统中的含义以及 CAP 理论，并与你证明了，C、A 和 P 在分布式系统中最多只能满足两个。

然后，我为你介绍了分布式系统设计时如何选择 CAP 策略，包括保 CA 弃 P、保 CP 弃 A、保 AP 弃 C，以及这三种策略适用的场景。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

相信通过今天的学习，你不仅对 CAP 理论有了更深刻的认识，并且可以针对不同场景采用哪种策略给出自己的建议。加油，行动起来，为你的业务场景选择一种合适的策略，来指导分布式系统的设计吧。相信你，一定可以的！

思考题

CAP 理论和 BASE 理论的区别是什么？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

24 | 分布式数据存储系统之三要素：顾客、导购与货架

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章中，我们一起学习了 CAP 理论。该理论指出，在分布式系统中，不能同时满足一致性、可用性和分区容错性，指导了分布式数据存储系统的设计。

随着数据量和访问量的增加，单机性能已经不能满足用户需求，分布式集群存储成为一种常用方式。把数据分布在多台存储节点上，可以为大规模应用提供大容量、高性能、高可用、高扩展的存储服务。而，分布式存储系统就是其具体实现。

在今天这篇文章，我将带你学习分布式存储系统的关键三要素，让你对分布式数据存储系统有一个直观的理解。在后面几篇文章中，我会针对这三要素中的关键技术进一步展开，以帮助你更深入地理解分布式数据存储系统。

接下来，我们就一起打卡分布式存储系统的三要素：顾客、导购与货架。

什么是分布式数据存储系统？

分布式存储系统的核心逻辑，就是将用户需要存储的数据根据某种规则存储到不同的机器上，当用户想要获取指定数据时，再按照规则到存储数据的机器里获取。

如下图所示，当用户（即应用程序）想要访问数据 D，分布式操作引擎通过一些映射方式，比如 Hash、一致性 Hash、数据范围分类等，将用户引导至数据 D 所属的存储节点获取数据。

静下心来想一下，获取数据的整个过程与你到商店购物的过程是不是有些类似呢？

顾客到商店购物时，导购会根据顾客想要购买的商品引导顾客到相应的货架，然后顾客从这个货架上获取要购买的商品，完成购物。这里的顾客就是图中的应用程序，导购就相当于分布式操作引擎，它会按照一定的规则找到相应的货架，货架就是存储数据的不同机器节点。

其实，这个过程就是分布式存储系统中获取数据的通用流程，顾客、导购和货架组成了分布式存储系统的三要素，分别对应着分布式领域中的数据生产者 / 消费者、数据索引和数据存储。

接下来，我们就详细看看这三个要素吧。

分布式数据存储系统三要素

顾客就是数据的生产者和消费者，也就是说顾客代表两类角色，生产者会生产数据（比如，商店购物例子中的供货商就属于生产类顾客），将数据存储到分布式数据存储系统中，消费者是从分布式数据存储系统中获取数据进行消费（比如，商店购物例子中购买商品的用户就属于消费类顾客）；导购就是数据索引，将访问数据的请求转发到数据所在的存储节点；货架就是存储设备，用于存储数据。

顾客：生产和消费数据

顾客相当于分布式存储系统中的应用程序，而数据是应用程序的原动力。根据数据的产生和使用，顾客分为生产者和消费者两种类型。生产者负责给存储系统添加数据，而消费者则可以使用系统中存储的数据。

就像是火车票存储系统，如图所示，铁路局就相当于生产者类型的顾客，而乘客就相当于消费者类型的顾客。铁路局将各个线路的火车票信息发布到订票网站的后台数据库中，乘客通过订票网站访问数据库，来进行查询余票、订票、退票等操作。

生产者和消费者生产和消费的数据通常是多种多样的，不同应用场景中数据的类型、格式等都不一样。根据数据的特征，这些不同的数据通常被划分为三类：结构化数据、半结构化数据和非结构化数据。

结构化数据通常是指关系模型数据，其特征是数据关联较大、格式固定。火车票信息比如起点站、终点站、车次、票价等，就是一种结构化数据。结构化数据具有格式固定的特征，因此一般采用分布式关系数据库进行存储和查询。

半结构化数据通常是指非关系模型的，有基本固定结构模式的数据，其特征是数据之间关系比较简单。比如 HTML 文档，使用标签书写内容。半结构化数据大多可以采用键值对形式来表示，比如 HTML 文档可以将标签设置为 key，标签对应的内容可以设置为 value，因此一般采用分布式键值系统进行存储和使用。

非结构化数据是指没有固定模式的数据，其特征是数据之间关联不大。比如文本数据就是一种非结构化数据。这种数据可以存储到文档中，通过 ElasticSearch（一个分布式全文搜索引擎）等进行检索。

导购：确定数据位置

导购是分布式存储系统必不可少的要素，如果没有导购， 顾客就需要逐个货架去寻找自己想要的商品。

想象一下，如果你去订票网站订火车票，按照自己的需求点击查询车票后，系统会逐个扫描分布式存储系统中每台机器的数据，寻找你想要购买的火车票。如果系统中存储的数据不多，响应时间也不会太长，毕竟计算机的速度还是很快的；但如果数据分布在几千台甚至上万台机器中，系统逐个机器扫描后再给你响应，我相信你会对这个订票网站很失望。

这种定位数据存储位置的方式会浪费你很多时间，严重影响购票体验。因此，在分布式存储系统中，必须有相应的数据导购，否则系统响应会很慢，效率很低。为解决这个问题，数据分片技术就走入了分布式存储系统的大家庭。

数据分片技术，是指分布式存储系统按照一定的规则将数据存储到相对应的存储节点中，或者到相对应的存储节点中获取想要的数据，这是一种很常用的导购技术。这种技术，一方面可以降低单个存储节点的存储和访问压力；另一方面，可以通过规定好的规则快速找到数据所在的存储节点，从而大大降低搜索延迟，提高用户体验。

也就是说，当铁路局发布各个线路的火车票信息时，会按照一定规则存储到相应的机器中，比如北京到上海的火车票存储到机器 A 中，西安到重庆的火车票存储到机器 B 中。当乘客查询火车票时，系统就可以根据查询条件迅速定位到相对应的存储机器，然后将数据返回给用户，响应时间就大大缩短了。如图所示，当查询北京 - 上海的火车票相关信息时，可以与机器 A 进行数据交互。

这个例子中按照数据起点、终点的方式划分数据，将数据分为几部分存储到不同的机器节点中，就是数据分片技术的一种。当查询数据时，系统可以根据查询条件迅速找到对应的存储节点，从而实现快速响应。

上述的例子中，按照数据特征进行了数据分片，当然，还有其他很多数据分片的方案。比如，按照数据范围，采用哈希映射、一致性哈希环等对数据划分。我会在下一篇文章中，与你详细讲述哈希和一致性哈希的内容。

接下来，我就针对数据范围这种数据分片方案做一个具体介绍吧。

针对数据范围的数据分片方案是指，按照某种规则划分数据范围，然后将在这个范围内的数据归属到一个集合中。这就好比数学中通常讲的整数区间，比如 1～1000 的整数，[1,100] 的整数属于一个子集、[101,1000] 的整数属于另一个子集。

对于前面讲的火车票的案例，按照数据范围分片的话，可以将属于某条线的所有火车票数据划分到一个子集或分区进行存储，比如机器 A 存储京广线的火车票数据，机器 B 存储京沪线的火车票数据。也就是说，数据范围的方案是按照范围或区间进行存储或查询。

如图所示，当用户查询北京 - 上海的火车票相关信息时，首先判断查询条件属于哪个范围，由于北京 - 上海的火车线路属于京沪线，因此系统按照规则将查询请求转到存取京沪线火车票数据的机器 B，然后由机器 B 进行处理并给用户返回响应结果。

为了提高分布式系统的可用性与可靠性，除了通过数据分片减少单个节点的压力外，数据复制也是一个非常重要的方法。数据复制就是将数据进行备份，以使得多个节点存储该数据。

想象一下，当某个存储节点出现故障时，如果只采用数据分片技术，那这个节点的数据就会丢失，从而给用户造成损失。因此，数据复制在分布式存储系统中是不可或缺的。关于数据复制技术，我会在第 26 篇文章中与你详细讲解。

接下来，我与你说说数据复制和数据分片技术的区别吧。关于它们之间的区别，你可以先看看下面这张图片：

数据 A 被拆分为两部分存储在两个节点 Node1 和 Node2 上，属于数据分片；而对数据 B 来说，同一份完整的数据在两个节点中均有存储，就属于数据复制。

在实际的分布式存储系统中，数据分片和数据复制通常是共存的：

数据通过分片方式存储到不同的节点上，以减少单节点的性能瓶颈问题；

而数据的存储通常用主备方式保证可靠性，也就是对每个节点上存储的分片数据，采用主备方式存储，以保证数据的可靠性。其中，主备节点上数据的一致，是通过数据复制技术实现的。

讲到这里，我们再回忆下第 20 篇文章中涉及的 Kafka 集群的总体架构图吧。我从中抽取出 Kafka 集群消息存储架构图，如下所示。

消息数据以 Partition（分区）进行存储，一个 Topic（主题）可以由多个 Partition 进行存储，Partition 可以分布到多个 Broker 中；同时，Kafka 还提供了 Partition 副本机制（对分区存储的信息进行备份，比如 Broker 1 中的 Topic-1 Partion-0 是对 Broker 0 上的 Topic-1 Partition-0 进行的备份），从而保证了消息存储的可靠性。

这就是数据分片和数据复制共存的一个典型应用场景。

货架：存储数据

货架是用来存储数据的，因为数据是由顾客产生和消费的，因此货架存储的数据类型与顾客产生和消费的数据类型是一致的，即包括结构化数据、半结构化数据和非结构化数据。

针对这三种不同的数据类型，存储“货架”可以大致划分为以下三种：

分布式数据库，通过表格来存储结构化数据，方便查找。常用的分布式数据库有 MySQL Sharding、Microsoft SQL Azure、Google Spanner、Alibaba OceanBase 等。

分布式键值系统，通过键值对来存储半结构化数据。常用的分布式键值系统有 Redis、Memcache 等，可用作缓存系统。具体的缓存技术我将在第 27 篇文章“分布式数据之缓存技术：‘身手钥钱’随身带”中与你详细介绍。

分布式存储系统，通过文件、块、对象等来存储非结构化数据。常见的分布式存储系统有 Ceph、GFS、HDFS、Swift 等。

而对货架材料也就是存储介质的选择，本质就是选择将数据存储在磁盘还是内存（缓存）上：

磁盘存储量大，但 IO 开销大，访问速度较低，常用于存储不经常使用的数据。比如，电商系统中，排名比较靠后或购买量比较少、甚至无人购买的商品信息，通常就存储在磁盘上。

内存容量小，访问速度快，因此常用于存储需要经常访问的数据。比如，电商系统中，购买量比较多或排名比较靠前的商品信息，通常就存储在内存中。

知识扩展：业界主流的分布式数据存储系统有哪些？

在前面介绍货架的时候，我有提到针对结构化数据、半结构化数据和非结构化数据，分别对应不同的“货架”，即分布式数据库、分布式键值系统和分布式文件系统进行存储。

对于分布式键值系统，我会在第 27 篇文章中进行讲解，并与你介绍和分析主流存储系统。

所以在这里，我就重点与你对比分析分布式数据库和分布式文件系统的几款主流的系统，以便于你理解和选型。

首先，我们看一下主流的分布式数据库，主要包括 MySQL Sharding、SQL Azure、Spanner、OceanBase 等，具体对比分析如下表所示。

然后，我们看一下主流的分布式存储系统，主要包括 Ceph、GFS、HDFS 和 Swift 等，具体对比分析如下所示。

总结

今天，我主要与你分享的是分布式数据存储系统的三要素，即顾客、导购和货架，对应到分布式领域的术语就是数据生产者 / 消费者、数据索引和数据存储。

其中，顾客包括产生数据的顾客和消费数据的顾客两类；导购，就是数据索引引擎，包括数据存储时确定数据位置，以及获取数据时确定数据所在位置；货架，负责数据存储，包括磁盘、缓存等存储介质等。

不同应用场景中，顾客产生的数据类型、格式等通常都不一样。根据数据的特征，这些不同的数据可以被划分为三类：结构化数据、半结构化数据和非结构化数据。与之相对应的，货架也就是数据存储系统，也包括三类：分布式数据库、分布式键值系统和分布式文件系统。

针对分布式数据库和分布式文件系统的主流框架，我在“知识扩展模块”进行了对比分析，以方便你理解、记忆与应用。而对于分布式键值系统，我将在第 27 篇文章中进行详细介绍。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

相信通过今天的学习，你对分布式数据存储有了更深入的理解，对其中的核心角色和关键技术也有个更清晰的认识。加油，和我一起学习后面的章节，一起揭开分布式数据存储系统的神秘面纱吧！

思考题

传统单机关系型数据库与分布式数据库的区别是什么？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

25 | 数据分布方式之哈希与一致性哈希：“掐指一算”与“掐指两算”的事

你好！我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章中，我带你了解了分布式存储系统的三个要素：顾客、导购和货架。其中，导购实现了分布式数据存储系统中数据索引的功能，包括存储数据时确定存储位置，以及获取数据时确定数据所在位置。

那么，在分布式系统中，具体是如何实现数据索引或数据分布的呢？目前最常用的方法就是哈希和一致性哈希。

接下来，我们就一起打卡数据分布式方式中的哈希与一致性哈希吧。

首先，我们来看一下数据分布设计的原则。数据分布设计原则是分布式存储系统设计的基本原则，指导了哈希和一致性哈希方法的选择和应用。

数据分布设计原则

其实，这里的数据分布，主要就是数据分片。相信你还记得，我在第 24 篇文章中与你分享分布式存储系统的导购时，已经和你提到数据分片技术，它解决了确定数据位置的问题，并着重与你讲述了按照数据特征进行划分的分片方法。今天，我主要与你讲解按照数据范围，采用哈希、一致性哈希等对数据划分的方法。

假设，现在有上百 G 数据需要进行分布式存储，也就是要存储到不同的节点上。提到这个问题，你可能立刻就会想到很多种方法，比如随机分布、范围分布、映射分布等。那么，我们应该如何选择到底要使用哪种方法呢？

在分布式数据存储系统中，存储方案选型时，通常会考虑数据均匀、数据稳定和节点异构性这三个维度。

从数据均匀的维度考虑，主要包括两个方面：

不同存储节点中存储的数据要尽量均衡，避免让某一个或某几个节点存储压力过大，而其他节点却几乎没什么数据。比如，现在有 100G 数据，4 个同类型节点，通常希望数据存储时尽可能均衡，比如每个节点存储 25G 数据。

另外，用户访问也要做到均衡，避免出现某一个或某几个节点的访问量很大，但其他节点却无人问津的情况。比如，现在有 1000 个请求，对于上述存储数据的 4 个节点，处理用户访问请求尽量均衡，比如每个节点处理 250 个请求，当然这是非常理想的情况，实际情况下，每个节点之间相差不太大即可。

从数据稳定的维度考虑，当存储节点出现故障需要移除或者扩增时，数据按照分布规则得到的结果应该尽量保持稳定，不要出现大范围的数据迁移。

比如，现有 100G 数据，刚开始有 4 个同类型节点（节点 1~4），每个节点存储 25G 数据，现在节点 2 故障了，也就是说每个节点需要存储 100G/3 数据。

数据稳定，就是尽可能只迁移节点 2 上的数据到其他节点上，而不需要对大范围或所有数据进行迁移存储。当然，如果有扩展同类型节点，也是尽可能小范围迁移数据到扩展的节点上。具体的迁移方法，可以采用下文介绍的一致性哈希方法。

从节点异构性的维度考虑，不同存储节点的硬件配置可能差别很大。比如，有的节点硬件配置很高，可以存储大量数据，也可以承受更多的请求；但，有的节点硬件配置就不怎么样，存储的数据量不能过多，用户访问也不能过多。

如果这种差别很大的节点，分到的数据量、用户访问量都差不多，本质就是一种不均衡。所以，一个好的数据分布算法应该考虑节点异构性。

当然，除了上面这 3 个维度外，我们一般还会考虑隔离故障域、性能稳定性等因素。

隔离故障域，是为了保证数据的可用和可靠性。比如，我们通常通过备份来实现数据的可靠性。但如果每个数据及它的备份，被分布到了同一块硬盘或节点上，就有点违背备份的初衷了。所以，一个好的数据分布算法，应该为每个数据映射一组存储节点，这些节点应该尽量在不同的故障域，比如不同机房、不同机架等。

性能稳定性是指，数据存储和查询的效率要有保证，不能因为节点的添加或者移除，造成存储或访问性能的严重下降。

了解了数据分布的设计原则后，接下来我们再看看主流的数据分布式方法，哈希和一致性哈希吧。其中，哈希和一致性哈希是数据分布的基础方法，在不同场景下，数据分布设计的原则需要考虑的维度也不一样。随着维度的增加，一致性哈希又可进一步演进为带有限负载的一致性哈希和带虚拟节点的一致性哈希方法。

接下来，我们就一起看看这 4 种方法的具体原理和应用场景吧。

数据分布方法

哈希是指，将数据按照提前规定好的函数（哈希函数）映射到相应的存储节点，即进行一个哈希计算，得到的结果就是数据应该存储的节点。

一致性哈希同样是采用哈希函数，进行两步哈希：

对存储节点进行哈希计算，也就是对存储节点做哈希映射；

当对数据进行存储或访问时，首先对数据进行映射得到一个结果，然后找到比该结果大的第一个存储节点，就是该数据应该存储的地方。我会在下面的内容中，与你详细介绍其中的原理。

总结来讲，哈希是一步计算直接得到相应的存储节点，而一致性哈希需要两步才可以找到相应的存储节点。这，是不是就是“掐指一算”与“掐指两算”的事呢？

接下来，我们先一起看看哈希的具体原理吧。

哈希

哈希是一种非常常用的数据分布方法，其核心思想是，首先确定一个哈希函数，然后通过计算得到对应的存储节点。我们通过一个具体的例子来看一下吧。

假设，有三个存储节点，分别为 Node1、Node2 和 Node3；现有以下数据，ID 的范围为 [0,1000]：D0:{ id:100, name:‘a0’}、D1:{ id:200, name:‘a1’} 、D2:{ id:300, name:‘a2’}、D3:{ id:400, name:‘a3’}、D4:{ id:500, name:‘a4’}、D5:{ id:600, name:‘a5’}和 D6:{ id:700, name:‘a6’}。

假设，哈希函数为“id% 节点个数”，通过计算可以得到每个数据应该存入的节点。在这个例子中，哈希函数是“id%3”，结果为 0 的数据存入 Node1、结果为 1 的数据存入 Node2、结果为 2 的数据存入 Node3。

如图所示，Node1 将存储数据 D2（300%3=0）和 D5（600%3=0），Node2 将存储数据 D0（100%3=1）、D3（400%3=1）和 D6（700%3=1），Node3 将存储数据 D1（200%3=2）和 D4（500%3=2）。

可以看出，哈希算法的一个优点是，只要哈希函数设置得当，可以很好地保证数据均匀性，但有一个较为严重的缺点，就是稳定性较差。

比如，随着数据量的增加，三个节点的容量无法再满足存储需求了，需要再添加一个节点。这时，哈希函数变成了 id%4，原先存储在那三个节点的数据需要重新计算，然后存入相应节点，即需要大规模的数据迁移，显然会降低稳定性。

所以，哈希方法适用于同类型节点且节点数量比较固定的场景。目前，Redis 就使用了哈希方法，你可以再回顾下第 10 篇文章“分布式体系结构之非集中式结构：众生平等”中的相关内容。

接下来，我们再看看一致性哈希吧。

一致性哈希

一致性哈希是指将存储节点和数据都映射到一个首尾相连的哈希环上，存储节点可以根据 IP 地址进行哈希，数据通常通过顺时针方向寻找的方式，来确定自己所属的存储节点，即从数据映射在环上的位置开始，顺时针方向找到的第一个存储节点。

我们看看如何用一致性哈希方法，来实现上述案例的数据存储吧。

如图所示，假设数据 D0～D7 按照 ID 进行等值映射，即映射值与 ID 值相等，比如数据 D0 映射到哈希环上的值为 100，数据 D1 映射到哈希环上的值为 200······；同时，假设存储节点 Node1、Node2 和 Node3 映射到哈希环上的值分别为 400、600、900。

按照规则，D0，D1，D2 和 D3 顺时针方向的下一个存储节点为 Node1，因此 Node1 将存储数据 D0（id = 100）、D1（id = 200）、D2（id = 300）和 D3（id = 400）；同理，Node2 将存取数据 D4（id = 500）和 D5（id = 600），Node3 将存取数据 D6（id = 700）。

可以看出，一致性哈希是对哈希方法的改进，在数据存储时采用哈希方式确定存储位置的基础上，又增加了一层哈希，也就是在数据存储前，对存储节点预先进行了哈希。

这种改进可以很好地解决哈希方法存在的稳定性问题。当节点加入或退出时，仅影响该节点在哈希环上顺时针相邻的后继节点。比如，当 Node2 发生故障需要移除时，由于 Node3 是 Node2 顺时针方向的后继节点，本应存储到 Node2 的数据就会存储到 Node3 中，其他节点不会受到影响，因此不会发生大规模的数据迁移。

所以，一致性哈希方法比较适合同类型节点、节点规模会发生变化的场景。目前，Cassandra 就使用了一致性哈希方法，你可以再回顾下第 10 篇文章“分布式体系结构之非集中式结构：众生平等”中的相关内容。

一致性哈希方法虽然提升了稳定性，但随之而来的均匀性问题也比较明显，即对后继节点的负载会变大。有节点退出后，该节点的后继节点需要承担该节点的所有负载，如果后继节点承受不住，便会出现节点故障，导致后继节点的后继节点也面临同样的问题。

那么，有没有更好的方法来解决这个问题呢？

Google 在 2017 年提出了带有限负载的一致性哈希算法，就对这个问题做了一些优化。

带有限负载的一致性哈希

带有限负载的一致性哈希方法的核心原理是，给每个存储节点设置了一个存储上限值来控制存储节点添加或移除造成的数据不均匀。当数据按照一致性哈希算法找到相应的存储节点时，要先判断该存储节点是否达到了存储上限；如果已经达到了上限，则需要继续寻找该存储节点顺时针方向之后的节点进行存储。

我们看看如何用带有限负载的一致性哈希方法，来实现上述案例的数据存储吧。

如图所示，假设每个存储节点设置的上限值为 3，按照一致性哈希算法，当存储数据 D3（id = 400）时，会发现应该存储到 Node1 中，但 Node1 已经存储了三个数据 D0（id = 100）、D1（id = 200）和 D2（id = 300），达到了存储上限，因此会存储到该节点顺时针方向的下一个节点 Node2 中。当然，在存储前，也会先检查 Node2 是否达到了存储上限，如果达到了，会继续寻找其他节点。

如果你想要了解该算法的详细内容，可以阅读“Consistent Hashing with Bounded Loads”这篇论文。

带有限负载的一致性哈希方法比较适合同类型节点、节点规模会发生变化的场景。目前，在 Google Cloud Pub/Sub、HAProxy 中已经实现该方法，应用于 Google、Vimeo 等公司的负载均衡项目中。

其实，哈希、一致性哈希、带有限负载的一致性哈希，都没有考虑节点异构性的问题。如果存储节点的性能好坏不一，数据分布方案还按照这些方法的话，其实还是没做到数据的均匀分布。

接下来，我们再看一种主要针对存储节点为异构节点场景的方法，即带虚拟节点的一致性哈希吧。

带虚拟节点的一致性哈希

带虚拟节点的一致性哈希方法，核心思想是根据每个节点的性能，为每个节点划分不同数量的虚拟节点，并将这些虚拟节点映射到哈希环中，然后再按照一致性哈希算法进行数据映射和存储。

假设，Node1 性能最差，Node2 性能一般，Node3 性能最好。以 Node1 的性能作为参考基准，Node2 是 Node1 的 2 倍，Node3 是 Node1 的 3 倍。

因此，Node1 对应一个虚拟节点 Node1_1，Node2 对应 2 个虚拟节点 Node2_1 和 Node2_2，Node3 对应 3 个虚拟节点 Node3_1、Node3_2 和 Node3_3。

假设，虚拟节点 Node1_1、Node2_1、Node2_2、Node3_1、Node3_2、Node3_3 的哈希值，分别为 100、200、300、400、500、600。

那么，按照带虚拟节点的哈希一致性方法， 数据 D0 和 D6 按顺时针方向的下一个虚拟存储节点为 Node 1-1，因此节点 Node1 将会存储数据 D0（id = 100）和 D6（id = 700）；同理，Node2 将会存储数据 D1（id = 200）和 D2（id = 300），Node3 将会存储数据 D3（id = 400）、D4（id = 500）和 D5（id = 600）。

可以看出，带虚拟节点的一致性哈希方法比较适合异构节点、节点规模会发生变化的场景。目前 Memcached 缓存系统实现了该方法，我会在第 27 篇文章中与你详细分析。

这种方法不仅解决了节点异构性问题，还提高了系统的稳定性。当节点变化时，会有多个节点共同分担系统的变化，因此稳定性更高。

比如，当某个节点被移除时，对应该节点的多个虚拟节点均会移除，而这些虚拟节点按顺时针方向的下一个虚拟节点，可能会对应不同的物理节点，即这些不同的物理节点共同分担了节点变化导致的压力。

当然，这种方法引入了虚拟节点，增加了节点规模，从而增加了节点的维护和管理的复杂度，比如新增一个节点或一个节点故障时，对应到虚拟节点构建的哈希环上需要新增和删除多个节点，数据的迁移等操作相应地也会很复杂。

四种数据分布方法对比

为方便理解与记忆，我再通过一个表格和你对比分析下这四种方法吧。请注意，以下方法之间的对比都是相对的比较，实际性能优劣与哈希函数的设定以及具体的数据场景密切相关。

知识扩展：数据分片和数据分区，有何区别？

首先，我们一起回忆一下第 20 篇文章中提到的数据分区。

数据分区是从数据存储块的维度进行划分，不同的分区物理上归属于不同的节点。比如，现在有 2 个节点 Node1 和 Node2，2 个数据分区 Partition1 和 Partition2，Partition1 属于 Node1、Partition2 属于 Node2。

对于数据分区，可用于存储不同的数据，也可以用来存储相同的数据实现数据备份。数据分区可以归结为是“货架”相关的关键技术，也就是为数据存储提供合适的位置。具体实例，参见第 20 篇文章中介绍的 Kafka 分区的内容。

接下来，我们再看一下数据分片。

数据分片是从数据的维度进行划分，是指将一个数据集合按照一定的方式划分为多个数据子集，不同的数据子集存在不同的存储块上，而这些存储块可以在不同的节点上，也可以在同一节点上。

具体的数据分片策略可以采用我今天和你分享的哈希、一致性哈希等方法。数据分片是实现“导购”的关键技术，目的是构建索引，为数据确定位置，包括存储数据和查询数据时确定数据位置。具体例子，可参考上文的相关内容。

由此可见，数据分片和数据分区是两个不同的概念，且属于分布式存储系统中不同角色的技术，前者是实现“导购”的关键技术，后者是“货架”相关的技术，不可直接等同。

但正因为一个是导购相关的关键技术，一个是货架相关的技术，一个提供确定数据索引的位置，一个提供合适的数据存储位置，因此正如我在第 24 篇文章中所说，这两个技术是可以共存的，比如下面这个例子。

有 3 个节点{Node1, Node2, Node3}，有 3 个分区{Partition1, Partition2, Partition3}用于存储用户信息，每个节点上 1 个分区。现在有 1000 个用户信息需要存储，用户 id 编号为 [1,1000]，为防止将所有信息存储到一个节点上，所有用户发起请求时，该节点成为瓶颈，为此需要将这 1000 个用户信息存储到 3 个节点上。

假设，我们采用最简单的哈希方法，用户 id% 节点总数 (3) 进行哈希映射，id%3 = 0 的所有用户信息存储到节点 1 的 Partition1， id%3=1 的所有用户信息存储到节点 2 的 Partition2，id%3=2 的所有用户信息存储到节点 3 的 Partition3。

总结

今天，我主要带你学习了数据分布式方法中的哈希与一致性哈希。

首先，我带你了解了分布式数据存储系统中，设计数据分布方法需要考虑的原则，主要包括数据均匀性、稳定性和节点异构性。

其次，基于数据分布设计原则，我为你介绍了哈希、一致性哈希、带有限负载的一致性哈希和带虚拟节点的一致性哈希方法，并以例子进行辅助讲解，以便于你理解和学习。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

加油，相信你通过对本讲的学习，对分布式数据存储系统中“导购”的角色有了更深入的理解，对分布式数据分布方法的原理，以及如何针对不同场景进行选型有了一定的认知。加油，赶紧行动起来，为你的应用场景选择一种合适的数据分布方法，并实践起来吧！

思考题

今天，我主要与你分享的是哈希方法相关的数据分布方法，你还知道哪些常用的数据分布方法吗？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

26 | 分布式数据复制技术：分身有术

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章中，我为你讲解了数据分布（也称数据分片）技术，主要用于构建数据索引，是实现“导购”功能的关键技术。数据分布的本质是，将原数据集划分为多个数据子集，以存储到不同的地方，在一定程度上体现了数据的可用性和可靠性（一个存储节点故障，只影响该存储节点的数据）。

我在第 24 讲中介绍“导购”时提到，数据分片和数据复制技术均是实现“导购”的关键技术。其中，数据分片是确定数据位置，数据复制是实现数据可靠性的关键方法。

在实际情况下，仅考虑数据分片，其实是无法真正应用到生产环境的。因为，故障导致数据丢失和不可用是很常见的情况。因此，在进行分布式数据存储设计时，通常会考虑对数据进行备份，以提高数据的可用性和可靠性，而实现数据备份的关键技术就是“数据复制技术”。

接下来，我们就一起打卡分布式数据复制技术吧。

什么是数据复制技术？

概括来讲，数据复制是一种实现数据备份的技术。比如，现在有节点 1 和节点 2，节点 1 上存储了 10M 用户数据，直观地说，数据复制技术就是将节点 1 上的这 10M 数据拷贝到节点 2 上，以使得节点 1 和节点 2 上存储了相同的数据，也就是节点 2 对节点 1 的数据进行了备份。当节点 1 出现故障后，可以通过获取节点 2 上的数据，实现分布式存储系统的自动容错。

也就是说，数据复制技术，可以保证存储在不同节点上的同一份数据是一致的。这样当一个节点故障后，可以从其他存储该数据的节点获取数据，避免数据丢失，进而提高了系统的可靠性。

这是不是就像数据有了自己的“分身”呢？那么，分布式系统是如何实现数据“分身有术”的呢？

接下来，我们通过一个例子来具体看下吧。

在分布式数据库系统中，通常会设置主备数据库，当主数据库出现故障时，备数据库可以替代主数据库进行后续的工作，从而保证业务的正常运行。这里，备数据库继续提供服务就是提高了分布式存储系统的可用性及可靠性。

那么，在这个过程中，又是如何实现备数据库替代主数据库的呢？这，就涉及到数据一致性的问题了，即只有主备数据库中的数据保持一致时，才可实现主备的替换。因此，在这个例子中，数据复制技术实际就是指，如何让主备数据库保持数据一致的技术。

理解了数据复制技术的基本含义，我们再一起看看数据复制技术的具体原理和应用吧。

数据复制技术原理及应用

不知你是否还记得，我在第 23 篇文章与你分享的 CAP 理论的 C、A 和 P 三个特性呢？我曾提到：在分布式存储系统中，分区容错性是肯定要满足的，为此需要在一致性和可用性之间做出权衡。

所以，对于数据的一致性，通常是指不同节点上数据要保持一致。要实现不同节点上的数据一致，数据复制技术必不可少。为此，对于分布式存储系统中的数据复制技术来讲，也需要在一致性和可用性之间做出一些权衡。因此，这就导致出现了多种数据复制技术方法，大体上有三类：

第一类方法，比较注重一致性，比如同步复制技术；

第二类方法，则更注重可用性，比如异步复制技术；

第三类方法，是介于前两者之间的，比如半同步复制技术。

接下来，我就针对同步数据复制技术、异步数据复制技术，以及半同步数据复制技术分别进行详细讲解。

同步复制技术原理及应用

同步复制技术是指，当用户请求更新数据时，主数据库必须要同步到备数据库之后才可给用户返回，即如果主数据库没有同步到备数据库，用户的更新操作会一直阻塞。这种方式保证了数据的强一致性，但牺牲了系统的可用性。

接下来，我们看一个具体的案例吧。

在一个分布式数据库系统中，有两个节点，分别作为主节点和备节点。通常情况下，两个节点均可接收用户读请求，然后将本节点的数据及时返回给用户，也就是说读请求响应比较快。而如果用户发送的是写请求，写操作必须由主节点进行，即使用户将写请求发送到备节点，备节点也会将该请求转发给主节点，因此写请求通常比读请求响应慢。MySQL 集群的读写分离就是一个典型实例。

如此设计的原因是，读请求不需要改变数据，只需要在更改数据时保证数据一致，就可以随时读；而写请求，因为要修改数据，如果每个节点均修改同一数据，则可能导致数据不一致。因此只有主节点可以进行写操作，但又要保证主节点和备节点的数据一致，这就是数据复制技术要发挥的作用了。

对于上述场景，如果采用同步复制技术的话，对于写请求，主数据库会执行写操作，并将数据同步到所有备数据库之后才可以响应用户。如图所示，客户端向主数据库发起更新操作 V，将 X 设置为 2，主数据库会将写请求同步到备数据库，备数据库操作完后会通知主数据库同步成功，然后主数据库才会告诉客户端更新操作成功。MySQL 集群支持的全复制模式就采用了同步复制技术。

在同步复制技术中，主数据库需要等待所有备数据库均操作成功才可以响应用户，性能不是很好，会影响用户体验，因此，同步复制技术经常用于分布式数据库主备场景（对于一主多备场景，由于多个备节点均要更新成功后，主节点才响应用于，所需时延比较长）或对数据一致性有严格要求的场合，比如金融、交易之类的场景。

异步复制技术原理及应用

异步复制技术是指，当用户请求更新数据时，主数据库处理完请求后可直接给用户响应，而不必等待备数据库完成同步，即备数据库会异步进行数据的同步，用户的更新操作不会因为备数据库未完成数据同步而导致阻塞。显然，这种方式保证了系统的可用性，但牺牲了数据的一致性。

如图所示，客户端 1 向主数据库发起更新操作 V，主数据库执行该操作，将 X=1 修改为 X=2，执行后直接返回给客户端 1 更新操作成功，而未将数据同步到备数据库。因此，当客户端 2 请求主数据库的数据 X 时，可以得到 X=2，但客户端 3 请求备数据库中的数据 X 时，却只能得到 X=1，从而导致请求结果不一致。

当然，分布式数据库主备模式场景下，若对数据一致性要求不高，也可以采用异步复制方法。MySQL 集群默认的数据复制模式采用的是异步复制技术，我就以 MySQL 集群默认的复制模式为例，与你简单描述下主备数据库同步的流程吧。

主数据库完成写操作后，可直接给用户回复执行成功，将写操作写入 binary log 中，binary log 中记录着主数据库执行的所有更新操作，以便备数据库获取更新信息。

备数据库启动一个 IO 线程专门读取 binary log 中的内容然后写入 relay log 中。

备数据库启动一个 SQL 线程会定时检查 relay log 里的内容，如发现有新内容则会立即在备数据库中执行，从而实现数据的一致。

异步复制技术大多应用在对用户请求响应时延要求很高的场景，比如很多网站或 App 等需要面向实际用户，这时后台的数据库或缓存如果采用同步复制技术，可能会流失用户，因此这种场景采用异步复制技术就比较合适。

除了 MySQL 集群，在缓存数据库 Redis 集群中，采用的也是异步复制技术，因此性能较高。但，在 Redis 中还会有其他机制来保证数据的一致性，我会在第 27 篇文章中与你详细介绍。

半同步复制技术原理及应用

同步复制技术会满足数据的强一致性，但会牺牲一定的可用性；异步复制技术会满足高可用，但一定程度上牺牲了数据的一致性。介于两者中间的是，半同步复制技术。

半同步复制技术的核心是，用户发出写请求后，主数据库会执行写操作，并给备数据库发送同步请求，但主数据库不用等待所有备数据库回复数据同步成功便可响应用户，也就是说主数据库可以等待一部分备数据库同步完成后响应用户写操作执行成功。

半同步复制技术通常有两种方式：

一种是，当主数据库收到多个备数据库中的某一个回复数据同步成功后，便可给用户响应写操作完成；

另一种是，主数据库等超过一半节点（包括主数据库）回复数据更新成功后，再给用户响应写操作成功。

显然，第二种半同步复制方案要求的一致性比第一种要高一些，但相对可用性会低一些。

前面所讲的 MySQL 集群，在一主多备场景下，也支持半同步复制模式，一般采用的是第一种半同步复制技术，这种技术既不会影响过多的性能，还可以更好地实现对数据的保护。

还记得我在第 23 篇文章中提到的具有 CP 特性的 ZooKeeper 集群吗？它采用的数据复制技术就是第二种半同步复制方案。在 ZooKeeper 集群中，写请求必须由 Leader 节点进行处理，每次写请求 Leader 会征求其他 Follower 的同意，只有当多数节点同意后写操作才可成功，因此保证了较高的一致性。

除此之外，还有很多系统采用了第二种半同步复制方案，比如微软云关系型数据库 Microsoft SQL Azure 的后端存储系统 Cloud SQL Server、Kubenetes 中保存集群所有网络配置和对象状态信息的 Etcd 组件（该组件采用的是 Raft 一致性协议，你可以再回顾下第 4 篇文章中的相关内容）等。

实际上，多数的分布式存储系统可以通过配置来选择不同的数据复制技术。比如上面讲过的 MySQL 数据库集群，就支持全同步复制、异步复制和半同步复制三种模式，再比如 Oracle 数据库，也提供了三种模式：

最大保护模式，对于写请求，要求主数据库必须完成至少一个备数据库的数据同步才可成功返回给客户端，采用的是半同步复制技术中的第一种方式。

最大性能模式，对于写请求，只要主数据库执行成功即可返回给客户端，采用的是异步复制技术。这种方式极大地提高了系统的可用性，但一致性难以保证。

最大可用性模式，介于最大保护模式和最大性能模式两者之间。这种模式是指，系统在通常情况下采用最大保护模式，但当主备之间出现网络故障时，切换为最大性能模式，等到网络恢复后，备数据库再进行数据同步。这种方式在系统的一致性和可用性之间做了一个权衡。

三种数据复制技术对比

以上，就是同步复制技术、异步复制技术和半同步复制技术的核心知识点了。接下来，我通过一张表格对比下这三种方法，以便于你记忆和理解。

知识扩展：在半同步复制技术中，对于未回复数据更新结果的节点，如何解决数据不一致或冲突呢？

对于半同步复制技术，因为只有部分备节点更新数据后，主节点即可返回响应用户。那么，对于未回复数据更新结果的节点，如何解决可能存在的数据不一致或冲突呢？

对于这个问题，不同的场景有不同的处理方式，需要根据用户的需求进行选择，比如以最新数据为准、以最大数据为准等，没有统一的评判规则，和用户的需求紧密相关。由于在分布式系统中，很多系统采用了 Raft 算法（你可以再回顾下第 4 篇文章中的相关内容），因此这里，我以 Raft 算法的处理策略为例与你展开介绍，以便你理解大部分常用的分布式系统的处理策略。

我刚刚提到，Raft 算法采用的是第二种半同步复制技术，也就是主数据库等超过一半节点（包括主数据库）回复数据更新成功后，再给用户响应写操作成功。当有 Follower 节点的数据与 Leader 节点数据不一致时，采用强制复制策略来解决不一致情况。

由于所有的数据更新操作最先在 Leader 节点执行，因此当产生冲突时，以 Leader 节点为准。Leader 节点上会对比与自己数据不一致的 Follower 节点所存储的信息，找到两者最后达成一致的地方，然后强制将这个地方之后的数据复制到该 Follower 节点上。

具体方法是，Leader 节点将每一次数据操作看作一条记录，并对这条记录标记一个 index，用于索引。Leader 节点会为每个 Follower 节点维护一个记录状态变量 nextIndex，即下一个记录的索引位置（nextIndex 的值为 Leader 节点当前存储数据记录的下一个 Index 值）。Leader 节点会将 nextIndex 发送给 Follower 节点，若 Follower 节点发现与本节点的 nextIndex 不一致，则告知 Leader 节点不一致，Leader 节点将 nextIndex 减 1，重复上述过程，直到与 Follower 节点的 nextIndex 相等位置，即找到了两者最后达成一致的地方。

比如，对于变量 X，Leader 节点记录的操作是{(Index 1, X = 1, Version:0), (Index 2, X=2, Version:1), (Index3 , X=3, Version:2)}，其中，Follower 节点 2 记录的操作为{(Index 2, X=1, Version:0), (Index 6, X=4, Version:2)}。

那么，Leader 节点发现两者最后一致的状态是{(Index 1, X=1, Version:0)}，为此将后续的{(Index 2, X=2, Version:1), (Index 3, X=3, Version:2)}复制到节点 2 上，则节点 2 更新为 (Index 1, X = 1, Version: 0), (Index 2, X=2, Version:1), (Index3 , X=3, Version:2)}。从而，节点 2 与 Leader 节点的数据保持一致。

总结

今天，我主要和你分析的是分布式数据复制技术。

首先，我通过分布式数据库主备节点数据一致的例子，为你比较直观的讲解了什么是数据复制技术。

然后，我为你介绍了数据复制技术的原理及应用，以及同步复制技术、异步复制技术和半同步复制技术这三种方法。其中，对于用户更新数据的请求，同步复制技术要求主备节点均更新完成，才返回结果告知用户更新成功；异步复制技术只需要主节点更新成功，就可返回结果；半同步复制技术，要求主节点更新成功，且备节点中至少有 1 个或过半节点更新成功，才可返回结果。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

相信通过今天的学习，你对分布式数据复制技术已经有了深刻的理解，并且对于不同技术适用的场景也有了自己的看法。加油，赶紧行动起来，相信你可以为你的业务场景选择一个合适的复制策略，也能够比较容易看懂 ZooKeeper 等开源软件采用的复制技术的原理了。

思考题

本讲主要是从应用或分布式数据系统的角度介绍了三种比较基本的数据复制技术，你还知道一些其他的基于本讲介绍的复制技术的演进或变种的复制方法吗？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

27 | 分布式数据之缓存技术：“身手钥钱”随身带

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

不知不觉，分布式数据存储这一站已经到了最后一讲。在前面几讲，我与你分享了 CAP 理论、分布式存储系统的三要素（顾客、导购和货架）、数据分布式分片方法和数据复制技术，其中数据分片方法和数据复制技术均是导购中的关键技术。

在这一讲，我将为你讲解分布式存储中“货架”的关键技术——缓存技术。

在计算机领域的各个方面，缓存都非常重要，是提升访问性能的一个重要技术。为什么这么说呢？

从单个计算机的体系结构来看，内存和处理器速度差异很大，如果不采用缓存技术，处理器的性能会受到很大的限制。

再看计算机应用，如果不采用缓存技术，对于每个请求，应用都要与后台数据库做一次交互，而数据库中的数据存储在磁盘上，因此每次请求都要和磁盘做交互，而磁盘访问的性能很低，造成访问延迟。

除此之外，还有网络访问，如果没有缓存机制，每次访问主机都要与远程机器做交互，速度又可想而知。

接下来，我们就一起打卡分布式缓存技术吧。

什么是分布式缓存？

打比方来说，缓存技术其实就像一个水缸，平时它会存储一定的水，而这些水就来自深井。如果每次都去深井打水，一方面井口比较小，导致一次能接收的用水请求有限；另一方面，井比较深，打水的工序比较复杂，导致所需时间比较长。

而有了这个水缸，我们就不需要去深井里打水，当水缸里没水时，水泵会将深井里的水抽到水缸中暂时存储起来。也就是说，“缓存技术”存储了满足人们一定时间内常用的“水量”，以提高用水效率。

在计算机领域，缓存技术一般是指，用一个更快的存储设备存储一些经常用到的数据，供用户快速访问。用户不需要每次都与慢设备去做交互，因此可以提高访问效率。

分布式缓存就是指在分布式环境或系统下，把一些热门数据存储到离用户近、离应用近的位置，并尽量存储到更快的设备，以减少远程数据传输的延迟，让用户和应用可以很快访问到想要的数据。这，是不是可以形象地理解为“身手钥钱”随身带呢？

其实，我们通常说的分布式数据缓存，属于计算机应用中的缓存的一种。而计算机应用中的缓存，一般指内存，即内存存储了用户经常访问的数据，用户或应用不再需要到磁盘中去获取相应的数据，大幅提高访问速度。

如下图所示，数据 A 是应用经常访问的数据，而数据 B 很少被应用访问，因此当应用访问数据 A 时，不需要到磁盘，而直接访问内存即可得到对应的值，速度较快；相反，访问数据 B 时，由于内存中没有缓存数据 B，所以应用需要到磁盘中获取对应的值，速度较慢。

那么今天，我要与你分享的分布式数据存储相关的缓存技术，就是以内存做为磁盘的缓存。

分布式缓存原理

接下来，我以主流的分布式缓存系统 Redis 和 Memcached 为例，与你讲述分布缓存技术，以加深你的理解吧。

Redis 分布缓存原理

Redis 的全称是 Remote Dictionary Server（远程字典服务器）。可以直观地看出，它是以字典结构将数据存储在内存中，应用可直接到内存读写 Redis 存储的数据。

Redis 集群是一个典型的去中心化结构，每个节点都负责一部分数据的存储，同时，每个节点还会进行主备设计来提高 Redis 的可靠性，具体原理你可以再回顾下第 10 篇文章中的相关内容。

接下来，我与你分享下，Redis 中与缓存关系最紧密的三个特性：支持多数据结构、支持持久化和主备同步。

第一，Redis 支持多数据结构。

Redis 是一个基于内存的 key-value 数据库，为了方便支持多应用的缓存，比如缓存文本类型、数据库的查询结果（字段与字段对应的值）等等，支持的数据结构不仅有简单的 k／v 类型，还可以支持 List、Set、Hash 等复杂类型的存储。

以 Hash 这种复杂类型的存储为例，Redis 将 Hash 视作一个整体当作数据库的 value（可以是一个对象，比如结构体对象）进行存储。如果把 Hash 结构的整体看作对象的话，Hash 结构里的 key-value 相当于该对象的属性名和属性值。

比如，插入 Hash 数据类型的命令：HMSET test field1 “Hello” field2 “World”中，如下图所示，test 为 key 值， field1 “Hello” field2 “World” 为 value 值，如果把整个 Hash 结构看做对象的话，则 field1、field2 类似于对象中的属性名，“Hello”“World”类似于对象中的属性值。

第二，Redis 支持持久化。

持久化是指，将数据从内存这种易失性存储设备中写入磁盘，从而让数据永久保存。Redis 中存储的数据虽然是基于内存的，但它也提供了持久化的机制，主要有两种方式：RDB 和 AOF。

RDB（Redis DataBase），也称快照方式，简单来说就是 Redis 会定时将内存中的数据备份到磁盘中，形成一个快照，比如每天保存一下过去一周的数据。这样当节点出现故障时，可以根据快照恢复到不同版本的数据。这种方式有一个明显的缺点，是会造成数据丢失，即当节点出现故障时，新数据可能还未备份到磁盘中。

AOF（Append Only File）的出现主要弥补了 RDB 数据不一致的问题，其思想与上一讲提到的数据库复制技术中 binary log 类似，即记录下 Redis 中所有的更新操作。

在 Redis 中，提供了三种实现 AOF 的策略：

AOF_FSYNC_NO （不同步），即不会自动触发写操作的同步；

AOF_FSYNC_EVERYSEC （每秒同步），即每隔一秒都会将写操作同步到磁盘；

AOF_FSYNC_ALWAYS （每次写都同步），即每次发生写操作会立即同步到磁盘。

Redis 中默认采用 AOF_FSYNC_EVERYSEC（每秒同步）的策略，因为这种策略的性能很不错，而且一旦出现故障，最多只会丢失一秒的数据。

第三，Redis 支持主备同步。

说到主备同步，我相信你应该想到了上一讲提到的数据复制技术。在 Redis 中，采用的是异步复制技术，但 Redis 可以通过配置 min-replicas-to-write 和 min-replicas-max-lag 这两个参数来有效地保证数据一致性。

比如，设置 min-replicas-to-write=3、min-replicas-max-lag=10，表示当至少有 3 个备数据库连接主数据库的延迟时间小于 10s 时，主数据库才可以执行写操作。延迟时间是从最后一次收到备数据库的心跳开始计算，通常每秒发送一次心跳。

除了上面对写操作的同步，在 Redis 中，还有两种情况是需要进行数据同步的：

一种情况是初次同步，即备数据库刚启动时的数据同步；

另一种情况是，因网络故障导致主备数据库断开连接，待网络恢复后的备数据库的数据同步。

针对这两种情况，Redis 提供了两种同步模式，即完整重同步和部分重同步。

完整重同步的流程如下所示：

当备服务器启动时，会向主服务器发送 SYNC 命令；

主服务器收到命令后会生成 RDB（快照）文件，并记录从现在起新执行的写操作；

RDB 生成后会发送给备服务器，备服务器通过 RDB 文件进行数据更新；

更新完成后，主服务器再将新记录的写操作发送给备服务器，备服务器执行完这些新记录的写操作，便与主服务器的数据保持一致了。

简单地说，部分重同步就是，当网络恢复后，主数据库将主备数据库断开连接之后的一系列写操作发送给备服务器，备数据库执行这些写操作，从而保证数据保持一致。

在这种方式的实现中，主备数据库会共同维护一个复制偏移量，这样主数据库就知道应该将哪些写操作发给备数据库，备数据库同步时也知道应该从哪里继续执行操作。

如图所示，主数据库的复制偏移量为 5000 时，向备数据库发送了 100 个字节的数据，发送结束后复制偏移量为 5100。

此时主备数据库连接断开，备数据库没有接收到这 100 个字节的数据，等到备数据库重新与主数据库连接上之后，会给主数据库发送 PSYNC 命令，并带上自己的复制偏移量 5000，主数据库接收到信息后，根据接收到的复制偏移量，将 5000 之后的数据发给备数据库，从而完成数据的部分重同步。

以上，就是分布式缓存系统 Redis 中涉及的关键技术，包括支持的数据结构、数据持久化方法和数据同步方法，相信通过上面的介绍，你对分布式缓存技术已经有了一定的了解。

接下来，我再带你学习另一个缓存数据库 Memcached。

Memcached 分布式缓存原理

与 Redis 类似，Memcached 也是一个基于内存的高性能 key-value 缓存数据库。Memcached 比 Redis 问世更早，也有很多公司在使用，比如 Facebook、Vox、LiveJournal 等。

其实，Memecached 的缓存原理和 Redis 类似。所以接下来的内容，我会着重于你讲述这两个数据库在支持的数据结构、持久化和主备同步上的不同。这样，你可以对比着学习这两个数据库，也会理解得更全面、深入些。

首先，我要先带你了解一下 Memcached 的集群结构。

Redis 的集群结构是每个节点负责一部分哈希槽，且每个节点可以设计主备。与 Redis 不同，Memcached 集群采用一致性哈希的思路，使用的是 Ketama 算法。该算法的主要思想就是，带虚拟节点的一致性哈希算法。

在实际应用中，每个物理节点对应 100~200 个虚拟节点，才能到达一个较好的存储均衡。这里为了方便理解，我对 Memcached 的集群结构做了简化，如下图所示，物理节点 1 对应两个虚拟节点 1-1、1-2，物理节点 2 对应三个虚拟节点 2-1、2-2 和 2-3。

采用带虚拟节点的一致性哈希方法，有一个优点是，当添加或移除节点时，不会出现大规模的数据迁移。你可以再回顾下第 25 篇文章中的相关内容。

对于数据结构的支持，Memcached 仅支持简单的 k／v 数据类型，如果想要存储复杂的数据类型，比如 List、Set 和 Hash 等，需要客户端自己处理，将其转化为字符串然后进行存储。这样就导致了一个缺点，操作不灵活。比如，Memcached 存储的数组中有一个元素需要修改，则需要将整个数组的数据取出来，修改后再整体写入到数据库中 。

而对于持久化，Memcached 是不支持的。这意味着断电时，Memcached 中存储的数据将会全部丢失。因为内存是一种易失性存储设备，断电后将不会存储数据。

在 Memcached 中，服务器和服务器之间没有任何通信，即自身不支持主备。如果想要实现高可用，需要通过第三方实现。比如，Repcached 实现了 Memcached 的复制功能，支持一主一备，从而使 Memcached 满足高可用。

对比分析

上面，我以 Redis 和 Memcached 这两个主流的分布式缓存系统为例，带你学习了分布式缓存技术。接下来，我以一个表格对它们进行分析对比，以便于你理解和查阅。

知识扩展：除了分布式存储中的缓存，还有计算机体系结构和网络中的缓存，它们又分别是什么呢？

计算机体系结构中的缓存，通常是指专用的缓存设备。由于内存和 CPU 访问速度相差很大，为了提高 CPU 的性能，计算机内部在 CPU 与内存之间设置了相应的缓存。

现在大多数机器分为三级缓存：L1 高级缓存、L2 高级缓存和 L3 高级缓存。就访问速度来讲，L1 高级缓存 > L2 高级缓存 > L3 高级缓存 > 内存。其中，L1 高级缓存的访问速度，几乎和 CPU 中寄存器的访问速度一样快。

有了这三级缓存，很多数据不需要到内存中读取，而直接读取这三级缓存中的数据即可，缩短了数据访问的时间，使得计算机运行速度变得更快。

网络访问中的缓存，通常是指本地的“磁盘”。通过网络访问数据时，需要与远程服务器交互来进行传输，而网络间数据传输以及远程服务器对请求的响应，会耗费很多时间。如果本机器的磁盘可以对你经常访问的远程内容进行存储，这样就不用每次都与远程服务器交互，从而减少网络数据传输与服务器响应的延迟，极大地提高性能。

可以看出，缓存的概念是相对的，基于不同的背景或应用场景，缓存所映射的存储设备是不一样的。

总结

今天，我主要与你分享了分布式数据的缓存技术。

首先，我以水缸的例子带你直观了解了什么是缓存，并引出了什么是分布式数据缓存。分布式数据缓存是以内存作为磁盘的缓存，存储一些用户经常需要用的数据，以提高访问速度。

其次，我以主流的 Redis 和 Memcached 为例，与你介绍了分布式缓存技术中的关键技术，包括支持的数据存储结构（比如 k/v、Set、List 等）、持久化技术（包括快照方式等）和数据同步技术（具体技术原理，可参见第 26 篇文章）。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

相信通过本讲的学习，你已不再觉得分布式缓存有多么神秘了，不管是使用 Redis 还是看 Redis 等系统的源码，一定会更容易理解和上手。加油，行动起来吧！

思考题

本讲我主要介绍了 Redis 和 Memcached 分布式数据缓存系统，你还知道哪些主流的分布式数据缓存系统呢？它们的缓存核心技术是什么呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

特别放送 | 徐志强：学习这件事儿，不到长城非好汉

你好，我是聂鹏程。今天，我要和你分享的是，一名专栏订阅用户“徐志强”的学习经历与方法，主题是“学习这件事儿，不到长城非好汉”。

你在学习专栏的过程中，有没有什么独特的学习方法和心路历程呢？欢迎你写在留言区，我很期待能跟你在这里交流、讨论，一起学习进步。

话不多说，现在就请你返回文稿看看徐志强的故事吧。

大家好，我是徐志强，一名 Java 开发者，从事互联网金融工作三年了。

从 2016 年毕业到现在，我从一枚“小白”变成了“熟练工”。但，我一直不甘心从“小熟练工”变成“老熟练工”，所以我来了“极客时间”。回过头来看，自学的这三年，我收获很多，也走了不少弯路。所以，我想把自己的学习经历分享出来，希望能帮你避开些弯路。

我是如何学习的？

我自学的三年，可以分为以下三个阶段。

第一阶段：啥都不懂，啥都想学。我买了非常多的书，数据库、操作系统、分布式等每个知识领域都买了一两本，但只是停留在“看了”的阶段。工作中遇到问题时，我不能融会贯通地用到所学知识，或者说我自学的知识基本用不到工作中。这就像大脑里装了很多武器，但我只记得它们的说明书，无法根据它们的特性做出选择。

其实在这个阶段，我们都很容易有个误解：觉得只要看了足够多的书、学了足够多的框架，能力自然就会得到提高，但其实蜻蜓点水般的学习收效甚微。所以，在我看来，广度学习虽然容易，但其实只算是到了长城脚下，只是起点。

第二阶段：调整思路，有选择地精读某些内容。觉察到不对后，我把之前那些书重新找了出来，并调整学习思路：挑选关键知识点采取精读的方式复习，并开始啃一些框架和库的源码。

“如果不了解核心原理，我始终都是一枚小白或者熟练工” ，这是我经常暗示自己的话。为此，我曾经通宵 Debug 就是为了搞明白一段很复杂的源码。每次我在源码中发现了书中描述的知识点后，都非常有成就感，原来就是这样呀！这也让我持续有动力去啃源码。

但我发现，学习源码并没有提升程序设计能力，我又进入了新的困惑期：怎样才能更好地消化和理解知识、怎样才能做到举一反三，养成解决问题的能力。为了爬上长城，我继续寻找答案。

第三阶段：学习一些架构方面的知识，形成知识点和问题的映射。当精读某一领域的知识内容后发现，我在系统设计上短板暴露了出来。因为做系统设计需要有全局思维，需要对整个系统架构的技术栈有清楚的认识，能对比分析不同技术的差异和特性，而不能局限于一个分支。

于是，我开始学习架构方面的知识，学习知识点和问题间的映射关系。每遇到一个问题我都会多思考几套解决方案并进行对比分析，每学到一个新知识点就思考它可以用在哪些场景，以及各个知识点有什么共性和联系。

说到这里，我再分享一段我的面试经历吧。去年年初我去参加面试，因为平时看了不少书、也阅读了不少源码，可以很轻松地通过一面、二面，但最后一面我就有点招架不住了。后来，我请教面试官，自己还欠缺哪方面的知识和能力。他告诉我，单论某一个知识点我掌握得还不错，但我不能将所有知识由点串成线，由线串成面，最终形成网，建议我平时多思考、多总结。

他的这番话对我影响非常大，一下就点醒了我，帮我找到了困惑许久的问题答案。正处于第三阶段学习的我，或许后面我还会有新的困惑，但方法总比问题多。现在，虽然我不知道自己爬到了长城的第几级台阶，但我坚信自己终将站上最高的烽火台！

总结我的学习经历，我觉得有下面几个点值得注意：

一定要坚持学习充电，否则很快就会被淘汰。

不要惧怕学习新技术，新技术不管怎么变化，原理和本质都是不变的。

一定不要只漂在技术的表面，一定要学原理，否则漂的时间久了你自己都不知道漂到哪了。

师傅领进门很关键。学一门新技术，最好是找一本经典的书，或者一门经典的课程系统地学习。

多思考，才能构建自己的知识体系和思维模式，才能将知识化作解决问题的方案。在学习时，我们要思考各个知识点的共性和内在联系，思考这个知识点能解决什么问题，以及遇到的问题能用什么知识点去解决。这样，脑中储存的知识，才能真正为我们所用。

为什么要学习分布式技术和原理？

分布式是当前技术领域的趋势之一，它扩展了计算机的计算边界，是区块链、人工智能、机器学习、大数据等众多前沿技术的基石。

所以，如果你不想永远当一枚小白或者熟练工，也不想被行业淘汰的话，就必须学习分布式技术，且一定要把原理学明白，不能局限于皮毛。我们不能一直随着浪花漂荡，要尝试将自己沉入海底，这样才能经受得住惊涛骇浪的洗礼，最终到彼岸。

我学习的第一个中间件是 Kafka，刚开始学的时候就被它的分区、选主、备份等概念搞得晕头转向，后来又学习了 Redis 和 Elasticsearch。这时，我发现这些中间件有一些共同特点：都会考虑分区来提高吞吐量，都会考虑备份来保证容错和可用性，多个备份之间都会选举一个主来提供服务。

慢慢地，对这些概念越来越熟悉后，我发现它们在分布式场景下都会遇到些类似的问题，只是各自的解决方案不同而已。因此，我心中萌生了系统学习分布式技术和原理的想法，因为只有弄清楚了分布式技术的本质和原理，才能在各种眼花缭乱的技术框架中游刃有余。

于是，我开始在网上搜集各种学习资料，又来到了极客时间，来到了“分布式技术原理与算法解析”这个专栏。

我是怎么学习这个专栏的？

说到我是怎么学习这个专栏的，因为之前已经自学过些分布式技术原理，所以可能会和其他人的方法不太一样。

首先，我不是利用碎片时间单篇文章地去学习，而是会找一个比较长的空闲时间，一气呵成地学习完一个模块的内容。这样我可以联系上下文，一下搞定相关知识点。

其次，学完每篇文章，我都会梳理、总结自己对这篇文章的理解，并尝试回答课后思考题，在评论区留言。同时，我也会关注其他同学的留言问题，并尝试做解答。在这个过程中，针对文章中的疑惑点，我会先去搜索相关资料学习，然后将具体的疑惑点留言给老师，等他的解答。

最后，关于知识点的思考和联想很重要。老师讲的很多分布式技术知识点都是通用的，在很多的中间件和框架中都有体现，根据这些理论去思考具体的组件是如何实现的，可以帮助我理解得更深刻。

学习到现在，我最大的收获和体会

到现在为止，整个专栏已经更新了 18 篇文章，可以说帮助我建立了对分布式技术和原理比较全面、系统的认知，越往后的体会越深。

其实，在学习这个专栏前，我就对分布式的一些概念，比如分布式事务、BASE 理论、CAP 理论、分布式锁等有些了解，但这些知识点在我脑中是独立没有关联起来的。所以，这个专栏对我最大的帮助是，帮我打通了这些知识点，有了一个系统化的认知。

此外，这个专栏给了我极大的信心。因为，聂老师的讲述是由浅入深的，并且还有大量形象的比喻和深刻的类比。比如，用事务模型来解释共享状态的分布式调度，在我看来就很形象、很有深度。

因此，这个专栏给我的感觉是“分布式技术和原理原来就是这么回事呀”，完全消除了我之前的畏难情绪。虽然学完这个课程不代表能透彻掌握分布式的技术原理和算法，但这肯定是一个非常棒的开始。

留给朋友们的建议

如果你之前没有接触过分布式的技术和中间件的话，我建议你先花点时间去补些基础，但这也不妨碍你将这个专栏作为你的“分布式技术和原理导论”来学习。

如果你之前了解过一些分布式技术和分布式组件，但是学得不深入的话，我觉得这个专栏可以帮你建立全面的分布式知识体系，直接带你练习上乘内功。非常值得拥有，我现在也正处于这个阶段。

如果你已经对分布式技术和常用组件都很熟悉了的话，我觉得这个专栏可以带你到更高的层次，去思考分布式的核心本质，帮助你更好地造轮子。

每个人都有自己的一座长城，有些人爬了一年数个月，有些人爬了十年，而有些人爬了一辈子，他们在长城上看到风景肯定大有不同。技术学习件事，我希望自己当一回好汉，爬上自己的长城，也祝愿各位小伙伴能爬上自己的长城！

特别放送 | 那些你不能错过的分布式系统论文

你好，我是聂鹏程。

古人云“以史为鉴，可以知兴替。”说的就是追本溯源的力量。通过学习和思考技术的发展和演进，我们方能更好地把握未来。而对分布式技术追本溯源的方式，无疑就是精读相关经典轮文了。

为此，今天我特地邀请了我的朋友刘梦馨，来与你系统分享下分布式系统领域的经典论文。你有时间和耐力的话，可以逐一阅读、学习下这些论文。

刘梦馨是灵雀云容器平台高级研发工程师，负责容器平台的架构、容器网络方案的设计和实现，也是开源 Kubernetes 网络插件 Kube-OVN 作者。他平时非常喜欢阅读论文，也总结了很多高效阅读论文的方法。

话不多说，我们来看刘梦馨的分享吧。

你好，我是刘梦馨。

分布式系统领域有着最令人费解的理论，全链路的不确定性堪比物理中的量子力学。同时，分布式系统领域又有着当代最宏伟的计算机系统，Google、Facebook、亚马逊遍布全球的系统支撑着我们的信息生活。

显然，能够征服分布式系统的，都是理论和实践两手抓两手都要硬的强者。然而，分布式系统领域还有着最高的上手门槛，没有大规模的基础设施、没有潮水般的流量，分布式领域幽灵般的问题并不会浮出水面。

那么，我们应该如何开启征服分布式系统的征程呢？

好在这条路上我们并不孤独。学术大牛们在五十年前就开始探索各方面理论上的问题，全球规模的互联网公司也有着丰富的实践和经验。而这些分布式领域人类的智慧，最终都沉淀为了一篇篇的经典论文。

和普通的技术文章相比，论文的发表有着极为严格的要求，随之而来的也是极高的质量。通过阅读分布式领域的经典问题，我们可以快速吸收前人的智慧，领略大型系统的风采，并收获最为宝贵的实战经验。

现在，就让我们从一篇篇经典论文开始，踏上征战分布式系统的征程吧！

我按照从理论到实践的顺序，将经典的分布式系统论文分成了分布式理论基础、分布式一致性算法、分布式数据结构和分布式系统实战四类，帮助你快速找到自己需要的论文。

这些论文我都给到了标题，你可以直接去 Google 学术里搜索。

分布式理论基础

分布式理论基础部分的论文，主要从宏观的角度介绍分布式系统中最为基本的问题，从理论上证明分布式系统的不确定、不完美，以及相互间的制约条件。研读这部分论文，你可以了解经典的 CAP 定理、BASE 理论、拜占庭将军问题的由来及其底层原理。

有了这些理论基础，你就可以明白分布式系统复杂的根源。当再碰到一些疑难杂症，其他人不得其解时，你可以从理论高度上指明方向。

以下就是分布式理论基础部分的论文：

Time, Clocks, and the Ordering of Events in a Distributed System

The Byzantine Generals Problem

Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services

CAP Twelve Years Later: How the “Rules” Have Changed

BASE: An Acid Alternative

A Simple Totally Ordered Broadcast Protocol

Virtual Time and Global States of Distributed Systems

分布式一致性算法

只要脱离了单机系统，就会存在多机之间不一致的问题。因此，分布式一致性算法，就成了分布式系统的基石。

在分布式一致性算法这一部分，我将与你推荐 2PC、Paxos、Raft 和 ZAB 等最知名的一致性算法。分布式算法的复杂度比普通算法要高出几个数量级，所以这部分论文是最为烧脑的一部分。

搞明白这部分论文，你的空间想象力和统筹规划能力都会得到质的提升。

A Brief History of Consensus, 2PC and Transaction Commit

Paxos Made Simple

Paxos Made Practical

Paxos Made Live: An Engineering Perspective

Raft: In Search of an Understandable Consensus Algorithm

ZooKeeper: Wait-Free Coordination for Internet-Scale Systems

Using Paxos to Build a Scalable, Consistent, and Highly Available Datastore

Impossibility of Distributed Consensus With One Faulty Process

Consensus in the Presence of Partial Synchrony

分布式数据结构

分布式数据结构部分的论文，将与你介绍管理分布式存储问题的知名数据结构原理。通过它们，你可以构建自己的分布式系统应用。

这部分论文的涵盖范围大致包括两部分：一是，分布式哈希的四个著名算法 Chord、Pastry、CAN 和 Kademlia；二是，Ceph 中使用的 CRUSH、LSM-Tree 和 Tango 算法。

和分布式一致性算法类似，分布式数据结构也极其考验空间想象力和统筹规划能力。不过，在经过分布式一致性算法的锻炼后，相信这些对你来说已经不再是问题了。

Chord: A Scalable Peer-to-Peer Lookup Service for Internet Applications

Pastry: Scalable, Distributed Object Location, and Routing for Large-Scale Peer-to-Peer Systems

Kademlia: A Peer-to-Peer Information System Based on the XOR Metric

A Scalable Content-Addressable Network

Ceph: A Scalable, High-Performance Distributed File System

The Log-Structured-Merge-Tree

HBase: A NoSQL Database

Tango: Distributed Data Structure over a Shared Log

分布式系统实战

分布式系统实战部分的论文，将介绍大量互联网公司在分布式领域的实践、系统的架构，以及经验教训。

Google 的新老三驾马车，Facebook、Twitter、LinkedIn、微软、亚马逊等大公司的知名系统都会在这一部分登场。你将会领会到这些全球最大规模的分布式系统是如何设计、如何实现的，以及它们在工程上又碰到了哪些挑战。

The Google File System

BigTable: A Distributed Storage System for Structured Data

The Chubby Lock Service for Loosely-Coupled Distributed Systems

Finding a Needle in Haystack: Facebook’s Photo Storage

Windows Azure Storage: A Highly Available Cloud Storage Service with Strong Consistency

Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing

Scaling Distributed Machine Learning with the Parameter Server

Dremel: Interactive Analysis of Web-Scale Datasets

Pregel: A System for Large-Scale Graph Processing

Spanner: Google’s Globally-Distributed Database

Dynamo: Amazon’s Highly Available Key-value Store

S4: Distributed Stream Computing Platform

Storm @Twitter

Large-scale Cluster Management at Google with Borg

F1 - The Fault-Tolerant Distributed RDBMS Supporting Google’s Ad Business

Cassandra: A Decentralized Structured Storage System

MegaStore: Providing Scalable, Highly Available Storage for Interactive Services

Dapper, a Large-Scale Distributed Systems Tracing Infrastructure

Kafka: A distributed Messaging System for Log Processing

Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases

以上就是我为你准备的分布式系统经典论文清单了。这个清单里的每一篇论文，都是经典中的经典。很多论文对之后的工业界及学术界产生了翻天覆地的影响，开创了一个又一个火热的产业。

希望你没有被这个清单吓到，当你翻开这些论文后，就会发现它们的内容并不是高高在上，包含了很多很实际、很具体的问题。认真读下去，你甚至会有掌握了屠龙之技的快感，一发而不可收拾。

为了帮助你高效阅读这些论文，并汲取其中的精华，我再和你说说我阅读论文的一些心法吧。

如何高效地阅读论文？

一般来说，单篇论文大概会有 15 到 20 页的内容，如果你是第一次读论文可以把重点放在前面的背景介绍、相关工作和概要设计上。好的论文通常会很仔细地介绍背景知识，帮助你从宏观上先对整个问题有一个初步认识，了解当前现状。

接下来，你可以再根据自己的兴趣，选择是否仔细阅读论文涉及的详细原理和设计。这一部分，通常是论文中最精华的部分，包含了最具创新的理念和做法，内容通常也会比较长，需要花费较多的时间和精力去研究。这时，你可以根据自己的情况，选择一批论文重点突破。

论文最后通常是评测和数据展示部分。这部分内容对我们最大的参考价值在于，学习作者的评测方法、用到的测试工具和测试样例，以便将其运用到工作中。

阅读完一篇论文后，如果你觉得内容还不错的话，可以通过 Google 学术去搜索相关的文章，找到所有引用这篇论文的新作品。这样一来，你就可以通过一篇经典论文不断深入，全面掌握一个领域。

最后，我希望你可以通过经典论文的助力，迅速建立起自己的知识武器库，来攻克日常工作中的难题。

28 | 分布式高可靠之负载均衡：不患寡，而患不均

你好！我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

到目前为止，我已经为你介绍了分布式起源、分布式协调与同步、分布式资源管理与负载调度、分布式计算技术、分布式通信技术和分布式数据存储。可以说，掌握了这些内容，基本上就掌握了分布式的关键技术。

然而，只有可靠的分布式系统才能真正应用起来。那么，分布式系统的可靠性又是如何实现的呢？

不要着急，接下来几篇文章，我会和你一起学习分布式可靠性相关的知识，包括负载均衡、流量控制、故障隔离和故障恢复。

在这其中，负载均衡是分布式可靠性中非常关键的一个问题或技术，在一定程度上反映了分布式系统对业务处理的能力。比如，早期的电商抢购活动，当流量过大时，你可能就会发现有些地区可以购买，而有些地区因为服务崩溃而不能抢购。这，其实就是系统的负载均衡出现了问题。

接下来，我们就一起来打卡分布式高可靠之负载均衡。

什么是负载均衡？

先举个例子吧。以超市收银为例，假设现在只有一个窗口、一个收银员：

一般情况下，收银员平均 2 分钟服务一位顾客，10 分钟可以服务 5 位顾客；

到周末高峰期时，收银员加快收银，平均 1 分钟服务一位顾客，10 分钟最多服务 10 位顾客，也就是说一个顾客最多等待 10 分钟；

逢年过节，顾客数量激增，一下增加到 30 位顾客，如果仍然只有一个窗口和一个收银员，那么所有顾客就只能排队等候了，一个顾客最多需要等待 30 分钟。这样购物体验，就非常差了。

那有没有解决办法呢？

当然有。那就是新开一个收银窗口，每个收银窗口服务 15 个顾客，这样最长等待时间从 30 分钟缩短到 15 分钟。但如果，这两个窗口的排队顾客数严重不均衡，比如一个窗口有 5 个顾客排队，另一个窗口却有 25 个顾客排队，就不能最大化地提升顾客的购物体验。

所以，尽可能使得每个收银窗口排队的顾客一样多，才能最大程度地减少顾客的最长排队时间，提高用户体验。

看完这个例子，你是不是想到了一句话“不患寡，而患不均”？这，其实就是负载均衡的基本原理。

通常情况下，负载均衡可以分为两种：

一种是请求负载均衡，即将用户的请求均衡地分发到不同的服务器进行处理；

另一种是数据负载均衡，即将用户更新的数据分发到不同的存储服务器。

我在第 25 篇文章分享数据分布方法时，提到：数据分布算法很重要的一个衡量标准，就是均匀分布。可见，哈希和一致性哈希等，其实就是数据负载均衡的常用方法。那么今天，我就与你着重说说服务请求的负载均衡技术吧。

分布式系统中，服务请求的负载均衡是指，当处理大量用户请求时，请求应尽量均衡地分配到多台服务器进行处理，每台服务器处理其中一部分而不是所有的用户请求，以完成高并发的请求处理，避免因单机处理能力的上限，导致系统崩溃而无法提供服务的问题。

比如，有 N 个请求、M 个节点，负载均衡就是将 N 个请求，均衡地转发到这 M 个节点进行处理。

服务请求的负载均衡方法

通常情况下，计算机领域中，在不同层有不同的负载均衡方法。比如，从网络层的角度，通常有基于 DNS、IP 报文等的负载均衡方法；在中间件层（也就是我们专栏主要讲的分布式系统层），常见的负载均衡策略主要包括轮询策略、随机策略、哈希和一致性哈希等策略。

今天，我着重与你分析的就是，中间件层所涉及的负载均衡策略。接下来，我们就具体看看吧。

轮询策略

轮询策略是一种实现简单，却很常用的负载均衡策略，核心思想是服务器轮流处理用户请求，以尽可能使每个服务器处理的请求数相同。生活中也有很多类似的场景，比如，学校宿舍里，学生每周轮流打扫卫生，就是一个典型的轮询策略。

在负载均衡领域中，轮询策略主要包括顺序轮询和加权轮询两种方式。

首先，我们一起看看顺序轮询。假设有 6 个请求，编号为请求 1~6，有 3 台服务器可以处理请求，编号为服务器 1~3，如果采用顺序轮询策略，则会按照服务器 1、2、3 的顺序轮流进行请求。

如表所示，将 6 个请求当成 6 个步骤：

请求 1 由服务器 1 处理；

请求 2 由服务器 2 进行处理。

以此类推，直到处理完这 6 个请求。

最终的处理结果是，服务器 1 处理请求 1 和请求 4，服务器 2 处理请求 2 和请求 5，服务器 3 处理请求 3 和请求 6。

接下来，我们看一下加权轮询。

加权轮询为每个服务器设置了优先级，每次请求过来时会挑选优先级最高的服务器进行处理。比如服务器 1~3 分配了优先级{4，1，1}，这 6 个请求到来时，还当成 6 个步骤，如表所示。

请求 1 由优先级最高的服务器 1 处理，服务器 1 的优先级相应减 1，此时各服务器优先级为{3，1，1}；

请求 2 由目前优先级最高的服务器 1 进行处理，服务器 1 优先级相应减 1，此时各服务器优先级为{2，1，1}。

以此类推，直到处理完这 6 个请求。每个请求处理完后，相应服务器的优先级会减 1。

最终的处理结果是，服务器 1 处理请求 1~4，服务器 2 处理请求 5，服务器 3 会处理请求 6。

以上就是顺序轮询和加权轮询的核心原理了。轮询策略的应用比较广泛，比如 Nginx 默认的负载均衡策略就是一种改进的加权轮询策略。我们具体看看它的核心原理吧。

首先，我来解释下 Nginx 轮询策略需要用到的变量吧。

weight：配置文件中为每个服务节点设置的服务节点权重，固定不变。

effective_weight：服务节点的有效权重，初始值为 weight。 在 Nginx 的源码中有一个最大失败数的变量 max_fails，当服务发生异常时，则减少相应服务节点的有效权重，公式为 effective_weight = effective_weight - weight / max_fails；之后再次选取本节点，若服务调用成功，则增加有效权重，effective_weight ++ ，直至恢复到 weight。

current_weight：服务节点当前权重，初始值均为 0，之后会根据系统运行情况动态变化。

假设，各服务器的优先级是{4，1，1}，我还是将 6 个请求分为 6 步来进行讲解，如表所示：

遍历集群中所有服务节点，使用 current_weight = current_weight + effective_weight，计算此时每个服务节点的 current_weight，得到 current_weight 为{4，1，1}，total 为 4+1+1=6。选出 current_weight 值最大的服务节点即服务器 1 来处理请求，随后服务器 1 对应的 current_weight 减去此时的 total 值，即 4 - 6，变为了 -2 。

按照上述步骤执行，首先遍历，按照 current_weight = current_weight + effective_weight 计算每个服务节点 current_weight 的值，结果为{2，2，2}，total 为 6，选出 current_weight 值最大的服务节点。current_weight 最大值有多个服务节点时，直接选择第一个节点即可，在这里选择服务器 1 来处理请求，随后服务器 1 对应的 current_weight 值减去此时的 total，即 2 - 6，结果为 -4。

以此类推，直到处理完这 6 个请求。

最终的处理结果为，服务器 1 处理请求 1、2、4、6，服务器 2 处理请求 3，服务器 3 会处理请求 5。

可以看到，与普通的加权轮询策略相比，这种轮询策略的优势在于，当部分请求到来时，不会集中落在优先级较高的那个服务节点。

还是上面的例子，假设只有 4 个请求，按照普通的加权轮询策略，会全部由服务器 1 进行处理，即{1,1,1,1}；而按照这种平滑的加权轮询策略的话，会由服务器 1 和 2 共同进行处理，即{1,1,2,1}。

轮询策略的优点就是，实现简单，且对于请求所需开销差不多时，负载均衡效果比较明显，同时加权轮询策略还考虑了服务器节点的异构性，即可以让性能更好的服务器具有更高的优先级，从而可以处理更多的请求，使得分布更加均衡。

但轮询策略的缺点是，每次请求到达的目的节点不确定，不适用于有状态请求的场景。并且，轮询策略主要强调请求数的均衡性，所以不适用于处理请求所需开销不同的场景。

比如，有两个服务器（节点 A 和节点 B）性能相同，CPU 个数和内存均相等，有 4 个请求需要处理，其中请求 1 和请求 3 需要 1 个 CPU，请求 2 和请求 4 需要 2 个 CPU。根据轮询策略，请求 1 和请求 3 由节点 A、请求 2 和请求 4 由节点 B 处理。由此可见，节点 A 和节点 B 关于 CPU 的负载分别是 2 和 4，从这个角度来看，两个节点的负载并不均衡。

综上所述，轮询策略适用于用户请求所需资源比较接近的场景。

随机策略

随机策略也比较容易理解，指的就是当用户请求到来时，会随机发到某个服务节点进行处理，可以采用随机函数实现。这里，随机函数的作用就是，让请求尽可能分散到不同节点，防止所有请求放到同一节点或少量几个节点上。

如图所示，假设有 5 台服务器 Server 1~5 可以处理用户请求，每次请求到来时，都会先调用一个随机函数来计算出处理节点。这里，随机函数的结果只能是{1,2,3,4,5}这五个值，然后再根据计算结果分发到相应的服务器进行处理。比如，图中随机函数计算结果为 2，因此该请求会由 Server2 处理。

这种方式的优点是，实现简单，但缺点也很明显，与轮询策略一样，每次请求到达的目的节点不确定，不适用于有状态的场景，而且没有考虑到处理请求所需开销。除此之外，随机策略也没有考虑服务器节点的异构性，即性能差距较大的服务器可能处理的请求差不多。

因此，随机策略适用于，集群中服务器节点处理能力相差不大，用户请求所需资源比较接近的场景。

比如，我在第 19 篇文章中提到的 RPC 框架 Dubbo，当注册中心将服务提供方地址列表返回给调用方时，调用方会通过负载均衡算法选择其中一个服务提供方进行远程调用。关于负载均衡算法，Dubbo 提供了随机策略、轮询策略等。

哈希和一致性哈希策略

无论是轮询还是随机策略，对于一个客户端的多次请求，每次落到的服务器很大可能是不同的，如果这是一台缓存服务器，就会对缓存同步带来很大挑战。尤其是系统繁忙时，主从延迟带来的同步缓慢，可能会造成同一客户端两次访问得到不同的结果。解决方案就是，利用哈希算法定位到对应的服务器。

哈希和一致性哈希，是数据负载均衡的常用算法。 我在第 25 篇文章介绍哈希与一致性哈希时，提到过：数据分布算法的均匀性，一方面指数据的存储均匀，另一方面也指数据请求的均匀。

数据请求就是用户请求的一种，哈希、一致性哈希、带有限负载的一致性哈希和带虚拟节点的一致性哈希算法，同样适用于请求负载均衡。

所以，哈希与一致性策略的优点是，哈希函数设置合理的话，负载会比较均衡。而且，相同 key 的请求会落在同一个服务节点上，可以用于有状态请求的场景。除此之外，带虚拟节点的一致性哈希策略还可以解决服务器节点异构的问题。

但其缺点是，当某个节点出现故障时，采用哈希策略会出现数据大规模迁移的情况，采用一致性哈希策略可能会造成一定的数据倾斜问题。同样的，这两种策略也没考虑请求开销不同造成的不均衡问题。

应用哈希和一致性哈希策略的框架有很多，比如 Redis、Memcached、Cassandra 等，你可以再回顾下第 25 篇文章中的相关内容。

除了以上这些策略，还有一些负载均衡策略比较常用。比如，根据服务节点中的资源信息（CPU，内存等）进行判断，服务节点资源越多，就越有可能处理下一个请求；再比如，根据请求的特定需求，如请求需要使用 GPU 资源，那就需要由具有 GPU 资源的节点进行处理等。

对比分析

以上，就是轮询策略、随机策略、哈希和一致性哈希策略的主要内容了。接下来，我再通过一个表格对比下这三种方法，以便于你学习和查阅。

知识扩展：如果要考虑请求所需资源不同的话，应该如何设计负载均衡策略呢？

上面提到的轮询策略、随机策略，以及哈希和一致性哈希策略，主要考虑的是请求数的均衡，并未考虑请求所需资源不同造成的不均衡问题。那么，如何设计负载均衡策略，才能解决这个问题呢？

其实，这个问题的解决方案有很多，常见的思路主要是对请求所需资源与服务器空闲资源进行匹配，也称调度。

关于调度，不知你是否还记得第 11 篇文章所讲的单体调度？我们可以使用单体调度的思路，让集群选举一个主节点，每个从节点会向主节点汇报自己的空闲资源；当请求到来时，主节点通过资源调度算法选择一个合适的从节点来处理该请求。

在这篇文章中，我提到了最差匹配和最佳匹配算法。这两种算法各有利弊，最差匹配算法可以尽量将请求分配到不同机器，但可能会造成资源碎片问题；而最佳匹配算法，虽然可以留出一些“空”机器来处理开销很大的请求，但会造成负载不均的问题。因此，它们适用于不同的场景，你可以再回顾下第 11 篇文章中的相关内容。

除此之外，一致性哈希策略也可以解决这个问题：让请求所需的资源和服务器节点的空闲资源，与哈希函数挂钩，即通过将资源作为自变量，带入哈希函数进行计算，从而映射到哈希环中。

比如，我们设置的哈希函数结果与资源正相关，这样就可以让资源开销大的请求由空闲资源多的服务器进行处理，以实现负载均衡。但这种方式也有个缺点，即哈希环上的节点资源变化后，需要进行哈希环的更新。

总结

今天，我主要带你学习了分布式高可靠技术中的负载均衡。

首先，我以超市收银为例，与你介绍了什么是负载均衡。负载均衡包括数据负载均衡和请求负载均衡，我在第 25 篇文章中介绍的数据分布其实就是数据的负载均衡，所以我今天重点与你分享的是请求的负载均衡。

然后，我与你介绍了常见的负载均衡策略，包括轮询策略、随机策略、哈希和一致性哈希策略。其中，轮询策略和随机策略，因为每次请求到达的目的节点不确定，只适用于无状态请求的场景；而哈希和一致性哈希策略，因为相同 key 的请求会落在同一个服务节点上，所以可以用于有状态请求的场景。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

加油，相信通过本讲的学习，你对分布式系统中的负载均衡有了一定的理解，也可以进一步对电商系统、火车票系统等涉及的请求负载均衡的问题进行分析了。加油，行动起来吧！

思考题

在分布式系统中，负载均衡技术除了各节点共同分担请求外，还有什么好处呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

29 | 分布式高可靠之流量控制：大禹治水，在疏不在堵

你好！我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章中，我带你学习了分布式高可靠中的负载均衡。负载均衡的核心在于，将用户请求均匀分配到多个处理服务器处理，以解决单个服务器的单点瓶颈问题。但，如果用户请求数非常多的话，即便实现了负载均衡，服务器能力达到上限，还是无法处理所有的用户请求。

比如，类似双十一、双十二的秒杀场景，用户流量突增时，即使做了负载均衡，我们仍然会感受到点击抢购时，需要等待较长的时间。这背后的原理是什么呢？

你是不是想到了，这是因为系统控制了用户的请求量呢？没错，这就是今天我们要一起打卡的流量控制技术。

什么是流量控制？

说到流量控制，如果你学过计算机网络的话，第一反应肯定是网络传输中的流量控制。网络传输中的流量控制，就是让发送方发送数据的速率不要太快，让接收方来得及接收数据，具体的实现方法就是滑动窗口。

简单来讲，滑动窗口指的是，在任意时刻，发送方都维持一个连续的允许发送的数据大小，称为发送窗口；接收方也会维持一个连续的允许接收的数据大小，称为接收窗口。每次发送方给接收方发送数据后，必须收到接收方返回的确认消息，发送窗口才可向后移动，发送新的数据。

接下来，我们通过一个简单的例子，来看看滑动窗口在网络流量控制中，是如何发挥作用的吧。如图所示，发送窗口和接收窗口大小均为 1，发送方发送数据 D1 后，只有接收到来自接收方的确认消息 ACK，发送窗口才可向后移动，即发送方才可以发送后续数据 D2。

这是网络传输中的流量控制，那么具体到分布式系统中，流量控制又是什么呢？

在前面提到的双十一、双十二秒杀场景中，用户流量突增，在这种高并发、大流量的情况下，服务器的处理能力成为电商系统的瓶颈，处理不好就会导致系统崩溃，服务不可用。而分布式系统中的流量控制，就是解决这类问题的一种关键技术。

通俗地说，分布式流量控制就是在分布式系统下，控制每个服务器接收的请求数，以保证服务器来得及处理这些请求，也就是说尽可能保证用户请求持续地被处理，而不是让大量的用户请求“阻塞”在服务器中，等待被执行。这就好比“大禹治水，在疏不在堵”。

接下来，我们就一起学习下分布式系统常用的流量控制策略吧。

分布式系统流量控制策略

还记得第 21 篇文章中讲到的消息队列吗？消息队列就是实现流量控制的一种方法，通过一个消息队列来存放用户的消息，然后服务器到消息队列中逐个消费，就可以避免消息过多时服务器处理不过来的情况。

除此之外，分布式系统的流量控制策略还有很多，常用的主要包括两种：漏桶策略和令牌桶策略。

漏桶策略

相信你看到“漏桶”两个字，头脑里应该已经有了一个漏桶的样子。确实，名字就已经很形象地说明了这种策略的含义。

如下图所示，有一个固定容量的水桶，桶底有一个小洞，水桶可以接收任意速率的水流，但无论水桶里有多少水，水从小洞流出的速率始终不变，桶里的水满了之后，水就会溢出。

漏桶策略借鉴上述原理，无论用户请求有多少，无论请求速率有多大，“漏桶”都会接收下来，但从漏桶里出来的请求是固定速率的，保证服务器可以处理得游刃有余。当“漏桶”因为容量限制放不下更多的请求时，就会选择丢弃部分请求。这种思路其实就是一种“宽进严出”的策略。

比如，在某段时间内，系统每秒会有 10 个用户发出请求，但这些请求经过漏桶后，每秒始终只流出 2 个请求，也就是说服务器每秒最多处理 2 个请求。这样的话，无论请求速率有多大，都能达到限流的目的，避免服务器在短暂时间内需要处理大量请求，但由于处理能力受限导致系统崩溃，从而保证了系统的高可靠。

这种策略的好处是，做到了流量整形，即无论流量多大，即便是突发的大流量，输出依旧是一个稳定的流量。但其缺点是，对于突发流量的情况，因为服务器处理速度与正常流量的处理速度一致，会丢弃比较多的请求。但是，当突发大流量到来时，服务器最好能够更快地处理用户请求，这也是分布式系统大多数情况下想要达到的效果。

所以说，漏桶策略适用于间隔性突发流量且流量不用即时处理的场景，即可以在流量较小时的“空闲期”，处理大流量时流入漏桶的流量；不适合流量需要即时处理的场景，即突发流量时可以放入桶中，但缺乏效率，始终以固定速率进行处理。

目前，漏桶算法已经用于很多框架了，比如阿里开源的流量控制框架 Sentinel 中的匀速排队限流策略，就采用了漏桶算法；分布式追踪系统 Jaeger 中，有一种采集策略是速率限制类型，内部使用的也是漏桶算法等。

令牌桶策略

令牌桶策略，也是一个很形象的名字，指的是桶里放着很多令牌，请求只有拿到令牌才能被服务器处理。

如图所示，有一个固定容量的存放令牌的桶，我们以固定速率向桶里放入令牌，桶满时会丢弃多出的令牌。每当请求到来时，必须先到桶里取一个令牌才可被服务器处理，也就是说只有拿到了令牌的请求才会被服务器处理。所以，你可以将令牌理解为门卡，只有拿到了门卡才能顺利进入房间。

同样的，我们通过一个具体的例子，来加深对令牌桶策略的理解吧。

假设，令牌以每秒 3 个的速率放入到令牌桶中，桶的容量为 10。通常情况下， 每秒会有 2 个用户请求，请求到来时就会到桶里取一个令牌，由于请求的速率低于放令牌的速率，因此令牌桶里令牌会逐渐增多，直到达到桶的容量。超过桶容量后，令牌会被丢弃。

当大流量到来时，比如某个时刻来了 10 个请求，此时桶里有 10 个令牌，因此，请求都会被服务器处理；但如果来的请求数不止 10 个，令牌会被取完，多余的请求取不到令牌，也就没办法及时被服务器处理，需要等待令牌。

通过上述的例子，就能看出这种策略的好处：当有突发大流量时，只要令牌桶里有足够多的令牌，请求就会被迅速执行。通常情况下，令牌桶容量的设置，可以接近服务器处理的极限，这样就可以有效利用服务器的资源。因此，这种策略适用于有突发特性的流量，且流量需要即时处理的场景。

在实际使用中，令牌桶算法也很常见。比如，Google 开源工具包 Guava 提供的限流工具类 RateLimiter，就是基于令牌桶算法来完成限流的。

两种策略对比

以上就是漏桶策略和令牌桶策略的核心原理了，接下来我们通过一张表格对比下这两种策略吧。

Sentinel 流量控制工作原理

我们都知道阿里的流量控制做得很好，特别是双十一、抢购等情况下。接下来，我以阿里开源的流量控制框架 Sentinel 为例，与你进一步介绍流量控制的工作原理。

Sentinel 的核心是，监控应用的并发线程数或 QPS（请求数 / 每秒）指标，当达到系统设定的阈值时，Sentinel 可以采取一定的策略对流量进行控制，以避免应用被瞬时高流量击垮，从而保证应用高可靠。

为此，在 Sentinel 中，关于流量控制有两种方式：一种是通过并发线程数进行流量控制，另一种是通过 QPS 指标进行流量控制。

首先，我们看一下通过并发线程数进行流量控制。

要理解这种限流方式，我需要先带你搞清楚什么是线程池。

我们知道，过多的线程会消耗非常多的系统资源，包括线程资源消耗、线程调度消耗等。为了解决这个问题，我们引入了线程池。线程池维护了多个启动着的线程，随时等待着去执行系统分配的任务，即系统每次需要处理任务时，可以直接从线程池中取线程，从而避免了创建和销毁线程的时间和资源等消耗。

同一时刻每个线程只能执行一个任务或请求，因此，可以通过并发线程数进行流量控制。我们看一个案例吧。

如图所示，假设现在线程池中有 3 个线程也就是说，最大并发处理数为 3，现在有 2 个请求 Q1 和 Q2 到来，由于请求数少于线程数，因此请求可以被并发执行。线程池中启动着的线程 1 和线程 2 会进行相应的处理，而不会创建新线程，除此之外，线程处理完请求后也不会被销毁，而是回到线程池中继续等待新的请求。

但如果现在同时有 4 个请求到来，那么只有 3 个请求可以被并发处理，而剩下的一个请求要么丢弃，要么等待空闲线程。

在分布式系统中，每个请求都会由一个线程去进行处理。当请求太多系统处理不过来时，意味着线程池可能已经被耗尽（线程池中无空闲线程），因此当请求过多时，执行请求的并发线程数自然会随之增加，当超过一定的阈值（比如线程池中线程总数）时，需要采取一定的策略来进行流量控制。

在 Sentinel 中，就采用了直接拒绝的方式，即新来的请求会直接拒绝。

然后，我们再看一下通过 QPS 指标进行流量控制吧。

QPS 是指每秒的请求数，大流量也就意味着 QPS 大。当 QPS 达到阈值时，Sentinel 提供了三种流量控制策略，分别是直接拒绝、预热（Warm Up）和匀速排队。

直接拒绝，是最直接也是最暴力的方式，与并发线程数流量控制采取的方式一致，就是当 QPS 达到系统设定的阈值时，直接拒绝新来的请求。

这种策略乍一听起来确实不是很好，但对于系统处理能力确切已知的情况（即阈值设定为每秒能接受的最大处理请求数），却非常实用。当请求超出阈值时，可以直接拒绝，因为系统已经没有更多的能力来处理多余的请求了。因此，该策略适用于对系统处理能力确切已知的场景。

接下来，我们看看预热。当系统的 QPS 长期处于一个较低水平时，一旦发生流量骤增，如果直接让系统每秒处理大量的请求，可能会因为服务器处理能力不足，导致系统崩溃。因此，Sentinel 提供了一种“预热”机制，让系统的 QPS 缓慢增加，在一定的时间内逐渐增加到上限。

下面以一个例子为例，带你进一步理解预热的原理。如下图所示，假设通常情况下系统每秒处理 3 个请求，即 QPS=3，当用户请求增加时，系统每秒处理的请求数相应增加，但不会一下子提高很多。比如，每秒增加 1 个处理请求，逐步达到 QPS=10 的处理上限，并不再继续增加，从而避免大流量一下子导致系统故障。

可以看出，预热这种策略有点像是一种特殊的令牌桶：放令牌的速率通常保持在一个较低的水平，当流量突增时，放令牌的速率不会一下子提高到最高水平，而是会慢慢增加，直到增加到最大速率则不可再增加。因此，该策略与令牌桶策略的适用场景类似，即适用于具有突发特性的流量，且流量可以即时处理的场景。

匀速排队的思想，其实本质就是漏桶策略。它会严格控制系统每秒处理的请求数，请求数很多时，请求之间的间隔也会保持一致。

如图所示，当 QPS=5 时，每隔 200ms 才允许服务器处理下一个请求。假设请求队列中有 10 个请求瞬间到达，服务器不会一下子全处理完，而是按照请求的顺序，每 200ms 处理一个请求，直到处理完所有请求。这时，处理的请求就像是在匀速排队，因此得名。

该策略中，系统会设定一个时间间隔 T，假设最大排队时长设置为 6T，上次请求通过的时刻为 t1。当新的请求在 t2 时刻到来的话，则进行判断，首先查看是否还有其他请求在排队。如果没有请求在排队，分两种情况：

当 t2 - t1 的值大于或等于时间间隔 T，请求可以通过；

当 t2 - t1 的值小于 T 时，需要等待，直到 t2 - t1 的值达到时间间隔 T 时，才可以让请求通过。

而如果新请求到来时，已经有请求在排队，就需要计算该新请求的预期通过时间。比如，有 3 个请求在排队，则该新请求预期通过时间为 t1+4T，因为需要等到在该请求前面的请求都通过后该请求才可通过，且两个请求通过的时间间隔必须达到 T 才可以。

另外，若排队的请求过多，新来的请求预期等待时间超出最大排队时长，即等待时间超过 6T 时，则直接拒接这个请求。

现在我想你应该理解了为什么说匀速排队策略本质就是漏桶策略了吧。因此，匀速排队的适用场景与漏桶策略类似，即适用于间隔性突发流量且流量不用即时处理的场景。

知识扩展：什么是拥塞控制？它与流量控制的区别是什么？

其实，在分布式领域拥塞控制与流量控制的区别还是蛮大的。为什么这么说呢？

今天，我们讲述的流量控制，主要是指业务上的流量，即用户请求。而拥塞控制通常针对的是网络上传输的数据，即网络上数据传输出现拥塞时应当如何控制。所以，这两个概念不是一回事儿。

但是，对于网络上数据的传输而言，流量控制与拥塞控制非常容易混淆。

网络数据传输中，流量控制是指控制发送方和接收方的传输和接收速率在双方都可以接受的范围，通常使用的方法是滑动窗口；而拥塞控制是通过检测网络状况，随时疏通网络，避免网络中过多数据堆积，导致无法传输数据，包括慢启动与拥塞避免方法。如果你想深入了解拥塞控制的相关内容，可以自行查阅计算机网络的相关书籍。

总结

今天，我主要带你学习了分布式高可靠技术中的流量控制。

首先，我以网络传输中的流量控制和电商系统的例子，和你引入了分布式系统中的流量控制，即控制每个服务器的请求数，以保证处理请求所需计算能力在服务器处理能力的上限之内，从而避免系统崩溃。

然后，我为你介绍了常见的流量控制策略，包括漏桶策略和令牌桶策略。其中，漏桶策略的核心是“宽进严出”，发送给服务器进行处理的请求速率固定，以避免超过服务器处理能力上限，导致系统崩溃，但这种方式不适合突发流量增加的场景。令牌桶策略的核心是，只要桶里有令牌，请求就可以被处理，只要在服务器处理能力内即可，所以适用于处理及时且处理速率非固定的场景。

最后，我和你分享了阿里开源的 Sentinel 流量控制，并介绍了通过并发线程数和通过 QPS 指标进行流量控制的两种方式。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

加油，相信通过本讲的学习，你对分布式系统中的流量控制有了一定的理解，也可以进一步对电商系统中抢购、秒杀中的流量控制问题进行分析了。加油，行动起来吧！

思考题

除了漏桶策略和令牌桶策略，你还知道哪些流量控制策略吗？它们的原理是什么呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

30 | 分布式高可用之故障隔离：当断不断，反受其乱

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在前面两篇文章中，我带你一起学习了分布式系统高可靠的关键技术，包括分布式负载均衡和流量控制。除了高可靠，在实际生产中，分布式系统的高可用问题也极其重要。

比如，在双十一的抢购高峰期，如果分布式系统不能满足高可用的特性，那么当大量用户同时抢购时就可能导致系统崩溃，无法提供服务，导致大量用户流失。

因此，在接下来的两篇文章，我将从故障隔离和恢复机制这两项关键技术入手，和你一起学习如何保证分布式系统的高可用。

今天，我们就先一起打卡分布式高可用中的故障隔离吧。

什么是故障隔离？

从字面意思来看，故障隔离就是，把故障通过某种方式与其他正常模块进行隔离，以保证某一模块出现故障后，不会影响其他模块。

其实，我们生活有很多故障隔离的例子，比如交通。一辆车就类似于分布式系统中的一个模块，当一辆车在高速公路上出现故障后，我们通常会将其停靠在紧急车道，或者在其前后设置故障指示牌，以防止其他车辆与其相撞，引起更大的交通事故。这种将故障车辆停靠在路边紧急车道或设置故障指标牌的方法，就是一种故障隔离。

现在我们回到分布式系统，故障隔离，就是采用一定的策略，以实现当某个模块故障时，不会影响其他模块继续提供服务，以保证整个系统的可用性。所以说，故障隔离，可以避免分布式系统出现大规模的故障，甚至是瘫痪，降低损失。

在分布式系统中，要实现故障隔离，通常需要在进行系统设计时，提前对可能出现的故障进行预防，以使得在出现故障后能实现故障隔离。此外，由于是提前设计预防的，因此故障隔离还可以帮助我们快速定位故障点。

也就是说，分布式系统中的故障隔离策略是在系统设计时就进行考虑，从预防的角度来实现故障发生时，该模块故障不会影响其他模块。因此，我今天与你介绍的故障隔离策略，是整个系统设计时，从高可用这个维度进行设计的策略。

好了，理解了故障隔离为什么可以提高分布式系统的可用性以后，我们再来看看实现故障隔离有哪些常见策略吧。

分布式故障隔离策略

分布式系统中的故障隔离策略有很多，大体上可以从两个维度来划分：

一类是以系统功能模块为粒度进行隔离。比如，通过系统功能 / 服务划分，将系统分为多个功能 / 服务模块，各个功能 / 服务模块之间实现松耦合，即一个功能 / 服务模块出现故障，不会影响其他功能 / 服务模块，根据功能模块或服务由线程执行还是进程执行，通常分为线程级隔离、进程级隔离。

另一类是，通过资源隔离来实现。比如，系统中各个模块拥有自己独立的资源，不会发生资源争抢，从而大大提升系统性能。根据资源所属粒度，通常包括进程级隔离（比如采用容器隔离）、虚拟机隔离、服务器隔离和机房隔离等。

基于这个分类，接下来，我将为你讲述三种比较常见的故障隔离策略，包括以功能模块为粒度进行隔离的线程级隔离和进程级隔离，以及以资源为隔离维度的资源隔离。

线程级隔离

线程级故障隔离，是指使用不同的线程池处理不同的请求任务。当某种请求任务出现故障时，负责其他请求任务的线程池不会受到影响，即会继续提供服务，从而实现故障的隔离。

如图所示，以电商购物平台为例，假设初期运行在单台机器的一个进程中，在这个进程中有三个线程池，分别负责订单任务、支付任务和配送任务。这样，当订单请求出现故障时，不会影响已下单用户的支付和仓库配送服务。

线程级的故障隔离策略，在生产环境中较为常用，尤其对于单体应用（单进程多线程的应用）。在单体应用场景下，应用被单个进程执行，但单进程中包括多个线程，因此该场景下，只需要实现线程级隔离即可，实现简单、效果好，因此是一种很常用的方式。

系统实现线程级隔离后，线程间的通信通常使用共享变量来实现。简单地说，共享变量就是一个进程中的全局变量，在进程的各个线程间可以同时使用。这种通信方式，实现简单且效果明显。

进程级隔离

随着业务逐渐扩大，业务系统也会越来越复杂，单体应用可能无法满足公司与用户的需求，这时候就需要对系统进行拆分。

一种常用的方式就是，将系统按照功能分为不同的进程，分布到相同或不同的机器中。如果系统的进程分布到不同机器上的话，从资源的角度来看，也可以说成是主机级的故障隔离。因为从另一个层面看，系统确实分布到了不同机器上，当某个机器出现故障时，不会对其他机器造成影响。

如图所示，电商购物平台可以分为订单系统、支付系统和配送系统三部分。这三个子系统可以采用三个不同的进程来服务用户。

这就是一个进程级的故障隔离方案，即不同的子系统对应不同的进程，某一个子系统出现故障，都不会导致其他系统不可用。

系统实现进程级隔离后，进程间的协同必须通过进程间通信（IPC）来实现。进程间通信有很多方式，大体可以分为以下两类：

如果进程都在同一台机器上，则可以通过管道、消息队列、信号量、共享内存等方式，来实现；

如果进程分布在不同机器上，则可以通过远程调用来实现，你可以再回顾下第 19 篇文章中的相关内容。

进程级故障隔离，目前在分布式应用中应用广泛，比如常见的电商、火车票购买等业务都可以采用。

资源隔离

前面介绍的是以服务或功能模块为粒度进行隔离的，下面我们一起看下从资源角度进行隔离是怎么做的？

简单来说，资源隔离就是将分布式系统的所有资源分成几个部分，每部分资源负责一个模块，这样系统各个模块就不会争抢资源，即资源之间互不干扰。这种方式不仅可以提高硬件资源利用率，也便于系统的维护与管理，可以大幅提升系统性能。

微服务就是一个典型的例子。当前，很多公司都在将自己的业务系统微服务化，比如亚马逊、阿里、华为、微软等。在微服务的理念中，是尽可能将服务最小化，服务与服务之间进行解耦合，包括运行环境的相互隔离等。比如，现在通常采用容器进行隔离，我在第 9 篇文章中分享的 Mesos、Kubernetes 等可实现容器管理与调度，而 Mesos 和 Kuberntes 的上层应用很多都是微服务。

实际上，在微服务框架中，一个服务通常对应一个容器，而一个容器其实就是操作系统中一个进程，不同容器负责不同的服务，就类似于刚才所讲的：不同进程负责系统不同的功能模块。

如图所示，如果将电商购物平台微服务化，则可以启动三个容器，分别负责订单服务、支付服务和配送服务。一个容器对应一个进程，因此微服务框架本质上还是一种进程级故障隔离策略。

但与进程级隔离不同的是，微服务框架采用容器进行故障隔离。容器虽然本质上是操作系统的一个进程，但具备普通进程不具备的特性，比如资源隔离。

一个普通进程有很大的计算或内存需求时，可能会占满物理机上所有的 CPU、内存资源，导致其他进程没有资源可用，引发进程间的资源争夺；

但容器可以实现资源限制，让每个容器占用的资源都有一个上限，比如 CPU、内存，均会设置一个上限值，这个上限值限定了该容器的处理能力，就好比一台服务器具有资源上限值一样。因此，一个容器使用的资源不会影响其他容器的资源，从而避免资源争抢，提高性能。

那到底什么是容器呢？ 容器是一种虚拟化技术，可以为应用提供一整套运行环境。容器通过限制自身使用的资源来实现资源隔离，从而让容器就像一个个的“集装箱”：容量固定，存放着任意的物品。

目前，比较常用的容器是 Docker。Docker 主要使用 Linux 内核中的 Linux Cgroups 模块来设置容器的资源上限，包括 CPU、内存、磁盘、网络带宽等。通过 Cgroups 模块，容器间就形成了资源隔离，从而避免了容器间的资源争夺，提升了系统性能。

通过容器进行资源隔离后，需要容器进行网络配置来进行容器间的通信。比如，Docker 默认是通过建立虚拟网桥来实现容器间通信的。如果你想深入了解容器网络配置相关的内容，可以自行查阅Docker 官方文档中网络部分的内容。

除了容器级别的资源隔离，虚拟机级别的隔离也是资源隔离的一种常用手段，一台物理机可以安装多个虚拟机，每个虚拟机都会分配一定的资源，即进行资源隔离。除此之外，主机级别的隔离也可以说是一种资源隔离，每台机器的资源是独享的，不会与其他机器发生资源争夺，从而做到资源隔离。

除了以上所讲到的故障隔离策略，其实还有一些更粗力度的隔离策略，比如集群隔离、机房隔离等，这些策略主要是跨集群或跨地域的隔离策略。这些粗粒度的隔离策略，不仅可以根据系统功能 / 服务等维度对系统进行划分，比如每个功能 / 服务由一个集群或一个机房单独负责，而且也是一种资源隔离策略，即集群间或机房间资源互相隔离，不会发生资源争夺，互不影响。

故障隔离策略综合对比

以上，就是分布式应用中常用的几种故障隔离策略了。接下来，我再通过一个表格进行对比分析，以便于你理解和记忆。

知识扩展：从用户角度看，有哪些常用的故障隔离方案？

无论是按照功能 / 服务划分模块，实现进程级、虚拟机级等故障隔离，还是按照系统资源进行故障隔离，它们都是一种针对服务方的故障隔离手段。除此之外，还有一种故障隔离策略是，针对用户的，即用户级别的故障隔离。

用户级别的故障隔离是指，将不同用户分开，当系统出现故障时，只影响部分用户，而不是全体用户。比如，发布产品前大多会有一个“灰度发布”过程， 就是先发布给一小部分用户进行测试，如果没问题再大规模发布；如果有问题也只是影响一小部分用户。这就是一种典型的用户级别的故障隔离。

常用的用户级别故障隔离策略，有数据分片、负载均衡等。你可以再回顾下第 25 篇文章中关于数据分片，以及第 28 篇文章中关于负载均衡技术的相关内容。

以数据分片为例，系统可以将不同用户的数据存储到不同的数据库，即一个数据库只存储部分用户的信息。这样当某个数据库出现故障时，仅影响该故障数据库存储的用户，而不会影响全部用户。

负载均衡也是这个道理。当处理请求的某个服务器出现故障时，只影响该故障服务器负责的用户请求，而不会影响其他服务器负责的用户请求。

总结

今天，我主要带你学习了分布式高可用技术中的故障隔离技术。

首先，我以汽车故障为例，带你了解了故障隔离的概念，并引出分布式系统中的故障隔离。分布式系统中的故障隔离技术是，在进行分布式系统可用性设计时，考虑故障隔离的设计，也就是提前预防或避免出现故障后对整个系统造成影响。

然后，我与你介绍了常见的故障隔离策略，包括线程级隔离、进程级隔离和资源隔离策略。其中，线程级隔离和进程级隔离是从对功能 / 服务模块进行隔离的维度进行划分的，借助了系统本身对线程或进程的隔离机制实现故障隔离；资源隔离是从资源的维度进行隔离，主要通过容器、服务器、集群、机房等物理资源维度进行隔离。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

加油，相信通过今天的学习，你对分布式系统中的故障隔离技术有了一定的理解，也可以进一步对容器、虚拟机等的隔离技术进行深入分析了。加油，行动起来吧！

思考题

分布式系统难免会发生故障，那么评判一个系统故障的指标有哪些呢？或者说通过哪些指标可以判断故障的健康度呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

31 | 分布式高可用之故障恢复：知错能改，善莫大焉

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

在上一篇文章，我带你学习了故障隔离。故障隔离的目的是，对故障组件进行隔离，以避免其影响系统中的其他组件，尽可能保证分布式系统的可用性。

在分布式系统中，故障在所难免，发生故障后仅仅进行隔离还远远不够，还需要进行故障恢复。比如，现在集群中有 3 个节点，节点 1 故障后，对节点 1 进行隔离，如果节点 2、节点 3 紧接着故障了，又隔离了这两个节点。那么，整个集群就无法继续提供服务了，何谈分布式系统的高可用呢？

为了解决这种问题，分布式领域还有一个关键技术来保证系统的高可用，即故障恢复。

接下来，我们就一起打卡分布式系统的故障恢复技术吧。

分布式故障基础知识

在介绍故障恢复之前，我先与你说说分布式系统中会有哪些故障类型。

故障类型

在任何一个分布式系统中，故障都是不可避免的。这里的故障，通常包括两类：

一类是物理故障，比如硬盘损坏、断电断网、硬件升级等；

另一类是软件层故障，比如系统存在 Bug 导致系统崩溃、系统负载过高导致系统崩溃等。

在讨论分布式系统故障时，我们通常还会从是否是网络导致的故障的角度来进行故障划分，包括节点故障和网络故障，而这两类故障可能同时包括物理故障和软件层故障。由于软件层故障和具体的程序实现等相关，因此主要由开发者根据自己的实现去解决；而物理故障通常具有很多共同特征，因此今天我主要针对物理故障导致软件不可用的情况进行讲解。

首先，我们看一下节点故障。

简单地说，节点故障就是单个机器自身出现故障。比如，由机器 A、B，……，Z 构成的分布式集群中，机器 A 自身出现故障，而不是非机器之间的网络连接出现故障，就是节点故障。

节点故障有很多种，大体可以分为两类：

一类是硬件故障，比如机器硬盘损坏、内存接触不良等；

另一类是软件故障，比如由于请求过多，超过服务器处理能力上限，导致无法处理，又或者是机器被攻击，导致机器瘫痪等。

节点故障在软件层的表现结果是，该机器无法为用户提供服务。

其次，我们看一下网络故障。

简单地说，网络故障就是分布式集群中，节点之间无法完成通信。比如，由机器 A，B，……，Z 构成的分布式集群中，机器间比如机器 A 和 B 之间无法完成通信，就属于网络故障。

网络故障也有很多种，比如路由器故障、DNS 故障、网络线路断裂等。这些物理故障在软件层的表现结果是，机器间无法通信，影响分布式应用正常提供服务。

了解了故障的类型，我们还要搞明白如何检查到故障，也就是如何进行故障检测，因为这是故障恢复的前提。

故障检测

故障检测，就是指通过一定的方式识别或发现故障。就好比，我们把火灾、地震等危险事件看作是故障，采用火灾报警器、地震仪等来检测发现火灾或地震。

如果可以提前检测到事件的发生，就能将损失降到最小。在分布式系统中，检测硬件故障通常比较麻烦，因此会通过查看软件层的表现结果来进行故障检测。比如，网络故障导致服务器之间无法通信，因此就可以通过检测服务器之间是否可以通信（比如，服务器之间心跳包是否可以正常地发送和接收），来检测是否存在网络故障。

关于故障检测的具体策略，我会在后文与你展开。当检测到故障后，就需要进行故障恢复了。

故障恢复

故障恢复，就是指修复分布式系统中出现的故障，使系统恢复正常。简单来说，故障恢复就是故障发生之后的弥补方案，可以理解为对故障进行修正或修复，以保证服务正常运行，有点类似“知错能改，善莫大焉”。

接下来，我们就具体看看故障检测和故障恢复的原理或者说策略吧。

分布式故障检测原理

在分布式系统中，常见的故障检测方法是心跳机制。基于心跳进行故障检测的策略主要分为两类，固定心跳检测策略和根据历史心跳信息预测故障策略。

还记得我在第 22 篇文章中，与你介绍的通过心跳方式判断集中式架构和非集中式架构中节点是否存活的方法吗？其实，这里用到的就是固定心跳检测策略。具体的检测原理，你可以再回顾下这篇文章。

所以接下来，我主要与你分享基于历史心跳消息预测故障的策略，也就是我们常说的φ值故障检测。

φ值故障检测是基于心跳间隔符合正态分布的假设进行计算的。其中，φ值是用来评估心跳是否超时的概率，是对心跳间隔的概率求对数，将非整数转换为整数以便于理解。

φ值故障检测方法中，通常会设置一个阈值Ф，若当前心跳计算得到的φ≥Ф，则判断心跳超时，否则心跳未超时。

那么，φ值是如何计算的呢？

从流程上来讲，φ值的计算可以分为三步，即：

采样窗口存储心跳到达的时间；

通过样本计算出心跳到达时间间隔的分布；

使用得到的正态分布计算当前的φ值。

接下来，我们就具体看看这三个阶段吧。

第一步：采样窗口存储心跳到达的时间。

采样窗口就是一个具有固定容量的容器，一般存储近 k 次的心跳信息，每次心跳到达时，会将到达时间存储到采样窗口，如果采样窗口已满，则会删除窗口中最旧的数据。

比如，采样窗口最多存储最近 10 次心跳到达的时间，t1，t2，……， t10，当第 11 次心跳到来时，采样窗口会将 t1 删除，存入 t11。到达时间的间隔很容易得到，比如第 11 次心跳到来后，到达时间的间隔是 t3 - t2，t4 – t3，……，t11 – t10。通过这些采样数据，可以计算出样本的平均值μ和方差 σ2，以便后面计算φ值。当然，随着新的心跳到来，这些数据会进行相应的更新。

第二步：通过样本计算出心跳到达时间间隔的分布。

φ值故障检测是假设心跳到达时间间隔的分布遵循正态分布，假设 Plater(t) 表示接收到上一次心跳之后 t 个时间片能收到下一次心跳的概率，则通过第一步中得到的样本平均值µ和方差 σ2，得到 Plater(t) 的计算结果如下：

Plater​(t)=σ2π​1​∫t+∞​e−2σ2(x−u)2​dx=1−F(t)

其中，F(t) 是具有均值µ和方差 σ2的正态分布的累积分布函数。

第三步：使用得到的正态分布计算当前的φ值。

假设，Tlast表示最近一次接收到心跳的时间，tnow表示当前时间。将 Tlast、tnow，和第二步求得的 Plater(t)，带入以下公式即可求得φ值：

φ(tnow​)def−log10​(Plater​(tnow​−Tlast​))

求得φ值后，与阈值Ф进行比较，即可判断节点是否发生故障。

以上就是φ值故障检测策略的介绍，它的基本思想是利用了历史心跳信息，来降低误判的可能性。如果你想了解这个策略更详细的内容，可以参考“The φ Accrual Failure Detector”这篇论文。

这篇论文中提到，当阈值Ф=1 时，误判的可能性大约为 10%；Ф=2 时，误判的可能性大约为 1%；Ф=3 时，误判的可能性大约为 0.1% ······

通过以上讲解，可以看出，φ值故障检测策略可以根据历史心跳信息动态预测下一次心跳是否超时，并可以通过设置阈值来自由调控误判的可能性。目前，该策略已被应用到一些框架中，比如我们熟悉的 Akka 集群的故障检测，便是采用了φ值故障检测策略。

当采用故障检测策略检测到故障后，故障如何恢复呢？接下来，我们就一起看看故障恢复策略吧。

故障恢复策略

关于故障恢复策略，我从单节点故障和网络故障两个维度展开。

对于单节点故障问题，往往采取主备策略，即当主节点故障后，从备节点中选出一个作为新的主节点，以继续提供服务。这种备升主的方式比较好理解。

如下图所示，用户 A 访问分布式集群时一直是与 Master 交互的，但当 Master 故障后，其他 Slave 会通过分布式选举算法选出一个新的主节点。

假设，从 Slave 1、Slave 2 和 Slave 3 中选举出 Slave 2 作为新的 Master，则 Slave 2 需要承担原来 Master 的职责，继续为用户提供服务，因此当用户 A 再次访问集群时，提供服务的是新选出的 Master，也就是 Slave 2。这就是备升主的过程。

关于分布式选举算法的相关内容，你可以再回顾下第 4 篇文章。

从用户 A 的角度来看，并不会感受到服务有什么异常，因为依旧可以正常访问集群。因此，主备策略可以大大提高分布式系统的可用性，在分布式系统中随处可见。比如，第 10 篇文章涉及的 Redis 集群、第 23 篇文章中讲到的 ZooKeeper 集群等，都是采用了这种主备策略来做故障恢复。

而对于网络故障问题的解决方案，简单来说就是 C、A、P 选择的问题，即在分布式系统的可用性和数据一致性之间做权衡。根据不同的应用场景，选择不同的解决方案。

当分布式系统中出现网络故障时，对于高可用性要求严格的系统，比如要求必须及时响应用户的场景，就需要采用保 AP 弃 C 的策略；对于数据一致性有严格要求的系统，比如银行、金融系统等场景，就需要采用保 CP 弃 A 的策略。具体内容，你可以再回顾下第 23 篇文章。

其实，网络故障恢复问题也可以看作数据复制的问题，即网络故障恢复后节点间数据同步的问题。还记得第 26 篇文章中的同步复制、异步复制和半同步复制技术吗？其中，半同步复制技术因为既能有效保证数据安全，又能满足系统高性能的要求，所以最受欢迎，被大多数分布式系统采用。

关于如何通过半同步复制技术，来进行网络故障恢复，你可以再回顾下第 26 篇文章的相关内容。

其实，节点故障和网络故障也有交叉的地方，比如网络故障产生的原因可能是节点故障，即因为节点故障导致节点间无法通信，而不是纯粹的网络链路问题。这种情况有两种可能性，一种是节点临时性故障，即一段时间后就会恢复；一种是节点永久性故障，即节点不会恢复。针对第一种情况，只需等到故障恢复后，数据进行同步即可；第二种情况则需要备升主策略来解决。

知识扩展：固定心跳检测和基于历史心跳信息预测故障的策略，各有什么特点呢？

首先，我们看一下固定心跳检测。

固定心跳检测的核心是，固定周期 T 秒发送心跳，若连续 k 次未收到心跳回复（时间 T 内），则判断心跳超时的时间为 k*T 秒。可以看出，k 和 T 的设置非常重要。

比如，对于要求秒级故障检测的场景（时延敏感性场景），则 k*T≤1s，因此需要将 T 设置为 ms 级，比如 200ms，k 设置为 1000/200=5 次。但，这样一来容易导致误判。因为判断超时的时间设置得太短，很可能是系统做内存回收或系统本身有高任务在运行导致心跳回复延后。

而对于时延不太敏感的场景，k 或 T 可以设置得大一些，降低误判率，但却会增加发现故障的时间。

接下来，我们看一下φ值故障检测。φ值故障检测是基于心跳间隔符合正态分布的假设，通过对历史心跳数据采样来预测当前心跳是否超时的。也就是说，心跳间隔符合比较平稳或符合规律的情况下，比较适合，但对于具有突发情况或心跳间隔无规律的场景误判率比较高。

在网络状况确定且比较稳定的场景下，大多数系统会采用固定心跳检测策略，因为其可以根据网络状况与业务场景自主设定合适的 k 和 T 值，简单有效；而当网络状况有所变化，且变化有规律的场景，则可以使用φ值故障检测策略。

总结

今天，我主要带你学习了分布式高可用技术中的故障恢复技术。

首先，我为你介绍了分布式系统中的故障类型，主要包括物理故障和软件故障，软件故障主要是由于程序或软件 Bug 等导致，通常由开发者在开发或测试过程中解决，而物理故障导致软件不可用的故障类型主要分为两类，节点故障和网络故障。

其次，我为你介绍了故障检测方法。故障检测方法主要是心跳检测方法，包括固定心跳策略和基于历史信息的心跳策略。其中，固定心跳策略的具体原理见第 22 篇文章中的相关内容；基于历史心跳策略的核心是通过统计历史数据规律，以预测当前心跳是否超时以进行故障检测；

紧接着，我为你介绍了故障恢复策略，主要涉及备升主、数据复制等关键技术，相关关键技术原理，你可以再回顾第 4 篇文章中的分布式选举算法，以及第 26 篇文章中的数据复制技术。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

加油，相信通过今天的学习，分布式系统中的故障恢复技术对你来说不再陌生了，你可以根据业务场景设计出对应的故障检测和故障恢复策略了。行动起来吧，加油！

思考题

在分布式系统中，网络分区是一个非常重要的故障问题。那么如何判断网络分区呢？以及出现网络分区后，如何处理呢？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

32 | 答疑篇：如何判断并解决网络分区问题？

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

到目前为止，“分布式技术原理与算法解析”专栏已经接近尾声了。在这里，我首先要感谢你坚持学习每一篇文章，以及对每一道思考题的积极思考与讨论，并在此基础上扩展了类似问题。

比如 @Jackey、@Eternal、@leslie、@mt11912、@小白啊、@随心而至等同学，一直在跟着专栏的更新节奏学习，并非常积极地在留言区留言讨论、总结自己的理解，并查询相关资料补充文中未讲解到或没有深入展开的问题。

今天，我梳理了文后的留言，发现大家对最近几篇文章介绍的分布式高可靠问题特别感兴趣，特别是我没有详细展开的网络分区问题。

比如，在第 4 篇文章“分布式选举：国不可一日无君”中，我给你留下的思考题是集群中是否会存在双主的场景，很多同学提到双主是网络分区导致的。

再比如，在第 31 篇文章“分布式高可用之故障恢复：知错能改，善莫大焉”中，我给你留下的思考题是，如何判断以及处理网络分区。

因此，在今天这篇文章中，我将会与你深入探讨网络分区问题，以帮助你进一步理解并解决业务中的故障恢复问题。

什么是网络分区？

我们先来看看网络分区到底是什么吧。在第 31 篇文章分享故障恢复时，我与你介绍了故障类型中的网络故障，网络分区就是其中的一种故障类型。

通常情况下，网络分区指的是在分布式集群中，节点之间由于网络不通，导致集群中节点形成不同的子集，子集中节点间的网络相通，而子集和子集间网络不通。也可以说，网络分区是子集与子集之间在网络上相互隔离了。

那么，应该如何判断是否发生了网络分区呢？

如何判断是否发生了网络分区？

在分布式集群中，不同的集群架构网络分区的形态略有不同。所以，要判断是否发生了网络分区，我们需要弄清楚不同的分布式集群架构，即集中式架构和非集中式架构中的网络分区形态是什么样的。

首先，我们来看一下集中式架构的网络分区形态。

集中式架构中，Master 节点通常以一主多备的形式部署，Slave 节点与 Master 节点相连接，Master 节点的主和备之间会通过心跳相互通信。

以 Master 节点主备部署为例，如下图所示，集中式架构中的网络分区主要是主节点与备节点之间网络不通，且一部分 Slave 节点只能与主 Master 节点连通，另一部分只能与备 Master 节点连通。

图 1 集中式架构中网络分区的形态

然后，我们再来看看非集中式架构中的网络分区形态。

如下图所示，非集中式架构中，节点是对称的，因此网络分区的形态是形成不同子集，子集内节点间可互相通信，而子集之间的节点不可通信。比如，子集群 1 中 Node1、Node2 和 Node4 可以相互通信，子集群 2 中 Node3 和 Node5 也可以相互通信，但子集群 1 和子集群 2 之间网络不通。

图 2 非集中式架构中网络分区的形态

从集中式和非集中式这两种分布式集群架构的网络分区形态可以看出，要判断是否形成网络分区，最朴素的方法就是判断节点之间心跳是否超时，然后将心跳可达的节点归属到一个子集中。

由于非集中式系统中，每个节点都是对等的、提供的服务相同，所以当多个子集群之间不可达，或部分节点出现故障后，尽管提供的服务质量（SLA）可能会下降，但并不影响这些剩余节点或子集群对外提供服务。所以，接下来我将与你重点讨论集中式系统的网络分区问题。

网络分区最微妙的地方在哪里？

在工作和生活中遇到一个问题，你的本能反应估计是，有问题就解决问题好了。而网络分区最微妙的地方在于，你很难通过程序去判断问题到底出在哪里，而只能通过心跳等手段知道部分节点的网络不可达了。

但，导致节点不可达的原因有很多，有可能是网络的原因，也有可能是节点本身的问题。也就是说，我们无法通过一些症状就判断出是否真的产生了分区。另外，你也很难通过程序去判断这个问题是不是很快就会被恢复。这也是应对网络分区问题最微妙的地方。

网络分区出现概率较高的场景是什么？

在第 31 篇文章留言区中有同学提到：

个人理解，网络分区故障一般是针对不同集群来说的，单个集群一般在同一个网络中，集群内的单点故障并不会出现网络分区。当整个集群的网络出故障时，才会有分区的说法。能想到检测办法是，单独有机器对不同集群的主节点进行心跳检测来判断。

首先，我要澄清的是，我们说的网络分区肯定是就同一个集群而言的。对于不同集群来说，正是因为集群间本就没有太多的交互，才需要从逻辑上分割成不同的集群，这些逻辑上不同的集群本就是可以独立对外提供服务的。

当集群跨多个网络时，确实正如这位同学所说，从概率上讲相对容易出现网络分区的情况，比如一个业务集群部署在多个数据中心时。所以，集群跨多网络部署时，就是网络分区出现概率较高的场景。

接下来，我们看看如何处理网络分区吧。

网络分区有哪些常见的处理方法？

为了不影响分布式系统的高可用性，检测到网络分区后，我们就需要尽快地进行处理。

假如，我们采用一种非常激进的方式去处理，即一旦发现节点不可达，则将不可达节点从现有集群中剔除，并在这个新集群中选出新的主。

以图 1 所示集中式集群为例，当备 Master、Slave3 和 Slave4 节点检测到主 Master、Slave1 和 Slave2 节点不可达时，剔除这些不可达节点后，备 Master 升主，连同 Slave3 和 Slave4 节点组成一个新的集群。

如果不可达是由于节点故障导致的，那么这种策略没有任何问题。这些剩余节点组成的集群可以继续对外提供服务。但，如果不可达是因为网络故障引起的，那么集群中的另一个子集，即主 Master、Slave1 和 Slave2，也会采用同样的策略，仍然对外提供服务。这时，集群就会出现第 22 篇文章中讲到的双主问题了。

假如，我们采用一种保守的方式去处理，即节点一旦发现某些节点不可达，则直接停止自己的服务。这样确实解决了双主的问题，但因为不同分区之间的不可达是相互的，且所有的分区都采取了这种停服策略，就会导致系统中所有的节点都停止服务，整个系统完全不可用。这显然也不是我们想看到的。

那么，当系统中出现节点不可达后，如何在不出现双主的情况下，尽可能地提升系统的可用性呢？或者说，有没有什么更均衡的策略呢？

接下来，我就与你分享四种均衡的网络分区处理方法，即 Static Quorum、Keep Majority、设置仲裁机制和基于共享资源的方式。

方法一：通过 Static Quorum 处理网络分区

Static Quorum 是一种固定票数的策略。在系统启动之前，先设置一个固定票数。当发生网络分区后，如果一个分区中的节点数大于等于这个固定票数，则该分区为活动分区。

为了保证发生分区后，不会出现多个活动分区，导致出现双主或多主的问题，需要对固定票数的取值进行一些约束，既：固定票数≤ 总节点数≤2* 固定票数 - 1。

这个策略的优点是，简单、容易实现，但却存在两个问题：

一是，对于分区数比较少的时候，比方 2 个分区时，该策略很容易选出一个唯一的活动分区。但是，当活动分区非常多的时候，由于各个分区的票数分散，不容易找到一个满足条件的分区，没有活动分区也就意味着整个集群不可用了。

二是，由于这种策略里固定票数是固定不变的，所以不适用于集群中有动态节点加入的场景。

方法二：通过 Keep Majority 处理网络分区

顾名思义，Keep Majority 就是保留具备大多数节点的子集群。由于不限定每个分区的节点数超过一个固定的票数，所以可以应用于动态节点加入的场景。

假设，集群数为 n，出现网络分区后，保留的子集群为节点数 w≥n/2 的集群。为防止出现双主或两个集群同时工作的情况，通常将集群总节点数 n 设置为奇数。

可想而知，若集群总数为偶数，比如图 1 集中式架构的例子中，子集群 1 和 2 都包含 2 个 Slave 节点，就会导致两个子集群同时存活，在业务场景只允许一个主的情况下，会导致业务运行不正确。

那么，如果真的出现了集群总节点数为偶数，两个子集群节点数均为总数一半时，又应该如何解决分区问题呢？

这时，我们可以在 Keep Majority 的基础上，叠加一些策略，比如保留集群节点 ID 最小的节点所在的子集群。如图 1 所示，假设集群节点总数为 6，现在因为网络故障形成网络分区子集群 1{主 Master，Slave1, Slave2}和子集群 2{备 Master，Slave3, Slave4}，假设 Slave1 是 ID 最小的节点，那么此时要保留包含 Slave1 的子集群 1。

虽然 Keep Majority 方法可以解决动态节点加入的问题，但也不适用于产生多分区的场景。因为随着分区数增多、节点分散，也难以在众多分区中出现一个节点数 w≥n/2 的分区。

前面我讲到集群跨多个网络部署时更容易产生网络分区，因此我不推荐采用 Static Quorum 和 Keep Majority 方法去处理跨多网络集群的网络分区问题。

方法三：通过设置仲裁机制处理网络分区

设置仲裁机制的核心是，引入一个第三方组件或节点作为仲裁者，该仲裁者可以与集群中的所有节点相连接，集群中所有节点将自己的心跳信息上报给这个中心节点。因此，该中心节点拥有全局心跳信息，可以根据全局心跳信息判断出有多少个分区。当出现网络分区后，由仲裁者确定保留哪个子集群，舍弃哪些子集群。

如下图所示，假设引入 Node0 作为第三个节点，该节点 IP 为 10.12.24.35，当出现网络分区子集群 1{Node1, Node3}和子集群 2{Node2, Node4}时，每个子集群中选择一个 Leader 节点并 ping 一下 Node0 的 IP，能 ping 通则保留，否则舍弃。比如下图中，子集群 1 可以 ping 通，子集群 2 ping 不通，因此保留子集群 1。

图 3 通过设置仲裁机制处理网络分区

方法四：基于共享资源的方式处理网络分区

说到共享资源，我们需要先回顾下分布式锁的相关知识。分布式锁是实现多个进程有序、避免冲突地访问共享资源的一种方式。

基于共享资源处理网络分区的核心，其实就类似于分布式锁的机制。也就是，哪个子集群获得共享资源的锁，就保留该子集群。获得锁的集群提供服务，只有当该集群释放锁之后，其他集群才可以获取锁。关于锁的管理和获取，你可以再回顾下第 7 篇文章中的相关内容。

这种方式的问题是，如果获取锁的节点发生故障，但未释放锁，会导致其他子集群不可用。因此，这种方式适用于获取锁的节点可靠性有一定保证的场景。

基于仲裁和共享资源的网络分区处理方法，其实都是依赖一个三方的节点或组件，然后借助这个第三方来保证系统中同时只有一个活动分区。所以，这两种处理方法适用于有一个稳定可靠的三方节点或组件的场景。

总结

今天，我与你进一步展开了分布式系统中的网络分区问题，以加深你对网络分区问题的检测、处理方式的理解，并帮助你在实践应用中处理网络分区问题。

关于网络分区的处理方法，其本质就是，在产生分区后，选出一个分区，保证同时最多有一个分区对外提供服务。基于此，我为你梳理了四种常见的处理方法，包括 Static Quorum、Keep Majority、设置仲裁机制和基于共享资源的方式。

其中，基于 Static Quorum 的方法，因为涉及固定票数策略，所以不适用于处理多个分区，以及有动态节点加入的场景；基于 Keep Majority 的方法，可以解决动态节点场景下分区问题，但因为要求子集群节点数≥1/2 总节点数，所以也不适用于处理多个分区的情况；而基于仲裁和共享资源的网络分区处理方法，其实都是依赖一个三方的节点或组件，所以适用于有一个稳定可靠的三方节点或组件的场景。

如果还有哪些思考题或者留言问题，还没来得及扩展的话，你可以留言给我，后续我会再找机会进行解答。最后，我要和你说的是，和我一起打卡分布式核心技术，一起遇见更优秀的自己吧。

篇幅所限，留言区见。

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

33 | 知识串联：以购买火车票的流程串联分布式核心技术

你好，我是聂鹏程。今天，我来继续带你打卡分布式核心技术。

还记得在专栏之初，我和你分享的“分布式四纵四横知识体系”吗？截止到目前，我已经带你学习了四横和三纵，包括分布式计算、分布式存储与管理、分布式通信、分布式资源池化、分布式协同、分布式调度和分布式追踪与高可用的关键技术（由于分布式追踪、分布式部署虽属于支撑技术，但并不会影响业务的构成，因此我没有在专栏中展开）。

但学以致用才是最终目的，所以在接下来的模块中，我将通过两篇总结性质的文章，为你串联起前面讲到的核心知识点，看看它们在业务中是如何应用的。

今天，我就先以购买火车票的流程，带你串联下整个专栏涉及的分布式核心技术吧。

首先，为方便你理解，并抓住其中涉及的核心技术，我对购买火车票的流程做了一个简化，大致划分为三大核心步骤：

首先，铁路局向购票系统发布火车票；

然后，用户通过系统查询火车票，找到需要的火车票后购买；

最后，购票系统给用户响应，完成购票。

这个流程看似简单，但涉及了我们之前讲过的很多知识。

这里，我有个小建议，在学习后面的内容前，你可以先自己思考下这个过程涉及了哪些知识点，然后再与我接下来的讲述进行对比，以验证自己对之前内容的掌握程度。这样一来，你可以加深对已掌握知识的理解深度，也可以查漏补缺进而有针对性地复习其他内容。

那么接下来，我就主要分为三部分进行讲解：铁路局发布火车票、用户查询火车票，以及用户购买火车票。

铁路局发布火车票

铁路局发布火车票的过程，主要涉及分布式数据存储这一站的知识，包括存储系统三要素、数据分布和数据复制等技术。

铁路局向购票系统发布火车票的过程，主要是将火车票信息发送到服务器集群进行存储，需要用到存储系统三要素的相关技术。其中，铁路局就是存储系统三要素中的“顾客”，火车票存储到具体哪个服务器需要构建数据索引，也就是我们说的三要素中的“导购”，而存储数据的服务器就是三要素中的“货架”。

由于涉及多个服务器存储火车票信息，不可避免地需要考虑服务器之间数据存储的均衡，以及快速确定火车票信息存储位置以方便后续查询和购买火车票。因此，这里需要用到分布式存储中的数据分布技术。

铁路局按照火车线路将火车票发布到不同的服务器上，即在数据分片技术中提到的按照数据范围进行分片。比如，京广线的车票信息存储在京广线服务器集群、京沪线的车票信息存储在京沪线服务器集群等。

除此之外，为了保证可靠性，也就是当一台服务器故障后，该服务器存储的火车票信息可以恢复或不丢失，通常会进行数据备份。也就是说，同一份数据可能会有多台服务器一起存储，比如京广线的火车票数据存储到服务器 A1、A2 和 A3 上，京沪线的火车票数据存储在服务器 B1、B2 和 B3 上。

而数据备份，用到的就是分布式存储中的数据复制技术。由于同一份数据被多台服务器存储，自然就需要保证数据的一致性。关于数据一致性，你可以参考CAP 理论这篇文章。

接下来，我们再看看用户查询火车票过程中涉及了哪些相关技术吧。

用户查询火车票

对于用户查询火车票来说，大致过程是用户向服务器发起查询请求，服务器根据用户请求，通过数据索引，也就是导购技术，定位到火车票信息存储的位置，然后获取数据返回给用户。

从这个流程可以看出，服务器接收用户查询请求是第一步。

正常情况下，用户并发请求量比较小，很少会出现服务器能力有限导致系统崩溃的情况。但，遇到节假日或春节，用户请求量通常会非常大，这时如果不采取一定策略的话，大概率会因为服务器能力受限导致系统崩溃。

而这里的策略，通常就是保证分布式高可靠的负载均衡和流量控制。比如，每年春运，火车票发布的瞬间，就有大量的用户抢票，如果购票系统后台不使用负载均衡和流量控制的话，服务器一下子就被击垮了。

除此之外，服务器还不可避免地会出现一些小故障，比如磁盘损坏、网络故障等问题。如何检测服务器故障，以及如何进行故障恢复，就需要用到分布式高可用的故障隔离与故障恢复的相关技术了。

接收用户请求后，接下来需要将请求转发至相应的服务器集群，然后再从中选择某一台服务器处理用户请求，也就是获取数据并返回给用户。本质上，这就是在进行数据索引，设计分布式数据存储中的数据分布方式的相关技术。

以用户查询从北京到上海的火车票信息为例，查询流程如下：

首先，根据查询条件，系统将请求转发至存储京沪线火车票信息的服务器集群中；

然后，服务器集群再使用一次负载均衡，比如使用轮询算法， 将请求转发至某一台服务器；

最后，这台服务器将火车票的车次、余票等信息返回给用户。

在这个过程中，还可以使用限流算法，比如漏桶、令牌桶等策略来限制用户流量，保证系统高可用。

除此之外，在存储火车票信息的服务器中，各个服务器的服务单独运行，也就相当于做了一定的故障隔离，比如图中的服务器 A1~A3、B1~B3。

当然，这里还需要注意一个问题。在上面的过程中，我一直提到的是服务器集群。既然是集群，就会涉及集群架构、分布式选主等策略。

以集中式架构 Master/Slave 为例，服务器集群中会通过分布式选举算法选出一个 Master，Master 和其他服务器节点之间会维持心跳，并通过心跳来感知服务器节点的存活状态。比如，京广线服务器集群选出的 Master 节点为 A2，A2 与 A1 和 A3 之间一直维持心跳。

具体的集群架构原理，你可以参考“第二站：分布式资源管理与负载调度”；而分布选主的原理，你可以再回顾“第一站：分布式协同与同步”的相关内容。

当集群中 Master 节点故障后，会从其他节点中重新选举出一个 Master 节点，继续为用户提供服务，也就是备升主，以保证服务的可用性。这里，备升主就是一种故障恢复策略。

最后，我们再来分析下用户购买火车票的过程。

用户购买火车票

与用户查询火车票的过程类似，用户购买火车票过程唯一不同的是，会造成数据库的变化。换句话说，用户查询火车票是读请求，而购买火车票相当于一个写请求。那么，写请求又会造成什么新问题呢？

写请求与读请求的区别是，写请求会造成数据的变化，因此相对于查询火车票，购买火车票的过程涉及的技术问题会多一些，但多出的无非就是数据的一致性问题。谈到数据的一致性，我们就会想到CAP 理论、数据复制技术、分布式事务、分布式锁等。其实，不仅仅是购买火车票，任何一个简单的购买操作或写操作中，都会涉及这些分布式知识。

本质上讲，每次购买火车票的操作，就是一个分布式事务，要么执行成功要么执行失败。

当用户购买了火车票时，该火车票对应时间的车次余票数量必须相应减 1，如果减少时发现原先票数就已经为 0 了，此时就应该提醒用户购买火车票失败，余票为 0；同样的，如果票数不为 0，则票数应该相应减 1，并提示用户购票成功。

不难看出，购买火车票会改变火车票数据，也不可避免地会存在多个用户同时购买相同路线、相同车次（比如京沪线的 T12）的场景。也就是说，这个购买过程存在多个进程同时访问共享资源的问题，因此还要用到分布式锁的相关技术。

另外，用户购买火车票的过程还会涉及用户体验、数据一致性和网络故障等问题，因此还涉及 C、A、P 策略的选择问题。

在铁路局发布火车票的流程中，为了保证可靠性，同一数据通常会备份到多个服务器上。当用户购买火车票导致火车票数据改变时，主节点上的数据必须与备节点上的数据保持一致，以防止主节点故障后，备升主，但数据不一致导致业务出错的情况。

比如，用户 A 购买 2019 年 10 月 12 日北京到上海的 T12 的火车票，已购买成功，座位号为 3 车厢 23B。假设主节点和备节点之间数据不一致，主节点上已经减去该火车票，但未在备节点上减去。此时，若主节点故障，备节点升主，用户 B 此时申请购买相同火车票，系统将 3 车厢 23B 火车票又卖给了用户 B。等到乘车时，用户 A 和 B 就难免“打架”了。

当然，通常因为网络故障或节点故障等原因导致主节点不能正常工作，才会发生备升主，而备升主其实就是故障恢复策略，而一致性问题涉及的是“第五站：分布式数据存储”的相关技术。

同时，购买火车票的场景需要快速响应用户，以保证用户体验。因此，通常优先保证系统的可用性，稍微降低对数据一致性的要求，但也必须保证最终一致性。这就是我们平常遇到的，查询火车票时还有余票，但下单后却提示余票为 0，无法购买。

导致这个结果的原因是，下单前你访问的数据库中，数据还未同步，显示有余票；而下单后，数据实现同步了，发现余票数量已经为 0，因此提示你无法购买该火车票。

实现上述策略的方法，通常会采用半同步复制技术，即将修改后的数据同步到多个备数据库中的某一个或几个后立即响应用户，而不用将数据同步到所有备数据库。

除此之外，业务量很大的情况下，为了让服务更加健壮、低耦合、便于管理，会根据功能拆分为不同的服务。比如，将整个购票系统拆分为订单系统、支付系统和通知系统，而当购票系统拆分为 3 个子系统后，子系统之间不可避免的存在信息的交互，子系统之间的交互就会涉及分布式通信的相关知识，比如远程调用 RPC、消息队列等。

如图所示，用户购买火车票后，会首先在订单系统下单，下单成功后会调用支付系统的支付操作进行支付，之后将支付成功的消息存放到消息队列中，通知系统到消息队列中获取消息，最后通知用户购买成功。

总结

今天，我主要以购买火车票为例，为你串联了分布式技术在实践中的应用。

为方便理解，我将购买火车票的模型简化为三个核心步骤，即铁路局发布火车票、用户查询火车票和用户购买火车票。

其次，我分别与你分析了这三个核心步骤涉及的关键的分布式技术。

对于铁路局发布火车票这个流程来说，铁路局是数据的生产者，需要将数据发布到服务器进行存储，主要涉及的是分布式数据存储相关技术，对应专栏“第五站：分布式数据存储”的内容。

对于用户查询火车票来说，主要是读请求，涉及负载均衡、流量控制、集群管理及选主等技术，对应专栏“第一站：分布式协调与同步”“第二站：分布式资源管理与负载调度”和“第六站：分布式高可靠”的内容。

而对于用户购买火车票来说，是写请求，涉及数据的改变，因此除了用户查询火车票涉及的技术外，还额外涉及一致性、分布式事务、远程通信等技术，对应专栏“第一站：分布式协调与同步”、“第四站：分布式通信技术”、“第五站：分布式数据存储”和第六站:“分布式高可用”等内容。

最后，我再通过一张思维导图来归纳一下今天的核心知识点吧。

思考题

结合购买火车票这个案例，你能和我分享你身边的应用场景或系统，都涉及或采用了哪些分布式技术吗？

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

34 | 搭建一个分布式实验环境：纸上得来终觉浅，绝知此事要躬行

你好，我是聂鹏程。

上一讲，我以购买火车票为例，为你串讲了分布式技术的应用，帮助你理解所学分布式技术可以应用到哪些业务中。其实，到目前为止，我们主要是从理论上学习相关的分布式技术。但，“纸上得来终觉浅，绝知此事要躬行”。

今天，我就以 Kubernetes 为例，和你一起搭建一个分布式实验环境。我先简单和你说下这篇文章的内容分配：

不会特别详细地讲述搭建过程，而是着重说明搭建的主要步骤以及可能遇到的问题；

在讲述搭建过程时，串联一下其中涉及的分布式相关知识；

搭建完 Kubernetes 集群之后，我会以部署 Nginx 服务为例，帮助你更直观地体验分布式技术，以巩固、加深对分布式技术的理解。

话不多说，接下来，我们就一起搭建这个分布式实验环境吧。

搭建目标

Kubernetes 是 Google 开源的容器集群管理系统，是 Borg 的开源版本。我在第 9 篇文章中讲解集中式架构时，和你分析过 Kubernetes 集群属于主从架构的分布式集群。

Kubernetes 集群主要由 Master 节点和 Worker 节点组成。Master 节点就是中心服务器，负责对集群进行调度管理；Worker 节点是真正的工作节点，负责运行业务应用的容器。而容器是一种虚拟化技术，通过限制自身使用的资源来实现资源隔离，可以为应用提供一整套运行环境，从而实现了服务运行环境的隔离，进而实现了故障隔离。你可以回顾下第 30 篇文章中，资源隔离的相关内容。

接下来，我们明确下这次搭建分布式实验室环境的目标：

搭建一个 Kubernetes 集群，包括一个 Master 节点，两个 Worker 节点 ;

在 Kubernetes 集群上创建一个 Nginx 服务。

搭建前的准备

今天我们要搭建的 Kubernetes 集群，以 3 台服务器为例，一台作为 Master 节点，两台作为 Worker 节点。服务器应具备的条件如下：

Ubuntu 16.04 操作系统；

2GB 或以上的内存；

2 核 CPU 或以上；

服务器间网络连通；

每台服务器具有唯一的主机名、MAC 地址和 product_uuid；

通过执行命令 swapoff -a 来关闭 Swap；

30GB 及以上的磁盘空间；

具备外网访问权限，以方便获取相关镜像。

在这次部署中，我采用的机器配置如下：

准备工作完成后，我们就开始搭建集群吧。

Kubernetes 集群搭建

搭建 Kubernetes 集群的步骤，主要包括安装 Docker，安装部署 kubeadm、kubelet、kubectl，部署 Master 节点，部署 Worker 节点，安装网络插件这几步。

其中，安装 Docker、部署 Master 节点和 Worker 节点涉及分布式的，需要在多个节点上部署，比如 Docker 节点需要在每个 Worker 节点部署，Master 节点若为集群模式，需要在多个节点上配置主备，Worker 节点需要与 Master 节点建立连接等。

接下来， 我们具体看看如何一步一步搭建出 Kubernetes 集群吧。

1. 安装 Docker

Kubernetes 是一个容器集群管理系统，因此每个 Worker 节点会运行容器，以实现业务运行环境隔离。我们在每台服务器上采用如下命令安装 Docker：

apt-get install -y docker.io

2. 安装部署 kubeadm、kubelet、kubectl

kubeadm 是 Kubernetes 社区提供的一个部署工具，该工具将 kubelet 组件之外的其他组件均采用容器部署，实现了自动化， 避免了手动部署容器的麻烦，简化了部署操作。

其中，Master 节点包括 API Server、Scheduler、Cluster State Store(默认 etcd) 和 Control Manager Srever 核心组件；Worker 节点包括 kubelet 和 kube-proxy 核心组件。具体的组件功能和原理，你可以再回顾下第 9 篇文章中的相关内容。

kubelet 组件本身是一个管控容器的组件，需要执行配置容器网络等操作，这些操作需要在宿主机上执行，不采用容器部署。因此，kubelet 组件需要单独部署，而不能用 kubeadm 进行部署。

除此之外，我们还需要安装一下 kubectl 组件。这个组件是 Kubernetes 的命令行工具，通过 kubectl 可以部署和管理应用，查看资源，创建、删除和更新组件。

那么，如何部署 kubeadm、kubelet 和 kubectl 这三个组件呢？

apt 是 Linux 下常用的安装管理工具，这里我就采用 apt 来安装这三个组件。

首先，我们需要添加 Kubernetes 源。

你可以通过执行以下语句获取 Kubernetes 源（需要外网权限）：

sudo apt-get update && sudo apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF

然后使用以下命令更新源：

sudo apt-get update

这时，我们就可以使用如下命令来安装 kubelet、kubectl 和 kubeadm 了：

sudo apt-get install -y kubelet kubeadm kubectl

如果没有外网访问权限，在添加 kubernetes 源的时候可以执行以下命令来添加阿里云的 Kubernetes 源：

cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main
EOF

同样的，使用以下命令来更新源：

apt-get update # 忽略 gpg 的报错信息

最后，使用如下命令安装 kubelet、kubectl 和 kubeadm：

apt-get install -y kubelet kubeadm kubectl --allow-unauthenticated

安装好这三个组件之后，我们就可以使用 kubeadm 来一键部署集群节点了。

首先，我们来部署 Master 节点。

3. 部署 Master 节点

这一步其实就是容器化启动 Master 节点中的各个组件，直接使用 kubeadm 工具提供的一条命令 kubeadm init 即可自动安装。

kubeadm init 这条命令底层其实就是将 Master 的各个组件，比如 API Server、etcd 等，以 Pod 形式（容器集合）运行起来。

当然了，你可以部署多个 Master 节点来实现集群的高可用，比如两个 Master 节点互为主备（你可以回顾下第 31 篇文章中介绍的主备机制）。具体的部署方法，你可以参考这篇文章。

除此之外，etcd 组件也可以采用集群方式部署，从而保证了数据不会丢失。etcd 采用的是 Raft 强一致性协议，相关技术你可以再回顾下第 4 篇文章中的相关问题。

在本次部署中我以一个 Master 节点为例为你讲解集群的搭建，关于 Master 为集群的方式，各节点上 kubernetes 配置类似，你可以参考 Kubernetes 官网。

在这里，我把 192.168.124.49 这台机器作为 Master 节点。在该机器上直接执行 kubeadm init，即可完成部署：

kubeadm init

如果有外网访问权限，基本就可以部署成功了。那么，我们可以根据如下信息判断自己是否部署成功：

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.124.49:6443 --token uv17vd.q3ber8i5knxg4h0x \
    --discovery-token-ca-cert-hash sha256:c55bd70d346d809e1079565cc1fc1a05f001671cc9f2d02c55bbbc4a00bcc2a3

可以看到，想要使用集群，需要执行以下命令，执行结束后才可以使用 kubectl。

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

如果没有外网访问权限，会报 pull image xxxxxx 的错误。

此时不要慌，从报错信息中，我们可以看到哪些镜像拉取不成功。我们可以手动在 Docker Hub 上寻找相对应的组件及版本，进行拉取，然后再通过 Docker 打 tag，修改为需要的镜像。

比如，以 [ERROR ImagePull]: failed to pull image k8s.gcr.io/kube-apiserver:v1.16.3 为例，可以通过以下代码进行拉取。

# 可以拉取的相对应的组件和版本
docker pull aiotceo/kube-apiserver:v1.16.3
# 通过打 tag 的方式修改为所需要的镜像
docker tag aiotceo/kube-apiserver:v1.16.3 k8s.gcr.io/kube-apiserver:v1.16.3

然后重新执行 kubeadm init 即可。

从以上操作也可以看出，kubeadm 的底层其实就是将容器化组件的操作实现了自动化， 省去了手动部署的麻烦。

部署完 Master 节点后，我们来继续部署 Worker 节点。

4. 部署 Worker 节点

部署 Worker 节点与部署 Master 节点类似，都可以通过命令一键部署。这里，我们使用 kubeadm 提供的 kubeadm join 命令来进行自动化部署。

kubeadm join 命令的底层与 kubeadm init 类似，会自动以 Pod 形式运行 Worker 节点中需要的组件。不同的是，命令执行后，底层还需要将 Worker 节点加入到 Kubernetes 集群中。

执行 kubeadm join 命令后（具体命令如下所示），就可以看到 Kubernetes 集群中的节点信息了。这条命令中需要配置 Master 节点的 IP 和 Port 信息，目的是 Worker 节点根据 IP 和 Port 信息建立连接，并在建立连接的基础上，建立心跳机制。

具体的心跳机制，你可以参考第 31 篇文章中关于故障恢复的内容。

到目前为止，Kubernetes 的集群已经完成大半了，下面我们继续部署集群。

根据 Master 节点部署成功后输出结果的最后几行可以知道，想要加入集群，可以执行 kubeadm join 命令。我在另外 2 台机器上都执行了如下命令：

kubeadm join 192.168.124.49:6443 --token uv17vd.q3ber8i5knxg4h0x \
    --discovery-token-ca-cert-hash sha256:c55bd70d346d809e1079565cc1fc1a05f001671cc9f2d02c55bbbc4a00bcc2a3

这条命令执行后，一个集中式架构的雏形就搭建完成了。接下来，我们需要安装相应的网络插件，以实现 Kubernetes 集群中 Pod 之间的通信。

5. 安装网络插件

网络插件有很多，比如 Canal、Flannel、Weave 等。不同的插件命令不一致，具体命令可参考官网。

这里，我以安装 Weave 插件为例，通过执行以下命令完成安装：

sysctl net.bridge.bridge-nf-call-iptables=1

kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"

6. 验证

到这里，集群就部署完成了，是不是很简单呢？接下来，我就通过获取节点和 Pod 信息来验证一下集群部署是否成功。

可以通过刚刚安装的 kubectl 组件提供的命令查看集群的相关信息。比如，查看节点的运行状态可以通过 kubectl get nodes 来获得，查看各个组件对应的 Pod 运行状态可以通过 kubectl get pods 来获得。命令执行结果，如下所示：

kubectl get nodes

# NAME           STATUS   ROLES    AGE   VERSION
# vm1-pc   Ready    master   11h   v1.16.3
# vm2-pc   Ready    <none>   11h   v1.16.3
# vm3-pc   Ready    <none>   24m   v1.16.3

kubectl get pods --all-namespaces

# NAMESPACE     NAME                                   READY   STATUS    RESTARTS   AGE
# kube-system   coredns-5644d7b6d9-9dprc               1/1     Running   0          11h
# kube-system   coredns-5644d7b6d9-ljv5w               1/1     Running   0          11h
# kube-system   etcd-vm1-pc                      1/1     Running   0          11h
# kube-system   kube-apiserver-vm1-pc            1/1     Running   0          11h
# kube-system   kube-controller-manager-vm1-pc   1/1     Running   0          11h
# kube-system   kube-proxy-qpvtb                       1/1     Running   0          25m
# kube-system   kube-proxy-v2xnb                       1/1     Running   0          11h
# kube-system   kube-proxy-wkxzg                       1/1     Running   0          11h
# kube-system   kube-scheduler-vm1-pc            1/1     Running   0          11h
# kube-system   weave-net-6nj4c                        2/2     Running   0          25m
# kube-system   weave-net-lm6dh                        2/2     Running   0          37m
# kube-system   weave-net-vwnc2                        2/2     Running   0          37m

可以看到，节点全部是 Ready 状态，各个组件对应的 Pod 也处于 Running 状态，表明部署成功。

7. 可能遇到的问题

如果整个安装失败的话，可以重置，重新安装，即重新 kubeadm init

kubeadm reset

部署 Worker 节点时，pod 部署不成功。原因可能是因为没有外网访问权限，镜像拉取不下来，可以通过以下命令查看 pod 的相关信息：

# 检查所有 pod 是否正常
kubectl get pod --all-namespaces -o wide
# 如果 pod 处于非 running 状态，则查看该 pod：
kubectl describe pod xxxxx -n kube-system

从错误信息里可以查看到是哪个镜像拉取不下来，与部署 Master 节点时采用的方式一样，到 Docker Hub 上手动拉取镜像，并设置 Tag 即可。

至此，Kubernetes 集群就配置成功了。

集群环境搭建后，如何验证集群是可用的呢？或者说，如何在集群上运行服务呢？接下来，我就以 Nginx 服务为例，带你了解如何在 Kubernetes 集群上进行服务部署。当然，你可以参考这个例子，在 Kubernetes 集群上部署其他服务。

Nginx 服务部署

Kubernetes 推荐使用 YAML 配置文件的方式来创建服务，所以我接下来会使用这种方式部署完成 Nginx 服务的部署。

部署 Nginx 服务这个 Demo 时，我会创建两个 Kubernetes 对象（Kubernetes 对象是 Kubernetes 系统中的持久实体，用于表示集群的状态），一个是 Deployment，一个是 Service：

Deployment 对象规定 Pod 创建的相关信息，比如期望创建几个 Pod，每个 Pod 应该部署什么应用等。

Service 对象用来给用户访问提供接口。它可以通过 Label Selector（标签选择器）来指定可以访问的 Pod 有哪些。关于 Kubernetes 对象的相关内容，你可以参考这篇文章。

因为 Pod 是 Kubernetes 中最小的工作单元，所以 Nginx 服务都部署在 Pod 中。下面，我就来创建一个 Deployment 对象来创建我们期望的 Pod 状态。

首先，创建一个 YAML 配置文件，我将其命名为 nginx-deployment.yaml。为将用户请求负载均衡到不同的 Pod，减轻单个 Pod 的访问压力，这里我会创建三个 Pod 共同运行 Nginx 服务。

文件内容如下：

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

文件中，replicas 字段就是副本数量，也就是 Pod 数量，设置为 3，即创建三个 Pod 来运行 Nginx 服务；template 字段规定了单个 Pod 中运行哪些容器，这里运行的是名称为 nginx 的容器。

创建完配置文件后，通过以下命令就可以将 Deployment 对象创建成功。

kubectl apply -f nginx-deployment.yaml

执行后，就等待对象的创建，可以通过以下命令来查看创建是否成功。

kubectl get deployment

以下是我创建成功后的输出：

NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           3m17s

同时，你也可以通过以下命令来查看创建的 Pod 的信息：

kubectl get pod

以下是我的输出结果：

NAME                               READY   STATUS    RESTARTS   AGE
nginx-deployment-59c9f8dff-dtg4w   1/1     Running   0          3m15s
nginx-deployment-59c9f8dff-f2hmv   1/1     Running   0          3m15s
nginx-deployment-59c9f8dff-lsvdh   1/1     Running   0          3m15s

创建完 deployment 之后，我们来创建 Service 服务。同样是通过配置文件来创建，文件名是 nginx-service.yaml，内容如下：

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  labels:
    app: nginx
spec:
  ports:
  - port: 88
    targetPort: 80
  selector:
    app: nginx
  type: NodePort

文件中 port 属性就是 service 对外提供的端口。

同样的，采用 kubectl apply 命令创建 Nginx 服务：

kubectl apply -f nginx-service.yaml

执行完成后，可以通过以下命令来查看创建是否成功：

kubectl get service

以下是我的输出结果：

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
kubernetes      ClusterIP   10.96.0.1        <none>        443/TCP          12h
nginx-service   NodePort    10.101.29.9      <none>        88:30755/TCP     5m12s

现在我们就可以通过访问 Nginx 服务来查看它是否部署成功了。访问该服务可以通过以下命令：

curl 10.101.29.9:88

结果如下，表明 Nginx 服务部署成功。

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

在这个过程中，有两个步骤涉及负载均衡的相关知识：

一个是创建 Deployment 时，该 Deployment 会创建三个 Pod，而 Pod 需要部署到某个 Worker 节点中，因此会将 Pod 均衡部署到各个 Worker 节点中；

另一个是用户访问，Nginx 服务后台三个运行的 Pod 都可以提供服务，用户访问到来时，可以均衡分布到各个 Pod 中进行处理。

到这里，我们搭建的目标就完成了，下面为你留几个实验题，你可以尝试去搭建一下或运行一下，以进一步加深对分布式技术的理解。

实验一：搭建高可用 Kubernetes 集群，也就是通过 etcd 实现 Master 节点以集群模式部署。具体搭建方法可参考https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/；

实验二：在 Kubenetes 上部署 Cassandra，其中 Cassandra 作为服务部署到容器中，以学习 Cassandra 集群的节点发现、集群组件等原理，具体搭建方法可参考https://kubernetes.io/docs/tutorials/stateful-application/cassandra/；

实验三：在 Kubenetes 集群上通过部署一个 MySQL 服务，体验在 Kubenetes 集群上如何运行一个单实例有状态应用。具体搭建方法可参考https://kubernetes.io/docs/tasks/run-application/run-single-instance-stateful-application/。

好了，整个搭建环境，我就讲到这里。

其实，到这里，对分布式世界的探索可以说才刚开始，只有动手去实践，你学到的知识才能真正转换为你自己的。加油，赶紧行动起来吧。

总结

今天，我主要带你学习了搭建分布式实验环境。

首先，我以 Kubernetes 为例，介绍了如何搭建 Kubernetes 集群环境，其中包括容器、Master 节点、Worker 节点等配置和安装。

然后，在搭建好的 Kubernetes 集群的基础上，我以 Nginx 服务为例，展示了如何在 Kubernetes 集群上部署服务。

其实，今天我演示的 Demo 只是冰山一角。在 Kubernetes 中，有很多非常实用的功能，比如 Kubernetes 可以让服务持续不断运行，当有 Pod 出现故障时，会自动重启另一个 Pod 来达到 Deployment 配置文件中规定的期望状态；还可以自动实现版本更迭等。

相信通过本讲的学习，你会对分布式技术有更进一步的认知。加油，赶紧行动起来，为你的服务搭建一个分布式实验环境吧。

我是聂鹏程，感谢你的收听，欢迎你在评论区给我留言分享你的观点，也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会！

结束语 | 为什么说提升职业竞争力要从尊重、诚实开始？

你好，我是聂鹏程。

斗转星移，时光如梭，一转眼就到了写结束语的时刻。写结束语，也就意味着这个专栏要告一段落了。

在这里，我首先要感谢你一路的鼓励与陪伴！你的孜孜不倦、坚持打卡让我备受感动，你的留言和肯定让我深受鼓舞，你对文中疏漏之处的提醒让我愈发严谨，你引人深思的提问更让我深受启发！

还记得专栏上线是在 9 月 23 日，那是硕果累累的金秋，而专栏结束时已是白雪皑皑的严冬。与此同时，业内也正经历着一场“寒冬”，各种裁员 / 辞退消息不绝于耳。

在这种大背景下，我看到有人在焦虑自己的职业生命力，还有人在大谈技术无用论。

记得专栏上线没多久，我就看到这么一条反馈：“分布式只有在面试中才用到，在工作中感觉没有什么用。”从中，我能感受到这位同学对分布式技术的爱恨交加，也有些担忧，有这种想法的同学若干年后会不会成为焦虑大军中的一员。

那么，如何提升职业竞争力，避免“中年危机”和“职业生命力的焦虑”呢？在我看来，无外乎两个方面。

第一，要敬畏技术。我们经常听见各种议论，说某种技术没有用，某种技术太 low 了。毛主席说：“战略上藐视敌人，战术上重视敌人。”如果只是战略上轻视，我觉得倒无伤大雅，可怕的是还没有深入了解，就从战术上轻视这些技术。殊不知，我们耳熟能详的很多新兴技术都是这些“陈芝麻烂谷子”技术的组合、延伸。

比如，Docker 已成为业务发布、部署标配的容器组件，但其底层依赖的是 Linux Namespace 环境隔离、cgroups 资源限制等基础得不能再基础的技术。再比如，区块链作为一种开创性的去中心化多方信任技术，现如今成为了国家的战略，但其所依赖的分布式共识、P2P 网络、非对称加密算法等没有一个是全新的技术。这样的例子数不胜数。

这些案例告诉我们，不是技术没用，而是我们没有深入理解它们，没有找到它们的用武之地。我相信，当我们学会了尊重技术之后，化腐朽为神奇也就不再遥远了。

第二，对自己要诚实。前一阵子，我遇到之前带的一个小兄弟。了解了他的近况后，我建议他在做一些重复工作之余，提升一下自我。没想到，他说：“你看，我是 985 名校毕业，技术够好，劝退这种事情根本不会发生到我头上。”殊不知，那些被劝退的人是不是也曾有过这样的想法呢？

如果我们都无法对自己诚实，那何谈去改变自己，更不要说突破自己、完善自己了。如果一个人选择欺骗自己，当时代抛弃他时，可是连一声再见都不会说的。

我想，这也是乔布斯为什么会说“Stay hungry, stay foolish”的原因吧！

所以，我始终认为提升职业竞争力，要从尊重和诚实开始。

当我们学会尊重和诚实后，又应该树立什么样的技术目标呢？于我来讲，把自己塑造成一个“倒三角”人才（或 T 型人才），是一直以来的目标。

我博士的研究方向是并行与分布式技术，按照 T 型人才的原则，我努力把并行与分布式技术，作为技术发展的根据地，也就是字母 T 中的一竖。在不断做深、做厚分布式技术的同时，我会努力探索分布式技术跨界到新兴领域的可能性，或结合分布式技术去研究一些新兴技术，提升自己的技术广度，也就是字母 T 中的一横。

有了分布式技术这只抓手，当 IoT、人工智能、区块链、云计算、大数据、边缘计算等新兴技术涌现后，我发现我基本上都能插上一手，因为这些新兴技术和分布式技术都存在交叉的地方。

那么对你来说，一竖在哪里，一横又在哪里呢？

最后，再次感谢你的支持和陪伴！希望分布式专栏带给你的不仅仅是知识，还有学习能力的提升和思维方式的变化。很幸运，你我都处在一个对创新、对人才都格外尊重的时代！请相信“Where there is a will, there is a way”。

在文章结尾，我为你准备了一份调查问卷，题目不多，希望你能抽出两三分钟填写一下。我非常希望听听你对这个专栏的意见和建议，期待你的反馈！专栏的结束，也是另外一种开始，我和编辑已经制定了内容迭代计划。比如，中间有一些留言还没来得及回复，我会花时间处理完。再比如，有同学反馈其中几篇文章说写得不通透，我看完觉得确实觉得有道理，所以会继续优化。

对于你来说也是这样，分布式博大精深，岂是我一个专栏能讲完的？所以，你的学习之旅，也才刚刚开始。让我们一起加油，一起去探寻那个最好的自己。

点击图片即可填写问卷：