本部分重点介绍如何开发假设的服务器端企业应用程序。
应用程序规范
假设的应用程序通过执行业务逻辑、访问数据库,然后返回 HTML、JSON 或 XML 响应来处理请求。 我们将说,应用程序必须支持各种客户端,包括运行单页应用程序(SPA)、传统 Web 应用、移动 Web 应用和本机移动应用的桌面浏览器。 应用程序可能还会公开供第三方使用 API。 它还应该能够异步集成其微服务或外部应用程序,以便在部分故障的情况下帮助微服务的复原能力。
应用程序将包含以下类型的组件:
演示组件。 这些组件负责处理 UI 和使用远程服务。
域或业务逻辑。 此组件是应用程序的域逻辑。
数据库访问逻辑。 此组件由负责访问数据库(SQL 或 NoSQL)的数据访问组件组成。
应用程序集成逻辑。 此组件包括基于消息代理的消息传递通道。
应用程序需要高可伸缩性,同时允许其垂直子系统自主扩展,因为某些子系统需要比其他子系统更多的可伸缩性。
应用程序必须能够在多个基础结构环境(多个公有云和本地)中部署,理想情况下应是跨平台的,能够轻松地从 Linux 迁移到 Windows(反之亦然)。
开发团队上下文
我们还假设以下关于应用程序的开发过程:
有多个开发团队专注于应用程序的不同业务领域。
新团队成员必须快速提高工作效率,并且应用程序必须易于理解和修改。
应用程序将具有长期演变和不断变化的业务规则。
你需要良好的长期可维护性,这意味着在将来实施新更改时具有敏捷性,同时能够更新多个子系统,同时对其他子系统的影响最小。
你想要练习应用程序的持续集成和持续部署。
你希望在开发应用程序的同时利用新兴技术(框架、编程语言等)。 迁移到新技术时,你不希望完全迁移应用程序,因为这会导致高成本并影响应用程序的可预测性和稳定性。
选择体系结构
应用程序部署体系结构应是什么? 应用程序的规范以及开发上下文强烈建议,应将其分解为自治子系统,以协作 微服务和容器 的形式将其分解为自治子系统,其中微服务是容器。
在此方法中,每个服务(容器)执行一组紧密结合且相关的功能。 例如,应用程序可能包含目录服务、订购服务、购物篮服务、用户配置文件服务等服务。
微服务使用 HTTP(REST)等协议进行通信,但也尽可能异步(例如使用 AMQP),尤其是在传播集成事件的更新时。
微服务作为相互独立的容器开发和部署。 此方法意味着开发团队可以在不影响其他子系统的情况下开发和部署特定的微服务。
每个微服务都有自己的数据库,允许它与其他微服务完全分离。 如有必要,通过使用应用程序级集成事件(通过逻辑事件总线)实现来自不同微服务的数据库之间的一致性 ,如命令和查询责任分离(CQRS)中处理的那样。 因此,业务约束必须接受多个微服务和相关数据库之间的最终一致性。
eShopOnContainers:使用容器部署的 .NET 和微服务的参考应用程序
因此,你可以专注于体系结构和技术,而不是考虑你可能不知道的假设业务域,我们选择了一个众所周知的业务域,即一个简化的电子商务(电子商务)应用程序,该应用程序提供产品目录,从客户获取订单,验证库存,并执行其他业务功能。 此基于容器的应用程序源代码在 eShopOnContainers GitHub 存储库中可用。
该应用程序由多个子系统组成,包括多个存储 UI 前端(Web 应用程序和本机移动应用),以及后端微服务和容器,用于所有必需的服务器端作,其中多个 API 网关作为内部微服务的合并入口点。 图 6-1 显示了引用应用程序的体系结构。
图 6-1. 开发环境的 eShopOnContainers 参考应用程序体系结构
上图显示了移动客户端和 SPA 客户端与单个 API 网关终结点通信,然后与微服务通信。 传统 Web 客户端与 MVC 微服务通信,该微服务通过 API 网关与微服务通信。
托管环境。 在图 6-1 中,可以看到单个 Docker 主机中部署了多个容器。 使用 docker-compose up 命令部署到单个 Docker 主机时,会出现这种情况。 但是,如果使用业务流程协调程序或容器群集,则每个容器都可以在不同的主机(节点)中运行,并且任何节点都可以运行任意数量的容器,如前面在体系结构部分所述。
通信体系结构。 eShopOnContainers 应用程序使用两种通信类型,具体取决于功能操作的类型,包括查询、更新和事务。
通过 API 网关进行 Http 客户端到微服务的通信。 此方法用于查询以及接受来自客户端应用的更新或事务命令。 后续部分详细介绍了使用 API 网关的方法。
基于事件的异步通信。 此通信通过事件总线进行,以跨微服务传播更新或与外部应用程序集成。 事件总线可以通过任何消息传送代理基础结构技术(如 RabbitMQ)或使用更高级别(抽象级别)服务总线(如 Azure 服务总线、 NServiceBus、 MassTransit 或 Brighter)来实现。
应用程序以容器的形式部署为一组微服务。 客户端应用可以通过 API 网关发布的公共 URL 与作为容器运行的微服务通信。
各个微服务的数据自主权
在示例应用程序中,每个微服务都有自己的数据库或数据源,尽管所有 SQL Server 数据库都部署为单个容器。 此设计决策只是为了便于开发人员从 GitHub 获取代码、克隆代码并在 Visual Studio 或 Visual Studio Code 中打开它。 或者,使用 .NET CLI 和 Docker CLI 轻松编译自定义 Docker 映像,然后在 Docker 开发环境中部署并运行它们。 无论哪种方式,使用数据源的容器,开发人员可以在几分钟内生成和部署,而无需预配外部数据库或任何其他依赖于基础结构(云或本地)的数据源。
在实际的生产环境中,为了实现高可用性和可伸缩性,数据库应基于云或本地的数据库服务器,而不是在容器中。
因此,微服务(甚至此应用程序中的数据库)的部署单元是 Docker 容器,引用应用程序是一个包含微服务原则的多容器应用程序。
其他资源
- eShopOnContainers GitHub 存储库。 引用应用程序的源代码
https://aka.ms/eShopOnContainers/
基于微服务的解决方案的优点
基于微服务的解决方案具有以下许多优势:
每个微服务相对较小,易于管理和改进。 具体说来:
开发人员可以轻松理解并快速上手,从而高效工作。
容器快速启动,使开发人员更高效。
像 Visual Studio 这样的 IDE 可以快速加载较小的项目,使开发人员能够高效工作。
每个微服务可以独立于其他微服务进行设计、开发和部署,从而提供敏捷性,因为可以更轻松地频繁部署新版本的微服务。
可以横向扩展应用程序的各个区域。 例如,目录服务或购物篮服务可能需要横向扩展,但不需要横向扩展订购流程。 与整体体系结构相比,微服务基础结构在横向扩展时使用的资源效率要高得多。
可以在多个团队之间划分开发工作。 每个服务都可以由单个开发团队拥有。 每个团队都可以独立于其余团队来管理、开发、部署和缩放其服务。
问题更加分散。 如果一个服务中存在问题,则只有该服务最初受到影响(除非使用了错误的设计,微服务之间的直接依赖关系除外),而其他服务可以继续处理请求。 相比之下,整体部署体系结构中的一个故障组件可能会降低整个系统,尤其是在涉及到资源(如内存泄漏)时。 此外,当微服务中的问题得到解决时,只需部署受影响的微服务,而不会影响应用程序的其余部分。
可以使用最新的技术。 由于可以独立地开始开发服务并排运行(这要归功于容器和 .NET),因此可以立即开始使用最新的技术和框架,而不是在整个应用程序的旧堆栈或框架上卡住。
基于微服务的解决方案的缺点
基于微服务的解决方案也存在一些缺点:
分布式应用程序。 分发应用程序会增加开发人员在设计和构建服务时的复杂性。 例如,开发人员必须使用 HTTP 或 AMQP 等协议实现服务间通信,这增加了测试和异常处理的复杂性。 它还会增加系统的延迟。
部署复杂性。 具有数十种微服务类型且需要高可伸缩性的应用程序(它需要能够为每个服务创建多个实例并跨多个主机平衡这些服务)意味着 IT作和管理的高度部署复杂性。 如果不使用面向微服务的基础结构(如业务流程协调程序和计划程序),那么额外的复杂性可能需要比业务应用程序本身更多的开发工作。
原子事务。 多个微服务之间的原子事务通常不可能。 业务要求必须接受多个微服务之间的最终一致性。 有关详细信息,请参阅 幂等消息处理的挑战。
增加全局资源需求 (所有服务器或主机的总内存、驱动器和网络资源)。 在许多情况下,将整体式应用程序替换为微服务方法时,基于微服务的新应用程序所需的初始全局资源量将大于原始整体应用程序的基础结构需求。 这种方法是因为更精细和分布式服务需要更多的全局资源。 但是,鉴于一般资源成本较低,并且能够扩展应用程序的特定领域的好处,与发展整体式应用程序的长期成本相比,资源使用量的增加通常是一个好的权衡之举适用于大型长期应用程序。
直接客户端到微服务通信的问题。 当应用程序很大(包含数十个微服务)时,如果应用程序需要直接客户端到微服务通信,则存在一些挑战和限制。 一个问题是客户端需求与每个微服务公开的 API 之间可能存在不匹配的问题。 在某些情况下,客户端应用程序可能需要发出许多单独的请求来撰写 UI,这在 Internet 上可能效率低下,并且通过移动网络是不切实际的。 因此,应尽量减少从客户端应用程序到后端系统的请求。
直接客户端到微服务通信的另一个问题是,某些微服务可能使用与 Web 不兼容的协议。 一个服务可能使用二进制协议,而另一个服务可能使用 AMQP 消息传送。 这些协议不区分防火墙,最好在内部使用。 通常,应用程序应使用 HTTP 和 WebSocket 等协议在防火墙外部进行通信。
然而,这种直接客户端到服务方法的另一个缺点是,很难重构这些微服务的协定。 随着时间的推移,开发人员可能会希望改变系统如何划分为服务的方式。 例如,它们可能会合并两个服务或将服务拆分为两个或多个服务。 但是,如果客户端直接与服务通信,则执行此类重构可能会中断与客户端应用的兼容性。
如体系结构部分所述,在基于微服务设计和构建复杂应用程序时,可以考虑使用多个精细 API 网关,而不是更简单的直接客户端到微服务通信方法。
对微服务进行分区。 最后,无论采用哪种方法用于微服务体系结构,另一个挑战是决定如何将端到端应用程序分区到多个微服务。 如本指南的体系结构部分所述,可以采用多种技术和方法。 基本上,需要确定与其他区域分离且具有少量硬依赖项的应用程序区域。 在许多情况下,此方法根据用例与分区服务保持一致。 例如,在我们的电子商店应用程序中,我们有一个负责与订单流程相关的所有业务逻辑的订购服务。 我们还提供实现其他功能的目录服务和购物篮服务。 理想情况下,每个服务应只有一小部分责任。 此方法类似于应用于类的单一责任原则(SRP),其中指出类应该只有一个更改的理由。 但在这种情况下,它与微服务有关,因此范围将大于单个类。 最重要的是,微服务必须是自主的、端到端的,包括对其自己的数据源负责。
外部体系结构与内部体系结构和设计模式
外部体系结构是由多个服务组成的微服务体系结构,遵循本指南的体系结构部分中所述的原则。 但是,根据每个微服务的本质,并独立于你所选择的高级别微服务体系结构,通常建议使用不同的内部体系结构,每个体系结构基于不同的模式,用于不同的微服务。 微服务甚至可以使用不同的技术和编程语言。 图 6-2 说明了这种多样性。
图 6-2. 外部体系结构与内部体系结构和设计
例如,在我们的 eShopOnContainers 示例中,目录、篮子和用户配置文件微服务很简单(基本上是 CRUD 子系统)。 因此,其内部体系结构和设计非常简单。 但是,你可能拥有其他微服务,例如排序微服务,该微服务更为复杂,并且表示具有高度域复杂性的不断变化的业务规则。 在这种情况下,你可能希望在特定的微服务中实现更高级的模式,例如使用域驱动设计(DDD)方法定义的模式,就像我们在 eShopOnContainers 订购微服务中所做的那样。 (我们将在后面的部分中查看这些 DDD 模式,该模式介绍了 eShopOnContainers 订购微服务的实现。
每个微服务采用不同技术的另一个原因可能是微服务本身的性质。 例如,如果面向 AI 和机器学习域,则最好使用 F# 等函数编程语言,甚至使用 R 等语言,而不是更面向对象的编程语言(如 C#)。
底线是,每个微服务都可以根据不同的设计模式使用不同的内部体系结构。 并非所有微服务都应使用高级 DDD 模式实现,因为这会过度设计它们。 同样,具有不断变化的业务逻辑的复杂微服务不应作为 CRUD 组件实现,或者最终可以使用低质量的代码。
新世界:多种架构模式和多语言微服务
软件架构师和开发人员使用许多体系结构模式。 以下是几个融合架构样式和架构模式的例子:
简单 CRUD、单层级、单层。
清洁体系结构 (与 eShopOnWeb 一起使用)
命令和查询责任分离 (CQRS)。
Event-Driven 体系结构 (EDA)。
还可以生成具有多种技术和语言的微服务,例如 ASP.NET 核心 Web API、NancyFx、ASP.NET Core SignalR(适用于 .NET Core 2 或更高版本)、F#、Node.js、Python、Java、C++、GoLang 等。
要点是,没有特定的体系结构模式或风格,也没有任何特定的技术适合所有情况。 图 6-3 显示了一些可用于不同微服务的方法和技术(尽管没有任何特定顺序)。
图 6-3. 多体系结构模式和多语言的微服务世界
多体系结构模式和 Polyglot 微服务意味着可以混合语言和技术以满足每个微服务的需求,并且仍相互通信。 如图 6-3 所示,在由许多微服务组成的应用程序中(域驱动设计术语中的边界上下文,或简单地将“子系统”用作自治微服务),可以采用不同的方式实现每个微服务。 每个体系结构模式可能具有不同的体系结构模式,并根据应用程序的性质、业务要求和优先级使用不同的语言和数据库。 在某些情况下,微服务可能类似。 但这种情况通常并非如此,因为每个子系统的上下文边界和要求通常不同。
例如,对于简单的 CRUD 维护应用程序,设计和实现 DDD 模式可能没有意义。 但是,对于核心域或核心业务,可能需要应用更高级的模式来处理不断变化的业务规则的业务复杂性。
尤其是在处理由多个子系统组成的大型应用程序时,不应基于单个体系结构模式应用单个顶级体系结构。 例如,CQRS 不应作为整个应用程序的顶级体系结构应用,但对于特定服务集可能很有用。
对于每个给定的情况,没有万能解决方案或正确的架构模式。 不能有“一种体系结构模式来全部规则”。根据每个微服务的优先级,必须为每个微服务选择不同的方法,如以下部分所述。