Orleans 2.0 官方文档 —— 7.4 流 -> 流编程API

Orleans 流编程API

应用程序通过API与流进行交互,这些API与众所周知的.NET的Reactive Extensions(Rx)非常相似。主要的区别在于,Orleans流扩展是异步的,以使Orleans的分布式和可扩展的计算fabric中的处理更加高效。

异步流

应用程序首先使用流提供程序来获取流的句柄。您可以在此处阅读有关流提供程序的更多信息,但是现在您可以将其视为流的工厂,它允许实现者自定义流的行为和语义:

IStreamProvider streamProvider = base.GetStreamProvider("SimpleStreamProvider");
IAsyncStream stream = streamProvider.GetStream(Guid, "MyStreamNamespace");

应用程序可以通过调用Grain类的GetStreamProvider方法(当在grain内时),或者调用GrainClient.GetStreamProvider()方法(当在客户端时),来获取流提供程序的一个引用。

Orleans.Streams.IAsyncStream虚拟流的逻辑的、强类型的句柄。它在本质上与Orleans grain 的引用相似。对GetStreamProviderGetStream的调用纯粹是本地的。GetStream的参数是一个GUID和一个附加字符串(可以为null),此附加字符串我们称之为流的命名空间。GUID和命名空间字符串一起构成流标识(本质上与GrainFactory.GetGrain的参数相类似)。GUID和命名空间字符串的组合,为确定流标识提供了额外的灵活性。就像PlayerGrain类型内可以存在一个grain 7而ChatRoomGrain内也可以存在一个grain 7一样,命名空间PlayerEventsStream内可以存在一个流123,命名空间ChatRoomMessagesStream内也可以存在一个不同的流123 。

生产和消费

IAsyncStream实现了 Orleans.Streams.IAsyncObserver和 Orleans.Streams.IAsyncObservable接口。这样,应用程序可以使用流的方式,要么通过使用Orleans.Streams.IAsyncObserver来生成新的事件到流中,要么通过使用Orleans.Streams.IAsyncObservable来订阅和消费流中的事件。

public interface IAsyncObserver<in T>
{
    Task OnNextAsync(T item, StreamSequenceToken token = null);
    Task OnCompletedAsync();
    Task OnErrorAsync(Exception ex);
}

public interface IAsyncObservable<T>
{
    Task> SubscribeAsync(IAsyncObserver observer);
}

要生成事件到流中,应用程序只需调用

await stream.OnNextAsync(event)

要订阅流,应用程序可以调用

StreamSubscriptionHandle subscriptionHandle = await stream.SubscribeAsync(IAsyncObserver)

SubscribeAsync的参数,可以是实现IAsyncObserver接口的对象,也可以是处理传入事件的lambda函数的组合。通过AsyncObservableExtensions类可以获得更多的SubscribeAsync选项。 SubscribeAsync返回一个StreamSubscriptionHandle,这是一个不透明的句柄,可以用来取消订阅流(本质上类似于IDisposable的异步版本)。

await subscriptionHandle.UnsubscribeAsync()

需要注意的是,订阅是针对grain,而不是针对激活体。一旦grain代码订阅了流,则此订阅将超越激活体的生命周期,并永远保持持久性,直到grain代码(可能在不同的激活体中)显式地取消订阅。这是虚拟流抽象的核心:不仅在逻辑上所有流都始终存在,而且流订阅是持久的,并且超越了发出此订阅的特定的物理激活体。

多重性

Orleans流可能有多个生产者和多个消费者。生产者发布的消息,将传递给在发布消息之前订阅了该流的所有消费者。

此外,消费者可以多次订阅同一个流。每次订阅时,它都会获得一个唯一的StreamSubscriptionHandle。如果一个grain(或客户端)订阅同一个流X次,那么它将收到相同的事件X次,1次订阅就会有1次事件。消费者还可以取消个别的订阅,或通过以下方式,查找其所有的当前订阅:

IList> allMyHandles = await IAsyncStream.GetAllSubscriptionHandles()

从故障中恢复

如果流的生产者死亡(或其grain被停用),则无需做任何事情。下一次这个grain想要生成更多的事件时,它可以再次获得流句柄,并以相同的方式生成新的事件。

消费者逻辑稍微复杂一些。如前所述,一旦消费者grain订阅了一个流,则此订阅在显式地取消之前都是有效的。如果流的消费者死亡(或其grain被停用)并且在流上有生成新的事件,则消费者grain将被自动重新激活(就像任何常规的Orleans grain在收到消息时自动激活一样)。grain代码现在唯一需要做的就是,提供一个IAsyncObserver来处理数据。基本上,消费者需要重新附加处理逻辑,来作为OnActivateAsync方法的一部分。为此,它可以调用:

StreamSubscriptionHandle<int> newHandle = await subscriptionHandle.ResumeAsync(IAsyncObserver)

消费者使用先前它在第一次订阅时获得的句柄,来“恢复处理”。请注意,ResumeAsync仅使用IAsyncObserver逻辑的新实例,来更新现有的订阅,并且不会更改消费者已订阅此流的事实。

消费者如何找出旧的订阅句柄?有两个选项。消费者可能保留了从原始SubscribeAsync操作返回的句柄,则现在可以使用它。或者,如果消费者没有句柄,它可以通过以下的调用,向IAsyncStream询问所有活动的订阅句柄:

IList> allMyHandles = await IAsyncStream.GetAllSubscriptionHandles()

消费者现在便可以恢复所有这些句柄,或者如果愿意,也可以从某些句柄中取消订阅。

注释:如果消费者grain 直接实现IAsyncObserver接口(public class MyGrain : Grain, IAsyncObserver),理论上不应该要求重新附加IAsyncObserver,因此不需要调用ResumeAsync。流运行时应该能够自动发现grain已经实现IAsyncObserver,并只调用那些IAsyncObserver方法。但是,流运行时当前不支持这一点,即使grain直接实现了IAsyncObserver,grain代码仍然需要显式地调用ResumeAsync。支持这一点在我们的待办事项列表中。

显式和隐式订阅

默认情况下,流使用者必须显式订阅流。此订阅通常由grain(或客户端)接收到的、指示它们订阅的一些外部消息触发。例如,在用户加入聊天室时的聊天服务中,它的grain会收到一条带有聊天名称的JoinChatGroup消息,这会导致用户grain订阅此聊天流。

此外,Orleans 流还支持“隐式订阅”。在这个模式下,grain没有显式地订阅流。此grain基于其grain标识和一个ImplicitStreamSubscription属性,来自动地、隐式地订阅。隐式订阅的主要价值是,允许流活动自动触发grain激活(从而触发订阅)。例如,使用SMS流,如果一个grain想要生成流,而另一个grain处理该流,则生产者需要知道消费者grain的标识,并对其进行grain调用,告诉它订阅流。只有在此之后,它才能开始发送事件。相反,使用隐式订阅,生产者可以开始在流上生成事件,并且消费者grain将自动激活并订阅流。在这种情况下,生产者根本不关心谁在读这些事件。

MyGrainType的grain实现类,可以声明一个[ImplicitStreamSubscription("MyStreamNamespace")]属性。这告诉流运行时,如果在标识为GUID XXX和命名空间为"MyStreamNamespace"的流上生成事件,则应将其传递给标识为XXX的、类型为MyGrainType的grain。也就是说,运行时将流映射到消费者grain 

ImplicitStreamSubscription的出现,会导致流运行时自动将grain订阅到流,并将流事件传递给grain。但是,grain代码仍然需要告诉运行时,它希望如何处理事件。本质上,它需要附加IAsyncObserver。因此,当grain被激活时,OnActivateAsync内的grain代码需要调用:

IStreamProvider streamProvider = base.GetStreamProvider("SimpleStreamProvider");
IAsyncStream stream = streamProvider.GetStream(this.GetPrimaryKey(), "MyStreamNamespace");
StreamSubscriptionHandle subscription = await stream.SubscribeAsync(IAsyncObserver);

编写订阅逻辑

以下是关于如何为各种情况编写订阅逻辑的指南:显式和隐式订阅、可回滚和不可回滚的流。显式订阅和隐式订阅之间的主要区别在于,对于隐式订阅,grain对于每个流命名空间始终只有一个隐式订阅,无法创建多个订阅(没有订阅的多重性),无法取消订阅,并且grain逻辑始终只需要附加处理逻辑。这也意味着对于隐式订阅,永远不需要恢复订阅。另一方面,对于显式订阅,需要恢复订阅,否则如果grain再次订阅,将导致grain被多次订阅。

隐式订阅:

对于隐式订阅,grain需要以附加处理逻辑来订阅。这应该在grain的OnActivateAsync方法中完成。grain应在它的OnActivateAsync方法中,简单地执行await stream.SubscribeAsync(OnNext ...)。这将导致此特定的激活体,附加OnNext函数来处理该流。grain能可选地指定StreamSequenceToken作为SubscribeAsync的参数,这会使得此隐式订阅,从该Token处开始消费。隐式订阅永远不需要调用ResumeAsync

public async override Task OnActivateAsync()
{
    var streamProvider = GetStreamProvider(PROVIDER_NAME);
    var stream = streamProvider.GetStream<string>(this.GetPrimaryKey(), "MyStreamNamespace");
    await stream.SubscribeAsync(OnNextAsync)
}

显式订阅:

对于显式订阅,grain必须调用SubscribeAsync订阅流。这会创建一个订阅,并附加处理逻辑。显式订阅将一直存在,直到grain取消订阅,因此,如果grain被停用并重新激活,那么grain仍然是显式地订阅,但不会附加处理逻辑。在这种情况下,grain需要重新附加处理逻辑。要做到这一点,在它的OnActivateAsync中,grain首先需要通过调用stream.GetAllSubscriptionHandles(),来找出它已有的订阅。grain必须在它希望继续处理的每个句柄上执行ResumeAsync,或者在它完成的任何句柄上执行UnsubscribeAsync。grain也能可选地指定StreamSequenceToken作为调用ResumeAsync的参数,这将使得此显式订阅,从该Token处开始消费。

public async override Task OnActivateAsync()
{
    var streamProvider = GetStreamProvider(PROVIDER_NAME);
    var stream = streamProvider.GetStream<string>(this.GetPrimaryKey(), "MyStreamNamespace");
    var subscriptionHandles = await stream.GetAllSubscriptionHandles();
    if (!subscriptionHandles.IsNullOrEmpty())
        subscriptionHandles.ForEach(async x => await x.ResumeAsync(OnNextAsync));
}

流顺序和序列Token

一个生产者和一个消费者之间的事件递送顺序,取决于流提供程序。

通过SMS,生产者通过控制发布事件的方式,来显式地控制消费者看到的事件的顺序。默认情况下(如果SMS提供程序的选项FireAndForget设置为false),如果生产者await每个OnNextAsync调用,则事件按FIFO的顺序到达。在SMS中,由生产者决定如何处理传递故障,此故障可由调用OnNextAsync时返回的中断的Task来指示。

Azure队列流不保证FIFO顺序,因为底层的Azure队列不保证在故障情况下的顺序(它们无故障执行中能保证FIFO顺序)。当生产者将事件生成到Azure队列中时,如果压入队列失败,则由生产者尝试再次压入队列,稍后再处理可能重复的消息。在发送方,Orleans 流运行时将事件从Azure队列中弹出队列,并尝试将其传递给消费者进行处理。仅在成功处理后,Orleans 流运行时才从队列中删除事件。如果传递或处理失败,则不会从队列中删除该事件,并且该事件将在稍后自动重新出现在队列中。流运行时将尝试再次传递它,从而可能破坏FIFO顺序。所描述的行为符合Azure队列的常规语义。

应用程序定义的顺序:为了处理上述排序问题,应用程序可以选择指定自己的顺序。这是通过StreamSequenceToken的概念来实现的。StreamSequenceToken是一个不透明的IComparable对象,可用于对事件进行排序。生产者能将可选的StreamSequenceToken传递给OnNext调用。此StreamSequenceToken将一路传递给消费者,并与事件一起传递。这样,应用程序可以独立于流运行时,来推理和重新构造它的顺序。

可回滚的流

一些流只允许应用程序从最新的时间点开始订阅它们,而一些其他流允许“时光倒流”。后一种能力取决于底层的队列技术和特定的流提供程序。例如,Azure Queues仅允许消费最新的已压入队列的事件,而EventHub允许从任意时间点(直到某个到期时间)重放事件。支持时光倒流的流称为可回滚流

可回滚流的消费者,可以将StreamSequenceToken传递给SubscribeAsync调用,运行时将从该StreamSequenceToken开始,向消费者传递事件(null Token意味着消费者希望从最新的时间点开始接收事件)。

在复原场景中,回放流的能力非常有用。例如,考虑订阅流并定期检查其状态和最新序列Token的grain。当从故障中复原时,grain可以从最新的检查点序列Token重新订阅相同的流,从而复原,而不会丢失自上一个检查点以来生成的任何事件。

可回滚流的当前状态: SMS和Azure队列提供程序都不可回滚,而Orleans目前不包含可回滚流的实现。我们正在积极致力于此。

无状态的自动扩展处理

默认情况下,Orleans流的目标是支持大量相对较小的流,每个流由一个或多个有状态的grain来处理。总的来说,所有流的处理都是在大量的常规(有状态)的grain之间被分片。应用程序代码通过分配流ID、grain ID和显式订阅来控制这种分片。目标是分片状态处理

但是,还有一个有趣的场景是自动扩展无状态处理。在这种情况下,应用程序具有少量流(甚至一个大流),而目标是无状态处理。例如,所有事件的所有消息的一个全局流,以及涉及某种解码/解密的处理,并且可能将它们转发到另一组流中,以用于进一步的有状态处理。Orleans 通过StatelessWorker grain,可以支持无状态的扩展流处理。

无状态自动扩展处理的当前状态: 目前尚未实现(由于优先级约束)。尝试订阅来自StatelessWorker grain的一个流,将导致未定义的行为。我们目前正在考虑支持这一选项。

grain和Orleans客户端

Orleans流在grain和Orleans客户端之间一致地工作。也就是说,可以在grain和Orleans客户端中使用完全相同的API,来生成和消费事件。这极大地简化了应用程序逻辑,使得特殊的客户端API(如Grain Observers)变得多余。

完全托管的和可靠的Streaming Pub-Sub

为了跟踪流订阅,Orleans使用名为Streaming Pub-Sub的运行时组件,作为流使用者和流生成者的集合点。Pub Sub跟踪所有流订阅,持久化它们,并将流消费者与流生成者匹配。

应用程序可以选择Pub-Sub数据的存储位置和存储方式。Pub-Sub组件本身被实现为grain(被称为PubSubRendezvousGrain),并且它正在使用Orleans Declarative Persistence来处理这些粒子。PubSubRendezvousGrain使用名为存储提供商PubSubStore。与任何粮食一样,您可以为存储提供商指定实施。对于Streaming Pub-Sub,您可以PubSubStore使用silo主机构建器更改at silo构造时间的实现:

下面配置Pub Sub,将其状态存储在Azure表中。

hostBuilder.AddAzureTableGrainStorage("PubSubStore", 
    options=>{ options.ConnectionString = "Secret"; });

这样,Pub-Sub数据将被持久地存储在Azure表中。对于开发初期,您也可以使用内存存储。除了Pub-Sub之外,Orleans流运行时还将事件从生产者传递给消费者,管理分配给活动使用的流的所有运行时资源,并透明地从未使用的流中回收运行时资源。

配置

要使用流,您需要通过silo主机或集群客户端构建器,启用流提供程序。您可以在此处详细了解流提供程序。流提供程序的设置示例:

hostBuilder.AddSimpleMessageStreamProvider("SMSProvider")
  .AddAzureQueueStreams("AzureQueueProvider",
    optionsBuilder => optionsBuilder.Configure(
      options=>{ options.ConnectionString = "Secret"; }))
  .AddAzureTableGrainStorage("PubSubStore",
    options=>{ options.ConnectionString = "Secret"; });

下一步

Orleans流提供程序

你可能感兴趣的:(Orleans)