微服务开发总结
微服务是一种流行的体系结构类型,用于构建可复原、高度可缩放、可独立部署且能快速演变的应用程序。 但是,成功的微服务体系结构需要利用不同的方法来设计和生成应用程序。
微服务体系结构由一系列小型的自治服务组成。 每个服务都是自包含服务,并且应在边界上下文中实现单个业务功能。 边界上下文是业务内的自然划分,提供域模型所在的显式边界。
什么是微服务?
微服务具有规模小、独立和松散耦合的特点。 一个小规模的开发人员团队就能编写和维护一个服务。
每个服务都是一个单独的基本代码,可由小型开发团队管理。
服务可独立部署。 团队可以更新现有服务,而无需重新生成和重新部署整个应用程序。
服务负责暂留自己的数据或外部状态。 这一点与传统模型不同,后者由单独的数据层处理数据暂留。
服务通过定义完善的 API 相互通信。 每个服务的内部实现细节均对其他服务隐藏。
支持 polyglot 编程。 例如,服务无需共享相同的技术堆栈、库或框架。
除了服务本身,典型微服务体系结构中还会出现其他组件:
管理/业务流程. 该组件负责将服务放置在节点上、确定故障,以及跨节点重新均衡服务等等。 该组件通常是一种现成的技术(例如 Kubernetes),而不是自定义构建的内容。
API 网关。 API 网关是客户端的入口点。 客户端会调用 API 网关,网关再将调用转发到后端上的相应服务,而不是由客户端直接调用服务。
使用 API 网关的优点如下:
它分离了客户端与服务。 无需更新所有客户端,便可对服务进行版本控制或重构。
服务可以使用对 Web 不友好的消息传递协议,比如 AMQP。
API 网关可执行身份验证、日志记录、SSL 终止和负载均衡等其他跨领域功能。
现成的策略,例如限制、缓存、转换或验证。
优点
敏捷性。 由于微服务是独立部署的,因此我们可以更轻松地管理 bug 修复和功能发布。 无需重新部署整个应用程序即可更新服务,出现问题时可回滚更新。 在很多传统应用程序中,如果在应用程序的一个部件中发现 bug,它会阻止整个发布流程。 新功能可能会被搁置,要等到 bug 修补程序后才能进行集成、测试和发布。
小型专属团队。 微服务应该足够小,单个功能团队就能构建、测试和部署。 即使团队规模不大,也能大幅提升敏捷性。 而由于沟通效率更慢、管理开销更高且敏捷性更低,大型团队的工作效率往往更低。
小型代码库。 在整体式应用程序,代码依赖项往往会随着时间的推移而变得混杂。 要在多个位置更改代码才能添加新功能。 如果不共享代码或数据存储,则微服务体系结构可将依赖项减到最少,使新功能的添加变得更容易。
混合技术。 团队可以选择最适合其服务的技术,并适当地使用混合技术栈。
错误隔离。 单个微服务在不可用的情况下不会中断整个应用程序,但前提是所有上游微服务能够正确处理故障(例如,通过实施熔断机制)。
可伸缩性。 服务可独立扩展,这样你就能横向扩展需要更多资源的子系统,而不横向扩展整个应用程序。 借助 Kubernetes 或 Service Fabric 等业务流程协调程序,你可将更高密度的服务打包到一台主机中,从而提高资源的利用效率。
数据隔离。 执行架构更新要容易得多,因为只会影响单个微服务。 在整体应用程序中,更新架构可能极具挑战性,因为应用程序的不同部分可能都要获取相同的数据,对架构进行任何更改都会带来风险。
挑战
微服务的优势并非没有代价。 下面是在开始使用微服务体系结构之前需要考虑的一些挑战。
复杂性。 与同等的单一式应用程序相比,微服务应用程序具有更多移动部件。 每个服务更简单,但整个系统作为整体来说更复杂。
开发和测试。 编写依赖于其他独立服务的小型服务时需要的方法与编写传统的整体式或分层式应用程序时的方法不同。 现有工具并非总是能够处理服务依赖关系。 跨服务边界进行重构可能很困难。 测试服务依赖关系也有一定难度,尤其是在应用程序快速发展之时。
缺乏监管。 用于生成微服务的分散式方法具有一定优势,但也可能导致许多问题。 用户在生成过程中可能采用了许多不同的语言和框架,从而使应用程序变得难以维护。 这种情况下可以实施一些项目范围内的标准,不过分限制团队的灵活性。 这尤其适用于日志记录等跨领域功能。
网络拥塞和延迟。 使用大量小型的精细服务可能会增加服务间的通信量。 此外,如果服务依赖关系链变得太长(服务 A 调用 B,B 调用 C…),额外延迟可能会成为一个问题。 用户需要精心设计 API。 应避免过于繁琐的 API,考虑使用序列化格式,并找到可以使用异步通信模式(例如基于查询的负载调节)的地方。
数据完整性。 每个微服务负责自己的数据暂留。 因此,数据一致性可能是个挑战。 如果可能,请采用最终一致性。
管理。 成功使用微服务需要有成熟的 DevOps 区域性。 跨服务的关联日志记录可能很难。 通常情况下,日志记录必须为单个用户操作关联多个服务调用。
版本控制。 对某个服务的更新不应中断依赖于它的其他服务。 多个服务可在任意给定时间更新,因此,若不精心设计,可能会遇到向后或向前兼容性问题。
技能集。 微服务是一种高度分布式系统。 请仔细评估团队是否具有成功使用微服务所需的技能和经验。
微服务体系结构的构建流程
域分析。 为了避免设计微服务时会出现的某些常见错误,请使用域分析来定义微服务边界。 执行以下步骤:
设计服务。 微服务需要利用不同的方法来设计和生成应用程序。 设计微服务体系结构。
在生产环境中操作。 由于微服务体系结构是分布式的,因此你必须对部署和监视提供可靠的操作。CI/CD
使用域分析为微服务建模
微服务的最大挑战之一是定义各个服务的边界。 一般规则是,服务应执行 “一项操作”,但将该规则放入实践需要认真考虑。 没有任何机械性的流程可以生成“适当的”设计。 必须深入考虑业务域、要求和目标。 否则,最终可能得到一个杂乱无章的设计,它呈现一些不需要的特征,例如隐藏服务之间的依赖关系、紧密耦合,或者界面的设计不佳。 下面使用无人机传递服务作为示例。
介绍
应该围绕业务功能而不是数据访问或消息传递等水平层来设计微服务。 此外,微服务应具有低耦合高内聚的特点。 如果在更新一个服务时无需同时更新其他服务,则该微服务是低耦合的。 如果微服务的职责单一且定义完善,例如管理用户帐户或跟踪交付历史记录,则它是高内聚的。 服务应该封装领域知识,使这些领域知识对于客户端而言处于抽象状态。 例如,客户端应该能够在无需知晓派遣算法或如何管理无人机群的情况下安排无人机。
领域驱动设计 (DDD) 提供一个框架,可以让你顺利访问一组设计完善的微服务。 DDD 包括两个不同的阶段:战略和战术。 在 DDD 的战略模式中,可以定义系统的大规模结构。 战略模式有助于确保体系结构专注于业务功能。 策略性 DDD 提供一组可用于创建域模型的设计模式。 这些模式包括实体、聚合和领域服务。 借助这些战术模式,可以设计低耦合高内聚的微服务。
接下来我们将逐步执行以下步骤,将它们应用于无人机交付应用程序:
我们首先分析业务领域,以了解应用程序的功能要求。 该步骤输出领域的非正式说明,可将其优化成更正式的一组领域模型。
接下来,定义领域的边界上下文。 每个边界上下文包含一个领域模型,该模型表示较大应用程序的特定子域。
在边界上下文中,应用战术 DDD 模式以定义实体、聚合和域服务。
使用前一步骤的结果可以标识应用程序中的微服务。
请务必记住,DDD 是迭代的持续过程。 服务边界不是一成不变的。 随着应用程序的演变,你可以决定将某个服务分解成多个较小服务。
备注:
本文章未完整介绍全面的域分析。 我们特意保留了示例简短内容来说明要点。 有关 DDD 的更多背景信息,我们建议阅读 Eric Evans 的 Domain-Driven Design(领域驱动设计),该书籍首次引入了该术语。 另一项优秀的参考资源是 Vaughn Vernon 撰写的《实现领域驱动设计》。
方案:无人机传递
Fabrikam, Inc. 正在推出无人机交付服务。 该公司经营无人机群。 各商家注册该服务,用户可以请求无人机收取要交付的商品。 当客户安排取件时,后端系统会分配一架无人机,并将估计的交付时间告知用户。 在交付过程中,客户可以通过持续更新的 ETA 跟踪无人机的位置。
此方案涉及到一个相当复杂的域。 部分业务难题包括安排无人机、跟踪包裹、管理用户帐户,以及存储和分析历史数据。 此外,Fabrikam 希望快速投放市场和扩张,同时添加新功能。 该应用程序需要以云的规模运行,并附带较高的服务级别目标 (SLO)。 此外,Fabrikam 预期系统的不同部件在数据存储和查询方面具有截然不同的要求。 所有这些考虑因素促使 Fabrikam 为无人机交付应用程序选择了微服务体系结构。
分析领域
借助 DDD 方法可以设计微服务,使每个服务原生都能符合业务功能要求。 此方法有助于避免组织边界或技术选择左右你的设计。
在编写任何代码之前,需要获取所创建的系统的鸟瞰图。 DDD 首先构建业务域并创建 域模型。 领域模型是业务领域的抽象模型。 它可以提取和组织领域知识,并为开发人员和领域专家提供通用语言。
首先,映射所有业务功能及它们之间的连接。 这可能是涉及到领域专家、软件架构师和其他利益干系人的协作性工作。 无需使用任何特定的形式。 可以直接草绘或者在白板上绘制关系图。
在填充关系图时,可以开始标识离散的子域。 哪些功能密切相关? 哪些功能是业务的核心?哪些功能提供辅助服务? 什么是依赖项关系图? 在此初始阶段,不需要考虑技术或实施细节。 也就是说,应该注意应用程序要在哪个位置与 CRM、付款处理或计费系统等外部系统集成。
示例:无人机传递应用程序
完成一些初始域分析之后,Fabrikam 团队绘制了一份描绘无人机交付域的草图。
- “交货”位于关系图的中心,因为它是业务的核心。 关系图中的其他任何元素都是为了支持此功能。
- “无人机管理”也是业务的核心。 与无人机管理密切相关的功能包括无人机维修,以及使用预测分析来预测无人机何时需要检修和维护。
- ETA 分析提供取件和交货的估计时间。
- 如果包裹无法完全由无人机交货,则应用程序可以通过第三方运输来安排替代的运输方式。
- 无人机共享是核心业务的可能扩展。 公司的无人机在某些时段可能容量过剩,在这种情况下,可以出租无人机,以避免闲置。 初始版本未包括此功能。
- 视频监督是公司以后可以拓展到的另一个领域。
- 用户帐户、开票和呼叫中心是支持核心业务的子域。
请注意,在此流程的此阶段,我们尚未做出有关实施或技术的任何决策。 某些子系统可能涉及到外部软件系统或第三方服务。 即便如此,应用程序也需要与这些系统和服务进行交互,因此,必须将它们包含在领域模型中。
备注
如果应用程序依赖于外部系统,则存在一种风险:外部系统的数据架构或 API 会渗入应用程序,最终暴露了体系结构设计。 不遵循最佳实践,并使用复杂数据架构或过时 API 的旧式系统尤其如此。 在这种情况下,必须在这些外部系统与应用程序之间妥善定义边界。 出于此目的,请考虑使用 Strangler 图模式 或 反损坏层模式 。
定义边界上下文
领域模型将包含现实世界中事物的表示形式 - 用户、无人机、包裹,等等。 但这并不意味着系统的每个部分都需要对相同的事物使用相同的表示形式。
例如,处理无人机 repair 和预测分析的子系统将需要表示无人机的许多物理特征,例如,其维护历史记录、里程、年龄、型号、性能特征等。 但是,在安排交付时,我们并不需要关心这些方面。 计划子系统只需知道无人机是否可用,以及取件和交货的 ETA。
如果尝试为这两个子系统创建了单个模型,则会不必要地增大复杂性。 此外,模型会更难得到发展,因为任何更改都需要满足处理不同子系统的多个团队的要求。 因此,更好的做法通常是设计不同的模型,用于在两种不同的上下文中呈现相同的真实实体(在本例中为无人机)。 每个模型仅包含其特定上下文中相关的功能和属性。
在这种情况下,受 限制的上下文 的 DDD 概念会成为重头戏。 边界上下文只是应用特定领域模型的领域中的边界。 在上图中,我们可以根据各种功能是否共享单个领域模型将功能分组。
边界上下文不一定相互独立。 在此图中,连接边界上下文的实线表示两个边界上下文交互的位置。 例如,“交货”依赖于“用户帐户”来获取有关客户的信息,并依赖于“无人机管理”来安排机群中的无人机。
在 Domain Driven Design(领域驱动设计)一书中,Eric Evans 描述了当某个领域模型与另一个边界上下文交互时,保持该模型完整性的多种模式。 微服务的主要原则之一是服务通过完善定义的 API 进行通信。 此方法对应于两种模式,即 Evans 所说的“开放主机服务”和“发布语言”。 “开放主机服务”的思路是子系统针对与它通信的其他子系统定义一个正式协议 (API)。 “发布语言”扩展了这种思路,它以其他团队可以用来编写客户端的形式发布 API。 在设计用于微服务的 api一文中,我们讨论了如何使用 OpenAPI 规范 (以前称为 Swagger) 为 REST api 定义与语言无关的接口说明(以 JSON 或 YAML 格式表示)。
使用DDD来设计微服务
在 DDD 的战略阶段,我们要绘制业务领域的关系图,并定义领域模型的边界上下文。 在战术 DDD 阶段,需要更精确地定义领域模型。 战术模式在单个边界上下文中应用。 在微服务体系结构中,我们对实体和聚合模式特别感兴趣。 应用这些模式将有助于我们确定应用程序中服务的自然边界 (请参阅本系列文章中的下一) 。 作为一般原则,微服务应小于聚合,且不大于边界上下文。 首先,让我们了解战术模式。 然后,我们对无人机交付应用程序中的“交货”边界上下文应用这些模式。
战术模式概述
本部分提供战术 DDD 模式的简要概述,如果你已熟悉 DDD,则可以跳过本部分。 Eric Evans 的书籍第 5 章 – 第 6 章和Vaughn Vernon 的实现 Domain-Driven中更详细地介绍了这些模式。
实体。 实体是一直保持唯一标识的对象。 例如,在银行应用程序中,客户和帐户就是实体。
- 实体在系统中有唯一的标识符,使用该标识符可以查找和检索该实体。 这并不意味着,该标识符始终直接向用户公开。 它可能是数据库中的 GUID 或主键。
- 一个标识可以跨多个边界上下文,并可能保留到应用程序生存期结束之后。 例如,银行帐号或政府颁发的身份证号不会与特定应用程序的生存期相关联。
- 实体的属性可随时变化。 例如,某人的姓名或地址可能有变化,但他(她)仍是同一个人。
- 一个实体可以包含对其他实体的引用。
值对象。 值对象没有标识。 它只由其属性值定义。 值对象也是不可变的。 若要更新值对象,始终需要创建一个新实例来替换旧实例。 值对象可以包含用于封装域逻辑的方法,但这些方法不应该给对象的状态产生负面影响。 值对象的典型示例包括颜色、日期时间和货币值。
聚合。 聚合定义一个或多个实体的一致性边界。 一个聚合只包含一个根实体。 可以使用根实体的标识符执行查找。 聚合中的其他任何实体是根的子级,由从根开始的后续指针引用。
聚合的作用是为事务不变性建模。 现实世界中的事物具有复杂的关系。 客户创建订单,订单包含产品,产品有供应商,等等。 如果应用程序修改了多个相关对象,它如何保证一致性? 如何跟踪并实施不变性?
传统应用程序通常使用数据库事务来实施一致性。 但是,在分布式应用程序中,这种做法通常不可行。 单个业务事务可能跨越多个数据存储、长时间运行,或者涉及第三方服务。 最终由应用程序而不是数据层来实施域所需的不可变性。 这就是要为聚合建模的目的。
备注
聚合可以包含单个实体且不包含子实体。 聚合的定义由事务边界确定。
领域服务和应用服务。 在 DDD 术语中,服务是实现某种逻辑且不保存任何状态的对象。 Evans区分封装域逻辑的域服务与提供技术功能(如用户身份验证或发送短信)的应用程序服务。 领域服务通常用于对跨多个实体的行为建模。
备注
软件开发中广泛使用了“服务”一词。 此处的定义不直接与微服务相关。
域事件。 发生某种情况时,可以使用领域事件来通知系统的其他部件。 顾名思义,领域事件应该表示领域中发生的某些情况。 例如,“在表中插入了记录”不是领域事件。 “已取消交付”是领域事件。 领域事件与微服务体系结构密切相关。 由于微服务为分发式且不共享数据存储,领域事件可为微服务提供相互协调的途径。 服务 间通信一文 更详细地讨论了异步消息传送。
还有其他几种 DDD 模式未在此处列出,包括工厂、仓储和模块。 实施微服务时,这些模式可能十分有用;但是,在微服务之间设计边界时,它们作用不大。
无人机交付:应用模式
首先,我们探讨“交货”边界上下文必须处理的方案。
- 某个客户可以请求派遣无人机从已注册到无人机交付服务的公司取件。
- 寄件人生成了一个标记(条形码或 RFID)并粘贴在包裹上。
- 无人机将会收取包裹,然后将包裹从源位置交付到目标位置。
- 当客户安排交付时,系统将会根据路线信息、天气情况和历史数据提供 ETA。
- 当无人机起飞时,用户可以跟踪当前位置和最新的 ETA。
- 在无人机收取包裹之前,客户可以取消交付。
- 完成交付时,客户将收到通知。
- 寄件人可以请求客户提供签名或指纹形式的交付信息。
- 用户可以查找已完成交付的历史记录。
在这些方案中,开发团队确定了以下实体。
- 交付
- 包裹
- 无人机
- 帐户
- 确认
- 通知
- 标记
前四个项(“交付”、“包裹”、“无人机”和“帐户”)都是表示事务一致性边界的聚合。 “确认”和“通知”是“交付”的子实体,“标记”是“包裹”的子实体。
此 设计中的值 对象包括 Location、ETA、PackageWeight 和 PackageSize。
为便于演示,下面提供了“交付”聚合的 UML 关系图。 请注意,该聚合包含对其他聚合(包括“帐户”、“包裹”和“无人机”)的引用。
有两个领域事件:
当无人机起飞时,“无人机”实体将发送 DroneStatus 事件,用于描述无人机的位置和状态(飞行中、已着陆)。
每当交付阶段发生更改时,“交付”实体将发送 DeliveryTracking 事件。 这些事件包括 DeliveryCreated、DeliveryRescheduled、DeliveryHeadedToDropoff 和 DeliveryCompleted。
请注意,这些事件描述领域模型中有意义的事物。 它们描述有关域的某些信息,但不与特定的编程语言构造相关。
开发团队还确定了另一个功能领域,但该功能领域并不与前面所述的任何实体紧密相关。 系统的某个部分必须协调有关安排或更新交付的所有步骤。 因此,开发团队在设计中添加了两个域服务:一个协调步骤的计划程序,一个监督程序,用于监视每个步骤的状态,以便检测任何步骤是否已失败或已过。 这是计划程序代理监督程序模式的变体。
确定微服务边界
微服务的适当大小是什么? 您通常会听到,”不太大且不太小” 这一影响,尽管这确实是正确的,但实际上并不是很有用。 但是,如果从一个精心设计的领域模型着手,则规划出微服务就容易得多。
本文使用无人机传递服务作为正在运行的示例。
从领域模型到微服务
上文中,我们为无人机传送应用程序定义了一组限制上下文。 然后,我们更详细地探讨了其中的某个边界上下文(“交货”边界上下文),并为该边界上下文标识了一组实体、聚合和领域服务。下面是一个用于从域模型派生微服务的方法。
从限定上下文开始。 通常,微服务中的功能不应跨多个限定上下文。 根据定义,边界上下文标记特定领域模型的边界。 如果你发现微服务混用了不同的领域模型,可能意味着需要重新进行领域分析以优化领域模型。
接下来,查看领域模型中的聚合。 聚合通常是微服务的适当候选项。 合理设计的聚合能够体现一个设计优良的微服务的许多特征,例如:
- 聚合派生自业务要求,而不是数据访问或消息传递等技术因素。
- 聚合应具有较高的功能内聚性。
- 聚合是持久性的边界。
- 聚合应为松散耦合。
域服务也是微服务的适当候选项。 域服务是跨多个聚合的无状态操作。 典型的示例是涉及多个微服务的工作流。 我们将在无人机交付应用程序中看到此示例。
最后,考虑非功能性要求。 分析团队规模、数据类型、技术、可伸缩性要求、可用性要求和安全要求等因素。 这些因素可能导致需要进一步将微服务分解成两个或更多个较小服务,或执行相反的操作,即,将多个微服务合并成一个。
在应用程序中标识微服务之后,请根据以下条件验证设计:
- 每个服务承担单一责任。
- 服务之间不存在琐碎的调用。 如果将功能拆分成两个服务会导致它们过度琐碎,该症状的原因可能是这些功能属于同一个服务。
- 每个服务足够小,独立工作的小团队即可构建它。
- 两个或更多个服务的部署不应该存在相互依赖的关系。 应该始终可以在不重新部署其他任何服务的情况下部署某个服务。
- 服务未紧密耦合,可独立演变。
- 服务边界不会造成数据一致性或完整性方面的问题。 有时,必须通过将功能放入单个微服务来保持数据一致性。 话虽如此,但应该是否确实需要强一致性。 可通过某些策略来解决分布式系统中的最终一致性,分解服务的好处通常比管理最终一致性所存在的挑战更具效益。
最重要的是,必须追求实用,并记住领域驱动的设计是一个迭代过程。 如果有疑问,可以从更粗粒度的微服务入手。 将微服务拆分成两个较小服务比跨多个现有微服务来重构功能更方便。
示例:定义无人机传递应用程序的微服务
回忆一下,开发团队已确定了四个聚合-传递、打包、无人机和帐户,以及两个域服务、计划程序和监督程序。
“交付”和“包裹”是微服务的突出候选项。 “计划程序”和“监督程序”协调其他微服务执行的活动,因此,将这些领域服务实施为微服务比较有利。
“无人机”和“帐户”比较特别,它们属于其他边界上下文。 一种做法是让“计划程序”直接调用“无人机”和“帐户”边界上下文。 另一种做法是在“交货”边界上下文中创建“无人机”和“帐户”微服务。 这些微服务通过公开更适合“交货”上下文的 API 或数据架构,在边界上下文之间充当中介。
“无人机”和“帐户”边界上下文的详细信息超出了本指南的范畴,因此我们在参考实现中创建了它们的模拟服务。 但在此情况下,需考虑一些因素:
直接调入其他边界上下文会产生多大的网络开销?
其他边界上下文的数据架构是否适用于此上下文,或者,专门针对此边界上下文定制一个架构是否更好?
其他边界上下文是否为旧式系统? 如果是,则可以创建一个充当防腐层的服务,用于在旧式系统与新式应用程序之间进行转换。
团队结构是什么? 是否能够方便地与负责其他边界上下文的团队通信? 如果不是,创建一个充当两个上下文之间的中介的服务可能有助于降低跨团队通信所产生的成本。
到目前为止,我们尚未考虑任何非功能性要求。 考虑到应用程序的吞吐量要求,开发团队决定创建一个负责引入客户端请求的独立“引入”微服务。 此微服务将传入的请求放入缓冲区进行处理,以此实施负载调节。 计划程序将从缓冲区读取请求,并执行工作流。
非功能性要求使得团队必须额外创建一个服务。 到目前为止,所有服务都与包裹的实时安排和交付过程相关。 但是,系统还需要在长期存储中存储每项交付的历史记录,以进行数据分析。 团队认为这是交付服务的责任。 但是,历史分析与现行操作的数据存储要求有较大的差别(请参阅数据注意事项)。 因此,团队决定创建一个独立的交付历史记录服务,用于侦听来自交付服务的 DeliveryTracking 事件,并将这些事件写入长期存储。
下图展示了现阶段的设计: