Orleans Klienty

Klient umožňuje interakci s clusterem bez odstupňovaného Orleans kódu. Klienti umožňují kódu aplikace komunikovat s zrnky a datovými proudy hostovanými v clusteru. Klienta můžete získat dvěma způsoby v závislosti na tom, kde je kód klienta hostovaný: ve stejném procesu jako sil nebo v samostatném procesu. Tento článek popisuje obě možnosti, počínaje doporučenou možností: spoluhostit kód klienta ve stejném procesu jako kód odstupňovaného kódu.

Spolu hostovaní klienti

Pokud je kód klienta hostovaný ve stejném procesu jako kód odstupňovaného kódu, klient se dá získat přímo z kontejneru injektáže závislostí hostitelské aplikace. V takovém případě klient komunikuje přímo se silem, ke kterému je připojený, a může využít dodatečné znalosti, které má silo o clusteru.

To poskytuje několik výhod, včetně snížení režie sítě a procesoru a snížení latence a zvýšení propustnosti a spolehlivosti. Klient využívá znalosti topologie a stavu clusteru silo a nepotřebuje používat samostatnou bránu. Tím se vyhnete síťovému směrování a serializaci nebo deserializaci. Tím se také zvyšuje spolehlivost, protože počet požadovaných uzlů mezi klientem a agregačním intervalem je minimalizovaný. Pokud se jedná o bezstavový pracovní interval nebo se jinak aktivuje na silu, kde je klient hostovaný, není potřeba provádět serializaci ani síťovou komunikaci a klient může získat další zvýšení výkonu a spolehlivosti. Spoluhostování klientského a odstupňovaného kódu také zjednodušuje nasazení a topologii aplikací tím, že eliminuje potřebu nasazení a monitorování dvou různých binárních souborů aplikací.

K tomuto přístupu existují také traktory, především proto, že kód zrnitosti už není izolovaný od procesu klienta. Problémy v klientském kódu, jako je blokování vstupně-výstupních operací nebo kolize zámků, které způsobují hladové vlákno, můžou mít vliv na výkon odstupňovaného kódu. I bez vad kódu, jako je výše uvedené, mohou efekty hlučného souseda vést jednoduše tak, že se klientský kód spustí na stejném procesoru jako kód zrnitosti, což obecně zatíží mezipaměť procesoru a další kolize místních prostředků. Kromě toho je identifikace zdroje těchto problémů teď obtížnější, protože systémy monitorování nemohou rozlišit, co je logicky klientský kód od odstupňovaného kódu.

I přes tyto traktory je spoluhostování klientského kódu s odstupňovaným kódem oblíbenou možností a doporučeným přístupem pro většinu aplikací. Chcete-li vypracovat, výše uvedené traktory jsou minimální v praxi z následujících důvodů:

  • Klientský kód je často velmi tenký, například překlad příchozích požadavků HTTP do volání odstupňovaného volání, a proto efekty hlučného souseda jsou minimální a srovnatelné s náklady na jinak požadovanou bránu.
  • Pokud dojde k problému s výkonem, typický pracovní postup pro vývojáře zahrnuje nástroje, jako jsou profilátory procesoru a ladicí programy, které jsou stále efektivní při rychlé identifikaci zdroje problému, i když má spuštěný klientský i odstupňovaný kód ve stejném procesu. Jinými slovy, metriky se stávají hrubějšími a méně schopnými přesně identifikovat zdroj problému, ale podrobnější nástroje jsou stále efektivní.

Získání klienta z hostitele

Pokud hostování pomocí obecného hostitele .NET, klient bude k dispozici v kontejneru injektáže závislostí hostitele automaticky a může být vložen do služeb, jako jsou kontrolery ASP.NET nebo IHostedService implementace.

Případně lze klientské rozhraní, jako IGrainFactory je nebo IClusterClient lze získat z ISiloHost:

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

Externí klienti

Klientský kód může běžet mimo Orleans cluster, kde je hostovaný kód odstupňovaného intervalu. Externí klient proto funguje jako konektor nebo konduita clusteru a všechna zrnka aplikace. Klienti se obvykle používají na front-endových webových serverech pro připojení ke Orleans clusteru, který slouží jako střední vrstva s zrnami provádějícími obchodní logiku.

V typickém nastavení front-endový webový server:

  • Přijme webový požadavek.
  • Provádí nezbytné ověřování a ověřování autorizace.
  • Určuje, které agregační intervaly mají požadavek zpracovat.
  • Používá Microsoft .Orleans. Balíček NuGet klienta pro volání jedné nebo více metod do agregací.
  • Zpracovává úspěšné dokončení nebo selhání volání agregace a všechny vrácené hodnoty.
  • Odešle odpověď na webový požadavek.

Inicializace odstupňovaného klienta

Než bude možné použít odstupňovaný klient pro volání zrn hostovaných v clusteru Orleans , je potřeba ho nakonfigurovat, inicializovat a připojit ke clusteru.

Konfigurace je poskytována prostřednictvím UseOrleansClient a několika doplňkových tříd možností, které obsahují hierarchii vlastností konfigurace pro programovou konfiguraci klienta. Další informace naleznete v tématu Konfigurace klienta.

Představte si následující příklad konfigurace 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();

host Po spuštění se klient nakonfiguruje a zpřístupní prostřednictvím vytvořené instance poskytovatele služeb.

Konfigurace je poskytována prostřednictvím ClientBuilder a několika doplňkových tříd možností, které obsahují hierarchii vlastností konfigurace pro programovou konfiguraci klienta. Další informace naleznete v tématu Konfigurace klienta.

Příklad konfigurace 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();

Nakonec je potřeba volat Connect() metodu na vytvořeném klientském objektu, aby se připojil ke clusteru Orleans . Jedná se o asynchronní metodu Task, která vrací . Takže musíte počkat na jeho dokončení s nebo await.Wait().

await client.Connect();

Volání zrn

Provádění volání z klienta se nijak neliší od provádění takových volání v rámci odstupňovaného kódu. Stejná IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) metoda, kde T je rozhraním cílového agregace, se používá v obou případech k získání odkazů na agregační intervaly. Rozdíl je v tom, co objekt továrny je vyvolán IGrainFactory.GetGrain. V kódu klienta to uděláte prostřednictvím připojeného objektu klienta, jak ukazuje následující příklad:

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

await joinGameTask;

Volání metody zrnitosti vrátí Task nebo podle Task<TResult> požadavků pravidel rozhraní pro agregační intervaly. Klient může pomocí klíčového await slova asynchronně očekávat vrácené Task bez blokování vlákna nebo v některých případech Wait() metoda blokovat aktuální vlákno spuštění.

Hlavním rozdílem mezi voláním zrn z klientského kódu a z jiného zrnka je model provádění zrn s jedním vláknem. Zrna jsou omezena na jednovláknové moduly Orleans runtime, zatímco klienti mohou být vícevláknové. Orleans neposkytuje žádnou takovou záruku na straně klienta, a proto je na klientovi, aby spravovala svou souběžnost pomocí jakýchkoli konstruktorů synchronizace, které jsou vhodné pro své prostředí – zámky, události a Tasks.

Příjem oznámení

Existují situace, kdy jednoduchý vzor odpovědi na požadavek nestačí a klient potřebuje přijímat asynchronní oznámení. Uživatel může například chtít být upozorněn, když někdo publikuje novou zprávu, kterou sleduje.

Použití pozorovatelů je jedním z takových mechanismů, které umožňují vystavit objekty na straně klienta jako cíle podobné zrnitosti, aby se vyvolaly zrnky. Volání pozorovatelů neposkytuje žádné informace o úspěchu nebo selhání, protože se odesílají jako jednosměrná zpráva s nejlepším úsilím. Proto je odpovědností kódu aplikace vytvořit mechanismus spolehlivosti vyšší úrovně nad pozorovateli, pokud je to potřeba.

Dalším mechanismem, který lze použít k doručování asynchronních zpráv klientům, je Toky. Toky zveřejnit informace o úspěchu nebo selhání doručení jednotlivých zpráv, a proto umožnit spolehlivou komunikaci zpět klientovi.

Možnosti připojení klienta

Existují dva scénáře, ve kterých může dojít k problémům s připojením klienta clusteru:

  • Když se klient pokusí připojit k silu.
  • Při volání odkazů na agregační intervaly, které byly získány z připojeného klienta clusteru.

V prvním případě se klient pokusí připojit k silu. Pokud se klient nemůže připojit k žádnému silu, vyvolá výjimku, která indikuje, co se nepovedlo. Můžete zaregistrovat, IClientConnectionRetryFilter jestli se má výjimka zpracovat, a rozhodnout se, jestli se má opakovat nebo ne. Pokud není k dispozici žádný filtr opakování nebo pokud se filtr opakování vrátí false, klient se v dobrém stavu vrátí.

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

Existují dva scénáře, ve kterých může dojít k problémům s připojením klienta clusteru:

  • Při počátečním zavolání IClusterClient.Connect() metody.
  • Při volání odkazů na agregační intervaly, které byly získány z připojeného klienta clusteru.

V prvním případě metoda Connect vyvolá výjimku, která indikuje, co se nepovedlo. To je obvykle (ale ne nutně) a SiloUnavailableException. V takovém případě je instance klienta clusteru nepoužitelná a měla by být odstraněna. Funkci filtru opakování lze volitelně zadat metodě Connect , která může například čekat na zadanou dobu před provedením dalšího pokusu. Pokud není k dispozici žádný filtr opakování nebo pokud se filtr opakování vrátí false, klient se v dobrém stavu vrátí.

Pokud Connect se klient clusteru úspěšně vrátí, je zaručeno, že bude použitelný, dokud nebude uvolněn. To znamená, že i když u klienta dochází k problémům s připojením, pokusí se obnovit neomezeně dlouho. Přesné chování obnovení lze nakonfigurovat u objektu GatewayOptions poskytovaného ClientBuilderobjektem, například:

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

V druhém případě, kdy dojde k problému s připojením během volání agregačního intervalu, SiloUnavailableException se na straně klienta vyvolá chyba. To se dá zpracovat takto:

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

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

Odkaz na agregační interval není v této situaci neplatný; volání by se mohlo opakovat na stejném odkazu později, když bylo připojení znovu navázáno.

Injektáž závislostí

Doporučeným způsobem, jak vytvořit externího klienta v programu, který používá .NET Generic Host, je vložit IClusterClient instanci singleton prostřednictvím injektáže závislostí, která se pak dá přijmout jako parametr konstruktoru v hostovaných službách, ASP.NET kontrolery atd.

Poznámka:

Při spoluhostování Orleans sila ve stejném procesu, ke kterému se připojíte, není nutné ručně vytvořit klienta; Orleans automaticky poskytne jeden a bude spravovat jeho životnost odpovídajícím způsobem.

Při připojování ke clusteru v jiném procesu (na jiném počítači) je běžným vzorem vytvoření hostované služby takto:

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

Služba se pak zaregistruje takto:

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

Příklad

Zde je rozšířená verze výše uvedeného příkladu klientské aplikace, která se připojuje k Orleans, najde účet hráče, přihlásí se k odběru aktualizací herní relace, na kterou je hráč součástí pozorovatele, a vytiskne oznámení, dokud program ručně neukončí.

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