在 Azure 上最大限度地提高基于队列的消息传送解决方案的可伸缩性和成本效率的最佳实践

更新时间: 2015年3月

注:本页面内容可能不完全适用中国大陆地区运营的 Windows Azure服务。如要了解不同地区 Windows Azure 服务的差异, 请参考本网站.

作者:Amit Srivastava 和 Valery Mizonov

审阅者:Brad Calder、Sidney Higa、Christian Martinez、Steve Marx、Curt Peterson、Paolo Salvatori 和 Trace Young

本文针对在 Azure 平台上构建可伸缩、高效和经济实用的基于队列的消息传送解决方案,提供了一些说明性指导和最佳做法。本文的目标读者包括解决方案架构师和开发人员,他们要设计和实现基于云的解决方案,这些解决方案需要利用 Azure 平台 
队列存储服务

摘要

传统的基于队列的消息传送解决方案利用的概念是一个称作消息队列的消息存储位置,它是与一个或多个参与者之间发送或接收(通常通过异步通信机制)的数据的存储库。

本文旨在说明开发人员如何将特定设计模式与 Azure 平台所提供的功能结合在一起使用,以便构建优化且经济实用的基于队列的消息传送解决方案。本文深入探讨了一些用于在 Azure 解决方案中实现基于队列的交互的最常用方法,并且就改进性能、增加可伸缩性和降低运营成本提供了一些建议。

我们还在论述基本概念的同时根据需要辅之以一些相关的最佳做法、提示和建议。本文中描述的方案主要来自于真实客户项目的技术实现案例。

基于队列的消息传送基础知识

一个使用消息队列在分布式组件之间交换数据的典型消息传送解决方案应该包括:“发布服务器”(将消息存入队列中)和一个或多个“订阅服务器”(用于接收这些消息)。在大多数情况下,订阅服务器(有时候称作“队列侦听器” )是作为单线程进程或多线程进程实现的,连续运行或者根据计划模式按需启动。

在更高的层级,可以使用两个主要的调度方法使队列侦听器能够接收在队列上存储的消息:

  • 轮询(基于请求的模型):侦听器通过定期检查队列中是否有新消息来监视队列。在队列为空时,侦听器会继续轮询队列,并且通过进入睡眠状态来定期退让。

  • 触发(基于推送的模型):侦听器会订阅一个只要一有消息到达队列就会触发(由发布服务器本身或队列服务管理器触发)的事件。然后,侦听器可以开始消息处理,从而不必轮询队列来确定是否有任何新工作可用。

还需要提及的是,这两种机制都可以进一步分为不同的形式。例如,轮询可以是阻止和非阻止的。阻止将在新消息在队列上出现(或遇到超时)前保留请求,而非阻止请求在队列上没有任何项时会立即完成。就触发模型而言,可为每个新消息都将通知推送到队列侦听器,也可以仅在第一个消息到达空队列时或者队列深度达到某个级别时将通知推送到队列侦听器。

note备注
Azure 队列服务 API 支持的取消排队操作是非阻止的。这意味着,如果在队列上找不到任何消息,则 GetMessageGetMessages 之类的 API 方法将会立即返回。与之相反,Azure Service Bus 队列提供阻止接收操作,这些操作将阻止调用线程,直到某一消息到达队列或者经过了指定的超时期限。

下面总结了当今在 Azure 解决方案中用来实现队列侦听器的最常见方法:

  1. 侦听器作为一个应用程序组件实现,该组件作为辅助角色实例的一部分实例化并执行。

  2. 该队列侦听器组件的生命周期通常受到宿主角色实例的运行时间的约束。

  3. 主要处理逻辑由一个循环构成,在这个循环中,将取消消息的排队并且安排进行处理。

  4. 如果未收到任何消息,则侦听线程将会进入睡眠状态,这个时间段通常受到特定于应用程序的退让算法驱动。

  5. 执行接收循环并且对队列进行轮询,直到通知侦听器退出循环并终止。

下面的流程图描绘了在 Azure 应用程序中使用轮询机制实现队列侦听器时常用的逻辑:

Best-Practices-Messaging-Solutions-Azure2
note备注
在本文中,没有使用更复杂的设计模式,例如要求使用中央队列管理器(代理)的那些设计模式。

在使用 Azure 队列时,将典型的队列侦听器与轮询机制一起使用可能不是最佳选择,因为 Azure 定价模型根据对队列执行的应用程序请求衡量存储事务,而与队列是否为空无关。在接下来的几个部分中,我们将针对 Azure 平台上的基于队列的消息传送解决方案,介绍一些提高性能和降低成本的技术。

用于性能、可伸缩性和成本优化的最佳做法

在本部分中,我们必须了解如何改进设计的相关方面以便实现更高的性能、更好的可伸缩性且更为经济实用。

也许,考量某一实现模式是否“更高效”的最简单方式就是确认设计是否满足以下目标:

  • 通过消除不会产生任何有价值工作的大量存储事务来降低运营开支

  • 消除由轮询间隔在检查是否有新消息时造成的额外的延迟

  • 通过调整处理能力以便适应不断变化的工作量来动态缩放

实现模式还应该在满足这些目标的同时不会产生其所带来的影响超过相关利益所带来的好处的复杂局面。

用于优化存储事务成本的最佳做法

当评估在 Azure 平台上部署的解决方案的总拥有成本 (TCO) 和投资回报 (ROI) 时,存储事务量是 TCO 计算公式中的主要变量之一。减少针对 Azure 队列的事务数量将降低运营成本,因为运营成本与正在 Azure 上运行的解决方案相关。

与 Azure Queue 相关联的存储空间成本可计算为 –

队列空间:24 字节 + Len(QueueName) * 2 +  For-Each Metadata(4 字节 + Len(QueueName) * 2 字节 + Len(Value) * 2 字节)

消息空间:12 字节 + Len(Message)

在基于队列的消息传送解决方案环境中,可以一起采用以下几种方法来减少存储事务量:

  1. 在将多个消息放入某一队列中时,将相关消息组合到单个更大的批次中,对消息进行压缩并将压缩后的映像存储于一个 blob 存储中,并且使用该队列保持对承载实际数据的 blob 的引用。此方法有助于优化事务成本和存储空间成本。

  2. 在从某一队列检索消息时,将多个消息一起组合到单个存储事务中。利用队列服务 API 中的 GetMessages 方法,可对单个事务中指定数量的消息取消排队(见下面的说明)。

  3. 当检查在某一队列上是否存在工作项时,如果队列保持连续为空,则避免频繁的轮询间隔并且实现增加轮询请求之间的时间的退让延迟

  4. 减少队列侦听器的数目 – 在使用基于请求的模型时,如果队列为空,则每个角色实例仅使用 1 个队列侦听器。若要进一步将每个角色实例的队列侦听器数目减少到零,请使用通知机制在队列接收到工作项时实例化队列侦听器。

  5. 如果在大部分时间中队列都保持为空,则自动减少角色实例的数目并且继续监视相关系统度量,以便确定应用程序是否以及何时应增加实例数目以便处理增加的工作负荷。

  6. 实现一种机制以删除有害消息。有害消息通常是指应用程序无法处理的格式不正确的消息。如果保留为未处理状态,这种消息可能会累积并引发重复的事务性成本和处理成本。比较简单的实现机制可以是:从队列中删除比某个持续期阈值旧的消息,并将其写入归档系统以便进一步评估。

  7. 减少正常的超时故障。你在向服务发送请求时,可指定自己的超时并将其设置为小于 SLA 超时。在这种情形中,当请求超时时,它会被归类为正常超时,并计入可计费事务的数量。

上述大多数建议都可以转化为一个十分通用的实现,这个实现处理消息批次并且封装许多基础队列/blob 存储和线程管理操作。在本文的后面,我们将说明如何操作。

Important重要提示
在通过 GetMessages 方法检索消息时,队列服务 API 在单个取消排队操作中支持的最大批次大小限制为 32。

一般来说,Azure 队列事务的成本将随着队列服务客户端数目的增加(例如,在增加角色实例的数目时或在增加取消排队线程的数目时)而线性增加。为了阐释未利用上述建议的解决方案设计的潜在成本影响,我们将提供一个示例,以具体的数字来佐证。

效率低下的设计的成本影响

如果解决方案架构师未实现相关优化,则在解决方案在 Azure 平台上部署并运行后,上述计费系统体系结构将很可能会产生额外的运营支出。在本部分中将介绍为什么会产生可能的额外支出。

如在应用场景定义中所述,业务事务数据定期到达。但是,假定在 8 小时标准工作日中,该解决方案忙于处理工作负荷的时间仅是这 8 小时中的 25%。这将导致在长达 6 小时的工作时间(8 小时 * 75%)内,系统处于没有任何事务处理的“空闲”状态。此外,该解决方案在每天的 16 小时非工作时间中将不会接收任何数据。

在总共 22 小时的空闲期间中,该解决方案仍会尝试取消排队工作,因为它并不明确地知道新数据何时到达。在这个时间窗口中,假定采用默认的 1 秒轮询间隔,则每个单独的取消排队线程都将对一个输入队列执行最多 79,200 个事务(22 小时 * 60 分钟 * 60 个事务/分钟)。

如前所述,Azure 平台中的定价模型基于单独的“存储事务”。存储事务就是用户应用程序为添加、读取、更新或删除存储数据而提出的请求。在撰写本白皮书之际,存储事务按 10,000 个事务 $0.01 的费率计费(未考虑任何促销或特殊定价安排)。

Important重要提示
在计算队列事务的数目时,请记住将单个消息放置于一个队列中将会作为 1 个事务计数,而使用消息通常是一个由两个步骤构成的过程,它涉及在检索后发出请求从队列中删除消息。因此,成功的取消排队操作将导致两个存储事务。请注意,即使在取消排队请求导致未检索任何数据时,它仍将计为一个收费事务。

在上述情形下由单个取消排队线程生成的存储事务会在你的每月帐单上加上一笔大约 $2.38(79,200 / 10,000 * $0.01 * 30 天)的金额。比较之下,200 个取消排队线程(或者,200 个辅助角色实例中 1 个取消排队线程)会将每月成本增加 $457.20。这是在该解决方案完全未执行任何计算(只是对队列进行检查以便确定是否有任何工作项可用)时产生的成本。上面这个示例比较抽象,因为没有人会这样实现其服务,这就是执行下面所介绍的优化之所以重要的原因。

消除额外延迟的最佳做法

为了优化基于队列的 Azure 消息传送解决方案的性能,方法之一就是使用本部分中所述的随 Azure Service Bus 一起提供的发布/订阅消息传送层。

在这个方法中,开发人员将需要关注创建轮询和实时的基于推送的通知的组合,并且使侦听器能够订阅根据某些条件触发的通知事件(触发器),以便指示某一队列上有了新的工作负荷。这个方法通过用于调度通知的发布/订阅消息传送层增强了传统的队列轮询循环。

在一个复杂的分布式系统中,此方法将需要使用“消息总线”或“面向消息的中间件”,以便确保通知可以可靠地以松散耦合方式中继到一个或多个订阅服务器。为满足在 Azure 上运行和内部运行的松散耦合的分布式应用程序服务之间的消息传送要求,Azure Service Bus 是当然之选。它还非常适合于“消息总线”体系结构,此体系结构将在基于队列的通信中涉及的各进程之间实现交换通知。

在基于队列的消息交换中参与的进程可以采用以下模式:

Best-Practices-Messaging-Solutions-Azure3

具体而言,因为它与队列服务发布服务器和订阅服务器之间的交互相关,所以,适用于 Azure 角色实例之间的通信的那些原则也同样满足针对基于推送的通知消息交换的大部分要求。

Important重要提示
使用 Azure Service Bus 受到定价模型的约束,该定价模型将考虑针对 Service Bus 消息传送实体(例如队列或主题)的消息传送操作量。

因此,执行成本收益分析以便评估将 Service Bus 引入某一给定体系结构的利弊十分重要。延续上面的思路,也有必要评估引入基于 Service Bus 的通知调度层实际上是否会导致成本降低,从而证明进行投资和执行额外的开发工作是值得的。

有关针对 Service Bus 的定价模型的详细信息,请参阅 Azure 平台 FAQ 中的有关部分。

尽管对延迟的影响可以通过发布/订阅消息传送层十分轻松地予以解决,但通过使用下一部分中描述的动态(弹性)缩放可以进一步降低成本。

可伸缩性方面的最佳做法

Azure 存储空间在整体帐户级别和单个分区级别定义可伸缩性目标。一个队列在 Azure 中是其自身的单一分区,因此,它可以处理多达每秒 2000 条消息。当消息数量超出此配额时,存储服务会用“HTTP 503 服务器忙”消息响应。此消息表明该平台正在限制队列。应用程序设计者应做好容量计划,以确保有数量适当的队列可以维持应用程序的请求率。如果某一个队列无法应对应用程序的请求率,请设计具备多个队列的分区队列基础结构,以确保可伸缩性。

应用程序也可以针对不同消息类型综合利用若干不同队列。这样,通过允许多个队列并存而不是只拥塞一个队列,可确保应用程序可伸缩性。这样还可以离散控制基于不同队列中所存储消息的敏感度和优先级进行的队列处理。与低优先级队列相比,高优先级队列可以有更多的专用工作线程。

动态缩放的最佳做法

借助于 Azure 平台,客户能够比以往更快、更轻松地进行缩放。能够适应不断变化的工作负荷和流量是云平台的主要价值主张之一。这意味着“可伸缩性”不再是成本高昂的 IT 代名词,它现在是一种现成的功能,可根据需要在良好设计的云解决方案中以编程方式实现。

“动态缩放”是一种技术能力,体现了某一给定解决方案通过在运行时增减工作容量和处理能力来适应波动的工作负荷的能力。Azure 平台本身通过设置可按需购买计算小时的分布式计算基础结构来支持动态缩放。

区分 Azure 平台上的以下两种动态缩放十分重要:

  • “角色实例缩放”表示添加和删除附加的 Web 或辅助角色实例以便处理该时间点的工作负荷。这常常包括更改服务配置中的实例计数。增加实例计数将会导致 Azure 运行时启动新实例,而减少实例计数将会导致关闭正在运行的实例。

  • “进程(线程)缩放”表示根据当前工作负荷增减线程数目,保持某一给定角色实例中正在处理的线程数量足够。

对于基于队列的消息传送解决方案,动态缩放一般建议采用以下的组合:

  1. 监视关键绩效指标,包括 CPU 使用率、队列深度、响应时间和消息处理延迟。

  2. 动态增减角色实例的数目,以便处理工作负荷中的峰值(无论是可预测的还是不可预测的)。

  3. 以编程方式增减正在处理的线程数目,以便适应某一给定角色实例处理的变化的负荷情况。

  4. 对工作负荷进行精细划分并且执行并行处理(使用 .NET Framework 4 中的任务并行库)。

  5. 对于工作负荷变化非常大的解决方案,需要保持可变的容量,不仅能够应对突发的峰值情形,也不会产生设置额外实例的开销。

借助于服务管理 API,Azure 托管服务能够通过在运行时更改部署配置来修改其正运行的角色实例的数目。

note备注
在一个典型订阅中,Azure 小型计算实例的最大数目(或者就核心数目而言其他大小的计算实例的等效数目)默认被限制为 20。增加此配额的任何请求都应该向 Azure 支持团队提出。有关详细信息,请参阅 Azure 平台 FAQ

根据 Azure 自动调整的介绍,该平台可以根据队列消息深度上下调整实例计数。这对于动态调整是非常自然而适合的。附加的好处是 Azure 平台会为该应用程序监控和调整任务。

角色实例计数的动态缩放不见得始终是用于处理负荷峰值的最合适选择。例如,一个新的角色实例可能需要用几秒钟的时间来启动,并且关于启动时间目前没有相关的 SLA 度量。而一个解决方案可能只需要增加工作线程的数目即可处理增加的工作负荷。在处理工作负荷时,解决方案将会监视相关负荷度量,并且确定是否需要动态减少或增加工作进程的数目。

Important重要提示
目前,单个 Azure 队列的可伸缩性目标是“限制”到每秒 2000 个事务。如果有应用程序尝试超过此目标,例如,通过从运行数百个取消排队线程的多个角色实例来执行队列操作,则可能会收到来自存储服务的 HTTP 503“服务器忙”响应。在这种情形发生时,应用程序应使用指数退让延迟算法实现某一重试机制。但是,如果此类 HTTP 503 错误经常发生,则建议使用多个队列并且实现基于分区的策略来跨多个队列进行缩放。

在大多数情况下,对工作进程进行自动缩放由单独的角色实例负责完成。与之相反,角色实例缩放常常涉及负责监视性能度量和执行相应缩放操作的解决方案体系结构的核心元素。下图描绘了一个称作“动态缩放代理”的服务组件,它对负荷度量进行收集和分析,以便确定是否需要设置新实例或停用空闲实例。

Best-Practices-Messaging-Solutions-Azure4

需要特别注意的是,该缩放代理服务可作为在 Azure 上运行的辅助角色部署,也可作为内部服务部署。该服务将能够访问 Azure 队列,而与部署拓扑无关。

note备注
请考虑使用 Azure 的内置自动调整功能,作为手动动态调整的备用方法。

现在,我们已介绍了延迟影响、存储事务成本和动态缩放要求,此时是将我们的这些建议整合到一个技术实现中的时候了。

客户方案

为了让示例更为生动和具体,我们归纳总结了一个如下所示的真实客户方案。

一个 SaaS 解决方案提供商启动了一个新的计费系统,这个系统是作为 Azure 应用程序实现的,用来满足不同规模的客户事务处理的业务需要。这个解决方案的主旨是能够将大量占用计算资源的工作负荷转移到云上,并且利用 Azure 基础结构的灵活性来执行计算密集型工作。

该端到端体系结构的内部元素在全天中会定期将大量事务整合并调度到 Azure 托管服务上。每次提交的事务量从几千到几十万个不等,每天的事务量可达数百万个。此外,假定该解决方案必须满足一个 SLA 要求,即必须保证事务处理延迟不超过一个最大限值。

该解决方案的体系结构是建立在分布式 map-reduce(映射-化简)设计模式基础上的,并且由基于多实例辅助角色的云层构成(使用 Azure 队列存储来进行工作调度)。事务批次由 Process Initiator 辅助角色实例接收,分解(批次拆分)成若干更小的工作项,并且排入到 Azure 队列集合中以便进行负荷分配。

工作负荷的处理由处理辅助角色的多个实例执行,并且从队列中提取工作项并经由计算过程传递它们。这些处理实例利用多线程队列侦听器来实现并行数据处理以便获得最佳性能。

已处理的工作项将被路由到一个专用队列中,在该队列中,Process Controller 辅助角色实例将会取消对这些工作项的排队,并且将这些工作项聚合并保存到一个数据存储中以便进行数据挖掘、报告和分析。

该解决方案的体系结构如下图所示:

AzureGuidance_MaxScale

上图描绘了用于向外扩展大型或复杂计算工作负荷的典型体系结构。对于需要通过队列彼此进行通信的许多其他 Azure 应用程序和服务而言,此体系结构所采用的基于队列的消息交换模式是一种非常典型的模式。这使得可以采取规范的方法来检查在基于队列的消息交换中涉及的特定基础组件。

结论

为了最大限度地提高在 Azure 平台上运行的基于队列的消息传送解决方案的效率和成本效益,解决方案架构师和开发人员应该考虑以下建议。

作为解决方案架构师,你应该:

  • 设置一个基于队列的消息传送体系结构,该体系结构使用 Azure 队列存储服务用于基于云的解决方案或混合解决方案中各层和服务之间高度可伸缩的异步通信

  • 建议分区的队列体系结构扩展到超过 2000条消息/秒。

  • 理解 Azure 定价模型的基础知识并且通过一系列最佳做法和设计模式优化解决方案以便降低事务成本

  • 通过设置适应变化和波动的工作负荷的体系结构来考虑动态的缩放要求。

  • 采用适当的自动缩放技术和方法灵活地增减计算能力,以便进一步优化运营支出。

  • 评估 Azure 自动调整以了解它是否适合应用程序的动态调整需要

  • 评估通过依赖 Azure Service Bus 进行实时的基于推送的通知调度减少延迟所带来的成本效益比。

作为开发人员,你应该:

  • 设计一个消息传送解决方案,该解决方案在存储和检索来自 Azure 队列的数据时使用批处理

  • 评估 Azure 自动调整以了解它是否适合应用程序的动态调整需要

  • 实现一个高效的队列侦听器服务,确保将在为空时按一个取消排队线程的最大数目轮询队列。

  • 当队列在较长的一个时间段内保持为空时动态减少辅助角色实例的数目

  • 实现特定于应用程序的随机指数退让算法,以便降低空闲队列轮询对存储事务成本的影响。

  • 在实现高度多线程的多实例队列发布服务器和使用者时,采用阻止超出单个队列的可伸缩性目标的正确技术。

  • 采用能够在发布和使用来自 Azure 队列的数据时处理多种瞬时条件的强健的重试策略

  • 使用 Azure Service Bus 提供的单向事件处理功能来支持基于推送的通知,以便缩短延迟并提高基于队列的消息传送解决方案的性能。

  • 利用 .NET Framework 4 的新功能(例如 TPL、PLINQ 和 Observer 模式)来最大限度地增加并行度、提高并发性并且简化多线程服务的设计。

随附的示例代码可从 MSDN 代码库下载。该示例代码还包括所有要求的基础结构组件,例如针对 Azure 队列服务的识别泛型的抽象层,它在上面的代码段中未提供。请注意,所有源代码文件均受到相应法律声明中所述的 Microsoft 公共许可证的管辖。

其他资源和参考

社区附加资源

显示: