Lezen in het Engels

Delen via


Orleans Clients

Kies een Orleans-versie

Een client staat niet-graancode toe om te communiceren met een Orleans cluster. Clients staan toepassingscode toe om te communiceren met korrels en streams die worden gehost in een cluster. Er zijn twee manieren om een client te verkrijgen, afhankelijk van waar de clientcode wordt gehost: in hetzelfde proces als een silo of in een afzonderlijk proces. In dit artikel worden beide opties besproken, te beginnen met de aanbevolen optie: co-hosting van de clientcode in hetzelfde proces als de graancode.

Co-hostende clients

Als de clientcode wordt gehost in hetzelfde proces als de graancode, kan de client rechtstreeks worden verkregen uit de afhankelijkheidsinjectiecontainer van de hostingtoepassing. In dit geval communiceert de client rechtstreeks met de silo waaraan deze is gekoppeld en kan profiteren van de extra kennis die de silo over het cluster heeft.

Dit biedt verschillende voordelen, waaronder het verminderen van netwerk- en CPU-overhead en het verlagen van de latentie en het verhogen van de doorvoer en betrouwbaarheid. De client maakt gebruik van de silo's kennis van de clustertopologie en -status en hoeft geen afzonderlijke gateway te gebruiken. Dit voorkomt een retour van een netwerkhop en serialisatie/deserialisatie. Dit verhoogt daarom ook de betrouwbaarheid, omdat het aantal vereiste knooppunten tussen de client en het graan wordt geminimaliseerd. Als het graan een staatloze werkrol is of anderszins wordt geactiveerd op de silo waar de client wordt gehost, hoeft er helemaal geen serialisatie of netwerkcommunicatie te worden uitgevoerd en kan de client de extra prestaties en betrouwbaarheidsverbeteringen opleveren. Co-hostingclient en grain-code vereenvoudigt ook de implementatie- en toepassingstopologie door de noodzaak te elimineren dat er twee binaire toepassingsbestanden moeten worden geïmplementeerd en bewaakt.

Er zijn ooktractors voor deze benadering, voornamelijk dat de graancode niet meer wordt geïsoleerd van het clientproces. Daarom kunnen problemen in clientcode, zoals het blokkeren van I/O of vergrendelingsconflicten die thread-starvatie veroorzaken, van invloed zijn op de prestaties van graancode. Zelfs zonder codefouten zoals hierboven, kunnen ruiseffecten eenvoudig het gevolg zijn door de clientcode uit te voeren op dezelfde processor als graancode, waardoor extra belasting op de CPU-cache en extra conflicten voor lokale resources in het algemeen worden veroorzaakt. Bovendien is het identificeren van de bron van deze problemen nu moeilijker omdat bewakingssystemen niet kunnen onderscheiden wat logisch clientcode is van graancode.

Ondanks deze aftreders is co-hostingclientcode met graancode een populaire optie en de aanbevolen aanpak voor de meeste toepassingen. Om uit te werken, zijn de bovengenoemde detractors minimaal in de praktijk om de volgende redenen:

  • Clientcode is vaak erg dun, bijvoorbeeld het vertalen van binnenkomende HTTP-aanvragen in graanoproepen, en daarom zijn de ruiseffecten minimaal en vergelijkbaar met de kosten van de anders vereiste gateway.
  • Als er een prestatieprobleem optreedt, omvat de typische werkstroom voor een ontwikkelaar hulpprogramma's zoals CPU-profilers en foutopsporingsprogramma's, die nog steeds effectief zijn bij het snel identificeren van de oorzaak van het probleem, ondanks dat zowel client- als graancode in hetzelfde proces worden uitgevoerd. Met andere woorden, metrische gegevens worden grofer en minder in staat om de bron van een probleem nauwkeurig te identificeren, maar meer gedetailleerde hulpprogramma's zijn nog steeds effectief.

Een client ophalen van een host

Als u hosten met behulp van de .NET Generic Host, is de client automatisch beschikbaar in de afhankelijkheidsinjectiecontainer van de host en kan deze worden geïnjecteerd in services zoals ASP.NET controllers of IHostedService implementaties.

U kunt ook een clientinterface, zoals IGrainFactory of IClusterClient kan worden verkregen van ISiloHost:

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

Externe clients

Clientcode kan buiten het Orleans cluster worden uitgevoerd waar graancode wordt gehost. Daarom fungeert een externe client als een connector of conduit voor het cluster en alle korrels van de toepassing. Clients worden meestal gebruikt op de front-endwebservers om verbinding te maken met een Orleans cluster dat als middelste laag fungeert met korrels die bedrijfslogica uitvoeren.

In een typische installatie, een front-end webserver:

  • Hiermee ontvangt u een webaanvraag.
  • Voert de benodigde verificatie- en autorisatievalidatie uit.
  • Bepaalt welke graan(en) de aanvraag moeten verwerken.
  • Maakt gebruik van Microsoft .Orleans. Client NuGet-pakket om een of meer methodeaanroepen te maken naar de grain(s).
  • Hiermee worden geslaagde voltooiingen of fouten van de graanoproepen en eventuele geretourneerde waarden verwerkt.
  • Hiermee wordt een antwoord naar de webaanvraag verzonden.

Initialisatie van de grain-client

Voordat een graanclient kan worden gebruikt voor het maken van aanroepen naar korrels die worden gehost in een Orleans cluster, moet deze worden geconfigureerd, geïnitialiseerd en verbonden met het cluster.

Configuratie wordt geboden via UseOrleansClient en verschillende aanvullende optieklassen die een hiërarchie van configuratie-eigenschappen bevatten voor het programmatisch configureren van een client. Zie Clientconfiguratie voor meer informatie.

Bekijk het volgende voorbeeld van een clientconfiguratie:

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

Wanneer de host client wordt gestart, wordt deze geconfigureerd en beschikbaar via het exemplaar van de samengestelde serviceprovider.

Configuratie wordt geboden via ClientBuilder en verschillende aanvullende optieklassen die een hiërarchie van configuratie-eigenschappen bevatten voor het programmatisch configureren van een client. Zie Clientconfiguratie voor meer informatie.

Voorbeeld van een clientconfiguratie:

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

Ten slotte moet u de methode aanroepen Connect() voor het samengestelde clientobject om verbinding te maken met het Orleans cluster. Het is een asynchrone methode die een Task. Dus u moet wachten op de voltooiing ervan met een await of .Wait().

await client.Connect();

Oproepen naar korrels maken

Het aanroepen van graan van een client verschilt niet van het maken van dergelijke aanroepen vanuit graancode. IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) Dezelfde methode, waarbij T de doelkorrelinterface is, wordt in beide gevallen gebruikt om graanverwijzingen te verkrijgen. Het verschil is in welk factory-object wordt aangeroepen IGrainFactory.GetGrain. In clientcode doet u dat via het verbonden clientobject, zoals in het volgende voorbeeld wordt weergegeven:

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

await joinGameTask;

Een aanroep naar een graanmethode retourneert een Task of meer Task<TResult> , zoals vereist door de regels voor de graaninterface. De client kan het await trefwoord gebruiken om asynchroon te wachten op het geretourneerde Task zonder de thread te blokkeren of in sommige gevallen de Wait() methode om de huidige thread van uitvoering te blokkeren.

Het belangrijkste verschil tussen het aanroepen van korrels vanuit clientcode en vanuit een ander graan is het uitvoeringsmodel met één thread van korrels. Korrels zijn beperkt tot één thread door de Orleans runtime, terwijl clients mogelijk meerdere threads hebben. Orleans biedt geen dergelijke garantie aan de clientzijde, en het is dus aan de client om de gelijktijdigheid ervan te beheren met behulp van de synchronisatieconstructies die geschikt zijn voor de omgeving: vergrendelingen, gebeurtenissen en Tasks.

Meldingen ontvangen

Er zijn situaties waarin een eenvoudig aanvraag-antwoordpatroon niet voldoende is en de client asynchrone meldingen moet ontvangen. Een gebruiker kan bijvoorbeeld een melding ontvangen wanneer een nieuw bericht is gepubliceerd door iemand die ze volgt.

Het gebruik van waarnemers is een dergelijk mechanisme waarmee objecten aan de clientzijde als korrelachtige doelen kunnen worden aangeroepen door korrels. Aanroepen naar waarnemers geven geen indicatie van succes of mislukking, omdat ze worden verzonden als een eenrichtingsbericht met de beste inspanning. Het is dus de verantwoordelijkheid van de toepassingscode om waar nodig een betrouwbaarheidsmechanisme op een hoger niveau te bouwen voor waarnemers.

Een ander mechanisme dat kan worden gebruikt voor het leveren van asynchrone berichten aan clients is Streams. Streams geven indicaties weer van succes of mislukking van de bezorging van afzonderlijke berichten en maken betrouwbare communicatie naar de client mogelijk.

Clientconnectiviteit

Er zijn twee scenario's waarin een clusterclient verbindingsproblemen kan ondervinden:

  • Wanneer de client verbinding probeert te maken met een silo.
  • Bij het aanroepen van korrelverwijzingen die zijn verkregen van een verbonden clusterclient.

In het eerste geval probeert de client verbinding te maken met een silo. Als de client geen verbinding kan maken met een silo, wordt er een uitzondering gegenereerd om aan te geven wat er mis is gegaan. U kunt een IClientConnectionRetryFilter registreren om de uitzondering af te handelen en te beslissen of u het opnieuw wilt proberen. Als er geen filter voor opnieuw proberen is opgegeven of als het filter voor opnieuw proberen wordt geretourneerd false, geeft de client voorgoed op.

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

Er zijn twee scenario's waarin een clusterclient verbindingsproblemen kan ondervinden:

  • Wanneer de IClusterClient.Connect() methode in eerste instantie wordt aangeroepen.
  • Bij het aanroepen van korrelverwijzingen die zijn verkregen van een verbonden clusterclient.

In het eerste geval genereert de Connect methode een uitzondering om aan te geven wat er mis is gegaan. Dit is doorgaans (maar niet noodzakelijkerwijs) een SiloUnavailableException. Als dit gebeurt, is het clusterclientexemplaren onbruikbaar en moet het worden verwijderd. Er kan eventueel een filterfunctie voor opnieuw proberen worden opgegeven voor de Connect methode die bijvoorbeeld kan wachten op een opgegeven duur voordat een andere poging wordt gedaan. Als er geen filter voor opnieuw proberen is opgegeven of als het filter voor opnieuw proberen wordt geretourneerd false, geeft de client voorgoed op.

Als Connect de retourneert, is de clusterclient gegarandeerd bruikbaar totdat deze wordt verwijderd. Dit betekent dat zelfs als de client verbindingsproblemen ondervindt, het voor onbepaalde tijd probeert te herstellen. Het exacte herstelgedrag kan worden geconfigureerd voor een GatewayOptions object dat wordt geleverd door de ClientBuilder, bijvoorbeeld:

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

In het tweede geval, waarbij een verbindingsprobleem optreedt tijdens een graanaanroep, wordt er een SiloUnavailableException aan de clientzijde gegenereerd. Dit kan als volgt worden afgehandeld:

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

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

De korrelreferentie wordt in deze situatie niet ongeldig gemaakt; de aanroep kan later opnieuw worden geprobeerd op dezelfde verwijzing wanneer een verbinding mogelijk opnieuw tot stand is gebracht.

Afhankelijkheidsinjectie

De aanbevolen manier om een externe client te maken in een programma dat gebruikmaakt van de .NET Generic Host is het injecteren van een IClusterClient singleton-exemplaar via afhankelijkheidsinjectie, die vervolgens kan worden geaccepteerd als een constructorparameter in gehoste services, ASP.NET controllers, enzovoort.

Notitie

Wanneer co-hosting van een Orleans silo in hetzelfde proces dat er verbinding mee maakt, is het niet nodig om handmatig een client te maken. Orleans Het zal automatisch een silo bieden en de levensduur ervan op de juiste wijze beheren.

Wanneer u verbinding maakt met een cluster in een ander proces (op een andere computer), is het gebruikelijk om een gehoste service als volgt te maken:

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

De service wordt vervolgens als volgt geregistreerd:

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

Opmerking

Hier volgt een uitgebreide versie van het bovenstaande voorbeeld van een clienttoepassing waarmee verbinding wordt gemaakt Orleans, vindt u het speleraccount, abonneert u op updates voor de gamesessie waarvan de speler deel uitmaakt met een waarnemer en drukt u meldingen af totdat het programma handmatig wordt beëindigd.

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