Bagikan melalui


Orleans Klien

Klien memungkinkan kode non-bijian untuk berinteraksi dengan Orleans kluster. Klien-klien memungkinkan kode aplikasi untuk berkomunikasi dengan grain serta stream data yang dihosting dalam kluster. Ada dua cara untuk mendapatkan klien, tergantung di mana Anda menempatkan kode klien: dalam proses yang sama dengan "silo" atau dalam proses terpisah. Artikel ini membahas kedua opsi, dimulai dengan pendekatan yang direkomendasikan: kode klien ditempatkan dalam proses yang sama dengan kode grain.

Klien yang dihosting bersama

Jika Anda menghosting kode klien dalam proses yang sama persis dengan kode grain, Anda dapat langsung mendapatkan klien dari wadah injeksi dependensi aplikasi hosting. Dalam hal ini, klien berkomunikasi langsung dengan silo tempat ia terhubung dan dapat memanfaatkan pengetahuan tambahan dari silo tentang kluster.

Pendekatan ini memberikan beberapa manfaat, termasuk berkurangnya overhead jaringan dan CPU, penurunan latensi, dan peningkatan throughput dan keandalan. Klien menggunakan pengetahuan silo tentang topologi dan status kluster dan tidak memerlukan gateway terpisah. Ini menghindari lompatan jaringan dan proses bolak-balik serialisasi/deserialisasi, sehingga meningkatkan keandalan dengan meminimalkan jumlah node yang diperlukan antara klien dan grain. Jika grain tersebut adalah grain pekerja tanpa status atau secara kebetulan diaktifkan pada silo yang sama di mana klien dihosting, tidak diperlukan serialisasi atau komunikasi jaringan sama sekali, yang memungkinkan klien mencapai peningkatan performa dan keandalan. Klien hosting bersama dan kode biji-bijian juga menyederhanakan penyebaran dan topologi aplikasi dengan menghilangkan kebutuhan untuk menyebarkan dan memantau dua biner aplikasi yang berbeda.

Ada juga kelemahan dari pendekatan ini, terutama adalah bahwa kode grain tidak lagi terisolasi dari proses klien. Oleh karena itu, masalah dalam kode klien, seperti pemblokiran I/O atau perebutan kunci yang menyebabkan kelaparan thread, dapat memengaruhi performa kode grain. Bahkan tanpa cacat kode seperti itu, efek noisy neighbor dapat terjadi hanya karena kode klien dijalankan pada prosesor yang sama dengan grain code, memberikan beban tambahan pada cache CPU dan meningkatkan persaingan untuk sumber daya lokal. Selain itu, mengidentifikasi sumber masalah ini menjadi lebih sulit karena sistem pemantauan tidak dapat membedakan secara logis antara kode klien dan kode biji-bijian.

Terlepas dari kelemahan ini, kode klien yang dihosting bersama dengan kode 'grain' adalah opsi populer dan pendekatan yang direkomendasikan untuk sebagian besar aplikasi. Kelemahan yang disebutkan di atas seringkali minimal dalam praktik karena alasan berikut:

  • Kode klien sering kali sangat tipis (misalnya, menerjemahkan permintaan HTTP masuk ke dalam panggilan grain). Oleh karena itu, efek tetangga berisik minimal dan biaya yang sebanding dengan gateway yang diperlukan biasanya.
  • Jika masalah performa muncul, alur kerja umum Anda kemungkinan melibatkan alat seperti profiler CPU dan debugger. Alat-alat ini tetap efektif dalam mengidentifikasi sumber masalah dengan cepat, bahkan ketika klien dan kode grain dijalankan dalam proses yang sama. Dengan kata lain, sementara metrik menjadi lebih kasar dan kurang dapat mengidentifikasi sumber masalah dengan tepat, alat yang lebih rinci masih efektif.

Memperoleh klien dari host

Jika Anda menghosting dengan menggunakan .NET Generic Host, klien secara otomatis tersedia di dalam kontainer dependensi injeksi milik host. Anda dapat menyuntikkannya ke layanan seperti pengontrol ASP.NET atau IHostedService implementasi.

Atau, Anda dapat memperoleh antarmuka klien seperti IGrainFactory atau IClusterClient dari ISiloHost:

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

Pelanggan eksternal

Kode klien dapat berjalan di luar Orleans kluster tempat kode biji-bijian dihosting. Dalam hal ini, klien eksternal bertindak sebagai konektor atau saluran ke kluster dan semua grain aplikasi. Biasanya, Anda menggunakan klien pada server web frontend untuk terhubung ke kluster yang berfungsi sebagai lapisan tengah, dengan unit yang menjalankan logika bisnis.

Dalam konfigurasi yang umum, server web frontend:

  • Menerima permintaan web.
  • Melakukan validasi autentikasi dan otorisasi yang diperlukan.
  • Memutuskan butir mana yang harus memproses permintaan.
  • Menggunakan paket NuGet Microsoft.Orleans.Client untuk melakukan satu atau beberapa panggilan metode ke grain.
  • Mengelola keberhasilan atau kegagalan dari panggilan grain serta nilai-nilai yang dikembalikan.
  • Mengirim respons ke permintaan web.

Menginisialisasi klien grain

Sebelum Anda dapat menggunakan klien biji-bijian untuk melakukan panggilan ke biji-bijian yang Orleans dihosting dalam kluster, Anda perlu mengonfigurasi, menginisialisasi, dan menghubungkannya ke kluster.

Menyediakan konfigurasi melalui UseOrleansClient dan beberapa kelas opsi tambahan yang berisi hierarki properti konfigurasi untuk mengonfigurasi klien secara terprogram. Untuk informasi selengkapnya, lihat Konfigurasi klien.

Pertimbangkan contoh konfigurasi klien berikut:

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

Saat Anda memulai host, klien dikonfigurasi dan tersedia melalui instans penyedia layanan yang telah dibangun.

Menyediakan konfigurasi melalui ClientBuilder dan beberapa kelas opsi tambahan yang berisi hierarki properti konfigurasi untuk mengonfigurasi klien secara terprogram. Untuk informasi selengkapnya, lihat Konfigurasi klien.

Contoh konfigurasi klien:

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

Terakhir, Anda perlu memanggil Connect() metode pada objek klien yang dibangun untuk menghubungkannya ke Orleans kluster. Ini adalah metode asinkron yang mengembalikan Task, jadi Anda perlu menunggu penyelesaiannya menggunakan await atau .Wait().

await client.Connect();

Melakukan panggilan ke biji-bijian

Melakukan panggilan ke biji-bijian dari klien tidak berbeda dengan melakukan panggilan seperti itu dari dalam kode biji-bijian. Gunakan metode yang sama IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) (di mana T adalah antarmuka butir target) dalam kedua kasus untuk mendapatkan referensi biji-bijian. Perbedaannya terletak di mana objek pabrik memanggil IGrainFactory.GetGrain. Dalam kode klien, Anda melakukan ini melalui objek klien yang terhubung, seperti yang ditunjukkan contoh berikut:

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

await joinGameTask;

Panggilan ke metode grain mengembalikan Task atau Task<TResult>, sebagaimana yang diperlukan oleh aturan antarmuka grain. Klien dapat menggunakan kata kunci await untuk secara asinkron menunggu hasil pengembalian Task tanpa memblokir utas, atau dalam beberapa kasus, menggunakan metode Wait() untuk memblokir utas eksekusi saat ini.

Perbedaan utama antara memanggil grain dari kode klien dengan dari dalam grain lainnya adalah model eksekusi satu utas pada grain. Orleans Runtime membatasi grain menjadi utas tunggal, sementara klien dapat multi-utas. Orleans tidak memberikan jaminan seperti itu di sisi klien, jadi terserah klien untuk mengelola konkurensinya menggunakan konstruksi sinkronisasi yang sesuai untuk lingkungannya—kunci, peristiwa, Tasks, dll.

Menerima pemberitahuan

Terkadang, pola respons permintaan sederhana tidak cukup, dan klien perlu menerima pemberitahuan asinkron. Misalnya, pengguna mungkin menginginkan pemberitahuan saat seseorang yang mereka ikuti menerbitkan pesan baru.

Menggunakan Observers adalah salah satu mekanisme yang memungkinkan paparan objek sisi klien sebagai target seperti biji-bijian untuk dipanggil oleh biji-bijian. Panggilan ke pengamat tidak memberikan indikasi keberhasilan atau kegagalan, karena dikirim sebagai pesan upaya terbaik satu arah. Oleh karena itu, ini adalah tanggung jawab kode aplikasi Anda untuk membangun mekanisme keandalan tingkat yang lebih tinggi di atas pengamat jika perlu.

Mekanisme lain untuk mengirimkan pesan asinkron kepada klien adalah Streams. Aliran mengekspos indikasi keberhasilan atau kegagalan untuk pengiriman pesan individual, memungkinkan komunikasi yang andal kembali ke klien.

Konektivitas klien

Ada dua skenario di mana klien kluster dapat mengalami masalah konektivitas:

  • Ketika klien mencoba untuk terhubung ke silo.
  • Saat melakukan panggilan pada referensi biji-bijian yang diperoleh dari klien kluster yang terhubung.

Dalam kasus pertama, klien mencoba untuk terhubung ke silo. Jika klien tidak dapat terhubung ke silo apa pun, akan melempar pengecualian yang menunjukkan apa yang salah. Anda dapat mendaftarkan IClientConnectionRetryFilter untuk menangani pengecualian dan memutuskan apakah akan mencoba kembali. Jika Anda tidak memberikan filter coba lagi, atau jika filter coba lagi mengembalikan false, klien menyerah secara permanen.

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

Ada dua skenario di mana klien kluster dapat mengalami masalah konektivitas:

  • Ketika metode IClusterClient.Connect() dipanggil pertama kali.
  • Saat melakukan panggilan pada referensi biji-bijian yang diperoleh dari klien kluster yang terhubung.

Dalam kasus pertama, Connect metode ini melempar pengecualian yang menunjukkan apa yang salah. Ini biasanya (tetapi tidak selalu) sebuah SiloUnavailableException. Jika ini terjadi, instans klien kluster tidak dapat digunakan dan harus dibuang. Anda dapat memberikan fungsi filter ulang secara opsional ke metode Connect, yang bisa, misalnya, menunggu selama durasi waktu yang ditentukan sebelum melakukan upaya lain. Jika Anda tidak memberikan filter coba lagi, atau jika filter coba lagi mengembalikan false, klien menyerah secara permanen.

Jika Connect berhasil dikembalikan, klien kluster dijamin dapat digunakan hingga dibuang. Ini berarti bahwa bahkan jika klien mengalami masalah koneksi, klien mencoba untuk pulih tanpa batas waktu. Anda dapat mengonfigurasi perilaku pemulihan yang tepat pada objek yang GatewayOptions disediakan oleh ClientBuilder, misalnya:

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

Dalam kasus kedua, di mana masalah koneksi terjadi selama grain call, pengecualian SiloUnavailableException terjadi di sisi klien. Anda dapat menangani ini seperti itu:

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

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

Referensi grain tidak dinyatakan tidak valid dalam situasi ini; Anda dapat mencoba kembali panggilan pada referensi yang sama nanti ketika koneksi telah terjalin kembali.

Injeksi Ketergantungan

Cara yang disarankan untuk membuat klien eksternal dalam program menggunakan Host Generik .NET adalah dengan menyuntikkan IClusterClient instans singleton melalui injeksi dependensi. Instans ini kemudian dapat diterima sebagai parameter konstruktor dalam layanan yang dihosting, ASP.NET pengontrol, dll.

Nota

Ketika bersama-sama mengelola silo dalam proses yang sama yang akan terhubung langsung ke dalamnya, tidak perlu membuat klien secara manual; sistem akan secara otomatis menyediakan dan mengelola masa pakainya dengan tepat.

Saat menyambungkan ke kluster dalam proses yang berbeda (pada komputer yang berbeda), pola umumnya adalah membuat layanan yang dihosting seperti ini:

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

Daftarkan layanan seperti ini:

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

Contoh

Berikut adalah versi yang diperluas dari contoh sebelumnya yang menunjukkan aplikasi klien yang terhubung ke Orleans, menemukan akun pemain, menggunakan pengamat untuk berlangganan pembaruan sesi permainan yang diikuti pemain, dan menampilkan pemberitahuan hingga program dihentikan secara manual.

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