Orleans 知多少 | 3. Hello Orleans

孤街浪徒 提交于 2020-12-13 14:43:45

1. 引言

是的,Orleans v3.0.0 已经发布了,并已经完全支持 .NET Core 3.0。所以,Orleans 系列是时候继续了,抱歉,让大家久等了。万丈高楼平地起,这一节我们就先来了解下Orleans的基本使用。

2. 模板项目讲解

在上一篇文章中,我们了解到Orleans 作为.NET 分布式框架,其主要包括三个部分:Client、Grains、Silo Host(Server)。因此,为了方便讲解,创建如下的项目结构进行演示:

这里有几点需要说明:

  1. Orleans.Grains:类库项目,用于定义Grain的接口以及实现,需要引用 Microsoft.Orleans.CodeGenerator.MSBuild和 Microsoft.Orleans.Core.Abstractions NuGet包。

  2. Orleans.Server:控制台项目,为 Silo 宿主提供宿主环境,需要引用 Microsoft.Orleans.Server 和 Microsoft.Extensions.Hosting NuGet包,以及 Orleans.Grains 项目。

  3. Orleans.Client:控制台项目,用于演示如何借助Orleans Client建立与Orleans Server的连接,需要引用 Microsoft.Orleans.Client 和 Microsoft.Extensions.Hosting NuGet包,同时添加 Orleans.Grains项目引用。

3. 第一个Grain

Grain作为Orleans的第一公民,以及Virtual Actor的实际代言人,想吃透Orleans,那Grain就是第一道坎。先看一个简单的Demo,我们来模拟统计网站的实时在线用户。在 Orleans.Grains添加 ISessionControl接口,主要用户登录状态的管理。

  
  
  1. public interface ISessionControlGrain : IGrainWithStringKey

  2. {

  3. Task Login(string userId);

  4. Task Logout(string userId);

  5. Task<int> GetActiveUserCount();

  6. }

可以看见Grain的定义很简单,只需要指定继承自IGrain的接口就好。这里面继承自 IGrainWithStringKey,说明该Grain 的Identity Key(身份标识)为 string类型。同时需要注意的是Grain 的方法申明,返回值必须是:Task、Task 、ValueTask 。紧接着定义 SessionControlGrain来实现 ISessionControlGrain接口。

  
  
  1. public class SessionControlGrain : Grain, ISessionControlGrain

  2. {

  3. private List<string> LoginUsers { get; set; } = new List<string>();


  4. public Task Login(string userId)

  5. {

  6. //获取当前Grain的身份标识(因为ISessionControlGrain身份标识为string类型,GetPrimaryKeyString());

  7. var appName = this.GetPrimaryKeyString();


  8. LoginUsers.Add(userId);


  9. Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}");

  10. return Task.CompletedTask;

  11. }


  12. public Task Logout(string userId)

  13. {

  14. //获取当前Grain的身份标识

  15. var appName = this.GetPrimaryKey();

  16. LoginUsers.Remove(userId);


  17. Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}");

  18. return Task.CompletedTask;

  19. }


  20. public Task<int> GetActiveUserCount()

  21. {

  22. return Task.FromResult(LoginUsers.Count);

  23. }

  24. }

实现也很简单,Grain的实现要继承自 Grain基类。代码中我们定义了一个 List<string>集合用于保存登录用户。

4. 第一个Silo Host(Server)

定义一个Silo用于暴露Grain提供的服务,在 Orleans.Server.Program中添加以下代码用于启动Silo Host。

  
  
  1. static Task Main(string[] args)

  2. {

  3. Console.Title = typeof(Program).Namespace;


  4. // define the cluster configuration

  5. return Host.CreateDefaultBuilder()

  6. .UseOrleans((builder) =>

  7. {

  8. builder.UseLocalhostClustering()

  9. .AddMemoryGrainStorageAsDefault()

  10. .Configure<ClusterOptions>(options =>

  11. {

  12. options.ClusterId = "Hello.Orleans";

  13. options.ServiceId = "Hello.Orleans";

  14. })

  15. .Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback)

  16. .ConfigureApplicationParts(parts =>

  17. parts.AddApplicationPart(typeof(ISessionControlGrain).Assembly).WithReferences());

  18. }

  19. )

  20. .ConfigureServices(services =>

  21. {

  22. services.Configure<ConsoleLifetimeOptions>(options =>

  23. {

  24. options.SuppressStatusMessages = true;

  25. });

  26. })

  27. .ConfigureLogging(builder => { builder.AddConsole(); })

  28. .RunConsoleAsync();

  29. }

  1. Host.CreateDefaultBuilder():创建泛型主机提供宿主环境。

  2. UseOrleans:用来配置Oleans。

  3. UseLocalhostClustering() :用于在开发环境下指定连接到本地集群。

  4. Configure<ClusterOptions>:用于指定连接到那个集群。

  5. Configure<EndpointOptions>:用于配置silo与silo、silo与client之间的通信端点。开发环境下可仅指定回环地址作为集群间通信的IP地址。

  6. ConfigureApplicationParts():用于指定暴露哪些Grain服务。

以上就是开发环境下,Orleans Server的基本配置。对于详细的配置也可以先参考Orleans Server Configuration。后续也会有专门的一篇文章来详解。

5. 第一个Client

客户端的定义也很简单,主要是创建 IClusterClient对象建立于Orleans Server的连接。因为 IClusterClient最好能在程序启动之时就建立连接,所以可以通过继承 IHostedService来实现。在 Orleans.Client中定义 ClusterClientHostedService继承自 IHostedService

  
  
  1. public class ClusterClientHostedService : IHostedService

  2. {

  3. public IClusterClient Client { get; }


  4. private readonly ILogger<ClusterClientHostedService> _logger;


  5. public ClusterClientHostedService(ILogger<ClusterClientHostedService> logger, ILoggerProvider loggerProvider)

  6. {

  7. _logger = logger;

  8. Client = new ClientBuilder()

  9. .UseLocalhostClustering()

  10. .Configure<ClusterOptions>(options =>

  11. {

  12. options.ClusterId = "Hello.Orleans";

  13. options.ServiceId = "Hello.Orleans";

  14. })

  15. .ConfigureLogging(builder => builder.AddProvider(loggerProvider))

  16. .Build();

  17. }


  18. public Task StartAsync(CancellationToken cancellationToken)

  19. {

  20. var attempt = 0;

  21. var maxAttempts = 100;

  22. var delay = TimeSpan.FromSeconds(1);

  23. return Client.Connect(async error =>

  24. {

  25. if (cancellationToken.IsCancellationRequested)

  26. {

  27. return false;

  28. }


  29. if (++attempt < maxAttempts)

  30. {

  31. _logger.LogWarning(error,

  32. "Failed to connect to Orleans cluster on attempt {@Attempt} of {@MaxAttempts}.",

  33. attempt, maxAttempts);


  34. try

  35. {

  36. await Task.Delay(delay, cancellationToken);

  37. }

  38. catch (OperationCanceledException)

  39. {

  40. return false;

  41. }


  42. return true;

  43. }

  44. else

  45. {

  46. _logger.LogError(error,

  47. "Failed to connect to Orleans cluster on attempt {@Attempt} of {@MaxAttempts}.",

  48. attempt, maxAttempts);


  49. return false;

  50. }

  51. });

  52. }


  53. public async Task StopAsync(CancellationToken cancellationToken)

  54. {

  55. try

  56. {

  57. await Client.Close();

  58. }

  59. catch (OrleansException error)

  60. {

  61. _logger.LogWarning(error, "Error while gracefully disconnecting from Orleans cluster. Will ignore and continue to shutdown.");

  62. }

  63. }

  64. }

代码讲解:

1.构造函数中通过借助 ClientBuilder() 来初始化 IClusterClient。其中 UseLocalhostClustering()用于连接到开发环境中的localhost 集群。并通过 Configure<ClusterOptions>指定连接到哪个集群。(需要注意的是,这里的ClusterId必须与Orleans.Server中配置的保持一致。

  
  
  1. Client = new ClientBuilder()

  2. .UseLocalhostClustering()

  3. .Configure<ClusterOptions>(options =>

  4. {

  5. options.ClusterId = "Hello.Orleans";

  6. options.ServiceId = "Hello.Orleans";

  7. })

  8. .ConfigureLogging(builder => builder.AddProvider(loggerProvider))

  9. .Build();

2. 在 StartAsync方法中通过调用 Client.Connect建立与Orleans Server的连接。同时定义了一个重试机制。

紧接着我们需要将 ClusterClientHostedService添加到Ioc容器,添加以下代码到 Orleans.Client.Program中:

  
  
  1. static Task Main(string[] args)

  2. {

  3. Console.Title = typeof(Program).Namespace;


  4. return Host.CreateDefaultBuilder()

  5. .ConfigureServices(services =>

  6. {

  7. services.AddSingleton<ClusterClientHostedService>();

  8. services.AddSingleton<IHostedService>(_ => _.GetService<ClusterClientHostedService>());

  9. services.AddSingleton(_ => _.GetService<ClusterClientHostedService>().Client);


  10. services.AddHostedService<HelloOrleansClientHostedService>();

  11. services.Configure<ConsoleLifetimeOptions>(options =>

  12. {

  13. options.SuppressStatusMessages = true;

  14. });

  15. })

  16. .ConfigureLogging(builder =>

  17. {

  18. builder.AddConsole();

  19. })

  20. .RunConsoleAsync();

  21. }

对于 ClusterClientHostedService,并没有选择直接通过 services.AddHostedService<T>的方式注入,是因为我们需要注入该服务中提供的 IClusterClient(单例),以供其他类去消费。

紧接着,定义一个 HelloOrleansClientHostedService用来消费定义的 ISessionControlGrain

  
  
  1. public class HelloOrleansClientHostedService : IHostedService

  2. {

  3. private readonly IClusterClient _client;

  4. private readonly ILogger<HelloOrleansClientHostedService> _logger;


  5. public HelloOrleansClientHostedService(IClusterClient client, ILogger<HelloOrleansClientHostedService> logger)

  6. {

  7. _client = client;

  8. _logger = logger;

  9. }

  10. public async Task StartAsync(CancellationToken cancellationToken)

  11. {

  12. // 模拟控制台终端用户登录

  13. await MockLogin("Hello.Orleans.Console");

  14. // 模拟网页终端用户登录

  15. await MockLogin("Hello.Orleans.Web");

  16. }


  17. /// <summary>

  18. /// 模拟指定应用的登录

  19. /// </summary>

  20. /// <param name="appName"></param>

  21. /// <returns></returns>

  22. public async Task MockLogin(string appName)

  23. {

  24. //假设我们需要支持不同端登录用户,则只需要将项目名称作为身份标识。

  25. //即可获取一个代表用来维护当前项目登录状态的的单例Grain。

  26. var sessionControl = _client.GetGrain<ISessionControlGrain>(appName);

  27. ParallelLoopResult result = Parallel.For(0, 10000, (index) =>

  28. {

  29. var userId = $"User-{index}";

  30. sessionControl.Login(userId);

  31. });


  32. if (result.IsCompleted)

  33. {

  34. //ParallelLoopResult.IsCompleted 只是返回所有循环创建完毕,并不保证循环的内部任务创建并执行完毕

  35. //所以,此处手动延迟5秒后再去读取活动用户数。

  36. await Task.Delay(TimeSpan.FromSeconds(5));

  37. var activeUserCount = await sessionControl.GetActiveUserCount();


  38. _logger.LogInformation($"The Active Users Count of {appName} is {activeUserCount}");

  39. }

  40. }


  41. public Task StopAsync(CancellationToken cancellationToken)

  42. {

  43. _logger.LogInformation("Closed!");


  44. return Task.CompletedTask; ;

  45. }

  46. }

代码讲解:这里定义了一个 MockLogin用于模拟不同终端10000个用户的并发登录。

  1. 通过构造函数注入需要的 IClusterClient

  2. 通过指定Grain接口以及身份标识,就可以通过Client 获取对应的Grain,进而消费Grain中暴露的方法。 varsessionControl=_client.GetGrain<ISessionControlGrain>(appName); 这里需要注意的是,指定的身份标识为终端应用的名称,那么在整个应用生命周期内,将有且仅有一个代表这个终端应用的Grain。

  3. 使用 Parallel.For 模拟并发

  4. ParallelLoopResult.IsCompleted 只是返回所有循环任务创建完毕,并不代表循环的内部任务执行完毕。

6. 启动第一个 Orleans 应用

先启动 Orleans.Server再启动 Orleans.Client

从上面的运行结果来看,模拟两个终端10000个用户的并发登录,最终输出的活动用户数量均为10000个。回顾整个实现,并没有用到诸如锁、并发集合等避免并发导致的线程安全问题,但却输出正确的期望结果,这就正好说明了Orleans强大的并发控制特性。

  
  
  1. public class SessionControlGrain : Grain, ISessionControlGrain

  2. {

  3. // 未使用并发集合

  4. private List<string> LoginUsers { get; set; } = new List<string>();


  5. public Task Login(string userId)

  6. {

  7. //获取当前Grain的身份标识(因为ISessionControlGrain身份标识为string类型,GetPrimaryKeyString());

  8. var appName = this.GetPrimaryKeyString();


  9. LoginUsers.Add(userId);//未加锁


  10. Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}");

  11. return Task.CompletedTask;

  12. }

  13. ....

  14. }

7. 小结

通过简单的演示,想必你对Orleans的编程实现有了基本的认知,并体会到其并发控制的强大之处。这只是简单的入门演练,Orleans很多强大的特性,后续再结合具体场景进行详细阐述。源码已上传至GitHub:Hello.Orleans


本文分享自微信公众号 - DotNet技术平台(DotNetCore_Moments)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!