软件架构入门-分层架构、事件驱动、微服务架构和云原生架构
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件架构入门-分层架构、事件驱动、微服务架构和云原生架构相关的知识,希望对你有一定的参考价值。
参考技术A
软件架构(software architecture)就是软件的基本结构。
合适的架构是软件成功的最重要因素之一。大型软件公司通常有专门的架构师职位(architect),只有资深程序员才可以担任。
O\'Reilly 出版过一本免费的小册子《Software Architecture Patterns》(PDF), 介绍了五种最常见的软件架构,是非常好的入门读物。
软件架构就是软件的基本结构。架构的本质是管理复杂性。 如果你觉得架构不重要,可能是你做的事情不够复杂,或者是你没有管理好复杂性。架构模式虽多,经过抽象沉淀之后,也就那么几种:
1. 分层架构(比较传统的单体架构)
2. 事件驱动架构 (一般适用于应用局部场景,用来实现异步解耦)
3. 微核架构(又称插件架构,开发难度较高,一般用来做工具软件开发,如Eclipse,不太适合分布式业务场景)
4. 微服务架构(当前比较流行的服务化架构,解决单体架构面临的问题,适合敏捷开发,快速迭代)
5. 云架构(现在的说法是云原生架构-Cloud Native,基于Docker、Kubernetes、Service Mesh 云原生架构)
在原文的基础上,我按照自己的想法,进行了小幅调整。
分层架构( layered architecture )是最常见的软件架构,也是事实上的标准架构。如果你不知道要用什么架构,那就用它。
这种架构将软件分成若干个水平层,每一层都有清晰的角色和分工,不需要知道其他层的细节。层与层之间通过接口通信。
虽然没有明确约定,软件一定要分成多少层,但是四层的结构最常见。
有的软件在逻辑层(business)和持久层(persistence)之间,加了一个服务层(service),提供不同业务逻辑需要的一些通用接口。
用户的请求将依次通过这四层的处理,不能跳过其中任何一层。
优点
缺点
事件(event)是状态发生变化时,软件发出的通知。
事件驱动架构(event-driven architecture)就是通过事件进行通信的软件架构。它分成四个部分。
事件驱动架构(event-driven architecture)核心组件:
对于简单的项目,事件队列、分发器和事件通道,可以合为一体,整个软件就分成事件代理和事件处理器两部分。
优点
缺点
事件驱动架构在通信产品中应用得也非常广泛,典型的如状态机处理。 事件驱动架构不适于做顶层架构,但适合做局部实现,几乎遍布在通信软件的各个角落。
微核架构(microkernel architecture)又称为"插件架构"(plug-in architecture),指的是软件的内核相对较小,主要功能和业务逻辑都通过插件实现。
内核(core)通常只包含系统运行的最小功能。插件则是互相独立的,插件之间的通信,应该减少到最低,避免出现互相依赖的问题。
优点
缺点
微核架构的设计和开发难度较高,这就注定它在企业产品中用得不多,虽然它的优点还不少。
微服务架构(microservices architecture)是服务导向架构(service-oriented architecture,缩写 SOA)的升级。
每一个服务就是一个独立的部署单元(separately deployed unit)。这些单元都是分布式的,互相解耦,通过远程通信协议(比如REST、SOAP)联系。
微服务架构分成三种实现模式。
现在开源的微服务框架比较多,如常用的有Spring Cloud、Dubbo、ServiceComb等等。
优点
缺点
云架构(cloud architecture,现在的说法是云原生-Cloud Native)主要解决扩展性和并发的问题,是最容易扩展的架构。
它的高扩展性,主要原因是可以基于云上计算资源弹性伸缩。然后,业务处理能力封装成一个个处理单元(prcessing unit)。访问量增加,就新建处理单元(Docker容器);访问量减少,就关闭处理单元(Docker容器)。由于没有中央数据库,所以扩展性的最大瓶颈消失了。由于每个处理单元的数据都独立分库。
这个模式主要分成两部分:处理单元(processing unit)和虚拟中间件(virtualized middleware)。
虚拟中间件又包含四个组件:
随着Docker、Kubernetes等容器化技术的快速发展,上述关于云架构描述有点陈旧了。当前最新的云原生架构,以Docker+Kubernetes为核心,尤其是容器编排Kubernetes 已经成为事实上的行业标准。
云原生架构图的主要特征:
主要目标:
1. 让开发人员聚焦业务逻辑的实现,其他交给容器云平台来完成;
2. 支持业务系统的快速迭代,支撑业务的快速变化和发展;
3. 构建以共享服务体系为核心的业务中台;
下面是我针对某新零售企业设计的云原生架构图,以云和微服务架构为基础构建云原生应用,这里云可以是公有云、私有云、混合云等等。
以上是从不同的视角,对架构进行了分类。实际应用中,各种架构并不是孤立的,可以根据业务环境和业务诉求,对各种架构进行综合和嫁接。每种架构都有其优点和缺点。优点不必多说,缺点则几乎都是通过工具工程(比如自动化发布工具、自动化测试等等)能力的方法来规避,工具工程对软件架构非常重要。
微服务架构之事件驱动架构
前言
为了解决传统的单体应用(Monolithic Application)在可扩展性、可靠性、适应性、高部署成本等方面的问题,许多公司(比如Amazon、eBay和NetFlix等)开始使用微服务架构(Microservice Architecture)构建自己的应用。
微服务架构():
微服务 (Microservices) 是一种软件架构风格 (Software Architecture Style),它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模组化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通讯。
但是,微服务架构在带来一系列好处的同时,也带来了若干挑战。除了分布式系统固有的复杂性以外,微服务架构也深刻影响了应用和数据库之间的关系,与传统多个服务共享一个数据库的方式不同,微服务架构每个服务都有自己的数据库。对于开发者来说,这就为微服务中的数据管理提出了更高的要求。
微服务架构中的数据管理
在传统的单体应用中,通常使用单个的关系型数据库。这类数据库所提供的事务语义,具备特性。
ACID:
Atomicity(原子性):一个事务中的操作是原子的,其中任何一步失败,系统都能够完全回到事务前的状态
Consistency(一致性):数据库的状态始终保持一致
Isolation(隔离性):多个并发执行的事务不会互相影响
Durability(持久性):事务处理结束后,对数据的修改是永久的
应用得益于数据库的这些特性,能够用简单的方式对数据进行修改与读取,而无需花费太多精力考虑数据一致性问题。
但是,在微服务架构下,为了在微服务之间建立松耦合的关系,通常每一个微服务都会拥有自己独立的数据库,仅仅通过对外暴露的API来进行数据交换。这种情况下,我们就要面临分布式数据管理带来的挑战。也就是说,在实现业务逻辑时,如何保证服务之间的数据一致性。
实时一致性
我们首先考虑在系统中实现实时一致性的情况。比如以一个银行系统为例,客户通常会有一个储蓄账户和一个理财账户。现在,考虑客户从自己的储蓄账户向理财账户转账10000元的场景。
假设现在有两张表 deposit_account 和 finance_account,分别用于存储储蓄账户和理财账户的信息,用户的ID是201。那么,在单一数据库场景下,通过数据库事务可以很容易完成这个操作:
Begin transaction update deposit_account_table set amount=amount-10000 where userId=201; update finance_account amount=amount+10000 where userId=1;
End transaction
commit;
这样在单体应用中,由于所有数据都是保存在同一个数据库中,通过数据库提供的ACID特性,就可以轻松实现数据的实时一致性。
但是,在微服务架构中,可能的设计是存在两个服务:储蓄服务(Deposit Service)和理财服务(Finance Service),假设由储蓄服务负责处理客户的转账请求。而如下图所示,这两个服务都分别维护自己的数据,因此储蓄服务无法直接访问理财服务的数据,而只能通过API去修改客户的余额。
此时,为了满足订单服务与客户服务之间的实时一致性要求,可以采用分布式事务,比如基于的实现来做到这一点。(关于2PC,已经有大量的研究成果和成功实践经验,本文将不再做太多阐述,具体可自行参见相关文献和资料)
根据,我们追求实时一致性时,通常需要牺牲掉部分可用性。比如以上场景中,当 Finance Service 由于软硬件故障或网络问题而不可用的时候,系统将无法为用户提供内部转账服务。
此外,作为典型的同步操作,2PC也存在着比较比较严重的性能问题,并不适合高并发场景。因此,在数据一致性上我们需要寻求其他的解决方案。
最终一致性
如果我们考虑只保证系统的最终一致性,那么就可以避免使用2PC,从而提高系统可用性和性能。
仍然以以上的用户内部账户之间的转账服务为例。当用户从储蓄账户向理财账户转账时,减少储蓄账户的金额与增加理财账户的金额这两个动作,可以无需在一个事务里面完成,而是分成两步:
储蓄服务减去储蓄账户中的金额,并生成一个凭证(消息)发送给理财服务;
理财服务收到凭证后,在理财账户中增加相应的金额。
我们会发现以上过程在第1步完成之后,第2步完成之前,储蓄账户与理财账户之间实际上是存在短时间的数据不一致的。但是,只要最终第2步能够完成,系统的数据就仍然能够保持一致性,这就是我们所说的最终一致性。
在最终一致性这个前提下,即使理财服务在某段时间内不可用,系统仍然能够能为用户提供内部转账服务,从而提高了系统的可用性。
而这样一种基于最终一致性的解决方案,就是本文将要介绍的事件驱动的架构(Event-driven Architecture)。
事件驱动的架构
所谓事件驱动的架构,也就是使用事件来实现跨多个服务的业务逻辑。
在这一架构里,当有重要事件发生时,比如更新业务数据,某个微服务会发布事件,其它微服务则订阅这些事件;当某一微服务接收到事件就可以更新自己的业务数据,同时发布新的事件触发下一步更新。而事件的发布与订阅,则依赖于一个可靠的消息代理(Message Broker)。
以上文的场景为例,在事件驱动的架构中,从储蓄账户转账到理财账户的过程如下:
储蓄服务将用户的储蓄账户中的金额减少10000,并发布“向理财账户转账”事件;
理财服务获取“转账到理财账户”事件, 更新理财账户,将理财账户的金额增加10000,并发布“理财账户转入”事件;
储蓄服务获取“理财账户转入”事件,结束本次转账交易。
在这里需要考虑的一个问题,就是转账失败处理。比如以上第2步如果因为“理财账户被冻结无法转入资金”之类的原因失败了,理财服务就应该发布“理财账户转入失败”事件,储蓄服务获取到该事件后,需要对储蓄账户进行回滚,将减少的金额重新增加回去。
以上的过程与传统的数据管理基于ACID模型不一样的是,它是基于BASE模型的。
BASE:
Basically Available(基本可用):系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用
Soft State(软状态):允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性
Eventually Consistent(最终一致性):系统保证最终数据能够达到一致
事件发布
在事件驱动的架构中,跨服务完成业务逻辑的一个关键点是每个服务自动更新数据库和发布事件,也就是要以原子粒度更新数据库和发布事件。例如,储蓄服务必须在对储蓄账户表进行更新,然后发布“向理财账户转账”事件,这两个操作需要原子化实现。如果服务在更新数据库之后、发布事件之前崩溃,系统会变得不一致。
保证数据更新与事件发布原子化的方法,有以下几种:
使用本地事务发布事件
挖掘数据库事务日志
使用事件源
使用本地事务发布事件
一个实现原子化的方法是使用本地事务来更新业务实体和事件列表,由一个独立进程来发布事件。具体来说,就是在存储业务实体状态的数据库中,使用一个事件表来充当消息队列。应用启动一个(本地)数据库事务,更新业务实体的状态,在事件表中插入一个事件,并提交该事务。一个独立的消息发布线程或进程查询该事件表,将事件发布到消息代理,并标注该事件为已发布。下图展示了这一设计。
储蓄服务更新储蓄账户的余额,然后在事件表中插入“转账到理财账户”的事件。事件发布线程或进程在事件表中查询未发布的事件并发布,然后更新事件表,将该事件标记为已发布。
这种方法的优点是:
使用本地事务,保证了数据被更新时事件一定能够被发布
实现简单,只需要系统具备本地事务的能力即可实现
这种方法的一个缺点是,数据更新操作与所要发布的事件之间的对应关系,是由应用的开发者实现的,因此有很大可能出错。
挖掘数据库事务日志
实现原子化的另一种方式是由线程或者进程通过挖掘数据库事务或提交日志来发布事件。应用更新数据库,数据库的事务日志会记录这些变更。事务日志挖掘线程或进程读取这些日志,并把事件发布到消息代理。
比如一个B2C的电商网站,就可以通过挖掘订单数据的更新日志,来进行事件发布。如下图所示:
这一方法的范例是开源的 LinkedIn Databus 项目。Databus 挖掘 Oracle 事务日志并发布与之对应的事件,LinkedIn 则使用 Databus 维持各种来源的数据存储与记录系统一致。
另一个范例则是 AWS DynamoDB 采用的流机制。AWS DynamoDB 是一个可管理的 NoSQL 数据库,其中每个 DynamoDB 流包括 DynamoDB 表在过去 24 小时之内的时序变化,包括创建、更新和删除操作。应用能够读取这些变更,将其作为事件发布。
这种方法的优点是:
要发布的事件直接来源于数据库的事务日志,因此不会出错
应用无需关注事件的发布,简化了应用开发者的工作
但是这种方法也有一些缺点:
事务日志的格式与所使用的数据库相关,因此事件挖掘 的实现会由于数据库的种类或版本的变化而随之需要修改
由于是直接从数据库的更新记录生成事件,因此可能会无法逆向推断出业务逻辑,因此并不适合于所有场景(比如前文所述的转账场景)
使用事件源
事件源采用一种截然不同的、以事件为中心的方法来保存业务实体——不同于存储实体的当前状态,应用存储的是状态改变的事件序列。每当业务实体的状态改变,新事件就被附加到事件列表,并且应用可以通过事件回放来重构实体的当前状态。鉴于保存事件是一个单一的操作,因此本质上也是原子化的。
要了解事件源如何运行,可以以储蓄服务为例。在传统的方法中,每次转账交易都会更新储蓄账户表的记录。而使用事件源的时候,储蓄服务以状态更改事件的方式存储用户的储蓄账户,每个事件都包含足够的数据去重建储蓄账户状态。
事件长期保存在事件仓库(Event Store),使用 API 添加和检索实体的事件。同时,事件仓库起到类似上文提及的消息代理的作用,通过 API 让服务订阅事件,将所有事件传达到所有感兴趣的订阅者。所以,事件仓库可以认为是数据库与消息代理的综合体,是事件源方法的支柱。
事件源方法有如下的优点:
事件即状态,发布事件就是在更新状态,因此天然具有原子性,并且不会出错
由于存储的是事件,而不是域对象,因此避免了
由于存储了所有的业务状态更新事件,因此可以通过事件回放推断出任一时间点的业务实体状态
事件源方法也有以下这些缺点:
要实现一个可靠和高性能的事件仓库并不是一件容易的事情
应用代码需要根据事件仓库的 API 进行重写
事件仓库只直接支持通过主键查询业务实体,因此对于复杂视图的查询比较困难(可以通过CQRS方法解决,具体参见下文)
命令查询分离(CQRS)
在事件源方法中,不再直接存储任何业务实体的状态,而是代之以状态变更事件。在进行复杂视图的查询时,如果还按照与命令操作同样的方式,将会遇到一些困难。比如要发起如下的一个同时涉及储蓄账户和理财账户的查询操作:
SELECT *
FROM DEPOSIT_ACCOUNT deposit, FINANCE_ACCOUNT finance
WHERE deposit.user_id = finance.user_id
AND finance.state = 'active' AND deposit.amount > 100000 AND finance.amount > 5000
在非事件源的方式下,可以很容易的从储蓄账户表和理财账户表查询到相应数据。但是在事件源方式下,事件仓库中存储的是一系列事件,并且只能通过主键(比如 deposit_account.id 或 finance_account.id)去查询相应的业务实体,此时要处理类似 deposit.amount > 100000 这样的查询条件以及条件组合时,是非常复杂和低效的。
为了解决这一问题,可以采用CQRS方法,将命令与查询分离。命令操作仍然通过各服务的 API 以更新事件列表的方式进行,而查询操作则通过一个统一的视图查询服务(View Query Service)完成。
根据存储在事件仓库中的事件集合,可以计算得到每个业务实体的状态,这些状态以物化视图(Materialized View)的方式存储在一个数据库中。当有新的事件产生时,也同样会自动更新视图。这样,视图查询服务就可以像查询普通的数据库数据一样实现各种查询场景。具体的设计可参考下图所示:
结论
在微服务架构中,每个微服务都有其私有数据存储,不同的微服务可能使用不同的数据库。这种架构带来便利的同时,也给分布式数据管理带来挑战,其中最大的挑战就是在实现跨服务的业务逻辑时,如何保持服务之间的数据一致性。
对于许多应用,解决方案就是使用事件驱动的架构。事件驱动的架构带来的挑战是如何原子化地更新状态和发布事件。有几个方法可以做到这一点,包括把数据库用作消息队列、事务日志挖掘和事件源。
参考文献
以上是关于软件架构入门-分层架构、事件驱动、微服务架构和云原生架构的主要内容,如果未能解决你的问题,请参考以下文章