.NET Aspire:以正确的方式构建云原生应用程序

· 4 分钟阅读

如果您曾经在 .NET 中构建过分布式应用程序,那么您就知道该怎么做。您启动一个 Web API,添加一个后台工作程序,添加 Redis 进行缓存,使用 PostgreSQL 进行持久化,也许使用 RabbitMQ 进行消息传递 — 突然之间,您花在连接基础设施上的时间比编写业务逻辑的时间还多。连接字符串分散在 appsettings.json 文件中,您忘记配置的运行状况检查,可观察性始终是“下一个冲刺的问题”。

我去过那里。比我愿意承认的次数还要多。

这正是 .NET Aspire 旨在解决的问题。在生产中运行了几个月后,我想分享我所学到的东西——好的、伟大的和陷阱。

什么是 .NET Aspire?

.NET Aspire 是一个固执己见的堆栈,用于使用 .NET 构建可观察的、可用于生产的分布式应用程序。它不是传统意义上的框架 - 它不会取代 ASP.NET Core 或强迫您采用新的编程模型。相反,它建立在您已知的知识之上,并填补了构建云原生应用程序时一直存在的空白。

Aspire 的核心为您提供四件事:

  1. AppHost — 定义整个分布式应用程序拓扑的项目。存在哪些服务、它们依赖什么以及它们如何连接。
  2. 服务默认值 — 一个共享项目,可为您的所有服务一次性配置健康检查、弹性策略和 OpenTelemetry 等横切关注点。
  3. 组件 — NuGet 包,提供与 Redis、PostgreSQL、RabbitMQ、Azure 存储等支持服务的标准化集成。
  4. 开发人员仪表板 — 一个实时 UI,显示本地开发期间整个分布式应用程序的日志、跟踪和指标。

这个理念很简单:如果每个 .NET 云应用程序都需要这些东西,为什么我们每次都从头开始实现它们?

设置您的第一个 Aspire 项目

入门非常简单。您需要 .NET 8 或更高版本并安装 Aspire 工作负载:

dotnet workload update
dotnet workload install aspire

现在创建一个新的 Aspire 入门项目:

dotnet new aspire-starter -n MyCloudApp

这会生成一个包含四个项目的解决方案:

  • MyCloudApp.AppHost — 协调器
  • MyCloudApp.ServiceDefaults — 共享配置
  • MyCloudApp.ApiService — 示例 Web API
  • MyCloudApp.Web — Blazor 前端

运行 AppHost,您将立即看到 Aspire 仪表板在浏览器中打开,显示您的所有服务、日志及其运行状况。没有 Docker Compose 文件。无需手动端口管理。它就是有效的。

dotnet run --project MyCloudApp.AppHost

第一次经历让我着迷。不到一分钟的时间,您就拥有了一个完全精心编排的分布式应用程序,并且具有可观察性。

AppHost 模式

AppHost 是神奇之处。它是一个小型控制台应用程序,使用构建器模式来定义分布式应用程序的拓扑 - 存在哪些资源以及服务如何连接到它们。

现实的 AppHost 是这样的:

var builder = DistributedApplication.CreateBuilder(args);

// Infrastructure resources
var postgres = builder.AddPostgres("postgres")
    .WithPgAdmin()
    .AddDatabase("catalogdb");

var redis = builder.AddRedis("cache");

var rabbitmq = builder.AddRabbitMQ("messaging")
    .WithManagementPlugin();

// Application services
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReference(rabbitmq);

var orderApi = builder.AddProject<Projects.OrderApi>("order-api")
    .WithReference(postgres)
    .WithReference(rabbitmq);

var frontend = builder.AddProject<Projects.WebFrontend>("frontend")
    .WithExternalHttpEndpoints()
    .WithReference(catalogApi)
    .WithReference(orderApi);

builder.Build().Run();
```大声读出该代码。它实际上记录了自己。 “目录 API 引用了 PostgreSQLRedis  RabbitMQ。前端引用了目录 API 和订单 API。”这就是您的架构,用代码表达。

有几点值得注意:

- **`WithReference`** 承担繁重的工作。它通过环境变量和配置自动将连接字符串和服务 URL 注入到使用项目中。您的服务不需要知道 Redis 在“哪里”运行  Aspire 会处理它。
- **`WithPgAdmin()`**  **`WithManagementPlugin()`** 在实际服务旁边启动 PostgreSQL  RabbitMQ 的管理 UI。在开发过程中,这些都是无价的。
- **`WithExternalHttpEndpoints()`** 将服务标记为可外部访问,这在部署时很重要。

### 资源配置

您可以通过细粒度控制来配置资源:

```csharp
var postgres = builder.AddPostgres("postgres")
    .WithEnvironment("POSTGRES_MAX_CONNECTIONS", "200")
    .WithDataVolume("postgres-data")
    .AddDatabase("catalogdb");

var redis = builder.AddRedis("cache")
    .WithDataVolume("redis-data");

数据量确保您的本地开发数据在容器重新启动后仍然存在。小细节,大生活质量改善。

服务默认值:无名英雄

ServiceDefaults 项目是 Aspire 最被低估的部分。它是一个共享库,解决方案中的每个服务都会引用它,并且它配置了您可能会忘记或不一致实现的所有横切关注点。

ServiceDefaults 中的典型 Extensions.cs 如下所示:

public static class Extensions
{
    public static IHostApplicationBuilder AddServiceDefaults(
        this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();
        builder.AddDefaultHealthChecks();
        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            http.AddStandardResilienceHandler();
            http.AddServiceDiscovery();
        });

        return builder;
    }

    public static IHostApplicationBuilder ConfigureOpenTelemetry(
        this IHostApplicationBuilder builder)
    {
        builder.Logging.AddOpenTelemetry(logging =>
        {
            logging.IncludeFormattedMessage = true;
            logging.IncludeScopes = true;
        });

        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation();
            })
            .WithTracing(tracing =>
            {
                tracing.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddGrpcClientInstrumentation();
            });

        builder.AddOpenTelemetryExporters();
        return builder;
    }

    public static IHostApplicationBuilder AddDefaultHealthChecks(
        this IHostApplicationBuilder builder)
    {
        builder.Services.AddHealthChecks()
            .AddCheck("self", () => HealthCheckResult.Healthy(),
                ["live"]);

        return builder;
    }
}

然后在每个服务的 Program.cs 中,一行就完成了这一切:

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();

// Your service-specific configuration...

var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();

单个 AddServiceDefaults() 调用将为您提供:

  • OpenTelemetry 具有结构化日志记录、指标和分布式跟踪
  • 健康检查以及活性和就绪端点
  • 服务发现,以便服务可以通过名称找到彼此
  • 针对所有传出 HTTP 调用的弹性策略(重试、断路器、超时)

这是将“在我的机器上运行”演示与生产就绪系统区分开来的东西。 Aspire 将其设为默认设置,而不是事后添加。

Aspire 组件

Aspire 组件是 NuGet 包,可标准化您的服务连接到支持基础设施的方式。它们不仅仅是客户端库,还包括运行状况检查、日志记录、跟踪和开箱即用的可配置弹性。

Redis

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddRedisDistributedCache("cache");

就是这样。连接字符串通过 WithReference 来自 AppHost。该组件注册由 Redis 支持的 IDistributedCache,并已连接健康检查和 OpenTelemetry 仪器。

您还可以使用 Redis 进行输出缓存:

builder.AddRedisOutputCache("cache");

带有 Entity Framework Core 的 PostgreSQL

builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb", settings =>
{
    settings.DisableRetry = false;
});

这会将您的 DbContext 注册为与 AppHost 中定义的 catalogdb 数据库的连接。它包括连接池、健康检查和重试策略。

RabbitMQ

builder.AddRabbitMQClient("messaging");

从 RabbitMQ 客户端库注册 IConnection,已完全配置并进行运行状况检查。

Azure 集成

Aspire 还拥有用于 Azure 服务的一流组件:

// Azure Blob Storage
builder.AddAzureBlobClient("blobs");

// Azure Service Bus
builder.AddAzureServiceBusClient("servicebus");

// Azure Key Vault
builder.AddAzureKeyVaultClient("secrets");
```模式始终相同:AppHost 中的一行用于定义资源,消费服务中的一行用于使用它。连接详细信息会自动流动。

## 开发者仪表板

Aspire 仪表板是那些看起来很不错的功能之一,直到您真正使用它为止,然后您就无法想象没有它会如何工作。

当您在本地运行 AppHost 时,仪表板将启动并为您提供:

- **资源概览**  您的所有服务和基础设施一目了然,并带有状态指示器
- **结构化日志**  来自每个服务的实时日志流,可过滤和可搜索
- **分布式跟踪** - 跨越多个服务的端到端请求跟踪,可视化为火焰图
- **指标**  实时 HTTP 请求率、错误率、延迟和自定义指标
- **控制台日志** - 来自每个容器和项目的原始 stdout/stderr

分布式追踪尤其有价值。当请求到达您的前端、流经目录 API、触及 Redis  PostgreSQL 时,您会看到整个链以及每个跃点的时间。不再猜测瓶颈在哪里。

我发现仪表板在调试过程中最有用。一切都集中在一处,并通过关联 ID 跨服务链接相关事件,而不是跟踪多个终端窗口或在日志文件之间跳转。

仪表板还可以作为独立容器使用,这意味着您可以在临时环境或 CI 管道中使用它:

```bash
docker run --rm -p 18888:18888 \
  mcr.microsoft.com/dotnet/aspire-dashboard:latest

部署

Aspire 在开发过程中有所帮助,但在生产过程中呢?这就是事情变得实用的地方。

Azure 容器应用程序

最直接的部署路径是 Azure 容器应用程序 (ACA),它具有一流的 Aspire 支持。您可以直接使用 Azure 开发人员 CLI 进行部署:

azd init
azd up

azd init 命令检测您的 Aspire AppHost 并生成必要的基础设施即代码。 azd up 根据您的 AppHost 拓扑提供一切内容 — 容器注册表、容器应用程序、数据库、Redis 实例。

您的 AppHost 本质上成为您的部署清单。定义“catalog-api 依赖于 PostgreSQL 和 Redis”的相同代码驱动基础设施配置。

库伯内特斯

对于 Kubernetes 部署,Aspire 不会直接生成清单,但您的 AppHost 拓扑会清晰地映射到 Kubernetes 资源。社区工具 aspirate (Aspir8) 可以从您的 AppHost 生成 Helm 图表或 Kubernetes 清单:

dotnet tool install -g aspirate
aspirate generate
aspirate apply

部署注意事项

需要记住以下几点:- 连接字符串在环境之间发生变化。 在本地,Aspire 会启动容器并自动注入连接字符串。在生产中,您将指向托管服务。 Aspire 使用标准 .NET 配置,因此环境变量和 Azure Key Vault 按预期工作。

  • AppHost 不在生产环境中运行。 它是一个开发和部署编排工具。在生产中,您的服务独立运行,通过环境变量和协调器进行配置。
  • 基础设施资源成为托管服务。 您的本地 PostgreSQL 容器成为 Azure Database for PostgreSQL。本地 Redis 容器将成为 Azure Redis 缓存。消费代码不会改变。

现实世界的技巧

在生产中运行 Aspire 一段时间后,以下是节省我们时间的经验教训:

1. 使用自定义资源生命周期检查

不要仅仅依靠容器启动来确定资源是否准备好。 PostgreSQL 可能会在实际准备好提供查询服务之前接受 TCP 连接。

var postgres = builder.AddPostgres("postgres")
    .AddDatabase("catalogdb")
    .WithHealthCheck();

Aspire 可以对资源运行运行状况检查并保留相关服务,直到它们真正准备好为止。

2. 将常见模式提取到扩展中

如果多个服务共享相似的配置,请创建扩展方法:

public static class AppHostExtensions
{
    public static IResourceBuilder<ProjectResource> AddWorkerService(
        this IDistributedApplicationBuilder builder,
        string name,
        IResourceBuilder<IResourceWithConnectionString> db,
        IResourceBuilder<IResourceWithConnectionString> messaging)
    {
        return builder.AddProject<Projects.WorkerService>(name)
            .WithReference(db)
            .WithReference(messaging)
            .WithReplicas(3);
    }
}

3. 利用 WithReplicas 进行负载测试

var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReplicas(5);

WithReplicas 在本地启动服务的多个实例。这非常适合测试负载平衡、并发错误和分布式缓存行为,而无需部署到集群。

4. 对敏感值使用参数

即使对于本地开发,也不要硬编码凭据:

var dbPassword = builder.AddParameter("db-password", secret: true);

var postgres = builder.AddPostgres("postgres", password: dbPassword)
    .AddDatabase("catalogdb");

在本地运行时,Aspire 将提示输入该值或从用户机密中读取该值。在 CI/CD 中,它来自环境变量。

5. 集成测试变得微不足道

Aspire 包含一个测试包,可以使分布式应用程序的集成测试变得非常简单:

[Fact]
public async Task CatalogApiReturnsProducts()
{
    var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.MyCloudApp_AppHost>();

    await using var app = await appHost.BuildAsync();
    await app.StartAsync();

    var httpClient = app.CreateHttpClient("catalog-api");
    var response = await httpClient.GetAsync("/api/products");

    response.EnsureSuccessStatusCode();

    var products = await response.Content
        .ReadFromJsonAsync<List<Product>>();
    Assert.NotEmpty(products);
}

这会启动您的“整个”分布式应用程序(包括数据库和消息代理),针对它运行测试,然后将其拆除。在 CI 管道中针对真实基础设施进行真正的集成测试。没有嘲笑。

6. 本地监控资源使用情况

在本地运行多个容器时,请密切关注资源消耗。具有管理 UI 的 PostgreSQL、Redis 和 RabbitMQ 实例可以轻松消耗 2-3 GB RAM。如果您使用的是受限计算机,请考虑使用较轻的资源配置:

var redis = builder.AddRedis("cache")
    .WithContainerRuntimeArgs("--memory=256m");

结论

.NET Aspire 从根本上改变了我构建分布式应用程序的方式。并不是因为它引入了革命性的概念——健康检查、OpenTelemetry 和容器编排并不是什么新鲜事。但因为它使它们成为“默认”。它需要您通常必须做出的数百个小决定,使用合理的默认值来实现它们,并允许您在需要时覆盖它们。在我看来,AppHost 模式是最大的胜利。将分布式应用程序拓扑表示为代码(而不是分散在 Docker Compose 文件、Kubernetes 清单和 README 文档中)使系统易于理解。新团队成员可以在AppHost中打开Program.cs并在几分钟内了解整个架构。

如果您正在使用 .NET 构建分布式应用程序,请认真考虑一下 Aspire。从 aspire-starter 模板开始,探索仪表板,并根据需要逐步采用组件。您不必在第一天就全力以赴——Aspire 的设计就是附加的。

花费第一次冲刺来连接基础设施样板的日子已经结束了。让 Aspire 处理管道,以便您可以专注于真正重要的事情:您的应用程序。