璧仓隔离策略 (v5.0 起可用)
目标
限制受治理操作可使用的资源,以便故障“风暴”不会导致连锁失败,也不会导致其他操作失败。
前提:“一个错误不应该搞垮整艘船!”
当一个流程开始出错时,它可能会积累大量请求,所有请求都可能并行地缓慢失败。如果不加以控制,它们会占用主机中更多的资源(CPU/线程/内存等),降低性能或最终彻底失败。
在另一种情形下,出现故障的下游系统可能导致在其调用者中积累大量请求。如果加以管理,这些积累的请求调用反过来消耗消费者中的所有资源,导致级联上游错误。
舱壁是船内的一堵墙,它将一个隔间与另一个隔间隔开,这样一个隔间的损坏不会导致整艘船沉没。
类似地,隔板隔离策略会约束操作,使得一个操作异常的的故障通道不能耗费系统中的所有资源(线程/CPU/任何资源),从而导致其他操作与之一起宕机。受到隔离的问题系统所能影响与耗用的资源是有限的;所以其他线程/池/容量仍然可以为其他调用正常使用。
有关使用舱壁策略的进一步讨论,请参见我们的关于主动弹性工程的概述讨论。
语法
BulkheadPolicy bulkhead = Policy .Bulkhead(int maxParallelization [, int maxQueuingActions] [, Action<Context> onBulkheadRejected]); BulkheadPolicy bulkhead = Policy .BulkheadAsync(int maxParallelization [, int maxQueuingActions] [, Func<Context, Task> onBulkheadRejectedAsync]);
参数:
- maxParallelization: 舱壁内允许的最大并行执行数量;
- maxQueuingActions (可选): 任何时候可能正在排队(等待获取执行槽)的最大数量.
- onBulkheadRejected/Async (可选): 如果隔板拒绝执行,则要运行的委托操作
抛出的异常:
BulkheadRejectedException
, 当操作由于舱壁和队列容量超过而被拒绝执行时
使用
理解该策略的一个有用方法是,单独的隔离壁将调用放入固定容量的单独线程池中。
然而,舱壁并不是这么实现的。相反,作为断路器,舱壁被实现为一个最大化并行化信号量,通过舱壁来执行方法。因此,隔板策略是一个简单的并行限流器。
当调用进入舱壁时,隔离策略:
- 确认舱壁中是否有执行槽,如果有则立即执行
- 如果没有,则确定_队列_中是否还有空间;
- 如果队列中没有空间, 抛出异常:
BulkheadRejectedException
- 当队列中有空间时,执行进入队列,并阻塞(根据策略是同步/异步的,同步或异步地阻塞),直到在隔板中获得一个执行槽。
- 如果队列中没有空间, 抛出异常:
策略本身不会对线程进行调用;它假设上游系统已经对线程进行了调用,但限制了它们的并行执行。
“队列”的意义是什么?
舱壁的主要目的是充当夜总会的保镖:确保“俱乐部”的最大容量不会被超过。与此同时,就像夜总会保镖一样,次要目标是确保俱乐部内部的资源总是得到最大限度的利用。
为了实现这一点,可以让一群“准备消费的人”排队(如果你愿意,可以在夜总会外的人行道上排队),等着一有空就占据舱壁内的执行槽。这是maxQueuingActions
。
有关设置maxQueuingActions
的指导,请参阅下面的配置建议。
舱壁隔离实例的范围
BulkheadPolicy的实例通过舱壁内部的各种操作维护其内部状态:你必须在每次通过调用点执行时重用相同的BulkheadPolicy实例,而不是在每次执行代码时创建一个新实例。
与策略操作交互
OnBulkheadRejected
可选的onBulkheadRejected
/ onBulkheadRejectedAsync
委托允许你在bulkhead拒绝执行时执行特定的代码(例如日志记录)。
State
舱壁隔离策略公开两个状态属性,用于报告/运行状况监视:
BulkheadAvailableCount
: 目前舱壁中可用的执行槽数QueueAvailableCount
: 舱壁执行槽队列中可用的空间数
BulkheadAvailableCount
和 QueueAvailableCount
的递减级别可能会被监控,用作触发自动水平伸缩的指标。
注意:这样的代码是不必要的:
if (bulkhead.BulkheadAvailableCount + bulkhead.QueueAvailableCount > 0) { bulkhead.Execute(...); // place call }
只要调用 bulkhead.Execute(...)
就足够了,而bulkhead将自己决定该操作是否可以执行或排队。此外,用户应该清楚,上述代码不能保证隔板不会阻止执行:在高度并发的环境中,在评估if条件和执行操作之间,状态仍可能会发生变化。然而,如果抛出异常的数量是一个性能问题,可以使用上面这样的代码模式来减少在舱壁处于容量不足状态时抛出的BulkheadRejectedException
的数量。
配置建议
从业务和体系结构的角度配置maxParallelization
舱壁策略既充当隔离单元,又(有意地)充当负载分流器。为了保持底层机器的运行状况,舱壁有意在其容量和队列耗尽时释放负载。
舱壁特别适合与自动水平伸缩策略配合使用。理想情况下,你希望能够容忍舱壁拒绝(要求用户或进程“稍后再来”);或者使用基于舱壁拒绝的指标,或根据BulkheadAvailableCount
/QueueAvailableCount
值的缩小作为自动水平扩展的触发器。Azure和Amazon云服务都允许定义自定义指标作为自动水平扩展的触发器。
舱壁的容量设置主要根据以下:
- 它是否管理一个I/O-bound(占用IO资源)或CPU-bound(占用Cpu资源)操作
- 底层应用程序或主机硬件/VM/云实例支持的其他操作(也可以期望在负载下同时操作)
- 你有什么可用的自动化水平扩展
配置隔离容量的推荐方法是模拟生产环境进行负载测试或饱和测试。
然而,重要的是,不要将隔离容量设置为单个操作的峰值负载量,因为该进程不是单独运行(言外之意:你还需要考虑该主机上其他进程的运行)。在这个级别设置隔离容量不会为同时运行的其他进程提供保护:(当它出现故障时)第一个进程的调用被允许执行,这样将消耗主机上的所有可用资源,从而降低其他进程的性能。
如果你的目的是在高并发容量下获得最大弹性,足够的自动化水平扩展可以支持这一点,例如,一个运行四个客户关键进程的应用程序(所有进程都希望在正常负载水平上同时运行)可能会分配隔离舱,将每个独立进程的容量限制为小于主机容量的四分之一。这种稳定性有限的策略 更适合触发水平伸缩—通过保持底层各个主机的健康状况来减少消费者的整体延迟。
或者,你可以容忍冒着一点风险来控制水平扩展的成本,通过不像上面除以4的例子所建议的那样严格限制每个舱壁容量,让调用共享一个舱壁。让不同的调用共享同一个舱壁实例,这一组调用共享同一个舱壁容量::如果可以预期不同调用的资源消耗情况,这可以提供更大的灵活性(以及更有效地使用资源),例如:有不同的调用峰值时间,代价是其中一个调用流有有可能减低其他调用流的性能。
最后,还要记住,应用程序中的软件级别分区(“BulkheadPolicy”只是线程级别)只是提供稳定性隔离的一个级别。例如,您也可以在服务器级别对系统进行分区,保留一些服务器纯粹用于管理功能,以便即使用户负载崩溃,也始终有重要的管理功能可用。请参阅Michael Nygard: Release It!了解更多隔离模式。
配置并行化 - 从技术和软件的角度
对于Cpu密集型工作(例如,调整用户上传的图片大小),根据主机系统中的处理器数量来配置隔板容量(单独考虑一个调用)是有意义的,就像“Parallel.For”一样。将并行度限制在接近处理器的数量可以防止不适当的上下文切换:在主机的处理器数量上,通常有一个最佳性能点。
对于IO密集型的异步操作,情况更加微妙。任何时候的多个调用可能在async
/await
的(非线程/cpu高消耗)await
阶段,因此建议将舱壁容量(单独考虑一个调用)设置为比纯线程容量高得多的级别。这使得.Net可以在实际活动中在调用和未调用之间优化线程的使用。最佳配置将取决于调用在await
中平均花费的时间;对于单个系统的特性,没有真正的性能调优捷径。
在这个特性中,我们希望用户的个人配置根据他们所管理的操作而变化:为了帮助其他用户,我们很有兴趣听你的故事。
配置 maxQueuingActions
maxQueuingActions
可以让调用并行化,而不是立即拒绝执行,从而提供了灵活性。它还通过提供准备好并等待的下一个操作来帮助最大限度地提高吞吐量,只要隔板执行槽变为可用。
然而,建议不要将maxQueuingActions
的值设置很高,原因如下:
同步情况 (Cpu密集型作业)
在(当前)同步实现中,队列项将阻塞线程。出于这个原因,我们建议在当前同步情况下只将maxQueuingActions
设置为0或1。(Polly的未来版本可能会探索一种调度同步工作的替代策略,例如SchedulerPolicy
,它将在底层的TaskScheduler
上调度工作。这将允许同步工作在隔板外'排队'而不占用线程,但可能需要一个新的语法,结合现有的同步Execute()
和异步ExecuteAsync()
重载。
异步情形 (I/O密集型作业)
理想的配置可能取决于受治理调用的特性,应通过实验确定。
对于非常低延迟、高吞吐量的调用,允许稍高一些的队列级别可能是有意义的,这样当舱壁槽可用时,下一个操作总是可以立即填充它。然而,设置非常高的队列值是没有意义的:在等待隔板槽可用时,队列中的调用当然会经历更高的延迟,因此,应该设置队列长度,使隔板以最大吞吐量提供,同时不会导致过高的延迟。主要的重点应该是正确地获得舱壁的主要容量,然后监测减少舱壁容量或拒绝呼叫,作为水平扩展的触发器。
线程安全及策略复用
线程安全
BulkheadPolicy的内部操作是线程安全的:多个调用可以通过一个策略实例安全地并发地进行。
策略复用
BulkheadPolicy
实例可以跨多个功能点重用。
当一个舱壁实例跨调用点重用时,调用点共享舱壁的容量(允许您将操作分组到舱壁),而不是每个调用点有独立的舱壁。
在重用策略时,使用“OperationKey”来区分日志和度量中的不同调用站点使用情况。
在重用策略时,使用“OperationKey”来区分日志和度量中的不同调用站点使用情况。
相关项目
Polly策略故意是不可改变的。然而,我们出色的社区贡献者贡献了Polly.Contrib.MutableBulkheadPolicy (github;nuget (https://www.nuget.org/packages/Polly.Contrib.MutableBulkheadPolicy)。