购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

Chapter 2
第2章
架构风格

本章介绍不同的架构风格。我们将讨论设计软件的不同方法及其优缺点,描述何时以及如何应用这些架构风格来获益。在本章中,我们将首先比较有状态架构和无状态架构。接下来,我们将介绍单体系统、各种类型的面向服务的设计,以及微服务。然后,我们将从不同的角度探讨架构风格,描述基于事件的系统、分层系统,以及模块化设计。

2.1 技术要求

你需要知道什么是软件服务,并能够读懂C++11代码。本章中的代码见GitHub页面https://github.com/PacktPublishing/Software-Architecture-with-Cpp/tree/master/Chapter02。

2.2 有状态风格和无状态风格

有状态风格和无状态风格是两种相反的软件编写风格,它们都有各自的优缺点。

顾名思义,有状态软件的行为依赖于其内部状态。我们以Web服务为例,如果服务记住了自己的状态,该服务的使用者可以在每个请求中发送更少的数据,因为该服务记住了这些请求的上下文。然而,虽然节省了发送请求大小和带宽数据的开销,但在Web服务方面有一项隐藏的成本。如果用户同时发送很多请求,则服务必须同步这些请求。由于可能会有多个请求同时改变服务状态,没有同步机制可能会导致数据争用。

但是,如果服务是无状态的,那么每个指向它的请求都需要包含成功处理它所需的所有数据。这意味着请求数据将变得更多,消耗更多的带宽,但服务将拥有更好的性能和可伸缩性。如果你熟悉函数式编程,你可能会发现无状态服务很直观。每个请求的处理都可以理解为对纯函数的调用。事实上,无状态编程的许多优点都源于它的函数式编程本质。可变状态是并发代码的大敌。函数式编程依赖于不可变值,即使这意味着需要复制而不是修改现有对象。得益于此,每个线程都可以独立工作,并且不可能产生数据争用问题。

由于没有竞争条件,因此不需要加锁,这就可能带来巨大的性能提升。没有锁也意味着不再需要处理死锁的问题。纯函数意味着代码更容易调试,因为没有任何副作用。反过来,没有副作用对编译器也有帮助,因为优化没有副作用的代码是一项更容易的任务,而且可以更激进地执行。以函数方式编写代码的另一个好处是,编写的源代码往往更简洁、更具表现力,特别是与严重依赖于GoF(Gang of Four,四人组)设计模式的代码相比。

这并不一定意味着如果没有带宽问题,就应该使用无状态风格。这些决策要从单个类或函数到整个应用程序等许多层面考虑。

以类为例,如果你正在建模Consultant(顾问)类,它理应包含诸如顾问姓名、联系人数据、小时费、当前和过去的项目等字段。自然,它是有状态的。现在,假设你需要计算他们的工作报酬。你应该创建一个PaymentCalculator类,还是应该添加一个成员函数或自由函数来进行计算?如果使用创建类的方法,应该将Consultant作为构造函数参数还是方法参数?这个类应该有津贴等属性吗?

添加成员函数来计算工作报酬将破坏单一责任原则(SRP),因为这样的类有两个职责:计算报酬和存储顾问的数据(状态)。这意味着应该优先引入自由函数或单独的类,而不是使用这样的混合类。

在这个类中,首先应该有一个状态吗?我们来讨论另一种方法,即使用PaymentCalculator类。

一种方法是把计算所需的属性设为可公共访问:

这种方法有两个缺点。第一,它不是线程安全的,PaymentCalculator类的实例不能在没有锁的多线程中使用。第二,一旦计算过程变得更加复杂,该类可能会从Consultant类中复制更多的字段。

为了消除代码重复,我们可以重新编写PaymentCalculator类来存储Consultant实例:

请注意,由于不能简单地重新绑定引用,因此我们使用指南支持库(Guideline Support Library,GSL)中的一个辅助类(not_null)将可重新绑定的指针存储在包装器(wrapper)中,它可自动确保不存储空值。

这种方法的缺点也是不具备线程安全性。那么,有没有更好的方法呢?事实上,我们可以通过类的无状态化来使类线程安全:

如果没有需要管理的状态,那无论是创建自由函数(可能在不同的命名空间中)还是类中的静态函数(像上面的代码那样)区别都不大。就类而言,区分值(实体)类型和操作类型是很有用的,因为把它们混在一起可能会违反单一责任原则(SRP)。

无状态服务和有状态服务

上面关于类的原则也可以用于更高级的概念,例如微服务。

有状态服务是什么样的?以FTP为例,如果FTP不是匿名的,则要求用户通过用户名和密码来创建会话。FTP服务器存储这些数据以识别用户是否仍然处于连接状态,因此它要一直存储这些状态。每次用户更改工作目录时,都会更新其状态。用户所做的每一个更改都会反映为状态的变化,包括断开连接。有状态服务意味着,根据不同的状态,两个外观相同的GET请求会返回不同的结果。如果服务器状态丢失了,那么请求就不会被正确处理。

有状态服务还可能存在会话不完整或事务未完成的问题,这增加了问题的复杂性。应该让会话保持多久?如何验证客户端是否已崩溃或断开连接?应该什么时候撤销所做的更改?虽然你可以想出这些问题的答案,但通常更容易的是依靠服务的消费者以一种动态的“智能”方式与服务进行沟通。因为服务的消费者会自己维护某种状态,所以让服务来同时维护状态不仅是不必要的,而且往往是一种浪费。

无状态服务(例如后面描述的REST服务)则采用相反的方式。每个请求必须包含成功处理它所需的全部数据,因此两个相同的幂等请求(如GET)将得到相同的响应结果。它假设存储在服务器上的数据不会改变,但数据与状态不一定是同一个事情。最重要的是,每个请求都是独立的。

无状态服务是现代互联网服务的基础。HTTP是无状态的,同时许多网络服务API(例如Twitter的)也是无状态的。Twitter的API依赖的REST被设计为功能无状态的(functionally stateless)。REST是REpresentational State Transfer( 表述性状态转移 )的缩写,其背后的思想是,处理请求所需的所有状态都必须随请求一起传输。如果不满足这个规则,就不能说服务是REST服务。然而,实际上,该规则也有一些例外情况。

如果你正在创建一个在线商店,你可能想要存储与客户有关的信息,比如他们的订单历史和收货地址。用户侧的客户端可能会存储身份验证cookie,而服务器可能会在数据库中存储一些用户数据。cookie让我们不需要再管理会话,就像有状态服务一样。

对于服务来说,将会话保持在服务器端是一种不好的方式,原因有几个:这增加了许多本可以避免的复杂性,使bug更难复现,最重要的是,服务无法扩展。如果想将负载转移到另一台服务器,很可能会在复制带有负载的会话以及在服务器之间同步它们时遇到困难。因此,所有的会话信息都应保存在客户端。

也就是说,如果希望使用有状态的架构,就需要有很充分的理由。以FTP为例,它必须在客户端和服务器端都记录这些更改。用户只对单个特定的服务器进行身份验证,以便执行单一状态的数据传输。将其与Dropbox这样的服务进行比较,后者的数据通常在用户之间共享,而文件访问抽象为API,你可以思考一下为什么无状态模型更适合后者这种情况。

2.3 单体风格

开发应用程序最简单的架构风格是单体风格,所以许多项目在开始的时候都使用这种风格。单体应用程序像一个大代码块,应用程序功能独立的部分,如I/O部分、数据处理部分和用户界面都是交织在一起的,而不是放在单独的架构组件中。这种架构风格的一个有名的例子是Linux内核。注意,内核是单体的并不妨碍它同时也是模块化的。

部署这样的单体应用程序可能比部署多组件的应用程序更容易,因为只需要部署一个东西。它的测试也更容易,因为端到端测试只需要启动单个组件。它的集成和扩展也都更容易,只需在负载均衡器后面添加更多的实例就行了。

有了上述这些优点,为什么会有人排斥这种架构风格呢?实际上,尽管有这些优点,但这种架构也有许多缺点。

提供的可伸缩性在理论上听起来不错,但是如果应用程序的模块有不同的资源需求呢?只对应用程序中的一个模块进行扩展该怎么做呢?缺乏模块化是单体系统的一个固有特性,这是该架构许多缺陷的根源。

更重要的是,开发单体应用程序的时间越长,在维护时遇到的问题就越多。保持这样一个应用程序的内部松耦合是一个挑战,因为在其模块之间添加额外的依赖关系很容易。随着应用程序的增长,理解它会变得越来越难,因此,由于增加的复杂性,开发过程很可能会随着时间的推移而减慢。在开发单体应用程序时,也可能很难维护 领域驱动开发 (DDD)的有界上下文。

大的应用程序在部署和执行方面也有缺点。启动这样的应用程序需要的时间比启动更多、更小的服务要长得多。无论你在应用程序中改了什么,你可能都不希望它会迫使你立即重新部署整个应用程序。现在,假设某个开发人员在应用程序中导致了一个资源泄漏。如果泄漏资源的代码被反复执行,它不仅会破坏应用程序的某项功能,还可能会破坏应用程序的其他功能。

如果你喜欢在项目中使用前沿技术,那么单体风格不会带来任何好消息。由于你现在需要一次迁移整个应用程序,因此很难更新任何库或框架。

前面的解释表明,单体架构只适用于简单的小型应用程序。然而,还有一种情况,使用单体架构可能是一个好主意。如果你特别在意性能,与微服务相比,单体服务有时可以在延迟或吞吐量方面帮你挤出更多水分。进程间的通信总是会产生一些开销,而单体应用程序不会产生这些开销。如果你对测量方法感兴趣,请参阅本章“进一步阅读”部分中列出的论文。

2.4 服务和微服务

由于单体架构存在这些缺点,人们提出了其他的架构方式。一个常见的想法是将解决方案分成多个相互通信的服务,并将开发工作分配给不同的团队,每个团队负责一个服务。每个团队的工作边界都是清晰的,而不是像单体架构那样耦合在一起。

面向服务的架构 (Service-Oriented Architecture,SOA)意味着业务功能被模块化,并作为单独的服务供客户使用。每个服务都应该有一个能够自我描述的接口,并隐藏实现细节,例如内部架构、技术或所使用的编程语言。这允许多个团队根据自己的喜好开发服务,这意味着在服务的底层,每个团队都可以使用适合自己的技术。假设你有两个开发团队(一个精通C#,另一个精通C++),那么他们可以开发两个彼此通信的服务(一个用C#实现,一个用C++实现)。

SOA的支持者提出了一份宣言,该宣言主要包括以下几点:

❑业务价值高于技术战略。

❑战略目标高于特定项目的收益。

❑内在互操作性高于定制的集成。

❑共享的服务高于特定目标的实现。

❑灵活性高于优化。

❑不断演进的提炼高于在最开始追求完美。

尽管此宣言没有绑定特定的技术栈、实现方式或服务类型,但最常见的两种服务类型是SOAP和REST。除此之外,最近还有第三个类型越来越受欢迎:基于gRPC的服务。

微服务

顾名思义,在微服务这种软件开发模式中,应用程序被分割为一组松耦合的服务,这些服务使用轻量级协议进行通信。微服务模式类似于UNIX的理念,即一个程序应该只有一个目的。根据UNIX的理念,应将这些程序组合到UNIX的管道(pipeline)中来解决复杂的问题。类似地,基于微服务的系统也由许多微服务和支持服务组成。

我们先看一下这种架构风格的优缺点。

1. 微服务的优缺点

微服务架构中服务规模小意味着它们的开发、部署速度更快,也更容易理解。由于这些服务是相互独立构建的,因此编译其新版本所需的时间可以大大减少。因此,在处理这种架构风格时,可以更容易地使用快速的原型设计和开发。这反过来又缩短了开发周期(lead-time),从而可以更快地对业务需求进行评估。

微服务架构的其他优点有:

❑模块化,这是这种架构风格固有的特点。

❑易测试。

❑替换系统模块(如单个服务、数据库、消息代理或云厂商)时更灵活。

❑可与旧系统集成:不需要迁移整个应用程序,只需要迁移当前开发的部分。

❑支持分布式开发:开发团队可以并行地处理多个微服务。

❑可伸缩性:一个微服务可以独立于其他微服务进行扩展。

另外,微服务也有一些缺点:

❑需要成熟的DevOps方法并依赖于CI/CD自动化。

❑更难调试,并且需要更好的监控和分布式追踪机制。

❑对较小的应用程序来说,额外的开销(需要用辅助服务的话)可能会超过带来的好处。

现在,我们来讨论一下这种架构风格的微服务的特点。

2. 微服务的特点

由于微服务架构相对比较新,所以对微服务没有统一的定义。根据Martin Fowler的说法,微服务有如下几个基本特点:

❑每个服务都应该是一个可更换、可升级的独立组件。这与更容易的部署和服务之间的松耦合有关,与之相反的是,在单体应用程序中组件是以库的形式存在的。在后一种情况下,当替换某个库时,通常必须重新部署整个应用程序。

❑每个服务都应该由一个跨职能的团队来开发,并专注于特定的业务能力。听说过康威(Conway)定律吗?

“任何组织设计的系统(广义的),其系统结构都是该组织通信结构的副本。”

——Melvyn Conway,1967

如果没有跨职能的团队,最终会得到一个个软件筒仓(software silo)。团队间没有沟通会减少很多麻烦,最终成功地交付。

❑每个服务都应该是一个产品,它的整个生命周期由开发团队负责。这与项目思维形成对比,在项目思维中,所开发的软件将交由别人维护。

❑服务应该有智能终端并使用仅作转存的管道,而不是相反(即管道不必智能)。这与传统服务相反,传统服务通常依赖于企业服务总线(Enterprise Service Bus,ESB)的逻辑,后者通常管理消息的路由并根据业务规则对消息进行转换。在微服务中,可以通过在服务中存储逻辑来提高内聚性,避免与消息组件耦合。使用消息队列,如ZeroMQ,也有助于实现这个目标。

❑服务应该以一种去中心化的方式进行管理。单体应用程序通常使用特定的技术栈来编写。当单体应用程序被拆分成微服务时,每个微服务都可以选择适合自己特定需求的技术栈。管理和确保每个微服务全天候(24小时×7)运行的是负责这个特定服务的团队,而不是中央部门。亚马逊、Netflix和Facebook等公司都采用这种方法,它们发现,让开发者确保自己负责的服务稳定有利于确保整体系统的高质量。

❑服务应该以一种去中心化的方式来管理它们的数据。每个微服务都可以选择适合其需求的数据库,而不是只有一个数据库。拥有去中心化的数据可能会给数据更新带来一些挑战,但具有更好的扩展性。这就是微服务经常以无事务方式协作并提供最终一致性的原因。

❑服务所使用的基础设施应该被自动管理。要高效地处理数十个微服务,需要有持续的集成和持续的交付(Continuous Intergration and Continuous Delivery,CI/CD),否则,部署这些服务将是地狱级难度。自动运行所有的测试将为你节省大量的时间和精力。在此基础上实施持续部署将缩短反馈循环,使客户能更快地使用新功能。

❑微服务应为其所依赖的其他服务的失败做好准备。在具有如此多可插拔组件的分布式部署环境中,一些组件偶尔出现故障是正常的。你的服务应该能够优雅地处理此类故障。熔断器或舱壁等模式(见4.3.4节)可以帮助你实现这一点。为了使架构具有弹性,还必须能够有效地恢复失败的服务,甚至提前预判这种崩溃。实时监控延迟、吞吐量和资源使用情况对此至关重要。了解Netflix的Simian Army工具包对于创建弹性架构是非常有用的。

❑微服务架构应该为不断演进做好准备。在设计微服务和它们之间的协作时,应考虑好如何方便地替换单个微服务,甚至一组微服务。正确地设计服务是很棘手的,尤其是把曾经是一整个大模块的复杂代码变成微服务之间复杂的通信方案,这很难管理——所谓的“意大利面条式”整合。这意味着与传统服务或单体架构相比,架构师的经验和技术栈扮演着更重要的角色。

除此之外,很多(但不是所有)微服务共有的其他特点有:

❑使用独立的进程,进程间通过网络通信。

❑使用与技术无关的协议(如HTTP和JSON)。

❑保持服务的小规模和较低的运行时开销。

现在,你应该很好地了解了基于微服务的系统的特点,所以我们来对比一下这种架构与其他架构。

3. 微服务和其他架构风格

微服务可以单独用作一种架构模式。然而,它们经常与其他架构相结合,如云原生计算(cloud-native computing)、无服务器应用(serverless application),以及大多数轻量级应用容器(lightweight application container)。

面向服务的架构带来了低耦合和高内聚。如果使用得当,微服务也可以做到。然而,它可能具有一定的挑战性,因为它需要良好的直觉来将系统划分为大量的微服务。

微服务和上述其他架构有很多相似之处,因为它们都可以使用基于SOAP、REST或gRPC的消息传递机制,并且可以使用消息队列等技术来实现事件驱动。

微服务也有一些成熟的模式来帮助实现所需的质量属性,例如容错(例如通过隔离故障组件实现),但是为了拥有高效的架构,你还必须决定如何实现API网关、服务注册、负载均衡、容错、监控、配置管理,以及使用的技术栈。

4. 微服务扩展

微服务的扩展与单体应用程序不同。在单体应用程序中,整个功能由单个进程处理。扩展应用程序意味着要在不同的机器上复制此进程。这种扩展没有考虑到哪些功能会被大量使用,以及哪些功能不需要额外的资源。

对于微服务,每个功能都作为一个单独的服务来处理,使用一个单独的进程。扩展基于微服务的应用程序时,只有需要更多资源的部分才被复制到不同的机器上。采用这种方法,可以更好地利用资源。

5. 如何过渡到微服务

大多数公司都存在一些单体程序,虽然不想立即使用微服务进行重写,但仍希望过渡到微服务架构。在这种情况下,通过添加越来越多的与单体程序交互的服务,可以逐步往微服务迁移。可以创建新的功能作为微服务,也可以删除单体程序中的某些部分,将其转为微服务。

更多关于微服务的细节,包括如何从零开始构建微服务,可参考第13章。

2.5 基于事件的架构

基于事件的系统是指架构围绕着处理事件来设计的系统。这个系统包含生成事件的组件、事件传播的通道,以及对这些事件做出反应的侦听器,侦听器也可能触发新的事件。这种架构提高了异步性,降低了耦合度,提高了性能和可伸缩性,同时也易于部署。

虽然有这些优势,但还有一些挑战需要解决,其中之一便是如何应对这类系统的创建复杂性。所有队列都必须进行容错,以便在处理期间不丢失任何事件。以分布式方式处理事务也是一个挑战。使用关联ID模式来跟踪进程之间的事件,搭配监控技术,可以节省调试的时间,省去大量烦恼。

基于事件的系统包括流处理器和数据集成系统,以及旨在实现低延迟或高可伸缩性的系统。

现在,我们来讨论这类系统中使用的常见拓扑结构。

2.5.1 基于事件的常见拓扑结构

事件驱动架构的两种主要拓扑结构分别是基于代理(broker)的和基于中介(mediator)的。这两种拓扑结构的区别是事件在系统中的流动方式不同。

当一个事件需要多个任务或步骤,而这些任务或步骤都可以独立执行时,适合使用中介拓扑结构(见图2.1)。产生的所有事件一开始都位于中介的事件队列中。中介知道处理事件需要做什么,但并不执行,而是通过每个事件处理器的事件通道将事件分派到适当的处理器。

图2.1 中介拓扑结构

如果这让你想起业务流程,那么说明你有很好的直觉。你可以在 业务流程管理 (Business Process Management,BPM)或 业务流程执行语言 (Business Process Execution Language,BPEL)中实现此拓扑结构。你也可以使用诸如Apache Camel、Mule ESB等技术来实现它。

代理是一个轻量级的组件,它包含所有的队列,但不对事件的处理进行编排。它可以要求事件接收方订阅特定类型的事件,然后简单地转发订阅事件。许多消息队列都依赖于代理,例如ZeroMQ,它是用C++编写的,目标是实现零浪费和低延迟。代理拓扑结构如图2.2所示。

图2.2 代理拓扑结构

既然了解了基于事件的系统中使用的两种常见拓扑结构,现在我们来了解一个以事件为核心的强大的架构模式。

2.5.2 事件溯源

你可以将事件看作通知,这个通知包含接收事件的服务要处理的附加数据。然而,还有另一种看待事件的方式:状态的改变。如果能够知道错误发生时的状态以及请求的更改,那么调试应用程序的逻辑问题会很容易。这是事件溯源(event sourcing)的好处之一。本质上,它通过简单地记录系统中发生的所有事件来记录系统产生的所有变化。

通常,你会发现服务不需要在数据库中持久化其状态了,因为将事件存储在系统的其他地方就足够了。即使要持久化服务状态,也可以异步完成。事件溯源的另一个好处是可以免费获得完整的审计日志。事件溯源架构如图2.3所示。

图2.3 事件溯源架构(提供应用程序状态的统一视图,它可用于创建定期快照以加速系统恢复)

由于数据同步需求的减少,事件溯源系统通常提供较低的延迟,这使得它们非常适合交易系统和活动跟踪器等。

现在,我们来学习另一种流行的架构风格。

2.6 分层架构

如果想避免架构看起来像意大利面,那么将组件结构进行分层可能会有所帮助。还记得模型-视图-控制器(Model-View-Controller,MVC),或者类似的模式,比如模型-视图-视图模型(Model-View-View Model,MVVM)或实体-控制-边界(Entity-Control-Boundary,ECB)吗?这些都是分层架构的典型例子(如果各层在物理上彼此分开,则也称为N-tier架构)。你可以将代码按层结构化,可以创建多层微服务,或者将此模式应用于你认为有价值的其他领域。分层可以使抽象和关注点分离,这也是引入它的主要原因。分层还可以降低复杂性,同时改善解决方案的模块化、可重用性和可维护性。

一个现实世界的例子是自动驾驶汽车,分层后可以按层决策:最低层将处理汽车的传感器数据,上一层将消耗传感器数据以便实现单一特性,再上一层将利用所有特性实现安全驾驶。当另一种车型更换传感器时,只需要更换最低层即可。

分层架构通常非常容易实现,因为大多数开发人员已经知道层的概念——他们只需要开发几个层并将它们像图2.4那样堆在一起即可。

图2.4 在表示层中使用文本界面的3-tier架构示例

创建高效的分层架构的挑战在于如何在各层之间设计稳定且定义良好的接口。通常,可以在一层上架设多层。例如,如果有一个处理领域逻辑的层,那么它可以是表示层和向其他服务提供API服务的层的基础。

分层并不总是一件好事。对于微服务,有两种主要的分层场景。第一个场景是,想要将一组服务与另一组服务分开。例如,你可以用一个快速变化的层来与业务伙伴交互,它包括频繁变化的内容,并设计另一个面向业务功能的层。后者并没有以非常快的速度发生变化,使用的技术也比较稳定。把这两者分开是有道理的。还有一个理念是,不太稳定的组件应该依赖于更稳定的组件,所以很容易想到,这里可以设计两层,让客户业务层依赖于业务功能层。

另一个场景是创建层来反映组织的通信结构(还是康威定律)。这可能会减少团队之间的沟通,从而导致创新的减少,因为团队无法很好地了解彼此的内部信息或想法。

现在,我们来讨论另一个经常用于微服务的分层架构——服务于前端的后端。

服务于前端的后端

许多前端依赖于同一个后端的情况很常见。假设你有一个移动应用程序和一个Web应用程序,它们都使用相同的后端。从一开始来看,这可能是一个不错的设计。但是,一旦这两个应用程序的需求和使用场景发生变化,后端将需要进行越来越多的更改,最终只能服务于其中一个前端。这可能导致后端不得不支持相互冲突的需求,比如支持更新数据存储的两种独立方法或提供数据的不同方式。与此同时,前端开始需要更多的带宽来与后端正常通信,从而导致移动应用程序耗电更多。此时,你应该考虑为每个前端引入一个单独的后端。

这样,你可以将面向用户的应用程序看作一个具有两层(前端和后端)的单一实体。后端可以依赖于另一层,该层用来提供下游服务,如图2.5所示。

图2.5 服务于前端的后端模式

服务于前端的后端(Backends For Frontends,BFF)模式的缺点是必须复制某些代码。不过,只要能加速开发,从长远来看不是负担就没关系。但这也意味着你应该关注在下游服务中聚合重复逻辑的可能性。有时,引入一个服务来聚合类似的调用可以帮助解决重复代码的问题。通常,如果有许多前端,有些前端仍然可以共享一个后端,只要没有相互冲突的需求就行。例如,如果你正在为iOS和Android创建移动应用程序,那么可以考虑用相同的后端,并为Web应用程序和桌面应用程序提供单独的后端。

2.7 基于模块的架构

在本节中,我们所说的模块是指可以在运行时加载和卸载的软件组件。关于C++20的模块,请参阅第5章。

如果需要以尽可能少的停机时间运行一个组件,但出于某些原因不能应用通常的容错模式,例如服务的备份,那么使用基于模块的组件可以节省时间。你也可能只是被模块化系统的愿景吸引,愿景包括对所有模块的版本管理、轻松查找所有可用服务,以及基于模块的系统可能带来的解耦、可测试性和更好的团队合作。因此,Java社区发起的 开放服务网关倡议 (Open Service Gateway initiative,OSGi)模块,通过多个框架移植到了C++中。使用基于模块的架构示例包括一些IDE(如Eclipse)、 软件定义网络 (Software Defined Networking,SDN)项目(如OpenDaylight)或家庭自动化软件(如OpenHAB)。

OSGi还允许自动管理模块之间的依赖关系,控制它们的初始化和卸载,以及控制它们的发现。由于OSGi是面向服务的,因此你可以认为OSGi服务是类似于“容器”中的微服务。这就是其中一个C++实现被命名为C++微服务(C++ Micro Service)的原因。要了解更多实际情况,请参阅本章“进一步阅读”部分。

C++微服务框架采用的一个有趣的概念是处理单例(singleton)的新方法。GetInstance()静态函数将返回一个从绑定的上下文获得的服务引用,而不是仅传递一个静态实例对象。因此,可配置的服务有效地取代了单例对象。它还可以使你避免静态去初始化(static deinitialization)的失败,在静态去初始化中多个相互依赖的单例必须以特定的顺序卸载。

2.8 总结

在本章中,我们讨论了实际中会遇到并应用的各种架构风格,包括单体架构、面向服务的架构、微服务,并讨论了它们提供外部接口和交互的各种方式。我们还讲解了如何编写REST服务,以及如何创建具有弹性且易于维护的微服务架构。

我们展示了如何创建客户端来使用服务。接着,我们讨论了其他各种架构方法:事件驱动的方法、基于运行时模块的方法。我们还展示了在什么情况下可以使用分层架构。我们探讨了如何实现事件溯源,以及何时使用BFF。此外,我们也解释了架构风格如何帮助你实现一些质量属性,以及将有哪些挑战。

在第3章中,我们将学习如何判断在给定的系统中哪些属性是重要的。

问题

1. REST服务的特点是什么?

2. 可以使用什么工具包来创建具有弹性的分布式架构?

3. 应该为微服务使用集中式存储吗?为什么?

4. 什么时候应该编写有状态的服务,而不是无状态的服务?

5. 代理和中介有何不同?

6. N-tier架构和N-layer架构之间的区别是什么?

7. 如何用基于微服务的架构来取代单体架构? dznEbZRPTbX5MSaqIoGcv+vXKzzc9GPmXvnfeoDBsrcP/DN0BWYLME/2EU+QciAS

进一步阅读

点击中间区域
呼出菜单
上一章
目录
下一章
×