架构风格,也称为架构模式,是描述一种包含各种架构特征的组件之间的确定关系。与设计模式类似,一种架构风格的命名,可以作为有经验的架构师之间简略的表达方式。例如,当架构师谈到分层单体架构,他们谈话的对象都能理解结构的各个层面,包括知道哪几种的架构特征可以工作得很好(也知道可能会导致什么问题发生),通常有哪几种部署模式,数据处理策略,还有其他很多的相关信息。因此,架构师应该对几种基本的、通用的架构风格的命名非常熟悉。
每一种架构命名都包含了大量大家都理解的细节,这也是设计模式起到的其中一个作用。每一种架构风格描述了包含拓扑结构,假定和默认的架构特征,以及此种架构的优缺点的内容。在本书这一部分(第二部分)后面我们将介绍很多常见的现代架构模式。然而,架构师应该熟悉嵌入在较大模式当中的几个基本模式。
基本模式
几种架构模式因为在组织代码、部署、或者架构的其他方面提供了一种有效的视角,从而在软件架构演变历史中一再出现。例如,在架构中根据不同功能的关注点分开的分层概念,在软件诞生之初就已经存在。然而,分层模式继续以不同的形式出现,包括将在第10章讨论的现代变体。
大泥球
架构师把缺乏清晰结构的软件架构称为“大泥球”,以Brian.Foote和Joseph.Yoder在1997年发表的一篇论文中定义的同名反模式命名:
所谓的大泥球就是一个偶然结构的,杂乱的,邋遢的,错综复杂的,拖泥带水的系统。这些系统呈现出明显的无序发展,和重复的,应急的修补痕迹。信息在系统当中不同的部分之间随意共享,往往最终变成几乎所有重要信息都成为全局共享或重复存在的地步。
系统整体的结构可能从来都没有被明确地定义过。
如果到达这种程度的话,系统可能已经被侵蚀得面目全非。只要具备一点点架构意识的程序员都会去避开这些泥潭。只有那些漠视架构,或者已经惯于日复一日地在即将溃决的堤坝上修复漏洞的人,才会满足于工作在这些系统之上。
- Brain.Foote和 Joseph.Yoder
用现代术语来说,大泥球形容那种一个简单的脚本应用程序通过事件处理直接连接数据库进行调用,缺乏真正的内部结构。许多琐碎的应用在开始的时候使用这种方式来开发,然后在持续的开发中变得笨拙。
通常情况下,架构师不惜任何代价都希望避免这种类型的架构。结构的缺失使得变更的难度逐渐增加。这种类型的架构还存在部署、可测试性、可扩展性和性能等方面的问题。
不幸的是,这种反模式架构在现实世界中普遍存在。几乎没有架构师在一开始会打算创造一个这种架构类型的系统,但是由于缺乏围绕代码质量和结构的治理,许多项目在不经意中变得一团糟。例如,尼尔曾经做过一个客户项目的结构如图 9 -1所示:
这个客户(因为大家都懂的原因就隐去客户的名字了)在几年的时间里很快地建立了一个基于Java的Web应用。通过技术可视化工具1显示出他们的架构如何耦合在一起:在圆圈边上的每一个点代表一个Java类,每一条线代表类之间的连接,线颜色越深代表连接越紧密。在这个代码库中,任何一个类的更改都很难预测对其他类所产生的连锁反应,导致变更成为一件可怕的事情。
单一架构
在软件诞生的时候,只有计算机,软件在计算机上运行。经过不同时代的硬件和软件的演进,两者从起初的单一实体形式,后来随着越来越复杂的功能需求的增长而分裂开来。例如,大型计算机一开始是单独的系统,然后逐渐将数据分离成各自的系统。类似的是,当个人电脑刚刚出现的时候,大部分的商业开发都集中在单机上。随着计算机网络的逐渐普及,分布式系统(像客户端/服务器)开始出现了。
在嵌入式系统和其他高度受限的环境中,很少有统一架构的出现。一般来说,软件系统的功能随着时间的推移而增长,需要单独来考虑操作的体系结构特性,如性能和规模。
客户端/服务器
随着时间的推移,各种力量推动架构从单一的系统中分离出来;如何做到这一点构成了许多架构风格的基础。许多架构风格就是去处理如何分离系统的各个部分。
一种基本的架构风格按技术功能把前端和后端分离出来,称为两层架构,也叫做客户端/服务器架构。根据时代和计算机处理能力的发展,这种架构形式有许多不同风格出现。
桌面 + 数据库服务器
在早期的个人计算机架构鼓励程序员在Windows用户界面中编写富桌面应用,将数据分离到独立的数据库服务器中。这种架构与单机的数据库服务器极为相似,能够通过标准的网络协议进行连接。它允许表现层逻辑集中在桌面上,而将计算量更大的操作(在容量和复杂度上)放在更加健壮的数据库服务器上。
浏览器 + Web服务器
当现代互联网开发出现后,普遍的划分变成Web浏览器连接Web服务器(以往是连接到数据库服务器)。这种职责的分离类似于桌面的变体,但使用浏览器这种更加轻量的客户端,允许防火墙内外更加广泛的分布。尽管数据库从Web服务器中分离开来,架构师仍然把这个整体看作一个两层架构,因为Web和数据库服务器还是运行在操作中心同一类型的机器上,而用户界面运行在用户浏览器上。
三层架构
三层架构在90年代末变得十分流行,这种架构提供了更多层次的分离。随着应用服务器等工具在Java和.NET中的流行,公司开始在其拓扑结构中构建更多的层:数据库层使用工业级别数据库服务器,由应用服务器管理应用层,前端使用HTML编码,并且随着功能的扩展,JavaScript也更多地被使用。
三层架构对应网络层协议,如公共对象请求代理体系结构(CORBA)和分布式组件对象模型(DCOM),这些协议有助于构建分布式架构。
就像今天的开发人员无需过多担心像TCP/IP这样的网络协议如何工作(它们本来就在工作),大多数的架构师也不必担心分布式架构这种级别的探索。在当今这个时代,一些工具以工具(像消息队列)或架构模式(像事件驱动架构,包含在第14章中)的形式提供了这些能力。
三层架构语言设计与长期影响
在Java语言设计出来的年代,三层架构风靡一时。因此,它假设将来所有的系统都是三层架构。在使用C++等现有语言时令人头疼的问题之一就是如何在系统之间以统一的方式在网络中传输对象。因此,Java的设计者决定以一种称为序列化的机制在语言的核心中构建这种能力。在Java中每一个对象都实现一个接口,要求它支持序列化。设计者认为,由于三层体系结构将会是长久不衰的架构风格,合并在语言当中将提供极大的便利。当然,架构风格不断变换,但在Java当中残存的设计让那些想要添加现代特性的语言设计者非常沮丧。为了向后兼容而必须支持序列化,但现在几乎没有人使用这种功能。
在软件和其他工程学科里,我们总是无法避免要去理解设计决策所带来的长期影响。一如既往地建议追求简单设计其实在很多方面是对未来后果的防御。
单体 vs 分布式架构
架构风格主要分为两种类型:单体架构(所有代码都在单个部署单元里)和分布式架构(多个部署单元通过远程访问协议进行连接)。没有哪一种分类方案是完美的,分布式架构类型都存在一系列在单体架构里面没有的共同挑战与问题,使得这种分类方案很好地分离了各种架构风格。在本书中,我们将详细描述以下架构风格:
单体:
- 分层架构(第10章)
- 管道架构(第11章)
- 微内核架构(第12章)
分布式
- 服务化架构(第13章)
- 事件驱动架构(第14章)
- 基于空间的架构(第15章)
- 面向服务的架构(第16章)
- 微服务架构(第17章)
分布式架构虽然在性能、可扩展性和可用性等方面比单体架构强大得多,但在获得这种能力上需要做出权衡。所有分布式架构面临的第一组问题在《分布式计算谬论》一书中有所描述,该书由L.Peter Deutsch和其他来自Sun Microsystems公司的同事于1994年首次提出。谬论是一种被认为或假设是正确,但事实并非如此的想法。分布式计算的八个谬论都适用于今天的分布式架构。下面的章节对每种谬论作出描述。
谬论#1:网络是可靠的
开发人员和架构师都认为网络是可靠的,但事实并非如此。虽然随着时间的推移,网络环境变得越来越可靠,但事实是,网络仍然普遍是不可靠的。这对于所有分布式架构来说都很重要,因为所有分布式架构都依赖于网络来进行与服务之间的通信。如图9-2所示,服务B可能是完全健康的,但服务A由于网络问题而无法对它进行访问;或者更糟的是,服务A向服务B发出了处理某些数据的请求,但由于网络问题而没有得到响应。这就是为什么服务之间需要有超时和断路器之类的处理。系统对网络的依赖程度越高(例如微服务架构),其可靠性就越低。
谬论#2:延迟为零
如图9-3所示,当通过方法或函数调用对另一个组件进行本地调用时,响应时间(t_local)为纳秒或微秒级别。但是,当通过远程访问协议(如REST、消息传递或RPC)进行相同的调用时,访问该服务(t_remote)的响应时间为毫秒级别。因此,t_remote将始终大于t_local。任何分布式体系结构中的延迟都不是零,但是大多数架构师忽略了这个谬论,坚持认为他们有高速的网络。问问自己这个问题:在你的生产环境中RESTful调用的平均往返延迟是多少吗?是60毫秒吗?还是500毫秒?
当使用分布式架构的时候,架构师必须知道平均延迟是多少。这是确定分布式体系结构是否可行的唯一方法,特别是在考虑微服务(见第17章)应付服务的细粒度特性和这些服务之间的通信量的时候。假设每个请求的平均延迟为100毫秒,将10个服务调用链接在一起以执行特定的业务功能,请求时间将增加1000毫秒!知道平均延迟很重要,但更重要的是还要知道95百分位数到99百分位数。虽然平均延迟可能只有60毫秒(这很好),但第95个百分位数可能是400毫秒!这种“长尾”延迟通常会影响分布式架构的性能。在大多数情况下,架构师可以从网络管理员那里获得延迟值(见谬论#6:只有一个管理员)。
谬论#3:带宽是无限的
通常在单体架构中不需要特别关注带宽,因为在单体中处理业务请求所需的带宽很少或根本不需要。然而,如图9-4所示,在分布式架构(如微服务)中,一旦系统被拆分成更小的部署单元(服务),这些服务之间的通信将大大依赖于带宽,导致网络速度变慢,从而影响延迟时间(谬误#2)和可靠性(谬误#1)。
为了说明这种谬论的重要性,请考虑图9-4所示的两种服务。假设左边的服务管理网站的愿望列表项,右边的服务管理客户档案信息。每当对愿望列表的请求进入左边的服务时,它必须对右边的客户档案服务进行服务间调用以获取客户名称,因为在愿望列表的响应中需要该数据,但是左边的愿望列表服务没有客户名称。客户档案服务向愿望列表服务返回45个总计500kb的属性,它只需要客户名称(200个字节)这一个属性。这是一种称为印记耦合(Stamp Coupling)的耦合形式。这听起来没什么大不了的,但对愿望列表项的请求大约每秒发生2000次。这意味着从愿望列表服务到客户档案服务的服务间调用每秒发生2000次。对于每个请求500kb,用于一个服务间呼叫的带宽量(在一秒内的数百个调用中的其中一个)是1Gb!
分布式架构中的印记耦合消耗大量带宽。如果客户档案服务只传回愿望列表服务所需的数据(在本例中为200字节),则用于传输数据的总带宽仅为400kb。印记耦合可通过以下方式解决:
- 创建专用的RESTful API端点(endpoints)
- 在契约中使用字段选择器
- 使用GraphQL来分离契约
- 使用包含消费者驱动契约(CDCs)的价值驱动契约
- 使用内部消息传输端点
不管使用何种技术,确保在分布式架构中的服务或系统之间传递的数据量最小是解决这一谬论的最佳方法。
谬论#4:网络是安全的
大多数架构师和开发人员在使用虚拟专用网络(VPNs)、可信网络和防火墙时非常自在,以至于他们往往忘记了分布式计算的谬论:网络是不安全的。在分布式体系结构中,安全性变得更具挑战性。如图9-5所示,每个分布式部署单元的每个端点都必须受到保护,以使未知或错误的请求不会到达该服务。当从单体架构向分布式架构转变时,受到威胁和攻击的表面范围会显著增加。即使在进行服务间通信时也必须确保每个端点的安全,这也是另一个原因为什么在同步、高度分布式架构(如微服务或基于服务的架构)中,性能往往会变慢。
谬论#5:网络拓扑从不改变
这个谬论是指整个网络的拓扑结构,包括整个网络中使用的所有路由器、集线器、交换机、防火墙、网络和设备。架构师假设拓扑结构是固定的并且永远不会改变。当然它会改变。它一直在变化。这个谬论的重要性是什么?
假设一个架构师在星期一早上上班,而每个人都像疯了一样到处跑,因为服务在生产环境中总是超时。架构师与团队一起疯狂地试图找出为什么会发生这种情况。在周末并没有部署新的服务。原因是什么呢?几个小时后,架构师发现一个小的网络升级发生在早上2点。这种所谓的“小”网络升级使所有的延迟假设失效,触发服务超时和断路器。
架构师必须与运维人员和网络管理员保持经常性的沟通,了解发生了什么变化以及何时发生变化,以便他们能够相应地进行调整,减少先前描述的意外事件。这看起来理所当然和很容易做到,但事实并非如此。事实上,这个谬论直接导致下一个谬论。
谬论#6:只有一个管理员
架构师总是陷入这种谬论,假设他们只需要与一个管理员协作和沟通。如图9-7所示,一个典型的大公司有几十个网络管理员。关于延迟(“谬论#2:延迟为零”)或拓扑更改(“谬论#5:网络拓扑从不改变”),架构师应该与谁交谈?这一谬论指出了分布式架构的复杂性,以及为使一切正常工作所必须进行的大量协作。由于单一部署单元特性,单体应用程序不需要这种级别的沟通和协作。
谬论#7:传输成本为零
许多软件架构师将这种谬论与延迟混淆(“谬论#2:延迟为零”)。这里的传输成本不是指延迟,而是指与进行“简单的RESTful调用”相关联的实际成本。架构师假设(错误地)必要的基础设施已经到位,足以进行简单的RESTful调用或拆分一个单体应用程序。但通常不是这样。分布式架构的成本远远高于单体架构,主要是由于对额外硬件、服务器、网关、防火墙、新子网、代理等的需求增加。无论何时开始使用分布式架构,我们都鼓励架构师分析当前服务器和网络拓扑的容量、带宽、延迟和安全区域,以免陷入这种谬论带来的意外陷阱。
谬论#8:网络是同质的
大多数架构师和开发人员假设一个网络是由一个网络硬件供应商组成的同质网络。真相永远只有一个。大多数公司的基础设施中有多家网络硬件供应商的产品。
那又怎么样?这个谬论的重要性在于,并非所有这些异构硬件供应商都能很好地合作。大部分情况都工作的很好,但Juniper的硬件与Cisco的硬件能无缝集成吗?网络标准经过多年的发展,使得这个不是什么大问题,但事实上并不是所有的状况、负载和环境都经过了充分的测试,因此,网络数据包偶尔会丢失。这反过来会影响网络的可靠性(“谬论#1:网络是可靠的”)、延迟假设和断言(“谬论#2:延迟为零”),以及对带宽的假设(“谬论3:带宽是无限的”)。换句话说,这种谬论与所有其他谬论联系在一起,在与网络打交道时形成一个无休止的混乱和挫败循环(这在使用分布式架构时是必要的)。
其他分布式架构考虑事项
除了前面描述的分布式计算的八个谬论之外,分布式架构还面临着其他问题和挑战,而这些问题和挑战在单体架构中并不存在。虽然这些其他问题的细节不在本书的讨论范围之内,我们在下面的章节中列出并对它们进行总结。
分布式日志
由于应用程序和系统日志的发散,在分布式架构中执行根本原因分析以确定某个订单被丢弃的原因非常困难和耗时。在单体应用程序中通常只有一个日志,这使得跟踪请求和确定问题更容易。然而,分布式架构系统包含几十到数百个不同的日志,它们的位置和格式都不同,因此很难对问题进行跟踪。
Splunk等日志整合工具有助于将来自不同来源和系统的信息整合到一个统一的日志和控制台中,但这些工具只触及了分布式日志复杂性的表面。分布式日志的详细解决方案和模式不在本书的讨论范围之内。
分布式事务
架构师和开发人员在一个单体架构的世界中认为事务处理是理所当然的,因为它们非常简单且易于管理。从持久性框架执行的标准提交和回滚利用ACID(原子性、一致性、隔离性、持久性)事务来保证以正确的方式更新数据,以确保高数据一致性和完整性。但分布式体系结构并不是这样。
分布式架构依赖所谓的最终一致性,以确保由独立部署单元处理的数据在某个不确定的时间点被同步到一致状态。这是分布式架构所需要作出的权衡之一:以牺牲数据一致性和数据完整性为代价的高可扩展性、性能和可用性。
事务Saga是管理分布式事务的一种方法。Sagas使用事件源进行补偿,或者使用有限状态机来管理事务状态。除了Sagas,也会使用事务BASE理论进行管理。BASE代表(B)asic availability(基本可用),(S)oft state(软状态),和 (E)ventual consistency(最终一致性)。事务BASE不是一个软件,而是一种技术。BASE中的软状态是指数据从源到目标的传输,以及数据源之间的不一致性。基于所涉及的系统或服务的基本可用性,通过使用体系结构模式和消息传递,系统最终将变得一致。
契约维护和版本控制
分布式架构中另一个特别困难的挑战是契约的创建、维护和版本控制。契约是客户端和服务双方达成一致的行为和数据规范。在分布式架构中,契约的维护尤其困难,这主要是由于不同团队和部门拥有的服务和系统相互之间是解耦的。更复杂的是版本过期处理所需的通信模型。