引言
在本章中,我们将讨论每位开发人员在分布式应用程序开发过程中面临的挑战和困难,以及 .NET 产品团队如何通过 Project Tye 来解决这些挑战。这是一个伟大的实验,旨在通过一个命令运行多个服务来简化微服务开发。Project Tye 持续了两年,通过收集和整合反馈,最终促成了 .NET Aspire 的诞生。我们将在接下来的章节中探讨所有细节。
结构
在本章中,我们将讨论以下主题:
- 常见的分布式应用程序难题
- .NET Aspire 的历史
- .NET Aspire
- .NET Aspire 模板
- 参考架构
目标
在本章结束时,你将了解我们为什么需要 .NET Aspire,它将解决构建分布式微服务中的哪些常见问题,使用入门模板创建一个示例 .NET Aspire 项目,并了解包括监控和设置在内的各种细节。你还将学习一个使用微服务架构模式的实用 Eshop 应用程序的参考架构。这些知识将帮助你学习本书的其他章节。
常见的分布式应用程序难题
编写分布式应用程序并非易事。从本地开发到生产环境,有许多挑战需要面对。
思考一个非常简单的分布式应用程序。我们来讨论一下,我们有一个显示一些天气预报的前端,一个暴露这些数据的 API,以及一个前端可以保存在后端的缓存。
一些会想到的问题是:
- API 的 URL 是什么?
- 缓存的 URL 是什么?
- 我们应该使用哪个端口?
- 我如何在本地运行所有东西?
- 我如何将我的应用程序部署到生产环境?
如果这听起来很熟悉,那么你并不孤单。每个开发人员都在为这些问题而苦苦挣扎,许多人已经尝试解决它们。
.NET Aspire 的历史
.NET 产品组首先尝试通过 Project Tye 来解决这些问题。它是 .NET 工具,具有以下明确目标:
- 通过以下方式使微服务的开发更容易:
- 用一个命令运行多个服务。
- 在容器中使用依赖项。
- 使用简单的约定发现其他服务的地址。
- 通过以下方式自动化 .NET 应用程序到 Kubernetes 的部署:
- 自动容器化 .NET 应用程序。
- 以最少的知识或配置生成 Kubernetes 清单。
- 使用单个配置文件。
分析这个工具的目标,我们可以清楚地看到,其目的仅仅是为上面提到的大多数难题找到答案。
使用 Project Tye,我们可以定义我们分布式应用程序的所有不同资源,包括项目和容器(如 Redis),并将它们联系在一起以在本地运行所有内容。它甚至提供了诸如配置之类的扩展包,以读取需要时的某些环境变量。
dotnet add frontend/frontend.csproj package Microsoft.Tye.Extensions.Configuration --version "0.2.0-*"
//...
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
/** Add the following to wire the client to the backend **/
services.AddHttpClient<WeatherClient>(client =>
{
client.BaseAddress = Configuration.GetServiceUri("backend");
});
/** End added code **/
}
注意: 上述代码是前端项目
startup.cs的摘录。
使用 Tye CLI 运行我们的解决方案将产生一个包含我们所有资源的仪表板(如图1.1所示):
前端应用程序的详细相关日志如图1.2所示:
Project Tye 是一个持续了大约两年的伟大实验。产品组收集了反馈并决定在其基础上进行构建和改进,这导致了 .NET Aspire 的创建。许多来自 Project Tye 的概念被保留了下来。一些基础概念,如用一个命令运行多个服务、在容器中使用依赖项以及服务发现,都被继承到了 .NET Aspire 中,并根据开发阶段收集的反馈进行了改进。
.NET Aspire
.NET Aspire 是一个构建在 .NET 之上的云原生应用程序栈。它旨在简化云原生应用程序的开发、部署和管理过程。它提供了一套全面的工具和功能,有助于整个开发生命周期,并提高云原生应用程序的性能和可靠性。以下是来自官方 Microsoft Learn 文档的 .NET Aspire 定义:
.NET Aspire 是一个有主见的、为云而生的技术栈,用于构建可观察的、生产就绪的分布式应用程序。
–.NET Aspire 团队
让我们逐字逐句地分解上面的句子,以便更好地理解它:
- 有主见的: 这意味着 .NET Aspire 有特定的做事方式,它通过提供特定的指导方针、最佳实践和它强制执行的约定来做到这一点。它就如何使用 .NET Aspire 框架来架构、实现和维护软件提供了强有力的建议。
- 云就绪: 云就绪或云原生应用程序主要设计用于在云环境(如 Azure、AWS 或 Google Cloud)中良好工作。它旨在默认利用云计算特性,如可伸缩性、可用性、性能和安全性。
- 可观察的: 这些应用程序易于监控,通过提供对其内部状态、行为和性能的洞察。
- 生产就绪: 这些应用程序已准备好部署到生产环境。它们可靠、可扩展且安全,能够满足生产应用程序的需求。
- 分布式应用程序: 这是指在多台计算机或服务器上运行的应用程序,通常分布在不同位置,它们协同工作以代表一个单一的系统。
解决方案
.NET Aspire 的创建主要是为了解决分布式应用程序开发人员的痛点,他们在本地开发机器上运行和调试多个云原生应用程序时面临问题。它简化了配置管理和依赖注入,这有助于设置 .NET Aspire 支持分布式跟踪,以可视化跨微服务的每个请求流,这有助于调试 .NET Aspire 在以下方面提供帮助:
- 编排: .NET Aspire 提供了用于运行和连接多项目应用程序及其依赖项以用于本地开发环境的功能。
- 集成: .NET Aspire 集成是用于常用服务(如 Redis 或 Postgres)的 NuGet 包,具有标准化的接口,确保它们一致且无缝地与你的应用程序连接。
- 工具: .NET Aspire 附带了用于 Visual Studio 和 .NET 命令行界面(CLI)的项目模板和工具体验,可帮助你创建和与 .NET Aspire 应用程序交互。与流行的日志记录和遥测框架(例如,Serilog 和 OpenTelemetry)的集成有助于监控和故障排除。
- 云原生部署: .NET Aspire 通过与 Kubernetes 等容器编排平台的无缝集成,简化了云原生应用程序的部署和管理。
- 安全性: .NET Aspire 的内置安全功能和与行业标准安全框架的集成有助于保护你的数据并构建弹性的云原生应用程序。它强制执行 HTTPS(安全通信协议)以防止数据篡改。
.NET Aspire 模板
.NET Aspire 提供了几种不同的模板。这些可以分为我们可以用来轻松开始将我们的代码与 Aspire 集成的模板,我们可以用来查看 Aspire 常见模式的模板,以及用于单元测试的模板。你可以通过键入以下内容来列出所有这些模板:
dotnet new --list
以下是可用模板的列表:
- 集成:
- .NET Aspire 应用主机:
aspire-apphost - .NET Aspire 空应用:
aspire - .NET Aspire 服务默认值:
aspire-servicedefaults
- .NET Aspire 应用主机:
- 入门模板:
- .NET Aspire 入门应用:
aspire-starter
- .NET Aspire 入门应用:
- 单元测试:
- .NET Aspire 测试项目 (MSTest):
aspire-mstest - .NET Aspire 测试项目 (NUnit):
aspire-nunit - .NET Aspire 测试项目 (xUnit):
aspire-xunit
- .NET Aspire 测试项目 (MSTest):
.NET Aspire 入门应用是我们在本章节中要看的一个。我们可以使用 Visual Studio、Visual Studio Code 或 .NET CLI 创建一个新的 .NET Aspire 入门应用程序。
使用命令行界面
让我们首先打开一个新的终端会话并运行以下命令来创建一个空文件夹并创建我们的入门应用:
mkdir FirstApp
cd FirstApp
dotnet new aspire-starter
我们可以附加标志 --use-redis-cache 来为我们的入门应用添加 Redis 集成,但我们将在以后的章节中看到这一点。
使用 Visual Studio 2022
在 Visual Studio 的顶部,导航到 文件 | 新建 | 项目。在对话框窗口中(如图1.3所示),搜索 Aspire 并选择 .NET Aspire 入门应用 模板。
单击 下一步 以添加项目名称和其他信息,如图1.4所示:
在我们首选的编辑器中打开创建的解决方案后,我们将在 解决方案资源管理器 中看到四个项目,如以下图1.5所示:
- FirstApp.ApiService
- FirstApp.AppHost
- FirstApp.ServiceDefaults
- FirstApp.Web
如果你熟悉 .NET,你可能已经知道 FirstApp.Web 和 FirstApp.ApiService。FirstApp.Web 是经典的 Blazor 前端,它从 FirstApp.ApiService 读取天气数据,这是一个返回随机天气预报的 .NET 最小应用程序编程接口(API)项目。
尽管如此,这两个项目都包含一些你可能从未见过的代码行,这些代码来自 .NET Aspire,特别是来自 FirstApp.ServiceDefaults。这个项目是 .NET Aspire 的有主见的部分,是实现一系列最佳实践的扩展方法的集合。让我们将这个项目分解为所有不同的方法:
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
// 默认开启弹性
http.AddStandardResilienceHandler();
// 默认开启服务发现
http.AddServiceDiscovery();
});
// 取消注释以下内容以限制允许的服务发现方案。
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });
return builder;
}
AddServiceDefaults,顾名思义,将默认服务添加到调用它的 ApplicationBuilder 中。这些服务如下:
- 遥测
- 健康检查
- 服务发现
- HttpClient 的默认配置
你可能会注意到,限制允许的方案为仅 HTTPS 的代码被注释掉了。这是因为这是一个不能总是应用的设置,但如果可能的话应该应用。这就是为什么 ServiceDefaults 项目不是 NuGet 包,而是一个项目,以便开发人员可以自由扩展和适应其需求。
另一个例子可以在 ConfigureOpenTelemetry 方法和 AddOpenTelemetryExporters 中找到,开发人员可以在其中选择是否在 HttpClientInstrumentation 之上启用 GrpcClientInstrumentation,并在默认导出器(由 .NET Aspire 本身提供)之上添加其他导出器。
我们可以找到的另一个有用的方法是 MapDefaultEndoints,如下所示:
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// 在非开发环境中向应用程序添加健康检查端点具有安全隐患。
// 在非开发环境中启用这些端点之前,请参阅 https://aka.ms/dotnet/aspire/healthchecks 了解详细信息。
if (app.Environment.IsDevelopment())
{
// 所有健康检查必须通过,应用程序才能在启动后被视为准备好接受流量
app.MapHealthChecks("/health");
// 只有标记为“live”的健康检查必须通过,应用程序才能被视为活动状态
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
此方法用于扩展 WebApplication 对象,因为它映射了标准的健康检查和活动端点。
在 ServiceDefaults 项目中拥有这些方法是不够的。我们需要在需要的地方调用它们,从 FirstApp.ApiService 的 Program.cs 开始,我们可以看到它们在哪里被使用:
var builder = WebApplication.CreateBuilder(args);
// 添加服务默认值和 Aspire 客户端集成。
builder.AddServiceDefaults();
// 向容器添加服务。
builder.Services.AddProblemDetails();
// 了解有关在 https://aka.ms/aspnet/openapi 配置 OpenAPI 的更多信息
builder.Services.AddOpenApi();
...
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.MapDefaultEndpoints();
app.Run();
同样的情况也发生在 FirstApp.Web 项目的 Program.cs 中。在前端项目的 Program.cs 中,我们还可以看到使用 .NET Aspire 的第一个好处,如果我们看一下 API 的 HttpClient 是如何创建的:
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
// 此 URL 使用 "https+http://" 来表示 HTTPS 优先于 HTTP。
// 在 https://aka.ms/dotnet/sdschemes 了解有关服务发现方案解析的更多信息。
client.BaseAddress = new("https+http://apiservice");
});
如你所见,没有端点,没有 localhost,也没有指定的端口。只有字符串 https+http://apiservice。这怎么可能呢?这不是我们只能通过 .NET Aspire 实现的。如果正确配置,.NET 支持服务发现,这就是 .NET Aspire 通过 AddServiceDefaults 方法为我们所做的。
问题:部署到生产环境时,服务发现如何实现? 从应用配置文件中检索?
.NET 具有一旦配置了服务发现,就可以从 appsettings.json 文件中检索某些资源的端点和连接字符串的能力:
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Services": {
"apiservice": {
"https": [
"https://localhost:44300"
]
}
},
"AllowedHosts": "*"
在 appsettings.json 文件中拥有这样的配置将允许我们为我们的 apiservice 添加一个 HttpClient,就像我们上面所做的那样。如果你查看 FirstApp.Web 项目的 appsettings.json,你将找不到类似的东西。那么,这怎么可能呢?
好吧,当我们运行 FirstApp.AppHost 项目时,我们会看到 .NET Aspire 将 apiservice 的正确 URL 作为前端的环境变量注入。它将使用 services_apiservice_http_0 的格式。环境变量的这种线性格式映射了 json 的整个结构。最后的 0 是必需的,因为 http 字段接受的值是一个数组。
问题:在部署时,是否也要配置环境变量?
由于我们在代码级别的 apiservice URL 中有 https+http,因此 .NET 服务发现将优先使用可用的 HTTPs 端点。
作为最后一步,让我们看一下 FirstApp.AppHost 项目。这是我们应用程序的入口点,并包含一个 Program.cs,如下所示:
var builder = DistributedApplication.CreateBuilder(args);
var apiService = builder.AddProject<Projects.FirstApp_ApiService>("apiservice");
builder.AddProject<Projects.FirstApp_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService)
.WaitFor(apiService);
builder.Build().Run();
该结构与每个其他 .NET 应用程序非常相似。因此,我们有一个构建器。在这种情况下,我们正在构建一个 DistributedApplication。我们正在使用 .NET Aspire 为我们创建的静态类来描述我们的项目,从而将两个项目添加到我们的构建器中。我们可以如下探索 FirstApp_ApiService:
namespace Projects;
[global::System.CodeDom.Compiler.GeneratedCode("Aspire.Hosting", null)]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification = "Generated code.")]
[global::System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, ProjectPath = {ProjectPath}")]
public class FirstApp_ApiService : global::Aspire.Hosting.IProjectMetadata
{
public string ProjectPath => """D:\src\FirstApp\FirstApp.ApiService\FirstApp.ApiService.csproj""";
}
除了编译器使用的少数属性外,此类还公开了项目的本地路径。它是为我们生成的,并且每次都会更改。这比拥有本地硬编码路径要好得多。
回到 AppHost 的 Program.cs,我们可以看到,由于 webfrontend 资源需要访问 apiservice,我们可以简单地将 ProjectResource 分配给一个变量,然后使用 WithReference 方法。这将使 .NET Aspire 在运行时将 services_apiservice_http_0 环境变量注入到 webfrontend 资源中。
WaitFor 方法允许 .NET Aspire 等待 API 服务健康后再启动 webfrontend。
如果我们运行 AppHost 项目的 Program.cs,我们的浏览器将打开此仪表板,显示我们所有正在运行的资源,如图1.6所示:
你机器上的端口可能(而且很可能)会不同。这是因为 .NET Aspire 和 .NET 服务发现,我们不再需要硬编码任何端点。
通过单击 webfrontend 行,我们可以探索我们资源的所有环境变量和配置,如图1.7所示:
注意: 我们有
apiservice的 HTTP 和 HTTPS 端点,以及一个 OpenTelemetry 端点。
你会注意到,在左侧,我们有结构化日志、跟踪和指标。我们的资源已由 ServiceDefaults 检测,.NET Aspire 已经为我们提供了 OpenTelemetry 端点和查看指标的仪表板。
如果我们在前端导航时产生一些流量,我们就可以探索我们的跟踪,如图1.8所示:
通过单击涉及 webfrontend 和 API 服务的 GET /weather 调用,我们可以探索所有的网络跟踪,如图1.9所示:
问题:是只能跟踪由前端项目发起请求?在 ApiService 中使用 Swagger 发起请求并不会在日志中追踪?
通过单击右上角的 查看日志 链接,我们可以探索与此跟踪 ID 相关的 结构化日志,如图1.10所示:
在左侧的控制台选项卡中,我们可以探索来自我们所有正在运行的资源的实时日志,最后,我们有如图1.11所示的指标:
参考架构
本书提供了一个使用微服务架构模式构建的动手实践应用程序。该应用程序围绕一个 Eshop 应用程序展开,使用户能够提交订单和接收货物。
下图显示了我们将在本书各章中构建的解决方案的架构(请参阅图1.12)。
解决方案的主要组件如下:
- Web 应用程序 UI (前端) 使用 React 开发。用户界面允许客户浏览不同的产品、创建订单和查看订单状态。前端与后端服务通信以实时获取和更新产品信息。
- 仓库 API (后端),使用 .NET 开发,处理库存管理。当向仓库 API 发出请求时,它将从仓库读取仓库状态和来自订单数据库的当前待处理订单。它将确保从仓库中的可用库存中删除来自待处理订单的物品。
- 创建订单 API (后端),使用 Golang 开发。当向创建订单 API 发出请求时,它将在订单数据库中创建一个新订单并将其设置为待处理。此外,它会向发布/订阅消息系统发布消息,从而启动订单处理管道。
- 支付 API (后端) 使用 Python 开发。它侦听来自发布/订阅系统的事件,当向支付 API 发出请求时,它会将订单状态设置为处理中。
- 发货 API (后端) 使用 Node.js 开发。一旦订单标记为已付款,此服务负责准备发货,并将订单数据库中的状态更新为已完成。
使用创建订单 API、支付 API 和发货 API 实现的订单处理队列是使用发布/订阅架构实现的,在本地开发中利用 Redis 缓存,在部署期间利用 Azure Service Bus。此应用程序中使用了两个数据库。它们如下:
- 仓库数据库: SQL 保存仓库的数据。
- 订单数据库: SQL 保存待处理、处理中和已完成订单的订单历史记录。
数据库的数据层是使用 数据 API 构建器 (DAB) 实现的。DAB 为你的数据库提供现代的 具象状态传输 (REST) 和 GraphQL 端点,并支持关系型和 NoSQL。DAB 主要为开发人员设计。DAB 是跨平台的、开源的,并且独立于语言和框架。DAB 需要零代码和单个配置文件。有关学习 DAB 的更多详细信息,请参阅微软的官方文档 https://learn.microsoft.com/en-us/azure/data-api-builder/。
结论
在本章中,我们探讨了开发人员在本地运行分布式应用程序时面临的问题,介绍了 .NET Aspire,并解释了它如何解决问题。我们还介绍了 .NET Aspire 入门模板以帮助入门。此外,我们通过引入构建真实世界应用程序的架构,为 .NET Aspire 功能的实际探索奠定了基础。
在下一章中,我们将讨论 .NET Aspire 清单和监控功能。我们还将讨论 .NET Aspire 功能并从头开始构建一个应用程序,在接下来的章节中提供动手实践经验。
到本书结束时,你将对使用 .NET Aspire 构建云原生应用程序有扎实的理解。