共用方式為


Orleans 客戶

用戶端可讓非粒紋程式代碼與 Orleans 叢集互動。 客户端可讓應用代碼與叢集中裝載的 "Grain" 和串流進行通訊。 根據您託管客戶端程式代碼的位置,有兩種方式可以取得客戶端:在與 Silo 相同的進程中運行,或在獨立的進程中運行。 本文討論這兩個選項,從建議的做法開始:在與 Grain 程式碼相同的程序中共置用戶端程式碼。

共同托管的用戶

如果您在與 Grain 程式碼相同的進程中托管客戶端程式代碼,您可以直接從主控應用程式的相依性注入容器取得客戶端。 在此情況下,用戶端會直接與所連接的儲存區通訊,並可以利用儲存區對叢集的額外知識。

這種方法提供數個優點,包括降低網路和CPU額外負荷、降低延遲,以及提高輸送量和可靠性。 客戶端會使用資料筒倉的知識來了解叢集拓撲和狀態,且不需要單獨的網關。 這可避免網路跳躍和序列化/反序列化的往返過程,藉由將用戶端與穀粒之間的必要節點數量降至最低,從而提高可靠性。 如果粒是無狀態工作粒,或者恰好在託管客戶端的同一個孤立主機上被啟動,則完全不需要序列化或網路通訊,讓客戶端能夠實現更高的效能和可靠性。 將用戶端和grain程式碼共同託管能夠簡化部署和應用程式拓撲,因為不需要另外部署和監視兩個不同的應用程式二進位檔。

此方法也有缺點,主要是細粒度的代碼不再與客戶端程序隔離。 因此,客戶端程式碼中的問題,例如封鎖 I/O 或鎖定競爭導致線程饑荒,可能會影響 grain 程式碼的效能。 即使沒有這類程式代碼缺陷,仍可能會發生嘈雜的 鄰近 效果,因為用戶端程序代碼會在與粒紋程序代碼相同的處理器上執行,對 CPU 快取造成額外壓力,並增加本機資源的爭用。 此外,識別這些問題的來源會變得更加困難,因為監視系統無法以邏輯方式區分用戶端程式代碼和粒紋程序代碼。

儘管有這些缺點,但共置用戶端程式碼和Grain程式碼是大多數應用程式的熱門選項與建議方法。 基於下列原因,上述缺點在實際情況中通常是微不足道的:

  • 用戶端程式代碼通常非常 精簡 (例如,將連入 HTTP 要求轉譯為粒紋呼叫)。 因此,吵鬧鄰居 效應很低,且成本可與其他必要的網關相提並論。
  • 如果發生效能問題,您的一般工作流程可能會牽涉到 CPU 分析工具和調試程式等工具。 這些工具在快速識別問題來源方面仍然有效,即使用戶端和粒度代碼在同一個進程中執行也一樣。 換句話說,雖然計量變得粗略,而且無法精確識別問題的來源,但更詳細的工具仍然有效。

從主機取得用戶端

如果您使用 .NET Generic Host 進行裝載,用戶端會自動在主機的 dependency injection 容器中可用。 您可以將它插入 ASP.NET 控制器IHostedService 實作等服務。

或者,您也可以從 IGrainFactory 取得用戶端介面,例如 IClusterClientISiloHost

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

外部用戶端

用戶端程式代碼可以在裝載粒紋程式代碼的 Orleans 叢集外部執行。 在此情況下,外部用戶端會做為叢集和所有應用程式粒紋的連接器或管道。 一般而言,您會使用前端 Web 伺服器的客戶端來連接到充當中間層的 Orleans 叢集,而這些叢集中的 Grain 用於執行業務邏輯。

在一般設定中,前端 Web 伺服器:

  • 接收 Web 要求。
  • 執行必要的驗證和授權驗證。
  • 決定哪些粒子應該處理請求。
  • 使用 Microsoft。Orleans用戶端 NuGet 套件,來對 Grain 進行一次或多次方法呼叫。
  • 處理 Grain 呼叫的成功或失敗,以及任何傳回的值。
  • 傳送回應至 Web 要求。

初始化粒度客戶端

您必須先設定、初始化並將其連線到叢集,才能使用粒紋用戶端來呼叫裝載於 Orleans 叢集中的粒紋。

透過UseOrleansClient以及數個包含組態屬性層次的補充選項類別來提供組態,以便透過程式設計方式設定用戶端。 如需詳細資訊,請參閱 客戶端設定

請考慮下列用戶端組態範例:

// 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 時, 用戶端已配置完成, 並可用於其建構的服務提供者實例。

透過ClientBuilder以及數個包含組態屬性層次的補充選項類別來提供組態,以便透過程式設計方式設定用戶端。 如需詳細資訊,請參閱 客戶端設定

用戶端組態的範例:

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

最後,您必須在建構的客戶端物件上呼叫 Connect() 方法,以將它連線到 Orleans 叢集。 這是一個異步方法,返回Task,因此您必須使用await.Wait()以等待其完成。

await client.Connect();

呼叫計算單元

從用戶端對粒紋進行呼叫與 從粒紋程式代碼中進行這類呼叫並無不同。 在這兩種情況下,使用相同的方法(其中 IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) 是目標穀粒接口),T。 不同之處在於哪個 Factory 物件叫用 IGrainFactory.GetGrain。 在用戶端程式代碼中,您會透過連線的客戶端物件執行此動作,如下列範例所示:

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

await joinGameTask;

對 grain 方法的呼叫會根據 Task 傳回 Task<TResult>。 用戶端可以使用 await 關鍵詞,以異步方式等候傳 Task 回的 ,而不封鎖線程,或在某些情況下,使用 Wait() 方法來封鎖目前的執行線程。

從客戶端程式碼對grain進行呼叫,與從另一個grain內部呼叫,主要差異在於grain的單執行緒執行模式。 執行時環境 Orleans 將粒度限制為單線程,而用戶端可以是多執行緒。 Orleans 不會在用戶端上提供任何這類保證,因此客戶端必須針對其環境使用適當的同步處理建構來管理其並行:鎖定、事件等 Tasks

接收通知

有時候,簡單的要求-回應模式是不夠的,而且用戶端需要接收異步通知。 例如,當用戶追蹤的人發佈新訊息時,使用者可能會想要通知。

使用 觀察者 作為一種機制,可以讓客戶端物件暴露為可被粒子調用的類似粒子的目標。 對觀察者發出的呼叫無法提供任何成功或失敗的保證,因為它們是以單向、盡力而為的方式傳送訊息。 因此,應用程式程式代碼有責任在必要時在觀察者之上建置較高層級的可靠性機制。

將異步訊息傳遞至用戶端的另一個機制是 Streams。 串流會顯示個別訊息的傳遞成功或失敗的指示,以確保與用戶端的可靠通訊。

用戶端連線能力

叢集用戶端可能會遇到連線問題的情況有兩種:

  • 當客戶端嘗試連線到孤島時。
  • 對從聯機叢集用戶端取得的粒紋參考進行呼叫時。

在第一個案例中,客戶端會嘗試連線到資料儲存庫。 如果客戶端無法連線到任何模組,則會拋出例外並指出錯誤原因。 您可以註冊 IClientConnectionRetryFilter 來處理例外狀況,並決定是否要重試。 如果您未提供重試篩選條件,或重試篩選傳回 false,則用戶端會永久放棄。

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

叢集用戶端可能會遇到連線問題的情況有兩種:

  • 當最初呼叫IClusterClient.Connect()方法時。
  • 對從聯機叢集用戶端取得的粒紋參考進行呼叫時。

在第一個案例中 Connect ,方法會擲回例外狀況,指出發生錯誤的情況。 這通常是(但不一定是)SiloUnavailableException。 如果發生這種情況,叢集用戶端實例將無法使用,而且應該處置。 您可以選擇性地將重試篩選函式提供給Connect方法,例如,可以在進行另一次嘗試前等待指定的時間。 如果您未提供重試篩選條件,或重試篩選傳回 false,則用戶端會永久放棄。

如果 Connect 成功傳回,則叢集用戶端一定會可供使用,直到處置為止。 這表示即使用戶端遇到連線問題,仍會嘗試無限期復原。 您可以在GatewayOptions提供的物件上設定ClientBuilder確切的復原行為,例如:

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

在第二個案例中,當在 Grain 呼叫期間發生連線問題時,會在客戶端拋出 SiloUnavailableException 。 您可以像這樣處理:

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

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

在此情況下,粒紋參考不會失效;您可以在稍後重新建立連線時,在相同的參考上重試呼叫。

依賴注入

在程式中使用 .NET 泛型主機建立外部客戶端的建議方式是透過相依性注入單例 IClusterClient 實例。 然後,此實例就可以在託管服務、ASP.NET 控制器等中接受為建構函式參數。

備註

在與它進行連線的同一個進程中共同裝載 Orleans silo 時,不需要手動建立客戶端;Orleans將自動提供一個並適當地管理其生命週期。

在不同的進程中聯機到叢集時(在不同的計算機上),常見的模式是建立如下的託管服務:

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

註冊服務,如下所示:

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

範例

以下是上一個範例的擴充版本,顯示一個用戶端應用程式如何連線至 Orleans,尋找玩家帳戶,透過使用觀察者訂閱與玩家所屬遊戲會話相關的更新,並持續列印通知,直到程式被手動終止為止。

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