Aracılığıyla paylaş


Orleans istemcileri

İ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ına izin verir. İstemci kodunun barındırıldığı yere bağlı olarak bir istemciyi edinmenin iki yolu vardır: siloyla aynı işlemde veya ayrı bir işlemde. Bu makalede önerilen seçenekle başlayarak her iki seçenek de ele alınacaktır: istemci kodunu tahıl koduyla aynı işlemde birlikte barındırma.

Ortak barındırılan istemciler

İstemci kodu, tahıl koduyla aynı işlemde barındırılıyorsa, istemci doğrudan barındırma uygulamasının bağımlılık ekleme kapsayıcısından alınabilir. Bu durumda istemci, bağlı olduğu siloyla doğrudan iletişim kurar ve silonun küme hakkında sahip olduğu ek bilgilerden yararlanabilir.

Bu, ağ ve CPU ek yükünü azaltmanın yanı sıra gecikme süresini azaltma ve aktarım hızı ile güvenilirliği artırma gibi çeşitli avantajlar sağlar. İstemci, silonun küme topolojisi ve durumu bilgilerini kullanır ve ayrı bir ağ geçidi kullanması gerekmez. Bu, ağ atlama ve serileştirme/seri durumdan çıkarma gidiş dönüşlerini önler. Bu nedenle, istemci ile tahıl arasındaki gerekli düğümlerin sayısı en aza indirildiğinden güvenilirliği de artırır. Tahıl durum bilgisi olmayan bir çalışan dilimiyse veya istemcinin barındırıldığı siloda başka bir şekilde etkinleştiriliyorsa, serileştirme veya ağ iletişimi hiç gerçekleştirilmesi gerekmez ve istemci ek performans ve güvenilirlik kazançlarını karşılayabilir. İstemci ve tahıl kodunun birlikte barındırılması, dağıtılacak ve izlenecek iki ayrı uygulama ikili dosyasının gereksinimini ortadan kaldırarak dağıtım ve uygulama topolojisini de basitleştirir.

Ayrıca bu yaklaşımın, öncelikli olarak tanecik kodunun artık istemci işleminden yalıtılmış olmamasıyla ilgili de saptırıcılar vardır. Bu nedenle, istemci kodundaki GÇ'yi engelleme veya iş parçacığının aç kalmasına neden olan çekişme kilitlenmesi gibi sorunlar, tahıl kodunun performansını etkileyebilir. Yukarıda belirtilen gibi kod hataları olmasa bile, gürültülü komşu efektleri yalnızca istemci kodunun tahıl koduyla aynı işlemcide yürütülmesini sağlayarak, CPU önbelleğine ek yük ve genel olarak yerel kaynaklar için ek çekişmeler oluşturarak sonuçlanabilir. Buna ek olarak, izleme sistemleri mantıksal olarak istemci kodunun grenli koddan ne olduğunu ayırt edemeyeceğinden, bu sorunların kaynağını belirlemek artık daha zordur.

Bu detraktörlere rağmen, istemci kodunun tahıl koduyla birlikte barındırilmesi popüler bir seçenektir ve çoğu uygulama için önerilen yaklaşımdır. Ayrıntılı olarak, yukarıda belirtilen detraktörler aşağıdaki nedenlerle pratikte çok azdır:

  • İstemci kodu genellikle çok incedir; örneğin, gelen HTTP isteklerini ayrıntılı çağrılara çevirir ve bu nedenle gürültülü komşu efektleri düşük maliyetlidir ve aksi takdirde gerekli olan ağ geçidiyle karşılaştırılabilir.
  • Bir performans sorunu ortaya çıkarsa, bir geliştirici için tipik iş akışı 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 taneli kod yürütülmesine rağmen sorunun kaynağını hızlı bir şekilde belirlemede hala etkilidir. Başka bir deyişle, ölçümler daha kaba hale gelir ve sorunun kaynağını tam olarak belirleyemez, ancak daha ayrıntılı araçlar hala etkilidir.

Bir konaktan istemci alma

.NET Genel Ana Bilgisayarı kullanılarak barındırılırsa, istemci konağın bağımlılık ekleme kapsayıcısında otomatik olarak kullanılabilir ve ASP.NET denetleyicileri veya IHostedService uygulamaları gibi hizmetlere eklenebilir.

Alternatif olarak, veya IClusterClient gibi IGrainFactory bir istemci arabiriminden ISiloHostelde edilebilir:

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

Dış istemciler

İstemci kodu, tahıl kodunun Orleans barındırıldığı kümenin dışında çalıştırılabilir. Bu nedenle, dış istemci kümeye ve uygulamanın tüm dilimlerine bağlayıcı veya kanal işlevi görür. İstemciler genellikle iş mantığını yürüten taneciklerle orta katman görevi görecek bir Orleans kümeye bağlanmak için ön uç web sunucularında kullanılır.

Tipik bir kurulumda ön uç web sunucusu:

Taneli istemcinin başlatılması

Kümede Orleans barındırılan taneciklere çağrı yapmak için bir taneli istemcinin kullanılabilmesi için önce yapılandırılması, başlatılması ve kümeye bağlanması gerekir.

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

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

// 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 başlatıldığında, istemci yapılandırılır ve kendi yapılandırılmış hizmet sağlayıcısı örneği aracılığıyla kullanılabilir.

Yapılandırma, bir istemciyi program aracılığıyla yapılandırmak için yapılandırma özellikleri hiyerarşisi içeren ve birkaç ek seçenek sınıfı aracılığıyla ClientBuilder sağlanır. 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.ConnectionString = connectionString)
    .ConfigureApplicationParts(
        parts => parts.AddApplicationPart(typeof(IValueGrain).Assembly))
    .Build();

Son olarak, kümeye bağlanması Orleans için, oluşturduğunuz istemci nesnesinde yöntemini çağırmanız Connect() gerekir. Bir döndüren zaman uyumsuz bir Taskyöntemdir. Bu nedenle veya .Wait()ile await tamamlanmasını beklemeniz gerekir.

await client.Connect();

Tahıllara çağrı yapma

Bir istemciden grain çağrısı yapmak, bu tür çağrıları tahıl kodu içinden yapmaktan farklı değildir. Hedef tanecik arabirimi olan T aynı IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) yöntem, her iki durumda da taneli başvurular elde etmek için kullanılır. Fark, hangi fabrika nesnesinin çağrıldığındadır IGrainFactory.GetGrain. İ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ı, hub'lı arabirim kurallarının gerektirdiği şekilde bir Task Task<TResult> veya döndürür. İstemci, iş parçacığını await engellemeden döndürülenleri Task zaman uyumsuz olarak beklemek veya bazı durumlarda Wait() geçerli yürütme iş parçacığını engellemek için yöntemini beklemek için anahtar sözcüğünü kullanabilir.

İstemci kodundan ve başka bir dilimin içinden taneciklere çağrı yapma arasındaki en büyük fark, taneciklerin tek iş parçacıklı yürütme modelidir. Tanecikler çalışma zamanı tarafından Orleans tek iş parçacıklı olacak şekilde kısıtlanırken, istemciler çok iş parçacıklı olabilir. Orleans istemci tarafında böyle bir garanti sağlamaz ve bu nedenle ortamı için uygun eşitleme yapılarını (kilitler, olaylar ve Tasks) kullanarak eşzamanlılığını yönetmek istemciye bağlıdır.

Bildirimler alma

Basit bir istek-yanıt deseninin yeterli olmadığı ve istemcinin zaman uyumsuz bildirimler alması gereken durumlar vardır. Örneğin, bir kullanıcı takip ettiği biri tarafından yeni bir ileti yayımlandığında bildirim almak isteyebilir.

Gözlemcilerin kullanımı, istemci tarafı nesnelerinin tanecik benzeri hedefler olarak kullanıma sunarak tanecikler tarafından çağrılmalarını 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 kodunun sorumluluğundadır.

İstemcilere zaman uyumsuz iletiler teslim etmek için kullanılabilecek bir diğer mekanizma da Akışlar. Akışlar tek tek iletilerin tesliminin başarılı veya başarısız olduğuna ilişkin göstergeler ortaya çıkarır ve bu nedenle istemciyle 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 alınan ayrıntılı başvurularda çağrı yapılırken.

İlk durumda, istemci bir siloya bağlanmayı dener. İstemci herhangi bir siloya bağlanamazsa, neyin yanlış gittiğini belirtmek için bir özel durum oluşturur. Özel durumu işlemek ve yeniden deneyip denememeye karar vermek için bir IClientConnectionRetryFilter kaydedebilirsiniz. Yeniden deneme filtresi sağlanmadıysa veya yeniden deneme filtresi döndürüyorsa false, istemci tam 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:

  • Yöntemi başlangıçta IClusterClient.Connect() çağrıldığında.
  • Bağlı bir küme istemcisinden alınan ayrıntılı başvurularda çağrı yapılırken.

İlk durumda yöntemi, Connect neyin yanlış gittiğini belirtmek için bir özel durum oluşturur. 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ğlanabilir. Bu işlev, örneğin başka bir deneme yapmadan önce belirtilen süreyi bekleyebilir. Yeniden deneme filtresi sağlanmadıysa veya yeniden deneme filtresi döndürüyorsa false, istemci tam olarak vazgeçer.

Başarılı bir şekilde döndürürse Connect , küme istemcisi atılana kadar kullanılabilir olması garanti edilir. Başka bir deyişle, istemci bağlantı sorunlarıyla karşılaşsa bile süresiz olarak kurtarma girişiminde bulunur. Tam kurtarma davranışı, tarafından sağlanan bir GatewayOptions nesnede ClientBuilderyapılandırılabilir; örneğin:

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

İkinci durumda, bir taneli çağrı sırasında bir bağlantı sorunu oluştuğunda, istemci tarafında bir SiloUnavailableException oluşturulur. Bu işlem şu şekilde işlenebilir:

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

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

Bu durumda gren başvurusu geçersiz kılınmamış; daha sonra bir bağlantı yeniden kurulduğunda çağrı aynı başvuruda yeniden denenebilir.

Bağımlılık ekleme

.NET Genel Ana Bilgisayarı kullanan bir programda dış istemci oluşturmanın önerilen yolu, bağımlılık ekleme yoluyla tek bir IClusterClient örnek eklemektir. Bu örnek, barındırılan hizmetlerde, ASP.NET denetleyicilerde ve benzeri durumlarda oluşturucu parametresi olarak kabul edilebilir.

Not

Bir Orleans siloyu bağlanacak aynı işlemde birlikte barındırırken, el ile bir istemci oluşturmak gerekli değildir ; Orleans otomatik olarak bir tane 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();
    }
}

Hizmet daha sonra şu şekilde kaydedilir:

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

Örnek

Aşağıda, öğesine bağlanan Orleansbir istemci uygulamasının yukarıda verilen genişletilmiş bir sürümü verilmiştir. Oyuncu hesabını bulur, oyuncunun bir gözlemciyle birlikte olduğu oyun oturumu güncelleştirmelerine abone olur ve program el ile sonlandırılana kadar bildirimleri yazdırır.

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