Megosztás a következőn keresztül:


Orleans Ügyfelek

Az ügyfél lehetővé teszi a nem szemcsés kód használatát egy Orleans fürttel. Az ügyfelek lehetővé teszik, hogy az alkalmazáskód kommunikáljon egy fürtben üzemeltetett szemcsékkel és streamekkel. Az ügyfél kétféleképpen szerezhető be, attól függően, hogy hol található az ügyfélkód: ugyanabban a folyamatban, mint egy siló, vagy egy külön folyamat. Ez a cikk mindkét lehetőséget ismerteti, kezdve az ajánlott lehetőséggel: az ügyfélkód közös üzemeltetése ugyanabban a folyamatban, mint a szemcsés kód.

Közös üzemeltetésű ügyfelek

Ha az ügyfélkódot ugyanabban a folyamatban üzemelteti, mint a szemcsés kód, akkor az ügyfél közvetlenül beszerezhető az üzemeltetési alkalmazás függőséginjektálási tárolójából. Ebben az esetben az ügyfél közvetlenül kommunikál azzal a silóval, amelyhez csatlakoztatva van, és kihasználhatja a siló fürttel kapcsolatos további ismereteit.

Ez számos előnnyel jár, például csökkenti a hálózati és cpu-terhelést, valamint csökkenti a késést, valamint növeli az átviteli sebességet és a megbízhatóságot. Az ügyfél a siló fürttopológiájával és állapotával kapcsolatos ismereteit használja, és nem kell külön átjárót használnia. Ez elkerüli a hálózati ugrást és a szerializálást/deszerializálást. Ez tehát növeli a megbízhatóságot is, mivel a szükséges csomópontok száma az ügyfél és a gabona között minimálisra csökken. Ha a gabona állapot nélküli feldolgozó, vagy más módon aktiválódik azon a silón, ahol az ügyfél üzemel, akkor egyáltalán nem kell szerializálást vagy hálózati kommunikációt végezni, és az ügyfél kihasználhatja a további teljesítményt és megbízhatóságot. Az ügyfél- és szemcsés kód együttes üzemeltetése azzal is leegyszerűsíti az üzembe helyezést és az alkalmazástopológiát, hogy nincs szükség két különálló alkalmazás bináris fájl üzembe helyezésére és figyelésére.

Ennek a megközelítésnek vannak detraktorai is, elsősorban az, hogy a szemcsés kód már nincs elkülönítve az ügyfélfolyamattól. Ezért az ügyfélkódokkal kapcsolatos problémák, például az I/O blokkolása vagy a szálak éhezését okozó zárolási versengés hatással lehetnek a gabonakód teljesítményére. A fentihez hasonló kódhibák nélkül is a zajos szomszédhatások egyszerűen azt eredményezhetik, hogy az ügyfélkód ugyanazon a processzoron fut, mint a grain code, ami további terhelést jelent a cpu-gyorsítótárban, és általában további versengést eredményez a helyi erőforrások számára. Emellett a problémák forrásának azonosítása most már nehezebb, mert a monitorozási rendszerek nem tudják megkülönböztetni a logikailag ügyfélkódokat a szemcsés kódtól.

Ezeknek a detraktoroknak ellenére az ügyfélkód és a grain code együttes üzemeltetése népszerű lehetőség, és a legtöbb alkalmazás számára ajánlott megközelítés. A fenti detraktorok a gyakorlatban minimálisak a következő okok miatt:

  • Az ügyfélkód gyakran nagyon vékony, például a bejövő HTTP-kéréseket szemcsés hívásokra fordítja, ezért a zajos szomszédhatások minimálisak és összehasonlíthatók az egyébként szükséges átjáróval.
  • Teljesítményproblémák esetén a fejlesztők tipikus munkafolyamata olyan eszközökkel jár, mint a CPU-profilozók és a hibakeresők, amelyek még mindig hatékonyak a probléma forrásának gyors azonosításában annak ellenére, hogy az ügyfél- és a gabonakód is ugyanabban a folyamatban fut. Más szóval a metrikák durvábbak lesznek, és kevésbé tudják pontosan azonosítani a probléma forrását, de a részletesebb eszközök még mindig hatékonyak.

Ügyfél beszerzése gazdagépről

Ha a .NET Generic Gazdagép használatával üzemelteti az ügyfelet, az automatikusan elérhető lesz a gazdagép függőséginjektálási tárolójában, és olyan szolgáltatásokba is injektálható, mint ASP.NET vezérlők vagy IHostedService implementációk.

Másik lehetőségként egy ügyféloldali felület, például IGrainFactory a következőből ISiloHostszerezhető be:IClusterClient

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

Külső ügyfelek

Az ügyfélkód azon a fürtön kívül is futtatható, ahol a Orleans gabonakód fut. Ezért a külső ügyfél összekötőként vagy összekötőként működik a fürthöz és az alkalmazás minden szemcséihez. Az ügyfeleket általában az előtérbeli webkiszolgálókon használják egy Orleans olyan fürthöz való csatlakozáshoz, amely középrétegként szolgál az üzleti logikát végrehajtó szemcsékkel.

Egy tipikus beállításban egy előtérbeli webkiszolgáló:

  • Webes kérést fogad.
  • Elvégzi a szükséges hitelesítést és engedélyezés-ellenőrzést.
  • Dönti el, hogy melyik gabona(ok)nak kell feldolgoznia a kérelmet.
  • A Microsoft.Orleans. Ügyfél NuGet-csomag egy vagy több metódushíváshoz a gabona(ok)hoz.
  • Kezeli a szemcsés hívások sikeres befejezését vagy sikertelenségét, valamint a visszaadott értékeket.
  • Választ küld a webes kérésnek.

A szemcsés ügyfél inicializálása

Ahhoz, hogy egy szemcsés ügyfél használható legyen egy Orleans fürtben üzemeltetett szemek hívásához, konfigurálnia, inicializálnia kell és csatlakoznia kell a fürthöz.

A konfigurációt több kiegészítő beállításosztály biztosítja UseOrleansClient , amelyek az ügyfél programozott konfigurálásához a konfigurációs tulajdonságok hierarchiáját tartalmazzák. További információ: Ügyfélkonfiguráció.

Tekintse meg az ügyfélkonfiguráció alábbi példáját:

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

Az indításkor az host ügyfél konfigurálva lesz, és elérhető lesz a létrehozott szolgáltatópéldányon keresztül.

A konfigurációt több kiegészítő beállításosztály biztosítja ClientBuilder , amelyek az ügyfél programozott konfigurálásához a konfigurációs tulajdonságok hierarchiáját tartalmazzák. További információ: Ügyfélkonfiguráció.

Példa ügyfélkonfigurációra:

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

Végül meg kell hívnia Connect() a metódust a létrehozott ügyfélobjektumon, hogy csatlakozzon a Orleans fürthöz. Ez egy aszinkron metódus, amely egy Task. Ezért meg kell várnia a befejezését egy await vagy .Wait().

await client.Connect();

Hívások kezdeményezése a szemcsékhöz

Az ügyféltől érkező hívások kezdeményezése nem különbözik az ilyen hívásoktól a grain code-on belül. Mindkét esetben ugyanazt IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) a módszert T használják a szemcsehivatkozások beszerzéséhez. A különbség az, hogy melyik gyári objektumot hívja meg a függvény IGrainFactory.GetGrain. Az ügyfélkódban ezt a csatlakoztatott ügyfélobjektumon keresztül teheti meg, ahogy az alábbi példa mutatja:

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

await joinGameTask;

A gabonametódusra irányuló hívás a szemcsés interfész szabályainak megfelelően ad vissza egy Task vagy egy Task<TResult> értéket. Az ügyfél a await kulcsszóval aszinkron módon várhatja meg a visszaadott Task adatokat a szál blokkolása nélkül, vagy bizonyos esetekben a Wait() végrehajtási szál letiltásának módszere nélkül.

A szemcsékre irányuló hívások ügyfélkódból és egy másik szemcsén belüli hívásai között a fő különbség a szemcsék egyszálas végrehajtási modellje. A szemcséket a futtatókörnyezet egyszálasra korlátozza, míg az Orleans ügyfelek többszálasak lehetnek. Orleans nem nyújt ilyen garanciát az ügyféloldalon, ezért az ügyfélnek kell kezelnie az egyidejűségét a környezetének megfelelő szinkronizálási szerkezetekkel – zárolásokkal, eseményekkel és Tasks.

Értesítések fogadása

Vannak olyan helyzetek, amikor egy egyszerű kérés-válasz minta nem elég, és az ügyfélnek aszinkron értesítéseket kell kapnia. Előfordulhat például, hogy egy felhasználó értesítést szeretne kapni arról, ha valaki közzétett egy új üzenetet, amelyet követett.

A Megfigyelők használata az egyik ilyen mechanizmus, amely lehetővé teszi az ügyféloldali objektumok szemcsés célként való felfedésének a szemcsék általi meghívását. A megfigyelőkhöz intézett hívások nem jelzik a sikert vagy a sikertelenséget, mivel azokat egyirányú legjobb munkaértekezeti üzenetként küldik el. Ezért az alkalmazáskód feladata egy magasabb szintű megbízhatósági mechanizmus létrehozása a megfigyelők fölé, ahol szükséges.

Egy másik mechanizmus, amely az aszinkron üzenetek ügyfeleknek való továbbítására használható, adatfolyamok. adatfolyamok az egyes üzenetek kézbesítésének sikerességére vagy sikertelenségére utaló jeleket tesz közzé, és ezáltal megbízható kommunikációt tesz lehetővé az ügyfél felé.

Ügyfélkapcsolat

A fürtügyfél két esetben tapasztalhat csatlakozási problémákat:

  • Amikor az ügyfél megpróbál csatlakozni egy silóhoz.
  • Csatlakoztatott fürtügyféltől beszerzett, részletes hivatkozásokat tartalmazó hívások indításakor.

Az első esetben az ügyfél megpróbál csatlakozni egy silóhoz. Ha az ügyfél nem tud csatlakozni egy silóhoz, kivételt küld a hiba jelzésére. Regisztrálhat egy kivételt IClientConnectionRetryFilter , és eldöntheti, hogy újra próbálkozik-e vagy sem. Ha nincs megadva újrapróbálkoztatási szűrő, vagy ha az újrapróbálkozások falseszűrője visszatér, az ügyfél jó esetben feladja.

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

A fürtügyfél két esetben tapasztalhat csatlakozási problémákat:

  • Amikor a IClusterClient.Connect() metódust először meghívják.
  • Csatlakoztatott fürtügyféltől beszerzett, részletes hivatkozásokat tartalmazó hívások indításakor.

Az első esetben a Connect metódus kivételt küld a hiba jelzésére. Ez általában (de nem feltétlenül) a SiloUnavailableException. Ha ez történik, a fürtügyfél példánya használhatatlan, ezért el kell helyezni. Az újrapróbálkozási szűrőfüggvény opcionálisan megadható a Connect metódushoz, amely például megvárhat egy adott időtartamot, mielőtt újabb kísérletet végezne. Ha nincs megadva újrapróbálkoztatási szűrő, vagy ha az újrapróbálkozások falseszűrője visszatér, az ügyfél jó esetben feladja.

Ha Connect sikeresen visszatér, a fürtügyfél biztosan használható lesz, amíg el nem kerül. Ez azt jelenti, hogy még akkor is, ha az ügyfél csatlakozási problémákat tapasztal, határozatlan ideig megpróbálja helyreállítani. A pontos helyreállítási viselkedés konfigurálható egy GatewayOptions olyan objektumon, amely a ClientBuilderkövetkező, például:

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

A második esetben, amikor kapcsolati probléma lép fel egy szemcsés hívás során, a rendszer az ügyféloldalra küld egy SiloUnavailableException hibát. Ez a következőképpen kezelhető:

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

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

Ebben a helyzetben a gabonahivatkozás nem érvénytelen; a hívás később újrapróbálkozott ugyanahhoz a hivatkozáshoz, amikor lehetséges, hogy újra létrejön egy kapcsolat.

Függőséginjektálás

A .NET Generic Gazdagépet használó programokban ajánlott külső ügyfelet létrehozni, ha függőséginjektáláson keresztül injektál egy IClusterClient egytonos példányt, amelyet aztán konstruktorparaméterként lehet elfogadni az üzemeltetett szolgáltatásokban, ASP.NET vezérlőkben stb.

Feljegyzés

Amikor ugyanabban a folyamatban üzemeltet egy Orleans silót, amely csatlakozik hozzá, nem szükséges manuálisan létrehozni egy ügyfelet; Orleans automatikusan biztosít egyet, és megfelelően kezeli annak élettartamát.

Ha egy fürthöz egy másik folyamatban (egy másik gépen) csatlakozik, gyakori minta egy üzemeltetett szolgáltatás létrehozása, mint a következő:

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

A szolgáltatás ezután a következő módon van regisztrálva:

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

Példa

Íme egy kiterjesztett változata a fenti példa egy ügyfélalkalmazás, amely csatlakozik Orleans, megkeresi a játékos fiókot, feliratkozik a frissítéseket a játék munkamenet a játékos része egy megfigyelő, és kinyomtatja az értesítéseket, amíg a program manuálisan leáll.

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