Orleans

Klient umożliwia interakcję kodu innego niż ziarno z klastrem Orleans . Klienci umożliwiają kodowi aplikacji komunikowanie się z ziarnami i strumieniami hostowanymi w klastrze. Istnieją dwa sposoby uzyskiwania klienta, w zależności od tego, gdzie jest hostowany kod klienta: w tym samym procesie co silos lub w osobnym procesie. W tym artykule omówiono obie opcje, począwszy od zalecanej opcji: współgospodarzem kodu klienta w tym samym procesie co kod ziarna.

Współgospodarzeni klienci

Jeśli kod klienta jest hostowany w tym samym procesie co kod ziarna, klient można uzyskać bezpośrednio z kontenera iniekcji zależności aplikacji hostingu. W takim przypadku klient komunikuje się bezpośrednio z silosem, do którego jest dołączony, i może skorzystać z dodatkowej wiedzy, że silos ma o klastrze.

Zapewnia to kilka korzyści, w tym zmniejszenie obciążenia sieciowego i procesora CPU, a także zmniejszenie opóźnienia i zwiększenie przepływności i niezawodności. Klient korzysta z wiedzy silosu o topologii i stanie klastra i nie musi używać oddzielnej bramy. Pozwala to uniknąć przeskoku sieciowego i serializacji/deserializacji w obie strony. W związku z tym zwiększa to również niezawodność, ponieważ liczba wymaganych węzłów między klientem a ziarnem jest zminimalizowana. Jeśli ziarno jest bezstanowym ziarnem procesu roboczego lub w inny sposób jest aktywowane na silosie, w którym jest hostowany klient, nie trzeba w ogóle wykonywać serializacji ani komunikacji sieciowej, a klient może czerpać dodatkowe korzyści z wydajności i niezawodności. Współgospodarz klienta i kod ziarna upraszcza również topologię wdrażania i aplikacji, eliminując konieczność wdrażania i monitorowania dwóch odrębnych plików binarnych aplikacji.

Istnieją również przeciwnicy tego podejścia, przede wszystkim, że kod ziarna nie jest już odizolowany od procesu klienta. W związku z tym problemy w kodzie klienta, takie jak blokowanie operacji we/wy lub rywalizacja o blokadę powodującą głód wątku, mogą mieć wpływ na wydajność kodu ziarna. Nawet bez wad kodu, takich jak wyżej wymienione, hałaśliwe efekty sąsiada mogą spowodować po prostu wykonanie kodu klienta na tym samym procesorze co kod ziarna, wprowadzenie dodatkowego obciążenia pamięci podręcznej procesora CPU i dodatkowe rywalizacji o zasoby lokalne w ogóle. Ponadto zidentyfikowanie źródła tych problemów jest teraz trudniejsze, ponieważ systemy monitorowania nie mogą odróżnić kodu logicznego klienta od kodu ziarna.

Pomimo tych przeciwników współgospodarzeń kod klienta z kodem ziarna jest popularną opcją i zalecanym podejściem dla większości aplikacji. Aby opracować, wyżej wymienionych przeciwników są minimalne w praktyce z następujących powodów:

  • Kod klienta jest często bardzo cienki, na przykład tłumaczenie przychodzących żądań HTTP na wywołania ziarna, a zatem efekty hałaśliwego sąsiada są minimalne i porównywalne w kosztach dla bramy wymaganej w inny sposób.
  • Jeśli wystąpi problem z wydajnością, typowy przepływ pracy dla dewelopera obejmuje narzędzia, takie jak profileery procesora CPU i debugery, które są nadal skuteczne w szybkim identyfikowaniu źródła problemu, mimo że zarówno klient, jak i kod ziarna są wykonywane w tym samym procesie. Innymi słowy, metryki stają się bardziej grube i mniej zdolne do dokładnego identyfikowania źródła problemu, ale bardziej szczegółowe narzędzia są nadal skuteczne.

Uzyskiwanie klienta z hosta

W przypadku hostowania przy użyciu hosta ogólnego platformy .NET klient będzie automatycznie dostępny w kontenerze wstrzykiwania zależności hosta i może zostać automatycznie wstrzyknięty do usług, takich jak kontrolery ASP.NET lub IHostedService implementacje.

Alternatywnie interfejs klienta, taki jak IGrainFactory lub IClusterClient można uzyskać z ISiloHostprogramu :

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

Klienci zewnętrzni

Kod klienta może działać poza klastrem Orleans , w którym jest hostowany kod ziarna. W związku z tym klient zewnętrzny działa jako łącznik lub kanał do klastra i wszystkie ziarna aplikacji. Zazwyczaj klienci są używane na serwerach internetowych frontonu do łączenia się z klastrem Orleans , który służy jako warstwa środkowa z ziarnami wykonującymi logikę biznesową.

W typowej konfiguracji serwer internetowy frontonu:

  • Odbiera żądanie internetowe.
  • Wykonuje niezbędne uwierzytelnianie i walidację autoryzacji.
  • Decyduje, które ziarna powinny przetworzyć żądanie.
  • Używa microsoft .Orleans. Pakiet NuGet klienta , aby wykonać co najmniej jedno wywołanie metody do ziarna.
  • Obsługuje pomyślne ukończenie lub błędy wywołań ziarna i wszystkie zwrócone wartości.
  • Wysyła odpowiedź do żądania internetowego.

Inicjowanie klienta ziarna

Zanim klient ziarna może służyć do wykonywania wywołań do ziarna hostowanego w Orleans klastrze, musi być skonfigurowany, zainicjowany i połączony z klastrem.

Konfiguracja jest udostępniana za pośrednictwem UseOrleansClient kilku dodatkowych klas opcji, które zawierają hierarchię właściwości konfiguracji programowych konfigurowania klienta. Aby uzyskać więcej informacji, zobacz Konfiguracja klienta.

Rozważmy następujący przykład konfiguracji klienta:

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

Po uruchomieniu host klient zostanie skonfigurowany i dostępny za pośrednictwem jego skonstruowanego wystąpienia dostawcy usług.

Konfiguracja jest udostępniana za pośrednictwem ClientBuilder kilku dodatkowych klas opcji, które zawierają hierarchię właściwości konfiguracji programowych konfigurowania klienta. Aby uzyskać więcej informacji, zobacz Konfiguracja klienta.

Przykład konfiguracji klienta:

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

Na koniec należy wywołać Connect() metodę na skonstruowanym obiekcie klienta, aby nawiązać połączenie z klastrem Orleans . Jest to metoda asynchroniczna, która zwraca Taskwartość . Dlatego musisz poczekać na jego zakończenie za pomocą elementu await lub .Wait().

await client.Connect();

Nawiązywanie wywołań do ziarna

Wykonywanie wywołań do ziarna od klienta nie różni się od wykonywania takich wywołań z poziomu kodu ziarna. Ta sama IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) metoda, gdzie T jest interfejsem ziarna docelowego, jest używana w obu przypadkach do uzyskiwania odwołań ziarna. Różnica polega na tym, jaki obiekt fabryki jest wywoływany IGrainFactory.GetGrain. W kodzie klienta można to zrobić za pośrednictwem połączonego obiektu klienta, jak pokazano w poniższym przykładzie:

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

await joinGameTask;

Wywołanie metody ziarna zwraca wartość Task lub zgodnie z Task<TResult> wymaganiami reguł interfejsu ziarna. Klient może użyć słowa kluczowego await , aby asynchronicznie oczekiwać na zwrócony Task bez blokowania wątku lub w niektórych przypadkach Wait() metoda, aby zablokować bieżący wątek wykonywania.

Główną różnicą między wykonywaniem wywołań do ziarna z kodu klienta i z innego ziarna jest model wykonywania jednowątkowego ziarna. Ziarna są ograniczone do pojedynczego wątku Orleans przez środowisko uruchomieniowe, podczas gdy klienci mogą być wielowątkowy. Orleans nie zapewnia żadnej takiej gwarancji po stronie klienta, dlatego klient musi zarządzać współbieżnością przy użyciu wszelkich konstrukcji synchronizacji odpowiednich dla środowiska — blokad, zdarzeń i Tasks.

Odbieranie powiadomień

Istnieją sytuacje, w których prosty wzorzec odpowiedzi na żądanie jest niewystarczający, a klient musi otrzymywać powiadomienia asynchroniczne. Na przykład użytkownik może chcieć otrzymywać powiadomienia, gdy nowa wiadomość została opublikowana przez osobę, którą obserwuje.

Użycie obserwatorów jest jednym z takich mechanizmów , który umożliwia uwidacznianie obiektów po stronie klienta jako obiektów docelowych przypominających ziarna w celu wywołania przez ziarna. Wywołania obserwatorów nie wskazują na powodzenie lub niepowodzenie, ponieważ są wysyłane jako jednokierunkowy komunikat o najlepszym wysiłku. W związku z tym kod aplikacji jest odpowiedzialny za utworzenie mechanizmu niezawodności wyższego poziomu na podstawie obserwatorów w razie potrzeby.

Innym mechanizmem, który może służyć do dostarczania komunikatów asynchronicznych klientom, jest Strumienie. Strumienie uwidacznia wskazania powodzenia lub niepowodzenia dostarczania poszczególnych komunikatów, a tym samym umożliwiają niezawodną komunikację z klientem.

Łączność z klientem

Istnieją dwa scenariusze, w których klient klastra może napotykać problemy z łącznością:

  • Gdy klient próbuje nawiązać połączenie z silosem.
  • Podczas wykonywania wywołań odwołań ziarna uzyskanych z połączonego klienta klastra.

W pierwszym przypadku klient podejmie próbę nawiązania połączenia z silosem. Jeśli klient nie może nawiązać połączenia z żadnym silosem, zgłosi wyjątek wskazujący, co poszło nie tak. Możesz zarejestrować obiekt IClientConnectionRetryFilter w celu obsługi wyjątku i zdecydować, czy ponowić próbę, czy nie. Jeśli nie podano filtru ponawiania prób lub jeśli filtr ponawiania zostanie zwrócony false, klient poda się na dobre.

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

Istnieją dwa scenariusze, w których klient klastra może napotykać problemy z łącznością:

  • Gdy metoda jest wywoływana IClusterClient.Connect() początkowo.
  • Podczas wykonywania wywołań odwołań ziarna uzyskanych z połączonego klienta klastra.

W pierwszym przypadku metoda zgłosi wyjątek wskazujący, Connect co poszło nie tak. Zazwyczaj jest to (ale niekoniecznie) SiloUnavailableException. W takim przypadku wystąpienie klienta klastra jest bezużyteczne i powinno zostać usunięte. Opcjonalnie można udostępnić funkcję filtru ponawiania prób do Connect metody, która może na przykład poczekać określony czas trwania przed podjęciem kolejnej próby. Jeśli nie podano filtru ponawiania prób lub jeśli filtr ponawiania zostanie zwrócony false, klient poda się na dobre.

W przypadku Connect pomyślnego zwrotu klient klastra ma gwarancję, że będzie można go używać do momentu usunięcia. Oznacza to, że nawet jeśli klient napotyka problemy z połączeniem, podejmie próbę odzyskania na czas nieokreślony. Dokładne zachowanie odzyskiwania można skonfigurować dla obiektu dostarczonego GatewayOptionsClientBuilderprzez obiekt , np.:

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

W drugim przypadku, gdy podczas wywołania ziarna występuje problem z połączeniem, SiloUnavailableException element zostanie zgłoszony po stronie klienta. Może to być obsługiwane w następujący sposób:

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

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

Odwołanie do ziarna nie jest unieważniane w tej sytuacji; wywołanie może zostać ponowione w tym samym odwołaniu później, gdy połączenie mogło zostać ponownie nawiązane.

Wstrzykiwanie zależności

Zalecanym sposobem utworzenia klienta zewnętrznego w programie, który używa hosta ogólnego platformy .NET, jest wstrzyknięcie pojedynczego IClusterClient wystąpienia za pośrednictwem wstrzykiwania zależności, które można następnie zaakceptować jako parametr konstruktora w hostowanych usługach, kontrolery ASP.NET itd.

Uwaga

W przypadku współgospodarzowania silosu Orleans w tym samym procesie, który będzie się z nim łączyć, nie jest konieczne ręczne utworzenie klienta; Orleans automatycznie zapewni jeden i odpowiednio zarządza jego okresem istnienia.

Podczas nawiązywania połączenia z klastrem w innym procesie (na innej maszynie) typowym wzorcem jest utworzenie hostowanej usługi w następujący sposób:

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

Usługa jest następnie zarejestrowana w następujący sposób:

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

Przykład

Poniżej przedstawiono rozszerzoną wersję przykładu podaną powyżej aplikacji klienckiej, która łączy się z Orleansprogramem , znajduje konto gracza, subskrybuje aktualizacje sesji gry, z którą gracz jest częścią obserwatora i wyświetla powiadomienia do momentu ręcznego zakończenia programu.

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