一、Grains
二、开发一个Grain
三、开发一个客户端
四、运行应用程序
五、调式
一、Grains
Grains是Orleans编程模型的关键原语。 Grains是Orleans应用程序的构建块,它们是隔离,分配和持久性的原子单元。 Grains是表示应用程序实体的对象。 就像在经典的面向对象编程(Object Oriented Programming)中一样,grain封装实体的状态并在代码逻辑中对其行为进行编码。 Grains可以持有对方的引用,并通过调用通过接口公开的对方的方法进行交互。
注意:在操作系统中叫原语,是执行过程中不可被打断的基本操作,你可以理解为一段代码,这段代码在执行过程中不能被打断(在多道程序设计里进程间相互切换,还可能有中断发生,经常会被打断)。像原子一样具有不可分割的特性, 所以叫原语, 像原子一样的语句
Orleans的目标是大大简化构建可扩展的应用程序,并消除大部分的并发挑战
- 除了通过消息传递之外,不在grains实例之间共享数据。
- 通过提供单线程执行保证每个单独的grain。
典型的grain封装单个实体(例如特定用户或设备或会话)的状态和行为。
1,Grain身份
个体grain是grain类型(类)的一个唯一寻址实例。 每个grain在其类型中都有一个唯一的标识,也被称为grain键。 其类型中的Grain标识可以是一个长整型,一个GUID,一个字符串,或者一个长+字符串或GUID +字符串的组合。
2,访问Grain
grain类实现一个或多个grain接口,与这种类型的grain进行交互的形式代码契约。 为了调用grain,调用者需要知道grain类实现的grain接口,包括调用者想要调用的方法和目标grain的唯一标识(关键字)。 例如,以下是如何使用用户配置文件grain来更新用户的地址,如果电子邮件用作用户身份。
var user = grainFactory.GetGrain(userEmail); await user.UpdateAddress(newAddress);
调用GetGrain是一个廉价的本地操作,构建一个具有嵌入标识和目标grain类型的grain参考。
注意,不需要创建或实例化目标grain。我们调用它来更新用户的地址,就好像用户的grain已经为我们实例化了一样。这是奥尔良编程模型最大的优点之一——我们从不需要创建、实例化或删除grains。我们可以编写代码,就像所有可能的grains一样,例如数百万的用户配置文件,总是在内存中等待我们调用它们。在幕后,Orleans运行时执行所有繁重的管理资源,透明地将grains带到内存中。
3,幕后 - Grain生命周期
Grains居住在称为仓储的执行容器中。 仓储形成了一个集多个物理或虚拟机资源的集群。 当工作(请求)grain时,Orleans确保在群集中的一个仓储中有一个grain实例。 如果任何仓储上没有grain实例,则Orleans运行时创建一个。 这个过程被称为激活。 在grain使用Grain持久性的情况下,运行时会在激活时自动从后备存储中读取状态。
在仓储上激活后,grain会处理来自其他grain或群集外(通常来自前端Web服务器)的传入请求(方法调用)。 在处理请求的过程中,grain可能会调用其他grain或一些外部服务。 如果grain停止接收请求并保持空闲状态,在可配置的不活动时间段之后,Orleans从内存中删除grain(取消激活)以释放其他grains的资源。 如果当有新的请求时,奥尔良会再次激活它,可能在不同的仓储,所以调用者得到的印象是,gran一直留在内存中。 grain从存在的整个生命周期开始,只有存储器中的持久化状态(如果有的话)在内存中被实例化,才能从内存中移除。
Orleans控制透明地激活和停用Grains的过程。 编码谷物时,开发者认为所有Grains总是被激活。
Grain生命周期中关键事件的顺序如下所示。
- 另一个Grain或客户端调用Grain的方法(通过Grain参考)
- grain被激活(如果它还没有在集群中的某个地方被激活),并创建一个grain类的实例,称为grain激活
- 如果适用的话,grain的构造者是利用依赖注入来执行的
- 如果使用声明性持久性,则从存储中读取grain状态
- 如果重写,则调用OnActivateAsync
- grain处理传入的请求
- grain保持闲置一段时间
- 仓储运行时间决定停用grain
- 仓储运行时调用OnDeactivateAsync,如果重写
- 仓储运行时间从内存中删除grain
一个仓储的正常关闭后,所有的grain激活它停止。 任何等待在grain队列中处理的请求都会被转发到集群中的其他仓储,在那里根据需要创建停用的grain的新激活。 如果一个仓储关闭或死亡失败,集群中的其他仓储检测到失败,并开始创建失败的仓储上丢失的新的激活,因为这些grain的新的请求到达。 请注意,检测仓储故障需要一些时间(可配置),因此重新激活丢失谷物的过程不是即时的。
4,Grain执行
grain激活在块中执行工作,并在每个块执行到下一段之前完成。大量的工作内容包括了对其他grains或外部客户的请求的方法调用,以及在完成前一段时间的关闭计划。与一大块工作相对应的基本执行单位称为turn。
虽然Orleans可能会执行很多次并行的不同激活,但每次激活都将一次执行一次。这意味着不需要使用锁或其他同步方法来防止数据竞争和其他多线程危害。
二、开发一个Grain
1,设置
在编写代码来实现Grain类之前,在Visual Studio中创建一个新的以.NET 4.6.1或更高版本为目标的类库项目,并向其中添加 Microsoft.Orleans.OrleansCodeGenerator.Build NuGet包。
PM> Install-Package Microsoft.Orleans.OrleansCodeGenerator.Build
2,Grain接口和类
Grain彼此交互,并通过调用声明为各自的Grain接口的一部分方法从外部被调用。 Grain类实现一个或多个先前声明的Grain接口。 Grains接口的所有方法必须返回一个Task(对于void方法)或Task
以下是Presence Service示例的摘录:
//一个Grain界面的例子 public interface IPlayerGrain : IGrainWithGuidKey { TaskGetCurrentGame(); Task JoinGame(IGameGrain game); Task LeaveGame(IGameGrain game); } //一个Grain类实现Grain接口的例子 public class PlayerGrain : Grain, IPlayerGrain { private IGameGrain currentGame; // 玩家目前所在的游戏。可能为空。 public Task GetCurrentGame() { return Task.FromResult(currentGame); } //游戏谷歌调用此方法通知玩家已加入游戏。 public Task JoinGame(IGameGrain game) { currentGame = game; Console.WriteLine( "Player {0} joined game {1}", this.GetPrimaryKey(), game.GetPrimaryKey()); return Task.CompletedTask; } //游戏谷歌称这种方法通知玩家已经离开游戏。 public Task LeaveGame(IGameGrain game) { currentGame = null; Console.WriteLine( "Player {0} left game {1}", this.GetPrimaryKey(), game.GetPrimaryKey()); return Task.CompletedTask; } }
3,从Grains方法返回值
返回类型T值的grain方法在grain接口中定义为返回Task
public TaskGrainMethod1() { ... return Task.FromResult( ); }
一个没有返回值的grain方法,实际上是一个void方法,在grain接口中被定义为返回Task。 返回的Task表示异步执行和方法的完成。 对于没有用async关键字标记的grain方法,当“void”方法完成它的执行时,它需要返回Task.CompletedTask的特殊值:
public Task GrainMethod2() { ... return Task.CompletedTask; }
标记为async的grain方法直接返回值:
public async TaskGrainMethod3() { ... return ; }
标记为async的“void”grain方法不返回任何值,只是在执行结束时返回:
public async Task GrainMethod4() { ... return; }
如果一个grain方法从另一个异步方法调用接收到返回值,并且不需要执行该调用的错误处理,那么它可以简单地将它从该异步调用接收的Task作为返回值返回:
public TaskGrainMethod5() { ... Task task = CallToAnotherGrain(); return task; }
类似地,“void”grain方法可以返回一个Task,而不是等待它,而是通过另一个调用返回给它。
public Task GrainMethod6() { ... Task task = CallToAsyncAPI(); return task; }
4,Grain引用
Grain引用是一个代理对象,它实现与相应的grain类相同的grain接口。 它封装了目标grain的逻辑标识(类型和唯一键)。 Grain引用是用来调用目标Grain的。 每个Grain引用都是针对一个Grain(Grain类的单个实例),但是可以为同一个Grain创建多个独立的引用。
由于Grain引用代表目标Grain的逻辑身份,它独立于Grain的物理位置,并且即使在系统完全重新启动之后也保持有效。 开发人员可以像使用其他.NET对象一样使用Grain引用。 它可以传递给一个方法,用作方法的返回值等等,甚至保存到持久存储中。
通过将Grain身份传递给GrainFactory.GetGrain
以下是如何获取上面定义的IPlayerGrain接口的Grain引用的示例。
①从Grain里面使用的方式:
//构建特定玩家的Grain引用 IPlayerGrain player = GrainFactory.GetGrain(playerId);
②从Orleans客户端代理里面使用的方式
在1.5.0版本之前: IPlayerGrain player = GrainClient.GrainFactory.GetGrain
自1.5.0版本之后: IPlayerGrain player = client.GetGrain
5,Grain方法调用
Orleans编程模型基于Async和Await异步编程。
使用前面例子中的Grain引用,下面是一个如何执行Grain方法调用:
//异步调用grain方法 Task joinGameTask = player.JoinGame(this); //await关键字有效地使方法的其余部分在稍后的时间点(在等待完成的任务完成时)异步执行,而不会阻塞线程。 await joinGameTask; //joinGameTask完成后,下一行将执行。 players.Add(playerId);
可以加入两个或多个任务; 连接操作将创建一个新的“任务”,该任务在其所有组成任务完成时解决。 当Grain需要启动多个计算并等待所有的计算完成之前,这是一个有用的模式。 例如,生成由多个部分组成的网页的前端页面可能会进行多个后端调用,每个部分一个,并为每个结果接收一个任务。 Grain将等待所有这些任务的加入; 当加入任务被解决时,单独的任务已经完成,并且已经接收到格式化网页所需的所有数据。
例子:
Listtasks = new List (); Message notification = CreateNewMessage(text); foreach (ISubscriber subscriber in subscribers) { tasks.Add(subscriber.Notify(notification)); } // WhenAll加入一个任务集合,并返回一个联合任务,将解决所有单个通知任务时将解决。 Task joinedTask = Task.WhenAll(tasks); await joinedTask; // 方法的其余部分的执行将在joinedTask解析后继续异步执行。
6,虚方法
一个Grain类可以选择重写OnActivateAsync和OnDeactivateAsync虚拟方法,这些方法被Orleans的运行时激活和取消激活时调用。这让Grain代码有机会进行额外的初始化和清理工作。OnActivateAsync引发的异常会使激活过程失败。虽然OnActivateAsync(如果被重写)总是被称为Grain激活过程的一部分,OnDeactivateAsync不能保证在所有情况下都被调用,例如,在服务器故障或其他异常事件时。因此,应用程序不应该依赖OnDeactivateAsync来执行关键操作,例如状态更改的持久性,只能用于最佳操作。
三、开发一个客户端
1,什么是Grain客户端?
术语“客户端”或有时“Grain客户端”用于与Grains相互作用的应用代码,但其本身不是Grain逻辑的一部分。 客户端代码运行在托管Grains的称为仓储的Orleans服务器群集之外。 因此,客户作为连接器或通往集群和应用程序的所有Grains的通道。
通常,前端Web服务器上使用客户端连接到作为中间层的Orleans群集,并执行业务逻辑。 在典型的设置中,前端Web服务器:
- 接收Web请求
- 执行必要的身份验证和授权验证
- 决定哪些Grain(s)应该处理请求
- 使用Grain Client对Grain(s)进行一个或多个方法调用
- 处理Grain调用的成功完成或失败以及任何返回的值
- 发送Web请求的响应
2,Grain Client的初始化
在Grain客户端可以用于调用Orleans集群托管的Grain之前,需要对Grain客户端进行配置,初始化和连接到集群。
通过ClientConfiguration对象提供配置,该对象包含用于以编程方式配置客户端的配置属性层次结构。 还有一种方法可以通过XML文件来配置客户端,但该选项将来会被弃用。 更多信息在“客户端配置”指南中。 在这里,我们将简单地使用一个帮助器方法来创建一个硬编码的配置对象,用于连接到作为本地主机运行的本地仓储
ClientConfiguration clientConfig = ClientConfiguration.LocalhostSilo();
一旦我们有了一个配置对象,我们可以通过ClientBuilder类建立一个客户端。
IClusterClient client = new ClientBuilder().UseConfiguration(clientConfig).Build();
最后,我们需要在构建的客户端对象上调用Connect()方法,使其连接到Orleans集群。 这是一个返回任务的异步方法。 所以我们需要等待完成,等待或者.Wait()
await client.Connect();
3,调用Grains
从客户端调用Grain与从Grain代码中进行这种调用确实没有区别。 在这两种情况下,使用相同的GetGrain
IPlayerGrain player = client.GetGrain(playerId); Task t = player.JoinGame(game) await t;
对grain方法的调用返回一个Task或Task
从客户端代码和其他Grain中调用Grain的主要区别在于Grain的单线程执行模式。 Grain被Orleans运行时限制为单线程,而客户端可能是多线程的。 Orleans并没有在客户端提供任何这样的保证,所以客户可以使用任何适合其环境的同步结构(锁,事件,任务等)来管理自己的并发。
4,接收通知
有些情况下,一个简单的请求 - 响应模式是不够的,客户端需要接收异步通知。 例如,用户可能希望在她正在关注的人发布新消息时收到通知。
观察者就是这样一种机制,可以将客户端对象暴露为Gtains目标以被Grain调用。对观察者的调用不提供任何成功或失败的指示,因为它们被发送为单向的最佳工作消息。因此,在必要的地方,应用程序代码负责在观察者之上构建更高级别的可靠性机制。
另一种可用于向客户端传递异步消息的机制是Streams。 数据流暴露了单个消息传递成功或失败的迹象,从而使可靠的通信回到客户端。
5,例子:
这是上面给出的客户端应用程序的一个扩展版本,连接到Orleans,查找玩家帐户,订阅游戏会话的更新(玩家是观察者的一部分),并打印出通知,直到手动终止程序。
namespace PlayerWatcher { class Program { ////// 模拟连接到特定玩家当前所参与的游戏的同伴应用程序,并订阅接收关于其进度的实时通知。. /// static void Main(string[] args) { RunWatcher().Wait(); //阻塞主线程,以便进程不退出。更新到达线程池线程。 Console.ReadLine(); } static async Task RunWatcher() { try { // 连接到本地仓储 var config = ClientConfiguration.LocalhostSilo(); var client = new ClientBuilder().UseConfiguration(config).Build(); await client.Connect(); // 硬编码的玩家ID Guid playerId = new Guid("{2349992C-860A-4EDA-9590-000000000006}"); IPlayerGrain player = client.GetGrain (playerId); IGameGrain game = null; while (game == null) { Console.WriteLine("为玩家准备当前的游戏 {0}...", playerId); try { game = await player.GetCurrentGame(); if (game == null) // 等待玩家加入游戏 { await Task.Delay(5000); } } catch (Exception exc) { Console.WriteLine("Exception: ", exc.GetBaseException()); } } Console.WriteLine("订阅更新游戏 {0}...", game.GetPrimaryKey()); // 订阅的更新 var watcher = new GameObserver(); await game.SubscribeForGameUpdates( await client.CreateObjectReference (watcher)); Console.WriteLine("成功订阅。按<输入>停止。"); } catch (Exception exc) { Console.WriteLine("意想不到的错误: {0}", exc.GetBaseException()); } } } /// ///实现Observer接口的Observer类。 需要将Grain引用传递给此类的实例以订阅更新。 /// class GameObserver : IGameObserver { // 接收更新 public void UpdateGameScore(string score) { Console.WriteLine("New game score: {0}", score); } } } }
四、运行应用程序
1,Orleans应用
如上一个主题所述,一个典型的Orleans应用程序由Grains所在的一组服务器进程(仓储)和一组客户进程(通常是Web服务器)组成,这些客户进程接收外部请求,将其转换为Grain方法调用,以及 返回结果。 因此,运行Orleans应用程序需要做的第一件事就是启动一个仓储群。 出于测试目的,群集可以由单个仓储组成。 为了实现可靠的生产部署,我们显然希望集群中有多个仓储用于容错和扩展。
集群运行后,我们可以启动一个或多个连接到集群的客户端进程,并可以向Grains发送请求。 客户端连接到仓储网关上的特殊TCP端点。 默认情况下,群集中的每个仓储都启用了客户端网关。 因此,客户可以并行连接到所有仓储,以获得更好的性能和弹性。
2,配置和启动仓储
一个仓储通过一个ClusterConfiguration对象以编程方式进行配置。 它可以被实例化和直接填充,从一个文件加载设置,或者为不同的部署环境使用几个可用的帮助器方法创建。 对于本地测试,最简单的方法是使用ClusterConfiguration.LocalhostPrimarySilo()辅助方法。 然后将配置对象传递给SiloHost类的新实例,可以在此之后进行初始化和启动。
您可以创建一个空的控制台应用程序项目,以.NET Framework 4.6.1或更高版本为主机。 将Microsoft.Orleans.Server NuGet元数据包添加到项目。
PM> Install-Package Microsoft.Orleans.Server
下面是一个如何开始本地仓储的例子:
var siloConfig = ClusterConfiguration.LocalhostPrimarySilo(); var silo = new SiloHost("Test Silo", siloConfig); silo.InitializeOrleansSilo(); silo.StartOrleansSilo(); Console.WriteLine("按Enter键关闭。"); // 在这里阻止 Console.ReadLine(); // 我们完成后关闭筒仓。 silo.ShutdownOrleansSilo();
3,配置和连接客户端
用于连接到仓储集群并向Grains发送请求的客户端通过ClientConfiguration对象和ClientBuilder以编程方式进行配置。 ClientConfiguration对象可以实例化并直接填充,从一个文件加载设置,或者为不同的部署环境使用多个可用的帮助器方法创建。 对于本地测试,最简单的方法是使用ClientConfiguration.LocalhostSilo()辅助方法。 然后将配置对象传递给ClientBuilder类的新实例。
ClientBuilder公开了更多配置其他客户端功能的方法。 之后调用ClientBuilder对象的Build方法来获得IClusterClient接口的实现。 最后,我们调用返回对象上的Connect()方法来连接到集群。
您可以创建一个空的控制台应用程序项目,以.NET Framework 4.6.1或更高版本为目标来运行客户端,或者重新使用您创建的控制台应用程序项目托管一个仓储。 将Microsoft.Orleans.Client NuGet元程序包添加到项目。
PM> Install-Package Microsoft.Orleans.Client
以下是一个客户端如何连接到本地仓储的例子:
var config = ClientConfiguration.LocalhostSilo(); var builder = new ClientBuilder().UseConfiguration(config). var client = builder.Build(); await client.Connect();
4,生产配置
我们在这里使用的配置示例是用于测试与本地主机在同一台机器上运行的仓储和客户机。 在生产中,仓储和客户机通常运行在不同的服务器上,并配置有一个可靠的群集配置选项。 有关详细信息,请参阅Configuration Guide和Cluster Management的说明。
五、调式
1,调试
在开发过程中,基于Orleans的应用程序可以在开发过程中使用调试器来调试程序或客户进程。为了快速开发迭代,使用单独的进程来结合仓储和客户端是很方便的,例如,由Orleans Dev/Test Host项目模板创建的控制台应用程序项目,它是Microsoft Orleans工具扩展Visual Studio的一部分。当在Azure计算模拟器中运行时,可以将调试器附加到Worker / Web Role实例进程。
在生产环境中,在一个断点处停止仓储几乎不是一个好主意,因为冻结的仓储很快就会被集群成员协议投票死掉,并且将无法与集群中的其他仓储进行通信。 因此,在生产追踪是主要的“调试”机制。
2,来源链接
从2.0 - beta1版本开始,我们增加了对符号的源链接支持。这意味着,如果一个项目使用了新Orleans的NuGet软件包,在调试应用程序代码时,它们就可以进入Orleans源代码。在Steve Gordon的博客文章中,您可以看到配置它需要哪些步骤。