Orleans

Клиент позволяет незернированному коду взаимодействовать с кластером Orleans . Клиенты позволяют коду приложения взаимодействовать с зернами и потоками, размещенными в кластере. Существует два способа получения клиента в зависимости от того, где размещается клиентский код: в том же процессе, что и в фрагменте, или в отдельном процессе. В этой статье рассматриваются оба варианта, начиная с рекомендуемого варианта: совместное размещение клиентского кода в том же процессе, что и код зерна.

Совместно размещенные клиенты

Если клиентский код размещен в том же процессе, что и код зерна, клиент может быть получен непосредственно из контейнера внедрения зависимостей приложения размещения. В этом случае клиент напрямую взаимодействует с хранилищем, к которому он подключен, и может воспользоваться дополнительными знаниями о кластере.

Это обеспечивает несколько преимуществ, включая сокращение нагрузки на сеть и ЦП, а также уменьшение задержки и повышение пропускной способности и надежности. Клиент использует знания silo о топологии кластера и состоянии и не должен использовать отдельный шлюз. Это позволяет избежать сетевого прыжка и сериализации/десериализации кругового пути. Это также повышает надежность, так как количество необходимых узлов между клиентом и зерном минимизируется. Если зерно является рабочая роль без отслеживания состояния или в противном случае будет активирована в хранилище, где размещается клиент, сериализация или сетевая связь не должна выполняться вообще, и клиент может получить дополнительные преимущества производительности и надежности. Совместное размещение клиента и кода зерна также упрощает развертывание и топологию приложений, устраняя необходимость развертывания и мониторинга двух отдельных двоичных файлов приложений.

К этому подходу также относятся недоброжелатели, в первую очередь, что код зерна больше не изолирован от клиентского процесса. Таким образом, проблемы в клиентском коде, такие как блокировка операций ввода-вывода или блокировки, что приводит к нехватке потока, может повлиять на производительность кода зерна. Даже без дефектов кода, как указано выше упоминание, шумные эффекты соседей могут привести просто путем выполнения клиентского кода на том же процессоре, что и код зерна, что и дополнительный нагрузку на кэш ЦП и дополнительные результаты для локальных ресурсов в целом. Кроме того, определение источника этих проблем в настоящее время сложнее, так как системы мониторинга не могут различать то, что логически клиентский код от кода зерна.

Несмотря на эти недоброжелатели, совместное размещение клиентского кода с кодом зерна является популярным вариантом и рекомендуемым подходом для большинства приложений. Для разработки указанные выше упоминание недоброжелатели являются минимальными на практике по следующим причинам:

  • Клиентский код часто очень тонкий, например перевод входящих HTTP-запросов в вызовы зерна, поэтому шумные соседские эффекты являются минимальными и сопоставимыми по стоимости с требуемым шлюзом.
  • Если возникает проблема с производительностью, типичный рабочий процесс для разработчика включает такие средства, как профилировщики ЦП и отладчики, которые по-прежнему эффективны при быстром определении источника проблемы, несмотря на то, что код клиента и зерна выполняются в одном процессе. Другими словами, метрики становятся более грубыми и менее способны точно определить источник проблемы, но более подробные средства по-прежнему эффективны.

Получение клиента из узла

При размещении с помощью универсального узла .NET клиент будет доступен в контейнере внедрения зависимостей узла автоматически и может быть внедрен в службы, такие как ASP.NET контроллеры или IHostedService реализации.

Кроме того, клиентский интерфейс, например IGrainFactory или IClusterClient из него можно получить:ISiloHost

var client = host.Services.GetService<IClusterClient>();
await client.GetGrain<IMyGrain>(0).Ping();

Внешние клиенты

Клиентский код может выполняться за пределами Orleans кластера, где размещен код зерна. Таким образом, внешний клиент выступает в качестве соединителя или канала в кластере и всех зерен приложения. Как правило, клиенты используются на внешних веб-серверах для подключения к Orleans кластеру, который служит средним уровнем с зернами, выполняющими бизнес-логику.

В типичной настройке интерфейсный веб-сервер:

  • Получает веб-запрос.
  • Выполняет необходимую проверку подлинности и авторизации.
  • Определяет, какие зерна должны обрабатывать запрос.
  • Использует Microsoft.Orleans. Пакет NuGet клиента для вызова одного или нескольких методов к зернам.
  • Обрабатывает успешное завершение или сбои вызовов зерна и любые возвращаемые значения.
  • Отправляет ответ на веб-запрос.

Инициализация клиента зерна

Прежде чем клиент зерна можно использовать для выполнения вызовов зерна, размещенных в кластере Orleans , его необходимо настроить, инициализировать и подключить к кластеру.

Конфигурация предоставляется с помощью UseOrleansClient нескольких дополнительных классов параметров, содержащих иерархию свойств конфигурации для программной настройки клиента. Дополнительные сведения см. в разделе "Конфигурация клиента".

Рассмотрим следующий пример конфигурации клиента:

// Alternatively, call Host.CreateDefaultBuilder(args) if using the 
// Microsoft.Extensions.Hosting NuGet package.
using IHost host = new HostBuilder()
    .UseOrleansClient(clientBuilder =>
    {
        clientBuilder.Configure<ClusterOptions>(options =>
        {
            options.ClusterId = "my-first-cluster";
            options.ServiceId = "MyOrleansService";
        });

        clientBuilder.UseAzureStorageClustering(
            options => options.ConfigureTableServiceClient(connectionString))
    })
    .Build();

host При запуске клиент будет настроен и доступен с помощью созданного экземпляра поставщика услуг.

Конфигурация предоставляется с помощью ClientBuilder нескольких дополнительных классов параметров, содержащих иерархию свойств конфигурации для программной настройки клиента. Дополнительные сведения см. в разделе "Конфигурация клиента".

Пример конфигурации клиента:

var client = new ClientBuilder()
    .Configure<ClusterOptions>(options =>
    {
        options.ClusterId = "my-first-cluster";
        options.ServiceId = "MyOrleansService";
    })
    .UseAzureStorageClustering(
        options => options.ConnectionString = connectionString)
    .ConfigureApplicationParts(
        parts => parts.AddApplicationPart(typeof(IValueGrain).Assembly))
    .Build();

Наконец, необходимо вызвать Connect() метод в созданном клиентском объекте, чтобы сделать его подключение к кластеру Orleans . Это асинхронный метод, который возвращает .Task Поэтому вам нужно ждать завершения с await помощью или .Wait().

await client.Connect();

Вызовы к зернам

Вызовы к зерню от клиента не отличаются от выполнения таких вызовов в коде зерна. Тот же IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) метод, где T является целевым интерфейсом зерна, используется в обоих случаях для получения ссылок на зерна. Разница заключается в том, какой объект фабрики вызывается IGrainFactory.GetGrain. В клиентском коде это выполняется с помощью подключенного клиентского объекта, как показано в следующем примере:

IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
Task joinGameTask = player.JoinGame(game)

await joinGameTask;

Вызов метода зерна возвращает или возвращается TaskTask<TResult> в соответствии с правилами интерфейса зерна. Клиент может использовать await ключевое слово для асинхронного ожидания возвращаемого Task потока, не блокируя поток или в некоторых случаях Wait() метод для блокировки текущего потока выполнения.

Основное различие между вызовами к зернам из клиентского кода и из другого зерна — это модель однопоточного выполнения зерна. Зерна ограничены одним потоком среды Orleans выполнения, в то время как клиенты могут быть многопоточными. Orleans не предоставляет никакой такой гарантии на стороне клиента, поэтому клиент может управлять его параллелизмом с помощью любых конструкций синхронизации, подходящих для своей среды— блокировки, события и Tasks.

Получение уведомлений

Существуют ситуации, в которых недостаточно простой шаблон ответа на запросы, и клиенту необходимо получать асинхронные уведомления. Например, пользователь может захотеть получать уведомления о публикации нового сообщения кем-то, что она выполняет.

Использование наблюдателей является одним из таких механизмов, позволяющих предоставлять клиентские объекты как целевые объекты, подобные зерне, вызываться зернами. Призывы к наблюдателям не указывают на успех или неудачу, так как они отправляются как односторонняя попытка. Поэтому код приложения несет ответственность за создание механизма надежности более высокого уровня на вершине наблюдателей, где это необходимо.

Другим механизмом, который можно использовать для доставки асинхронных сообщений клиентам, является Потоки. Потоки предоставлять признаки успеха или сбоя доставки отдельных сообщений и, следовательно, обеспечивают надежную связь с клиентом.

Клиентские подключения

Существует два сценария, в которых клиент кластера может столкнуться с проблемами с подключением:

  • Когда клиент пытается подключиться к silo.
  • При вызове ссылок на зерна, полученных от подключенного клиента кластера.

В первом случае клиент попытается подключиться к silo. Если клиент не может подключиться к любому сило, он вызовет исключение, чтобы указать, что пошло не так. Вы можете зарегистрировать IClientConnectionRetryFilter исключение и решить, следует ли повторить попытку или нет. Если фильтр повторных попыток не указан или если фильтр повторных попыток возвращается, клиент откажается false.

using Orleans.Runtime;

internal sealed class ClientConnectRetryFilter : IClientConnectionRetryFilter
{
    private int _retryCount = 0;
    private const int MaxRetry = 5;
    private const int Delay = 1_500;

    public async Task<bool> ShouldRetryConnectionAttempt(
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (_retryCount >= MaxRetry)
        {
            return false;
        }

        if (!cancellationToken.IsCancellationRequested &&
            exception is SiloUnavailableException siloUnavailableException)
        {
            await Task.Delay(++ _retryCount * Delay, cancellationToken);
            return true;
        }

        return false;
    }
}

Существует два сценария, в которых клиент кластера может столкнуться с проблемами с подключением:

  • При первоначальном вызове IClusterClient.Connect() метода.
  • При вызове ссылок на зерна, полученных от подключенного клиента кластера.

В первом случае метод вызовет исключение, указывающее, Connect что пошло не так. Обычно это (но не обязательно) SiloUnavailableException. В этом случае экземпляр клиента кластера непригоден для использования и должен быть удален. Функцию фильтра повторных попыток можно при необходимости предоставить Connect методу, который, например, может ожидать указанной длительности, прежде чем предпринять другую попытку. Если фильтр повторных попыток не указан или если фильтр повторных попыток возвращается, клиент откажается false.

При Connect успешном возвращении клиент кластера гарантированно будет использоваться до тех пор, пока он не будет удален. Это означает, что даже если клиент испытывает проблемы с подключением, он попытается восстановиться на неопределенный срок. Точное поведение восстановления можно настроить для объекта, предоставленного GatewayOptionsClientBuilderобъектом, например:

var client = new ClientBuilder()
    // ...
    .Configure<GatewayOptions>(
        options =>                         // Default is 1 min.
        options.GatewayListRefreshPeriod = TimeSpan.FromMinutes(10))
    .Build();

Во втором случае, когда во время вызова зерна возникает проблема с подключением, SiloUnavailableException создается на стороне клиента. Это может быть обработано так:

IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);

try
{
    await player.JoinGame(game);
}
catch (SiloUnavailableException)
{
    // Lost connection to the cluster...
}

Ссылка на зерно не является недопустимой в этой ситуации; Вызов может быть получен в той же ссылке позже, когда подключение может быть восстановлено.

Внедрение зависимостей

Рекомендуемый способ создания внешнего клиента в программе, которая использует универсальный узел .NET, заключается в внедрении IClusterClient однотонного экземпляра с помощью внедрения зависимостей, который затем можно принять в качестве параметра конструктора в размещенных службах, ASP.NET контроллерах и т. д.

Примечание.

При совместном размещении Orleans сило в том же процессе, который будет подключаться к нему, не обязательно вручную создать клиент; Orleans автоматически предоставит один и управляет его временем существования соответствующим образом.

При подключении к кластеру в другом процессе (на другом компьютере) обычно создается размещенная служба следующим образом:

using Microsoft.Extensions.Hosting;

namespace Client;

public sealed class ClusterClientHostedService : IHostedService
{
    private readonly IClusterClient _client;

    public ClusterClientHostedService(IClusterClient client)
    {
        _client = client;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Use the _client to consume grains...

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
public class ClusterClientHostedService : IHostedService
{
    private readonly IClusterClient _client;

    public ClusterClientHostedService(IClusterClient client)
    {
        _client = client;
    }

    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();
    }
}

Затем служба регистрируется следующим образом:

await Host.CreateDefaultBuilder(args)
    .UseOrleansClient(builder =>
    {
        builder.UseLocalhostClustering();
    })
    .ConfigureServices(services => 
    {
        services.AddHostedService<ClusterClientHostedService>();
    })
    .RunConsoleAsync();

Пример

Ниже приведена расширенная версия примера, приведенного выше клиентского приложения, к которому подключается Orleans, находит учетную запись игрока, подписывается на обновления игрового сеанса, который игрок является частью наблюдателя, и выводит уведомления до тех пор, пока программа не будет завершена вручную.

try
{
    using IHost host = Host.CreateDefaultBuilder(args)
        .UseOrleansClient((context, client) =>
        {
            client.Configure<ClusterOptions>(options =>
            {
                options.ClusterId = "my-first-cluster";
                options.ServiceId = "MyOrleansService";
            })
            .UseAzureStorageClustering(
                options => options.ConfigureTableServiceClient(
                    context.Configuration["ORLEANS_AZURE_STORAGE_CONNECTION_STRING"]));
        })
        .UseConsoleLifetime()
        .Build();

    await host.StartAsync();

    IGrainFactory client = host.Services.GetRequiredService<IGrainFactory>();

    // Hardcoded player ID
    Guid playerId = new("{2349992C-860A-4EDA-9590-000000000006}");
    IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
    IGameGrain? game = null;
    while (game is null)
    {
        Console.WriteLine(
            $"Getting current game for player {playerId}...");

        try
        {
            game = await player.GetCurrentGame();
            if (game is null) // Wait until the player joins a game
            {
                await Task.Delay(TimeSpan.FromMilliseconds(5_000));
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Exception: {ex.GetBaseException()}");
        }
    }

    Console.WriteLine(
        $"Subscribing to updates for game {game.GetPrimaryKey()}...");

    // Subscribe for updates
    var watcher = new GameObserver();
    await game.ObserveGameUpdates(
        client.CreateObjectReference<IGameObserver>(watcher));

    Console.WriteLine(
        "Subscribed successfully. Press <Enter> to stop.");
}
catch (Exception e)
{
    Console.WriteLine(
        $"Unexpected Error: {e.GetBaseException()}");
}
await RunWatcherAsync();

// Block the main thread so that the process doesn't exit.
// Updates arrive on thread pool threads.
Console.ReadLine();

static async Task RunWatcherAsync()
{
    try
    {
        var client = new ClientBuilder()
            .Configure<ClusterOptions>(options =>
            {
                options.ClusterId = "my-first-cluster";
                options.ServiceId = "MyOrleansService";
            })
            .UseAzureStorageClustering(
                options => options.ConnectionString = connectionString)
            .ConfigureApplicationParts(
                parts => parts.AddApplicationPart(typeof(IValueGrain).Assembly))
            .Build();

            // Hardcoded player ID
            Guid playerId = new("{2349992C-860A-4EDA-9590-000000000006}");
            IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
            IGameGrain game = null;
            while (game is null)
            {
                Console.WriteLine(
                    $"Getting current game for player {playerId}...");

                try
                {
                    game = await player.GetCurrentGame();
                    if (game is null) // Wait until the player joins a game
                    {
                        await Task.Delay(TimeSpan.FromMilliseconds(5_000));
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Exception: {ex.GetBaseException()}");
                }
            }

            Console.WriteLine(
                $"Subscribing to updates for game {game.GetPrimaryKey()}...");

            // Subscribe for updates
            var watcher = new GameObserver();
            await game.SubscribeForGameUpdates(
                await client.CreateObjectReference<IGameObserver>(watcher));

            Console.WriteLine(
                "Subscribed successfully. Press <Enter> to stop.");
        }
        catch (Exception e)
        {
            Console.WriteLine(
                $"Unexpected Error: {e.GetBaseException()}");
        }
    }
}

/// <summary>
/// Observer class that implements the observer interface.
/// Need to pass a grain reference to an instance of
/// this class to subscribe for updates.
/// </summary>
class GameObserver : IGameObserver
{
    public void UpdateGameScore(string score)
    {
        Console.WriteLine("New game score: {0}", score);
    }
}