
点击上方小坤探游架构笔记可以订阅哦
近期在阅读有关分布式架构、DDD以及微服务相关知识, 今天主要是针对在分布式架构中如何去识别架构中的耦合做一个简单的笔记记录. 其中大部分是来自《Software Architecture: The Hard Parts》提及的概念.
现代架构权衡分析建议
当我们的一个单体系统无法满足其中某一个架构特征的时候, 比如需要支持高并发能力, 高并发意味着我们的系统需要实现三大目标, 即高性能、高可用以及可扩展. 这三大目标一直是我们要构建一个分布式系统架构三大架构特征.
当我们要构建一个不影响用户体验的高性能系统, 那么也就意味着我们需要在整体延迟P99<=200ms才能满足目标. 这个时候当我们单体系统无法满足高性能目标, 我们就会考虑拆分服务, 也就是“分而治之”的手段.
那么在对系统进行拆分之前, 首先我们需要去厘清作用于系统中的各个组件、服务或者是模块之间的联系与权衡. 正如在《Software Architecture: The Hard Parts》一书提到:
One of the most difficult tasks an architect will face is untangling the various forces and trade-offs at play in distributed architectures. 在分布式架构中,架构师面对的最困难的任务之一就是厘清作用于其中的各方势力和权衡。
我们需要的是有一种方法或者框架来帮助我们解决架构中的难题. 在前面的架构思维中我们曾提及到软件架构的解决方案并不存在银弹, 它是我们在网上搜索不到答案的东西, 对于现代架构师而言, 我们必须要要掌握的一项技能那就是懂得如何做权衡分析, 即Trade-Off.
进行权衡分析的第一步就是先厘清问题的维度, 分析系统内部的服务、组件或者是模块之间哪些是紧密联系在一起, 如果我们换用软件中的一个词来表示系统内部的联系, 那便是Coupling, 即耦合.
如何定义Coupliing(耦合)呢? 这里我同样引用《Software Architecture: The Hard Parts》关于耦合的定义:
Two parts of a software system are coupled if a change in one might cause a change in the other. 如果软件系统的两个部分中,一个部分的变更可能导致另一个部分的变更,那么这两个部分就是耦合的。
我们是不是经常这么去思考系统: 如果我们做什么, 都必须要尽可能实现解耦, 正如在网络上看到的, 系统实现松耦合, 将系统变更影响层面达到最小. 但是我们换一个角度去思考: 如果所有的东西都彻底解耦, 那么软件中还如何完成彼此通信来实现软件的目标呢? 就像很多东西一样, 耦合本质上并不坏, 我们需要的是知道如何恰当应用它.
建议松耦合很简单, 但是实际落地细节又是另外一回事, 在《Software Architecture: The Hard Parts》提出现代软件架构的权衡分析建议:
那么如何决定微服务体量和通信方式呢? 服务过小会导致事务和编排问题, 服务过度则会引发扩展和分布问题. 在《Software Architecture: The Hard Parts》提供新的术语来区分相似但不同的模式, 即通过架构量子以及两种类型的耦合, 静态耦合和动态耦合来测量软件架构中各部分之间的连接和通信方式相关的拓扑结构和行为关系.
架构量子
那什么是架构量子呢? 在《Software Architecture: The Hard Parts》有关于架构量子(Architecture quantum) 的定义如下:
An architecture quantum is an independently deployable artifact with high functional cohesion, high static coupling, and synchronous dynamic coupling. A common example of an architecture quantum is a well-formed microservice within a workflow.
翻译过来就是架构量子是一种可独立部署的构件,具有高功能内聚性、高静态耦合性和同步动态耦合性。工作流中设计良好的微服务是架构量子的典型示例。
从上述定义我们识别到架构量子具备四个重要的特征, 即具备可独立部署的组件、具备高功能内聚性、高静态耦合性以及同步动态耦合性. 那怎么理解呢?
可独立部署意味着每个量子都代表特定架构中一个独立的可部署单元, 比如像单体架构是作为一个单元部署的架构, 根据定义它就是单个架构量子. 在我们单体架构内部中采用的架构风格要么是分层架构、要么是模块化架构抑或是微内核架构, 但不论是哪种风格, 它都只能算是单个架构量子, 如果我们通过分数表示软件架构中的量子个数, 那么我们单体架构量子数量均为 1 分(这里我用分数代表数量, 下文也是), 即:

从上述可知, 架构量子得分为 1, 也就是说任何单一部署单元有且有唯一数据库的架构永远只有一个量子, 这其实和我们的微服务定义是类似的, 如果我们换用DDD设计方法, 那么界限上下文服务也可以理解为一个架构量子, 但还是有点区分, 那就是架构量子还会区分耦合类型, 即静态耦合和动态耦合.
我们通过可独立部署的单元来代表架构中的一个量子, 那么采用可独立部署单元去定义我们的架构量子有什么作用呢?
First, the boundary represented by an architecture quantum serves as a useful common language among architects, developers, and operations.
其一架构量子呈现的边界有助于架构师、开发以及运维人员之间形成通用的语言, 彼此建立相同的认知, 有助于不同角色关注架构的问题点, 即架构师关注架构的耦合特征、开发关注行为作用域、运维关注可部署特征.
Second, the architecture quantum represents one of the forces (static coupling) architects must consider when striving for proper granularity of services within a distributed architecture
其次是当架构师在分布式架构中寻求合适的服务粒度时, 架构量子将会是我们必须要考量的因素之一. 因为拆分服务粒度意味着我们的架构要增加对应的架构量子, 而一个新的架构量子反过来又会影响到系统的可独立部署性, 即需要考量发布周期、变更影响以及是否哪些工程实践等进行权衡.
Third, independent deployability forces the architecture quantum to include common coupling points such as databases.
第三点是可独立部署单元强制要求架构量子需要包含通用的耦合点, 比如像上述我们看到数据库, 不论是采用什么样的架构风格, 都包含一个共享的数据库. 怎么理解呢? 比如基于服务的架构, 如果一个基于服务的架构都依赖相同的数据库, 不论其是拆分成多少个服务, 它都是只能算是一个架构量子, 即:

因此如果我们想让上述的架构量子得分为3, 那么相应的service服务都必须要依赖各自独立的数据库耦合点, 由此才能得到3个架构量子, 即:

我们再说回高功能的内聚性, 那么如何去理解我们的内聚性呢? 在《Fundamentals of Software Architecture》有提及到对应的内聚性说明:
Cohesion refers to what extent the parts of a module should be contained within the same module.
内聚性指的是一个模块的各个部分应该在多大程度上包含在同一个模块中。换句话说,它是衡量架构元素之间关联程度的指标。理想情况下,一个内聚的模块是指其所有部分都应打包在一起,因为将它们拆分成更小的部分需要通过模块间的调用来耦合这些部分,以实现有用的结果。
由此可见, 高功能内聚性在结构上意味着架构中的元素, 比如类、组件或者服务等之间的相似性, 计算机科学家定义了一系列内聚性度量标准(按好到坏顺序)如下:

如果单纯按可独立部署的单元来定义架构量子, 一个巨大的单体架构也可以称之为架构量子, 但是它包含了整个系统的所有功能, 因此并不具备高功能内聚性, 即单体越大越不可能内聚.
那么怎样才能让我们的系统实现高功能内聚呢? 在我们微服务架构中DDD的设计方法里, 从领域角度来讲, 它其实就是我们DDD设计方法中的界限上下文, 即实现特定领域的工作流行为和数据, 内聚在这个上下文中不是关于服务如何交互来完成工作,而是关于服务之间如何彼此独立又相互耦合的。
聊完架构量子的可独立部署以及高功能内聚, 接下来我们来聊聊架构量子中重要的两类耦合类型, 即静态耦合和动态耦合.
静态耦合
什么是静态耦合呢? 这里我同样引用《Software Architecture: The Hard Parts》的定义:
Represents how static dependencies resolve within the architecture via contracts. These dependencies include operating system, frameworks, and/or libraries delivered via transitive dependency management, and any other operational requirement to allow the quantum to operate.
静态耦合表示架构如何通过契约解析静态依赖项。这些依赖项包括操作系统、框架和通过传递性依赖项管理发布的库,以及任何启动量子所需的操作需求。
怎么理解呢? 静态耦合主要关注于分析运行依赖, 描述的是服务如何串联起来. 换言之就是要启动这个架构量子, 必须要提前准备好什么? 也就是我们部署阶段的耦合, 不涉及运行时交互.
上述我们讲述到采用权衡分析框架来帮助我们解决架构中的难题, 那么在分布式架构中如果我们要进行拆分, 那么如何拆分便是我们的困难点, 但这个困难点突破点就是我们要识别到分布式架构中的耦合点.那么如何识别呢?
第一是我们系统要正常运作起来, 离不开架构量子中的元素之间的连接, 而架构量子元素之间的连接方式, 要么通过定义协议来实现通信, 比如REST、gRpc或者SOAP等格式, 要么是通过方法签名或者是依赖库实现, 比如像我们在项目中需要引入第三方JAR包来实现一些通用操作, 比如像google的guava、apache的commons等包依赖, 因此我们的耦合点是识别架构量子中元素之间协作的契约.
第二正如我们上述阐述只有一个架构量子的情况, 不论是单体架构还是基于服务化的架构, 它们都是依赖共享的数据库, 可见数据库是以下架构中的静态耦合点, 服务启动需要依赖数据库, 即:


从上述的架构中可以看出, 我们可以通过采用架构量子来度量我们的静态耦合, 由此来帮助我们识别上述架构中的耦合点. 即可以采用架构量子的静态耦合分析来回答这个问题: 当前架构的依赖是否对于架构量子服务启动是必需的.
再比如我现在有一个广告投放服务, 其一是依赖于广告数据服务拉取数据并构建倒排索引, 其二是依赖通用数据查询服务拉取算法推数信息构建算法倒排以及正排索引, 而数据查询服务依赖于Redis, 数据服务依赖于Mysql以及Redis, 因为启动都依赖于它们, 由此它们都是属于广告投放服务的静态耦合点, 如果我们要进行拆分, 那么就需要识别到广告数据查询服务以及通用查询服务的存在, 因为它们都是服务启动必需项,如下:

如果我将上述的通用查询服务, 即查算法倒排索引改成异步方式, 也就是非必需方式, 采用运行时拉取, 那么这个时候架构量子的得分是不是就是2呢? 不一定, 因为如果Redis是共用一个集群, 那么仍然得分仍然是1, 如果是两个Redis集群, 那么得分就是2, 即:

我们还可以再举一个例子, 就是我们熟悉的微服务架构风格, 其主要特征就是服务高度解耦且数据依赖也是一样解耦, 如下:

如果我们采用领域的视角来看待上述的服务, 那么每个Service都可以看成一个界限上下文, 并且每个界限上下文都有自己的一套架构特征, 比如Service1是秒杀系统, 那么其对高性能要求要比其他两个Service更为明显, 再比如Service2是用户系统, 那么其可用性要比其他两个Service要高, 因为用户系统不可用, 那么将无法进行登录完成相应的下单, 支付等操作.
然而如果上述都是用同一个用户界面同步调用, 比如Service1是用户系统、Service2是商品系统、Service3是订单系统, 那么假如进入到一个订单支付页面, 假设页面上既需要展示用户信息, 同时又展示推荐商品, 还有一个订单信息查询, 那么这个用户界面就需要同时分别查询三个service, 如果都聚合在一个API, 相当于一个操作同步调用三个服务, 这个时候由于用户界面的紧密耦合导致我们的架构又降低分数为1的架构量子:

这个时候我们原本松耦合现在又变为紧耦合, 于是在我们的微服务架构又开始演进一种新的方式, 即微前端框架来作为微服务架构里的用户界面, 在这种架构代表服务交互的用户界面元素是从服务本身发起的, 用户界面表面充当了可以显示用户界面元素的画布,并且通常使用事件协助组件之间松散耦合的通信, 如下:

这个时候我们圈起来的服务以及对应的微前端一起构成架构量子, 并且每个架构量子都可以有自己的架构特征.
由此可见, 我们架构中任何的耦合点都可能创造静态耦合点, 正如下面原本两个系统互不干扰且相互独立, 但是由于引入了集成数据库, 导致整个架构中创造了一个新的架构量子:

至此我们通过上述了解了静态耦合的识别方式, 我们可以通过架构量子元素的契约以及启动同步依赖项来识别我们的静态耦合, 但它仅是分布式系统耦合的一部分,还有一部分是接下来我们将要阐述的动态耦合.
动态耦合
关于动态耦合, 在《Software Architecture: The Hard Parts》中是这样定义:
Represents how quanta communicate at runtime, either synchronously or asynchronously. Thus, fitness functions for these characteristics must be continuous, typically utilizing monitors.
动态耦合表示量子在运行时如何通信, 是同步还是异步, 因此对于动态耦合的架构特征我们一般需要通过增加监控来保证我们系统的fitness functions(适应度函数)是持续的.相比静态耦合而言, 动态耦合描述了运行时服务如何相互调用, 因此动态耦合主要分析通信依赖.
在这里我们通过上述的定义遇到了一个新词, 即Fitness Functions, 这里我简称为FF,那么什么是FF呢? 这里按我的理解总结如下:
Fitness function 是把架构特征进行“可测量化”的一种手段。它把架构目标(比如性能、可用性、安全、模块化、可部署性等)转化为可执行的、自动化的度量或测试:给出一个度量方法、一个判定阈值,以及触发位置(CI、监控、混沌实验等)。
也就是说它一套可执行的检查机制, 我们用它来衡量我们的架构特征是否满足系统的要求, 比如说我们要测量性能延迟是否满足在P99 < 200ms 条件下才能保证系统不影响用户体验, 那么此时我们测量RT的FF可以是增加监控, 或者是增加日志埋点来统计得到我们的RT对应的P99延迟.
由此我们言归正传, 动态耦合主要包含以下三个维度, 而这三个维度也将是我们分布式Saga事务的9种不同的变体, 即:


Communication: Refers to the type of connection synchronicity used: synchronous or asynchronous.
第一是通信方式, 也就是我们所说的服务同步调用还是异步调用;
同步调用意味着需要等待接收者响应, 期间调用服务需要阻塞直到接收者返回值, 即成功、失败或者是错误, 即:

异步调用意味着调用方发起调用之后无需一直接收者的响应就继续往下执行工作,如果需要接收者返回数据值那么就会使用一个异步队列告知调用者结果, 一般我们画架构图会省去MQ这类的细节, 即下图的第2种方式, 如下:

Consistency: Describes whether the workflow communication requires atomicity or can utilize eventual consistency.
第二是一致性, 表示我们工作流通信是要实现原子性还是最终一致性, 注意这里的一致性和我们先前讲述的一致性区分, 这里的一致性主要是事务的完整性, 即发生异常是采用原子性方式立即回滚(在整个请求保持一致性), 还是执行事务期间发生异常之后我们采用事务补偿机制弥补损失.
Coordination: Describes whether the workflow utilizes an orchestrator or whether the services communicate via choreography.
第三是协调, 描述的是我们执行的工作流是采用编排方式还是分散协作方式进行通信.
由此我们通过动态耦合的三个因素, 即通信、事务的一致性以及服务协调方式来识别我们软件架构的动态耦合, 从而帮助我们在进行架构设计做好每个特征的权衡分析, 有助于我们更好的进行架构决策.
架构量子与耦合价值分析
架构量子的关键价值
因此当我们拆分微服务的时候, 需要记住一个原则, 即任何使用共享的数据库由于无法独立部署都无法形成独立的架构量子, 例如两个服务共享“用户数据库”,若其中一个服务修改用户表结构,另一个服务必须同步适配,二者因共享数据库被绑定为“一个量子”,违背了微服务“解耦”的核心目标。
静态耦合的分析价值
即我们要进行服务拆分的时候, 首先要重构为一个独立的架构量子, 这也就意味着我们要先解除静态耦合.
动态耦合的分析价值
当我们拆分服务之后, 我们需要建立适应度函数持续观察我们动态耦合的架构特征, 以便于后续针对对应的架构量子进行优化.
总结
当我们不知道如何进行服务拆分的时候, 那么这个时候架构量子的术语为我们提供了支撑, 而一个完整的架构量子不仅仅是可独立部署的单元、功能的高内聚, 同时也具备高静态耦合以及同步动态耦合等特征, 基于此, 我们利用反向思维角度来识别我们架构的耦合点, 即通过架构量子这一个工具间接识别静态耦合与动态耦合.
而静态耦合我们需要考量一个架构量子从头开始搭建服务需要什么, 那么它就是我们的静态耦合关注点, 即它包含了启动运行所需要的所有依赖项, 对此我们也就静态耦合的关注点提出了要求: 一是构建依赖清单, 或者我们所说的上线清单; 二是如果我们改动了依赖项, 通过静态耦合关注点我们就能够清楚我们必需要测试什么, 即变更的影响面.总结起来就是静态耦合是描述架构量子内部元素是如何连接起来的, 也确定了运维架构特征的范围.
对于动态耦合, 它关注是架构量子彼此之间的通信、协调与系统数据事务的完整性以及如何影响运维架构特征的, 比如性能、弹性、可伸缩性等架构特征, 它关注的是量子之间的协作方式并各自承担自己的目标一同完成整个系统的目标.
最后我们要如何基于耦合分析做架构决策: 第一是拆分前提我们先要识别静态耦合, 确保量子可独立部署; 其二是优先降低动态耦合, 减少运行时风险并建立起对应的适应度函数持续监控; 最后是不追求完全耦合, 而是耦合与价值匹配.
你好,我是疾风先生, 主要从事互联网搜广推行业, 技术栈为java/go/python, 记录并分享个人对技术的理解与思考, 欢迎关注我的公众号, 致力于做一个有深度,有广度,有故事的工程师,欢迎成长的路上有你陪伴,关注后回复greek可添加私人微信,欢迎技术互动和交流,谢谢!