Dela via


Orleans Klienter

En klient tillåter att icke-kornig kod interagerar med ett Orleans kluster. Klienter tillåter att programkod kommunicerar med korn och strömmar som finns i ett kluster. Det finns två sätt att hämta en klient, beroende på var klientkoden finns: i samma process som en silo eller i en separat process. I den här artikeln beskrivs båda alternativen, som börjar med det rekommenderade alternativet: samvärd för klientkoden i samma process som kornkoden.

Samvärdade klienter

Om klientkoden finns i samma process som kornkoden kan klienten hämtas direkt från värdprogrammets container för beroendeinmatning. I det här fallet kommunicerar klienten direkt med den silo som den är kopplad till och kan dra nytta av den extra kunskap som silon har om klustret.

Detta ger flera fördelar, inklusive att minska nätverks- och CPU-omkostnaderna samt minska svarstiden och öka dataflödet och tillförlitligheten. Klienten använder silons kunskaper om klustrets topologi och tillstånd och behöver inte använda en separat gateway. Detta undviker ett nätverkshopp och serialisering/deserialisering tur och retur. Detta ökar därför också tillförlitligheten eftersom antalet nödvändiga noder mellan klienten och kornet minimeras. Om kornet är ett tillståndslöst arbetskorn eller på annat sätt råkar aktiveras på silon där klienten finns, behöver ingen serialisering eller nätverkskommunikation utföras alls och klienten kan skörda ytterligare prestanda- och tillförlitlighetsvinster. Klient- och kornkod för samvärdering förenklar även distributions- och programtopologin genom att eliminera behovet av att två distinkta programbinärfiler distribueras och övervakas.

Det finns också belackare för den här metoden, främst att kornkoden inte längre är isolerad från klientprocessen. Därför kan problem i klientkoden, till exempel blockering av I/O eller låskonkurring som orsakar trådsvältning, påverka prestanda för kornkod. Även utan kodfel som ovan nämnda , kan bullriga granneffekter resultera helt enkelt genom att klientkoden körs på samma processor som kornkod, vilket ger ytterligare belastning på CPU-cachen och ytterligare konkurrens om lokala resurser i allmänhet. Dessutom är det nu svårare att identifiera källan till dessa problem eftersom övervakningssystem inte kan skilja vad som är logiskt klientkod från kornkod.

Trots dessa belackare är co-hosting-klientkod med kornkod ett populärt alternativ och den rekommenderade metoden för de flesta program. För att utveckla ovan nämnda belackare är minimala i praktiken av följande skäl:

  • Klientkoden är ofta mycket tunn, till exempel att översätta inkommande HTTP-begäranden till korniga anrop, och därför är de bullriga granneffekterna minimala och jämförbara i kostnad med den annars nödvändiga gatewayen.
  • Om ett prestandaproblem uppstår omfattar det typiska arbetsflödet för en utvecklare verktyg som CPU-profilerare och felsökningsprogram, som fortfarande är effektiva för att snabbt identifiera källan till problemet trots att både klient- och kornkod körs i samma process. Med andra ord blir måtten mer grova och mindre kapabla att exakt identifiera källan till ett problem, men mer detaljerade verktyg är fortfarande effektiva.

Hämta en klient från en värd

Om du är värd för att använda den generiska .NET-värden blir klienten automatiskt tillgänglig i värdens container för beroendeinmatning och kan matas in i tjänster som ASP.NET kontrollanter eller IHostedService implementeringar.

Alternativt kan ett klientgränssnitt som IGrainFactory eller IClusterClient kan hämtas från ISiloHost:

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

Externa klienter

Klientkoden kan köras utanför klustret Orleans där kornkod finns. Därför fungerar en extern klient som en anslutningsapp eller kanal till klustret och alla korn i programmet. Vanligtvis används klienter på klientdelswebbservrarna för att ansluta till ett Orleans kluster som fungerar som en mellannivå med korn som kör affärslogik.

I en typisk konfiguration, en klientdelswebbserver:

  • Tar emot en webbbegäran.
  • Utför nödvändig verifiering av autentisering och auktorisering.
  • Avgör vilka korn som ska bearbeta begäran.
  • Använder Microsoft.Orleans. Klientens NuGet-paket för att göra ett eller flera metodanrop till kornen.
  • Hanterar lyckade slutföranden eller fel i kornanropen och eventuella returnerade värden.
  • Skickar ett svar på webbbegäran.

Initiering av kornklient

Innan en kornklient kan användas för att göra anrop till korn som finns i ett Orleans kluster måste den konfigureras, initieras och anslutas till klustret.

Konfiguration tillhandahålls via UseOrleansClient och flera kompletterande alternativklasser som innehåller en hierarki med konfigurationsegenskaper för att programmatiskt konfigurera en klient. Mer information finns i Klientkonfiguration.

Tänk dig följande exempel på en klientkonfiguration:

// 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 När den startas konfigureras klienten och är tillgänglig via den konstruerade tjänstproviderinstansen.

Konfiguration tillhandahålls via ClientBuilder och flera kompletterande alternativklasser som innehåller en hierarki med konfigurationsegenskaper för att programmatiskt konfigurera en klient. Mer information finns i Klientkonfiguration.

Exempel på en klientkonfiguration:

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

Slutligen måste du anropa Connect() metoden för det konstruerade klientobjektet för att få det att ansluta till Orleans klustret. Det är en asynkron metod som returnerar en Task. Så du måste vänta tills den har slutförts med en await eller .Wait().

await client.Connect();

Göra anrop till korn

Att göra anrop till korn från en klient skiljer sig inte från att göra sådana anrop inifrån kornkod. Samma IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) metod, där T är målkornsgränssnittet, används i båda fallen för att hämta kornreferenser. Skillnaden är i vilket fabriksobjekt som anropas IGrainFactory.GetGrain. I klientkoden gör du det via det anslutna klientobjektet som följande exempel visar:

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

await joinGameTask;

Ett anrop till en kornmetod returnerar en Task eller en Task<TResult> som krävs av reglerna för korngränssnittet. Klienten kan använda nyckelordet await för att asynkront invänta den returnerade Task utan att blockera tråden eller i vissa fall Wait() metoden för att blockera den aktuella körningstråden.

Den största skillnaden mellan att göra anrop till korn från klientkod och inifrån ett annat korn är den entrådade körningsmodellen med korn. Kornigheter är begränsade till att vara enkeltrådade av körningen Orleans , medan klienter kan vara flertrådade. Orleans ger ingen sådan garanti på klientsidan, och därför är det upp till klienten att hantera dess samtidighet med de synkroniseringskonstruktioner som är lämpliga för dess miljö – lås, händelser och Tasks.

Ta emot aviseringar

Det finns situationer där ett enkelt mönster för begäran-svar inte räcker och klienten måste ta emot asynkrona meddelanden. En användare kanske till exempel vill bli meddelad när ett nytt meddelande har publicerats av någon som hon följer.

Användningen av Observatörer är en sådan mekanism som gör det möjligt att exponera objekt på klientsidan som kornliknande mål för att anropas av korn. Uppmaningar till observatörer ger ingen indikation på framgång eller misslyckande, eftersom de skickas som ett enkelriktad budskap om bästa förmåga. Det är därför programkodens ansvar att bygga en tillförlitlighetsmekanism på högre nivå ovanpå observatörerna vid behov.

En annan mekanism som kan användas för att leverera asynkrona meddelanden till klienter är Flöden. Flöden exponera indikationer på framgång eller misslyckande med leverans av enskilda meddelanden och därmed möjliggöra tillförlitlig kommunikation tillbaka till klienten.

Klientanslutning

Det finns två scenarier där en klusterklient kan uppleva anslutningsproblem:

  • När klienten försöker ansluta till en silo.
  • När du anropar kornreferenser som hämtades från en ansluten klusterklient.

I det första fallet försöker klienten ansluta till en silo. Om klienten inte kan ansluta till någon silo utlöser den ett undantag för att ange vad som gick fel. Du kan registrera en IClientConnectionRetryFilter för att hantera undantaget och bestämma om du vill försöka igen eller inte. Om inget återförsöksfilter anges, eller om återförsöksfiltret returnerar false, ger klienten upp för gott.

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

Det finns två scenarier där en klusterklient kan uppleva anslutningsproblem:

  • IClusterClient.Connect() När metoden anropas initialt.
  • När du anropar kornreferenser som hämtades från en ansluten klusterklient.

I det första fallet Connect utlöser metoden ett undantag för att ange vad som gick fel. Detta är vanligtvis (men inte nödvändigtvis) en SiloUnavailableException. Om detta händer är klusterklientinstansen oanvändbar och bör tas bort. Du kan också ange en återförsöksfilterfunktion för Connect metoden som till exempel kan vänta en angiven varaktighet innan du gör ett nytt försök. Om inget återförsöksfilter anges, eller om återförsöksfiltret returnerar false, ger klienten upp för gott.

Om Connect det returneras kommer klusterklienten garanterat att kunna användas tills den tas bort. Det innebär att även om klienten får anslutningsproblem kommer den att försöka återställas på obestämd tid. Det exakta återställningsbeteendet kan konfigureras för ett GatewayOptions objekt som tillhandahålls av ClientBuilder, t.ex.

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

I det andra fallet, när ett anslutningsproblem uppstår under ett kornigt anrop, utlöses ett SiloUnavailableException på klientsidan. Detta kan hanteras så här:

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

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

Kornreferensen är inte ogiltig i den här situationen. anropet kan göras om på samma referens senare när en anslutning kan ha återupprättats.

Beroendeinmatning

Det rekommenderade sättet att skapa en extern klient i ett program som använder .NET Generic Host är att mata in en IClusterClient singleton-instans via beroendeinmatning, som sedan kan accepteras som en konstruktorparameter i värdbaserade tjänster, ASP.NET styrenheter och så vidare.

Kommentar

När du är värd för en Orleans silo i samma process som ansluter till den, är det inte nödvändigt att skapa en klient manuellt. Orleans Den tillhandahåller automatiskt en och hanterar dess livslängd på lämpligt sätt.

När du ansluter till ett kluster i en annan process (på en annan dator) är ett vanligt mönster att skapa en värdbaserad tjänst som den här:

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

Tjänsten registreras sedan så här:

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

Exempel

Här är en utökad version av exemplet ovan av ett klientprogram som ansluter till Orleans, hittar spelarkontot, prenumererar på uppdateringar av spelsessionen som spelaren är en del av med en observatör och skriver ut meddelanden tills programmet avslutas manuellt.

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.");

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