导读
开源文档:凤凰架构 构建可靠的大型分布式系统
为了得到高质量的软件产品,我们是应该把精力更多地集中在提升每一个人员、过程、产出物的能力和质量上,还是放在整体流程和架构上?这两者都重要。前者重术,后者重道;前者更多与编码能力相关,后者更多与软件架构相关;前者主要由开发者个体水平决定,后者主要由技术决策者水平决定。
如何学习一项具体的语言、框架、工具,比如 Java、Spring、Vue.js,等等,都是相对具象的,不论其蕴含的内容多少、复杂程度的高低,它们至少是能看得见、摸得着的。
而如何学习某一种风格的架构方法,比如单体、微服务、服务网格、无服务、云原生,等等,则是相对抽象的,谈论它们可能要面临着“一百个人眼中有一百个哈姆雷特”的困境。
可靠系统:不死鸟
计算机之父冯 · 诺依曼(John von Neumann)在 1940 年代末期,就曾经花了大约两年的时间,来研究这个问题,并且得出了一门理论《自复制自动机》(Theory of Self-Reproducing Automata),这个理论以机器应该如何从基本的部件中,构造出与自身相同的另一台机器引出。他的目的并不是想单纯地模拟或者理解生物体的自我复制,也并不是简单地想制造自我复制的计算机,而就是想回答一个理论问题:如何用一些不可靠的部件来构造出一个可靠的系统。
当时自复制机的艺术表示
生命系统之所以可靠的本质,恰恰是因为它可以使用不可靠的部件来完成遗传迭代。这其中的关键点,便是承认细胞、分子等这些零部件可能会出错,某个具体的零部件可能会崩溃消亡,但在存续生命的微生态系统中,一定会有其后代的出现,重新代替该零部件的作用,以维持系统的整体稳定。在这个微生态里,每一个部件都可以看作是一只不死鸟(Phoenix)(凤凰),它会老迈,而之后又能涅槃重生。
架构的演进
架构演变最重要的驱动力,或者说产生这种“从大到小”趋势的最根本的驱动力,始终都是为了方便某个服务能够顺利地“死去”与“重生”而设计的。
流水不腐,有老朽、有消亡、有重生、有更迭,才是正常生态的运作合理规律。
户枢不蠹流水不腐
流水不争先,争的是滔滔不绝
只要在整体架构设计中,有恰当的、自动化的错误熔断、服务淘汰和重建的机制,那在系统外部来观察,它在整体上仍然有可能表现出稳定和健壮的服务能力。
(凤凰项目demo:在企业软件开发的历史中,当发布一项新技术的时候,常常会有伴以该技术开发的“宠物店(PetStore)”作为演示的传统(如J2EE PetStore、.NET PetShop、Spring PetClinic等)。)
原始分布式时代:UNIX分布式哲学下的分布式探索
UNIX 的分布式设计哲学 Simplicity of both the interface and the implementation are more important than any other attributes of the system — including correctness, consistency, and completeness 保持接口与实现的简单性,比系统的任何其他属性,包括准确性、一致性和完整性,都来得更加重要。 —— Richard P. Gabriel,The Rise of ‘Worse is Better’
DCE的失败
为了避免Unix 系统的版本战争在分布式领域中重演,负责制定 Unix 系统技术标准的开放软件基金会(Open Software Foundation,OSF,也就是后来的“国际开放标准组织”)就邀请了各个主要的研究厂商一起参与,共同制订了“分布式运算环境”(Distributed Computing Environment,DCE)的分布式技术体系。DCE 包括了一整套完整的分布式服务组件的规范与实现。
因为 OSF 本身的背景(它是一个由 Unix 开发者组成的 Unix 标准化组织),所以在当时研究这些分布式技术,通常都会有一个预设的重要原则,也就是在实现分布式环境中的服务调用、资源访问、数据存储等操作的时候,要尽可能地透明化、简单化,让开发人员不用去过于关注他们访问的方法,或者是要知道其他资源是位于本地还是远程。这样的主旨呢,确实非常符合Unix 设计哲学(有过几个版本的不同说法,这里我指的是 Common Lisp 作者Richard P. Gabriel提出的简单优先“Worse is Better”原则),但这个目标其实是过于理想化了,它存在一些在当时根本不可能完美解决的技术困难。
“调用远程方法”与“调用本地方法”尽管只是两字之差,但要是想能同时兼顾到简单、透明、性能、正确、鲁棒(Robust)、一致的目标的话,两者的复杂度就完全不能相提并论了。
我们先不说,远程方法是不可能做到像本地方法一样,能用内联等传统编译原理中的优化算法,来提升程序运行速度的,光是“远程”二字带来的网络环境下的新问题。
比如说,远程的服务在哪里(服务发现)、有多少个(负载均衡)、网络出现分区、超时或者服务出错了怎么办(熔断、隔离、降级)、方法的参数与返回结果如何表示(序列化协议)、如何传输(传输协议)、服务权限如何管理(认证、授权)、如何保证通信安全(网络安全层)、如何令调用不同机器的服务能返回相同的结果(分布式数据一致性)等一系列问题,就需要设计者耗费大量的心思。
DCE 不仅从零开始、从无到有地回答了其中大部分问题,构建出了大量的分布式基础组件与协议,而且它还真的尽力去做到了相对意义的“透明”。
但是在那个年代,在机器硬件的限制下,开发者为了让程序在运行效率上可以接受,就只有在方法本身的运行时间很长,可以相对忽略远程调用成本时的情况下,才去考虑使用分布式。如果方法本身的运行时长不够,就要人为地用各种奇技淫巧来刻意构造出这样的场景,比如可能会将几个原本毫无关系的方法打包到一个方法内,一块进行远程调用。
本地与远程,无论是从编码、部署,还是从运行效率的角度上看,都有着天壤之别,所以在设计一个能运作良好的分布式应用的时候,就变得需要极高的编程技巧和各方面的知识来作为支撑,这个时候,反而是人员本身对软件规模的约束,超过机器算力上的约束了。
对 DCE 的研究呢,算得上是计算机科学中第一次有组织领导、有标准可循、有巨大投入的分布式计算的尝试。但无论是 DCE,还是稍后出现的 CORBA(Common ObjectRequest Broker Architecture,公共对象请求代理体系结构),我们从结果来看,都不能说它们取得了成功。因为把一个系统直接拆分到不同的机器之中,这样做带来的服务的发现、跟踪、通讯、容错、隔离、配置、传输、数据一致性和编码复杂度等方面的问题,所付出的代价远远超过了分布式所取得的收益。
原始分布式时代的教训Just because something can be distributed doesn’t mean it should be distributed. Trying to make a distributed call act like a local call always ends in tears.某个功能能够进行分布式,并不意味着它就应该进行分布式,强行追求透明的分布式操作,只会自寻苦果。—— Kyle Brown,IBM Fellow,Beyond buzzwords: A brief history of microservices patterns,2016
简单性的理解
整个“演进中的架构”这部分,一条重要的逻辑线索就是软件工业对如何拆分业务、隔离技术复杂性的探索。从最初的不拆分,到通过越来越复杂的技术手段逐渐满足了业务的拆分与协作,再到追求隔离掉这些复杂技术手段,将它们掩埋于基础设施之中,到未来(有可能的)重新回到无需考虑算力、无需拆分的云端系统。
简单需要从两方面来看待,分别是业务和技术。
2.先说业务。现代的软件系统的业务复杂性越来越高。而分离关注点无疑是应对日益增长的业务复杂性的有效手段。但如果依旧是是一个大单体系统(所有业务单元都在一个容器下),那么跨业务单元的知识诉求便很难避免,并且开发迭代以及版本发布中彼此还会相互影响。而微服务的出现为其提供了设定物理边界的技术基础。使得多个特性团队对业务知识的诉求可以收敛在自身领域,降低单个特性团队所需了解的业务知识。
3.再说下技术。这里我认为主要提现在技术隔离上。就像rpc让你像调用本地方法一样调用远程方法,微服务技术组件的出现,大多是为了让开发人员可以基于意图去使用各种协调分布式系统的工具,而不用深入具体工具的实现细节去研究怎么解决的分布式难题。
4.同时就像sprngboot提到的生产就绪,微服务的生态已经不局限于开发的阶段。在部署和运行阶段都有健全组件支持。它可以让开发人员基于意图就可以简便的实现金丝雀发布,基于意图就能拿到所有系统运行期的数据。所有的这些便利都算是技术隔离带来的好处。
单体系统时代:应用最广泛的架构风格
大型单体系统
思维误区:单体架构是落后的架构。
对于小型系统而言,单体不仅易于开发、易于测试、易于部署,而且因为各个功能、模块、方法的调用过程,都是在进程内调用的,不会发生进程间通讯,所以程序的运行效率也要比分布式系统更高。单体架构更为适合。
当我们在讨论单体系统的缺陷的时候,必须基于软件的性能需求超过了单机,软件的开发人员规模明显超过了“2 Pizza Teams”(两个披萨原则)范畴的前提下,这样才有讨论的价值。
单体系统并不意味着不可拆分
从纵向角度来看,在现代信息系统中,我从来没有见到过实际的生产环境里,有哪个大型的系统是完全不分层的。分层架构(Layered Architecture)已经是现在几乎所有的信息系统建设中,都普遍认可、普遍采用的软件设计方法了。无论是单体还是微服务,或者是其他架构风格,都会对代码进行纵向拆分,收到的外部请求会在各层之间,以不同形式的数据结构进行流转传递,在触及到最末端的数据库后依次返回响应。那么,对于单体架构来说,在这个意义上的“可拆分”,单体其实完全不会展露出丝毫的弱势,反而还可能因为更容易开发、部署、测试而更加便捷。比如说,当前市面上所有主流的 IDE,如 Intellij IDEA、Eclipse 等,都对单体架构最为友好。IDE 提供的代码分析、重构能力,以及对编译结果的自动化部署和调试能力,都是主要面向单体架构而设计的。

图 1-1 分层架构示意
图片来自 O’Reilly 的开放文档《Software Architecture Patterns》
单体系统并不意味着就只能有一个整体的程序封装形式,如果有需要,它完全可以由多个 JAR、WAR、DLL、Assembly 或者其他模块格式来构成。即使是从横向扩展(Scale Horizontally)的角度来衡量,如果我们要在负载均衡器之后,同时部署若干个单体系统的副本,以达到分摊流量压力的效果,那么基于单体架构,也是轻而易举就可以实现的。
非独立的单体
“拆分”这方面,单体系统的真正缺陷实际上并不在于要如何拆分,而在于拆分之后,它会存在隔离与自治能力上的欠缺。
在单体架构中,所有的代码都运行在同一个进程空间之内,所有模块、方法的调用也都不需要考虑网络分区、对象复制这些麻烦事儿,也不担心因为数据交换而造成性能的损失。
在获得了进程内调用的简单、高效这些好处的同时,也就意味着,如果在单体架构中,有任何一部分的代码出现了缺陷,过度消耗进程空间内的公共资源,那所造成的影响就是全局性的、难以隔离的。
首先,一旦架构中出现了内存泄漏、线程爆炸、阻塞、死循环等问题,就都将会影响到整个程序的运行,而不仅仅是某一个功能、模块本身的正常运作;而如果消耗的是某些更高层次的公共资源,比如端口占用过多或者数据库连接池泄漏,还将会波及到整台机器,甚至是集群中其他单体副本的正常工作。
此外,同样是因为所有代码都共享着同一个进程空间,如果代码无法隔离,那也就意味着,我们无法做到单独停止、更新、升级某一部分代码,因为不可能有“停掉半个进程,重启 1/4 个进程”这样不合逻辑的操作。所以,从动态可维护性的角度来说,单体系统也是有所不足的,对于程序升级、修改缺陷这样的工作,我们往往需要制定专门的停机更新计划,而且做灰度发布也相对会更加复杂。
由于隔离能力的缺失,除了会带来难以阻断错误传播、不便于动态更新程序的问题,还会给带来难以技术异构等困难。
技术异构:后面在介绍微服务时,我会提到马丁 · 福勒(Martin Fowler)提出的 9 个特征,技术异构就是其中之一。它的意思是说允许系统的每个模块,自由选择不一样的程序语言、不一样的编程框架等技术栈去实现。单体系统的技术栈异构不是一定做不到,比如 JNI 就可以让 Java 混用 C/C++,但是这也是很麻烦的事,是迫不得已下的选择。
不过,在我看来,我们提到的这些问题,还不是我们今天以微服务去代替单体系统的根本原因。我认为最根本的原因是:单体系统并不兼容“Phoenix”的特性。
单体这种架构风格,潜在的观念是希望系统的每一个部件,甚至每一处代码都尽量可靠,不出、少出错误,致力于构筑一个 7×24 小时不间断的可靠系统。
这种观念在小规模软件上能运作良好,但当系统越来越大的时候,交付一个可靠的单体系统就会变得越来越有挑战性。
就像我在导读《什么是“The Fenix Project”?》中所说的,正是随着软件架构的不断演进,我们构 建可靠系统的观念,开始从“追求尽量不出错”,转变为了正视“出错是必然”。实际上,这才是微服务架构能够挑战,并且能逐步开始代替运作了几十年的单体架构的根本驱动力。
微服务和单体的演化关系
做微服务后,每一个服务其实还是单体,还是需要一些单体的理念去看待服务的生命周期等。微服务只不过是在规模大了之后,如何更好的发挥出单体的价值逐渐演化而来。
作者回复: 微服务确实可以理解为“一群能够配合良好工作的、关注各自领域的小单体”,或者说它们最终会是殊途同归的。微服务与单体之间的关注点差异不在于“大与小”,而在于一群小单体“能够配合良好工作”, 这中间涉及到许多既麻烦又不得不去处理的事情。
