程序员架构修炼:架构思维的分解与集成

本文探讨了软件架构的设计过程,强调了分解和集成的重要性,区分了组成派和决策派,介绍了高内聚低耦合等原则,并详细讨论了在微服务架构中如何进行数据服务设计、处理数据一致性问题和分布式查询,包括事件驱动架构和命令查询责任分离的运用。
摘要由CSDN通过智能技术生成

程序员架构修炼:架构思维的分解与集成

软件架构是一个系统的草图,描述了组成架构的组件及各个组件之间的关系,组件和环境之间的关系,以及设计组件的原则。组件可以是子系统、模块、类、方法等。

架构设计是架构决策的过程,涉及系统分解、接口定义、通信协议定义、交互关系和集成方式确定。架构决策指在架构设计中统筹全局并做出决定、权衡和取舍,比如将系统拆分为几个子系统,子系统的职责是什么,子系统之间如何交互,如何调用和采用什么集成机制,使用什么开发语言和技术框架。

软件架构根据定义可分为组成派和决策派。

(1)组成派。软件架构是计算机组件与组件之间的交互,有如下两个显著的特点。

◎ 关注软件本身,以软件本身为描述的对象。

◎ 通过分析软件的组成来说明软件不是一个简单的整体,而是由不同的组件通过对接口的约定连接成的一个整体。

(2)决策派。指软件架构包含一系列决策,如下所述。

◎ 软件系统的组织。

◎ 选择组成系统的组件和组件之间的接口,以及组件之间协作时的所有行为。

◎ 如何将这些组件集成在一起,形成更大的子系统。

分解在软件架构中,分解是一种非常重要的手段,是“分而治之”思想的体现。“分而治之”是一种处理复杂问题的通用方法,能保证分解后的各个部分高内聚、松耦合,最终集成为一个整体。多层架构、OSI七层模型等都体现了“分而治之”的思想。在架构设计过程中,会通过将关注点分离对架构进行多层次分解,将系统层层分解为多个架构元素,进而识别架构元素。可以说,架构中的很多元素都是在架构分解的过程中产生的。

解的作用

万事开头难,架构分解是架构设计过程中非常关键的一步。除了识别架构元素,对于大型软件系统而言,分解还是解决非功能需求的重要手段。为了解决可伸缩性、可用性和可管理性等问题,可在架构的多个层面进行分解。

(1)在应用层面,按照功能或SOA服务进行分解,将系统垂直拆分为多个应用池(应用池中的服务是无状态的),在每个应用池中都有多个应用(水平拆分),可以独立、灵活地进行伸缩。

(2)在数据层面,对数据进行垂直拆分(分库)和水平拆分(即数据分片,DB Sharding),将分布式事务拆分成多个本地事务独自提交,避免分布式事务。

分解的原则

德国哲学家、数学家莱布尼茨一针见血地指出:“不讲究分解技巧,分而治之的作用就不大。无经验者对问题分解不当,反而增加了解决问题的困难。”为了正确地进行分解,需要遵循一些原则,如下所述。

(1)高内聚、低耦合。莱布尼茨指出:“分解的主要难点在于怎么分。分解策略之一是按容易求解的方式来分,之二是在弱耦合处下手,切断联系。”高内聚、低耦合也是软件设计的基本原则,其实,我们可以将软件设计中的很多设计原则都看作它的派生或具体化,例如单一职责原则、依赖倒置原则和模块化封装原则,这些原则在架构分解中也是适用的。

(2)层次性。分解通常是先业务后技术,循序渐进,先逻辑后物理,从上到下逐级进行分解和展开,一般是系统→子系统→模块→组件→类。

(3)正交原则。和物理学中的正交分解类似,架构分解出的架构元素应是相互独立的,在职责上没有重叠。

(4)抽象原则。架构元素识别在较大程度上是架构师抽象思维的结果,架构师应该具备在抽象概念层面进行架构构思和架构分解的能力。

(5)稳定性原则。将稳定部分和易变部分分解为不同的架构元素,稳定部分不应依赖易变部分。根据稳定性原则,将通用部分和专用部分分解为不同的元素;将动态部分和静态部分分解为不同的元素;将机制和策略分离为不同的元素;将应用和服务分离。

(6)复用性原则。就是对之前成果的复用,复用类似的架构设计、分析设计、开源框架、设计模式、数据结构、领域模型、架构思想等,已有的这些成果已经在不同纬度上分解并识别出了很多架构元素、或指出了分解的方式和方法,可以帮助我们快速分析和分解现有需求和架构。

分解的时机

架构分解的时机通常就是架构改造和演变的时机。架构在腐化时,已经难以满足关键场景的关键需求,例如对用户请求的响应速度越来越慢,已经接近临界值,并且根据经验,响应速度还有可能继续降低,越来越难以维护,这时可以考虑进行架构演变,对架构进行改造和分解。当然,如果能提前预见系统的问题,在经过慎重评估后,提前进行架构演变,则也是可以的。注意,不要过早分解、过度分解,这样做除了增加成本,还可能带来风险。例如,很多系统在建设初期考虑到规模较小和快速上线,通常都会打造一个整体的系统,不会进行大的架构分解,之后随着需求和规模的逐渐增大,会逐步进行架构改造和架构分解。

集成

集成是配合分解完成的动作。分解完成的各个组件或子系统,通过合适的接口设计,最终还能够集成为一个完整的整体。分解的目的仅仅是加速开发和降低问题的复杂度,如果分解后的内容无法集成在一起,分解就没有任何意义。我们可以将“分解+集成”理解为架构的

核心思考方式和方法。

常见的集成方式

在SOA体系下,服务之间通过ESB通信,许多业务逻辑存在于中间层(消息的路由、转换和组织)。

微服务架构倾向于减少中心消息总线(类似于ESB)的依赖,将业务逻辑分布在每个具体的服务终端。大部分微服务基于 HTTP、JSON这样的标准协议,集成不同的标准和格式变得不再重要;并且,在微服务架构中还可以采用轻量级的消息总线或者网关,有路由功能,没有复杂的业务逻辑。

下面介绍几种常见的集成方式。

1.点对点方式

在点对点方式中,服务之间直接调用。每个微服务都开放RestAPI,并且调用其他微服务的接口,如图4.1所示。

图4.1

将这种方式应用于简单的微服务架构是完全可行的,但是当微服务架构越来越复杂时,这种方式就不适用了。这有些类似于 SOA 的ESB,所以我们尽量不采用点对点的集成方式。

2.API网关方式

API网关方式的核心要点是,所有客户端和消费端都通过统一的API网关接入微服务,在API网关层处理所有的非业务功能。通常,API网关也提供 Rest、HTTP的访问API,服务端通过API-GW注册和管理服务。

举个例子,在某个项目中,所有业务接口都通过API网关暴露,是所有客户端接口的唯一入口,微服务之间的通信也通过API网关进行,如图4.2所示。

图4.2

可以看出,API网关方式有如下优势。◎ 为微服务的接口提供一个统一的抽象网关层,比如微服务提供的接口有很多类型,在网关层提供了统一规范的接口。

◎ 轻量的消息路由、格式转换功能。

◎ 统一控制安全、监控、限流等非业务功能。

◎ 每个微服务都变得更加轻量,非业务功能都在网关层统一处理,微服务只需要关注业务逻辑。

3.消息代理方式

微服务也可以通过异步方式来集成,通过队列和订阅主题实现消息的发布和订阅,一个微服务可以是消息的发布者,将消息通过异步方式发送到队列或者订阅主题下。作为消费者的微服务可以从队列或者主题中获取消息,通过消息中间件将服务之间的直接调用解耦,如图4.3所示。

图4.3

如图4.3所示,异步的生产者/消费者模式通常通过AMQP、MQTT等异步消息进行规范。

集成的难点

微服务架构变得越来越流行了,还成为模块化的一种方法,将一整个应用拆分成一个个服务,让团队在开发大型复杂系统时,能更快地交付高质量的项目,并且可以轻松接受新技术,因为团队成员可以使用最新且流行的技术栈实现各自的服务,微服务架构也通过让每个服务都被部署在最佳状态的硬件上来改善应用的扩展性。

但是,在微服务集成过程中,我们需要迈过三道难关:数据服务设计;数据的一致性;分布式查询。不同于传统的单体架构、基于ESB的SOA系统的架构,在微服务化过程中产生了许多新问题,许多应用大到一个人根本无法完成,而且复杂到只靠一个人都无法理解。

在这种情况下,应用就必须被拆分成一个个模块。在单体应用中,模块被定义为一个Java包,这种做法在实践中效果并不很理想,时间长了,单体应用会变得越来越庞大。微服务架构将服务作为一个模块单元,每个服务都有一个不可渗透且很难逾越的边界,即每个微务都要提供一种单独且独立的能力,这样的话,应用程序的模块化就更容易随时保存。

接下来看看怎么迈过以上所述的三道难关。

1.数据服务设计

通用架构是不存在的,所以我们需要了解微服务架构下的几个关键考量点,然后针对自己的实际应用选择更重要的考量点。下面讨论从哪几个角度着手设计一个符合微服务架构原则的数据架构。

◎ 是否可以用一个数据库或者多个数据库来支持多个微服务?

◎ 如果有多个数据库,则是否需要为每个微服务都挑选一个最合适的数据库,还是选择同一种类型的数据库?

◎ 如何在微服务架构下扩展数据库?

◎ 当一个依赖的服务需要修改数据库Schema的时候,是否会影响到现有系统?

◎ 当微服务应用不断演变的时候,数据库是否可以快速响应应用需求的变化?在数据服务设计遇到以上问题时,我们可以通过下面的手段来解决。

1)采用一库一服还是一库多服

无论是单体应用还是微服务应用,有一点是肯定的:在应用的各个模块之间都需要进行较为频繁的通信,通过协同合作来实现应用的整体价值。在单体应用中,这种通信是通过方法调用来完成的,在微服务中则是通过API调用来完成的。这些模块或者服务间的调用,在大多数时候是为了共享数据。共享数据的最简单方式就是采用一种共享数据库的模式,也就是单体应用常用的方式:应用可以有多个系统模块,但一般只有一个数据库。

这种架构模式通常被认为是微服务架构下的反范式,它的问题如下。

◎ 单点故障,一个数据库倒下,整批服务全部停止。

◎ 数据在同一个地方,随着需求量的增加,数据间的依赖越来越复杂。

◎ 无法针对某个服务进行精准优化或扩展。

所以一般推荐的做法,是为每个微服务都准备一个单独的数据库,即一库一服(DataBase Per Service)模式,这种模式更加适合微服务架构:它满足每个服务都是独立开发、独立部署、独立扩展的特性。当需要对一个服务进行升级或者数据架构改动的时候,无须影响到其他服务;当需要对某个服务进行扩展的时候,也可以手术式地对某个服务进行局部扩容。另外,如果某些服务对数据库有特殊的需求 , 则 在 这 种 模 式 也 为 下 文 所 讲 的 混 合 持 久 化 ( PolyglotPersistence)提供了可能性。

2)混合持久化混合持久化在大型互联网公司中是比较流行的模式,它秉承的原则就是为特别的任务提供最好的工具。比如,如果某个系统提供一个高并发、低延迟的共享用户会话方案(Shared Session Storage),则Redis可能是一个非常理想的选择;如果实现一个产品目录,涉及大量不确定结构的商品数据及属性的建模管理,则系统可能会采用模式灵活、有动态Schema的MongoDB作为数据库解决方案;如果系统希望支持非常强大的全文搜索能力,则可以采用Elasticsearch。

混合持久化的优势很明显,可以让每个单独的服务都使用最佳的工具和技术,但它的弊端也是不容忽视的。

◎ 部署、监控、备份、升级等都是运维工作中不可或缺的内容。

◎ 引入多个不同的数据库,也意味着系统管理与维护的复杂度、成本提高了很多,在这种情况下,比较有资源的公司或者团队才可以采用混合持久化模式。

这也解释了混合持久化模式在大型互联网公司中得到较多采用与推广的原因。对于其他小规模用户,或者缺乏掌握各种新型技术人才的公司来说,另一种更为可行的模式可能是多模数据库(Multimodel)。

3)多模数据库

多模数据库的特征是:

◎ 依然是一库一服务(为一个服务部署一个单独的数据库);

◎ 采用可以支持多种应用场景的数据库;

◎ 虽然是多实例模式,但属于同一种类型的数据库,人员组成和维护管理都比较简单。

如果正在开发的应用是一款企业级产品,会将其交付到客户环境下安装、部署,则运维管理的简单性将在技术选型中占据非常大的比重,在这种情况下,多模数据库更加适用。

2.数据的一致性

单体应用可以通过ACID事务来实现数据的一致性。比如个人消费金融系统的用户都有信用额度,应用程序在创建订单前会先看用户的信用额度如何,必须保证在有多个并发尝试创建订单时不超过客户的信用额度。如果订单表和用户表都在同一个库中,就可以通过ACID事务来完成。但是在微服务系统中无法通过这样的方式来实现数据的一致性,因为订单表和用户表都在各自的应用服务中,只能通过各自的服务API访问,它们甚至可能存在于不同的网络中。

比较常见的做法是使用分布式事务来完成,比如2PC(二阶段提交)等,但这对于现在的应用来说可能并不可行。由于系统设计要遵循 CAP 定理,所以系统必须在可用性和一致性之间进行选择,可用性通常是较好的选择,而且现在的很多技术,例如大多数NoSQL数据库甚至不支持ACID事务,更不用说2PC。所以在实现数据的一致性时要采用其 他 方 式 。 我 们 使 用 事 件 驱 动 架 构 中 的 一 种 事 件 源 ( EventSourcing)技术来应对数据一致性的问题。

事件源是通过使用不同的事件中心来获得不需要2PC的原子性,保证业务对象的一致性。这种应用存储业务对象的一系列状态改变事件,而不是存储对象现在的状态。应用可以通过重放事件来重建对象的现在状态,只要业务对象发生了变化,新事件就会被添加到时间表中。因为保存事件是单一操作,因此肯定是原子性的。

为了理解事件源的实现原理,我们考虑将事件对象作为一个例子。在传统方式中,每个订单都被映射为订单表中的一条记录,例如订单商品表。对于事件源方式,订单服务基于事件状态的改变来存储一个订单,比如创建订单、待支付、已支付、待发货、已发货、取消订单,每个事件都包括足够的数据来重建订单状态。

事件状态被长期保存在事件服务的数据库中,通过API添加和获取实体事件的功能列表。事件存储和之前描述的消息代理模式类似,通过API来订阅事件。事件存储将事件传送给对事件感兴趣的所有订阅者,是事件驱动微服务架构的基础。

事件源的优点如下。

◎ 解决了事件驱动架构的关键问题,使得只要有状态变化就可以可靠地发布事件,也就解决了微服务架构中数据的一致性问题。

◎ 因为是持久化事件而不是对象,所以避免了面向对象编程与关系型数据库间的不一致性。

◎ 事件源方法提供了100%可靠的业务实体变化监控日志,使得获取任何时点的实体状态都成为可能。

◎ 事件源方法可以使业务逻辑由事件交换的松耦合业务实体构成。

以上的优点保证了将单体应用移植到微服务架构变得相对容易。

事件源缺点如下。

◎ 因为采用了不同的或者人们不太熟悉的编程模式,所以重新学习其编程模式不太容易。

◎ 事件存储只支持主键查询业务实体,必须使用命令查询的责任分离(Command Query Responsibility Segregation)来完成查询业务,应用需要通过最终一致性来保证数据的一致性。

3.分布式查询

在单体系统中一般使用 Join语句实现多表查询。比如,可以通过SQL语句轻松查询出客户最近所订的大额订单。但是,我们无法在微服务架构中实现这样的查询。就像前面提到的那样,Orders和Customers表分属不同的服务,只能通过服务API来访问,而且它们可能使用了不同的数据库,使用事件源更新和查询问题可能更麻烦。

使用命令查询的责任分离模式是实现查询的最好方法。命令查询的责任分离模式将应用程序分为如下两部分。

(1)命令侧(command-side)。处理命令(例如HTTP POST、PUT和DELETE)可以创建、更新和删除聚合,前提是这些聚合是使用事件源实现的。

(2)查询侧(query-side)。通过查询聚合的一个或多个物化视图来处理查询(例如HTTP GET),通过订阅由命令侧发布的事件保持视图与聚合同步。查询侧的视图可以使用任意类型的能满足需求的数据库来实现。根据需求,应用程序的查询侧可能使用一个或多个数据库。

在很多场合下,命令查询的责任分离模式都是一个以事件为基础的综合体,比如使用RDBMS作为记录系统,使用Elasticsearch查询文本,查询侧可以使用其他类型的数据库,支持多种类型的数据库,不仅仅是文本搜索引擎,而且通过订阅事件准实时地更新查询侧的视图。

命令查询的责任分离模式的优点如下。

◎ 在微服务架构中实现了查询,特别是使用事件源的架构,使应用程序有效地支持一组不同的查询。

◎ 将命令侧和查询侧分离,达到了解耦的目的。命令查询的责任分离模式的缺点如下。

◎ 需要额外的工作来开发和维护这套系统,需要开发和部署更新视图、查询视图的查询端服务,还需要部署视图数据库。◎ 在命令侧和查询侧的视图之间有“滞后”。查询侧与命令侧存

在一定的时延,在数据更新、聚合后,查询到的可能依然是聚合之前的数据,这时就需要通过其他手段强制更新,使查询侧和命令侧的数据保持一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值