Orleans核心功能

一、Grain持久性

二、定时器和提醒

三、依赖注入

四、观察者

五、无状态工作者Grains

六、流

一、Grain持久化

1,Grain持久化目标

①允许不同类型的存储提供者使用不同类型的存储提供者(例如,一个使用Azure表,一个使用ADO.NET表),或者使用不同类型的存储提供者,但具有不同的配置(例如,两者都使用Azure表, 存储帐户#1和一个使用存储帐户#2)

②允许配置存储提供程序实例(例如Dev-Test-Prod),只更改配置文件,不需要更改代码。

③提供一个框架,以便稍后由Orleans团队或其他人编写其他存储提供程序。

④提供最少量的生产级存储提供商

⑤存储提供者可以完全控制他们如何在持久性后台存储中存储Grain状态数据。 推论:Orleans没有提供全面的ORM存储解决方案,但允许定制存储提供商在需要时支持特定的ORM要求。

2,Grain持久化Api

Grain类型可以通过以下两种方式之一进行声明:

  • 如果它们没有任何持久状态,或者它们将自己处理所有的持续状态,则扩展Grain
  • 如果他们有一些他们想要Orleans运行时处理的持续状态,请扩展Grain <T>。 换句话说,通过扩展Grain <T>,grain类型自动加入到Orleans系统管理的持久化框架中。

对于本节的其余部分,我们将只考虑选项#2 / Grain <T>,因为选项1的Grain将继续像现在一样运行而不会有任何行为改变。

3,Grain状态存储

从Grain <T>继承的Grain类(其中T是需要持久化的特定于应用程序的状态数据类型)将从指定的存储区自动加载它们的状态。

Grain将被标记为[StorageProvider]属性,该属性指定用于读取/写入谷物的状态数据的存储提供者的命名实例。

[StorageProvider(ProviderName="store1")]
public class MyGrain<MyGrainState> ...
{
...
}

Orleans提供者管理框架提供了一种机制,指定和注册仓储配置文件不同的存储供应商和存储选项。

<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<StorageProviders>
<Provider Type="Orleans.Storage.MemoryStorage" Name="DevStore" />
<Provider Type="Orleans.Storage.AzureTableStorage" Name="store1"
DataConnectionString="DefaultEndpointsProtocol=https;AccountName=data1;AccountKey=SOMETHING1" />
<Provider Type="Orleans.Storage.AzureBlobStorage" Name="store2"
DataConnectionString="DefaultEndpointsProtocol=https;AccountName=data2;AccountKey=SOMETHING2" />
</StorageProviders>

4,配置存储提供程序

①AzureTableStorage

<Provider Type="Orleans.Storage.AzureTableStorage" Name="TableStore"
DataConnectionString="UseDevelopmentStorage=true" />

以下属性可以添加到<Provider />元素来配置提供程序:

  • DataConnectionString="..."(必需) - 要使用的Azure存储连接字符串
  • TableName="OrleansGrainState" (可选) - 表格存储中使用的表格名称,默认为OrleansGrainState
  • DeleteStateOnClear="false" (可选) - 如果为true,则在grain状态被清除时记录将被删除,否则将写入空记录,默认为false
  • UseJsonFormat="false" (可选) - 如果为true,则使用json序列化程序,否则将使用Orleans二进制序列化程序,默认为false
  • UseFullAssemblyNames="false"(可选) - (如果UseJsonFormat =“true”)序列化具有完整程序集名称(true)或简单(false)的类型,默认为false
  • IndentJSON="false"(可选) - (如果UseJsonFormat =“true”)缩进序列化的json,默认为false

注意:状态不应超过64KB,由表存储限制。

②AzureBlobStorage

<Provider Type="Orleans.Storage.AzureTableStorage" Name="BlobStore"
DataConnectionString="UseDevelopmentStorage=true" />

以下属性可以添加到<Provider />元素来配置提供程序:

  • DataConnectionString="..." (必需) - 要使用的Azure存储连接字符串
  • ContainerName="grainstate" (可选) - 要使用的Blob存储容器,默认为grainstate
  • UseFullAssemblyNames="false" (可选) - 使用完整程序集名称(true)或简单(false)序列化类型,默认为false
  • IndentJSON="false" (可选) - 缩进序列化的json,默认为false

③DynamoDBStorageProvider

<Provider Type="Orleans.Storage.DynamoDBStorageProvider" Name="DDBStore"
DataConnectionString="Service=us-wes-1;AccessKey=MY_ACCESS_KEY;SecretKey=MY_SECRET_KEY;" />
  • DataConnectionString="..."(必需) - 要使用的DynamoDB存储连接字符串。 您可以在其中设置Service,AccessKey,SecretKey,ReadCapacityUnits和WriteCapacityUnits。
  • TableName="OrleansGrainState" (可选) - 表格存储中使用的表格名称,默认为OrleansGrainState
  • DeleteStateOnClear="false" (可选) - 如果为true,则在grain状态被清除时记录将被删除,否则将写入空记录,默认为false
  • UseJsonFormat="false" (可选) - 如果为true,则使用json序列化程序,否则将使用Orleans二进制序列化程序,默认为false
  • UseFullAssemblyNames="false" (可选) - (如果UseJsonFormat =“true”)序列化具有完整程序集名称(true)或简单(false)的类型,默认为false
  • IndentJSON="false" (可选) - (如果UseJsonFormat =“true”)缩进序列化的json,默认为false

④ADO.NET Storage Provider (SQL Storage Provider)

ADO .NET存储提供程序允许您在关系数据库中存储grain状态。 目前支持以下数据库:

  • SQL Server
  • MySQL/MariaDB
  • PostgreSQL
  • Oracle

首先,安装基础包: Install-Package Microsoft.Orleans.OrleansSqlUtils

在与您的项目一起安装软件包的文件夹下,可以找到支持数据库供应商的不同SQL脚本。 你也可以从OrleansSQLUtils仓库获取它们。 创建一个数据库,然后运行适当的脚本来创建表。

接下来的步骤是安装第二个NuGet软件包(请参阅下表),并根据需要安装数据库供应商,并以编程方式或通过XML配置来配置存储提供程序。

Database Script NuGet Package AdoInvariant Remarks
SQL Server CreateOrleansTables_SQLServer.sql System.Data.SqlClient System.Data.SqlClient  
MySQL / MariaDB CreateOrleansTables_MySQL.sql MySql.Data MySql.Data.MySqlClient  
PostgreSQL CreateOrleansTables_PostgreSQL.sql Npgsql Npgsql  
Oracle CreateOrleansTables_Oracle.sql ODP.net Oracle.DataAccess.Client No .net Core support

以下是如何使用XML配置来配置ADO .NET存储提供程序的示例:

<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<StorageProviders>
<Provider Type="Orleans.Storage.AdoNetStorageProvider"
Name="OrleansStorage"
AdoInvariant="<AdoInvariant>"
DataConnectionString="<ConnectionString>"
UseJsonFormat="true" />
</StorageProviders>
</Globals>
</OrleansConfiguration>

在代码中,你需要像下面这样的东西:

var properties = new Dictionary<string, string>()
{
["AdoInvariant"] = "<AdoInvariant>",
["DataConnectionString"] = "<ConnectionString>",
["UseJsonFormat"] = "true"
}; config.Globals.RegisterStorageProvider<AdoNetStorageProvider>("OrleansStorage", properties);

本质上,您只需要设置数据库供应商特定的连接字符串和标识供应商的AdoInvariant(参见上表)。 您也可以选择保存数据的格式,可以是二进制(默认),JSON或XML。 虽然二进制是最紧凑的选项,但它是不透明的,你将无法读取或处理数据。 JSON是推荐的选项。

您可以设置以下属性:

Name Type Description
Name String 持久性Grain将用于引用这个存储提供程序的任意名称
Type String 设置为 Orleans.Storage.AdoNetStorageProvider
AdoInvariant String 标识数据库供应商(请参阅上表中的值;默认是System.Data.SqlClient)
DataConnectionString String 供应商特定的数据库连接字符串(必需)
UseJsonFormat Boolean 使用JSON格式(推荐)
UseXmlFormat Boolean 使用XML格式
UseBinaryFormat Boolean 使用紧凑的二进制格式(默认)

StorageProviders示例提供了一些代码,您可以使用它们来快速测试以上内容,并展示一些自定义存储提供程序。 在软件包管理器控制台中使用以下命令将所有的Orleans软件包更新到最新版本:

Get-Package | where Id -like 'Microsoft.Orleans.*' | foreach { update-package $_.Id }

ADO.NET持久性具有对数据进行版本化的功能,并可以使用任意应用程序规则和流来定义任意(de)序列化程序,但目前没有办法将它们公开给应用程序代码。

⑤MemoryStorage

MemoryStorage是一个简单的存储提供者,它并不真正使用下面的持久数据存储。 了解如何快速与存储提供商合作很方便,但不打算在实际情况下使用。

注意:这个提供者将状态持久化到不稳定的内存中,该内存在仓储关闭时被删除。只使用进行测试。

使用XML配置设置内存存储提供程序:

<?xml version="1.0" encoding="utf-8"?>
<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<StorageProviders>
<Provider Type="Orleans.Storage.MemoryStorage"
Name="OrleansStorage"
NumStorageGrains="10" />
</StorageProviders>
</Globals>
</OrleansConfiguration>

要在代码中设置它:

siloHost.Config.Globals.RegisterStorageProvider<MemoryStorage>("OrleansStorage");

您可以设置以下属性:

Name Type Description
Name String 持久性Grain将用于引用这个存储提供程序的任意名称
Type String 设置为 Orleans.Storage.MemoryStorage
NumStorageGrains Integer 用来存储状态的Grain数量,默认为10

⑥ShardedStorageProvider

<Provider Type="Orleans.Storage.ShardedStorageProvider" Name="ShardedStorage">
<Provider />
<Provider />
<Provider />
</Provider>

简单的存储提供程序,用于编写多个其他存储提供程序共享的Grain状态数据。

一致的散列函数(默认是Jenkins Hash)用于决定哪个碎片(按照它们在配置文件中定义的顺序)负责存储指定Grain的状态数据,然后将读/写/清除请求桥接 到合适的底层提供者执行。

⑦存储提供者的注意事项

如果没有为Grain <T> grain类指定[StorageProvider]属性,则会搜索名为Default的提供程序。 如果没有找到,则将其视为缺少的存储提供者。

如果在仓储配置文件中只有一个提供者,它将被视为这个仓储的默认提供者。

使用存储提供程序的Grain(Grain装载时不存在并定义在Grain配置中)将无法加载,但是仓储里的其他Grain仍然可以装载和运行。Orleans之后的任何一种Grain类型的调用都将失败。指定未加载Grain类型的Storage.BadProviderConfigException错误。

用于给定Grain类型的存储提供程序实例由该Grain类型的[StorageProvider]属性中定义的存储提供程序名称,加上仓储配置中定义的提供者的提供者类型和配置选项。

不同的Grain类型可以使用不同的配置存储提供程序,即使它们是相同的类型:例如,两个不同的Azure表存储提供程序实例连接到不同的Azure存储帐户(请参阅上面的配置文件示例)。

存储提供程序的所有配置详细信息是在仓储启动时读取的仓储配置中静态定义的。 目前没有提供机制来动态更新或更改仓储所使用的存储提供商列表。 但是,这是一个优先/工作量约束,而不是一个基本的设计约束。

5,状态存储API

对于Grain 状态/持久性api有两个主要部分:grainto - runtime和runtimeto - storage - provider。

6,Grain状态存储API

Orleans Runtime中的Grain状态存储功能将提供读写操作,以自动填充/保存该Grain的GrainState数据对象。 在内部,这些功能将被连接到通过配置为该Grain适当持久性提供(由Orleans 客户端根工具生成的代码中)。

7,Grain状态读/写功能

当Grain被激活时,Grain状态将自动被读取,但是Grain负责明确地触发任何改变的Grain状态的写入。 有关错误处理机制的详细信息,请参见下面的失败模式部分。

在为该激活调用OnActivateAsync()方法之前,将自动读取GrainState(使用base.ReadStateAsync()的等效项)。 在任何方法调用Grain之前,Grain状态不会被刷新,除非这个Grain被激活了。

在任何grain方法调用期间,grain可以请求Orleans运行时通过调用base.WriteStateAsync()将该激活的当前grain状态数据写入指定的存储提供者。 grain是负责执行明确写操作时,他们做出显著更新他们的状态数据。 最常见的是,grain方法将返回base.WriteStateAsync()任务作为从grain方法返回的最终结果Task,但不要求遵循此模式。 在任何grain方法之后,运行时不会自动更新存储的粮食状态。

在grain中的grain方法或定时器回调处理程序期间,grain可以通过调用base.ReadStateAsync()来请求Orleans运行时从指定的存储提供程序重新读取当前的grain状态数据。 这将使用从持久性存储中读取的最新值完全覆盖当前存储在Grain状态对象中的当前状态数据。

不透明的特定于提供者的Etag值(字符串)可能由存储提供程序设置,作为在读取状态时填充的Grain状态元数据的一部分。如果不使用Etags,一些提供者可能会选择将其保留为null。

从概念上讲,在任何写操作中,奥尔良运行时都将对grain状态数据对象进行深入的复制。在覆盖范围内,运行时可以使用优化规则和启发式来避免在某些情况下执行某些或全部的深度拷贝,前提是保留预期的逻辑隔离语义。

8,Grain状态读取/写入操作的示例代码

Grain必须扩展Grain< T >类,以参与Orleans Grain状态的持久性机制。以上定义中的T将被一种特定于应用的Grain状态类所取代;看下面的例子。

grain类还应该用一个[StorageProvider]属性进行注释,该属性告诉运行时哪个存储提供者(实例)与这种类型的Grain一起使用。

public class MyGrainState
{
public int Field1 { get; set; }
public string Field2 { get; set; }
} [StorageProvider(ProviderName="store1")]
public class MyPersistenceGrain : Grain<MyGrainState>, IMyPersistenceGrain
{
...
}

9,Grain状态读取

在Grain的OnActivateAsync()方法被调用之前,Grain状态的初始读取将由Orleans运行时自动发生;不需要应用程序代码来实现这一点,从那时起,Grain的状态将通过Grain<T>.State属性获取

10,Grain状态写入

在对grain的内存状态进行任何适当的更改之后,grain应该调用base.WriteStateAsync()方法通过定义的存储提供程序将这些更改写入到持久存储中。 此方法是异步的,并返回一个通常由grain方法作为其自己的完成任务返回的Task。

public Task DoWrite(int val)
{
State.Field1 = val;
return base.WriteStateAsync();
}

11, Grain状态刷新

如果Grain希望明确地从后备存储中重新读取这个Grain的最新状态,Grain应该调用base.ReadStateAsync()方法。这将从持久性存储中重新加载Grain状态,通过为这种Grain类型定义的存储提供程序,并且在ReadStateAsync()任务完成时,将覆盖任何先前的Grain状态的内存副本。

public async Task<int> DoRead()
{
await base.ReadStateAsync();
return State.Field1;
}

12,Grain状态持久性操作的失败模式

①Grain状态读取操作的失败模式

存储提供者在初始读取该特定Grain的状态数据期间返回的故障将导致该Grain的激活操作失败;在这种情况下,将不会有任何的调用

OnActivateAsync()生命周期回调方法。对引起激活的Grain的原始请求将会被错误地反馈给调用者,就像在Grain激活过程中其他的故障一样。存储提供程序遇到的错误读取特定Grain的状态数据将导致ReadStateAsync()任务出错。就像Orleans的其他任务一样,Grain可以选择处理或忽略这一断层任务

任何试图发送一个消息到未能在筒仓中加载的Grain会抛Orleans.BadProviderConfigException错误。

②Grain状态写入操作的失败模式

存储提供程序遇到写入特定Grain的状态数据时遇到的故障将导致WriteStateAsync()任务出现故障。 通常情况下,如果WriteStateAsync()任务被正确链接到这个grain方法的最终返回Task中,这将意味着grain调用会返回给客户端调用者。 但是,某些高级方案可能会编写grain代码来专门处理这种写入错误,就像他们可以处理任何其他故障的Task一样。

执行错误处理/恢复代码的Grain必须捕获异常/故障的WriteStateAsync()任务,而不是重新抛出以表示他们已成功处理写入错误。

13,存储提供商框架

有一个服务提供者API用于编写额外的持久性提供者 - IStorageProvider。

持久性提供程序API涵盖GrainState数据的读取和写入操作。

public interface IStorageProvider
{
Logger Log { get; }
Task Init();
Task Close(); Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState);
Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState);
}

14,存储提供程序语义

当存储提供程序检测到Etag约束违规时,任何尝试执行写入操作都应该导致写入任务出现瞬态错误Orleans.InconsistentStateException,并封装基础存储异常。

public class InconsistentStateException : AggregateException
{
/// <summary>Etag值当前持久存储。</summary>
public string StoredEtag { get; private set; }
/// <summary>Etag值目前保存在内存中,并试图更新。</summary>
public string CurrentEtag { get; private set; } public InconsistentStateException(
string errorMsg,
string storedEtag,
string currentEtag,
Exception storageException
) : base(errorMsg, storageException)
{
this.StoredEtag = storedEtag;
this.CurrentEtag = currentEtag;
} public InconsistentStateException(string storedEtag, string currentEtag, Exception storageException)
: this(storageException.Message, storedEtag, currentEtag, storageException)
{ }
}

来自写入操作的任何其他故障情况都应该导致写入任务被破坏,并且包含基础存储异常的异常。

15,数据映射

单独的存储提供商应该决定如何最好地存储Grain状态blob(各种格式/序列化的形式)或列每场是明显的选择。

Azure Table的基本存储提供程序使用Orleans二进制序列化将状态数据字段编码为单个表列。

16,ADO.NET持久性原理

ADO.NET支持的持久性存储的原则是:

  1. 在数据,数据和代码格式不断发展的同时,保持业务关键型数据的安全。
  2. 利用供应商和存储特定的功能。

实际上,这意味着坚持ADO.NET implementation goals,并在ADO.NET特定的存储提供程序中添加一些实现逻辑,允许演变存储中的数据形状。

除了通常的存储提供者功能外,ADO.NET提供者还具有内置的功能

  1. 在往返状态时将存储数据格式从一种格式更改为另一种格式(例如,从JSON格式转换为二进制格式)。
  2. 以任意方式对存储的类型进行整形或从存储中读取。 这有助于演变版本状态。
  3. 将数据流出数据库。

两个1和2。可用于任意选择参数,如Grain ID、Grain Type、payload data等。

发生这种情况以便人们选择一种格式,例如 简单的二进制编码(SBE)并实现IStorageDeserializer和IStorageSerializer。 内置的(de)序列化器是使用这种方法构建的。 OrleansStorageDefault(De)序列化程序可以用作如何实现其他格式的示例。

当(de)序列化器已经实现时,他们需要将ba添加到AdoNetStorageProvider的StorageSerializationPicker属性中。 这是IStorageSerializationPicker的一个实现。 默认情况下将使用StorageSerializationPicker。 在RelationalStorageTests中可以看到更改数据存储格式或使用(de)序列化器的示例。

目前没有任何方法可以将这个信息暴露给Orleans应用程序,因为没有方法可以访问创建的AdoNetStorageProvider实例的框架。

二、定时器和提醒

Orleans运行时提供了两种称为定时器和提醒的机制,使开发人员能够指定谷物的周期性行为。

1,定时器

①描述

定时器用于创建不需要跨越多个激活(Grain实例化)的周期性Grain行为。 它与标准的.NET System.Threading.Timer类基本相同。 另外,它在运行的Grain激活内受单线程执行保证。

每个激活可能有零个或多个与之关联的定时器。 运行时在与其关联的激活的运行时上下文中执行每个定时器例程。

②用法
要启动计时器,请使用Grain.RegisterTimer方法,该方法返回一个IDisposable引用:

protected IDisposable RegisterTimer(Func<object, Task> asyncCallback, object state, TimeSpan dueTime, TimeSpan period)
  • asyncCallback是当计时器计时时调用的函数。
  • state是一个对象,当计时器计时时,它将被传递给asyncCallback。
  • dueTime指定在发出第一个计时器之前等待的时间量。
  • period 指定计时器的周期。
    取消定时器。
    如果激活被停用或发生故障或发生故障,计时器将停止触发。
    重要的注意事项

  • 启用激活集合时,计时器回调的执行不会将激活状态从空闲状态更改为使用。这意味着计时器不能用于延迟其他空闲激活的停用。

  • 传递给Grain.RegisterTimer的时间是,从asyncCallback返回的任务到下一次调用asyncCallback时所经过的时间。这不仅使连续调用asyncCallback无法重叠,还使得异步回调的时间长度影响到异步调用的频率。这与system.thread.timer的语义有很大的偏差。
  • 每次调用asyncCallback都会在一个单独的回合中传递给一个激活,并且永远不会与其他回合在同一个激活中同时运行。 但是,请注意,asyncCallback调用不作为消息传递,因此不受消息交错语义限制。 这意味着asyncCallback的调用应该被认为是像对其他消息运行在一个可重入的Grain上一样。

2,提醒

①描述

提醒与定时器类似,但有一些重要的区别:

  • 除非明确取消,否则提醒将保持不变,并会在所有情况下继续触发(包括部分或全部群集重新启动)。
  • 提醒与Grain有关,而不是任何特定的激活。
  • 如果Grain有没有与之相关的激活和提醒蜱,一个将被创建。例如:如果激活变为空闲并且被停用,则与相同Grain相关联的提醒将在接下来ticks时重新激活Grain。
  • 提醒是通过消息传递的,并且与所有其他的grain方法一样,都受到相同的交错语义的影响。
  • 提醒不应该用于高频计时器——它们的周期应该以分钟、小时或天数来衡量。

②配置

提醒,持久,依赖存储功能。在提醒子系统将运行之前,您必须指定使用哪种存储支持。提醒功能由服务器端配置中的SystemStore元素控制。它使用Azure表或SQL Server作为存储。

<SystemStore SystemStoreType="AzureTable" /> OR
<SystemStore SystemStoreType="SqlServer" />

如果您只是想要一个占位符实现的提醒,而不需要设置一个Azure帐户或SQL数据库,然后将这个元素添加到配置文件(在“Globals”下)将会给你一个提醒系统的开发实现:

<ReminderService ReminderServiceType="ReminderTableGrain"/>

③使用

使用提醒Grain必须实现IRemindable.RecieveReminder方法。

Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
Console.WriteLine("感谢提醒我 - 我差点忘了!");
return TaskDone.Done;
}

要开始提醒,请使用Grain.RegisterOrUpdateReminder方法,该方法返回一个IOrleansReminder对象:

protected Task<IOrleansReminder> RegisterOrUpdateReminder(string reminderName, TimeSpan dueTime, TimeSpan period)
  • reminderName是一个字符串,必须在上下文的范围内唯一标识提醒。
  • dueTime指定在发出第一个计时器tick之前等待的时间量。
  • period指定定时器的周期。

由于提醒在任何一次激活的生命周期中都能够存活,因此必须明确取消(而不是被处置)。 您可以通过调用Grain.UnregisterReminder来取消提醒:

protected Task UnregisterReminder(IOrleansReminder reminder)

提醒是由Grain.RegisterOrUpdateReminder返回的处理对象。

IOrleansReminder的实例不能保证超过激活的有效期。 如果您希望以某种方式识别提醒,请使用包含提醒名称的字符串。

如果您只有提醒的名称并需要IOrleansReminder的相应实例,请调用Grain.GetReminder方法:

protected Task<IOrleansReminder> GetReminder(string reminderName)

3,我应该使用哪个?

我们建议您在以下情况下使用计时器:

  • 如果激活被停用或发生故障,则定时器停止工作并不重要(或不期望)。
  • 如果计时器的频率很小(例如几秒钟或几分钟)
  • 定时器回调可以从Grain.OnActivateAsync启动,或者在调用grain方法时启动。

我们建议您在以下情况下使用提醒:

  • 当周期性行为需要经受激活和任何失败。
  • 执行不频繁的任务(例如在几分钟,几小时或几天内合理表达)。

结合计时器和提醒您可以考虑使用提醒和定时器的组合来实现您的目标。 例如,如果您需要一个需要在激活状态下保留的小频率的计时器,则可以使用每5分钟运行一次的提醒,其目的是唤醒一个可重新启动本地计时器的计时器,该计时器可能由于 停用。

三、依赖注入

1,什么是依赖注入

依赖注入(DI)是一种软件设计模式,它实现了解决依赖关系的控制反转。

Orleans正在使用由ASP.NET Core开发人员编写的抽象概念。

2,Orleans中的DI

目前仅在Orleans的服务器端支持依赖注入。

Orleans可以将依赖关系注入到Grains应用程序中。

然而Orleans支持每个容器依赖的注入机制,其中最常用的方法是构造器注入。

理论上,任何类型都可以在Silo启动期间注册在IServiceCollection中。

注:由于新Orleans是不断发展的,因为目前的计划,将有可能利用其他应用类的依赖注入,以及像StreamProviders。

3,配置DI

DI配置是一个全局配置值,必须在此配置。

Orleans使用与ASP.NET Core类似的方法来配置DI。 您的应用程序中必须包含一个启动类,其中必须包含ConfigureServices方法。 它必须返回一个类型为IServiceProvider的对象实例。

通过下面介绍的方法之一来指定启动类的类型来完成配置。

注意:以前的DI配置是在集群节点级别指定的,在最近的版本中进行了更改。

4,从代码配置
可以通过基于代码的配置告诉Orleans您喜欢使用什么Startup类型。。 在ClusterConfiguration类中有一个名为UseStartup的扩展方法,您可以使用它进行此操作。

var configuration = new ClusterConfiguration();
configuration.UseStartupType<MyApplication.Configuration.MyStartup>();

5,通过XML配置
要使用Orleans注册Startup类,必须将Startup元素添加到Defaults部分,并在Type属性中指定该类型的程序集限定名称。

<?xml version="1.0" encoding="utf-8" ?>
<tns:OrleansConfiguration xmlns:tns="urn:orleans">
<tns:Defaults>
<tns:Startup Type="MyApplication.Configuration.Startup,MyApplication" />
</tns:Defaults>
</tns:OrleansConfiguration>

6,例子
这是一个完整的Startup类示例:

namespace MyApplication.Configuration
{
public class MyStartup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IInjectedService, InjectedService>(); return services.BuildServiceProvider();
}
}
}

这个例子显示了Grain如何通过构造函数注入来利用IInjectedService,以及注入服务的完整声明和实现:

public interface ISimpleDIGrain : IGrainWithIntegerKey
{
Task<long> GetTicksFromService();
} public class SimpleDIGrain : Grain, ISimpleDIGrain
{
private readonly IInjectedService injectedService; public SimpleDIGrain(IInjectedService injectedService)
{
this.injectedService = injectedService;
} public Task<long> GetTicksFromService()
{
return injectedService.GetTicks();
}
} public interface IInjectedService
{
Task<long> GetTicks();
} public class InjectedService : IInjectedService
{
public Task<long> GetTicks()
{
return Task.FromResult(DateTime.UtcNow.Ticks);
}
}

7,测试框架集成
与真正的测试框架结合在一起验证代码的正确性。

你需要做两件事,设置DI和测试。首先,您需要实现服务的模拟。这是在我们的例子中使用Moq来完成的,Moq是.net的一个流行的mocking框架。这里有一个对服务进行mocking的例子。

public class MockServices
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var mockInjectedService = new Mock<IInjectedService>(); mockInjectedService.Setup(t => t.GetTicks()).Returns(knownDateTime);
services.AddSingleton<IInjectedService>(mockInjectedService.Object);
return services.BuildServiceProvider();
}
}

要将这些服务包含在您的测试仓库中,您需要指定MockServices作为仓储启动类。 这是做这个的一个例子。

[TestClass]
public class IInjectedServiceTests: TestingSiloHost
{
private static TestingSiloHost host; [TestInitialize]
public void Setup()
{
if (host == null)
{
host = new TestingSiloHost(
new TestingSiloOptions
{
StartSecondary = false,
AdjustConfig = clusterConfig =>
{
clusterConfig.UseStartupType<MockServices>();
}
});
}
}
}

四、观察者

有些情况下,简单的消息/响应模式不够,客户端需要接收异步通知。 例如,用户可能希望在朋友发布新的即时消息时得到通知。

客户端观察者是一种允许异步通知客户端的机制。 观察者是从IGrainObserver继承的单向异步接口,它的所有方法必须是无效的。 Grain通过调用Grain接口方法来发送通知给观察者,除了它没有返回值,Grain不需要依赖结果。 Orleans运行时将确保通知的单向传递。 发布此类通知的Grain应提供API以添加或删除观察者。 另外,公开允许取消现有订阅的方法通常是方便的。 Grain开发人员可以使用 Orleans ObserverSubscriptionManager <T>泛型类来简化观察到的Grain类型的开发。

要订阅通知,客户端必须首先创建一个实现观察者接口的本地C#对象。 然后调用观察者工厂的一个静态方法CreateObjectReference(),将C#对象变成一个Grain引用,然后可以将它传递给通知Grain上的订阅方法。

其他Grain也可以使用此模型来接收异步通知。 与客户端订阅的情况不同,订阅的grain只是将观察者接口作为一个方面来实现,并传入一个对自身的引用(例如this.AsReference <IMyGrainObserverInterface>)。

1,代码示例
我们假设我们有一个周期性地向客户发送消息的Grain。 为了简单起见,我们示例中的消息将是一个字符串。 我们首先定义将接收消息的客户端上的接口。

界面将如下所示:

public interface IChat : IGrainObserver
{
void ReceiveMessage(string message);
}

唯一特别的是,该接口应该从IGrainObserver继承。 现在任何想要观察这些消息的客户端都应该实现一个实现IChat的类。

最简单的情况是这样的:

public class Chat : IChat
{
public void ReceiveMessage(string message)
{
Console.WriteLine(message);
}
}

现在在服务器上,我们应该有一个Grain发送这些聊天消息到客户端。 Grain也应该有一个机制,客户订阅和退订自己接收通知。 对于订阅,Grain可以使用实用工具类ObserverSubscriptionManager:

class HelloGrain : Grain, IHello
{
private ObserverSubscriptionManager<IChat> _subsManager; public override async Task OnActivateAsync()
{
// 我们在激活时创建了这个工具。
_subsManager = new ObserverSubscriptionManager<IChat>();
await base.OnActivateAsync();
} //客户端调用此订阅
public Task Subscribe(IChat observer)
{
_subsManager.Subscribe(observer);
return TaskDone.Done;
} //此外,客户端使用这个取消订阅自己不再接收消息。
public Task UnSubscribe(IChat observer)
{
_subsManager.Unsubscribe(observer);
return TaskDone.Done;
}
}

要将消息发送到客户端,可以使用ObserverSubscriptionManager <IChat>实例的通知方法。 该方法采用Action <T>方法或lambda表达式(其中T是IChat类型)。 您可以调用接口上的任何方法将其发送给客户端。 在我们的例子中,我们只有一个方法ReceiveMessage,我们在服务器上的发送代码如下所示:

public Task SendUpdateMessage(string message)
{
_subsManager.Notify(s => s.ReceiveMessage(message));
return TaskDone.Done;
}

现在我们的服务器有一个向观察者客户端发送消息的方法,订阅/取消订阅的方法有两种,客户端实现了一个类来观察这些消息。 最后一步是使用我们以前实现的Chat类在客户端上创建一个观察者引用,并在订阅它之后让它接收消息。

代码看起来像这样:

//首先创建Grain引用
var friend = GrainClient.GrainFactory.GetGrain<IHello>();
Chat c = new Chat(); //创建可用于订阅observable grain的引用。
var obj = await GrainClient.GrainFactory.CreateObjectReference<IChat>(c);
//订阅该实例以接收消息。
await friend.Subscribe(obj);

现在,当服务器上的Grain调用SendUpdateMessage方法时,所有订阅的客户端都将收到消息。 在我们的客户端代码中,变量c中的Chat实例将接收消息并将其输出到控制台。

注意:传递给CreateObjectReference的对象通过WeakReference <T>被保存,因此如果不存在其他引用,将被垃圾回收。 用户应该为每个观察者保留一个他们不想被收集的参考。

注意:观察者本质上是不可靠的,因为您没有得到任何回应,知道是否由于分布式系统中可能出现的任何情况而收到并处理了消息或者仅仅是失败了。 因为你的观察者应该定期轮询Grain,或者使用其他机制来确保他们收到了他们应该收到的所有信息。在某些情况下,你可以损失一些信息,你不需要任何额外的机制,但是如果你需要确保所有的观察者都接收到这些信息并且接收所有的信息,定期的重新订阅和轮询观察Grain,可以帮助确保最终处理所有消息。

五、无状态工作者Grains

默认情况下,Orleans运行时只会在集群内创建一个Grain的激活。 这是虚拟主角模型的最直观的表达方式,每个Grain对应于具有唯一类型/标识的实体。 但是,也有一些情况是应用程序需要执行功能无状态的操作,而这些操作不会绑定到系统中的特定实体。 例如,如果客户端发送带有压缩有效载荷的请求,并在它们能够被路由到目标Grain进行处理之前需要被解压缩,则这样的解压缩/路由逻辑不绑定到应用中的特定实体,并且可以容易地向外扩展。

当[StatelessWorker]特性应用于Grain类时,它向Orleans运行时指示该类的Grain应被视为无状态工作者Grain。 无状态工作者Grain具有以下特性,使其执行与普通Grain类别的执行有很大不同。

  1. Orleans运行时可以并且将在集群的不同仓储上创建无状态工作者Grain的多个激活。
  2. 对无状态工作者Grain的请求总是在当地执行,也就是在请求发起的同一个仓储里,要么由Grain运行,要么由仓储的客户端网关接收。 因此,从其他Grain或客户网关调用无状态工作者Grain从不会产生远程信息。
  3. Orleans运行时自动创建一个无状态工作者Grain额外的激活,如果现有的忙。 除非可选的maxLocalWorkers参数明确指定,否则运行时创建的无状态工作器的最大激活次数默认受机器上CPU内核数量的限制。
  4. 由于2和3,无状态工作者Grain激活并不是单个可寻址的。对无状态工作者Grain的两个后续请求可以通过不同的激活来处理。

无状态工作者Grain提供了一个直接的方式,创建一个自动管理的Grain激活池,根据实际负载自动扩展和缩减。运行时总是以相同的顺序扫描可用的无状态工作者Grain激活。因此,它总是将请求发送到它可以找到的第一个空闲本地激活,并且如果以前的所有激活都忙,则只能到最后一个激活。如果所有的激活都很忙,并且没有达到激活限制,它会在列表的末尾再创建一个激活,并将请求发送给它。这意味着,当对无状态工作者Grain需求量增加,而且现有的激活当前都很忙时,运行时将其激活池扩大到极限。相反,当负载下降,并且可以通过少量无状态工作者Grain的激活来处理时,在列表尾部的激活将不会被发送到他们的请求。他们将变得闲置,并最终被标准的激活收集过程停用。因此,激活池将最终缩小以匹配负载。

以下示例使用默认的最大激活数限制定义无状态工作者谷物类MyStatelessWorkerGrain。

[StatelessWorker]
public class MyStatelessWorkerGrain : Grain, IMyStatelessWorkerGrain
{
...
}

调用无状态工作者Grain和其他Grain一样。 唯一的区别是,在大多数情况下,使用一个单一的GrainID,0或Guid.Empty。 具有多个无状态工作者Grain池时,可以使用多个GrainID,每个ID需要一个Grain池。

var worker = GrainFactory.GetGrain<IMyStatelessWorkerGrain>();
await worker.Process(args);

这个定义了一个无状态的工作者Grain类每个仓储不超过一个Grain激活。

[StatelessWorker()] // max 1 activation per silo
public class MyLonelyWorkerGrain : ILonelyWorkerGrain
{
...
}

请注意,[StatelessWorker]属性不会改变目标Grain类的重入性。 就像任何其他Grain一样,无状态工作者Grain默认情况下是不可重入的。 可以通过向Grain类添加一个[Reentrant]属性来明确地重新定位它们。

可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享

1,State

“Stateless”部分的“Stateless Worker”部分并不意味着无状态的工作者不能拥有状态,并且仅限于执行功能操作。和其他Grain一样,无状态的工人Grain可以装载并保存它需要的任何状态。这只是因为在同一个和不同的集群上创建了一个无状态的工作者Grain的多个激活,没有简单的机制来协调不同激活状态所持有的状态。

涉及Stateless Worker 有几种有用的模式。

①向外扩展热缓存项
对于经历高吞吐量的热缓存项目,将每个这样的项目保持在无状态工作者Grain中使得a)在仓储中并在群集中的所有仓储中自动扩展; b)通过客户端网关在收到客户端请求的仓储上使数据始终本地可用,这样就可以在不需要额外网络跳转到另一个仓储的情况下应答请求。

②减少样式聚合

在某些场景中,应用程序需要计算集群中特定类型的所有Grains的特定指标, 并定期报告聚集体。 举例来说,每个游戏地图的玩家数量,VoIP呼叫的平均持续时间, 等等,如果成千上万的Grain中的每一个都将它们的指标报告给一个单一的全局聚合器,那么聚合器就会立即过载,无法处理大量的报告。另一种方法是将此任务转换成一个2(或更多)步骤,减少样式聚合。第一层的聚合是通过向无状态的工作者预聚集的Gtrain发送他们的指标。Orleans运行时将自动为每个仓储创建无状态工作者Grain的多个激活。由于所有这些调用都将在本地处理,不需要远程调用或序列化消息,因此此类聚合的成本将显著低于远程情况。现在,每个预聚集无状态的工作者Grain激活,独立地或与其他本地激活进行协调,可以在不超载的情况下将聚合报告发送到全局最终聚合器(或在必要时再进行另一个还原层)。

六、流

1,介绍

1)Orleans Streams
Orleansv.1.0.0增加了对编程模型的流式扩展的支持。 流式扩展提供了一系列抽象和API,使工作流更简单、更健壮。 流式扩展允许开发人员编写响应式应用程序,以结构化的方式对一系列事件进行操作。 流提供程序的可扩展性模型使得编程模型与大量现有排队技术(如Event HubsServiceBusAzure QueuesApache Kafka.)兼容并可移植。 不需要编写特殊的代码或运行专门的进程来与这样的队列进行交互。

2)我为什么要关心?

如果你已经知道了 Stream Processing 和熟悉各种技术 Event HubsKafkaAzure Stream AnalyticsApache StormApache Spark Streaming, 和Reactive Extensions (Rx) in .NET, 你可能会问,你为什么要关心这个。为什么我们需要另一个流处理系统,以及Actors如何与流相关? "Why Orleans Streams?" 是用来回答这个问题的。

3)编程模型
Orleans流编程模型背后有许多原理。

  1. 遵循Orleans的虚拟Actors,Orleans流是虚拟的。也就是说,流总是存在。它没有被显式地创建或销毁,它永远不会失败。
  2. 流是由流id识别的,它们只是由GUIDs和字符串组成的逻辑名称。
  3. Orleans Streams允许从时间和空间的处理中分离数据的生成。 这意味着,流生产者和流消费者可能在不同的服务器上,在不同的时间,并会承受失败。
  4. Orleans流是轻量级和动态的。 Orleans Streaming Runtime旨在处理大量来来往往的高速流。
  5. Orleans流绑定是动态的。 Orleans Streaming Runtime旨在处理grain以高速连接和断开流的情况。
  6. Orleans Streaming Runtime透明地管理流消耗的生命周期。 应用程序订阅一个流之后,即使在出现故障的情况下,它也会收到流的事件。
  7. Orleans流在grain和Orleans的客户中工作一致。

4)编程API

应用程序通过与.NET中众所周知的Reactive Extensions(Rx)非常相似的API与流进行交互,通过使用Orleans.Streams.IAsyncStream <T>实现
Orleans.Streams.IAsyncObserver <T>和Orleans.Streams.IAsyncObservable <T>接口。

在下面的典型示例中,设备会生成一些数据,这些数据会作为HTTP请求发送到云中运行的服务。 在前端服务器上运行的Orleans客户端接收到这个HTTP调用并将数据发布到匹配的设备流中:

public async Task OnHttpCall(DeviceEvent deviceEvent)
{
// 将数据直接发布到设备的流中。
IStreamProvider streamProvider = GrainClient.GetStreamProvider("myStreamProvider");
IAsyncStream<DeviceEventData> deviceStream = streamProvider.GetStream<DeviceEventData>(deviceEvent.DeviceId);
await deviceStream.OnNextAsync(deviceEvent.Data);
}

在下面的另一个例子中,聊天用户(实现为Orleans Grain)加入聊天室,获取由该房间中所有其他用户生成的聊天消息流,并订阅该消息。 请注意,聊天用户既不需要知道聊天室Grain本身(我们的系统中可能没有这样的Grain),也不需要知道该群中产生消息的其他用户。 不用说,为了产生聊天流,用户不需要知道谁当前订阅了流。 这说明聊天用户如何在时间和空间上完全分离。

public class ChatUser: Grain
{
public async Task JoinChat(string chatGroupName)
{
IStreamProvider streamProvider = base.GetStreamProvider("myStreamProvider");
IAsyncStream<string> chatStream = streamProvider.GetStream<string>(chatGroupName);
await chatStream.SubscribeAsync((string chatEvent) => Console.Out.Write(chatEvent));
}
}

5)快速启动示例

快速入门示例是在应用程序中使用流的总体工作流程的快速概览。 阅读之后,您应该阅读Streams Programming API,以深入了解这些概念。

6)流编程API
流编程API提供了编程API的详细描述。

7)流提供者
流可以通过各种形状和形式的物理通道来实现,并且可以具有不同的语义。 Orleans Streaming旨在通过流提供程序的概念来支持这种多样性,这是系统中的一个可扩展点。 Orleans目前有两个流提供程序的实现:基于TCP的简单消息流提供程序和基于Azure队列的Azure队列流提供程序。 有关Steam提供商的更多详细信息可以在Stream Providers上找到。

8)流语义
流Subsription语义:Orleans流保证Stream Subsription操作的顺序一致性。 具体说就是,当消费者订阅一个流时,一旦代表该subsription操作的Task被成功解决,消费者就会看到订阅之后生成的所有事件。 另外,可重放的流允许通过使用StreamSequenceToken从任意时间点订阅。

单个流事件传送保证:单个事件传送保证取决于各个流提供者。 一些服务器只提供一次交付(例如简单消息流),而另一些则至少提供一次交付(例如Azure队列流)。 甚至有可能建立一个能够保证一次交付的流提供者(我们还没有这样的提供者,但是可以用可扩展性模型来构建一个提供者)。

事件传递顺序:事件顺序还取决于特定的流提供者。 在SMS流中,制作者通过控制发布它们的方式来控制消费者看到的事件的顺序。 Azure队列流不保证FIFO顺序,因为下层的Azure队列不能保证顺序失败。 应用程序还可以通过使用StreamSequenceToken来控制自己的流传送顺序。

10)流实现
Orleans Streams Implementation提供了内部实现的高层次概述。

11)流扩展性
Orleans Streams Extensibility介绍了如何使用新功能扩展流。

12)代码示例
在这里可以找到更多关于如何在谷物中使用流API的例子。 我们计划在未来创造更多样本。

2,流,快速启动

本指南将向您展示安装和使用Orleans Streams的快速方法。 要详细了解流式传输功能的详细信息,请阅读本文档的其他部分。

1)所需的配置
在本指南中,我们将使用基于简单消息的流,它使用谷物消息向订户发送流数据。 我们将使用内存存储提供商存储的订阅列表,所以它不是真正的生产应用明智的最佳的选择。

<Globals>
<StorageProviders>
<Provider Type="Orleans.Storage.MemoryStorage" Name="Default" />
<Provider Type="Orleans.Storage.MemoryStorage" Name="PubSubStore" />
</StorageProviders>
<StreamProviders>
<Provider Type="Orleans.Providers.Streams.SimpleMessageStream.SimpleMessageStreamProvider" Name="SMSProvider"/>
</StreamProviders>

现在我们可以创建流,使用它们作为生产者发送数据,也可以作为订户接收数据。

2)生产事件
为流生成事件相对容易。 您应该首先访问您在上面的配置(SMSProvider)中定义的流提供程序,然后选择一个流并将数据推送到它。

//选择一个聊天室grain和聊天室流guid
var guid = some guid identifying the chat room
//获取我们在配置中定义的提供者之一
var streamProvider = GetStreamProvider("SMSProvider");
//获取流的引用
var stream = streamProvider.GetStream<int>(guid, "RANDOMDATA");

正如你可以看到我们的流有一个GUID和一个命名空间。 这将使识别独特的流变得容易。 例如,在聊天室命名空间中,“Rooms”和GUID可以是拥有RoomGrain的GUID。

这里我们使用一些已知的聊天室的GUID。 现在使用流的OnNext方法,我们可以将数据推送到它。 让我们在一个计时器内使用随机数字。 您也可以使用任何其他数据类型的流。

RegisterTimer(s =>
{
return stream.OnNextAsync(new System.Random().Next());
}, null, TimeSpan.FromMilliseconds(), TimeSpan.FromMilliseconds());

3)订阅和接收流数据
为了接收数据,我们可以使用隐式/显式订阅,在手册的其他页面中对这些订阅进行了全面描述。 在这里,我们使用的是更容易隐士订阅。 当grain类型想要隐式地订阅一个流时,它使用ImplicitStreamSubscription (namespace)]。

对于我们的情况,我们将像这样定义一个ReceiverGrain:

[ImplicitStreamSubscription("RANDOMDATA")]
public class ReceiverGrain : Grain, IRandomReceiver

现在,无论何时将某些数据推送到名称空间RANDOMDATA的流中(如定时器中所示),具有相同流的GUID的ReceiverGrain类型的Grain将收到该消息。 即使目前不存在Grain的激活,运行时也会自动创建一个新消息并将消息发送给它。

为了使这个工作,我们需要通过设置我们的OnNext方法接收数据来完成订阅过程。 所以我们的ReceiverGrain应该调用OnActivateAsync这样的东西

//Create a GUID based on our GUID as a grain
var guid = this.GetPrimaryKey();
//获取我们在配置中定义的一个提供者
var streamProvider = GetStreamProvider("SMSProvider");
//获取对流的引用
var stream = streamProvider.GetStream<int>(guid, "RANDOMDATA");
//将我们的OnNext方法设置为lambda,它只输出数据,这不会产生新的订阅
await stream.SubscribeAsync<int>(async (data, token) => Console.WriteLine(data));

我们现在都准备好了 唯一的要求就是触发我们的生产者Grain的创建,然后它将注册计时器,并开始发送随机整数给所有订阅的各方。

再次,这个指南跳过了很多细节,只是为了展示大局。 阅读本手册的其他部分以及RX上的其他资源,以便了解可用的内容和方式。

反应式编程是解决许多问题的一个非常有效的方法。 你可以例如在用户中使用LINQ来过滤数字,并做各种有趣的东西。

3,为什么选择流

1)为什么选择Orleans Streams?

已经有很多技术可以让你建立流处理系统。 这些系统包括持久存储流数据的系统(例如,事件中心和Kafka)以及用于在流数据上表达计算操作的系统(例如,Azure流分析,Apache风暴和Apache Spark流)。 这些都是非常棒的系统,可以让您构建高效的数据流处理管道。

2)现有系统的局限性

然而,这些系统不适合fine-grained free-form compute over stream data。他的流计算系统首先提到,允许您指定一个统一的数据流图,以相同的方式应用于所有的流项目。当数据是一致的时候,这是一个强大的模型,并且您想要对这些数据表达相同的转换、过滤或聚合操作。但是也有其他的用例,您需要在不同的数据项上表达完全不同的操作。在其中一些过程中,作为处理的一部分,您偶尔需要进行外部调用,例如调用一些任意REST API。统一的数据流处理引擎要么不支持这些场景,要么以有限的、受限的方式支持它们,或者在支持它们方面效率低下。这是因为它们天生就针对大量类似的项目进行优化,并且通常在表达性和处理方面都很有限。Orleans流的目标是那些其他的情形。

3)动机
这一切都是从Orleans用户的请求开始的,他们支持从一个Grain方法调用返回一个项目序列。你可以想象,这只是冰山一角。实际上他们需要的远不止这些。

Orleans Streams的一个典型场景是当你有每个用户流,并且你想在每个用户的上下文中为每个用户执行不同的处理。 我们可能有数百万用户,但其中一些对天气感兴趣,可以订阅特定位置的天气警报,而有些则对体育赛事感兴趣; 有人正在跟踪某个航班的状态。 处理这些事件需要不同的逻辑,但是您不希望运行两个独立的流处理实例。 一些用户只对特定的库存感兴趣,并且只有在某些外部条件适用的情况下,条件可能不一定是流数据的一部分(因此需要在运行时作为处理的一部分在动态时检查)。

用户一直在改变他们的兴趣,因此他们对特定事件流的订阅动态地来来回回,因此流动拓扑结构动态而迅速地变化。 此外,根据用户状态和外部事件,每个用户的处理逻辑也会动态变化和变化。 外部事件可能会修改特定用户的处理逻辑。 例如,在游戏作弊检测系统中,当发现新的作弊方式时,处理逻辑需要根据新的规则进行更新,以检测出新的违规行为。 这当然需要在不中断正在进行的处理流程的情况下完成。 批量数据流流处理引擎不是为了支持这种情况而构建的。

毋庸置疑,这样的系统必须在多个联网的机器上运行,而不是在单个节点上运行。 因此,处理逻辑必须以可扩展和弹性的方式分布在一组服务器上。

4)新的要求
我们确定了我们的流处理系统的4个基本要求,这将允许它针对上述情况。

  1. 灵活的流处理逻辑
  2. 支持动态拓扑
  3. 细粒度的流粒度
  4. 分布

5)灵活的流处理逻辑
我们希望系统支持表达流处理逻辑的不同方式。 我们上面提到的现有系统要求开发人员编写一个声明式的数据流计算图,通常是遵循函数式编程风格。 这限制了处理逻辑的表达性和灵活性。 Orleans流对表达处理逻辑的方式漠不关心。 它可以表示为数据流(例如,通过在.NET中使用Reactive Extensions(Rx)); 作为功能程序; 作为声明性查询; 或在一般的命令逻辑。 逻辑可以是有状态或无状态的,可能有也可能不会有副作用,并且可以触发外部行为。 所有权力都交给开发者。

6)支持动态拓扑
我们希望系统允许动态演进的拓扑结构。 我们上面提到的现有系统通常仅限于在部署时固定并且不能在运行时发展的静态拓扑。 在下面的数据流表达式例子中,一切都很好,很简单,直到你需要改变它。

Stream.GroupBy(x=> x.key).Extract(x=>x.field).Select(x=>x+).AverageWindow(x, 5sec).Where(x=>x > 0.8) *

在Where过滤器中更改阈值条件,添加额外的Select语句或在数据流图中添加另一个分支并生成新的输出流。 在现有的系统中,如果不拆除整个拓扑并重新启动数据流,这是不可能的。 实际上,这些系统将检查现有的计算,并能够从最新的检查点重新启动。 但是,这样的重启对于实时产生结果的在线服务是破坏性的并且是昂贵的。 当我们谈论大量这样的以相似但不同的(每用户,每个设计等等)参数执行并且保持不断变化的表达式时,这样的重新启动变得特别不切实际。

我们希望系统允许在运行时演进流处理图,通过向计算图添加新的链接或节点,或通过改变计算节点内的处理逻辑。

7)细粒度的流粒度
在现有的系统中,抽象的最小单位通常是整个流程(拓扑)。但是,我们的许多目标场景要求拓扑中的单个节点/链路本身是一个逻辑实体。这样每个实体都可以独立管理。例如,在由多个链路组成的大流量拓扑中,不同链路可以具有不同的特性,并且可以在不同的物理传输上实现。一些链接可以通过TCP套接字,而另一些则通过可靠的队列。不同的链接可以有不同的交付保证。不同的节点可以有不同的检查点策略,其处理逻辑可以用不同的模型甚至不同的语言来表示。现有系统通常不具有这种灵活性。

抽象单位和灵活性的论点类似于SoA(面向服务的体系结构)与参与者的比较。演员系统允许更多的灵活性,因为每个人本质上是一个独立管理的“小服务”。同样,我们希望系统允许这样一个细粒度的控制。

8)分配
当然,我们的系统应该具有“良好的分布式系统”的所有特性。 包括:

  1. 可伸缩性 - 支持大量的流和计算元素。
  2. 弹性 - 允许添加/删除资源以根据负载进行增长/收缩。
  3. 可靠性 - 对故障具有恢复能力
  4. 效率 - 高效使用底层资源
  5. 响应能力 - 启用接近实时的情况。

说明:Orleans目前不直接支持编写声明式数据流表达式,如上例所示。 目前的Orleans流媒体API是更低层次的构建块,如下所述。 提供声明性数据流表达式是我们未来的目标。

4,流编程API

1)Orleans Streams编程API
应用程序通过与.NET中众所周知的反应式扩展(Rx)非常相似的API与流进行交互。 主要区别在于Orleans流扩展是异步的,以便在Orleans的分布式和可伸缩计算结构中提高处理效率。

2)异步流
应用程序启动使用流提供者得到一个处理流。 您可以在这里阅读关于流提供者的更多信息,但现在您可以将其视为流工厂,允许实现者自定义流行为和语义:

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

应用程序可以通过在Grain类中调用Grain类的GetStreamProvider方法,或者在客户端上调用GrainClient.GetStreamProvider()方法来获得对流提供者的引用。

Orleans.Streams.IAsyncStream <T>是虚拟流的逻辑强类型句柄。 它与Orleans Grain引用的相似。 调用GetStreamProvider和GetStream纯粹是本地的。 GetStream的参数是一个GUID和一个额外的字符串,我们称之为流名称空间(可以为空)。 GUID和名称空间字符串一起组成流标识(类似于GrainFactory.GetGrain的参数)。 GUID和名称空间字符串的组合为确定流标识提供了额外的灵活性。 就像Grain类型PlayerGrain中可能存在Grain 7,并且Grain类型ChatRoomGrain内可能存在不同Grain 7,则流123可以与流名称空间PlayerEventsStream一起存在,并且流名称空间ChatRoomMessagesStream内可以存在不同的流123。

3)生产和消费
IAsyncStream <T>实现了Orleans.Streams.IAsyncObserver <T>和Orleans.Streams.IAsyncObservable <T>接口。 这样,应用程序就可以使用这个流来使用Orleans.Streams.IAsyncObserver <T>来产生新的事件到流中,或者通过使用Orleans.Streams.IAsyncObservable <T>来订阅和使用来自流的事件。

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

为了在流中生成事件,应用程序只需要调用

await stream.OnNextAsync<T>(event)

要订阅一个流,应用程序调用

StreamSubscriptionHandle<T> subscriptionHandle = await stream.SubscribeAsync(IAsyncObserver)

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

await subscriptionHandle.UnsubscribeAsync()

需要注意的是订阅是一个Grain,而不是一个激活。 一旦Grain代码订阅了流,这个订阅超过了这种激活的生命,并保持永久持续,直到Grain代码(可能在不同的激活)明确退订。 这是虚拟流抽象的核心:不仅所有的流都是逻辑地存在,而且流订阅也是持久的,并且超出了发布此订阅的特定物理激活。

4)多重性
Orleans流可能有多个生产者和多个消费者。 生产者发布的消息将被传递给消息发布之前订阅了流的所有消费者。

另外,消费者可以多次订阅相同的流。 每次订阅时,都会返回一个唯一的StreamSubscriptionHandle <T>。 如果Grain(或客户端)被X次订阅到同一个流,它将接收相同的事件X次,每次订阅一次。 消费者还可以通过以下方式退订个人订阅或查找其当前所有订阅:

IList<StreamSubscriptionHandle<T>> allMyHandles = await IAsyncStream<T>.GetAllSubscriptionHandles()

5)从故障中恢复
如果一个流的生产者挂了(或者它的Grain被停用),那么它就没有必要去做。 下一次这个Grain想要产生更多的事件,它可以重新获得流处理,并以相同的方式产生新的事件。

消费者逻辑涉及更多一点。 正如我们之前所说,一旦消费者Grain订阅了一个流,这个订阅是有效的,直到它明确退订。 如果流的消费者死亡(或其Grain被停用),并且在该流上产生新的事件,则消费者Grain将被自动地重新激活(就像任何常规的Orleans Grain在向其发送消息时自动激活)。 grain代码现在唯一需要做的就是提供一个IAsyncObserver <T>来处理数据。 消费者基本上需要重新附加处理逻辑作为OnActivateAsync方法的一部分。 要做到这一点,可以调用:

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

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

消费者如何拥有一个旧的订阅句柄?有两个选项。消费者可能已经持久化了从原来的SubscribeAsync操作中返回的句柄,现在可以使用它了。或者,如果消费者没有这个句柄,它可以通过调用来要求IAsyncStream 的所有主动订阅句柄:

IList<StreamSubscriptionHandle<T>> allMyHandles = await IAsyncStream<T>.GetAllSubscriptionHandles()

消费者现在可以恢复所有这些,或者如果他愿意的话退订。

注释:如果用户grain直接实现了IAsyncObserver接口(公共类MyGrain <T>:Grain,IAsyncObserver <T>),理论上不需要重新连接IAsyncObserver,因此不需要调用ResumeAsync。 流式运行时应该能够自动确定grain已经实现了IAsyncObserver,并且只会调用那些IAsyncObserver方法。 然而,流式运行环境目前不支持这个,即使粮食直接实现了IAsyncObserver,粮食代码仍然需要显式调用ResumeAsync。 支持这是在我们的TODO名单上。

6)显式和隐式订阅
默认情况下,流消费者必须显式订阅流。 这种订阅通常会由Grain(或客户端)收到的一些外部消息触发,指示他们订阅。 例如,在聊天服务中,当用户加入聊天室时,他的Grain会收到带有聊天名称的JoinChatGroup消息,并且会导致用户Grain订阅这个聊天流。

此外,Orleans Streams也支持“隐式订阅”。在这个模型中,Grain并不明确订阅流。这个Grain是自动订阅的,隐式的,只是基于其Grain身份和一个ImplicitStreamSubscription属性。隐式订阅的主要价值是允许流活动触发Grain激活(因此触发订阅)自动。 例如,使用SMS流,如果一个Grain想要产生一个流,而另一个Grain处理这个流,那么生产者就需要知道消费者Grain的身份,并且要求Grain调用它来订购这个流。 只有在此之后,才能开始发送事件。 只有在此之后,才能开始发送事件。 相反,使用隐式订阅,生产者可以开始将事件生成到流上,并且消费者Grain将自动被激活并订阅流。 在这种情况下,制片人完全不在乎谁在阅读这些事件

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

ImplicitStreamSubscription的存在使得流式运行时自动将这个Grain订阅到一个流,并将流事件传递给它。 然而,grain代码仍然需要告诉运行时如何处理事件。 本质上,它需要附加IAsyncObserver。 因此,在激活Grain时,OnActivateAsync中的Grain代码需要调用:

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

7)编写订阅逻辑
以下是关于如何为各种情况编写订阅逻辑的准则:显式和隐式订阅,可回放和不可回放的流。 显式和隐式订阅的主要区别在于,对于隐式的grain,每个流名称空间总是只有一个隐式订阅,没有办法创建多个订阅(没有订阅多重性),没有办法退订,而 grain逻辑总是只需要附加处理逻辑。 这也意味着,对于隐式订阅,从不需要恢复订阅。 另一方面,对于明确的订阅,需要恢复订阅,否则如果再次订阅,将会导致订阅多次。

①隐含订阅:

对于隐式订阅,Grain需要订阅附加处理逻辑。 这应该在Grain的OnActivateAsync方法中完成。 Grain应该简单地执行在其OnActivateAsync方法中等待stream.SubscribeAsync(OnNext ...)。 这将导致这个特定的激活附加OnNext函数来处理该流。 grain可以选择指定StreamSequenceToken作为SubscribeAsync的参数,这将导致这个隐式订阅从该标记开始消耗。 从不需要隐式订阅来调用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必须在每个希望继续处理的handle上执行ResumeAsync,或者在完成的任何handle上执行UnsubscribeAsync。 Grain还可以选择指定StreamSequenceToken作为ResumeAsync调用的参数,这将导致显式订阅从该令牌开始消耗。

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));
}

8)流顺序和序列令牌
个体生产者和个人消费者之间交付事件的顺序取决于流提供者。

通过SMS,生产者通过控制他发布的方式来明确地控制消费者看到的事件的顺序。 默认情况下(如果SMS提供程序的FireAndForget选项设置为false),并且生产者等待每个OnNextAsync调用,则事件按FIFO顺序到达。 在SMS中,由生产者决定如何处理由OnNextAsync调用返回的破坏的Task所指示的交付失败。

Azure队列流不保证FIFO顺序,因为底层的Azure队列不能保证在失败情况下的顺序(它们确保在无故障执行中的FIFO顺序)。 当生产者将事件生成到Azure队列中时,如果排队操作失败,则由生产者尝试另一个排队,然后再处理潜在的重复消息。 在交付方面,Orleans Streaming运行时从Azure队列中取出事件并尝试将其交付给消费者进行处理。 Orleans Streaming运行时只有在成功处理后才会从队列中删除事件。 如果交付或处理失败,则该事件不会从队列中删除,并会在稍后自动重新出现在队列中。 Streaming运行时将尝试再次传送,因此可能会破坏FIFO的顺序。 描述的行为符合Azure队列的常规语义。

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

9)可回放的流
一些数据流只允许应用程序在最近的时间点开始订阅,而其他数据流允许“回溯”。 后者的能力取决于潜在的排队技术和特定的流提供者。 例如,Azure队列只允许使用最新的入队事件,而EventHub允许从任意时间点(最多到某个过期时间)重放事件。 支持回溯的流被称为可回溯流。

可重放流的使用者可以将StreamSequenceToken传递给SubscribeAsync调用,并且运行时将从该StreamSequenceToken(一个null标记表示消费者希望从最近开始接收事件)开始向其传递事件。

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

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

10)无状态自动扩展处理
默认情况下,Orleans Streaming的目标是支持大量相对较小的流,每个流都由一个或多个完整的Grains进行处理。 所有的流加工在一起,在大量的正常(稳定的)Grain中被分割。 应用程序代码通过分配流ID,grain ID和显式订阅来控制这个分片。 目标是分解状态处理。

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

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

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

12)完全管理和可靠的流媒体Pub-Sub
为了跟踪流预订,Orleans使用一个名为Streaming Pub-Sub的运行时组件,它作为流消费者和流生产者的集合点。 Pub Sub跟踪所有的流订阅,保持它们,并将流消费者与流生产者匹配。

应用程序可以选择Pub-Sub数据的存储位置和方式。 Pub-Sub组件本身被实现为Grain(称为PubSubRendezvousGrain),它正在使用Orleans声明式持久性来表示这些Grain。 PubSubRendezvousGrain使用名为PubSubStore的存储提供程序。 与任何Grain一样,您可以指定一个存储提供者的实现。 对于Streaming Pub-Sub,您可以在配置文件中更改PubSubStore的实现:

<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<StorageProviders>
<Provider Type="Orleans.Storage.AzureTableStorage" Name="PubSubStore" />
</StorageProviders>
</Globals>
</OrleansConfiguration>

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

13)配置
为了使用流,您需要通过配置启用流提供程序。 你可以在这里阅读更多关于流提供者。 示例流提供程序配置:

<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<StreamProviders>
<Provider Type="Orleans.Providers.Streams.SimpleMessageStream.SimpleMessageStreamProvider" Name="SMSProvider"/>
<Provider Type="Orleans.Providers.Streams.AzureQueue.AzureQueueStreamProvider" Name="AzureQueueProvider"/>
</StreamProviders>
</Globals>
</OrleansConfiguration>

也可以通过调用Orleans.Runtime.Configuration.GlobalConfiguration或Orleans.Runtime.Configuration.ClientConfiguration类中的一个RegisterStreamProvider方法来以编程方式注册流提供程序。

public void RegisterStreamProvider(string providerTypeFullName, string providerName, IDictionary<string, string> properties = null)

public void RegisterStreamProvider<T>(string providerName, IDictionary<string, string> properties = null) where T : IStreamProvider

5,流提供者

流可以有不同的形状和形式, 一些流可能通过直接的TCP链接传递事件,而另一些则通过持久队列传递事件。不同的流类型可能使用不同的批处理策略、不同的缓存算法或不同的背压过程。我们不希望将流应用程序限制在这些行为选择的一小部分。 相反,Stream Providers是Orleans Streaming Runtime的扩展点,允许用户实现任何类型的流。 这个可扩展性与Orleans存储提供商的精神是相似的。 Orleans目前提供两个默认流提供程序:简单消息流提供程序和Azure队列流提供程序。

1)简单的消息流提供者
简单的消息流提供商,也被称为SMS提供商,通过利用常规的Orleans Grain消息传递在TCP上传递事件。 由于SMS中的事件是通过不可靠的TCP链接传送的,因此SMS不保证可靠的事件传送,也不会自动重新发送SMS流的失败消息。 SMS流的生产者有一种方法来知道他的事件是否被成功地接收和处理:默认情况下,对stream.OnNextAsync的调用返回一个代表流消费者处理状态的Task。 如果这个任务失败了,生产者可以决定再次发送相同的事件,从而在应用层面上实现可靠性。 虽然个人流消息传递是尽力而为的,但SMS流本身是可靠的。 也就是说,Pub Sub执行的用户到生产者的绑定是完全可靠的。

2)Azure队列(AQ)流提供程序
Azure队列(AQ)流提供程序通过Azure队列传递事件。 在生产者方面,AQ Stream Provider将事件直接排入Azure队列。 在消费者方面,AQ Stream Provider管理一组拉取代理,这些拉取代理从一组Azure队列中提取事件,并将其传递给使用它们的应用程序代码。 人们可以把提取代理想象成一个分布式的“微服务” - 一个分区的,高度可用的,有弹性的分布式组件。 拉出的代理程序运行在宿主应用程序Grains的同一仓储内。 因此,不需要运行独立的Azure角色来从队列中拉出。 牵引代理的存在,其管理,后台管理,平衡他们之间的队列以及将失败代理中的队列交给另一个代理完全由Orleans Streaming Runtime管理,并且对于使用流的应用程序代码是透明的。

3)队列适配器
通过持久队列传递事件的不同流提供者表现出类似的行为,并受到类似的实现。 因此,我们提供了一个通用的可扩展PersistentStreamProvider,它允许开发人员从头开始写入不同类型的队列,而无需从头开始编写全新的流提供程序。 PersistentStreamProvider用IQueueAdapter参数化,IQueueAdapter抽象出特定的队列实现细节,并提供入队和出队事件的方法。 其余的由PersistentStreamProvider内部的逻辑处理。 上面提到的Azure队列提供程序也是这样实现的:它是具有AzureQueueAdapter的PersistentStreamProvider实例。

6,流实现

本节提供了Orleans Stream实现的高级概述。 它描述了在应用程序级别上不可见的概念和细节。 如果您只打算使用流,则不必阅读本节。 但是,如果您打算扩展流,请在阅读Streams Extensibility部分之前阅读本节。

术语:

我们将“queue”这个词引用到任何可以提取流事件的持久存储技术,并允许提取事件或提供基于推的机制来消费事件。 通常,为了提供可伸缩性,这些技术提供分片/分区队列。 例如,Azure队列允许创建多个队列,事件中心有多个中心,Kafka主题,...

1)持久化流
所有Orleans持久流提供者共享一个通用的实现PersistentStreamProvider。 这个通用的流提供者是通过一个技术特定的IQueueAdapter进行参数化的。

当流生成器生成一个新的流项并调用stream.OnNext()时,Orleans流运行时调用该流提供者的IQueueAdapter上的适当方法,将该项直接排入适当的队列。

2)拉代理
持续流提供者的核心是拉动代理。 提取代理从一组持久队列中提取事件,并将它们传送到消耗它们的Grain中的应用程序代码。 人们可以把提取代理想象成一个分布式的“微服务” - 一个分区的,高度可用的,有弹性的分布式组件。 牵引剂运行在托管Grain的同一个仓储内,并由Orleans Streaming Runtime完全管理。

3)StreamQueueMapper和StreamQueueBalancer

Pulling代理使用IStreamQueueMapper和StreamQueueBalancerType进行参数化。IStreamQueueMapper提供了所有队列的列表,并负责将流映射到队列。 这样,持久流提供者的生产者端知道将哪个队列排入消息。StreamQueueBalancerType表示Orleans仓储和代理之间的队列平衡方式。 目标是以平衡的方式将队列分配给代理,以防止瓶颈和支持弹性。 当新的仓储被添加到Orleans集群时,队列会自动在旧的和新的筒仓中重新平衡。 StreamQueueBalancer允许自定义该进程。 Orleans有一些内置的StreamQueueBalancers,用于支持不同的平衡方案(大小队列数)和不同的环境(Azure,on prem,static)。

4)Pulling 协议

每个仓储都运行一组拖放代理,每个代理都从一个队列中拉出。提取代理本身由内部运行时组件(称为SystemTarget)实现。系统目标本质上是运行时的Grain,受到单线程并发性的影响,可以使用常规的Grain消息传递,并且像Grain一样轻。与Grain相反,系统目标不是虚拟的:它们是显式创建的(由运行时创建的),也不是位置透明的。通过将拉代理实现为系统目标,对Orleans的流运行时可以依赖于许多内置的Orleans特性,并且可以扩展到大量的队列,因为创建一个新的拉动代理就像创建一种新的谷Grain一样便宜。

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

5)队列缓存
IQueueCache是一种内部的每个代理数据结构,允许将新事件从队列中传递给消费者。 它也允许将交付分离到不同的流和不同的消费者。

想象一下,一个流有三个流消费者,其中一个流很慢。 如果不小心,缓慢的消费者有可能会影响代理商的进度,减缓其他消费者的消费,甚至有可能减缓其他消费者的排队和交付。 为了防止这种情况,并允许代理中的最大并行性,我们使用IQueueCache。

IQueueCache缓存流事件,并为代理提供一种方式,以按照其速度向每个消费者传递事件。 每个消费者交付由称为IQueueCacheCursor的内部组件实现,该组件跟踪每个消费者的进度。 这样,每个消费者都能按照自己的速度接收事件:快速消费者接收事件的速度就像从队列中出列的速度一样快,而慢速消费者接收事件的速度也是如此。 一旦消息被传递给所有消费者,它可以从缓存中删除。

6)Backpressure
在Orleans,流运行时的Backpressure应用于两个地方:将流事件从队列中引入代理,并将事件从代理传递到流消费者。

后者由内置的Orleans消息传递机制提供。 每一个流事件都是通过标准的Orleans grain 消息从代理商传递给消费者,一次一个。 也就是说,代理向每个流消费者发送一个事件(或一个有限的大小的事件)并等待这个呼叫。 下一个事件将不会开始传递,直到上一个事件的任务已解决或中断。 这样,我们自然地将每个消费者的交付率限制在一个消息。

关于从排队到代理商的流事件Orleans流媒体提供了一个新的特殊的Backpressure机制。由于代理将队列中的事件从队列中分离出来并交付给消费者,所以单个缓慢的消费者可能落后得太多以至于IQueueCache将被填满。为了防止IQueueCache无限增长,我们限制它的大小(大小限制是可配置的)。然而,代理从来没有抛出未交付的事件。相反,当缓存开始填满时,代理会降低从队列中取出事件的速率。那样的话,我们可以通过调整我们从队列中消耗的速度(“背压”)来“调整”缓慢的交付周期,并在稍后恢复到快速消费速度。为了检测“慢速递送”谷,IQueueCache使用高速缓存桶的内部数据结构来追踪事件传递给单个流消费者的进度。这导致了一个非常灵敏和自我调整的系统。

7,流扩展性

开发人员可以通过三种方式扩展当前已实现的Orleans流的行为:

  1. 利用或扩展流提供者配置。
  2. 编写一个自定义队列适配器。
  3. 写入一个新的流提供程序

我们将在下面描述这些。请在阅读本节之前阅读新Orleans的Streams实现,以便对内部实现有一个高级的视图。

1)流提供程序配置
目前实现的流提供程序支持一些配置选项。

简单的消息流提供者配置。 SMS Stream Provider当前仅支持单个配置选项:

  1. FireAndForgetDelivery:这个选项指定SMS流生成器发送的消息是否被发送,忘记了是否被发送。 当FireAndForgetDelivery设置为false(消息发送不是FireAndForget)时,流生成器的调用stream.OnNext()将返回一个Task,它表示流消费者的处理状态。 如果这个任务成功了,那么制作人就可以确定消息已经成功传递和处理了。 如果FireAndForgetDelivery设置为true,则返回的Task仅表示Orleans运行时已接受消息并将其排入队列以供进一步传送。 FireAndForgetDelivery的默认值为false。

持续流提供程序配置。 所有持久流提供程序都支持以下配置选项:

  1. GetQueueMessagesTimerPeriod - 在代理尝试再次拉取之前,最后一次尝试从队列中拉出没有返回任何项目的拉取代理等待的时间。 缺省值是100毫秒。
  2. InitQueueTimeout - 拉取代理程序等待适配器初始化与队列的连接的时间。 默认是5秒。
  3. QueueBalancerType - 用于在队列和代理之间平衡队列的平衡算法的类型。 默认是ConsistentRingBalancer。

Azure队列流提供程序配置。 Azure队列流提供程序除持久化流提供程序支持的以外,还支持以下配置选项:

  1. DataConnectionString - Azure队列存储连接字符串。
  2. DeploymentId - 此Orleans集群的部署标识(通常类似于Azure部署标识)。
  3. CacheSize - 持久提供者缓存的大小,用于存储流消息以进一步传递。 默认是4096。

这将是完全可能的,而且很多时候很容易提供额外的配置选项。 例如,在某些场景中,开发人员可能需要更多地控制队列适配器使用的队列名称。 这是目前抽象与IStreamQueueMapper,但目前没有办法配置哪个IStreamQueueMapper使用,而无需编写一个新的代码。 如果需要的话,我们很乐意提供这样的选择。 所以在编写一个全新的提供者之前,请考虑在现有的流提供者中添加更多的配置选

2)编写自定义队列适配器
如果您想使用不同的排队技术,则需要编写一个队列适配器,将适配器的访问权限抽象出来。 下面我们提供如何完成的细节。 有关示例,请参阅AzureQueueAdapterFactory。

  1. 首先定义一个实现IQueueAdapterFactory的MyQueueFactory类。 你需要:

    a. 初始化工厂:读取传递的配置值,如果需要,可能会分配一些数据结构等。

    b. 实现一个返回IQueueAdapter的方法。

    c.实现一个返回IQueueAdapterCache的方法。 理论上来说,你可以建立你自己的IQueueAdapterCache,但是你不需要。 分配并返回Orleans的SimpleQueueAdapterCache是个好主意。

    d.实现一个返回IStreamQueueMapper的方法。 再一次,理论上可以建立你自己的IStreamQueueMapper,但是你不需要。 分配并返回一个Orleans HashRingBasedStreamQueueMapper是一个好主意。

  2. 实现实现IQueueAdapter接口的MyQueueAdapter类,该接口是管理对分片队列的访问的接口。 IQueueAdapter管理对一组队列/队列分区(这些是由IStreamQueueMapper返回的队列)的访问。 它提供了在指定的队列中排队消息的能力,并为特定的队列创建IQueueAdapterReceiver。

  3. 实现实现IQueueAdapterReceiver的MyQueueAdapterReceiver类,它是管理对一个队列(一个队列分区)的访问的接口。 除了初始化和关闭之外,它基本上提供了一种方法:从队列中检索最多maxCount消息。

  4. 声明公共类MyQueueStreamProvider:PersistentStreamProvider <MyQueueFactory>。 这是您的新Stream Provider。

  5. 配置:为了加载和使用你新的流提供商,你需要通过仓储配置文件来正确配置它。 如果您需要在客户端上使用它,则需要在客户端配置文件中添加一个类似的配置元素。 也可以以编程方式配置流提供程序。 以下是仓储配置的一个例子:

<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<StreamProviders>
<Provider Type="My.App.MyQueueStreamProvider" Name="MyStreamProvider" GetQueueMessagesTimerPeriod="100ms" AdditionalProperty="MyProperty"/>
</StreamProviders>
</Globals>
</OrleansConfiguration>

3)编写一个完全新的流提供程序也可以编写一个全新的Stream Provider

在这种情况下,从Orleans 的角度来看,需要做的很少的整合。 您只需要实现IStreamProviderImpl接口,该接口是一个允许应用程序代码获取流的句柄的精简接口。 除此之外,如何实施它完全取决于你。 实现一个全新的Stream Provider可能变成一个相当复杂的任务,因为您可能需要访问各种内部运行时组件,其中一些可能具有内部访问权限。

我们目前没有预想到需要实现一个全新的Stream Provider并且不能通过上面提到的两个选项来实现他的目标的场景:通过扩展配置或者通过编写队列适配器。 但是,如果您认为您有这样的情况,我们希望听到这个消息,并一起努力简化编写新的流提供程序。

上一篇:jenkins集群(三) -- master和slave配置git


下一篇:mysql交互协议解析——mysql包基础数据、mysql包基本格式