漫谈软件架构
创建一个良好的分布式应用并非易事:这类系统通常遵循12要素应用和微服务原则它们必须是无状态的、可伸缩的、可配置的、独立发布的、容器化的,可自动化的,并且有时是事件驱动的或者 Serverless 的。一旦创建后,它们应该易于升级、可长期低成本维护。要使用当前的技术,在这些相互竞争的要求之间找到良好的平衡仍是种艰难的尝试。
—— Bilgin Ibryam《多运行时微服务架构》
今天,我们站在云原生的基础设施之上,思考着如何按照《机甲运行时》,将自己的分布式应用设计得既…又…还…,以同时满足 ROI(Return on Investment)和 SLI(Service Level Indicator),并能有远见地让商业和技术在高速的发展中保持先进性和可持续发展。
第一性原理
万法归一,一归何处?无论是哲学还是科学,人类孜孜以求的是找寻事物的本源。Elon Musk 把火箭发射成本降低到原先的几十甚至上百分之一,将古希腊圣贤(从 Thales 到 Plato 和 Aristotle)奠基的第一性原理(First Principle)重新点亮。
那么,以机甲运行时为代表的这些现代技术架构,本源到底是什么、为什么在这样的时间和空间中出现、要解决什么样的问题?
复杂性
当我们说一个系统很复杂,这里的复杂性到底是什么呢?John Ousterhout 在《软件设计的哲学》一书中对复杂性做出了如下定义:复杂性与软件系统的结构有关,这使它很难理解和修改系统。
如果一个软件系统难以理解和修改,那就很复杂。如果很容易理解和修改,那就很简单。复杂性的表现形式有:
变更放大(Change amplification):看似简单的变更需要在许多不同地方进行代码修改。
认知负荷(Cognitive load):指开发人员需要多少知识才能完成一项任务。
未知的未知(Unknown unknowns):必须修改哪些代码才能完成任务,或者开发人员必须获得哪些信息才能成功地执行任务,这些都是不明显的。
Fred Brooks 在 20 世纪 80 年代在其《人月神话》中将复杂性粗略地分为两类:本质复杂性(Essential Complexity)和偶然复杂性(Accidental Complexity)。本质复杂性是系统固有的、功能性的,增加一个新功能必然驱使本质复杂性上升;偶然复杂性是外在的、辅助性的,实现一个功能必然产生偶然复杂性。比如,订单、商品、结算的业务复杂性属于本质复杂性,服务发现、事件驱动这些技术复杂性属于偶然复杂性。《混沌工程系统韧性实践》一书中说,系统的复杂性不可避免、无法减少和消除, 软件要发展,复杂性必然增加。我们只能接纳复杂性、学习如何应对复杂性。另外,根据必要多样性定律(Law of Requisite Variety),一个完全控制系统 B 的系统 A,必须至少和系统 B 一样复杂。
John Ousterhout 提出软件设计的最大目标,就是降低复杂性。
软件架构的演化从本质上讲,是在面对不同时间和空间中的本质复杂性和偶然复杂性,并持续不断地进行内聚、解耦,降低认知负荷。
软件架构
如果你让架构师描述一个架构,他回答“这是一个微服务架构”。其实他只谈到了系统的结构(the structure of the system),而不是系统的架构(the architecture of the system)。为了充分理解系统的架构,还需要了解架构特征,架构决策和设计原理。
[Neal Ford](https://book.douban.com/search/Neal Ford) 《软件架构基础》
软件架构这个名词尚无明确的定义,有人称其为系统蓝图(blueprint),也有人把它叫做开发系统的路线图(roadmap)。Neal Ford 将其内涵定义为如下 4 个维度:
Architecture Characteristics(架构特征)
Design principles(设计原则)
Architecture Decisions(架构决策)
Structure(结构)
架构特征定义了系统的成功标准,该标准通常与系统的功能正交。比如可用性(availability)、韧性(resiliency)、可伸缩性(scalability)、性能(performance)等。准则不是一成不变的规则,相对于政策,准则是方针。架构决策定义了应如何构建系统的规则,是系统开发的约束和指南。我们经常所说的 Architecture Style(架构风格)和 Architecture Patterns(架构模式),本质上都是在讲软件系统的结构。
Neal Ford 定义的这 4 个维度正好与《道德经》中所述的道、法、术、器相对应。
随着时空的变换,软件架构随之演化,以持续有效地降低复杂度。这是我们一次次“问道”的过程。例如,同样是性能这个架构特征,在 B 端商品入库的业务中强调的是高吞吐,而在 C 端检索的业务中强调的则是低延迟。为此,商品入库业务的设计原则定义为异步执行;而检索业务的设计原则为就近访问、极简的数据结构。在这样的方针之下,架构决策就可以分别制定为事件驱动和分布式多级缓存。最后,B 端服务的架构模式随之演化为 EDA 甚至 Serverless,C 端服务的架构模式为微服务+多级分布式缓存。
虽然这个推演存在着想当然的成分(因为其中有诸多不确定的假设),但足矣示意 Neal Ford 定义的软件架构的 4 个维度。
根据热力学第二定律,封闭的系统总从有序走向无序,当熵增随着规模达到阈值后,系统的复杂度会击溃我们的维系,直至重建。当年,Oracle 数据库就是亚马逊的达摩克利斯之剑,迫使亚马逊从单体逐渐演化到 SOA,随即到微服务,直至今天的云原生,从而持续为客户提供高可用、可伸缩、……的服务。
没有一开始就完美的架构, 好的架构设计一定是演化来的,不是一开始就设计出来的。通过架构 4 个维度的持续演进,系统的复杂性得到降低便是成功的架构。但反过来讲,成功的软件架构是其 4 个维度共同发挥作用的,背后的驱动力是其具体时空下的问题。因此,除了架构模式,我们很难看到通用的、共识的设计原则和架构决策。比如,微服务架构(Microservices Architecture, MSA)通过引入分布式技术、韧性工程、可观测性等部分降低了系统的偶然复杂性,通过对单体服务进行领域建模并按领域拆分,部分降低了系统的本质复杂性。但微服务架构依然无法告诉我们,权衡业务建模的范式和服务拆分粒度的标准,降低认知负荷依然需要我们因地制宜地去探索和努力。
业务复杂性
领域建模
微服务架构的起跑线上响彻了单体架构之恶的口号,因此服务的领域建模及其拆分需要一个指导原则,那就是[Eric Evans](https://book.douban.com/search/Eric Evans)在 2003 年提出的领域驱动设计(Domain Driven Design, DDD)。
DDD 祭出通用语言(UBIQUITOUS LANGUAGE)、模型驱动设计(MODEL-DRIVEN DESIGN)和上下文映射(Context Map),在界限上下文(Bounded Context)之间识别重复概念(Duplicate Concept)和假同源(False Cognate),搭建起消化知识(Crunching Knowledge)、降低本质复杂性认知负荷的巴别塔。通过聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑,设计和实现领域模型。
同时,DDD 也提出了如何识别、沟通和选择模型边界和关系,保证模型的完整性和组织间的协作效率。
共享内核 两个团队决定共享核心领域(Core Domain)或通用子领域(Generic Subdomain),通过共享内核(Shared Kernel)减少重复,使两个子系统之间的集成趋向容易。一个事件的生产者和消费者之间就是共享内核。两个服务都依赖共享内核但仍然互相无感知。
客户方-提供方 客户方(Customer)服务单向依赖提供方(Supplier)服务,客户方决定提供方的开发自由度,但双方的领域模型有各自的边界上下文,且独立发展。
跟随者 客户方(Customer)服务单向依赖提供方(Supplier)服务,提供方处于利他主义向客户方共享信息。作为跟随者(Conformist),客户方通过严格遵从提供方模型,消除界限上下文之间转换的复杂性。
防腐层 客户方(Customer)服务单向依赖提供方(Supplier)服务,提供方独立演化领域模型,客户方为减少提供方模型变化带来的转换成本,通过 Facade 或者 Adapter 模式建立防腐层(Anticorruption Layer)隔离转换逻辑。
各行其道 双方不考虑集成时可以采用各行其道(Separate Way)的模式。
开放主机服务 当一个子系统具有某种内聚性,满足其他子系统的公共需求时,可以将其封装为一个服务,通过开放协议供所有需要与之集成的子系统使用。
领域分层
分层架构的目的是隔离领域模型,将业务复杂性的关注点按层次分离,并隔离本层业务复杂性的变化对下层的影响。
Eric Evans 定义了 4 层模型,将组件分别归纳到接口层、应用层、领域层,及基础设施层。[Vaughn Vernon](https://book.douban.com/search/Vaughn Vernon)在《实现领域驱动设计》中提出了端口和适配器架构,也就是六边形架构(Hexagonal Architecture),成为微服务分层架构的事实标准。
接着,Jeffrey Palermo 提出了洋葱架构。在端口和适配器架构的基础上贯彻了将领域放在应用中心,将传达机制(UI)和系统使用的基础设施(ORM、搜索引擎、第三方 API...)放在外围的思路,在其中加入了内部层次:代表传达机制和基础设施的外层、 代表业务逻辑的内层。端口和适配器架构与洋葱架构有着相同的思路,它们都通过编写适配器代码将应用核心从对基础设施的关注中解放出来,避免基础设施代码渗透到应用核心之中。这样应用使用的工具和传达机制都可以轻松地替换,可以一定程度地避免技术、工具或者供应商锁定。
然后,Bob 大叔(Robert C. Martin)对各种分层架构总结为整洁架构(Clean Architecture)。Herberto Graça进一步总结为清晰架构(Explicit Architecture)。
服务粒度
微服务架构相对于单体架构,有效地将业务复杂性分解到各个微服务中。由此带来了一个新的话题,服务拆分粒度的标准是什么?Neal Ford 在Software Architecture: The Hard Parts一书中提出了 2 个度量指标。语句的数量是其中的一个用于客观地衡量服务大小的指标,至少让架构师或开发团队客观地度量服务正在做什么。度量和跟踪服务公开的公共接口或操作的数量是另一个确定服务粒度的指标。虽然这两个指标仍然存在一些主观性和可变性,但这是迄今为止我们提出的最接近客观地衡量和评估服务粒度的东西。
6 条服务拆分的标准:
服务范围及功能。服务是否做了太多不相关的事情?是否足够内聚?是否遵循 Bob 大叔在《敏捷软件开发》中定义的单一责任原则?
代码的波动性。更改是否可以隔离在服务的一部分内?服务中是否存在频繁更改的区域?
可伸缩性和吞吐量。服务的不同部分是否存在以不同的方式进行扩展以满足相应的吞吐量?
容错。服务内是否存在导致关键功能失败的错误?这些错误是否频繁出现?
安全。服务的某些部分是否需要比其他部分有更高的安全级别?
可扩展性。服务是否总是在扩展以添加新的上下文?
4 条服务合并的标准:
数据库事务。是否需要在服务间进行 ACID 事务?
工作流和编舞。处于编舞模式的各个服务是否存在影响性能或者 Saga 事务的相互通信?
共享代码。服务是否需要彼此共享代码?共享的代码库是否是特定的共享域功能、是否频繁更改、是否存在版本控制的缺陷?
数据关系。服务拆分是否保证它们使用的数据也可以拆分?
权衡拆分和合并的理由:
服务范围:高内聚的单一用途服务
代码波动性:敏捷性——减少测试范围和部署风险
可伸缩性:降低成本,加快响应速度
容错:更好的整体正常运行时间
安全访问:对特定功能有更好的安全访问控制
可扩展性:敏捷性——易于添加新功能
数据库事务:数据完整性和一致性
工作流:容错、性能和可靠性
共享代码:可维护性
数据关系:数据的完整性和正确性
流程编排
**工作流(Workflow)**是一种实现服务内流程编排的方式,**业务流程管理(BPM)**可以实现服务内和服务间的流程编排。
如果把飞机降落看做是一个工作流,那么整个机场的运营就是 BPM。
业务流程是业务逻辑的执行过程。对业务流程进行编排并以可视化的方式呈现,可以有效地将业务串联,降低认知负荷,从而降低业务复杂度。
微服务架构缺乏服务内和服务间的流程编排,对业务中工作流状态缺少监控和展示,对流程中的故障缺乏统一的处理。我们要做的是,在微服务这种架构模式上,丰富设计原则和架构决策,以探索并持续完善基于微服务的流程编排机制。
在 SOA 时代,大量的单体服务使用 BPEL 引擎或企业服务总线(Enterprise Service Bus, ESB)编排。ESB 通过消息管道实现各个子系统之间的通信交互,使服务间在 ESB 调度下无须直接依赖即可相互通信。但 ESB 这种**强管道弱端点(Smart Pipe and Dumb Endpoint)**模式,将太多的逻辑移进入网络,昂贵、复杂,且几乎不可能排除故障。
今天的时代,我们更强调建立小型的、单一用途的微服务,通过强端点(Smart Endpoints)弱管道(Dumb Pipes)进行连接和通信。服务内的组件具有更强大的业务流程处理能力,服务间流程编排(跨越多个服务的事务)遵循 saga 模式。
Saga 模式
Sagas由普林斯顿大学的 Hector Garcia-Molina 和 Kenneth Salem 在 1987 年提出,讲述了如何处理长活事务(long lived transactions, LLTs)。一个长活事务((minutes, hours, or perhaps even days)可被分解成可以交错运行的子事务集合。其中每个子事务都是一个保持数据库一致性的真实事务。
Saga 的组成及执行顺序
由一系列 sub-transaction Ti 组成
每个 Ti 都有对应的补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果
Saga 的恢复方式
向后恢复(backward recovery): 如果任一子事务失败,补偿所有已完成的事务。
向前恢复(forward recovery): 重试失败的事务,假设每个子事务最终都会成功。
Saga 的协调模式
编曲模式(Orchestrated):mediator 模式,由中心化的协调者来定义执行顺序并触发任何所需的补偿操作,是 command-and-control 架构。
编舞模式(Choreographed):broker 模式,将分布式职责分配给多个协作服务,是 trust-but-verify 架构。
Neal Ford 在Software Architecture: The Hard Parts一书中使用通信(Communication)、一致性(Consistency)、协调(**Coordination)**3 个维度,将 saga 细分为 8 中模式。
技术复杂性
高可用
底层逻辑
CAP 原则:一致性(Consistency)、可用性(Availability)、分区容忍性(Partition tolerance),三者最多只能同时满足两项。该理论是由加州大学的计算机科学家 Eric Brewer 在 1998 年提出(布鲁尔定理 Brewer's theorem)。由 Gilbert and Lynch 在 2003 年给出了证明。
网络分区(Network Partitioning)是指由于网络故障导致集群被分割为两个及以上的分区,每个分区内的节点无法与其他分区的节点通信的现象。分区容忍性是指出现网络分区时,各分区内的节点可以继续工作。分布式服务必须满足 P,只能保证 AP 或者 CP。那么在高可用的场景下,就要对一致性进行权衡。
一致性包括:
强一致性(Atomic Consistency/Strong Consistency)
最终一致性(Eventual Consistency)
顺序一致性(Sequential consistency)满足偏序(partial order)即可。(Zookeeper 是顺序一致性读)
线性一致性(Linearizable consistency)要求达到全序(total order)一致。(etcd 满足线性一致性读)
BASE 理论:基本可用(Basically Available)、软状态(Soft state)和最终一致性(Eventually consistency)三个短语的简写。由 eBay 架构师 Dan Pritchett 提出。
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。副本同步的延时现象就是软状态。
BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于 CAP 定理逐步演化而来的,其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
可用性与可靠性
高可用时常让我们联系到 SRE,前者是指系统高度可用(High Availability),而后者是指系统的可靠性(Reliability)工程。
可用性是关于系统可供使用时间的描述,以丢失的时间为驱动(Be Driven By Lost Time)。系统在给定时间内总体的运行时间越长,可用性越高。
可靠性是关于系统无故障间隔的描述,以发生的失效个数为驱动(Be Driven By Number of Failure)。
通常由如下 3 项指标来衡量:
平均故障间隔(Mean Time Between Failure,MTBF)
平均无故障时间(Mean Time To Failure, MTTF)
平均故障恢复时间(Mean Time To Repair, MTTR)
99.9 = (1 年 = 365 天 = 8760 小时) * 0.1% = 8760 * 0.001 = 8.76 小时
A.可用性 > B.可用性
A.可靠性 < B.可靠性
韧性工程
韧性(Resiliency)是对预期的故障进行容忍的架构特征,比如容错(Fault Tolerance)、容灾(Disaster Tolerance)、自愈(Automatically recover from failure)、灾难恢复(Disaster Recovery)。
恢复能力的关键指标
恢复点目标(Recovery Point Obejective, RPO):系统能够容忍的最大数据丢失量。用来衡量容灾系统的数据冗余备份能力,系统容忍丢失的数据量越小,RPO 的值越小。
恢复时间目标(Recovery Time Objective, RTO):系统能够容忍的服务停止的最长时间。用来衡量容灾系统的业务恢复能力。系统服务的紧迫性要求越高,RTO 的值越小。
David D. Woods 在《韧性工程》中定义了韧性工程(Resilience Engineering)关注的 4 个概念:
鲁棒性(Robustness):吸收预期扰动的能力
反弹(Rebound):从创伤事件(traumatic event)中恢复的能力
优雅的可扩展性(Graceful extensibility):非预期情况的处理能力
持续适应性(Sustained adaptability):持续适应不断变化的环境、涉众和需求的能力
混沌工程
混沌工程(Chaos Engineering)是在分布式系统上进行实验的学科, 目的是建立对系统抵御生产环境中失控条件的能力以及信心。混沌工程采用“可证伪性”的科学实验,只关注所定义的稳态假说是否被证伪,在生产环境的动荡条件下建立信心。混沌工程是面对非预期的系统结果,建立一种韧性文化。
塔勒布(Nassim Nicholas Taleb) 2012 年出版的《反脆弱:从无序中获益》中有这样定义:脆弱性是指因为波动和不确定而承受损失,反脆弱性则是指让自己避免这些损失,甚至从混乱和不确定中获利。
反脆弱提出了与韧性工程(Resilience Engineering)、人因学(Human Factors)与安全系统研究(Safety System research)相悖的指导思想。对于提高系统鲁棒性(robustness),反脆弱建议寻找并消除弱点(to hunt for weaknesses and remove them),而韧性工程告诉我们寻找做对的地方比寻找做错的地方提供的信息要多得多(hunting for what goes right in safety is much more informative than what goes wrong);反脆弱建议增加冗余(to add redundancy) ,增加冗余既可以缓解故障,也可以导致故障。韧性工程的文献中有众多这样的案例。
SRE 和开发人员通过混沌工程的实验中研究什么是对的,而不仅仅是诊断出什么是错的。选择正确的实验以了解被测系统的稳态。这需要领域专家对被测系统的内部逻辑有所理解,尤其是要理解该系统如何容错原先设计中考虑到的故障。这也意味着,不仅要理解系统可以容错某些故障,还要去理解符合预期的实际运行状况。
可观测性
可观测性是韧性工程和混沌工程的重要组成部分,也是服务网格的 3 大特性之一,而服务网格为云原生服务治理而生。
治理(Governance)一词,源于希腊语 kubernan(κυβερνάω)词,不由得让我们联想到 kubernetes(κυβερνήτης)。我们在管控侧(控制平面(Control Plane))观察实时采集到的遥测数据(temeletry),通过服务自身状态和服务间状态决定是否干预,并将干预规则下发到运行侧(数据平面(Data Plane))。
因此,可观测性是实现高可用的重要手段,最终目的是治理微服务或者云原生服务。
Metrics:用于记录连续的、可聚合(Aggregatable)的数据。例如,队列的当前深度可被定义为一个度量值,在元素入队或出队时被更新;HTTP 请求个数可被定义为一个计数器,新请求到来时进行累加。
Logging:用于记录离散的事件。例如,应用程序的调试信息或错误信息。它是我们诊断问题的依据。
Tracing:用于记录请求范围内的信息。例如,一次远程方法调用的执行过程和耗时。它是我们排查系统性能问题的利器。
三者在可观察性上缺一不可:基于 Metrics 的告警发现异常,通过 Tracing 定位可疑模块,根据模块具体的日志详情定位到错误根源,最后再基于这次问题根因分析调整 Metrics(增加或者调整报警阈值等),以便下次可以更早发现/预防此类问题。
如今,大多数请求级的分布式跟踪基础设施,都是基于 2010 年发布的 Google Dapper 的设计。2012 年,Twitter 工程师实现了名为Zipkin的开源版本。2015 年,Zipkin 变体已经激增到许多基于微服务的组织(例如 Uber 的Jaeger)。
**OpenTracing**为调用链跟踪图提供了一个开放标准。2016 年 11 月的时候 CNCF 技术委员会投票接受 OpenTracing 作为 Hosted 项目,这是 CNCF 的第三个项目,第一个是 Kubernetes,第二个是 Prometheus,可见 CNCF 对 OpenTracing 背后可观察性的重视。比如大名鼎鼎的 Zipkin、Jaeger 都遵循 OpenTracing 协议。
**OpenCensus**的发起者是谷歌,也就是 Google Dapper 的社区版。OpenCensus 和 OpenTracing 最大的不同在于除了 Tracing 外,它还把 Metrics 也包括进来,这样也可以在 OpenCensus 上做基础的指标监控;还一点不同是 OpenCensus 并不是单纯的规范制定,他还把包括数据采集的 Agent、Collector 一股脑都搞了。OpenCensus 也有众多的追随者,比如微软。
**OpenTelemetry**合并了 OpenTracing 和 OpenCensus,直接进入 CNCF Sanbox 项目。目前尚未将 Logging 纳入进来,主要有两个原因:
工作组目前主要的工作是在把 OpenTracing 和 OpenCensus 的概念尽早统一并开发相应的 SDK,Logging 是 P2 的优先级。
他们还没有想好 Logging 该怎么集成到规范中,因为这里还需要和 CNCF 里面的 Fluentd 一起去做,大家都还没有想好。
OpenTelemetry 的终态就是实现 Metrics、Tracing、Logging 的融合,作为 CNCF 可观测性的终极解决方案。OpenTelemetry 试图使用 Context 为 Metrics、Logging、Tracing 提供统一的上下文,三者均可以访问到这些信息,由 OpenTelemetry 本身负责提供 Context 的存储和传播。
扩展阅读:Apache 应用程序性能监控(Application Performance Monitor, APM)工具SkyWalking
性能
相对于高可用,“高性能”这个说法从修辞上讲是仿词,就像公理婆理,有聊无聊。在软件架构这个上下文中,“高性能”其实对应的英文是 Performance,而不是 High Performance。High Performance 通常出现在算力的上下文中,High Performance (Distributed) Computing 用来描述处理器(CPU/GPU)的计算能力。
那么技术架构下的高性能的内涵是怎样的呢?Stripe 工程师高田幹人(*Mikito Takada*)在其著名的Distributed systems for fun and profit一书中是这样定义的:
根据上下文不同,性能涉及实现以下一到多项:
短的响应时间(Response Time, RT)/低延迟(Low Latency)
高吞吐(Throughput)(处理工作的速度)
计算资源利用率低(Low Utilization)
延迟和响应时间 延迟和响应时间经常用作同义词,但实际上它们并不一样。
响应时间是客户所看到的,除了实际处理请求的时间——服务时间(Service Time),还包括网络延迟和排队延迟。
延迟是某个请求等待处理的持续时长,在此期间它处于休眠(latent)状态,并等待服务。
[Murat Erder](https://book.douban.com/search/Murat Erder)在《持续架构实践》一书中称其之为性能效率(Performance Efficiency),包含 3 个方面:
时间行为(Time Behavior)
资源利用率(Resource Utilization)
容量(Capacity)——预期的满负荷(expected full-peak load)承载
[Martin Kleppmann](https://book.douban.com/search/Martin Kleppmann)在《设计数据密集型应用》有这样一段描述:
由此,我们可以粗略地将性能归类为时间维度的高并发(高吞吐/低延迟)、资源维度的资源利用率/饱和度。
[Martin Kleppmann](https://book.douban.com/search/Martin Kleppmann)在《设计数据密集型应用》有这样一段描述:
对于 Hadoop 这样的批处理系统,通常关心的是吞吐量,即每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间。对于在线系统通常更重要的是服务的响应时间,即客户端发送请求到接收响应之间的时间。
这在我们的 B 端离线批处理和 C 端在线检索两个不同的场景中提高性能,带来一定的启发。通过异步机制提高 B 端吞吐、通过多级缓存/索引降低 C 端检索延迟。
https://docs.microsoft.com/en-us/azure/architecture/solution-ideas/articles/elastic-workplace-search
事件驱动架构
微服务和 EDA 是现代架构风格的倚天屠龙。前者是以数据为中心的请求驱动架构,后者是以事件为中心的事件驱动架构。
Martin Fowler定义了三种不同类型的事件模式:
事件通知(Event Notification)
事件携带状态转换(Event-Carried State Transfer)
事件溯源(Event-Sourcing)
事件
事件是已经发生的事实,并且是不可变的。相比而言,消息是一个服务为了另一个服务的消费或存储而生产的原始数据,消息是可以被修改的。
事件的生产者如实地产生和投递事件,它不关心这个事件将由谁、因何,以及怎样去处理。而消息的生产者是知道谁来消费的,并且知道封装哪些因素到消息中,以便消费者处理。
事件的 Broker 被设计为提供事实日志。事件在超时时间后被删除,这个超时时间是由组织或者业务定义的。而消息的 Broker 被设计为处理各类问题的,当消费者感知到消息后,消息即可被删除。
离散事件:描述状态(state)的变化 可执行的
连续事件:描述处于怎样的状态(condition) 可分析的
通常,事件是离散的,用于描述一个事物的状态变化,可以被执行。消费者根据离散事件所描述的状态,执行相应的动作。
事件也可以是连续数据流中的一部分,用来描述一个事物当前处于某种状态下。这些连续的事件是可分析的,消费者可以根据这些状态的变化,分析出某种趋势及背后的原因。
事件应当被设计为最小尺寸、最简类型、单一目的。这里要着重介绍下 CloudEvents。CloudEvents 在 2018 年 5 月进入 CNCF 基金会的沙箱项目,然后只用了 1 年多时间就成为 CNCF 的孵化项目,其发展速度非常快。CloudEvents 将会成为云服务之间,事件通讯的标准协议。同时要强调的是,CloudEvents 已经发布了多个消息中间件的绑定规范。
服务定义模式
我们已经知道,事件的生产者并不知道消费者是谁,因此不能像消息那样预先定义消息的格式。因此,在事件驱动架构中,需要一个 Schema Registry 为生产者提供序列化依据,为消费者提供反序列化依据。
Schema 类似 gRPC 中的 proto 定义。在请求驱动模式下,gRPC 的服务端和客户端会分别根据 proto 定义,生成 stub 模板代码。然后将模板代码提供给自己的上层代码调用,从而实现序列化和反序列化。
与之类似,在事件驱动模式下,消费者在获取事件后,可以根据CloudEvents标准协议,解析出 Schema 和 Content(通常是二进制),然后通过消费者调用 Schema Registry 服务,将 Content 反序列化为事件体。
可以看到,事件的服务定义模式,可以将事件的生产者和消费者充分地解耦。
EventBridge
EventBridge 是为用户提供构建松耦合、分布式的事件驱动架构的 Serverless 事件总线服务。EventBridge 的事件传输和存储遵循 CloudEvents 协议。
在 EventBridge 中,事件的生产者称为事件源,传输和存储事件的介质称为事件总线,事件的消费者称为事件目标。事件由事件规则转换、匹配、聚合,并路由到事件目标。
EventBridge 连接了事件生产和消费的两端,为用户提供了低代码、松耦合、高可用的事件处理能力。EventBridge 基于标准的事件协议,有利于促进各类事件源的事件标准统一,使事件孤岛逐步融合进完整的事件生态体系之中。
扩展阅读:AWS EventBridge
云原生
怎样才算云原生
云原生应用服务不需要主动实现与协调服务的通信来实现服务的注册和发现;服务的可观测性数据的采集、请求的认证和授权、负载均衡等能力都不需要主动实现,云原生基础设施(包括服务网格)为应用服务提供了这些能力。应用服务基于云原生架构,通过云原生 API 可以直接使用这些能力。进而,基于 Dapr 的服务可以泛化使用各种中间件和基础设施实现连接和传输。
不可变基础设施
Chad Fowler 提出的不可变基础设施架构(immutable infrastructure)是指任何基础设施的实例一旦创建之后变成为只读状态,如需要修改和升级,则使用新的实例进行替换。部署简单性,可靠性和一致性可以最大限度地减少或消除许多常见的痛点和故障点。
不可变基础设施的好处是在基础设施中有更多的一致性和可靠性,以及更简单、更可预测的部署过程。它可以缓解或完全防止可变基础设施中常见的问题,如配置漂移(configuration drift)和雪花服务器(snowflake servers)。然而,想要高效地使用不可变基础设施,通常需要包括全面的自动化部署、云计算环境中的快速服务器配置,以及处理有状态数据或临时数据(如日志)的解决方案。
宠物与牛:
在过去的操作方式中,我们把服务器当作宠物,例如邮件服务器 Bob。如果 Bob 倒下了,所有人都无法工作了。CEO 收不到电子邮件,这就是世界末日。在新方式中,服务器被编号,就像一群牛。例如,www001 到 www100。当一个服务器故障了,它就会被下掉,然后换一个新的到线上。
雪花服务器类似于宠物。它们是手工管理的服务器,经常更新和调整,从而形成独特的环境。凤凰服务器(phoenix servers)类似于牛。这些服务器总是从零开始构建,并且很容易通过自动化过程重新创建(或“浴火重生”)。
基础设施即代码(Infrastructure as Code, IaC)将基础设施层通过代码的形式描述,并通过 codebase 版本化。典型工具:Terraform。
机甲运行时
Bilgin Ibryam 在《多运行时微服务架构》中把现代分布式应用的需求分为四种类型:
生命周期(Lifecycle) 包括组件的打包、部署、运行过程,以及从错误中恢复以及扩展服务。
网络(Networking) 包括服务发现和错误恢复、各种跟踪和遥测,以及消息交换模式,比如点对点和发布/订阅、智能路由机制等。
状态(State) 当我们谈论状态时,通常是关于服务状态以及为什么最好是无状态的。但是管理我们服务的平台本身是需要状态的。这是执行可靠的服务编排和工作流、分布式单例、临时调度(cron 作业)、幂等,有状态错误恢复,缓存等所需的。这里列出的所有功能都依赖于底层的状态。
绑定(Binding) 分布式系统的组件不仅必须彼此对话,而且还必须与现代或旧式外部系统集成。这就要求连接器能够转换各种协议,支持不同的消息交换模式,例如轮询,事件驱动,请求/答复,转换消息格式,甚至能够执行自定义错误恢复过程和安全机制。
随后,Bilgin Ibryam 提出 Mecha 是未来架构的趋势,作为业务服务(Micrologic)进程外的扩展机制(Sidecar),Mecha 具有如下特征:
Mecha 是一个通用的,高度可配置的,可重用的组件,提供分布式原语(primitive)作为现成的能力。
Mecha 的每个实例都必须配置为与单个 Micrologic 组件一起使用,或者配置为与几个组件共享。
Mecha 不对 Micrologic 运行时做任何假设。它与使用开放协议和格式(HTTP/gRPC、JSON、Protobuf、CloudEvents)的多语言微服务甚至单体系统一起使用。
Mecha 以简单的文本格式(YAML,JSON)声明式地配置,指示要启用的功能以及如何将其绑定到 Micrologic 端点。对于特定的 API 交互,可以为 Mechan 附加规范,例如OpenAPI,AsyncAPI,ANSI-SQL 等。对于由多个处理步骤组成的有状态工作流,可以使用诸如Amazon State Language的规范。对于无状态集成,可以使用与Camel-K YAML DSL类似的方法来使用企业集成模式(EIP)。
与其依靠多个代理来实现不同的目的(例如网络代理、缓存代理、绑定代理),不如使用一个 Mecha 提供所有这些能力。一些功能(例如存储,消息持久化,缓存等)的实现将被其他云或本地服务插入并支持。
某些与生命周期管理有关的分布式系统问题可以由管理平台(例如 Kubernetes 或其他云服务)提供 ,而 Mecha 运行时则使用诸如 Open App Model 这样的通用开放规范。
服务网格
服务网格是一种以 Sidecar 模式为云原生服务间通信提供韧性、流量转移、通信安全、可观测性等能力的基础设施层。服务网格的本质是利用容器技术,以无侵入的方式对微服务架构进行升级和演化。
Sidecar 模式最大的好处是将基础设施能力下沉并独立部署,这样基础能力的升级将与服务本身解耦,最大的坏处是业务容器与 Sidecar 容器共存于一个 POD 内,增加了技术复杂性。
当前主流的产品有 Google 主导的istio(其数据平面 Sidecar 的内核是Envoy)和 CNCF 产品linkerd。
分布式应用运行时
分布式应用运行时(Distributed Application Runtime, Dapr)同样是一种 Sidecar 模式、同样利用了容器技术。但 Dapr 的本质是在开发阶段提供框架、在运行时提供 Mecha 能力,更像Java EE。Dapr 是一个可移植的、无服务器的、事件驱动的运行时,它使开发人员可以轻松构建在云和边缘运行的弹性、无状态和有状态的微服务,从而降低基于微服务架构构建现代云原生应用的准入门槛。
Apache Camel
Apache Camel是一个 EIP(Enterprise Integration Patterns)框架,提供了很多的连接器,集成的组件数量惊人。是 Mecha 绑定的新趋势。如下列举 APACHE CAMEL PROJECTS:
Camel Core:集成各种系统进行生产和消费数据的框架。
Camel Karaf:支持 Camel 运行在 OSGi 容器Karaf上。
Camel Kafka Connector:将所有的 Camel 组件作为 Kafka Connect 连接器使用。
Camel Spring Boot:通过自动检测 Spring 上下文中可用的 Camel 路由,提供了 Camel 上下文的自动配置,并将关键的 Camel 实用程序注册为 bean。
Camel Quarkus:托管了将 280 多个 Camel 组件移植和打包作为为 Quarkus 的扩展。
Camel K:是一个轻量级集成框架,构建在 Apache Camel 上,在 Kubernetes 上运行,专为无服务器和微服务体系结构设计。
可伸缩性和弹性
云原生为其上的服务提供了即用即弹的能力,为服务的高可用提供了**可伸缩性(Scalability)保证;同时,这种弹性(Elasticity)**特征为我们带来了削峰填谷的能力,提高了服务的性能(资源利用率)、降低了服务的成本。
[Neal Ford](https://book.douban.com/search/Neal Ford) 在《软件架构基础》中给出了两者的定义:
可伸缩性:不出现严重的性能下降的同时保证处理大量并发用户的能力。
弹性:处理突发请求的能力。
无服务架构
无服务架构(Serverless)的愿景是让开发者只需要纯粹地关注业务复杂性,不需要考虑技术复杂性。不需要关注部署、资源等技术问题。无服务架构提供了极致的弹性,与 EDA 紧密结合,在离线批处理、流式计算场景下有广泛的使用前景。
当前主要的无服务形态包含 2 部分:
后端即服务(Backend as a Service, BaaS):数据库、消息队列等用于支撑业务且本身无业务含义的云上中间件。
函数即服务(Function as a Service, FaaS):业务逻辑函数。FaaS全景图收录了 FaaS 生态下 50 多种产品:
Java 虚拟机自身的特点,GraalVM和Quarkus正在成为无服务方向上的 JVM 和 SpringBoot。
参考及推荐书目
版权声明: 本文为 InfoQ 作者【韩陆】的原创文章。
原文链接:【http://xie.infoq.cn/article/31aaf4a8a8eddcdddf5d89edf】。文章转载请联系作者。
评论