“如果你认为良好的体系结构很昂贵,请尝试糟糕的体系结构。 - 布赖恩·福特和约瑟夫·约德
大多数传统的 .NET 应用程序部署为对应于单个 IIS 应用域中运行的可执行文件或单个 Web 应用程序的单个单元。 此方法是最简单的部署模型,非常适用于许多内部和较小的公共应用程序。 然而,即使提供此单一单位部署,大多数重要的商业应用程序仍受益于逻辑分层。
什么是整体应用程序?
就其行为而言,整体式应用程序是完全独立的应用程序。 它可能会在执行其作的过程中与其他服务或数据存储进行交互,但其行为的核心在其自己的进程中运行,并且整个应用程序通常部署为单个单元。 如果此类应用程序需要水平缩放,则整个应用程序通常会在多台服务器或虚拟机之间复制。
一体化应用程序
应用程序体系结构的最小项目数是一个。 在此体系结构中,应用程序的整个逻辑包含在单个项目中,编译为单个程序集,并部署为单个单元。
一个新的 ASP.NET 核心项目,无论是在 Visual Studio 中创建还是从命令行创建,都以简单的“一体式”整体开始。 它包含应用程序的所有行为,包括呈现、业务和数据访问逻辑。 图 5-1 显示了单项目应用的文件结构。
图 5-1. 单项目 ASP.NET Core 应用。
在单个项目方案中,通过使用文件夹实现关注点分离。 默认模板包括用于模型、视图和控制器的 MVC 模式职责的单独文件夹,以及用于数据和服务的其他文件夹。 在这种结构安排中,应尽可能地将展现逻辑限制在“Views”文件夹,将数据访问逻辑限制在“Data”文件夹中保存的类。 业务逻辑应驻留在 Models 文件夹中的服务和类中。
虽然简单,但单项目整体解决方案有一些缺点。 随着项目的大小和复杂性的增长,文件和文件夹的数量也会继续增长。 用户界面(UI)问题(模型、视图、控制器)驻留在多个文件夹中,这些文件夹没有按字母顺序分组。 当额外的 UI 级构造(如筛选器或 ModelBinders)被添加到各自的文件夹中时,这个问题就会愈加严重。 业务逻辑分散在模型和服务文件夹之间,没有明确指示哪些类应该依赖于哪些文件夹。 这种项目级别缺少组织的情况通常会导致面条式代码。
为了解决这些问题,应用程序通常会演变成多项目解决方案,其中每个项目都被视为驻留在应用程序的特定 层 中。
什么是层?
随着应用程序的复杂性的增长,管理这种复杂性的一种方法是根据应用程序的责任或问题来分解应用程序。 此方法遵循关注点分离原则,有助于保持不断增长的代码库井井有条,使开发人员可以轻松找到某些功能的实现位置。 不过,分层体系结构除了代码组织之外,还具有许多优势。
通过将代码组织到层中,可以在应用程序中重复使用常见的低级别功能。 这种重用是有益的,因为它意味着需要编写更少的代码,因为它可以允许应用程序在单个实现上标准化,遵循 不重复(DRY) 原则。
使用分层体系结构,应用程序可以强制限制哪些层可以与其他层通信。 此体系结构有助于实现封装。 当一个层被更改或替换时,只有与其配合的层才应受到影响。 通过限制哪些层依赖于其他层,可以缓解更改的影响,以便单个更改不会影响整个应用程序。
层(和封装)可以更轻松地替换应用程序中的功能。 例如,应用程序最初可能使用自己的 SQL Server 数据库进行暂留,但后来可以选择使用基于云的持久性策略或 Web API 后面的一个。 如果应用程序已在逻辑层中正确封装了其持久性实现,则 SQL Server 特定的层可以替换为实现同一公共接口的新层。
除了交换实现以响应未来需求更改的可能性外,应用程序层还可以更轻松地交换实现以进行测试。 无需编写针对应用程序的实际数据层或 UI 层运行的测试,这些层可以在测试时替换为提供对请求的已知响应的假实现。 与针对应用程序的实际基础结构运行测试相比,此方法通常使测试更易于编写,运行速度更快。
逻辑分层是改进企业软件应用程序中代码组织的常见技术,可通过多种方式将代码组织到层中。
注释
层 表示应用程序中的逻辑分离。 如果应用程序逻辑以物理方式分发到单独的服务器或进程,则这些单独的物理部署目标称为 层。 具有部署到单一层级的 N 层应用程序是可能的,也很常见。
传统的“N 层”体系结构应用程序
应用程序逻辑最常见的分层组织如图 5-2 所示。
图 5-2. 典型的应用程序层。
这些层通常缩写为 UI、BLL(业务逻辑层)和 DAL(数据访问层)。 使用此体系结构,用户通过 UI 层发出请求,该层仅与 BLL 交互。 BLL 可以调用 DAL 来满足数据访问请求。 UI 层不应直接向 DAL 发出任何请求,也不应通过其他方式直接与持久性存储交互。 同样,BLL 应仅通过 DAL 与持久性发生交互。 这样,每个层都有自己的已知责任。
这种传统分层方法的一个缺点是编译时依赖项从上到下运行。 也就是说,UI 层依赖于 BLL,具体取决于 DAL。 这意味着通常保存应用程序中最重要的逻辑的 BLL 依赖于数据访问实现详细信息(通常依赖于数据库的存在)。 在此类体系结构中测试业务逻辑通常很困难,需要测试数据库。 依赖项反转原则可用于解决此问题,如下一部分所示。
图 5-3 显示了一个示例解决方案,按责任(或层)将应用程序分解为三个项目。
图 5-3. 包含三个项目的简单整体式应用程序。
尽管此应用程序出于组织目的使用多个项目,但它仍部署为单个单元,其客户端将作为单个 Web 应用与之交互。 这允许非常简单的部署过程。 图 5-4 显示了如何使用 Azure 托管此类应用。
图 5-4. Azure Web 应用的简单部署
随着应用程序需求的增长,可能需要更复杂的可靠部署解决方案。 图 5-5 显示了支持其他功能的更复杂的部署计划的示例。
图 5-5. 将 Web 应用部署到 Azure 应用服务
在内部,该项目通过基于责任的细分组织为多个子项目,提高了应用程序的可维护性。
可以纵向扩展或横向扩展此单元,以利用基于云的按需可伸缩性。 纵向扩展意味着向托管应用的服务器添加额外的 CPU、内存、磁盘空间或其他资源。 横向扩展意味着添加此类服务器的其他实例,无论是物理服务器、虚拟机还是容器。 当应用跨多个实例托管时,负载均衡器用于向单个应用实例分配请求。
在 Azure 中缩放 Web 应用程序的最简单方法是在应用程序的应用服务计划中手动配置缩放。 图 5-6 显示了适当的 Azure 仪表板屏幕,用于配置为应用提供服务的实例数。
图 5-6. Azure 中的应用服务计划缩放。
干净架构
遵循依赖关系反转原则以及 Domain-Driven 设计(DDD)原则的应用程序往往到达类似的体系结构。 多年来,这种体系结构一直由许多名称命名。 第一个名字之一是六边形架构,后续是端口和适配器。 最近,它被引用为 洋葱体系结构 或 清洁体系结构。 后一个名称“清理体系结构”在此电子书中用作此体系结构的名称。
eShopOnWeb 参考应用程序使用 Clean Architecture 方法将代码组织到项目中。 您可以在 ardalis/cleanarchitecture 的 GitHub 存储库中找到用于自己 ASP.NET Core 解决方案的起始模板,或者通过 从 NuGet 安装该模板。
干净体系结构将业务逻辑和应用程序模型置于应用程序中心。 而不是让业务逻辑依赖于数据访问或其他基础结构问题,这种依赖关系被反转:基础结构和实现细节依赖于应用核心。 此功能是通过在 Application Core 中定义抽象或接口来实现的,后者随后由基础结构层中定义的类型实现。 可视化此体系结构的一种常见方法是使用一系列同心圆,类似于洋葱。 图 5-7 显示了此体系结构表示形式的一个示例。
图 5-7. 干净体系结构,洋葱视图
在该图中,依赖关系流向最内层的圆。 应用核心之所以得名,是因为它位于此图示的核心。 在关系图中可以看到,应用程序核心与其他应用程序层没有依赖关系。 应用程序的实体和接口位于中心。 在外圈但仍在应用程序核心中的是域服务,它通常实现内圈中定义的接口。 在应用程序核心之外,UI 和基础结构层都依赖于应用程序核心,但不必然相互依赖。
图 5-8 显示了更传统的水平层关系图,它更好地反映 UI 和其他层之间的依赖关系。
图 5-8. 干净体系结构,水平层次视图
请注意,实心箭头表示编译时依赖项,而虚线箭头表示仅运行时依赖项。 借助干净的体系结构,UI 层在编译时使用 Application Core 中定义的接口,理想情况下不应知道基础结构层中定义的实现类型。 但是,在运行时,应用需要执行这些实现类型,因此它们需要存在并通过依赖项注入连接到 Application Core 接口。
图 5-9 显示了按照这些建议构建的 ASP.NET Core 应用程序的体系结构的更详细视图。
图 5-9. ASP.NET“清理体系结构”后的核心体系结构关系图。
由于 Application Core 不依赖于基础结构,因此很容易为此层编写自动化单元测试。 图 5-10 和 5-11 显示了测试如何适应此体系结构。
图 5-10. 以隔离的方式对 Application Core 进行单元测试。
图 5-11. 使用外部依赖关系的集成测试基础结构实现。
由于 UI 层对基础结构项目中定义的类型没有任何直接依赖关系,因此同样很容易交换实现,以便进行测试或响应不断变化的应用程序要求。 ASP.NET Core 对依赖项注入的内置使用和支持使此体系结构成为构建非普通整体应用程序的最合适方法。
对于整体应用程序,应用程序核心、基础结构和 UI 项目都作为单个应用程序运行。 运行时应用程序体系结构可能类似于图 5-12。
图 5-12. 一个 ASP.NET Core 示例应用程序的运行时架构。
在干净体系结构中组织代码
在干净体系结构解决方案中,每个项目都有明确的职责。 因此,某些类型属于每个项目,你经常会在相应的项目中找到与这些类型对应的文件夹。
应用程序核心
Application Core 包含业务模型,其中包括实体、服务和接口。 这些接口包括将使用基础结构执行的作的抽象,例如数据访问、文件系统访问、网络调用等。有时,在此层定义的服务或接口需要使用不依赖于 UI 或基础结构的非实体类型。 这些可以定义为简单的数据传输对象(DTO)。
应用程序核心类型
- 实体(持久化的业务模型类)
- 聚合(实体组)
- 接口
- 域服务
- 规格
- 自定义异常和临界子句
- 域事件和处理程序
基础设施
基础结构项目通常包括数据访问实现。 在典型的 ASP.NET Core Web 应用程序中,这些实现包括 Entity Framework (EF) DbContext、已定义的任何 EF Core Migration
对象和数据访问实现类。 抽象数据访问实现代码的最常见方法是使用 存储库设计模式。
除了数据访问实现之外,基础结构项目还应包含必须与基础结构问题交互的服务的实现。 这些服务应实现在 Application Core 中定义的接口,因此基础结构应具有对 Application Core 项目的引用。
基础结构类型
- EF Core 类型(
DbContext
、Migration
) - 数据访问实现类型(存储库)
- 特定于基础结构的服务(例如,
FileLogger
或SmtpNotifier
)
UI 层
ASP.NET Core MVC 应用程序中的用户界面层是应用程序的入口点。 此项目应引用 Application Core 项目,其类型应严格地通过 Application Core 中定义的接口与基础结构交互。 UI 层中不应允许直接实例化或静态调用基础结构层类型。
UI 层类型
- 控制器
- 自定义筛选器
- 自定义中间件
- 浏览量
- ViewModels
- 初创公司
类 Startup
或 Program.cs 文件负责配置应用程序,并将实现类型连接到接口。 执行此逻辑的位置称为应用的 组合根,它可以确保依赖项在运行时正确注入。
注释
为了在应用启动期间连接依赖项注入,UI 层项目可能需要引用基础结构项目。 可以通过使用内置支持从程序集加载类型的自定义 DI 容器来消除此依赖项。 就此示例而言,最简单的方法是允许 UI 项目引用基础结构项目(但开发人员应将基础结构项目中类型的实际引用限制为应用的合成根目录)。
单体式应用程序和容器
可以构建基于单一部署的单一部署 Web 应用程序或服务,并将其部署为容器。 在应用程序中,它可能不是整体式的,而是组织成多个库、组件或层。 在外部,它是具有单个进程、单个 Web 应用程序或单个服务的单个容器。
若要管理此模型,请部署单个容器来表示应用程序。 若要进行扩展,只需在前端添加带有负载均衡的额外副本即可。 简单性来自管理单个容器或 VM 中的单个部署。
可以在每个容器中包含多个组件/库或内部层,如图 5-13 所示。 但是,遵循容器原则(“一个容器在一个进程中做一件事”),整体模式可能成为冲突。
当应用程序增长并需要扩展时,此方法的缺点就会显现。 如果整个应用程序能够扩展,那么就不是问题。 但是,在大多数情况下,应用程序的一些部分是需要扩展的瓶颈,而其他组件则使用较少。
使用典型的电子商务示例,可能需要扩展的内容是产品信息模块。 更多的客户浏览产品而不是购买。 相比使用支付流程,更多的客户使用他们的篮子。 较少客户添加评论或查看其购买历史记录。 在单个区域中,你可能只有少数员工需要管理内容和市场营销活动。 通过缩放整体式设计,可多次部署所有代码。
除了“缩放所有内容”问题外,对单个组件的更改还需要对整个应用程序进行完整重新测试,以及所有实例的完整重新部署。
整体式方法很常见,许多组织都使用此体系结构方法进行开发。 许多人的成绩足够好,而另一些人则达到了极限。 许多人在此模型中设计了他们的应用程序,因为工具和基础结构太难构建面向服务的体系结构(SOA),在应用增长之前,他们才看到需求。 如果你发现自己达到了单体架构的限制,下一步的合理做法可能是通过拆分应用以更好地利用容器和微服务。
可以使用每个实例的专用 VM 在 Microsoft Azure 中部署整体应用程序。 使用 Azure 虚拟机规模集,可以轻松缩放 VM。 Azure 应用服务 可以运行整体式应用程序并轻松缩放实例,而无需管理 VM。 Azure 应用服务还可以运行 Docker 容器的单个实例,从而简化部署。 使用 Docker,可以将单个 VM 部署为 Docker 主机,并运行多个实例。 使用 Azure 均衡器,如图 5-14 所示,可以管理缩放。
可以使用传统部署技术管理对各种主机的部署。 Docker 主机可以使用手动执行的 docker 运行 等命令进行管理,也可以通过自动化(例如持续交付(CD)管道进行管理。
部署为容器的整体式应用程序
使用容器管理整体应用程序部署有好处。 与部署其他 VM 相比,缩放容器的实例要快得多且更容易。 即使使用虚拟机规模集来缩放 VM,也需花费一段时间才能创建。 部署为应用实例时,应用配置作为 VM 的一部分进行管理。
将更新以 Docker 映像形式部署不仅速度快,而且网络资源利用效率很高。 Docker 映像通常会在几秒内启动,加快了推出速度。 拆解 Docker 实例与发出 docker stop
命令一样简单,通常只需不到一秒钟即可完成。
由于容器在设计上本质上是不可变的,因此永远不需要担心损坏的 VM,而更新脚本可能会忘记考虑磁盘上留下的某些特定配置或文件。
可以将 Docker 容器用于简单 Web 应用程序的整体部署。 此方法可改进持续集成和持续部署管道,并帮助实现部署到生产的成功。 不再“它在我的计算机上工作,为什么它在生产中不起作用?”
基于微服务的体系结构具有许多优势,但这些优势以增加复杂性为代价。 在某些情况下,成本超过了优势,因此单一部署应用程序在单个容器或只有几个容器中运行是更好的选择。
整体式应用程序可能不容易分解为分隔良好的微服务。 微服务应彼此独立工作,以提供更具弹性的应用程序。 如果无法提供应用程序的独立功能切片,则分离它只会增加复杂性。
应用程序可能尚不需要独立缩放功能。 许多应用程序在需要扩展到超出单个实例时,可以通过相对简单的过程克隆整个实例来实现。 将应用程序分离成各自分散的服务不仅需增加额外工作量,且收效甚微,相比之下,缩放应用程序的完整实例则既简单又节约成本。
在应用程序开发初期,你可能尚未明确了解自然功能边界的位置。 开发最小可独立产品时,自然分离可能尚未出现。 其中一些条件可能是暂时的。 首先,可以创建一个整体式应用程序,然后分离一些要开发并部署为微服务的功能。 其他条件可能对应用程序的问题空间至关重要,这意味着应用程序可能永远不会分解为多个微服务。
将应用程序分离成许多离散进程也会产生开销。 将功能分离到不同的进程时,复杂性更高。 通信协议变得更加复杂。 在服务之间必须使用异步通信,而不得使用方法调用。 迁移到微服务体系结构时,需要添加在 eShopOnContainers 应用程序的微服务版本中实现的许多构建基块:事件总线处理、消息复原和重试、最终一致性等。
较为简单的 eShopOnWeb 参考应用程序支持单容器整体化容器应用。 该应用程序包括一个 Web 应用程序,其中包括传统的 MVC 视图、Web API 和 Razor Pages。 (可选)可以运行应用程序的基于 Blazor 的管理员组件,这还需要单独的 API 项目才能运行。
可以使用 docker-compose build
和 docker-compose up
命令从解决方案根目录启动应用程序。 此命令配置用于 Web 实例的容器,使用在 Web 项目的根目录中找到的 Dockerfile
,并在指定端口上运行该容器。 可以从 GitHub 下载此应用程序的源,并在本地运行它。 即使是这种整体应用程序,也受益于在容器环境中部署。
一是容器化部署意味着应用程序的每个实例都在同一环境中运行。 此方法包括开发人员环境,在该环境中进行早期测试和开发。 开发团队可以在与生产环境匹配的容器化环境中运行应用程序。
此外,容器化应用程序以较低的成本横向扩展。 使用容器环境比传统虚拟机环境实现更高效的资源共享。
最后,容器化应用程序会强制业务逻辑与存储服务器之间分离。 随着应用程序横向扩展,多个容器将全部依赖于单个物理存储介质。 此存储介质通常是运行 SQL Server 数据库的高可用性服务器。
Docker 支持
该项目 eShopOnWeb
在 .NET 上运行。 因此,它可以在基于 Linux 的容器或基于 Windows 的容器中运行。 请注意,对于 Docker 部署,需要对 SQL Server 使用相同的主机类型。 基于 Linux 的容器允许更小的占用空间,并且是首选容器。
可以通过右键单击 解决方案资源管理器 中的项目并选择 “添加>Docker 支持”,使用 Visual Studio 2017 或更高版本将 Docker 支持添加到现有应用程序。 此步骤添加所需的文件,并修改项目以使用这些文件。 当前 eShopOnWeb
示例已经包含这些文件。
解决方案级 docker-compose.yml
文件包含有关要生成的映像以及要启动哪些容器的信息。 该文件允许你使用 docker-compose
命令同时启动多个应用程序。 在这种情况下,它只会启动 Web 项目。 还可以使用它来配置依赖项,例如单独的数据库容器。
version: '3'
services:
eshopwebmvc:
image: eshopwebmvc
build:
context: .
dockerfile: src/Web/Dockerfile
environment:
- ASPNETCORE_ENVIRONMENT=Development
ports:
- "5106:5106"
networks:
default:
external:
name: nat
该docker-compose.yml
文件在Dockerfile
项目中引用了Web
。
Dockerfile
用于指定使用的基本容器以及应用程序在其上的配置方式。
Web
' Dockerfile
:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app
COPY *.sln .
COPY . .
WORKDIR /app/src/Web
RUN dotnet restore
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/src/Web/out ./
ENTRYPOINT ["dotnet", "Web.dll"]
Docker 故障处理
运行容器化应用程序后,它会继续运行,直到停止它。 您可以使用 docker ps
命令查看哪些容器正在运行。 可以使用命令并指定容器 ID 来停止正在运行的容器 docker stop
。
请注意,运行 Docker 容器可能会占用你在开发环境中可能希望使用的端口。 如果尝试使用与正在运行的 Docker 容器相同的端口运行或调试应用程序,将收到一个错误,指出服务器无法绑定到该端口。 再次停止容器可能可以解决此问题。
如果要使用 Visual Studio 将 Docker 支持添加到应用程序,请确保 Docker Desktop 在执行此作时正在运行。 如果在启动向导时 Docker Desktop 未运行,向导将无法正确运行。 此外,向导会检查当前的容器选择,以添加正确的 Docker 支持。 如果要添加对 Windows 容器的支持,则需要在配置了 Windows 容器的情况下运行 Docker Desktop 时运行向导。 若要为 Linux 容器添加支持,请在运行配置了 Linux 容器的 Docker 时运行向导。
其他 Web 应用程序体系结构样式
- Web-Queue-Worker:此体系结构的核心组件是一个 Web 前端,用于处理客户端请求,以及一个执行资源密集型任务、长时间运行的工作流或批处理作业的工作者。 Web 前端通过消息队列与辅助角色进行通信。
- N 层:N 层体系结构将应用程序划分为逻辑层和物理层。
- 微服务:微服务体系结构由一组小型自治服务组成。 每个服务都是自包含服务,并且应在边界上下文中实现单个业务功能。
参考 - 常见 Web 体系结构
-
干净体系结构
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html -
洋葱体系结构
https://jeffreypalermo.com/blog/the-onion-architecture-part-1/ -
存储库模式
https://deviq.com/repository-pattern/ -
清晰架构解决方案模板
https://github.com/ardalis/cleanarchitecture -
构建微服务电子书
https://aka.ms/MicroservicesEbook -
DDD(域驱动设计)
https://learn.microsoft.com/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/