Orleans 2.0 官方文档 —— 8.6 实现细节 -> 流的实现

Orleans 流的实现细节

本节提供了Orleans 流实现的高级概述。它描述了在应用程序级别上不可见的概念和细节。如果您只打算使用流,则不必阅读本节。

术语

我们用“队列”一词,来指可以吸收流事件的、并允许拉取事件或提供基于推送的机制来消费事件的、任何的持久存储技术。通常,为了提供可伸缩性,这些技术提供分片/分区队列。例如,Azure队列允许创建多个队列,Event Hubs有多个hub,Kafka主题,……

持久流

所有Orleans持久化的流提供程序,都共享一个共同的实现PersistentStreamProvider。此通用的流提供程序,需要使用特定于技术的IQueueAdapterFactory,来进行配置。

例如,出于测试目的,我们有队列适配器生成自己的测试数据,而不是从队列中读取。下面的代码显示了我们如何配置持久化流提供程序,以使用我们的自定义(生成器)队列适配器。它通过使用工厂方法,配置持久流提供程序来创建适配器,来实现这一点。

hostBuilder.AddPersistentStreams(StreamProviderName, GeneratorAdapterFactory.Create);

当流生成器生成一个新的流条目,并调用时stream.OnNext(),Orleans 流运行时会在该流提供程序的IQueueAdapter上,调用适当的方法,将该条目直接排队到适当的队列中。

拉取代理

持久流提供商的核心是拉取代理。拉取代理从一组持久队列中拉取事件,并将它们传递给消费它们的grain中的应用程序代码。人们可以将拉取代理视为分布式的“微服务” —— 一个分区的、高可用的、弹性的分布式组件。拉取代理运行在与承载应用grain的相同的silo上,并由Orleans 流运行时完全管理。

StreamQueueMapper和StreamQueueBalancer

使用IStreamQueueMapper和参数化IStreamQueueBalancer对拉取代理进行参数化。 IStreamQueueMapper 提供所有队列的列表,并负责将流映射到队列。这样,持久流提供程序的生产者端,就知道将消息排队到哪个队列。

IStreamQueueBalancer 表达了Orleans silo和代理之间平衡队列的方式。目标是以平衡的方式,将队列分配给代理,以防止瓶颈并支持弹性。当新的silo添加到Orleans集群时,新旧silo之间的队列将自动重新平衡。StreamQueueBalancer允许自定义该过程。Orleans有许多内置的StreamQueueBalancers,支持不同的平衡场景(大量和少量队列)和不同的环境(Azure、on prem、static)。

使用上面的测试生成器示例,下面的代码,展示了如何配置队列映射器和队列平衡器。

hostBuilder
  .AddPersistentStreams(StreamProviderName, GeneratorAdapterFactory.Create,
    providerConfigurator=>providerConfigurator
      .Configure<HashRingStreamQueueMapperOptions>(ob=>ob.Configure(
        options=>{ options.TotalQueueCount = 8; }))
      .UseDynamicClusterConfigDeploymentBalancer()
);

上面的代码将GeneratorAdapter配置为使用具有8个队列的队列映射器,并使用DynamicClusterConfigDeploymentBalancer,对整个集群中的队列进行平衡。

拉取协议

每个silo都运行一组拉取代理,每个代理都从一个队列中进行拉取。拉取代理本身由内部运行时组件(称为SystemTarget)实现。SystemTargets本质上是运行时grain,遵循受单线程并发,可以使用常规的grain消息传递,并且和grain一样轻量级。与grain相反,SystemTargets不是虚拟的:它们是(由运行时)显式地创建的,并且也不是位置透明的。通过将拉取代理实现为SystemTargets,Orleans 流运行时可以依赖于许多内置的Orleans功能,并且还可以扩展到大量的队列,因为创建新的拉取代理的代价与创建新的grain一样廉价。

每个拉取代理都运行周期性的计时器,从队列中(通过调用IQueueAdapterReceiverGetQueueMessagesAsync()方法)进行拉取。返回的消息,放入内部的逐代理的名为IQueueCache的数据结构。每个消息都会被检查,以找出其目标流。代理使用Pub Sub,查找订阅此流的流消费者列表。一旦检索到消费者列表,代理就会将其存储在本地(在它的pub-sub缓存中),因此不需要每条消息都咨询Pub Sub。代理还订阅了pub-sub,以接收订阅该流的任何新消费者的通知。代理和pub-sub之间的这种握手,保证了强大的流订阅语义一旦消费者订阅了流,它将看到在订阅之后生成的所有事件(此外,使用StreamSequenceToken允许订阅过去的事件)。

队列缓存

IQueueCache是一种内部的逐代理的数据结构,它允许将从队列中的取事件和将事件传递给消费者,进行了解耦。它还允许将传递给不同的流和不同的消费者进行了解耦。

想象一下这样一种情况:一个流有3个流消费者,而其中一个流很慢。如果不加小心,这个缓慢的消费者可能会影响代理的处理,减慢该流的其他消费者的消费,甚至可能减慢其他流事件的出队和传递。为了防止这种情况,并允许代理中的最大并行性,我们使用IQueueCache

IQueueCache缓冲流事件,并为代理提供一种方式,以便按照其节奏,向每个消费者传递事件。每个消费者的传递,由称为IQueueCacheCursor的内部组件实现,该组件跟踪每个消费者的进度。这样,每个消费者都可以按照自己的节奏接收事件:快速消费者会在队列出列时,尽快地接收事件,而慢速的消费者则会在以后接收事件。一旦消息被传递给所有消费者,就可以从缓存中删除它。

反压(Backpressure)

Orleans 流运行时中的反压适用于两个地方:将队列中的流事件传递给代理并将事件从代理传递给流使用者

后者由内置的Orleans消息传递机制提供。每一个流事件,都是由代理通过标准的Orleans grain消息传递,传递给消费者的,一次一个。也就是说,代理向每个单独的流消费者发送一个事件(或一个有限大小的事件批),然后等待此调用。在上一个事件的Task被解决或中断之前,下一个事件将不会开始传递。这样,我们自然地将每个消费者的传送速率限制为一次一条消息。

关于将流事件从队列传递给代理,Orleans 流提供了一种新的特殊的反压机制。由于代理将事件的从队列中出队,与将事件传递给消费者进行了解耦,因此单个速度较慢的消费者可能会落后太多,以致IQueueCache会填满。为了防止IQueueCache无限增长,我们限制其大小(大小限制是可配置的)。但是,代理从不丢弃未送达的事件。相反,当缓存开始填满时,代理会降低事件从队列中出队的速率。这样,我们就可以通过调整从队列中消耗的速率(“反压”),来“度过”缓慢的传送期,然后再回到快速的消费速率。为了检测“慢速传递”的谷值,IQueueCache使用缓存桶的内部数据结构,跟踪向单个流使用者传递事件的进度。这导致了一个非常灵敏和自我调节的系统。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值