Orleans简单使用

.NET7更新后Orleans也随着更新了一个大版本,但是感觉微软官方的文档并不是很好,这里写个小demo来演示简单的集群管理(本次使用redis,官方文档甚至都没有提过redis),可以参考Orleans 中的群集管理

创建项目

创建文件夹

mkdir Client
mkdir Server
mkdir Grain
mkdir IGrain
mkdir Model

初始化代码

cd Server
dotnet new webapi -minimal
cd ../Client
dotnet new console
cd ../Grain
dotnet new classlib
cd ../IGrain
dotnet new classlib
cd ../Model
dotnet new classlib

添加项目引用

cd ../Server
dotnet add reference ../Grain/Grain.csproj
cd ../Client
dotnet add reference ../IGrain/IGrain.csproj
cd ../IGrain
dotnet add reference ../Model/Model.csproj
cd ../Grain
dotnet add reference ../IGrain/IGrain.csproj

添加包引用

cd ../Server
dotnet add package Microsoft.Orleans.Server
dotnet add package Orleans.Clustering.Redis
dotnet add package Orleans.Persistence.Redis
cd ../Client
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Configuration.CommandLine
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
dotnet add package Microsoft.Orleans.Client
dotnet add package Orleans.Clustering.Redis
cd ../Model
dotnet add package Microsoft.Orleans.Sdk
cd ../IGrain
dotnet add package Microsoft.Orleans.Sdk
cd ../Grain
dotnet add package Microsoft.Orleans.Sdk

添加代码

Model:

//File:Message
namespace Model;
[GenerateSerializer]
public class Message
{
    [Id(0)]
    public string Ip { get; set; }
}

IGrain:

//File:IMessageHandler.cs
using Model;
namespace IGrain;
public interface IMessageHandler : IGrainWithIntegerKey
{
    ValueTask<string> Send(Message message);
}

Grain:

//File:MessageHandler.cs
using IGrain;
using Model;
namespace Grain;
public class MessageHandler : IMessageHandler
{
    public ValueTask<string> Send(Message message)
    {
        Console.WriteLine($"[DateTime] {DateTime.Now} [Message:Ip] {message.Ip}");
        return ValueTask.FromResult("ok");
    }
}

Server:

//File:Program.cs
using Orleans.Hosting;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseOrleans((ctx, orleansBuilder) =>
{
    var redisAddress = builder.Configuration["redisHost"];
    orleansBuilder.UseRedisClustering(options => options.ConnectionString = redisAddress);
    orleansBuilder.AddRedisGrainStorage("votes", options => options.ConnectionString = redisAddress);
});
var app = builder.Build();
app.Run();

Client:

//File:Program.cs
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using IGrain;
using System.Net;
var config = new ServiceCollection()
    .AddSingleton<IConfiguration>(serviceProvider =>
    {
        IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.AddEnvironmentVariables();
        if (args != null)
        {
            configurationBuilder.AddCommandLine(args);
        }
        return configurationBuilder.Build();
    })
    .BuildServiceProvider().GetService<IConfiguration>();
Thread.Sleep(5000);//docker直接跑的话,有可能Server没跑起来Client就已经准备去连接了
try
{
    IHost host = await StartClientAsync(config["redisHost"] ?? "127.0.0.1:6379");
    IClusterClient client = host.Services.GetRequiredService<IClusterClient>();
    //下面做一些事情
    await DoClientWorkAsync(client);
    while (true)
    {
        await Task.Delay(5000);
        await DoClientWorkAsync(client);
    }
    await host.StopAsync();
    return 0;
}
catch (Exception e)
{
    Console.WriteLine($$"""
        Exception while trying to run client: {{e.Message}}
        Make sure the silo the client is trying to connect to is running.
        Press any key to exit.
        """);
    return 1;
}

static async Task<IHost> StartClientAsync(string redisHost)
{
    var redisAddress = redisHost;
    var builder = new HostBuilder()
        .UseOrleansClient(client =>
        {
            client.UseRedisClustering(options => options.ConnectionString = redisAddress);
        })
        .ConfigureLogging(logging => logging.AddConsole());

    var host = builder.Build();
    await host.StartAsync();
    Console.WriteLine("Client successfully connected to silo host \n");
    return host;
}

static async Task DoClientWorkAsync(IClusterClient client)
{
    var friend = client.GetGrain<IMessageHandler>(0);
    var result = await friend.Send(new Model.Message
    {
        Ip = Dns.GetHostName()
    });
}

项目发布

publish

在根目录创建publish.bat

dotnet publish Server -o server_publish
dotnet publish Client -o client_publish

执行./publish

dockerfile

dockerfile_client:

FROM mcr.microsoft.com/dotnet/aspnet:7.0
COPY client_publish /app
WORKDIR /app
EXPOSE 80/tcp
ENTRYPOINT ["dotnet", "Client.dll"]

dockerfile_server

FROM mcr.microsoft.com/dotnet/aspnet:7.0
COPY server_publish /app
WORKDIR /app
EXPOSE 80/tcp
ENTRYPOINT ["dotnet", "Server.dll"]

docker-compose

version: '3'
services:
  redis:
    image: redis:latest
  server:
    build:
      context: ./
      dockerfile: ./dockerfile_server
    depends_on:
      - redis
    environment:
      - redisHost=redis:6379
  client:
    build:
      context: ./
      dockerfile: ./dockerfile_client
    depends_on:
      - server
    environment:
      - redisHost=redis:6379

执行docker compose up --scale client=3 --scale server=2

效果展示


我们关闭server2后,发现他会自动选举别的server