本文将介绍如何实现新式 Web 应用模式。 新式 Web 应用模式定义了如何在云中实现 Web 应用的现代化并引入面向服务的体系结构。 新式 Web 应用模式提供规范性的体系结构、代码和配置指南,这些指南符合 Azure 良好架构框架的原则,并基于 Reliable Web 应用模式进行构建。
为什么要使用新式 Web 应用模式?
新式 Web 应用模式有助于优化 Web 应用的高需求区域。 它提供了详细的指导,将这些领域分离开来,从而实现独立缩放,以便优化成本。 此方法可以为关键组件分配专用资源,从而提高整体性能。 对可分离的服务进行分离,可以防止应用的某个部分速度变慢影响到其他部分,从而提高可靠性。 分离还能使各个应用组件独立进行版本控制。
如何实现新式 Web 应用模式
本文包含实现新式 Web 应用模式的体系结构、代码和配置指导。 使用以下链接导航到所需的指导:
- 体系结构指南:了解如何模块化 Web 应用组件,并选择适当的平台即服务(PaaS)解决方案。
- 代码指导:实现四种设计模式来优化分离组件:Strangler Fig、基于队列的负载调控、竞争性使用者和运行状况终结点监控模式。
- 配置指导:为分离的组件配置身份验证、授权、自动缩放和容器化。
提示
有一个新式 Web 应用模式的参考实现(示例应用)。 它表示新式 Web 应用实现的最终状态。 它是一个生产级 Web 应用,具有本文讨论的所有代码、体系结构和配置更新。 部署并使用参考实现来指导你实现新式 Web 应用模式。
体系结构指南
新式 Web 应用模式建立在可靠 Web 应用模式的基础上。 它需要一些额外的体系结构组件来实现。 需要一个消息队列、容器平台、分离服务数据存储和容器注册表(请参阅图 1)。
为了实现更高的服务级别目标 (SLO),可以在 Web 应用体系结构中添加第二个区域。 第二个区域要求配置负载均衡器,将流量路由到第二个区域,以便支持主动-主动或主动-被动配置。 使用中心辐射型网络拓扑来集中和共享资源,如网络防火墙。 通过中心虚拟网络访问容器存储库。 如果有虚拟机,请在中心虚拟网络中添加一台堡垒主机,以便安全地管理虚拟机(请参阅图 2)。
图 2. 带有第二个区域和中心辐射型网络拓扑的新式 Web 应用模式体系结构。
分离体系结构
要实现新式 Web 应用模式,需要将现有的 Web 应用体系结构分离。 将体系结构分离涉及将整体式应用程序分解为较小的独立服务,每个服务负责特定的特征或功能。 这一过程包括评估当前的 Web 应用程序、修改体系结构,最后将 Web 应用代码提取到容器平台。 目标是系统地识别和提取从分离中获益最大的应用程序服务。 要实现体系结构分离,请遵循以下建议:
标识服务边界。 应用域驱动的设计原则,以识别整体应用程序中的有限上下文。 每个边界上下文都代表一个逻辑边界,可以作为单独服务的候选对象。 代表不同业务功能且依赖项较少的服务适合进行分离。
评估服务优势。 重点关注从独立缩放中获益最大的服务。 将这些服务分离,并将处理任务从同步操作转换为异步操作,可以提高资源管理效率,支持独立部署,并降低在更新或更改期间影响应用程序其他部分的风险。 例如,可以将订单结帐与订单处理分开。
评估技术可行性。 检查当前体系结构,确定可能影响分离过程的技术限制和依赖项。 规划如何跨服务对数据进行管理和共享。 已分离服务应管理自己的数据,并要尽量减少跨服务边界的直接数据库访问。
部署 Azure 服务。 选择并部署所需的 Azure 服务,以支持要提取的 Web 应用程序服务。 请参考以下选择正确的 Azure 服务部分。
分离 Web 应用服务。 为新提取的 Web 应用程序服务定义明确的接口和 API,以便与系统的其他部分进行交互。 设计数据管理策略,允许每个服务管理自己的数据,同时确保一致性和完整性。 有关在提取过程中使用的具体实现策略和设计模式,请参阅代码指导部分。
对已分离服务使用独立存储。 每个已分离服务都应有自己独立的数据存储,以便于进行独立的版本管理、部署和扩展,并保持数据的完整性。 例如,引用实现将票证呈现服务与 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 Kubernetes 服务(AKS)和Azure App 服务。 从容器应用作为默认选项开始,如果容器应用不满足需求,请使用其他两个选项。
服务 用例 容器应用 如果需要自动缩放和管理事件驱动应用程序中的容器的无服务器平台,请选择“容器应用”。 AKS 如果需要对 Kubernetes 配置进行详细控制,并需要缩放、网络和安全方面的高级功能,请选择 AKS。 用于容器的 Web 应用 在 App 服务 上为容器选择 Web 应用,以获取最简单的 PaaS 体验。 实现容器存储库。 使用任何基于容器的计算服务时,必须有一个存储库来存储容器映像。 可以使用 Docker 中心等公共容器注册表或 Azure 容器注册表等托管注册表。 使用 Azure 指南中的容器注册表简介来帮助做出决策。
代码指导
要成功分离和提取独立服务,需要使用以下设计模式更新 Web 应用代码:Strangler Fig 模式、基于队列的负载调节模式、使用者竞争模式、运行状况终结点监控模式和重试模式。
Strangler Fig 模式:Strangler Fig 模式会将功能从整体应用程序逐步迁移到已分离服务。 在主 Web 应用中实现此模式,通过根据终结点引导流量,将功能逐步迁移到独立服务中。
基于队列的负载调配模式:基于队列的负载调配模式通过使用队列作为缓冲区来管理生成者和使用者之间的消息流。 在已分离服务的生成者部分中实现此模式,以便使用队列来异步管理消息流。
使用者竞争模式:使用者竞争模式允许已分离服务的多个实例独立地从同一个消息队列读取消息,并争用处理消息。 在分离服务中实现此模式,将任务分配到多个实例中。
运行状况终结点监视模式:运行状况终结点监视模式会公开用于监控 Web 应用不同部分的状态和运行状况的终结点。 (4a) 在主 Web 应用中实现此模式。 (4b) 同时在已分离服务中实现该功能,以跟踪终结点的运行状况。
重试模式:重试模式通过重试可能间歇性失败的操作来处理暂时性故障。 (5a) 在主 Web 应用中对其他 Azure 服务的所有出站调用(如对消息队列和私有终结点的调用)中实现此模式。 (5b) 同时在已分离服务中实现此模式,以处理调用私有终结点时出现的暂时性故障。
每种设计模式都能提供与“架构良好的框架”的一个或多个支柱相一致的优势(请参阅下表)。
设计模式 | 实现位置 | 可靠性 (RE) | 安全性 (SE) | 成本优化 (CO) | 卓越运营 (OE) | 性能效率 (PE) | 支持架构良好的框架原则 |
---|---|---|---|---|---|---|---|
Strangler Fig 模式 | 主 Web 应用 | ✔ | ✔ | ✔ | RE:08 CO:07 CO:08 OE:06 OE:11 |
||
基于队列的负载调节模式 | 已分离服务的生成者 | ✔ | ✔ | ✔ | RE:06 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.CreateProcessor
并确保消息仅处理一次,并且可以在失败时重新处理(请参阅以下代码)。ReceiveMode = ServiceBusReceiveMode.PeekLock
AutoCompleteMessages = true
// 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 模式,并在消息未得到解决的情况下重新发送消息。
- 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 可对许多常见应用的依赖项实现运行状况检查依赖项检查。
例如,通过使用
builder.Services
对象上的AddHealthChecks()
方法,参考实现可使用 ASP.NET Core 运行状况检查中间件来公开运行状况检查终结点。 该代码使用包的AspNetCore.Diagnostics.HealthChecks
一部分和方法验证密钥依赖项、Azure Blob 存储和服务总线队列AddAzureBlobStorage()
AddAzureServiceBusQueue()
的可用性。 容器应用允许配置 受监视的运行状况探测 ,以衡量应用是正常还是需要回收。// 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(
ServiceBusClient
和ServiceBusRetryOptions
)的内置重试功能。ServiceBusRetryOptions
对象会从MessageBusOptions
提取设置,以配置重试设置,如 MaxRetries、Delay、MaxDelay 和 TryTimeout。// 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.ActivitySource
和System.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 客户端WithMetrics
等组件的客户端检测,并WithTracing
启用自动指标和跟踪收集,而无需更改现有客户端使用情况,仅更新配置。
配置指南
以下各部分将为实现配置更新提供指导。 每个部分都与“架构良好的框架”的一个或多个支柱相一致。
配置 | 可靠性 (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 为添加的服务分配托管标识,并为每个标识分配特定角色。 它定义部署的角色和权限访问权限(containerRegistryPushRoleId
)、应用程序所有者(containerRegistryPushRoleId
)和容器应用应用程序()(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 事件驱动的自动缩放程序 (KEDA))通常可提供精细的控制,从而让你能够根据事件指标进行缩放。 容器应用 和 AKS 支持 KEDA。 对于不支持 KEDA 的服务(如 App 服务),请使用平台本身提供的自动缩放功能。 这些功能通常包括根据基于指标的规则或 HTTP 流量进行缩放。
配置最小副本。 为防止冷启动,请配置自动缩放设置,以保持至少一个副本。 冷启动是指从停止状态初始化服务,这通常会造成响应延迟。 如果成本最小化是优先考虑的因素,并且可以忍受冷启动延迟,则可在配置自动扩展时将最小副本数设置为 0。
配置冷却期。 应用适当的冷却期,在缩放事件之间引入延迟。 目标是防止临时负荷高峰所触发的过度缩放活动。
配置基于队列的缩放。 如果应用程序使用消息队列(如服务总线),请将自动缩放设置配置为根据队列长度和请求消息进行缩放。 缩放程序的目标是为队列中的每 N 条信息维护一个服务副本(四舍五入)。
例如,参考实现使用 服务总线 KEDA 缩放程序根据队列的长度缩放容器应用。 根据service-bus-queue-length-rule
指定的服务总线队列的长度缩放服务。 messageCount
参数设置为 10,因此队列中每出现 10 条消息,缩放程序就会有一个服务副本。 scaleMaxReplicas
和 scaleMinReplicas
参数设置服务的最大和最小副本数。 从 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,将生成时资产与运行时容器映像分离。 它有助于保持较小的生产映像并确保安全。
以非根用户身份运行。 以非根用户身份(通过用户名或 UID、$APP_UID)运行 .NET 容器,以符合最低权限原则。 它限制了遭泄露容器带来的潜在影响。
侦听端口 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 non-root user
USER $APP_UID
ENTRYPOINT ["dotnet", "./Relecloud.TicketRenderer.dll"]
部署参考实现
部署适用于 .NET 的新式 Web 应用模式的参考实现。 存储库中提供了开发和生产部署的说明。 部署后即可模拟和观察设计模式。
图 3. 参考实现的体系结构。下载此体系结构的 Visio 文件。