Aracılığıyla paylaş


Orleans Müşteriler

İstemci, ayrıntılı olmayan kodun bir Orleans kümeyle etkileşim kurmasına izin verir. İstemciler, uygulama kodunun kümede barındırılan tanecikler ve akışlarla iletişim kurmasını sağlar. İstemci kodunu nerede barındırdığınıza bağlı olarak istemciyi edinmenin iki yolu vardır: siloyla aynı işlemde veya ayrı bir işlemde. Bu makalede önerilen yaklaşımdan başlayarak her iki seçenek de ele alınmaktadır: istemci kodunu tahıl koduyla aynı işlemde birlikte barındırma.

Ortak sunucuda barındırılan istemciler

İstemci kodunu tahıl koduyla aynı işlemde barındırırsanız, istemciyi barındırma uygulamasının bağımlılık ekleme kapsayıcısından doğrudan alabilirsiniz. Bu durumda istemci, bağlı olduğu siloyla doğrudan iletişim kurar ve silonun küme hakkındaki ek bilgilerinden yararlanabilir.

Bu yaklaşım daha az ağ ve CPU yükü, düşük gecikme süresi ve daha yüksek aktarım hızı ve güvenilirlik gibi çeşitli avantajlar sağlar. İstemci, silonun küme topolojisi ve durumu bilgisini kullanır ve ayrı bir ağ geçidine ihtiyaç duymaz. Bu, ağ geçişini ve serileştirme/seri durumdan çıkarma gidiş dönüşlerini önler ve böylece istemci ile grain (tahıl) arasındaki gerekli düğüm sayısını en aza indirerek güvenilirliği artırır. Grain durumsuz bir çalışan grain'se veya istemcinin barındırıldığı aynı siloda etkinleştirilirse, istemcinin ek performans ve güvenilirlik kazanımları elde etmesine olanak sağlayacak şekilde serileştirme veya ağ iletişimine gerek kalmaz. İstemci ve tahıl kodunu birlikte barındırmak, iki ayrı uygulama ikili dosyasını dağıtma ve izleme gereksinimini ortadan kaldırarak dağıtım ve uygulama topolojisini de basitleştirir.

Ayrıca bu yaklaşımın dezavantajları da vardır; öncelikle taneli kod artık istemci işleminden yalıtılmış değildir. Bu nedenle, bloklanan G/Ç veya iş parçacığının aç kalmasına neden olan işlemci kilidi çekişmesi gibi istemci kodundaki sorunlar, grain kodu performansını etkileyebilir. Bu tür kod hataları olmasa bile, gürültülü komşu etkileri yalnızca istemci kodunun tahıl koduyla aynı işlemcide yürütülmesi, CPU önbelleğinde ek yük oluşturması ve yerel kaynaklar için çekişmeyi artırması nedeniyle oluşabilir. Buna ek olarak, izleme sistemleri istemci kodu ile taneli kod arasında mantıksal olarak ayrım yapamayacağından, bu sorunların kaynağını belirlemek daha zor hale gelir.

Bu dezavantajlara rağmen, istemci kodunu tahıl koduyla birlikte barındırmak popüler bir seçenektir ve çoğu uygulama için önerilen yaklaşımdır. Yukarıda belirtilen dezavantajlar genellikle aşağıdaki nedenlerle pratikte çok azdır:

  • İstemci kodu genellikle çok incedir (örneğin, gelen HTTP isteklerini taneli çağrılara çevirme). Bu nedenle gürültülü komşu etkileri asgari düzeydedir ve aksi takdirde gerekli olan ağ geçidiyle karşılaştırılabilir maliyettedir.
  • Bir performans sorunu oluşursa, tipik iş akışınız büyük olasılıkla CPU profil oluşturucuları ve hata ayıklayıcıları gibi araçları içerir. Bu araçlar, aynı işlemde hem istemci hem de tahıl kodu yürütülürken bile sorunun kaynağını hızla belirlemede etkili olmaya devam etmektedir. Başka bir deyişle ölçümler daha kabalaşıp sorunun kaynağını daha az tanımlayabildiğinden daha ayrıntılı araçlar etkili olmaya devam eder.

Bir sunucudan istemci edinme

.NET Genel Ana Bilgisayar kullanarak barındırırsanız, istemci ana bilgisayarın bağımlılık enjeksiyonu kapsayıcısında otomatik olarak kullanılabilir. bunu ASP.NET denetleyicileri veya IHostedService uygulamaları gibi hizmetlere ekleyebilirsiniz.

Alternatif olarak, IGrainFactory gibi veya IClusterClient istemci arabirimini ISiloHost'dan elde edebilirsiniz.

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

Dış istemciler

İstemci kodu, tahıl kodunun barındırıldığı küme dışında Orleans çalıştırılabilir. Bu durumda, dış istemci kümeye ve uygulamanın tüm dilimlerine bağlayıcı veya kanal işlevi görür. Genellikle, iş mantığını yürüten bileşenlerle orta katman olarak hizmet veren bir Orleans kümeye bağlanmak için ön uç web sunucuları kullanarak istemcileri kullanırsınız.

Tipik bir kurulumda, bir ön uç web sunucusu:

  • Bir web isteği alır.
  • Gerekli kimlik doğrulama ve yetkilendirme doğrulama işlemlerini gerçekleştirir.
  • İsteği hangi taneciklerin işleyeceğine karar verir.
  • Microsoft.Client NuGet istemci paketini kullanarak grainlere bir veya daha fazla yöntem çağrısı yapar.
  • Grain çağrılarının ve döndürülen değerlerin başarıyla tamamlanmasını veya hatalarını ve her türlü başarılı veya hatalı sonuçlarını işler.
  • Web isteğine bir yanıt gönderir.

Tane istemcisini başlat

Kümede Orleans barındırılan taneciklere çağrı yapmak için bir taneli istemciyi kullanabilmeniz için önce kümeyi yapılandırmanız, başlatmanız ve kümeye bağlamanız gerekir.

UseOrleansClient ve yapılandırma özellikleri hiyerarşisine sahip birkaç ek seçenek sınıfı aracılığıyla bir istemciyi program aracılığıyla yapılandırmak için yapılandırma sağlayın. Daha fazla bilgi için bkz. İstemci yapılandırması.

aşağıdaki istemci yapılandırması örneğini göz önünde bulundurun:

TokenCredential kullanarak bir hizmet URI'si kullanmak önerilen yaklaşımdır. Bu düzen, gizli dizilerin yapılandırmada depolanmasını önler ve güvenli kimlik doğrulaması için Microsoft Entra Id'yi kullanır.

DefaultAzureCredential yerel geliştirme ve üretim ortamlarında sorunsuz çalışan bir kimlik bilgisi zinciri sağlar. Geliştirme sırasında Azure CLI veya Visual Studio kimlik bilgilerinizi kullanır. Azure'daki üretimde, kaynağınıza atanan yönetilen kimliği otomatik olarak kullanır.

Tip

DefaultAzureCredential yerel geliştirme ve üretim genelinde sorunsuz çalışır. Geliştirme aşamasında Azure CLI veya Visual Studio kimlik bilgilerinizi kullanır. Azure'daki üretimde otomatik olarak kaynağın yönetilen kimliğini kullanır. Üretimde performans ve hata ayıklama yeteneğini artırmak için bunu ManagedIdentityCredential gibi belirli bir kimlik bilgisiyle değiştirmeyi düşünün. Daha fazla bilgi için bkz . DefaultAzureCredential kullanım kılavuzu.

using Azure.Identity;

var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleansClient(clientBuilder =>
{
    clientBuilder.Configure<ClusterOptions>(options =>
    {
        options.ClusterId = "my-first-cluster";
        options.ServiceId = "MyOrleansService";
    });

    clientBuilder.UseAzureStorageClustering(options =>
    {
        options.ConfigureTableServiceClient(
            new Uri("https://<your-storage-account>.table.core.windows.net"),
            new DefaultAzureCredential());
    });
});

using var host = builder.Build();
await host.StartAsync();

host'ı başlattığınızda, istemci yapılandırılır ve yapılandırılan hizmet sağlayıcı örneği aracılığıyla kullanılabilir.

ClientBuilder ve yapılandırma özellikleri hiyerarşisine sahip birkaç ek seçenek sınıfı aracılığıyla bir istemciyi program aracılığıyla yapılandırmak için yapılandırma sağlayın. Daha fazla bilgi için bkz. İstemci yapılandırması.

İstemci yapılandırması örneği:

var client = new ClientBuilder()
    .Configure<ClusterOptions>(options =>
    {
        options.ClusterId = "my-first-cluster";
        options.ServiceId = "MyOrleansService";
    })
    .UseAzureStorageClustering(
        options => options.ConfigureTableServiceClient(connectionString))
    .ConfigureApplicationParts(
        parts => parts.AddApplicationPart(typeof(IValueGrain).Assembly))
    .Build();

Son olarak, oluşturduğunuz istemci nesnesinde Connect() yöntemini çağırarak, Orleans kümesine bağlanmanız gerekir. Bir Task döndüren zaman uyumsuz bir yöntemdir, bu nedenle tamamlanmasını beklemek için await veya .Wait() kullanmanız gerekir.

await client.Connect();

Tahıllara çağrı yapma

Bir istemciden grains'e çağrı yapmak, bu tür çağrıları grain kodu içinden yapmaktan farklı değildir. Her iki durumda da IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) yöntemini kullanarak (T burada hedef tanecik arabirimidir) tanecik referansları elde edin. Fark, hangi fabrika nesnesinin IGrainFactory.GetGrain'yi çağırdığıdır. İstemci kodunda, aşağıdaki örnekte gösterildiği gibi bunu bağlı istemci nesnesi aracılığıyla yaparsınız:

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

await joinGameTask;

Bir taneli yöntem çağrısı, Task veya Task<TResult> döndürür, tane arabirimi kurallarının gerektirdiği gibi. İstemci, iş parçacığını engellemeden zaman uyumsuz olarak döndürüleni beklemek için await anahtar kelimesini kullanabilir ya da bazı durumlarda, mevcut yürütme iş parçacığını engelleyerek beklemek için Task yöntemini kullanabilir.

İstemci kodundan ya da başka bir tanecik içinden taneciklere çağrı yapmanın en büyük farkı, taneciklerin tek iş parçacığıyla çalışan yürütme modelidir. Çalışma Orleans zamanı, tanecikleri tek iş parçacıklı olacak şekilde kısıtlarken, istemciler çok iş parçacıklı olabilir. Orleans istemci tarafında böyle bir garanti sağlamaz, bu nedenle ortamı için uygun eşitleme yapılarını (kilitler, Tasksolaylar vb.) kullanarak eşzamanlılığını yönetmek istemciye bağlıdır.

Bildirimler alma

Bazen basit bir istek-yanıt deseni yeterli olmaz ve istemcinin zaman uyumsuz bildirimler alması gerekir. Örneğin, bir kullanıcı takip ettikleri biri yeni bir ileti yayımladığında bildirim almak isteyebilir.

Gözlemcileri kullanmak, istemci tarafı nesneleri taneciklere benzer hedefler olarak ortaya çıkarılması ve bu hedeflerin tanecikler tarafından çağrılabilmesini sağlayan bir mekanizmadır. Gözlemcilere yapılan çağrılar, tek yönlü, en iyi çaba iletisi olarak gönderildiklerinden başarı veya başarısızlık göstergesi sağlamaz. Bu nedenle, gerektiğinde gözlemciler üzerinde daha üst düzey bir güvenilirlik mekanizması oluşturmak uygulama kodunuzun sorumluluğundadır.

İstemcilere eşzamansız iletiler teslim etmeye yönelik bir diğer mekanizma Akışlar'dır. Akışlar, tek tek ileti teslimi için başarı veya başarısızlık göstergelerini ortaya çıkararak istemciye güvenilir iletişim sağlar.

İstemci bağlantısı

Küme istemcisinin bağlantı sorunlarıyla karşılaşabileceği iki senaryo vardır:

  • İstemci bir siloya bağlanmayı denediğinde.
  • Bağlı bir küme istemcisinden elde edilen tahıl referanslarında çağrı yaparken.

İlk durumda, istemci bir siloya bağlanmayı dener. İstemci herhangi bir siloya bağlanamıyorsa, neyin yanlış gittiğini belirten bir özel durum oluşturur. Özel durumu işlemek ve yeniden deneyip denemeyeceğine karar vermek için bir IClientConnectionRetryFilter kaydedebilirsiniz. Yeniden deneme filtresi sağlamazsanız veya yeniden deneme filtresi döndürürse false, istemci kalıcı olarak vazgeçer.

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

Küme istemcisinin bağlantı sorunlarıyla karşılaşabileceği iki senaryo vardır:

  • Başlangıçta IClusterClient.Connect() metodu çağrıldığında.
  • Bağlı bir küme istemcisinden elde edilen tahıl referanslarında çağrı yaparken.

İlk durumda, Connect yöntemi neyin yanlış gittiğini belirten bir özel durum atar. Bu genellikle (ancak mutlaka olmasa da) bir SiloUnavailableExceptionolur. Bu durumda, küme istemci örneği kullanılamaz ve atılmalıdır. İsteğe bağlı olarak yöntemine Connect bir yeniden deneme filtresi işlevi sağlayabilirsiniz. Bu işlev, örneğin başka bir deneme yapmadan önce belirtilen süreyi bekleyebilir. Yeniden deneme filtresi sağlamazsanız veya yeniden deneme filtresi döndürürse false, istemci kalıcı olarak vazgeçer.

Connect başarılı bir şekilde dönerse, atılana kadar küme istemcisinin kullanılabilirliği garanti edilir. Başka bir deyişle, istemci bağlantı sorunlarıyla karşılaşsa bile, kurtarma girişimini başarıyla tamamlayana kadar sürdürür. GatewayOptions tarafından sağlanan bir ClientBuilder nesnesinde tam kurtarma davranışını yapılandırabilirsiniz; örneğin:

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

İkinci durumda, bir tahıl çağrısı sırasında bir bağlantı sorunu oluşursa, istemci tarafında bir SiloUnavailableException hatası atılır. Bunu şu şekilde halledebilirsiniz:

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

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

Bu durumda tahıl referansı geçersiz değildir; daha sonra bir bağlantı yeniden kurulmuş olabileceğinde aramayı aynı referansta yeniden deneyebilirsiniz.

Bağımlılık enjeksiyonu

.NET Generic Host kullanarak bir programda dış istemci oluşturmanın önerilen yolu, bağımlılık ekleme yoluyla bir IClusterClient tekil örneği enjekte etmektir. Bu örnek daha sonra barındırılan hizmetlerde, ASP.NET denetleyicilerinde vb. oluşturucu parametresi olarak kabul edilebilir.

Uyarı

Aynı işlemde bağlanacak bir Orleans siloyu birlikte barındırırken, manuel bir istemci oluşturmak gerekli değildir; Orleans otomatik olarak bir istemci sağlar ve ömrünü uygun şekilde yönetir.

Farklı bir işlemde (farklı bir makinede) bir kümeye bağlanırken, yaygın bir desen şöyle barındırılan bir hizmet oluşturmaktı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();
    }
}

Hizmeti şu şekilde kaydedin:

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

Örnek

İşte Orleans'ye bağlanan, oyuncu hesabını bulan ve oyuncunun parçası olduğu oyun oturumunun güncelleştirmelerine bir gözlemci kullanarak abone olan ve program el ile sonlandırılana kadar bildirimleri görüntüleyen istemci uygulamasını gösteren bir önceki örneğin genişletilmiş sürümü.

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.TableServiceClient = new TableServiceClient(
                    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()}");
}
internal static class ExternalClientExample
{
    private static string connectionString = "UseDevelopmentStorage=true";

    public static async Task RunWatcherAsync()
    {
        try
        {
            var client = new ClientBuilder()
                .Configure<ClusterOptions>(options =>
                {
                    options.ClusterId = "my-first-cluster";
                    options.ServiceId = "MyOrleansService";
                })
                .UseAzureStorageClustering(
                    options => options.ConfigureTableServiceClient(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);
    }
}