Skip to content

samz406/fundamentals-of-software-architecture

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 

Repository files navigation

fundamentals-of-software-architecture 学习笔记

目录

  • 基础
  1. 第一章
  2. 第二章 架构思维
  3. 第三章 模块化
  4. 第四章 架构特性定义
  5. 第五章 识别架构特性
  6. 第六章 度量和管理架构特性
  7. 第七章 架构特性范围
  8. 第八章 基于组件的思维
  9. 第九章 基础
  • 架构风格
  1. 第10章 分层架构样式
  2. 第11章 管道架构
  3. 第12章 管道架构
  4. 第13章 管道架构
  5. 第14章 管道架构
  6. 第15章 空间架
  7. 第16章 基于编排的面向服务架构
  8. 第17章 微服务架构
  9. 第18章 选择合适的架构风格
  10. 第19章 架构决策
  11. 第20章:架构风险分析

第一章

image 架构由结构、架构特征、设计原则组成
1. 软件架构定义

结构:系统的结构包括架构风格(如微服务、分层架构等)​​。

架构特性:架构特性是系统必须支持的成功标准,通常与系统功能无关,但对系统正常运行至关重要​​。

架构决策:这些是系统应如何构建的规则。例如,架构师可能会决定在分层架构中,只有业务层和服务层可以访问数据库​​。

设计原则:设计原则是构建系统的指南,而不是硬性规定。例如,在微服务架构中使用异步消息传递以提高性能​​。

2. 架构师的期望

架构师需要具备以下核心能力:

做出架构决策:引导而不是指定技术选择​​。

持续分析架构:确保架构在业务和技术变化中保持活力​​。

紧跟最新趋势:保持对最新技术和行业趋势的了解​​。

确保决策的合规性:确保开发团队遵循架构决策和设计原则​​。

广泛的技术和经验:至少熟悉多种技术​​。

商业领域知识:了解业务领域,以便更有效地设计架构​​。

人际交往能力:具备团队合作、协调和领导能力​​。

理解并驾驭公司政治:能够在企业的政治环境中有效地工作​​。

3. 架构与其他领域的交集

工程实践:包括持续集成、自动化机器配置等​​。

运维/DevOps:架构与运维的紧密结合使得设计更加简化​​。

开发过程:软件开发过程(如敏捷开发)对架构有重要影响​​。

数据:数据架构是软件架构的重要组成部分,涉及如何存储和管理数据​​。

4. 软件架构法则

第一法则:软件架构中的一切都是权衡取舍​​。

第二法则:理解为什么比理解如何更重要​​。

第二章 架构思维

1. 架构与设计的区别
image

定义差异:架构和设计的区别常常引起混淆。架构涉及高层次的系统结构和全局决策,而设计关注实现这些结构的细节 。

传统职责分工:架构师负责分析业务需求、选择架构模式和创建系统组件,开发团队则负责具体的类图、用户界面和代码开发 。

协作需求:为了成功,架构师和开发团队必须紧密合作,确保架构设计和实现保持同步 。

2. 技术广度
image
  • 知识金字塔

技术知识的层次:技术知识可以分为三个层次:已知的知识(即技术深度)、已知的未知(知道自己不知道的知识)和未知的未知(不知道自己不知道的知识)。这三个层次共同构成了知识金字塔 。

已知的知识:指的是一个技术人员在日常工作中使用的技术、框架、语言和工具。例如,一个Java程序员对Java语言的掌握。

已知的未知:指的是技术人员听说过但不熟悉的技术。例如,许多技术人员知道Clojure是一种基于Lisp的编程语言,但可能并不精通。

未知的未知:指的是那些技术人员不知道它们存在,但实际上可能是解决当前问题的最佳方案的技术和工具。

  • 技术人员的知识发展

开发人员的早期职业:开发人员在职业生涯的早期通常专注于扩展知识金字塔的顶端,即增加技术深度。这是因为开发人员需要更多的实践经验和操作知识来完成他们的工作 。 技术深度的维护:随着开发人员获得更多的经验,他们需要不断维护和更新他们的技术知识,因为技术在不断发展。例如,一个开发人员如果多年不使用某种技术,那么他们对该技术的掌握程度会逐渐减弱 。

  • 架构师的技术广度

技术广度的重要性:架构师需要比开发人员拥有更广泛的技术知识,以便在不同的技术约束和业务需求下做出最佳决策。相比于单一的技术专长,广泛的技术知识更有助于架构师识别和应用最适合的技术解决方案 。

广度优先于深度:架构师应更加注重技术广度而不是深度。这意味着他们需要对多种技术、框架和工具有所了解,而不必精通每一种技术 。

技术广度的培养:架构师可以通过参与不同类型的项目、持续学习新技术和与其他技术专家交流来扩展他们的技术广度 。

  • 角色转换中的挑战

从开发人员到架构师的过渡:开发人员在转变为架构师时,通常会面临一个从深度到广度的知识转换过程。这可能是一个具有挑战性的过程,因为这需要他们改变长期以来形成的专注于技术深度的思维模式 。 避免技术过时:架构师需要警惕其技术知识的过时性,即避免陷入“冰冻穴居人反模式”,即架构师依赖于过去的技术知识而忽略了新的技术发展 。

  • 实践中的技术广度

技术广度的实际应用:在实际工作中,架构师需要广泛的技术知识来解决复杂的技术问题。例如,在选择缓存解决方案时,了解多种缓存产品及其优缺点会比只精通一种产品更有价值 。

3. 权衡分析

不可避免的权衡:架构决策总是伴随着权衡,无法通过简单的搜索得到答案。每个架构问题的答案都依赖于具体的环境和需求 。 实例分析:例如,在竞拍系统中,选择使用队列还是主题进行消息传递,架构师需要根据具体情况分析两者的优缺点 。

4. 理解业务驱动因素

业务需求转换:架构师需要将业务需求转化为架构特性(如可扩展性、性能和可用性),这需要对业务领域的深入理解和与业务利益相关者的紧密合作 。

5. 平衡架构与实践编码

保持技术深度:架构师应保持一定的编码实践,以避免成为团队的瓶颈。通过参与实际项目中的代码开发,可以更好地理解团队面临的挑战 。 避免瓶颈陷阱:架构师不应过多参与关键路径的编码工作,而是应将这些任务分配给开发团队,以防止自己成为项目的瓶颈 。

ps: 没有完美架构,都是trade off

第三章 模块化

定义

模块化在软件架构中是一个核心概念,但其定义常常混乱且不统一。一般来说,模块化是指将相关代码逻辑分组的方法,这可以是面向对象语言中的类组,或结构化或函数式语言中的函数组​​。大多数编程语言提供了某种模块化机制,如Java中的包(package)和.NET中的命名空间(namespace)。

模块化的意义

模块化是组织代码的一种原则,目的是通过逻辑分组来管理代码的复杂性。如果架构师在设计系统时不关注各个部分的相互连接方式,就会导致系统难以维护和扩展​​。模块化有助于保持系统结构的有序性和一致性,即使在项目需求中没有明确要求,也需要架构师主动确保模块间的良好区分和沟通。

模块化复用

在面向对象编程之前,开发者使用模块化语言来组织代码,例如Modula和Ada。这些语言提供了类似于今天的包或命名空间的编程结构,用于逻辑分组相关代码​​。尽管面向对象语言引入了类和继承等新概念,模块化的思想仍然保留了下来。

度量模块化

架构师需要工具来理解和度量模块化。关键概念包括内聚性、耦合性和共生性​​。

内聚性

内聚性衡量模块内各部分的相关程度。高内聚性意味着模块的各部分紧密相关,所有部分应包含在同一模块内。例如,功能内聚的模块中,每个部分都与其他部分相关,并且模块包含完成其功能所需的一切​​。

内聚性是指模块内部各部分之间的紧密程度,它衡量的是模块内的元素在一起工作的合理性和相关性。高内聚性意味着模块内的各个元素密切相关,共同实现一个明确的功能或目标。内聚性是评估模块化设计质量的重要标准之一,因为高内聚性通常表示模块更容易理解、维护和复用。

内聚性的类型

内聚性从高到低可以分为以下几种类型:

  • 功能内聚(Functional Cohesion)

定义:模块内的所有元素共同完成单一功能。

示例:一个模块负责处理用户登录验证,包括接收用户输入、验证用户名和密码、返回验证结果。

特点:这种内聚性最高,模块内的所有元素都与完成该功能密切相关,任何对功能的更改都在模块内完成。

  • 顺序内聚(Sequential Cohesion)

定义:模块内的元素按顺序执行,每个元素的输出作为下一个元素的输入。

示例:一个模块首先读取文件数据,然后处理数据,最后将结果写入数据库。

特点:元素之间存在顺序依赖关系,功能较为集中,但各部分之间的联系较强。

  • 通信内聚(Communicational Cohesion)

定义:模块内的元素操作同一数据集合。

示例:一个模块负责从数据库中读取数据、处理数据并生成报告。

特点:元素通过共享数据联系在一起,数据变化可能会影响多个元素。

  • 过程内聚(Procedural Cohesion)

定义:模块内的元素必须按特定顺序执行,但不一定操作相同的数据。

示例:一个模块包括一系列用户界面操作步骤,如显示登录页面、接收输入、验证用户。

特点:步骤之间有顺序依赖,但数据和功能之间的关联性较低。

  • 时间内聚(Temporal Cohesion)

定义:模块内的元素在同一时间段执行。

示例:一个初始化模块负责在系统启动时执行多个初始化任务,如加载配置文件、建立数据库连接。

特点:元素仅在时间上相关,功能上可能毫无关联。

  • 逻辑内聚(Logical Cohesion)

定义:模块内的元素执行逻辑上相似的任务,但实际上可以独立完成。

示例:一个输入输出模块包含读取文件、读取数据库、读取网络数据等功能。

特点:元素之间逻辑相关,但彼此独立,内聚性较低。

  • 偶然内聚(Coincidental Cohesion)

定义:模块内的元素没有任何明显的关系,只是偶然被放在一起。

示例:一个模块包含各种杂乱无章的功能,如日志记录、数据转换、用户通知等。

特点:这种内聚性最低,模块内的元素彼此独立,维护困难,修改时容易引入错误。

内聚性的影响

可维护性:高内聚性的模块更容易理解和修改,因为所有相关功能都集中在一起。维护高内聚性模块时,开发者可以集中关注该模块的特定功能,而不必担心其他不相关的部分。 复用性:高内聚性的模块更容易复用,因为其功能明确且独立。一个功能内聚的模块可以在不同项目或系统中被直接使用,而无需做大量修改。 测试性:高内聚性的模块更容易测试,因为其功能集中,测试覆盖率更高。测试一个高内聚性的模块通常只需要考虑模块内部的逻辑,而不必处理大量的外部依赖。 灵活性:高内聚性的模块在系统中更灵活,可以更容易地进行重构和扩展。因为其内部元素紧密相关,更改或扩展一个功能通常只影响该模块内部,而不会波及到其他模块。 实现高内聚性的方法 单一职责原则(Single Responsibility Principle, SRP)

定义:一个模块只负责一个功能或职责。 应用:确保每个模块只包含实现该功能所需的代码,避免将无关的功能混杂在一起。 模块化设计

定义:将系统划分为多个独立的模块,每个模块都有明确的职责和功能。 应用:根据功能将代码分组,确保每个模块内部的功能紧密相关,模块之间的依赖最小化。 重构

定义:通过调整代码结构来提高内聚性和降低耦合性。

耦合性

耦合性衡量模块之间的相互依赖程度。较低的耦合性意味着模块间依赖较少,模块可以更独立地变化。Edward Yourdon和Larry Constantine在1979年定义了两个关键耦合性指标:传入耦合(afferent coupling)和传出耦合(efferent coupling)​​。

抽象度、稳定性和主序列距离(Abstractness, Instability, and Distance from the Main Sequence) Robert Martin定义了一些派生度量来深入评估模块化,其中包括抽象度、稳定性和主序列距离​​。

抽象度(Abstractness):抽象工件(如抽象类和接口)与具体工件(如实现类)的比率。 稳定性(Instability):传出耦合与传入和传出耦合之和的比率。 公式如下:

在这些公式中,ma代表抽象元素,mc代表具体元素,Ce代表传出耦合,Ca代表传入耦合。主序列距离(Distance from the Main Sequence)衡量抽象度与稳定性之间的理想关系,类的位置越接近理想线,类的平衡性越好​​。

共生性

Meilir Page-Jones在1996年提出了共生性的概念,用于描述软件系统中两个组件之间的依赖关系。当一个组件的变化需要另一个组件也做出相应变化以保持系统的正确性时,这两个组件被称为共生性​​

共生性分为两种类型:

静态共生性:

是指源代码级的耦合,它是对Structured Design中定义的传入耦合和传出耦合的细化,架构师将以下静态共生性的类型视作事物之间相互耦合的程度,无论它们是传入耦合还是传出耦合:包含如下类型:

  • 名称共生性

定义:多个组件必须使用相同的名称来引用某个实体。 示例:方法名是代码库中最常见的耦合方式,尤其是在现代重构工具可以轻松地进行系统范围内的名称更改时​​。

  • 类型共生性

定义:多个组件必须同意实体的类型。 示例:这类共生性在许多静态类型语言中很常见,通过类型检查来限制变量和参数的类型​​。 意义共生性

定义:多个组件必须同意特定值的意义。 示例:代码库中硬编码的数值是最常见的例子,如定义 int TRUE = 1; int FALSE = 0;​​。 位置共生性

定义:多个组件必须同意值的顺序。 示例:方法和函数调用中的参数值顺序问题​​。

算法共生性

定义:多个组件必须同意使用特定的算法。 示例:安全哈希算法必须在服务器和客户端上生成相同的结果以进行用户验证​​。

动态共生性: 分析运行时调用

  • 执行顺序共生性

定义:多个组件的执行顺序是重要的。

示例:电子邮件发送顺序:必须先设置收件人和发件人,然后才能发送邮件​​。

  • 时间共生性

定义:多个组件的执行时间是重要的。

示例:两个线程同时执行可能会导致竞态条件,从而影响联合操作的结果​​。

  • 值共生性

定义:多个值之间存在依赖关系,必须一起更改。

示例:分布式系统中的事务处理,所有值必须一起更改,否则不能更改​​。

  • 身份共生性

定义:多个组件必须引用相同的实体。

示例:独立组件共享和更新公共数据结构,如分布式队列​​

共生性的属性

强度(Strength):强度决定了重构的难易程度。架构师应优先考虑强度较低的共生性,因为它们更容易重构​​。 局部性(Locality):共生性的局部性衡量模块之间的接近程度。同一模块内的强共生性比跨模块的强共生性对代码库的影响更小​​。 程度(Degree):共生性的程度与其影响范围有关。影响范围较小的共生性对代码库的破坏较小​​。 Jim Weirich提出了两条关于共生性的重要建议:

程度规则:将强共生性转换为弱共生性。

局部性规则:随着软件元素之间距离的增加,使用更弱的共生性形式​​。

Page-Jones提供了三种通过共生性来提升系统模块化的准则:

1.将系统分解为封装元素来最小化整体共生性

2.最小化跨越封装边界的任何剩余共生性

3.最大化封装边界内的共生性

统一耦合和共生性指标
image
从模块到组件

架构师通常从组件的角度来思考系统。组件是模块的物理实现形式,开发者可以以不同的方式打包组件,具体取决于其开发平台。例如,Java中的jar文件、.NET中的dll文件等​​。

组件构成了架构中的基本模块化构建块,是架构师必须关注的关键内容之一。架构师的主要任务之一是确定组件的顶级划分​​。

第四章 架构特性定义

架构特性的定义标准

一个架构特性需要满足以下三个标准:

非领域设计考虑因素:架构特性描述了如何实现系统需求及其原因,而不仅仅是系统应做什么。例如,性能通常不会出现在需求文档中,但它是一个重要的架构特性 。

影响设计的某些结构方面:架构特性往往需要特定的结构考虑。例如,安全性是大多数项目的关注点,如果系统需要处理支付处理,架构可能需要设计一个特定的模块来处理这些敏感信息 。

对应用成功至关重要:支持每个架构特性会增加设计的复杂性,因此架构师需要选择最少的架构特性,而不是尽可能多 。

架构特性的分类

操作性架构特性

操作性架构特性涵盖了系统在运行时必须具备的能力,如性能、可扩展性、弹性、可用性和可靠性。以下是一些常见的操作性架构特性 :

可用性:系统需要多长时间可用。

连续性:灾难恢复能力。

性能:包括压力测试、峰值分析、功能使用频率分析、所需容量和响应时间。

可恢复性:灾难发生后系统需要多快恢复在线。

可靠性/安全性(Reliability/Safety):评估系统是否需要故障安全或任务关键。

鲁棒性(Robustness):在运行过程中处理错误和边界条件的能力。

可扩展性(Scalability):系统在用户数量或请求增加时的性能和操作能力。

结构性架构特性

结构性架构特性关注代码结构和质量,如良好的模块化、受控的组件之间的耦合、可读代码等。以下是一些常见的结构性架构特性 :

可配置性(Configurability):终端用户通过可用界面轻松更改软件配置的能力。

可扩展性(Extensibility):添加新功能的难易程度。

可安装性(Installability):系统在所有必要平台上的安装难易程度。

杠杆性/复用性(Leverageability/Reuse):在多个产品中复用通用组件的能力。

本地化(Localization):支持多语言输入/查询屏幕、数据字段、报告等。

可维护性(Maintainability):应用更改和增强系统的难易程度。

可移植性(Portability):系统是否需要在多个平台上运行。

支持性(Supportability):应用所需的技术支持级别。

可升级性(Upgradeability):从旧版本升级到新版本的难易程度。

跨领域架构特性(Cross-Cutting Architecture Characteristics)

某些架构特性难以归类,但它们仍然是重要的设计约束和考虑因素。以下是一些常见的跨领域架构特性 :

可访问性(Accessibility):包括色盲、听力障碍等残疾用户的访问。

可归档性(Archivability):数据是否需要归档或在一段时间后删除。

认证(Authentication):确保用户身份的安全要求。

授权(Authorization):确保用户只能访问应用的某些功能的安全要求。

法律合规性:系统运营中涉及的立法约束。

隐私:对内部公司员工隐藏事务的能力。

安全性:数据在数据库中的加密需求、内部系统之间网络通信的加密需求等。

可支持性:应用所需的技术支持级别和调试系统错误所需的日志记录设施。

可用性/可达成性:用户通过应用达到目标所需的培训级别。

架构特性之间的权衡

应用程序通常只能支持我们列出的少数几个架构特性,原因多种多样。首先,每一个支持的特性都需要设计上的努力,甚至可能需要结构上的支持。其次,更大的问题在于,每个架构特性往往会影响到其他特性。例如,如果架构师想要提高安全性,几乎肯定会对性能产生负面影响:应用程序必须进行更多的实时加密、秘密隐藏的间接处理以及其他可能降低性能的活动。

一个比喻可以帮助说明这种互联性。据说飞行员在学习驾驶直升机时经常会遇到困难,因为驾驶直升机需要每只手和每只脚都控制一个操纵杆,改变一个操纵杆会影响到其他操纵杆。因此,驾驶直升机是一种平衡的练习,这很好地描述了选择架构特性时的权衡过程。每个架构师设计支持的架构特性都有可能使整体设计变得复杂。

因此,架构师很少遇到能够设计一个系统并最大化每个架构特性的情况。更多的时候,决策是几种相互竞争的关注点之间的权衡

设计“最不差”架构

总之,架构师很少能设计出在每个方面都最佳的系统。通常,决策涉及多个相互竞争的关注点之间的权衡 。架构师应尽量设计可迭代的架构,这样可以更容易地对架构进行更改,从而减少复杂性和风险 。

第五章 识别架构特性

识别关键的架构特性是创建架构或确定现有架构有效性的重要步骤。要识别适用于特定问题或应用的正确架构特性(“-ilities”),架构师不仅需要了解领域问题,还需要与问题域的利益相关者合作,以确定从领域角度看什么是真正重要的 。

从领域关注点中提取架构特性

架构师必须能够将领域关注点转化为架构特性。例如,扩展性是最重要的关注点,还是容错性、安全性或性能?也许系统需要所有这四个特性。了解关键的领域目标和领域情况可以让架构师将这些领域关注点转化为“-ilities”,从而形成正确且合理的架构决策的基础 。

在与领域利益相关者合作定义驱动架构特性时,一个提示是尽量保持最终列表尽可能简短。架构中的一个常见反模式是试图设计一个通用架构,支持所有架构特性。每个支持的架构特性都会增加系统设计的复杂性;支持太多的架构特性会在架构师和开发人员还没有开始解决问题域时就导致复杂性大大增加。因此,不要过分关注特性的数量,而要关注保持设计简单的动机 。

案例研究:Vasa战舰

Vasa战舰是过度规定架构特性最终导致项目失败的经典例子。这是一艘瑞典战舰,建造于1626年至1628年间,由希望建造最宏伟战舰的国王下令建造。当时的船只通常是运兵船或战舰,而Vasa战舰要同时具备这两种功能。大多数船只有一层甲板,而Vasa战舰有两层!尽管经验丰富的造船工匠有些担忧,但最终还是完成了建造。在庆祝时,战舰驶入港口并向一侧发射了炮弹。由于战舰重心过高,最终倾覆并沉入瑞典湾底。这个故事告诉我们,过度规定架构特性会导致设计复杂性和风险增加,最终可能导致项目失败 。

从需求中提取架构特性

从需求中提取架构特性

从需求中提取架构特性需要架构师深入理解需求文档并识别出其中的架构特性。架构师必须将需求翻译成架构特性,以确保系统满足这些需求。例如,一个购物网站可能希望支持特定数量的并发用户,这是需求文档中明确规定的。架构师需要将这种需求转化为扩展性特性,以确保系统能够处理大量并发用户而不会出现性能问题 。

隐含特性

隐含特性是指那些没有明确写在需求文档中的特性,但它们对系统的成功至关重要。例如,安全性可能不会在需求文档中明确指出,但对于处理支付信息的系统来说,安全性是不可或缺的 。

案例研究:硅三明治

为了更好地说明这些概念,书中引入了一个架构Kata——硅三明治(Silicon Sandwiches)。这个案例研究展示了架构师如何从需求中提取架构特性 。

描述:一家全国性的三明治店希望实现在线订购(除了现有的电话订购服务)。

用户:成千上万,可能有一天会有数百万用户。

需求:

用户下订单后,将获得一个取餐时间和商店的方向(需要集成多个外部地图服务,包括交通信息)。

如果商店提供送餐服务,调度司机将三明治送到用户手中。

移动设备可访问性。

提供全国每日促销/特价。

提供本地每日促销/特价。

接受在线支付、现场支付或送餐支付 。

附加背景:

三明治店是特许经营店,每家店都有不同的店主。 总公司计划在不久的将来扩展到海外。 公司目标是雇佣廉价劳动力以最大化利润 。 在这个场景中,架构师如何提取架构特性呢?架构师不会在这里设计整个系统,而是寻找那些影响或影响设计的因素,特别是结构性的因素 。

显式特性

显式架构特性出现在需求规范中,作为必要设计的一部分。例如,一个购物网站可能希望支持特定数量的并发用户,这在需求规范中由领域分析师指定。架构师需要考虑需求的每一部分,看看它是否构成架构特性 。

一个架构师首先应该注意到的细节是用户数量:目前是成千上万,可能有一天会达到数百万。因此,扩展性——处理大量并发用户而不会严重性能下降的能力——是顶级架构特性之一。注意,这个问题陈述并没有明确要求扩展性,而是以预期用户数量的形式表达了这一需求。架构师经常需要将领域语言解码为工程等价物 。

第六章 度量和管理架构特性

度量架构特性

组织中存在几个关于架构特性定义的常见问题:

它们不是物理学:许多常用的架构特性含义模糊。例如,架构师如何设计敏捷性或可部署性?行业对这些常见术语的定义存在巨大差异,有时是由于不同的上下文,有时是偶然的​​。

定义变化大:即使在同一个组织内,不同部门对关键特性的定义也可能存在分歧。除非开发人员、架构师和运维部门统一定义,否则很难进行有效的对话​​。

过于复合:许多理想的架构特性在较小的尺度上包含了许多其他特性。例如,开发人员可以将敏捷性分解为模块化、可部署性和可测试性​​。

通过组织范围内达成架构特性的具体定义,团队可以围绕架构创建通用语言。同时,通过鼓励具体定义,团队可以解开复合特性,揭示可以客观定义的可测量特性​​。

操作性度量

许多架构特性具有明显的直接测量方法,如性能或可扩展性。然而,即使是这些也提供了许多细微的解释,取决于团队的目标。例如,团队可能会测量某些请求的平均响应时间,这是操作性架构特性的一个很好的例子。但如果团队只测量平均值,如果某个边界条件导致1%的请求花费比其他请求长10倍的时间会怎样?如果站点的流量足够多,这些异常值可能根本不会出现。因此,团队可能还希望测量最大响应时间以捕捉异常值​​。

性能的多种形式

我们描述的许多架构特性有多种细微的定义。性能就是一个很好的例子。许多项目关注一般性能:例如,网络应用程序的请求和响应周期需要多长时间。然而,架构师和DevOps工程师在建立性能预算方面做了大量工作:为应用程序的特定部分制定具体的预算。例如,许多组织研究了用户行为,确定了首次页面渲染(在浏览器或移动设备中网页的第一个可见进度标志)的最佳时间是500毫秒(半秒);大多数应用程序在这个指标上落在两位数范围内。但对于试图捕获尽可能多用户的现代站点来说,这是一个重要的指标,这些组织已经建立了非常细致的度量标准​​。

这些指标中的一些对应用程序的设计有额外的影响。许多前瞻性组织为页面下载设定了K-weight预算:特定页面上允许的库和框架的最大字节数。他们这样做的理由源于物理限制:一次只能通过网络传输一定数量的字节,特别是对于高延迟区域的移动设备​​。

高级团队不仅设定了严格的性能数字;他们基于统计分析建立了定义。例如,假设一个视频流服务想要监控可扩展性。工程师不是设置一个任意数字作为目标,而是测量随时间变化的规模,并建立统计模型,然后如果实时指标超出预测模型就会发出警报。故障可能意味着两件事:模型不正确(团队希望知道)或有问题(团队也希望知道)​​。

结构性度量

一些客观度量不如性能那么明显。内部结构特性如良好定义的模块化怎么办?不幸的是,目前还没有全面的内部代码质量度量标准。然而,一些度量和常用工具确实允许架构师解决代码结构的一些关键方面,尽管是沿着狭窄的维度​​。

圈复杂度

圈复杂度(CC)是由Thomas McCabe Sr.于1976年开发的代码级度量,旨在为代码复杂性提供一个客观的度量标准​​。

它是通过将图论应用于代码,特别是决策点,从而导致不同的执行路径来计算的。例如,如果一个函数没有决策语句(如if语句),那么CC=1。如果该函数有一个条件语句,则CC=2,因为存在两种可能的执行路径​​。

计算单个函数或方法的CC公式为CC = E − N + 2,其中N代表节点(代码行),E代表边(可能的决策)​​。

过程度量

一些架构特性与软件开发过程交叉。例如,敏捷性经常被视为一个理想的特性。然而,它是一个复合的架构特性,架构师可以将其分解为特性如可测试性和可部署性​​。

可测试性可以通过几乎所有平台的代码覆盖工具进行测量,这些工具评估测试的完整性。像所有软件检查一样,它不能取代思考和意图。例如,代码库可能有100%的代码覆盖率,但如果断言质量差,实际上并不能提供代码正确性的信心。然而,可测试性显然是一个客观可测量的特性。同样,团队可以通过各种度量来测量可部署性:成功部署与失败部署的比例、部署花费的时间、部署引发的问题/错误等​​。

治理和适应性函数

一旦架构师确定了架构特性并对其进行优先排序,他们如何确保开发人员会遵守这些优先事项?模块化是架构的一个很好的例子,它很重要但不是紧急的;在许多软件项目中,紧迫性占据主导地位,但架构师仍然需要一个治理机制​​。

治理架构特性

治理,源于希腊词kubérnan(掌舵),是架构师角色的重要职责。顾名思义,架构治理的范围涵盖架构师(包括企业架构师)希望影响的软件开发过程的任何方面。例如,确保组织内的软件质量属于架构治理的范畴,因为它属于架构的范围,疏忽可能导致灾难性的质量问题​​。

幸运的是,越来越多复杂的解决方案存在,以缓解架构师的这一问题,这是软件开发生态系统中能力逐步增长的一个很好的例子。极限编程推动的软件项目自动化催生了持续集成,进一步自动化到运维,我们现在称之为DevOps,并延续到架构治理​​。

适应性函数

“进化”一词在《构建进化架构》一书中更多地源于进化计算而非生物学。作者之一,Rebecca Parsons博士,在进化计算领域工作了一段时间,包括使用遗传算法的工具。遗传算法执行并产生一个答案,然后通过进化计算领域定义的知名技术进行变异。如果开发人员试图设计一个遗传算法来产生一些有益的结果,他们通常希望引导算法,提供一个客观的度量来指示结果的质量。这个指导机制被称为适应性函数:一个用于评估结果接近目标程度的对象函数。例如,假设开发人员需要解决旅行推销员问题,这是一个著名的机器学习问题。给定一个推销员和他们必须访问的一系列城市,以及城市之间的距离,什么是最优路径?如果开发人员设计一个遗传算法来解决这个问题,一个适应性函数可能会评估路径的长度,因为最短的路径表示最高的成功。另一个适应性函数可能是评估路径的总成本,并尝试将成本保持在最低水平。还有一个可能是评估旅行推销员的总旅行时间,并优化以缩短总旅行时间​​。

用途

健壮函数不仅限于新框架,而是提供了一个新的视角来使用许多现有工具。健壮函数可以作为指标、监控器、单元测试库、混沌工程等来使用 。例如,架构师可以通过健壮函数来评估页面加载时间,这是架构的一个重要特征。为了允许系统在不降低性能的情况下进行更改,架构可以构建一个健壮函数作为测试,每次页面加载时测量其时间,并在项目的持续集成中运行测试。

Netflix的混沌猴子和灵长类军队

Netflix的混沌工程是健壮函数的一个有趣例子。他们创建了混沌猴子来模拟生产环境中的一般混乱,以查看系统能否承受这些情况。例如,Conformity Monkey允许Netflix架构师定义治理规则并在生产中执行这些规则。如果架构师决定每个服务应该对所有RESTful动词作出有用的响应,他们会将这一检查构建到Conformity Monkey中。类似地,Security Monkey检查每个服务是否存在已知的安全缺陷,而Janitor Monkey查找不再有其他服务路由到的实例,并将其从生产中移除​​。

第七章 架构特性范围

耦合和共生性

耦合

是指系统中各个模块或组件之间的依赖程度。高耦合会导致系统的可维护性和灵活性下降,因为一个模块的变化可能会影响其他模块。为了减少耦合,架构师需要设计模块化的系统,使得各个模块尽可能独立。

共生性

是另一种描述系统中组件间依赖关系的方式。共生性分为静态共生性和动态共生性,静态共生性包括名称共生性和类型共生性等,而动态共生性包括执行顺序共生性和时间共生性等。通过分析共生性,架构师可以识别并减少系统中的强依赖关系,从而提高系统的灵活性和可维护性 。

架构量子和粒度

架构量子

是定义系统中独立可部署单元的一种方法。每个架构量子包含实现某个功能所需的所有组件,并且这些组件之间具有强耦合关系。通过识别和定义架构量子,架构师可以更好地管理系统的复杂性和演变过程

粒度

是指系统中模块或组件的大小和细化程度。粒度过大可能导致模块内部复杂性增加,而粒度过小则可能导致模块之间的依赖关系复杂化。架构师需要在系统设计中找到适当的粒度,以平衡模块的独立性和系统的整体复杂性 。

案例研究:竞拍系统“Going, Going, Gone” 为了更好地说明架构量子和粒度的应用,书中引入了一个竞拍系统的案例研究。通过分析和定义竞拍系统中的架构量子,架构师能够确定不同功能模块的边界,减少模块间的耦合,确保系统的可扩展性和维护性 。

在这个案例中,架构师识别了几个关键的架构量子,包括支付服务、竞拍者、出价流和出价跟踪器等。通过使用架构量子分析,架构师能够更容易地确定服务、数据和通信的边界,从而设计出最不差的解决方案 。

确定架构特性范围的方法

识别关键架构特性:架构师需要确定哪些架构特性对系统成功至关重要。例如,性能、可扩展性、安全性等特性在不同系统中可能具有不同的重要性 。

定义架构量子:通过识别系统中的架构量子,架构师可以确定每个独立可部署单元的边界。这有助于减少模块间的耦合,提高系统的灵活性和可维护性 。

确定适当的粒度:在系统设计中找到适当的粒度,以平衡模块的独立性和系统的整体复杂性。过大的粒度可能导致模块内部复杂性增加,而过小的粒度则可能导致模块之间的依赖关系复杂化 。

第八章 基于组件的思维

组件范围

组件是物理封装模块的实现方式。大多数编程语言支持物理封装,例如Java中的jar文件、.NET中的dll文件和Ruby中的gem文件。在架构中,组件形成了模块化设计的基础,成为架构师必须关注的关键内容之一 。

组件的类型

组件可以以多种形式出现,如子系统或架构中的层,事件处理器的可部署单元等。另一种类型的组件是服务,它通常运行在自己的地址空间中,通过低级别的网络协议(如TCP/IP)或高级别的格式(如REST或消息队列)进行通信,形成独立的、可部署的单元,如微服务架构 。

组件提供了一种高于编程语言最低级别的模块化机制。例如,在微服务架构中,服务可能包含足够多的代码以至于需要组件,或者可能简单到只包含少量代码 。

架构师的角色

架构师通常在架构中定义、改进、管理和治理组件。软件架构师需要与业务分析师、主题专家、开发人员、QA工程师、运维人员和企业架构师合作,创建软件的初步设计,涵盖架构特性和系统需求 。

架构独立于开发过程存在,这意味着无论采用何种软件开发过程,架构都应存在并发挥作用。尽管某些工程实践(如敏捷软件开发)对架构产生了影响,但总体上,软件架构独立于开发过程 。

架构划分

软件架构的第一定律指出,软件中的一切都是权衡,包括架构师在架构中创建组件的方式。组件代表了一种通用的容器机制,架构师可以构建任何类型的划分。常见的架构风格包括技术划分和域划分 。

不同架构模式之间的一个根本区别是它们各自支持的顶级划分风格不同。 它还对架构师如何识别原始组件产生了巨大的影响— —架构师是想从技术上划分还是按领域划分? 使用技术划分的架构师将架构划分为展示层、 业务规则层、 服务层、 持久层等。 因此,此架构的组织原则之一是技术关注点分离。 这进而创建了解耦级别:如果服务层只连接到下面的持久层和上面的业务规则层,那么持久层中.的更改只会潜在地影响这些层。 这种风格的划分提供了一种解耦技巧,减少了对相关组件的连锁副作用。 第10章的分层架构模式中详细介绍这种架构风格。 使用技术划分来组织系统当然是合乎逻辑的,但是,就像软件架构中的所有东西一样,这里有一些权衡。

技术划分

在技术划分中,系统的组件按技术能力组织,如表示层、业务规则、持久层等。这种划分方式使得代码库的组织更为清晰,开发人员可以快速找到与技术能力相关的代码。例如,分层架构就是一种常见的技术划分方式 。

优点

-清晰地分离定制代码。 -更接近分层架构模式。

缺点

  • 更高程度的全局耦合。 对通用组件或本地组件的更改可能会影响所有其他组件。

  • 开发人员可能不得不在通用层和本地层中重复引入领域概念。

  • 通常在数据级有更高的耦合。 在这样的系统中,应用程序架构师和数据架构师可能会协作创建单个数据库,该数据库包含定制化需求的数据和领域数据。 如果架 构师日后想要将此架构迁移到分布式系统中,那么这反过来又会给数据关系的分解带来困难

域划分

域划分基于域驱动设计(Domain-Driven Design, DDD),将架构划分为独立且解耦的域或工作流。每个域组件可能包含子组件,包括层,但顶层划分围绕域进行,更好地反映了项目中最常发生的变化类型。例如,微服务架构就是基于这种哲学的 。

优点:

  • 更接近于业务功能而不是实现细节的建模。

  • 更容易利用逆向康威演习来围绕领域构建跨功能团队。

  • 更接近模块单体和微服务架构风格。

  • 消息流匹配问题域。

  • 易于将数据和组件迁移到分布式架构中。

缺点:

  • 定制化代码出现在多个地方

开发人员角色

组件识别流

识别组件是架构师在新项目中必须完成的首要任务之一。为了识别组件,架构师需要了解如何划分架构。通常,组件是架构师直接交互的最低级别的软件系统 。组件识别流程包括以下步骤:

识别初始组件:确定系统中需要的初始组件。

分配需求到组件:将系统需求分配给相应的组件。

分析角色和职责:分析每个组件的角色和职责。

分析架构特性:确保每个组件都满足其相关的架构特性。

重构组件:根据需求和特性对组件进行重构,以优化系统设计 。

组件粒度(Component Granularity)

组件的粒度是指其大小和细化程度。粒度过大可能导致组件内部复杂性增加,而粒度过小则可能导致组件之间的依赖关系复杂化。架构师需要在系统设计中找到适当的粒度,以 平衡组件的独立性和系统的整体复杂性 。

组件设计(Component Design)

组件设计包括确定每个组件的具体实现细节。架构师需要考虑组件的接口、依赖关系、通信方式等,确保每个组件都能够独立运行并与其他组件有效协作 。架构师通常与其他角色(如开发人员、 业务分析人员和主题专家)协作,基于系统的一般知识以及架构划分方式(基于技术或领域划分)来设计初始组件。 团队的目标是进行初步设计,将问题空间划分为考虑不同架构特征的粗块。

发现组件

实体陷阱
演员/动作方法

演员/动作(actor/actions)方法是架构师用于将需求映射到组件的流行方法,最初由IBM旗下的Rational公司开发的统一软件开发过程(Rational Unified Process)定义。 在这种方法中,架构师确定了使用应用程序执行活动的参与者,以及这些参与者可能执行的操作。 它提供了一种技术,用于发现系统的典型用户以及他们可能使用系统做什么。演员/动作方法与特定的软件开发过程(特别是有利于前期设计的正式过程)结合后流行开来。 它仍然很流行,并且当需求具有不同的角色和可以执行的各种操作时工作得很好。这种类型的组件分解风格适用于所有类型的系统,不管是单体系统还是分布式系统。

事件风暴

事件风暴作为一种组件发现技术,来自领域驱动设计。 它与微服务一样受欢迎,也深受领域驱动设计的影响。 在事件风暴中,架构师假定项目将使用消息或事件在各个组件之 间进行通信。 为此,团队尝试根据需求和识别出的角色确定系统中发生的事件,并围绕这些事件和消息处理程序构建组件。 这在使用事件和消息的分布式架构(如微服务)中 很有效,因为它帮助架构师定义最终系统中使用的消息

工作流方法

该方法是事件风暴的另一种替代方法,为不使用领域驱动设计或消息传递的架构师提供了更通用的方法。 工作流方法围绕工作流进行建模,非常类似于事件风暴,但是这种方 法没有用于构建基于消息的系统的显式约束。 工作流方法识别关键角色,确定这些角色参与的工作流类型,并围绕识别的活动构建组件。

第九章 基础

架构样式的定义与重要性

架构样式(architecture styles)或称架构模式(architecture patterns),指的是组件之间的一种命名关系,涵盖了各种架构特性。架构样式的命名类似于设计模式,它们为有经验的架构师提供了简明的交流方式。例如,当谈到分层单体架构(layered monolith)时,讨论的对象就能立即理解其结构特征、适用的架构特性、典型的部署模型、数据策略等信息。因此,架构师应熟悉这些基础的通用架构样式的名称及其背后的详细信息 。

基本模式

基础模式是指在软件架构历史上反复出现的一些基本组织代码、部署或其他架构方面的模式。这些模式不仅有助于理解更复杂的现代架构,还在不同的架构风格中反复出现。

  • Big Ball of Mud(混乱的大球):

这是指缺乏任何可辨别的架构结构的系统,其代码结构混乱无序,缺乏模块化和组织性,导致维护和扩展极其困难。这样的系统常见于初期开发过程中未被明确架构定义的项目 。

  • Unitary Architecture(单一架构):

    这种架构通常出现在早期的计算机软件中,当时软件和硬件高度集成。随着计算能力的提高,系统开始分离出不同的功能,例如从单体系统发展到数据库系统的分离。这种架构现今已较少出现,主要存在于嵌入式系统等高度受限的环境中 。

  • Client/Server(客户机/服务器):

这是一种将系统技术功能划分为前端和后端的架构模式。最常见的变种包括桌面应用程序+数据库服务器、浏览器+网络服务器等。这种模式的一个扩展是三层架构,它进一步将系统划分为用户界面层、业务逻辑层和数据库层 。

  • 单体架构与分布式架构

架构样式可以大致分为单体和分布式两类。单体架构通常是单个可部署单元的所有代码,而分布式架构则涉及多个部署单元通过远程访问协议连接 。

单体架构包括:

  • 分层架构:

是一种非常常见的架构风格,因其简单、熟悉和成本低廉而成为多数应用程序的默认选择。这种架构的组件组织成逻辑层,每一层执行特定的功能,如展示逻辑、业务逻辑、持久层等 。

  • 管道架构:

主要用于数据处理流程明确且依赖链条清晰的场景 。

  • 微内核架构:
  • 这种架构中,核心系统提供最小的基础服务,而特定功能由插件组件提供 。

分布式架构包括:

  • 服务为中心的架构:

将系统功能划分为多个服务,每个服务独立部署和管理 。

  • 事件驱动架构: 系统通过事件进行通信和处理,适合于实时处理和高并发场景 。

  • 空间架构:

主要用于需要高可用性和可扩展性的场景 。

  • 微服务架构:

将系统功能进一步细化为微小的、独立的服务单元,每个服务专注于特定业务功能 。

分布式架构的挑战与误区

分布式架构虽然提供了性能、可扩展性和高可用性方面的显著优势,但也带来了显著的复杂性和挑战。这些挑战包括分布式计算的八大误区,如网络可靠性、延迟、带宽、安全性等问题 。这些误区强调了架构师在设计分布式系统时必须充分考虑的关键因素,以避免系统中的潜在问题和陷阱。

第10章 分层架构样式

1. 分层架构的定义与重要性

分层架构,也称为n层架构,是一种将系统的不同功能按逻辑分层的架构风格。每一层负责特定的功能,比如表现逻辑或业务逻辑。这种架构风格因其简单性、熟悉性和低成本而成为大多数应用程序的标准选择​​。此外,根据康威定律,系统的设计会复制该组织的沟通结构,这也使得分层架构自然适用于多数企业应用​​。

2. 拓扑结构

image

分层架构的组件被组织成水平的逻辑层,典型的有四层:表现层、业务层、持久层和数据库层。这些层的划分可以根据应用程序的复杂性进行调整,小型应用可能只有三层,而大型复杂的企业应用可能有五层或更多层​​。

表现层:负责处理用户界面和浏览器通信逻辑。它不需要关心数据的获取方式,只需显示数据即可。 业务层:负责执行与业务请求相关的特定业务规则。它从持久层获取数据,进行业务逻辑处理后传递给表现层。 持久层:管理数据库交互逻辑,通常包括查询、数据存储和检索等功能。 数据库层:实际存储数据的地方。 在实际部署时,这些层可以物理上分离成不同的部署单元,也可以组合成单个部署单元。对于小型应用程序,有时所有层甚至包含数据库层都会组合在一个部署单元中​​。

3. 分离关注点与职责模型

分层架构的一个关键概念是分离关注点,即每一层只处理与其相关的逻辑。例如,表现层只关心如何展示数据,而不关心数据的来源或如何处理;业务层则只关心业务逻辑的实现,而不涉及数据的展示​​。这种分离使得开发人员可以专注于各自领域的技术专长,提升了系统的模块化和职责清晰度。

4. 隔离层(Layers of Isolation)

每一层可以是关闭的或开放的。在关闭的层中,处理请求时不能跳过任何层,必须逐层向下传递;而在开放的层中,可以跳过中间层直接访问底层。这种设计使得各层之间具有独立性,降低了层之间的耦合​​。然而,过多的开放层可能导致系统的脆弱性和维护困难。

5. 添加新层

在某些情况下,添加新层可能是必要的。例如,为了限制表现层对业务对象的直接访问,可以引入一个服务层将这些对象隔离开来​​。这种层次的调整有助于明确系统中的访问限制和关系,从而避免紧耦合和脆弱的架构。

6. 反模式和其他考虑因素

分层架构容易出现一种称为“架构陷阱”的反模式,即各层之间的请求仅仅是简单地传递,而没有实际的业务逻辑处理。这种情况会导致不必要的对象实例化和性能损失​​。此外,分层架构由于其单体性质,不利于大规模应用的扩展和性能优化。尽管可以通过缓存、多线程等技术提升性能,但这并非分层架构的固有特性​​。

7. 使用分层架构的理由

分层架构非常适合小型、简单的应用或网站。由于其简单性和开发人员的熟悉性,分层架构在成本和开发效率上具有优势,特别是在预算和时间紧张的情况下​​。然而,对于大型应用程序,由于可维护性、灵活性、可测试性和可部署性较差,分层架构可能不如其他更模块化的架构风格合适​​。

8. 架构特性评分

分层架构在总体成本和简单性方面得分较高,但在部署、可测试性、弹性和扩展性方面得分较低。这种架构的单体特性导致了较高的部署风险和较长的恢复时间(MTTR),因此在可靠性方面也得分不高​​。总体来说,尽管分层架构在某些应用场景中非常有效,但其在处理复杂系统时的局限性也是显而易见的。

分层架构特性评分

特性 评分(星级)
总体成本 ★★★★★
简单性 ★★★★★
可部署性 ★☆☆☆☆
可测试性 ★★☆☆☆
可靠性 ★★★☆☆
可扩展性 ★☆☆☆☆
弹性 ★☆☆☆☆
性能 ★★☆☆☆
容错性 ★☆☆☆☆
可用性 ★★☆☆☆

分层架构在总体成本和简单性方面得很好,但在部署、可测试性、弹性和扩展性方面较低。单体特性导致了较高的部署风险和较长的恢复时间(MTTR)。

第11章 管道架构

管道架构的定义与概述

管道架构是一种将功能划分为独立处理单元(称为过滤器)的模式,这些过滤器通过单向通信的管道连接在一起。这个模式的基本思想是将数据流的每个处理步骤封装在独立的过滤器中,这些过滤器依次处理数据流,最终将数据输出到终点。管道架构广泛应用于Unix终端shell语言(如Bash和Zsh)以及MapReduce编程模型中,展示了其强大的组合复用能力​​。

拓扑结构

image

管道架构的拓扑结构主要由两部分组成:管道和过滤器。

管道(Pipes):在这个架构中,管道是过滤器之间的通信通道。每个管道通常是单向的,点对点传输(而非广播),以提高性能。管道承载的数据载荷可以是任何数据格式,但为了高性能,架构师倾向于使用较小的数据量​​。

过滤器(Filters):过滤器是自包含的,独立于其他过滤器的单元,通常是无状态的。每个过滤器只执行一个任务,复杂的任务则通过多个过滤器的序列来处理​​。

过滤器类型

过滤器有四种基本类型:

生产者(Producer):流程的起点,通常只出不进,有时也称为源头(Source)。

转换器(Transformer):接受输入,可选地对部分或全部数据进行转换,然后将其发送到输出管道。函数式编程中的map操作类似于此类过滤器。

测试者(Tester):接受输入,测试一个或多个条件,然后根据测试结果选择性地产生输出。函数式编程中的reduce操作类似于此类过滤器。

消费者(Consumer):管道流程的终点。消费者有时会将最终结果持久化到数据库中,或者在用户界面上显示​​。

管道架构的特点与优势

组合复用性:由于管道和过滤器的单向性质和简单性,管道架构鼓励组合复用。开发者可以通过组合现有的过滤器和管道来创建新的数据处理流程​​。

模块化:管道架构中的每个过滤器都是独立的,可以单独修改或替换,而不影响其他过滤器。这种分离关注点的方式有助于提高系统的可维护性和可扩展性​​。

易于扩展:例如,在处理Kafka流数据的例子中,可以轻松地在Uptime Filter之后添加新的测试过滤器,以处理其他新的指标​​。

示例应用

管道架构模式在各种应用中都有体现,特别是那些涉及简单的单向数据处理任务。例如,许多电子数据交换(EDI)工具使用这种模式来转换文档类型;ETL工具(提取、转换和加载)利用管道架构将数据从一个数据库或数据源流向另一个;Apache Camel等编排器和中介者利用管道架构在业务流程的各个步骤之间传递信息​​。

管道架构特性评分

特性 评分(星级)
总体成本 ★★★★☆
简单性 ★★★★☆
可部署性 ★★★☆☆
可测试性 ★★★☆☆
可靠性 ★★★☆☆
可扩展性 ★☆☆☆☆
弹性 ★☆☆☆☆
性能 ★★☆☆☆
容错性 ★☆☆☆☆
可用性 ★★☆☆☆

管道架构在成本、简单性和模块化方面表现突出,但在可部署性、可测试性、弹性和可扩展性方面得分较低。这是因为尽管管道架构通过过滤器的独立性实现了一定程度的模块化,但它通常以单体部署为基础,带来了与分布式架构相比的复杂性和部署风险​​。特别是在应对高并发和大规模数据处理时,由于其单体特性,管道架构的扩展性受到限制​​。

第11章 管道架构

拓扑结构

image

微内核架构是一种相对简单的单体架构,由两个主要部分组成:核心系统和插件组件。应用逻辑被分为独立的插件组件和基本的核心系统,从而提供了可扩展性、适应性和应用功能及自定义处理逻辑的隔离 。

核心系统

核心系统被定义为运行系统所需的最小功能集合。例如,Eclipse IDE的核心系统仅包含基本的文本编辑功能,如打开文件、修改文本和保存文件。只有在添加插件后,Eclipse才开始成为一个可用的产品。核心系统的另一个定义是“理想路径”,即应用程序中的一般处理流程,几乎没有或完全没有自定义处理。通过将核心系统的环形复杂性移除并放入单独的插件组件中,可以实现更好的可扩展性和可维护性,同时也提高了可测试性 。

实现方式

image

核心系统可以实现为分层架构或模块化单体架构,有时甚至可以将核心系统拆分为单独部署的领域服务,每个领域服务包含特定领域的插件组件 。

插件组件

插件组件是独立的,包含专门的处理、附加功能和用于增强或扩展核心系统的自定义代码的独立组件。插件组件用于隔离高度不稳定的代码,创建更好的可维护性和可测试性。理想情况下,插件组件应彼此独立且没有相互依赖 。

插件通信

  • Shared library plug-in implementation
image
  • Package or namespace plug-in implementation

    image
  • Remote plug-in access using REST

image
  • Plug-in components can own their own data store
image

插件组件与核心系统之间的通信通常是点对点的,这意味着连接插件和核心系统的“管道”通常是对插件组件入口类的方法调用或函数调用。此外,插件组件可以是编译时的或运行时的。运行时插件组件可以在运行时添加或删除,而不需要重新部署核心系统或其他插件 。

注册表和合同

注册表 是核心系统用来了解哪些插件模块可用及如何访问它们的工具。它包含每个插件模块的信息,包括名称、数据合同和远程访问协议等。注册表可以是核心系统内部的简单映射结构,也可以是复杂的注册表和发现工具 。

使用案例

微内核架构在产品和大型业务应用中都有广泛的应用。例如,Eclipse IDE、PMD、Jira和Jenkins等开发和发布工具,网络浏览器如Chrome和Firefox,都是使用微内核架构的典型产品 。

微内核架构特性评分

特性 评分(星级)
总体成本 ★★★★☆
简单性 ★★★★☆
可部署性 ★★★☆☆
可测试性 ★★★☆☆
可靠性 ★★★☆☆
可扩展性 ★☆☆☆☆
弹性 ★☆☆☆☆
性能 ★★★☆☆
容错性 ★☆☆☆☆
可用性 ★★☆☆☆

第十三章 基于服务的架构

拓扑结构

image

基于服务的架构的基本拓扑结构包括独立部署的用户界面、远程粗粒度服务和单一数据库。服务通常是独立的、单独部署的应用部分(通常称为领域服务),它们的部署方式与传统的单体应用相同,如EAR文件、WAR文件或汇编文件。由于这些服务通常共享一个单一的单体数据库,因此在应用上下文中通常有4到12个服务,平均约为7个服务​​。

服务通常通过远程访问协议从用户界面访问。虽然通常使用REST访问服务,但也可以使用消息传递、远程过程调用(RPC)或SOAP。此外,可以使用代理或网关的API层从用户界面(或其他外部请求)访问服务。大多数情况下,用户界面直接使用嵌入在用户界面、API网关或代理中的服务定位器模式访问服务​​。

一个重要方面是,基于服务的架构通常使用共享的中心数据库。这允许服务以类似于传统单体分层架构的方式利用SQL查询和连接。由于服务数量少(4到12个),数据库连接通常不是问题。然而,数据库更改可能是一个问题​​。

拓扑变体

image image

基于服务的架构样式中存在许多拓扑变体,使其成为最灵活的架构样式之一。例如,可以将单一的单体用户界面分解为用户界面域,甚至到与每个领域服务匹配的程度。同样,也可以将单一的单体数据库分解为多个独立的数据库,甚至是与每个领域服务匹配的领域范围数据库。在这种情况下,确保每个独立数据库中的数据不会被其他领域服务需要是很重要的​​。

此外,还可以在用户界面和服务之间添加由反向代理或网关组成的API层。这在将领域服务功能暴露给外部系统或将共享的横切关注点(如指标、安全性、审计要求和服务发现)从用户界面中移出时是一个很好的做法​​。

服务设计与粒度

image

在基于服务的架构中,领域服务通常是粗粒度的。每个领域服务通常设计为使用分层架构样式,包括API外观层、业务层和持久层。另一种流行的设计方法是使用子领域对每个领域服务进行域分区,类似于模块化单体架构样式​​。

无论服务设计如何,领域服务必须包含某种API访问外观,用户界面可以通过它执行某种业务功能。API访问外观通常负责协调来自用户界面的业务请求。例如,考虑来自用户界面的一个业务请求:下订单(也称为目录结账)。这个单一的请求由OrderService领域服务中的API访问外观接收,内部协调单一业务请求:下订单、生成订单ID、应用付款以及更新每个订单产品的库存。在微服务架构样式中,这可能涉及协调多个独立部署的远程单一用途服务来完成请求。这个内部类级协调和外部服务协调之间的区别突显了基于服务的架构和微服务在粒度方面的显著差异​​。

由于领域服务是粗粒度的,因此使用常规的ACID(原子性、一致性、隔离性、持久性)数据库事务来确保单个领域服务内的数据库完整性。与微服务架构等高度分布式的架构不同,后者通常具有细粒度的服务,并使用一种称为BASE事务(基本可用性、软状态、最终一致性)的分布式事务技术,依赖最终一致性,因此不支持与基于服务的架构中的ACID事务相同的数据库完整性​​。

数据库分区

image

基于服务的架构通常使用单一的共享数据库,这意味着所有服务共享同一个数据库。这种共享数据库结构的好处是所有服务可以轻松地访问和修改数据,无需担心数据同步问题。然而,这也带来了挑战,特别是在服务之间需要保持数据一致性和完整性时。在某些情况下,可以将单一的数据库分解为多个独立的数据库,每个服务都有自己的数据库。这种做法可以减少服务之间的耦合,提高系统的灵活性和扩展性​​。

示例架构

image

一个电子设备回收系统的示例展示了基于服务的架构的许多优点,如可扩展性、容错性和安全性(数据和功能保护和访问)。例如,Assessment服务经常更改以添加新产品的评估规则。这种频繁的更改仅限于单个领域服务,从而提供了敏捷性(快速响应变化的能力)、可测试性(测试的容易性和完整性)和可部署性(部署的容易性、频率和风险)​​。

基于服务的架构特性评分

特性 评分(星级)
总体成本 ★★★★☆
简单性 ★★★★☆
可部署性 ★★★★☆
可测试性 ★★★★☆
可靠性 ★★★★☆
可扩展性 ★★★☆☆
弹性 ★★☆☆☆
性能 ★★★☆☆
容错性 ★★★★☆
可用性 ★★★★☆

基于服务的架构在成本方面表现良好,得分接近最高分。这种架构模式相对其他复杂的分布式架构(如微服务或事件驱动架构)更具成本效益。它能够通过独立部署的领域服务减少开发和维护的成本。这种架构的简单性和灵活性使得在初始开发和后期维护中的成本都相对较低 。

基于服务的架构的简单性得分也很高。其结构清晰,将系统划分为独立的领域服务,每个服务处理特定的业务功能。这种分离使得系统更易于理解、开发和维护。简单性也体现在开发团队能够专注于各自领域的服务,而不必处理整个系统的复杂性 。

可部署性得分高的原因在于,每个领域服务可以独立部署。这意味着可以在不影响整个系统的情况下更新或更改某个服务。此外,独立部署还允许并行开发和测试,进一步提高了系统的可维护性和更新的频率。这种架构适合持续交付和DevOps实践 。

可靠性方面,基于服务的架构表现出色。由于服务是独立的,即使某个服务出现故障,也不会影响其他服务的正常运行。这种隔离性提高了系统的整体可靠性。服务之间的松耦合和独立性使得系统在面对故障时具有更高的恢复能力 。

由于领域服务是独立的,因此每个服务可以单独测试。这种架构使得单元测试和集成测试更加容易,减少了测试过程中可能出现的依赖和冲突。此外,独立的服务模块使得测试更加细粒度,可以更早地发现和修复问题,提升了整体系统的质量 。

在可扩展性方面,基于服务的架构得分中等。虽然领域服务可以独立扩展,但由于通常使用单一的共享数据库,数据库的扩展性可能成为瓶颈。此外,服务间的同步和协调也会在高并发情况下带来一定的挑战。尽管如此,通过适当的设计和优化,系统仍能实现一定程度的扩展性 。

弹性方面的得分较低。这主要是因为基于服务的架构虽然能够在一定程度上处理动态负载,但由于其核心数据库和服务之间的依赖关系,使得系统在面对大规模的负载变化时可能难以灵活调整。此外,缺乏细粒度的服务实例管理也限制了其弹性 。

容错性方面,基于服务的架构表现优异。服务的独立性意味着一个服务的故障不会影响其他服务的运行。这种隔离性提高了系统的整体容错能力。此外,通过实施重试机制和故障隔离,可以进一步增强系统的容错性 。

可用性方面,基于服务的架构同样表现良好。服务的独立部署和松耦合特性使得系统具有较高的可用性。即使某个服务需要停机维护,其他服务仍能继续提供功能,从而保证了系统的整体可用性。此外,通过负载均衡和冗余设计,可以进一步提升系统的可用性 。

性能方面,基于服务的架构表现一般。由于服务是独立的,内部通信可能带来一定的性能开销。此外,共享数据库的读写性能也可能成为系统的瓶颈。然而,通过优化服务间的通信和数据库查询,可以在一定程度上提升系统的性能 。

注意事项

基于服务的架构中,事务管理是一项关键挑战,尤其是在需要跨多个服务或数据库进行操作时。传统的ACID事务(原子性、一致性、隔离性、持久性)在这种分布式环境中难以维持,因为它们依赖于所有参与者都能成功完成操作才能提交事务。为了解决这一问题,服务型架构通常采用BASE事务模型(基本可用性、软状态、最终一致性)。这种模型允许系统在部分服务失败的情况下仍能继续运行,并且最终会达到一致性状态 。

在设计服务时,服务的边界和粒度非常重要。一般来说,服务应该围绕业务功能划分,每个服务负责特定的业务域,这有助于减少服务之间的耦合。此外,服务之间的数据共享和同步需要小心管理,以避免一致性问题。常见的做法是为每个服务设置独立的数据库,避免共享数据库带来的耦合和瓶颈 。

第十五章 空间架构

image

这种架构主要用于处理大规模、高并发的Web应用,旨在解决传统三层架构在面对大量用户请求时的性能瓶颈问题

image

总体拓扑结构

基于空间的架构名称源于元组空间(tuple space)的概念,即使用多个并行处理器通过共享内存进行通信的技术。其高可扩展性和高性能主要通过以下方式实现:去除系统中作为同步约束的中央数据库,而使用复制的内存数据网格。应用程序数据保存在内存中,并在所有活跃的处理单元之间复制。当处理单元更新数据时,它会异步地将这些数据发送到数据库,通常通过持久队列的消息传递方式进行。处理单元根据用户负载的增加和减少动态启动和关闭,从而解决可变的可扩展性问题。由于标准事务处理中没有涉及中央数据库,因此消除了数据库瓶颈,实现了接近无限的应用程序可扩展性​​。

image

处理单元

image

应用逻辑:负责处理业务逻辑的代码。

数据存储:包含数据存储的组件,可以是内存数据网格(IMDG)或数据库实例。

消息处理器:处理来自消息队列的异步消息。

image

数据泵:负责数据的读写操作,确保数据的一致性和持久性 。

image

虚拟化中间件

虚拟化中间件负责管理和协调多个处理单元。它提供以下功能:

负载均衡:动态分配请求到不同的处理单元,以均衡负载。 故障转移:当某个处理单元发生故障时,将请求转移到其他可用的处理单元。 扩展和收缩:根据流量动态增加或减少处理单元的数量 。

数据泵

image

数据泵是空间架构中处理数据读写的关键组件,分为数据写入器和数据读取器。

数据写入器

image

数据写入器负责将数据从处理单元写入到持久化存储中。它确保数据的一致性和持久性,通常使用批处理的方式来提高性能。

数据读取器

数据读取器从持久化存储中读取数据,并将其加载到处理单元的内存中。它通过缓存机制提高数据访问的速度和效率 。

数据冲突与一致性

在空间架构中,处理单元之间的数据一致性是一个重要的问题。为了处理数据冲突,通常采用以下几种策略:

乐观锁:假设冲突很少发生,只有在提交数据时才检查冲突。 悲观锁:在读取数据时就锁定数据,以防止其他处理单元修改。 版本控制:每次修改数据时增加版本号,通过版本号来检测和解决冲突 。

云端与本地实现

image

空间架构既可以在本地部署,也可以在云端实现。云端实现的优势在于可以根据需求动态调整资源,减少初始投资和维护成本。然而,本地实现则提供了更高的安全性和控制权。

复制与分布式缓存

在空间架构中,数据缓存是提高性能的关键。数据可以在多个处理单元之间复制,以提高数据的可用性和访问速度。分布式缓存通过将数据分布在多个节点上,实现了高并发访问和快速数据读取 。

近缓存

近缓存(Near Cache)是一种将数据缓存在处理单元本地的技术。通过在处理单元本地缓存数据,可以显著减少远程数据访问的延迟,提高系统的响应速度。然而,这种技术也增加了数据一致性管理的复杂性 。

实现示例

空间架构在实际应用中有许多成功的案例,如音乐会售票系统和在线拍卖系统。这些系统需要处理大量并发用户请求,并确保高可用性和快速响应。

音乐会售票系统

在音乐会售票系统中,空间架构通过分布式处理单元和数据缓存,实现了高并发售票和实时座位更新。处理单元处理用户请求,数据泵负责将售票信息写入持久化存储,确保数据一致性和可靠性 。

在线拍卖系统

在线拍卖系统需要处理大量的实时竞价请求,并确保竞价过程的公平和透明。空间架构通过分布式处理单元和实时数据同步,实现了高性能的竞价处理和实时更新 。

空间架构特性评分

特性 评分(星级)
总体成本 ★★★☆☆
简单性 ★★★☆☆
可部署性 ★★★★☆
可测试性 ★☆☆☆☆
可靠性 ★★★★☆
可扩展性 ★★★★★
弹性 ★★★★★
性能 ★★★★★
容错性 ★★★★☆
可用性 ★★★☆☆

总体成本 空间架构在总体成本上得分较高,因为其高效的资源利用和灵活的扩展性能够显著降低运营成本。然而,初始设置和实现复杂性可能会带来一定的开发成本 。

简单性 空间架构在简单性方面得分中等。尽管其基本概念相对简单,但实际实现和管理的复杂性可能较高,尤其是在处理数据一致性和故障转移时 。

可部署性和可测试性 由于处理单元是独立的,空间架构的可部署性和可测试性较高。每个处理单元可以独立部署和测试,减少了系统更新和维护的复杂性 。

可靠性和容错性 空间架构通过处理单元的冗余和故障转移机制,实现了高可靠性和容错性。当某个处理单元发生故障时,系统能够自动转移请求,确保服务的连续性 。

可扩展性和弹性 空间架构的可扩展性和弹性得分最高。通过动态增加或减少处理单元,系统可以根据流量负载自动调整,提供高并发处理能力和快速响应 。

性能和可用性 空间架构通过近缓存和分布式缓存技术,实现了高性能和高可用性。处理单元的分布式设计确保了系统的高并发访问能力和快速数据处理 。

第十六章 基于编排的面向服务架构

image

历史与哲学

面向服务的架构(SOA)是为了应对企业级复用需求而发展起来的架构风格。许多大型公司希望通过逐步减少重复的软件开发工作来节约成本和时间,SOA 的发展正是为了实现这一目标。SOA 的哲学核心在于,通过定义和实现可复用的服务,使得业务功能能够被多个应用系统和不同的业务流程共享。尽管这种方法提供了巨大的理论复用潜力,但实践中它面临许多挑战和局限性​​。

拓扑结构与分类法

SOA 的拓扑结构通常由四个主要服务层次组成:业务服务、企业服务、应用服务和基础设施服务。每个层次的服务都有其特定的职责和粒度水平。

业务服务:提供业务逻辑的高层次抽象,通常由业务用户定义,不涉及具体的实现细节。 企业服务:实现细粒度的、跨业务线的共享服务,是业务服务的构建块。这些服务通常由开发团队实现,并在业务功能中复用。 应用服务:特定于某个应用的服务,通常由单个应用团队拥有,不具备复用性。 基础设施服务:提供如监控、日志、认证和授权等操作性支持,通常由基础设施团队负责​​。

编排引擎与消息流

编排引擎是 SOA 的核心,它负责将各个业务服务和企业服务编排成完整的业务流程。编排引擎不仅处理事务协调和消息转换,还充当集成枢纽,使定制代码与包和遗留系统集成​​。

在消息流方面,所有请求都通过编排引擎处理。即使是内部调用,消息流也必须经过引擎。这种方法确保了系统的逻辑集中管理,但也带来了单点故障和性能瓶颈等问题​​。

复用与耦合

SOA 的一个主要目标是通过服务级别的复用逐步构建业务行为。然而,这种复用策略也导致了高耦合的问题。例如,一个共享的客户服务如果发生变动,可能会影响到依赖它的所有其他服务。这种高度耦合使得系统的增量变化变得困难,需要进行协调部署和全面测试​​。

架构特征评估

特性 评级
灵活性 ★★★☆☆
可扩展性 ★★★★☆
性能 ★★☆☆☆
部署性 ★★☆☆☆
可测试性 ★★☆☆☆
可用性 ★★★★☆
容错性 ★★★☆☆
演化性 ★★☆☆☆

SOA 在一些现代工程目标上得分较低,如部署性和可测试性,因为这些特性在该架构流行时并不被重视。此外,尽管 SOA 是一个分布式架构,但由于其常常依赖单一数据库,这种集中式的数据管理导致了耦合问题。这种架构在弹性和可扩展性方面表现尚可,因为工具供应商投入了大量努力使这些系统可扩展,如通过应用服务器间的会话复制等技术。然而,性能始终是 SOA 的短板,因为每个业务请求通常会跨越多个架构层​​。

现代工程目标如可部署性和可测试性在这种架构中表现不佳,因为它们不仅得不到良好的支持,而且在那个时代这些目标也不是重点甚至不被追求的。

尽管在实现这些行为方面存在困难,但这种架构确实支持了一些目标,如弹性和可扩展性。这是因为工具供应商投入了大量精力,使这些系统通过在应用服务器之间构建会话复制等技术实现可扩展性。然而,作为一种分布式架构,性能从未成为这种架构风格的亮点,并且由于每个业务请求都要分布到架构的多个部分,其性能极差。

由于所有这些因素,简单性和成本之间的关系与大多数架构师的期望相反。这种架构是一个重要的里程碑,因为它教会了架构师在现实世界中分布式事务的复杂性以及技术分割的实际限制。

第17章 微服务架构

历史背景

大多数架构风格是在其使用后期才被命名,而微服务架构是个例外。2014年3月,Martin FowlerJames Lewis 发表了一篇题为《Microservices》的博客文章,使这种新兴架构风格广为人知。他们总结了微服务架构的许多共性特征,并帮助架构师理解其背后的哲学 。

微服务架构深受领域驱动设计(DDD)的启发,尤其是“限界上下文”概念,这代表了一种解耦风格。在限界上下文内,内部部分(如代码和数据模式)是紧密耦合的,但它们从不与外部上下文耦合,这种设计方法旨在最大限度地实现独立性和灵活性​​。

拓扑结构

image

微服务的拓扑结构如图17-1所示。由于微服务的单一功能性质,其服务规模比其他分布式架构(如面向服务架构SOA)要小得多。每个服务都包含操作所需的所有部分,包括数据库和其他依赖组件。这种设计使得每个服务能够独立运行,并减少了系统间的相互依赖​。

微服务架构中的服务通常按领域进行划分,每个服务代表一个特定的业务功能。每个服务的边界应与领域边界一致,这是微服务架构的一个关键特征​。

分布式

微服务架构是一种分布式架构:每个服务都在其独立的进程中运行,最初这意味着运行在物理计算机上,但很快就发展为虚拟机和容器。将服务解耦到这种程度为解决多租户基础设施托管应用的常见问题提供了简单的解决方案。例如,使用应用服务器来管理多个运行的应用程序,可以在网络带宽、内存、磁盘空间等方面实现操作复用。然而,如果所有支持的应用程序继续增长,最终会导致共享基础设施上的某些资源变得紧张。另一个问题是共享应用程序之间的隔离不当。

将每个服务分离到自己的进程中可以解决所有由共享带来的问题。在自由可用的开源操作系统以及自动化机器配置技术发展的早期,为每个领域提供独立的基础设施是不可行的。然而,现在有了云资源和容器技术,团队可以在领域和操作层面上实现极端解耦的好处。

微服务的分布式特性常常带来负面影响,即性能问题。网络调用的时间远比方法调用长,而且每个端点的安全验证增加了额外的处理时间,这需要架构师在设计系统时仔细考虑服务粒度的影响。

由于微服务是一种分布式架构,有经验的架构师建议不要跨服务边界使用事务,因此确定服务的粒度是这种架构成功的关键。

限界上下文

限界上下文(Bounded Context)是微服务架构的核心概念之一,这种哲学强调每个服务建模一个特定的领域或工作流程。在微服务架构中,每个服务都包括了运行所需的一切,如类、其他子组件和数据库模式。这个理念引导了架构师在设计中的许多决策。例如,在单体应用中,开发人员常常在应用的不同部分之间共享通用类,如“地址”类。然而,微服务架构则尽量避免这种耦合,因此构建这种架构风格的架构师更倾向于重复而不是耦合​​。

粒度

架构师在微服务中确定服务的合适粒度时常常遇到困难,并且经常犯的错误是将服务设计得过于细小。这种设计导致需要在服务之间建立大量的通信链接,以完成实际工作。这不仅增加了系统的复杂性,还可能引发性能瓶颈和维护困难的问题。因此,确定适当的服务粒度是微服务架构设计中的一个关键挑战。过于细小的服务可能会破坏服务的独立性,导致频繁的跨服务调用,增加系统的延迟和耦合度。

微服务将领域分区的架构概念推向极致。每个服务旨在代表一个领域或子领域,从某种意义上说,微服务是领域驱动设计中逻辑概念的物理体现。找到服务的适当边界是设计成功的关键,常见的边界选择准则包括:

目的:每个微服务应该具有极强的功能内聚性,提供整体应用所需的一个重要行为。

事务:限界上下文通常反映业务工作流程,通常那些需要在事务中协同工作的实体可以为架构师提供良好的服务边界。

协同:如果架构师设计了一组提供良好领域隔离但需要广泛通信的服务,可能需要将这些服务打包回一个较大的服务,以避免通信开销​​。

数据隔离

微服务架构中的数据隔离也受限界上下文概念的驱动。与其他架构风格通常使用单一数据库进行持久化不同,微服务架构避免所有形式的耦合,包括共享模式和作为集成点使用的数据库。虽然这种程度的数据隔离会带来挑战,但也提供了机会。团队不再被迫统一使用单一数据库,而是可以根据价格、存储类型或其他因素选择最合适的工具。这种极端解耦的系统允许团队在不影响其他团队的情况下更换数据库或其他依赖项​​。

限界上下文的理念确保了微服务架构中的服务具有高度的独立性和低耦合性,避免了共享组件带来的复杂性和协调困难。每个微服务在其限界上下文内是独立的,不与其他上下文共享细节,从而实现了服务的高内聚和低耦合​。

API层

API 层在微服务架构中起着重要的作用,尽管它是一个可选的组件。在典型的微服务图中,API 层通常位于系统的消费者(无论是用户界面还是其他系统的调用)和微服务之间。API 层的存在为执行有用的任务提供了一个良好的位置,例如通过代理的间接访问或与操作设施的集成,如命名服务。

在微服务架构中,API 层不应作为中介或编排工具使用,这与架构的底层哲学一致:所有逻辑都应发生在限界上下文内。在中介中放置编排或其他逻辑违反了这一规则。这也说明了技术分割与领域分割在架构中的区别:架构师通常在技术分割架构中使用中介,而微服务架构则严格地基于领域分割​。

此外,API 层通常用于托管服务发现,提供一个单一的接口,让用户界面或其他调用系统能够一致地找到和创建服务,特别是在涉及弹性扩展的情况下。这种设计允许架构在操作层面实现复用,例如监控、日志记录和断路器等通用操作性问题​。

操作性复用

在微服务架构中,操作性复用是一个需要仔细考虑的问题。传统的服务导向架构(SOA)倾向于尽可能多地复用功能,无论是领域内的还是操作性的功能。然而,在微服务架构中,架构师倾向于将这些关注点分离。

当一个团队构建了多个微服务后,他们会发现每个服务都包含了一些通用的元素,这些元素从相似性中获益。例如,如果一个组织允许每个服务团队自行实现监控功能,那么如何确保每个团队都这样做?以及如何处理升级问题?是否需要每个团队负责升级到监控工具的新版本,这需要多长时间?

为了解决这个问题,微服务架构引入了边车模式(sidecar pattern)。边车是一个独立的组件,它可以由各个团队或共享基础设施团队拥有,处理所有的操作性关注点,如监控、日志记录和断路器等。这意味着,当需要升级监控工具时,共享的基础设施团队可以更新边车,而每个微服务都会接收到这些新功能​。

随着团队意识到每个服务都包含了一个通用的边车,他们可以构建一个服务网格(service mesh),从而在整个架构中实现统一的操作控制,如日志记录和监控。服务网格本质上形成了一个控制台,让开发人员能够全局管理操作性耦合,例如监控级别、日志记录和其他跨领域的操作性问题​。

image image image

前端

在微服务架构中,前端部分也必须考虑解耦。最初的微服务愿景包括用户界面作为限界上下文的一部分,这忠实于领域驱动设计(DDD)的原则。然而,由于Web应用和其他外部限制的实际情况,使得这一目标难以实现。因此,微服务架构中常见的用户界面样式有两种​。

  • 单一用户界面
image

第一种样式如上图所示,是单一的用户界面。它通过API层调用微服务以满足用户请求。前端可以是丰富的桌面应用程序、移动应用程序或Web应用程序。例如,许多Web应用程序现在使用JavaScript框架来构建单一的用户界面​。

  • 微前端
image

第二种用户界面选项是微前端,如上图所示。这种方法利用用户界面级别的组件来创造与后端服务同步的粒度和隔离。每个服务为其功能提供用户界面组件,前端协调这些组件以形成完整的用户界面。使用这种模式,团队可以从用户界面到后端服务隔离服务边界,使整个领域在一个团队内统一​。

通信

在微服务架构中,通信方式是架构设计中的一个关键方面。正确的通信风格有助于保持服务的解耦,同时仍然能够协调地完成有用的工作。微服务架构通常采用同步和异步两种通信方式,各自有其优点和适用场景​​。

同步通信

同步通信要求调用方等待被调用方的响应。通常情况下,微服务架构使用协议感知的异构互操作性,这意味着服务之间需要知道如何相互调用。常见的标准化通信方式包括REST、消息队列等。每个服务在调用其他服务时,必须知道或发现使用何种协议进行通信​。

异步通信

在异步通信中,架构师通常使用事件和消息来实现。这种方式内部利用了事件驱动架构(EDA),其中的Broker和Mediator模式在微服务中表现为编排(orchestration)和协同(choreography)​​。

编排:在编排中,存在一个中央协调者来管理服务之间的调用,适用于复杂的业务流程。在这种模式下,一个单独的服务负责协调调用,管理事务的开始和结束。虽然这种方法可以集中管理协调,但也可能导致服务之间的耦合​​。

image

协同:协同的通信方式类似于Broker模式的事件驱动架构。在协同中,没有中央协调者,每个服务根据需要调用其他服务。这种方法尊重限界上下文的哲学,使得服务之间的事件完全解耦,增加了系统的灵活性​。

  • Using orchestration in microservices
image
  • Using choreography for a complex business process
image
  • an architect may choose to use orchestration for complex business processes
image
通信方式的选择与粒度

在设计微服务架构时,架构师需要决定采用哪种通信方式。同步通信适合于需要立即响应的场景,而异步通信则适用于更松散耦合的系统。服务的粒度也对通信有重大影响:过于细粒度的服务可能导致大量的网络调用,增加性能开销。架构师在设计时需要平衡服务的粒度与系统的性能​​。

事务管理与Sagas模式

在微服务架构中,分布式事务管理是一个复杂的问题。Sagas是一种处理分布式事务的模式,它将一个大事务分解为一系列小的本地事务,每个小事务完成后再触发下一个事务。这样,即使事务的一部分失败,也可以通过回滚已完成的事务来保持系统的一致性​。

image

在发生错误的情况下,如果其中一个部分失败,协调者必须确保事务的任何部分都不会成功。

image

架构特征评估

特性 评级
灵活性 ★★★★★
可扩展性 ★★★★☆
性能 ★★★☆☆
部署性 ★★★★☆
可测试性 ★★★★☆
可用性 ★★★★☆
容错性 ★★★★☆
演化性 ★★★★☆

这种架构的亮点是可扩展性、弹性和演化性。一些最具可扩展性的系统已经成功地使用了这种风格。同样,由于该架构严重依赖于自动化和与操作的智能集成,开发人员也可以在架构中构建弹性支持。由于该架构在增量级别上高度解耦,它支持现代商业实践中的演化性变更,即使在架构层面也是如此。现代商业发展迅速,软件开发一直在努力跟上步伐。通过构建具有极小部署单元且高度解耦的架构,架构师拥有了支持更快变化速度的结构。

微服务中的性能通常是一个问题——分布式架构需要进行大量的网络调用来完成工作,这会带来高性能开销。此外,还必须对每个端点进行身份验证和访问控制检查。为提高性能,微服务领域存在许多模式,包括智能数据缓存和复制,以减少过多的网络调用。

性能问题也是微服务通常采用协同而非编排的原因之一,因为较少的耦合允许更快的通信和更少的瓶颈。

微服务明确是一种以领域为中心的架构,其中每个服务边界应与领域相对应。它也是所有现代架构中具有最明确量子特性的架构之一,在许多方面,它体现了量子度量评估的内容。极端解耦的驱动哲学在这种架构中会带来许多挑战,但如果处理得当,也能带来巨大的好处。正如在任何架构中一样,架构师必须理解规则,以便智能地打破它们。

第18章 选择合适的架构风格

架构风格的“流行”变迁

在软件架构的历史中,架构风格的流行趋势随着时间不断变化。这种变化由多个因素驱动,包括过去的经验、生态系统的变化、新能力的出现、变化的加速、领域变化、技术进步以及外部因素。这一部分探讨了这些因素如何影响架构风格的演变,并给出了实际例子和说明。

过去的经验

新的架构风格通常源于对过去经验和痛点的观察。架构师依赖于他们过去的经验,这些经验帮助他们成为架构师。许多新架构设计反映了过去架构风格中的具体缺陷。例如,在使用重视代码复用的架构后,架构师们重新思考了代码复用的影响,并意识到其中的负面权衡​。

生态系统的变化

软件开发生态系统中的不断变化是一个可靠的特征。变化通常是混乱且不可预测的。例如,几年前Kubernetes还不为人知,但现在它已成为全球多个开发者大会的焦点。然而,未来几年可能会有新的工具出现,取代当前的流行工具​。

新能力的出现

新能力的出现可能会导致整个架构范式的转变。例如,Docker等容器技术的出现对软件开发界产生了重大影响,超出了大多数人的预期。新能力的引入不仅带来了新的工具,还可能引发新的架构范式​。

变化的加速

不仅生态系统在不断变化,而且变化的速度也在加快。新工具催生了新的工程实践,这些实践进一步引导了新设计和新能力的出现。架构师必须在这种持续变化的状态下工作,因为变化是普遍且持续的​。

领域变化

开发者所编写软件的领域也在不断变化,这可能是由于业务的持续发展或公司合并等因素引起的。这些变化影响了架构风格的选择​。

技术变化

随着技术的不断进步,组织尝试跟上这些变化,特别是那些明显具有经济效益的变化。例如,新技术的引入可能会促使组织转向新的架构风格,以提高效率或降低成本​。

####外部因素

许多与软件开发只有间接关联的外部因素也可能驱动组织内部的变化。例如,架构师和开发者可能对某个工具非常满意,但由于其许可成本过高,不得不迁移到其他选项。这些外部因素会影响组织对架构风格的选择​。

决策标准

领域知识

架构师应理解领域中的许多重要方面,尤其是那些影响操作性架构特性的方面。虽然架构师不需要成为特定领域的专家,但他们必须对设计中涉及的主要方面有良好的理解​。

影响结构的架构特性

架构师必须发现并阐明支持领域和其他外部因素所需的架构特性。这些特性决定了架构风格的选择,例如性能、扩展性、安全性等​。

数据架构

架构师和数据库管理员(DBA)需要合作解决数据库、模式和其他数据相关的问题。虽然数据架构是一个专门领域,但架构师必须了解数据设计对整体设计的影响,尤其是在新系统必须与旧有或正在使用的数据架构交互时​。

组织因素

许多外部因素可能影响设计。例如,某些云供应商的成本可能会阻碍理想设计的实现,或者公司可能计划进行并购,这会促使架构师倾向于开放解决方案和集成架构​。

过程与团队的知识

架构师的设计还受到许多特定项目因素的影响,例如软件开发过程、与运营的互动以及质量保证(QA)过程。例如,如果一个组织缺乏成熟的敏捷工程实践,那么依赖这些实践成功的架构风格将面临困难​。

领域/架构的同构性

某些问题领域与架构的拓扑结构非常匹配。例如,微内核架构风格非常适合需要定制化的系统,架构师可以将定制部分设计为插件。另一个例子是基因组分析,它需要大量离散操作,基于空间的架构可以提供大量离散处理器​。

关键决策点

在考虑上述所有因素后,架构师必须做出多个关键决策:

  • 单体架构与分布式架构

架构师必须确定单一的架构特性集是否足以支持设计,或者系统的不同部分是否需要不同的架构特性。单一集意味着单体架构是合适的,而不同的架构特性则暗示分布式架构的必要性​。

  • 数据的存放位置:

    在单体架构中,架构师通常假设使用单一的关系型数据库或几个数据库。在分布式架构中,架构师必须决定哪些服务应该持久化数据,这也意味着要考虑数据在整个架构中的流动以构建工作流​

  • 服务之间的通信方式——同步或异步:

确定数据分区后,架构师接下来的设计考虑是服务之间的通信方式。同步通信在大多数情况下更方便,但可能导致可扩展性、可靠性和其他不良特性。异步通信在性能和规模方面提供了独特的优势,但也带来了数据同步、死锁、竞争条件、调试等问题​。

第19章 架构决策

架构决策反模式

架构决策反模式描述了在做决策时常见的一些错误方法,这些反模式会影响系统的整体质量和团队的效率。以下是一些主要的反模式:

保护资产反模式:这种反模式强调在决策时过度关注自我保护,而不是以项目或团队的最佳利益为重。架构师可能会倾向于选择风险较低的方案,以避免个人责任,而不是选择最合适的解决方案。

土拨鼠日反模式:这个反模式描述了不断重复相同的决策过程,而没有从过去的错误中吸取教训。架构师需要不断迭代和改进决策过程,以避免重复过去的错误。

邮件驱动架构反模式:在这种反模式中,架构决策依赖于非正式的沟通渠道(如电子邮件),而不是通过正式的决策记录和文档化过程。这会导致决策缺乏透明度和可追溯性​。

架构决策的重要性

架构决策对系统的结构和行为有深远影响,以下是几个主要因素:

架构决策的意义:每个架构决策都会影响系统的结构、性能、可维护性等方面。因此,做出明智的架构决策是确保系统成功的关键。

架构决策记录(ADR):ADR 是一种记录架构决策的有效方法。它们不仅记录了做出某个决策的原因,还记录了可选方案和选择某个方案的理由。这种记录方式有助于未来的架构师理解和继承这些决策​​。

架构决策记录

架构决策记录(ADR)是记录和跟踪架构决策的一种方法。以下是ADR的基本结构和使用方法:

基本结构:

标题:包含序号和描述性短语,例如“42. 在订单服务和支付服务之间使用异步消息传递”。 状态:包括“提议中”、“已接受”、“被取代”等状态。 上下文:描述决策的背景和需要解决的问题。 决策:详细描述所做的决策。 后果:描述决策的影响和权衡​。 存储与文档化:建议将ADR存储在Wiki或版本控制系统中,以便团队成员访问和更新。使用ADR记录架构决策可以作为有效的文档化方法,帮助团队成员理解和遵循架构决策​。

示例:在“Going, Going, Gone”案例研究中,使用ADR记录了多个架构决策,例如在服务之间使用发布-订阅消息传递。每个决策都详细记录了背景、选择和理由​​。

反模式的避免

避免架构决策反模式可以提高决策的质量和团队的效率。以下是一些建议:

保护资产反模式的避免:鼓励架构师以项目和团队的最佳利益为重,选择最合适的解决方案,而不是为了自我保护选择风险较低的方案​。

土拨鼠日反模式的避免:通过不断迭代和改进决策过程,吸取过去的教训,避免重复相同的错误​。

邮件驱动架构反模式的避免:采用正式的决策记录和文档化过程,确保决策的透明度和可追溯性​。

第20章 架构风险分析

风险矩阵

架构风险分析的首要步骤是确定风险的分类,即风险是低、中还是高。这通常会涉及到一定的主观判断,导致对于哪些部分是真正的高风险与中等风险存在混淆。为了减少主观性,架构师可以利用风险矩阵来量化风险。

风险矩阵使用两个维度来量化风险:风险的整体影响和发生的可能性。每个维度都有低(1)、中(2)和高(3)三个等级。这些数字在矩阵的每个网格中相乘,提供一个代表风险的客观数值。数字1和2被认为是低风险(绿色),数字3和4被认为是中等风险(黄色),数字6到9被认为是高风险(红色)​​。

风险评估

利用风险矩阵可以构建所谓的风险评估。风险评估是一个总结报告,描述了架构在某种有意义的评估标准下的总体风险。风险评估报告通常包含根据应用的服务或领域区域评估的风险,提供一个全面的风险视图​​。

风险风暴

没有一个架构师能够单独确定系统的整体风险。风险风暴是一种协作活动,用于在特定维度内确定架构风险。常见的风险维度包括未经验证的技术、性能、可扩展性、可用性(包括传递性依赖)、数据丢失、单点故障和安全性​​​。

风险识别

风险识别活动涉及每个参与者单独识别架构中的风险区域。架构师会提前一到两天向所有参与者发送邀请,邀请中包含架构图、风险风暴维度、风险风暴的时间和地点。参与者使用风险矩阵分析架构并分类风险,然后准备颜色对应的小便利贴,写上相应的风险数值​​。

共识达成

共识达成活动是高度协作的,目标是让所有参与者在架构中的风险方面达成共识。参与者在风险风暴会议上将便利贴贴在架构图上,然后共同讨论和分析这些风险区域,达成一致意见​​。

风险缓解 最后的活动是风险缓解,涉及对架构的某些区域进行更改或增强,以降低或消除识别出的风险。这通常会带来额外成本,因此关键利益相关者需要决定成本是否超过风险。通过这种协作,架构师可以找到降低风险的有效方法,并在必要时与利益相关者进行协商​​。

案例研究:风险风暴示例

为了展示风险风暴的实际应用,书中提供了一个支持护士咨询患者健康状况的呼叫中心系统的示例。该系统的要求包括使用第三方诊断引擎、支持全国范围内的250名护士和数十万名自助患者、确保符合HIPAA法规等​。

####可用性风险

在首次风险风暴中,架构师选择关注可用性。参与者识别出以下风险区域:

中央数据库的使用被识别为高风险(6),因为其高影响(3)和中等可能性(2)。 诊断引擎的可用性被识别为高风险(9),因为其高影响(3)和未知的可能性(3)。 医疗记录交换的可用性被识别为低风险(2),因为它不是系统运行所必需的组件​。 参与者建议将单一物理数据库拆分为两个独立的数据库,以降低数据库风险,同时帮助保护病例笔记不被管理员访问。

弹性风险

在第二次风险风暴中,架构师选择关注弹性,即用户负载的波动。参与者担心流感季节的高负载,诊断引擎接口可能无法处理预期的吞吐量。为了缓解这一风险,建议在API网关和诊断引擎接口之间使用异步队列,以提供背压点,并缓存与流行病相关的诊断问题​​。

安全风险

在最后一次风险风暴中,架构师选择关注安全性,特别是确保只有护士能够访问医疗记录。参与者建议为每种类型的用户(管理员、自助诊断和护士)设置单独的API网关,以防止管理员和自助患者的调用到达医疗记录交换接口​。

##第21章 架构图和架构展示

架构图绘制

架构图是表达系统设计的基本工具。它们帮助团队理解系统的结构和组件之间的关系。在绘制架构图时,使用适当的工具和标准至关重要。

工具

绘制架构图的工具有很多,包括但不限于:

Visio:广泛使用的图表绘制工具。 Lucidchart:一种基于云的图表绘制工具,便于协作。 Draw.io:免费且功能强大的在线图表工具。 PlantUML:一种使用简单文本描述UML图表的工具,适合于版本控制和自动化生成​。

图表标准

使用一致的图表标准有助于确保团队和利益相关者能够轻松理解和解释图表。常用的图表标准包括:

UML(统一建模语言):用于软件密集系统的通用建模语言,适用于各种图表类型,如类图、序列图和用例图。

C4模型:一种专注于软件架构的分层模型,包括上下文图、容器图、组件图和代码图。

ArchiMate:一种用于企业架构建模的开放标准,提供了一种描述、分析和可视化企业领域间关系的方法​。

图表指南

绘制有效的架构图时,应遵循一些基本指南:

清晰性:图表应清晰明了,避免不必要的复杂性。

一致性:使用一致的符号和命名约定,确保图表易于理解。

层次化:根据系统的复杂性,使用多层次的图表,从高层次的概览到详细的实现。

关键图例:如果图表中的形状不明确,包含一个图例以清楚地说明每个形状的含义​。

架构展示

架构展示是架构师传达其设计和决策的重要手段。有效的展示能够帮助团队和利益相关者理解架构的关键方面,促进协作和决策。

操作时间

展示工具提供了两种操作时间的方法:过渡和动画。过渡用于幻灯片之间的移动,而动画用于创建幻灯片内的移动。通过适当使用过渡和动画,展示者可以控制观众对信息的接收速度和顺序,增强展示的效果​。

增量构建

增量构建是一种逐步构建幻灯片内容的方法。与一次性展示所有内容不同,增量构建可以帮助观众逐步理解信息,避免信息过载。例如,可以逐步展示图表的各个部分,使观众能够专注于当前讨论的部分​。

信息图和展示

信息图和展示是两种不同的信息传达方式。信息图强调视觉传达,使用图形、图表和简洁的文本来传达信息。而展示则结合了视觉和口头表达,展示者通过口头解释和强调图表内容,提供更全面的信息传达​。

幻灯片只是故事的一部分 在展示中,幻灯片只是故事的一部分。展示者的口头表达、肢体语言和互动也是展示的重要组成部分。通过有效结合这些元素,展示者可以更好地传达信息,吸引观众的注意力​。

架构图绘制和展示的挑战

架构图绘制和展示面临一些挑战,如“子弹 riddled 尸体反模式”。这种反模式是指幻灯片上充满了大量的文字,导致观众难以专注于关键信息。为了避免这种情况,展示者应使用增量构建和简洁的图表,使幻灯片内容更具吸引力和可理解性​。

About

fundamentals-of-software-architecture 学习笔记

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published