奥尔良的用例极简,速度缓慢

Mur*_*ock 2 .net c# actor azure-table-storage orleans

我正在评估奥尔良的一个我们即将开始的新项目。

最终我们想要运行一群持久的演员,但我目前正在努力让奥尔良的内存版本的基线变得高性能。

给定以下颗粒

using Common.UserWallet;
using Common.UserWallet.Messages;
using Microsoft.Extensions.Logging;

namespace Grains;

public class UserWalletGrain : Orleans.Grain, IUserWalletGrain
{
    private readonly ILogger _logger;

    public UserWalletGrain(ILogger<UserWalletGrain> logger)
    {
        _logger = logger;
    }

    public async Task<CreateOrderResponse> CreateOrder(CreateOrderCommand command)
    {


        return new CreateOrderResponse(Guid.NewGuid());
    }

    public Task Ping()
    {
        return Task.CompletedTask;
    }
}
Run Code Online (Sandbox Code Playgroud)

以下筒仓配置:

static async Task<IHost> StartSiloAsync()
{
    ServicePointManager.UseNagleAlgorithm = false;

    var builder = new HostBuilder()
        .UseOrleans(c =>
        {
            c.UseLocalhostClustering()
            .Configure<ClusterOptions>(options =>
            {
                options.ClusterId = "dev";
                options.ServiceId = "OrleansBasics";
            })
            .ConfigureApplicationParts(
                parts => parts.AddApplicationPart(typeof(HelloGrain).Assembly).WithReferences())

            .AddMemoryGrainStorage("OrleansMemoryProvider");
        });

    var host = builder.Build();
    await host.StartAsync();

    return host;
}
Run Code Online (Sandbox Code Playgroud)

以及以下客户端代码:

static async Task<IClusterClient> ConnectClientAsync()
{
    var client = new ClientBuilder()
        .UseLocalhostClustering()
        .Configure<ClusterOptions>(options =>
        {
            options.ClusterId = "dev";
            options.ServiceId = "OrleansBasics";
        })
        //.ConfigureLogging(logging => logging.AddConsole())
        .Build();

    await client.Connect();
    Console.WriteLine("Client successfully connected to silo host \n");

    return client;
}

static async Task DoClientWorkAsync(IClusterClient client)
{
    List<IUserWalletGrain> grains = new List<IUserWalletGrain>();

    foreach (var _ in Enumerable.Range(1, 100))
    {
        var walletGrain = client.GetGrain<IUserWalletGrain>(Guid.NewGuid());
        await walletGrain.Ping(); //make sure grain is loaded
        grains.Add(walletGrain);
    }

    var sw = Stopwatch.StartNew();
    await Parallel.ForEachAsync(Enumerable.Range(1, 100000), async (o, token) =>
    {
        var command = new Common.UserWallet.Messages.CreateOrderCommand(Guid.NewGuid(), 4, 5, new List<Guid> { Guid.NewGuid(), Guid.NewGuid() });

        var response = await grains[o % 100].CreateOrder(command);

        Console.WriteLine($"{o%10}:{o}");
    });

    Console.WriteLine($"\nElapsed:{sw.ElapsedMilliseconds}\n\n");
}
Run Code Online (Sandbox Code Playgroud)

我可以在 30 秒内发送 100,000 条消息。相当于每秒约 3333 条消息。这比我在查看时所期望的要少得多(https://github.com/yevhen/Orleans.PingPong

我从 10 粒、100 粒或 1000 粒开始似乎并不重要。

当我添加配置表存储的持久性时

.AddAzureTableGrainStorage(
        name: "OrleansMemoryProvider",
        configureOptions: options =>
        {
            options.UseJson = true;
            options.ConfigureTableServiceClient(
                "secret);
        })
Run Code Online (Sandbox Code Playgroud)

还有一个单

等待 WriteStateAsync(); 在 CreateOrder 中,情况变得更加糟糕,大约为 280 条消息/秒

当我更进一步并实现一些基本的领域逻辑时。调用其他参与者等我们基本上以 1.2 条消息/秒的蜗牛速度

是什么赋予了?

编辑:

  • 我的CPU利用率大约是50%。

Reu*_*ond 10

构建高性能应用程序可能很棘手且微妙。奥尔良的一般解决方案是你有很多grain和很多调用者,这样你就可以实现高度的并发性和吞吐量。在您的情况下,您有很多颗粒(100),但调用者很少(我相信默认情况下每个核心都有一个Parallel.ForEachAsync),并且每个调用者在每次调用后都会写入控制台,这会大大减慢速度。

如果我删除Console.WriteLine并使用 Orleans 7.0-rc2 在我的机器上运行您的代码,则对 100 个grains 的 100K 次调用将在大约 850 毫秒内完成。如果我将CreateOrderRequest&CreateOrderResponse类型从类更改为结构,持续时间会减少到 750 毫秒。

如果我运行更优化的 ping 测试(来自 Orleans 存储库的测试),我会看到我的计算机上每秒大约有 550K 个请求,其中一个客户端和一个筒仓进程共享同一 CPU。对于 Orleans 3.x,这个数字大约是这个数字的一​​半。如果我在筒仓进程中共同托管客户端(即IClusterClient从筒仓中拉取IServiceProvider),那么我会看到每秒超过 500 万个请求。

一旦你开始在每个颗粒上做大量的工作,你就会开始遇到其他限制。我最近测试了从同一进程中调用单个grain,发现如果一个grain正在做琐碎的工作(乒乓球),它可以处理500K RPS。如果grain必须在每个请求上写入存储并且每个存储写入需要1毫秒,那么它将无法处理超过1000 RPS,因为默认情况下每个调用都会等待前一个调用完成。如果您想选择退出该行为,可以通过启用grain的重入性来实现,如此处文档中所述:https: //learn.microsoft.com/en-us/dotnet/orleans/grains/reentrancy。Chirper 示例提供了有关如何通过存储更新实现重入的更多详细信息: https: //github.com/dotnet/orleans/tree/main/samples/Chirper

当grain方法变得更加复杂并且grain需要执行大量I/O来服务每个请求(例如,存储更新和后续grain调用)时,每个单独grain的吞吐量将会下降,因为每个请求涉及更多工作。希望上述数字能为您提供大概的指导。