适用于 .NET 的新式 Web 应用模式

Azure 应用服务
Azure Front Door
用于 Redis 的 Azure 缓存
.NET

本文介绍如何实现新式 Web 应用模式。 新式 Web 应用模式定义了如何在云中实现 Web 应用的现代化并引入面向服务的体系结构。 新式 Web 应用模式提供规范性的体系结构、代码和配置指南,这些指南符合 Azure 良好架构框架的原则,并基于 Reliable Web 应用模式进行构建。

为什么要使用新式 Web 应用模式?

新式 Web 应用模式可帮助你优化 Web 应用的高需求区域。 它提供了有关分离这些区域的详细指南,这些区域可实现成本优化的独立缩放。 此方法允许将专用资源分配给关键组件,从而提高整体性能。 对可分离的服务进行分离,可以防止应用的某个部分速度变慢影响到其他部分,从而提高可靠性。 分离还允许单独对单个应用组件进行版本控制。

如何实现新式 Web 应用模式

本文包含用于实现新式 Web 应用模式的体系结构、代码和配置指南。 使用以下链接转到所需的指南:

  • 体系结构指南。 了解如何模块化 Web 应用组件,并选择适当的平台即服务(PaaS)解决方案。
  • 代码指南。 实现四种设计模式以优化分离组件:Strangler Fig、Queue-Based 负载调配、竞争使用者和运行状况终结点监视。
  • 配置指南。 为分离的组件配置身份验证、授权、自动缩放和容器化。

提示

GitHub 徽标 有一个新式 Web 应用模式的参考实现(示例应用)。 它表示新式 Web 应用实现的最终状态。 它是一个生产级 Web 应用,具有本文讨论的所有代码、体系结构和配置更新。 部署并使用参考实现来指导你实现新式 Web 应用模式。

体系结构指南

新式 Web 应用模式建立在可靠 Web 应用模式的基础上。 它需要一些额外的体系结构组件来实现。 需要消息队列、容器平台、分离服务数据存储和容器注册表。 下图演示了基线体系结构。

显示新式 Web 应用模式基线体系结构的关系图。

为了实现更高的服务级别目标 (SLO),可以在 Web 应用体系结构中添加第二个区域。 如果添加第二个区域,则需要将负载均衡器配置为将流量路由到该区域,以支持主动-主动或主动-被动配置。 使用中心辐射型网络拓扑来集中和共享资源,例如网络防火墙。 通过中心虚拟网络访问容器存储库。 如果有虚拟机,请将堡垒主机添加到中心虚拟网络,以增强安全性来管理它们。 下图说明了此体系结构。

显示具有第二个区域和中心辐射型网络拓扑的新式 Web 应用模式体系结构的关系图。

分离体系结构

要实现新式 Web 应用模式,需要将现有的 Web 应用体系结构分离。 将体系结构分离涉及将整体式应用程序分解为负责特定特征或功能的小型独立服务。 这一过程包括评估当前的 Web 应用程序、修改体系结构,最后将 Web 应用代码提取到容器平台。 目标是系统地识别和提取从分离中获益最大的应用程序服务。 要实现体系结构分离,请遵循以下建议:

  • 标识服务边界。 应用域驱动的设计原则,以识别整体应用程序中的有限上下文。 每个边界上下文都代表一个逻辑边界,可以作为单独服务的候选对象。 代表不同业务功能且依赖项较少的服务适合进行分离。

  • 评估服务优势。 重点关注从独立缩放中获益最大的服务。 将这些服务分离,并将处理任务从同步转换为异步作可实现更高效的资源管理,支持独立部署,并减少在更新或更改期间影响应用程序其他部分的风险。 例如,可以将订单结帐与订单处理分开。

  • 评估技术可行性。 检查当前体系结构,确定可能影响分离过程的技术限制和依赖项。 规划如何跨服务对数据进行管理和共享。 已分离服务应管理自己的数据,并要尽量减少跨服务边界的直接数据库访问。

  • 部署 Azure 服务。 选择并部署需要支持要提取的 Web 应用服务的 Azure 服务。 有关指南,请参阅 选择正确的 Azure 服务

  • 分离 Web 应用服务。 定义明确的接口和 API,使新提取的 Web 应用服务能够与系统的其他部分进行交互。 设计数据管理策略,允许每个服务管理自己的数据,同时确保一致性和完整性。 有关在此提取过程中要使用的特定实现策略和设计模式,请参阅本文的 “代码指南 ”部分。

  • 对已分离服务使用独立存储。 每个分离的服务都应有自己的独立数据存储,以促进独立的版本控制、部署和可伸缩性,并维护数据完整性。 例如,引用实现将票证呈现服务与 Web API 分离,并且无需服务访问 API 的数据库。 相反,该服务通过 Azure 服务总线消息将票证映像生成回 Web API 的 URL,API 会保留其数据库的路径。

  • 为每个已分离服务实现单独的部署管道。 独立的部署管道使每项服务都能以自己的速度更新。 如果公司内不同的团队或组织拥有不同的服务,那么拥有独立的部署管道就能让每个团队控制自己的部署。 使用 Jenkins、GitHub Actions 或 Azure Pipelines 等持续集成和持续交付(CI/CD)工具设置这些管道。

  • 修订安全控制。 确保更新安全控制以适应新的体系结构,包括防火墙规则和访问控制。

选择正确的 Azure 服务

对于体系结构中的每个 Azure 服务,请查阅“架构良好的框架”中的相关 Azure 服务指南。 对于新式 Web 应用模式,需要一个支持异步消息传递的消息传递系统、一个支持容器化的应用程序平台和一个容器映像存储库。

  • 选择消息队列。 消息队列是面向服务的体系结构的重要组成部分。 它使消息发送方与接收方分离,从而实现异步消息传送。 使用有关选择 Azure 消息传送服务的的指导来选择支持你的设计需求的 Azure 消息传送系统。 Azure 有三个消息传送服务:Azure 事件网格、Azure 事件中心和 Azure 服务总线。 首先服务总线作为默认选择,如果服务总线不符合需求,请使用其他两个选项。

    服务 用例
    服务总线 为企业应用程序中高价值消息的可靠、有序和可能事务传递选择服务总线。
    事件网格 如果需要高效地处理大量离散事件,请选择事件网格。 事件网格对于事件驱动的应用程序可缩放,其中许多小型独立事件(如资源状态更改)需要在低延迟发布-订阅模型中路由到订阅服务器。
    事件中心 选择事件中心进行大规模高吞吐量的数据引入,例如遥测、日志或实时分析。 事件中心针对需要持续引入和处理批量数据的流式处理方案进行了优化。
  • 实现容器服务。 对于要容器化的应用程序组件,需要一个支持容器的应用程序平台。 选择 Azure 容器服务指南可帮助你做出决策。 Azure 有三个主要容器服务:Azure 容器应用、Azure Kubernetes 服务(AKS)和Azure App 服务。 从容器应用作为默认选项开始,如果容器应用不满足需求,请使用其他两个选项。

    服务 用例
    容器应用 如果需要自动缩放和管理事件驱动应用程序中的容器的无服务器平台,请选择“容器应用”。
    AKS 如果需要对 Kubernetes 配置进行详细控制,并需要缩放、网络和安全方面的高级功能,请选择 AKS。
    用于容器的 Web 应用 在应用服务中选择用于容器的 Web 应用,以获取最简单的 PaaS 体验。
  • 实现容器存储库。 使用任何基于容器的计算服务时,需要有一个存储库来存储容器映像。 可以使用 Docker 中心等公共容器注册表或 Azure 容器注册表等托管注册表。 Azure 指南中的容器注册表简介可帮助你做出决策。

代码指导

要成功分离和提取独立服务,需要使用以下设计模式更新 Web 应用代码:Strangler Fig 模式、基于队列的负载调节模式、使用者竞争模式、运行状况终结点监控模式和重试模式。 此处演示了这些模式的角色:

显示现代 Web 应用模式体系结构中设计模式角色的关系图。

  1. Strangler Fig 模式:Strangler Fig 模式会将功能从整体应用程序逐步迁移到已分离服务。 在主 Web 应用中实现此模式,通过根据终结点引导流量,将功能逐步迁移到独立服务中。

  2. 基于队列的负载调配模式:基于队列的负载调配模式通过使用队列作为缓冲区来管理生成者和使用者之间的消息流。 在生成队列消息的代码库中实现此模式。 然后,分离的服务会异步使用队列中的这些消息。

  3. 使用者竞争模式:使用者竞争模式允许已分离服务的多个实例独立地从同一个消息队列读取消息,并争用处理消息。 在分离服务中实现此模式,将任务分配到多个实例中。

  4. 运行状况终结点监视模式:运行状况终结点监视模式会公开用于监控 Web 应用不同部分的状态和运行状况的终结点。 (4a) 在主 Web 应用中实现此模式。 (4b) 同时在已分离服务中实现该功能,以跟踪终结点的运行状况。

  5. 重试模式:重试模式通过重试可能间歇性失败的操作来处理暂时性故障。 (5a) 在主 Web 应用中对其他 Azure 服务的所有出站调用(例如对消息队列和专用终结点的调用)上实现此模式。 (5b) 同时在已分离服务中实现此模式,以处理调用私有终结点时出现的暂时性故障。

每个设计模式都提供与 Well-Architected 框架的一个或多个支柱保持一致的优势。 有关详细信息,请参阅下表。

设计模式 实现位置 可靠性 (RE) 安全性 (SE) 成本优化 (CO) 卓越运营 (OE) 性能效率 (PE) 支持架构良好的框架原则
Strangler Fig 模式 主 Web 应用 RE:08
CO:07
CO:08
OE:06
OE:11
基于队列的负载调节模式 主 Web 应用(消息生成者) RE:07
RE:07
CO:12
PE:05
使用者竞争模式 已分离服务 RE:05
RE:07
CO:05
CO:07
PE:05
PE:07
运行状况终结点监视模式 主 Web 应用和分离服务 RE:07
RE:10
OE:07
PE:05
重试模式 主 Web 应用和分离服务 RE:07

实现 Strangler Fig 模式

使用 Strangler Fig 模式逐步将功能从整体代码库迁移到新的独立服务。 从现有的整体代码库中提取新的服务,并逐步实现 Web 应用关键部分的现代化。 要实现 Strangler Fig 模式,请遵循以下建议:

  • 设置路由层。 在整体 Web 应用代码库中,实现基于终结点定向流量的路由层。 根据需要使用自定义路由逻辑,以处理引导流量的特定业务规则。 例如,如果在整体应用中具有 /users 终结点,并且将该功能移动到分离的服务,则路由层会将所有请求定向到 /users 到新服务。

  • 管理功能推出。 使用 .NET 功能管理库 实现功能标志分阶段推出 ,逐步推出分离的服务。 现有的整体应用路由应控制已分离服务接收请求的数量。 从少量的请求开始,随着你对新服务的稳定性和性能充满信心,随着时间的推移增加使用量。 例如,参考实现将票证呈现功能提取到独立服务中,可以逐步引入该服务来处理票证呈现请求的较大部分。 随着新服务证明其可靠性和性能,它最终可以从整体上接管整个票证呈现功能,完成转换。

  • 使用外观服务(如有必要)。 当单个请求需要与多个服务交互时,或者当你想向客户端隐藏基础系统的复杂性时,外观服务就能派上用场。 但是,如果分离的服务没有任何面向公众的 API,则可能不需要外观服务。 在整体 Web 应用代码库中,实现外观服务,将请求路由到适当的后端(整体或微服务)。 在新的分离服务中,确保在通过外观访问时,新服务可以独立处理请求。

实现基于队列的负载调节模式

分离服务的生成者部分中实现基于队列的负载调配模式 ,以异步处理不需要即时响应的任务。 这种模式通过使用队列来管理工作负荷分配,提高了系统的整体响应速度和可伸缩性。 它允许已分离服务以一致的速度来处理请求。 要有效实现这种模式,请遵循以下建议:

  • 使用非阻止消息队列。 确保向队列发送消息的进程在等待已分离服务处理队列中的消息时不会阻止其他进程。 如果进程需要已分离服务操作的结果,那么在等待队列操作完成的同时,要有其他方法来处理这种情况。 例如,引用实现使用 服务总线 和await关键字messageSender.PublishAsync()将消息异步发布到队列,而不会阻止运行此代码的线程:

    // Asynchronously publish a message without blocking the calling thread.
    await messageSender.PublishAsync(new TicketRenderRequestMessage(Guid.NewGuid(), ticket, null, DateTime.Now), CancellationToken.None);
    

    这种方法可确保主应用程序保持响应速度,并能同时处理其他任务,而已分离服务则以可控的速度处理排队请求。

  • 实现消息重试和删除。 实现一种机制,以便重试处理无法成功处理的队列消息。 如果故障持续存在,则应从队列中删除这些消息。 例如,服务总线具有内置的重试和死信队列功能。

  • 配置幂等消息处理。 处理来自队列的消息的逻辑必须是 幂等 的,才能处理可能多次处理消息的情况。 例如,引用实现使用ServiceBusClient.CreateProcessorAutoCompleteMessages = trueReceiveMode = ServiceBusReceiveMode.PeekLock确保消息仅处理一次,并且可以在失败时重新处理。 以下代码演示了此逻辑。

    // Create a processor for idempotent message processing.
    var processor = serviceBusClient.CreateProcessor(path, new ServiceBusProcessorOptions
    {
        // Allow the messages to be auto-completed
        // if processing finishes without failure.
        AutoCompleteMessages = true,
    
        // PeekLock mode provides reliability in that unsettled messages
        // will be redelivered on failure.
        ReceiveMode = ServiceBusReceiveMode.PeekLock,
    
        // Containerized processors can scale at the container level
        // and need not scale via the processor options.
        MaxConcurrentCalls = 1,
        PrefetchCount = 0
    });
    
  • 管理体验的变化。 异步处理可能导致任务无法立即完成。 当用户的任务仍在处理过程中时,应让用户知道,以设定正确的预期,避免混淆。 使用视觉提示或消息来指明任务正在进行中。 让用户选择在任务完成时接收通知,如电子邮件或推送通知。

实现使用者竞争模式

在已分离服务中实现使用者竞争模式,以管理来自消息队列的传入任务。 这种模式涉及在已分离服务的多个实例之间分配任务。 这些服务处理来自队列的消息、增强负载均衡和提升系统处理同时请求的容量。 使用者竞争模式在以下情况下有效:

  • 消息处理的顺序并不重要。
  • 队列不受格式错误消息的影响。
  • 处理操作具有幂等性,这意味着它可以多次使用,而不会改变初次使用后的结果。

要实现使用者竞争模式,请遵循以下建议:

  • 处理并发消息。 当系统从队列接收消息时,请确保系统旨在同时处理多个消息。 将并发调用的最大值设为 1,以便由单独的使用者处理每条消息。

  • 禁用预提取。 禁用预提取消息,以便使用者仅在消息准备就绪时提取消息。

  • 使用可靠的消息处理模式。 使用可靠处理模式,如 PeekLock 或类似模式来自动重试处理失败的消息。 此模式比删除优先方法更可靠。 如果一个工作线程无法处理消息,则另一个工作线程必须能够无差错地处理该消息,即使该消息已被多次处理。

  • 实现错误处理。 将格式不正确或无法处理的消息路由到单独的死信队列。 这种设计可避免重复处理。 例如,可以在消息处理过程中捕获异常,并将有问题的消息移到单独的队列中。

  • 处理失序消息。 设计使用者以处理未按顺序送达的消息。 如果你有多个并行使用者,则它们可能会无序处理消息。

  • 根据队列长度进行缩放。 使用来自队列的消息的服务应考虑根据队列长度或使用 其他缩放条件 自动缩放,以更好地处理传入消息的峰值。

  • 使用消息回复队列。 如果系统需要消息后处理的通知,请设置专用回复或响应队列。 这种设置会将操作消息与通知流程分开。

  • 使用无状态服务。 考虑使用无状态服务来处理队列中的请求。 这些服务可实现轻松缩放和高效的资源使用。

  • 配置日志记录。 在消息处理工作流中集成日志记录和特定异常处理。 专注于捕获序列化错误并将这些有问题的消息定向到死信机制。 这些日志为故障排除提供了宝贵的见解。

例如,引用实现使用容器应用中运行的无状态服务的竞争使用者模式来处理来自服务总线队列的票证呈现请求。 它通过以下方式来配置队列处理器:

  • AutoCompleteMessages。 如果消息未经失败处理,则会自动完成消息。
  • ReceiveMode。 如果未解决 PeekLock 模式,则使用 PeekLock 模式和重新传送消息。
  • MaxConcurrentCalls。 设置为 1,一次处理一条消息。
  • PrefetchCount。 设置为 0 以避免预提取消息。

处理器记录消息处理详细信息,这有助于进行故障排除和监视。 它捕获反序列化错误并将无效消息路由到死信队列,以防止重复处理错误消息。 服务在容器级别进行缩放,从而基于队列长度有效地处理消息高峰。

// Create a processor for the given queue that will process
// incoming messages.
var processor = serviceBusClient.CreateProcessor(path, new ServiceBusProcessorOptions
{
    // Allow the messages to be auto-completed
    // if processing finishes without failure.
    AutoCompleteMessages = true,
    // PeekLock mode provides reliability in that unsettled messages
    // are redelivered on failure.
    ReceiveMode = ServiceBusReceiveMode.PeekLock,
    // Containerized processors can scale at the container level
    // and need not scale via the processor options.
    MaxConcurrentCalls = 1,
    PrefetchCount = 0
});

// Called for each message received by the processor.
processor.ProcessMessageAsync += async args =>
{
    logger.LogInformation("Processing message {MessageId} from {ServiceBusNamespace}/{Path}", args.Message.MessageId, args.FullyQualifiedNamespace, args.EntityPath);
    // Unhandled exceptions in the handler will be caught by
    // the processor and result in abandoning and dead-lettering the message.
    try
    {
        var message = args.Message.Body.ToObjectFromJson<T>();
        await messageHandler(message, args.CancellationToken);
        logger.LogInformation("Successfully processed message {MessageId} from {ServiceBusNamespace}/{Path}",args.Message.MessageId, args.FullyQualifiedNamespace, args.EntityPath);
    }
    catch (JsonException)
    {
        logger.LogError("Invalid message body; could not be deserialized to {Type}", typeof(T));
        await args.DeadLetterMessageAsync(args.Message, $"Invalid message body; could not be deserialized to {typeof(T)}",cancellationToken: args.CancellationToken);
    }
};

实现运行状况终结点监视模式。

在主应用代码和已分离服务代码中实现运行状况终结点监视模式,以跟踪应用程序终结点的运行状况。 AKS 或容器应用等业务流程协调程序可以轮询这些终结点,以验证服务运行状况并重启不正常的实例。 ASP.NET Core 应用可以添加专用的运行状况检查中间件,以高效地提供终结点运行状况数据和关键依赖项。 要实现运行状况终结点监控模式,请遵循以下建议:

  • 实现运行状况检查。 使用 ASP.NET Core 运行状况检查中间件 提供运行状况检查终结点。

  • 验证依赖项。 确保运行状况检查会验证密钥依赖项(如数据库、存储和消息传送系统)的可用性。 非Microsoft包 AspNetCore.Diagnostics.HealthChecks 可以为许多常见应用依赖项实现运行状况检查依赖项检查。

    例如,参考实现使用 ASP.NET 核心运行状况检查中间件来公开运行状况检查终结点。 它使用 AddHealthChecks() 对象上的 builder.Services 方法。 该代码使用AddAzureBlobStorage()包中包含的AspNetCore.Diagnostics.HealthChecks方法和AddAzureServiceBusQueue()方法验证密钥依赖项、Azure Blob 存储和服务总线队列的可用性。 容器应用允许配置 受监视的运行状况探测 ,以衡量应用是正常还是需要回收。

    // Add health checks, including health checks for Azure services
    // that are used by this service.
    // The Blob Storage and Service Bus health checks are provided by
    // AspNetCore.Diagnostics.HealthChecks
    // (a popular open source project) rather than by Microsoft. 
    // https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks
    builder.Services.AddHealthChecks()
    .AddAzureBlobStorage(options =>
    {
        // AddAzureBlobStorage will use the BlobServiceClient registered in DI.
        // We just need to specify the container name.
        options.ContainerName = builder.Configuration.GetRequiredConfigurationValue("App:StorageAccount:Container");
    })
    .AddAzureServiceBusQueue(
        builder.Configuration.GetRequiredConfigurationValue("App:ServiceBus:Host"),
        builder.Configuration.GetRequiredConfigurationValue("App:ServiceBus:RenderRequestQueueName"),
        azureCredentials);
    
    // Further app configuration omitted for brevity.
    app.MapHealthChecks("/health");
    
  • 配置 Azure 资源。 将 Azure 资源配置为使用应用的运行状况检查 URL 来确认实时性和就绪性。 例如,参考实现会使用 Bicep 来配置运行状况检查 URL,以确认 Azure 资源的实时性和就绪性。 运行情况探测在初始延迟为 2 秒后每 10 秒命中 /health 一次终结点。

    probes: [
      {
        type: 'liveness'
        httpGet: {
          path: '/health'
          port: 8080
        }
        initialDelaySeconds: 2
        periodSeconds: 10
      }
    ]
    

实现重试模式

重试模式让应用程序能够从暂时性故障中恢复。 重试模式是可靠 Web 应用模式的核心所在,因此 Web 应用应该已经使用了重试模式。 将重试模式应用于从 Web 应用中提取的分离服务发出的消息系统和请求。 要实现重试模式,请遵循以下建议:

  • 配置重试选项。 与消息队列集成时,请务必配置负责与队列交互的客户端,并设置适当的重试设置。 指定参数,例如最大重试次数、重试之间的延迟和最大延迟。

  • 使用指数退避。 为重试尝试实现指数退避策略。 此策略涉及增加每次重试之间的时间,这有助于在高故障率期间减少系统上的负载。

  • 使用 SDK 重试功能。 对于具有专用 SDK(例如服务总线或 Blob 存储)的服务,请使用内置的重试机制。 内置重试机制针对服务的典型用例进行优化,可以更有效地处理重试,且所需的配置更少。 例如,引用实现使用 服务总线 SDK(ServiceBusClientServiceBusRetryOptions)的内置重试功能。 对象ServiceBusRetryOptionsMessageBusOptions中提取设置以配置重试设置,例如MaxRetriesDelayMaxDelayTryTimeout

    // ServiceBusClient is thread-safe and can be reused for the lifetime
    // of the application.
    services.AddSingleton(sp =>
    {
        var options = sp.GetRequiredService<IOptions<MessageBusOptions>>().Value;
        var clientOptions = new ServiceBusClientOptions
        {
            RetryOptions = new ServiceBusRetryOptions
            {
                Mode = ServiceBusRetryMode.Exponential,
                MaxRetries = options.MaxRetries,
                Delay = TimeSpan.FromSeconds(options.BaseDelaySecondsBetweenRetries),
                MaxDelay = TimeSpan.FromSeconds(options.MaxDelaySeconds),
                TryTimeout = TimeSpan.FromSeconds(options.TryTimeoutSeconds)
            }
        };
        return new ServiceBusClient(options.Host, azureCredential ?? new DefaultAzureCredential(), clientOptions);
    });
    
  • 为 HTTP 客户端采用标准复原库。 对于 HTTP 通信,请集成标准复原库,例如 Polly 或 Microsoft.Extensions.Http.Resilience。 这些库提供全面的重试机制,对于管理与外部 Web 服务的通信至关重要。

  • 处理消息锁定。 对于基于消息的系统,实现支持在不丢失数据丢失的情况下重试的消息处理策略,例如使用“速览锁定”模式( 如果可用)。 确保有效重试失败的消息,并在重复失败后将其移至死信队列。

实现分布式跟踪

随着应用程序越来越多地面向服务,其组件也会相互分离,监控服务之间的执行流就变得至关重要。 新式 Web 应用模式使用 Application Insights 和 Azure Monitor 通过支持分布式跟踪的 OpenTelemetry API 来了解应用程序运行状况和性能。

分布式跟踪可在用户请求穿越多个服务时对其进行跟踪。 收到请求时,会使用跟踪标识符进行标记,该标识符通过 HTTP 标头传递到其他组件,并在依赖项调用期间服务总线属性。 然后,跟踪和日志包括跟踪标识符和活动标识符(或跨度标识符),后者与特定组件及其上级活动相对应。 Application Insights 等监视工具使用此信息跨不同服务显示活动和日志树,这对于监视分布式应用程序至关重要。

  • 安装 OpenTelemetry 库。 使用检测库从常见组件启用跟踪和指标。 必要时使用 System.Diagnostics.ActivitySourceSystem.Diagnostics.Activity 来添加自定义检测。 使用导出程序库侦听 OpenTelemetry 诊断,并将其记录在持久性存储中。 使用现有导出程序或使用自己的 System.Diagnostics.ActivityListener导出程序。

  • 设置 OpenTelemetry。 使用 OpenTelemetry (Azure.Monitor.OpenTelemetry.AspNetCore) 的 Azure Monitor 分发版。 确保它将诊断导出到 Application Insights,并包括适用于 .NET 运行时和 ASP.NET Core 中的常见指标、跟踪、日志和异常的内置检测。 包括 SQL、Redis 和 Azure SDK 客户端的其他 OpenTelemetry 检测包。

  • 监控和分析。 配置跟踪后,请确保捕获日志、跟踪、指标和异常并将其发送到 Application Insights。 验证是否包含跟踪、活动和父活动标识符。 这些标识符使 Application Insights 能够跨 HTTP 和服务总线边界提供端到端跟踪可见性。 使用此设置可以跨服务监视和分析应用程序的活动。

新式 Web 应用示例使用 OpenTelemetry 的 Azure Monitor 分发版 (Azure.Monitor.OpenTelemetry.AspNetCore)。 更多检测包用于 SQL、Redis 和 Azure SDK 客户端。 OpenTelemetry 在现代 Web 应用示例票证呈现服务中配置,如下所示:

builder.Logging.AddOpenTelemetry(o => 
{ 
    o.IncludeFormattedMessage = true; 
    o.IncludeScopes = true; 
}); 

builder.Services.AddOpenTelemetry() 
    .UseAzureMonitor(o => o.ConnectionString = appInsightsConnectionString) 
    .WithMetrics(metrics => 
    { 
        metrics.AddAspNetCoreInstrumentation() 
                .AddHttpClientInstrumentation() 
                .AddRuntimeInstrumentation(); 
    }) 
    .WithTracing(tracing => 
    { 
        tracing.AddAspNetCoreInstrumentation() 
                .AddHttpClientInstrumentation() 
                .AddSource("Azure.*"); 
    }); 

该方法 builder.Logging.AddOpenTelemetry 通过 OpenTelemetry 路由所有日志记录,以确保在整个应用程序中进行一致的跟踪和日志记录。 由于 OpenTelemetry 服务已注册 builder.Services.AddOpenTelemetry到其中,因此应用程序设置为收集和导出诊断,然后通过 UseAzureMonitor这些诊断发送到 Application Insights。 此外,配置了服务总线和 HTTP 客户端等组件的客户端WithMetricsWithTracing检测,这样就可以启用自动指标和跟踪收集,而无需更改现有客户端使用情况。 只需要对配置进行更新。

配置指南

以下各部分将为实现配置更新提供指导。 每个部分都与“架构良好的框架”的一个或多个支柱相一致。

配置 可靠性 (RE) 安全性 (SE) 成本优化 (CO) 卓越运营 (OE) 性能效率 (PE) 支持架构良好的框架原则
配置身份验证和授权 SE:05
OE:10
实现独立的自动缩放 RE:06
CO:12
PE:05
容器化服务部署 CO:13
PE:09
PE:03

配置身份验证和授权

若要在添加到 Web 应用的任何新 Azure 服务(工作负荷标识)上配置身份验证和授权,请遵循以下建议:

  • 为每项新服务使用托管标识。 每项独立服务都应有自己的标识,并使用托管标识进行服务间身份验证。 托管标识无需在代码中管理凭据,同时还降低了凭据泄露的风险。 它们有助于避免在代码或配置文件中输入连接字符串等敏感信息。

  • 为每个新服务授予最低权限。 只为每个新服务标识分配必要的权限。 例如,如果标识只需要推送到容器注册表,请不要向其授予拉取权限。 定期查看这些权限,并根据需要对其进行调整。 为不同角色(如部署和应用程序)使用不同的标识。 这样就能限制标识被泄露时可能造成的损失。

  • 采用基础结构即代码 (IaC)。 使用 Bicep 或类似的 IaC 工具定义和管理云资源。 IaC 可确保安全配置在部署中的一致应用,并允许对基础结构设置进行版本控制。

要配置用户(用户标识)的身份验证和授权,请遵循以下建议:

  • 授予用户最低权限。 与服务一样,请确保仅向用户提供执行其任务所需的权限。 定期查看和调整这些权限。

  • 定期进行安全审核。 定期查看和审核安全设置。 查找任何错误配置或不必要的权限,并立即予以纠正。

参考实现使用 IaC 为添加的服务分配托管标识,并为每个标识分配特定角色。 它定义部署()、应用程序所有者(containerRegistryPushRoleIdcontainerRegistryPushRoleId)和容器应用应用程序()的角色和权限访问权限。containerRegistryPullRoleId 下面的示例演示了代码。

roleAssignments: \[
    {
    principalId: deploymentSettings.principalId
    principalType: deploymentSettings.principalType
    roleDefinitionIdOrName: containerRegistryPushRoleId
    }
    {
    principalId: ownerManagedIdentity.outputs.principal_id
    principalType: 'ServicePrincipal'
    roleDefinitionIdOrName: containerRegistryPushRoleId
    }
    {
    principalId: appManagedIdentity.outputs.principal_id
    principalType: 'ServicePrincipal'
    roleDefinitionIdOrName: containerRegistryPullRoleId
    }
\]

参考实现在部署时将托管标识分配为新的容器应用标识:

module renderingServiceContainerApp 'br/public:avm/res/app/container-app:0.1.0' = {
  name: 'application-rendering-service-container-app'
  scope: resourceGroup()
  params: {
    // Other parameters omitted for brevity.
    managedIdentities: {
      userAssignedResourceIds: [
        managedIdentity.id
      ]
    }
  }
}

配置独立的自动缩放

新式 Web 应用模式开始分解整体体系结构,并引入服务分离。 在 Web 应用体系结构分离后,可以独立缩放已分离服务。 缩放 Azure 服务以支持独立的 Web 应用程序服务,而不是整个 Web 应用,在满足需求的同时优化了缩放成本。 要自动缩放容器,请遵循以下建议:

  • 使用无状态服务。 确保服务无状态。 如果 .NET 应用程序包含进程内会话状态,请将其外部化到 Redis 等分布式缓存或 SQL Server 等数据库。

  • 配置自动缩放规则。 使用自动缩放配置,对服务进行最具成本效益的控制。 对于容器化服务,基于事件的缩放(例如 Kubernetes Event-Driven 自动缩放程序(KEDA)通常提供精细控制,使你能够基于事件指标进行缩放。 容器应用 和 AKS 支持 KEDA。 对于不支持 KEDA 的服务(如 应用服务),请使用平台提供的自动缩放功能。 这些功能通常包括基于指标的规则或 HTTP 流量的缩放。

  • 配置最小副本。 为防止冷启动,请配置自动缩放设置,以保持至少一个副本。 从停止状态初始化服务时,会发生 冷启动 ,这通常会创建延迟的响应。 如果最小化成本是一个优先事项,并且可以容忍冷启动延迟,请在配置自动缩放时将最小副本计数设置为 0。

  • 配置冷却期。 应用适当的冷却期,在缩放事件之间引入延迟。 目标是防止临时负载高峰触发 的过度缩放 活动。

  • 配置基于队列的缩放。 如果应用程序使用消息队列(如服务总线),请将自动缩放设置配置为根据队列长度和请求消息进行缩放。 缩放程序旨在维护队列中每个 N 消息的服务副本(向上舍入)。

例如,参考实现使用 服务总线 KEDA 缩放程序根据队列的长度缩放容器应用。 根据service-bus-queue-length-rule指定的服务总线队列的长度缩放服务。 messageCount 参数设置为 10,因此队列中每出现 10 条消息,缩放程序就会有一个服务副本。 scaleMaxReplicasscaleMinReplicas 参数设置服务的最大和最小副本数。 从 queue-connection-string Azure 密钥库检索包含服务总线队列连接字符串的机密。 此机密用于验证缩放程序与服务总线的连接。

scaleRules: [
  {
    name: 'service-bus-queue-length-rule'
    custom: {
      type: 'azure-servicebus'
      metadata: {
        messageCount: '10'
        namespace: renderRequestServiceBusNamespace
        queueName: renderRequestServiceBusQueueName
      }
      auth: [
        {
          secretRef: 'render-request-queue-connection-string'
          triggerParameter: 'connection'
        }
      ]
    }
  }
]

scaleMaxReplicas: 5
scaleMinReplicas: 0

容器化服务部署

在容器化部署中,应用所需的所有依赖项都封装在可可靠地部署到各种主机的轻型映像中。 要实现容器化部署,请遵循以下建议:

  • 标识域边界。 首先在整体应用程序中标识域边界。 这样做有助于确定应用程序的哪些部分可以提取到单独的服务中。

  • 创建 Docker 映像。 为 .NET 服务创建 Docker 映像时,请使用 已插入的基础映像。 这些映像仅包含运行 .NET 所需的最小包集,这将最大程度地减少包大小和攻击外围应用。

  • 使用多阶段 Dockerfile。 实现多阶段 Dockerfile,将生成时资产与运行时容器映像分离。 使用此类型的文件有助于保持生产映像的较小且安全。

  • 以非根用户身份运行。 以非根用户身份运行 .NET 容器(通过用户名或 UID $APP_UID),以符合最低特权原则。 这样做会限制已泄露容器的潜在影响。

  • 侦听端口 8080。 以非根用户身份运行容器时,请将应用程序配置为侦听端口 8080。 这是非根用户的常见约定。

  • 封装依赖项。 确保应用的所有依赖项都封装在 Docker 容器映像中。 封装使你能够可靠地将应用部署到各种主机。

  • 选择正确的基础映像。 所选的基础映像取决于部署环境。 例如,如果要部署到容器应用,则需要使用 Linux Docker 映像。

例如,参考实现使用多阶段生成过程。 初始阶段使用完整的 SDK 映像 (mcr.microsoft.com/dotnet/sdk:8.0-jammy) 来编译和生成应用程序。 最终运行时映像是从 chiseled 基础映像创建的,其中未包括 SDK 和生成工件。 该服务以非根用户 (USER $APP_UID) 身份运行,并公开端口 8080。 要运行的应用程序所需的依赖项包含在 Docker 映像中,如用于复制项目文件和还原包的命令所证明的那样。 使用基于 Linux 的映像(mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled)可确保与容器应用兼容,这需要 Linux 容器进行部署。

# Build in a separate stage to avoid copying the SDK into the final image.
FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src

# Restore packages.
COPY ["Relecloud.TicketRenderer/Relecloud.TicketRenderer.csproj", "Relecloud.TicketRenderer/"]
COPY ["Relecloud.Messaging/Relecloud.Messaging.csproj", "Relecloud.Messaging/"]
COPY ["Relecloud.Models/Relecloud.Models.csproj", "Relecloud.Models/"]
RUN dotnet restore "./Relecloud.TicketRenderer/Relecloud.TicketRenderer.csproj"

# Build and publish.
COPY . .
WORKDIR "/src/Relecloud.TicketRenderer"
RUN dotnet publish "./Relecloud.TicketRenderer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

# Chiseled images contain only the minimal set of packages needed for .NET 8.0.
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled AS final
WORKDIR /app
EXPOSE 8080

# Copy the published app from the build stage.
COPY --from=build /app/publish .

# Run as nonroot user.
USER $APP_UID
ENTRYPOINT ["dotnet", "./Relecloud.TicketRenderer.dll"]

部署参考实现

部署适用于 .NET 的新式 Web 应用模式的参考实现。 存储库中提供了开发和生产部署的说明。 部署实现后,可以模拟和观察设计模式。

下图显示了参考实现的体系结构:

显示参考实现体系结构的关系图。

下载此体系结构的 Visio 文件