Orleans 2 实例
建立官方实例进行说明:
一、项目结构
包括2个控制台应用和2个类库,要下载的Nuget包和项目之间的依赖如下:
二、项目结构分析
1、Grain Interfaces
只有一个借口IHello
接口内容很简单,就是一个异步函数。Task和Task<T>类型都表示一个可能还未完成的操作。Task本身也可以理解为Task<void>。调用SayHello方法的调用者只关心调用这个方法,而并不关心什么时候SayHello方法处理完毕,但因为方法返回类型是Task<T>,所以调用者可以选择等待操作完成并获得返回值。
2、Grains
Grains是orleans中最小的执行单元,是Virtual Actor的具体实现,Grain可以理解成一个可寻址(每个Grains都是一个独立的隔离的可寻址的.Net对象实例)
Grains可以由自己的状态和行为;其自身的状态和行为不受外界干预;在分布式环境下,分布在集群中的Grain有自己的Identity实现跨机器寻址。
在分布式环境下,不可能通过单纯的new对象就简单的实现内存寻址,因为引用不局限于单个地址空间。Grain有一个单独的身份标识,Identity。通过Identity来唯一确定一个Grain,比如Unique Grain=Grain Type+Grain Identity。
Grain本身可以理解为一个服务,是抽象业务逻辑和其实现。
Grain可以被其他Grain或者是外部客户端比如web前端通过强类型接口调用,每个Grain都是一个实现了一个或多个这些接口的类实例。
orleans运行时可以按需自动对谷物进行实例化,一段时间未使用的grain会自动从内存中删除以释放资源。同事还允许从故障中透明恢复。grain具有自己的托管生命周期,由orleans负责激活和停用,并按需放置放置和定位。
这就允许程序员编写代码,就像所有颗粒始终在内存中。
实现这种模型的原因是orleans运行时,其核心是silo,silo是grains的宿主。一般来说,一组silo会集群运行,以实现可伸缩和容错,作为集群运行时,silo可以相互协调工作,检测故障并从中恢复。运行时,silo集群中托管的grain能够互相通信,就像单线程一样。
3、Client
//需要配置一个ClusterID以匹配指定的本地Silo,并指明静态集群作为自己的集群选择,将其指向silo ClientBuilder的网关 IClusterClient clusterClient; //为单个本地Silo使用本地集群:UseLocalHostClustering() clusterClient = new ClientBuilder().UseLocalhostClustering().Configure<ClusterOptions>(options => { //配置ClusterId和ServiceId options.ClusterId = "dev"; options.ServiceId = "OrleansBasics"; }).ConfigureLogging(logging => logging.AddConsole()).Build(); //完成这些步骤后,可以构建客户端和方法连接到clusterClient.Connect。 await clusterClient.Connect();
Client会和Grain通信,Client通过连接至集群(示例中集群有一个Silo)然后调用Grain。集群配置本身必须要和Silo配置匹配。
4、Silo
Silo相当于一台服务器,用于存储Grains,Grains需要注册到Silo中,然后等待调用。示例中使用了开发集群提供源以在没有外部存储系统以来的情况下运行所有业务。
private static async Task<ISiloHost> StartSilo() { //需要配置经由方法来指定自己的包含有Silo的集群作为首选项。 var builder = new SiloHostBuilder().UseLocalhostClustering().Configure<ClusterOptions>(options => { options.ClusterId = "dev"; options.ServiceId = "OrleansBasics"; //ConfigureApplicationParts会显示的把Grain类相关程序集添加到应用中 //同时还会根据其扩展添加关联的程序集。 //当上述步骤完成后,silo会创建然后启动 }).ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(HelloGrain).Assembly).WithReferences()) .ConfigureLogging(logging => logging.AddConsole()); var host = builder.Build(); await host.StartAsync(); return host; }
三、深入理解Grain Client
Client或Grain Client被用来和Grain交互,但它本身并不是Grain逻辑。Client代码在Orleans集群服务外部的Silo中允许,Silo也是Grain的宿主。因此,Client扮演了在集群和所有Grains的一种交互者的角色。
通常,客户端被前端web服务使用,以连接orleans集群,orleans集群是一种带有Grains执行逻辑的中间层,在典型的前端web服务中:
首先接收到web请求——执行必要的授权验证——决定哪一个Grains来处理请求——使用Grain Client来对Grain的一个或多个方法进行调用——调用成功完成或失败以及返回值——返回web请求响应
2、初始化Grain Client
在Grain Client可以调用寄宿在orleans集群中的Grain前,Client需要配置,初始化和连接到集群。配置通过ClientBuilder和很多补充可选类提供,这些补充类包含配置属性的层级,以使用编程方式来配置Client。
IClusterClient clusterClient; //为单个本地Silo使用本地集群:UseLocalHostClustering() clusterClient = new ClientBuilder().UseLocalhostClustering().Configure<ClusterOptions>(options => { //配置ClusterId和ServiceId options.ClusterId = "dev"; options.ServiceId = "OrleansBasics"; }).ConfigureLogging(logging => logging.AddConsole()).Build();
最后,调用该Client实例的Connect()方法来让Client连接到orleans集群,他是异步方法,并返回一个Task。因此可以使用await或者Wait()来异步非阻塞等待或同步阻塞式等待结果。
await clusterClient.Connect();
3、调用Grains
从Client调用Grain和从Grain代码中调用Grain没什么不同。都是GetGrain<T>(key)方法。
private static async Task DoClientWork(IClusterClient clusterClient) { //example of calling grains from the initialized client var friend = clusterClient.GetGrain<IHello>(0); var response = await friend.SayHello("Good morning,HelloGrain"); Console.WriteLine("\n\n{0}\n\n", response); }
T是目标Grain接口,一般是一个接口类。2种情况下都可以获取Grain引用,细微的区别在于使用何种工厂对象来调用GetGrain,在Client代码中,通过连接Client对象来实现。按照grain接口规则,调用grain方法会返回一个Task或者Task<T>。客户端可以使用await来异步非阻塞式地等待返回Task,也可以使用Wait()来阻塞当前线程的执行直到结果返回。
从client代码中调用grains和从其他grain中调用grain的主要区别在于grain的单线程执行模型。grains被orleans运行时约束只能通过单线程的方式,而client却可能是多线程。orleans不会对client提供任何担保,因此完全取决于客户端自己怎么使用适合其环境的同步结构来管理自己的并发性。(锁、事件、Tasks等等)
4、接收通知
简单的请求回复模式可能不够,client有时需要接收异步的消息。比如,当一条新的信息被发布,而用户正在监听时,用户可能想要被通知。
观察者机制允许client对象像grain-like对象一样被grains调用而暴露出来。对观察者的调用不会有任何成功或失败的提示,因为他们被作为one-way最大化信息方式来发送。因此,在观察者上层有必要创建一个更高可信机制。
5、Client的连接性
有2种情形集群client可能会遇到连接性问题:
当IClusterClient.Connect方法被首次调用时。
当调用从连接的集群Client获得的grain引用时。
因为暂时还没有遇到这两种问题,所以先保留,容后研究。
6、依赖注入
在程序中使用一个集群client的推荐方式是通过依赖注入,使用.Net泛型宿主来注入一个IClusterInstance单例实例,之后它可以在宿主服务中以一个构造器参数的形式被接收。
注意在同一个进程中共同托管一个Silo,而这个Silo将要连接Client时,并不需要一定手动创建一个client,orleans会自动的提供一个client兵有效的管理它的生命周期。
当在不同进程连接一个集群时(比如不同的设备上),共同的模式是创建一个托管服务,比如:
public class ClusterClientHostedService : IHostedService { public IClusterClient Client { get; } public ClusterClientHostedService(ILoggerProvider loggerProvider) { Client = new ClientBuilder() // Appropriate client configuration here, e.g.: .UseLocalhostClustering() .ConfigureLogging(builder => builder.AddProvider(loggerProvider)) .Build(); } public async Task StartAsync(CancellationToken cancellationToken) { // A retry filter could be provided here. await Client.Connect(); } public async Task StopAsync(CancellationToken cancellationToken) { await Client.Close(); Client.Dispose(); } }
public class Program { static Task Main() { return new HostBuilder() .ConfigureServices(services => { services.AddSingleton<ClusterClientHostedService>(); services.AddSingleton<IHostedService>(sp => sp.GetService<ClusterClientHostedService>()); services.AddSingleton<IClusterClient>(sp => sp.GetService<ClusterClientHostedService>().Client); services.AddSingleton<IGrainFactory>(sp => sp.GetService<ClusterClientHostedService>().Client); }) .ConfigureLogging(builder => builder.AddConsole()) .RunConsoleAsync(); } }
服务可以像类似于Core MVC/WebAPI的依赖注入一样注册到容器中去。此时,IClusterClient实例就可以在依赖注入支持的任何位置被使用,比如ASP.NET Controller
public class HomeController : Controller { readonly IClusterClient _client; public HomeController(IClusterClient client) => _client = client; public IActionResult Index() { var grain = _client.GetGrain<IMyGrain>(); var model = grain.GetModel(); return View(model); } }