Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Klient umożliwia interakcję kodu innego niż ziarno z klastrem Orleans . Klienci umożliwiają kodowi aplikacji komunikowanie się z ziarnami i strumieniami hostowanymi w klastrze. Istnieją dwa sposoby uzyskania klienta, w zależności od tego, gdzie hostujesz kod klienta: albo w tym samym procesie co silos, albo w osobnym procesie. W tym artykule omówiono obie opcje, począwszy od zalecanego podejścia: współdzielenia kodu klienta w tym samym procesie, co kod modułu ziarna.
Klienci współgospodarze
Jeśli hostujesz kod klienta w tym samym procesie co kod ziarna, możesz bezpośrednio uzyskać klienta z kontenera wstrzykiwania zależności aplikacji hostującej. W takim przypadku klient komunikuje się bezpośrednio z silosem dołączonym do niego i może wykorzystać dodatkową wiedzę silosu na temat klastra.
Takie podejście zapewnia kilka korzyści, w tym zmniejszenie obciążenia sieciowego i procesora CPU, zmniejszenie opóźnienia oraz zwiększoną przepływność i niezawodność. Klient korzysta z wiedzy silosu o topologii i stanie klastra i nie potrzebuje oddzielnej bramy. Pozwala to uniknąć przeskoku sieciowego oraz procesów serializacji i deserializacji, zwiększa tym samym niezawodność poprzez minimalizację liczby wymaganych węzłów między klientem a daną jednostką obliczeniową. Jeśli ziarno jest bezstanowym ziarnem roboczym lub zostanie zaktywowane na tym samym silosie, który hostuje klienta, nie jest wymagana serializacja ani komunikacja sieciowa, co pozwala klientowi uzyskać dodatkowe korzyści w zakresie wydajności i niezawodności. Współdzielenie hostingu dla klienta i kodu ziarna również upraszcza wdrażanie i topologię aplikacji, usuwając konieczność wdrażania i monitorowania dwóch odrębnych plików binarnych aplikacji.
Istnieją również wady tego podejścia, głównie dlatego, że kod ziarna nie jest już odizolowany od procesu klienta. W związku z tym problemy w kodzie klienta, takie jak blokowanie I/O lub rywalizacja o blokadę prowadząca do wyczerpania zasobów wątku, mogą wpływać na wydajność kodu ziarna. Nawet bez takich wad kodu, efekty hałaśliwego sąsiada mogą wystąpić po prostu dlatego, że kod klienta jest wykonywany na tym samym procesorze co kod ziarna, co zwiększa obciążenie pamięci podręcznej procesora CPU oraz zwiększa konkurencję o zasoby lokalne. Ponadto zidentyfikowanie źródła tych problemów staje się trudniejsze, ponieważ systemy monitorowania nie mogą logicznie rozróżniać kodu klienta i kodu ziarna.
Pomimo tych wad, łączny hosting kodu klienta wraz z kodem ziarna jest popularną praktyką i zalecanym podejściem w większości aplikacji. Wyżej wymienione wady są często minimalne w praktyce z następujących powodów:
- Kod klienta jest często bardzo lekki (np. mapowanie przychodzących żądań HTTP na wywołania ziarna). W związku z tym efekty zakłócającego sąsiada są minimalne i porównywalne kosztowo do bramy, która byłaby inaczej wymagana.
- Jeśli wystąpi problem z wydajnością, typowy przepływ pracy prawdopodobnie obejmuje narzędzia, takie jak profileery procesora CPU i debugery. Te narzędzia pozostają skuteczne w szybkim identyfikowaniu źródła problemu, nawet gdy kod klienta i kod ziarna są wykonywane w ramach tego samego procesu. Innymi słowy, podczas gdy metryki stają się grubsze i mniej w stanie precyzyjnie zidentyfikować źródło problemu, bardziej szczegółowe narzędzia są nadal skuteczne.
Uzyskiwanie klienta z hosta
Jeśli hostujesz przy użyciu hosta ogólnego platformy .NET, klient jest automatycznie dostępny w kontenerze iniekcji zależności hosta. Można wstrzyknąć go do usług, takich jak kontrolery ASP.NET lub IHostedService implementacje.
Alternatywnie można uzyskać interfejs klienta, taki jak IGrainFactory lub IClusterClient z witryny ISiloHost:
var client = host.Services.GetService<IClusterClient>();
await client.GetGrain<IMyGrain>(0).Ping();
Klienci zewnętrzni
Kod klienta może działać poza klastrem Orleans , w którym jest hostowany kod ziarna. W takim przypadku klient zewnętrzny działa jako łącznik lub kanał do klastra i wszystkich ziaren aplikacji. Zazwyczaj używane są klienci na serwerach internetowych frontendowych do łączenia się z klastrem, który służy jako warstwa pośrednia, gdzie ziarna wykonują logikę biznesową.
W typowej konfiguracji serwer frontendowy:
- Odbiera żądanie internetowe.
- Wykonuje niezbędne uwierzytelnianie i walidację autoryzacji.
- Decyduje, które jednostki przetwarzające powinny przetworzyć żądanie.
- Używa pakietu NuGet Orleans, aby wykonać jedno lub więcej wywołań metod do ziaren.
- Obsługuje pomyślne ukończenie lub niepowodzenia wywołań ziaren i wszystkie zwrócone wartości.
- Wysyła odpowiedź na żądanie internetowe.
Inicjowanie klienta ziarna
Aby można było użyć klienta ziaren do wywoływania ziaren hostowanych w klastrze Orleans, należy go skonfigurować, zainicjować i połączyć z klastrem.
Podaj konfigurację za pośrednictwem UseOrleansClient oraz kilku dodatkowych klas opcji zawierających hierarchię właściwości konfiguracyjnych w celu programowego skonfigurowania klienta. Aby uzyskać więcej informacji, zobacz Konfiguracja klienta.
Rozważmy następujący przykład konfiguracji klienta:
// 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();
Po uruchomieniu host
programu klient jest skonfigurowany i dostępny także za pośrednictwem jego skonstruowanej instancji dostawcy usług.
Podaj konfigurację za pośrednictwem ClientBuilder oraz kilku dodatkowych klas opcji zawierających hierarchię właściwości konfiguracyjnych w celu programowego skonfigurowania klienta. Aby uzyskać więcej informacji, zobacz Konfiguracja klienta.
Przykład konfiguracji klienta:
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();
Na koniec należy wywołać metodę Connect()
na skonstruowanym obiekcie klienta, aby połączyć go z klastrem Orleans . Jest to metoda asynchroniczna zwracająca Task
, więc musisz poczekać na jej ukończenie, używając await
lub .Wait()
.
await client.Connect();
Nawiązywanie wywołań do jednostek ziarna
Wykonywanie wywołań do ziarna od klienta nie różni się od wykonywania takich wywołań z poziomu kodu ziarna. Użyj tej samej IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) metody (gdzie T
jest interfejsem ziarna docelowego) w obu przypadkach , aby uzyskać odwołania do ziarna. Różnica polega na tym, który obiekt fabryki wywołuje IGrainFactory.GetGrain. W kodzie klienta można to zrobić za pośrednictwem połączonego obiektu klienta, jak pokazano w poniższym przykładzie:
IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
Task joinGameTask = player.JoinGame(game)
await joinGameTask;
Wywołanie metody ziarna zwraca wartość Task lub Task<TResult>, zgodnie z wymaganiami reguł interfejsu ziarna. Klient może użyć słowa kluczowego await
aby asynchronicznie oczekiwać na zwrócony Task
bez blokowania wątku lub w niektórych przypadkach użyć metody Wait()
aby zablokować bieżący wątek.
Główną różnicą między wykonywaniem wywołań do ziaren z kodu klienta a wywołaniami z wnętrza innego ziarna jest jednowątkowy model wykonywania ziaren. Środowisko uruchomieniowe Orleans wymusza jednordzeniowość na ziarnach, podczas gdy klienci mogą być wielowątkowi.
Orleans nie zapewnia żadnej gwarancji po stronie klienta, dlatego klient musi zarządzać współbieżnością, używając odpowiednich konstrukcji synchronizacji dla swojego środowiska — blokady, zdarzeń, Tasks
itp.
Odbieranie powiadomień
Czasami prosty wzorzec odpowiedzi na żądanie nie jest wystarczający, a klient musi otrzymywać powiadomienia asynchroniczne. Na przykład użytkownik może chcieć otrzymywać powiadomienia, gdy ktoś, kogo obserwuje, publikuje nową wiadomość.
Korzystanie z obserwatorów to jeden mechanizm umożliwiający narażenie obiektów po stronie klienta jako obiektów przypominających ziarna, które mają być wywoływane przez ziarna. Wywołania obserwatorów nie dają żadnych oznak sukcesu ani porażki, ponieważ są wysyłane jako jednokierunkowe wiadomości z dołożeniem wszelkich starań. W związku z tym kod aplikacji jest odpowiedzialny za utworzenie mechanizmu wyższej niezawodności w razie potrzeby na bazie obserwatorów.
Innym mechanizmem dostarczania komunikatów asynchronicznych do klientów jest usługa Streams. Strumienie ukazują oznaki powodzenia lub niepowodzenia przy dostarczaniu pojedynczych komunikatów, umożliwiając niezawodną komunikację z powrotem do klienta.
Łączność klienta
Istnieją dwa scenariusze, w których klient klastra może napotykać problemy z łącznością:
- Gdy klient próbuje nawiązać połączenie z silosem.
- Podczas nawiązywania wywołań na podstawie odwołań do ziarna uzyskanych od połączonego klienta klastra.
W pierwszym przypadku klient próbuje nawiązać połączenie z silosem. Jeśli klient nie może nawiązać połączenia z żadnym silosem, zgłasza wyjątek wskazujący, co poszło nie tak. Możesz zarejestrować obiekt IClientConnectionRetryFilter w celu obsługi wyjątku i zdecydować, czy ponowić próbę. Jeśli nie podasz filtru ponawiania lub jeśli filtr ponawiania zwróci false
, klient trwale zaprzestaje prób.
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;
}
}
Istnieją dwa scenariusze, w których klient klastra może napotykać problemy z łącznością:
- Gdy początkowo wywoływana jest metoda IClusterClient.Connect().
- Podczas nawiązywania wywołań na podstawie odwołań do ziarna uzyskanych od połączonego klienta klastra.
W pierwszym przypadku metoda Connect
zgłasza wyjątek, wskazując, co poszło nie tak. Zazwyczaj jest to (ale niekoniecznie) SiloUnavailableException. W takim przypadku instancja klienta klastra jest nieużywalna i powinna zostać zlikwidowana. Opcjonalnie możesz podać funkcję filtru ponawiania prób do Connect
metody, która może na przykład poczekać określony czas trwania przed podjęciem kolejnej próby. Jeśli nie podasz filtru ponawiania lub jeśli filtr ponawiania zwróci false
, klient trwale zaprzestaje prób.
Jeżeli Connect
zakończy się sukcesem, klient klastra jest gwarantowany do użytkowania aż do chwili usunięcia. Oznacza to, że nawet jeśli klient napotyka problemy z połączeniem, próbuje się ponownie połączyć w nieskończoność. Możesz skonfigurować dokładne zachowanie odzyskiwania za pomocą obiektu GatewayOptions dostarczonego przez ClientBuilder, np.:
var client = new ClientBuilder()
// ...
.Configure<GatewayOptions>(
options => // Default is 1 min.
options.GatewayListRefreshPeriod = TimeSpan.FromMinutes(10))
.Build();
W drugim przypadku, gdy podczas wywołania ziarna występuje problem z połączeniem, po stronie klienta jest wyrzucany SiloUnavailableException. Możesz to zrobić w następujący sposób:
IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
try
{
await player.JoinGame(game);
}
catch (SiloUnavailableException)
{
// Lost connection to the cluster...
}
Odwołanie do ziarna nie zostaje unieważnione w tej sytuacji, więc możesz spróbować ponowić połączenie z tym samym odniesieniem później, gdy połączenie może zostać ponownie nawiązane.
Wstrzykiwanie zależności
Zalecanym sposobem utworzenia klienta zewnętrznego w programie przy użyciu .NET Generic Host jest wstrzyknięcie singletonowej instancji IClusterClient za pośrednictwem wstrzykiwania zależności. To wystąpienie można następnie zaakceptować jako parametr konstruktora w usługach hostowanych, ASP.NET kontrolerach itp.
Uwaga
W przypadku współgospodarzowania silosu Orleans w tym samym procesie, który będzie się z nim łączyć, nie jest konieczne ręczne utworzenie klienta; Orleans automatycznie zapewni jeden i odpowiednio zarządza jego okresem istnienia.
Podczas nawiązywania połączenia z klastrem w innym procesie (na innej maszynie) typowym wzorcem jest utworzenie hostowanej usługi w następujący sposób:
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();
}
}
Zarejestruj usługę w następujący sposób:
await Host.CreateDefaultBuilder(args)
.UseOrleansClient(builder =>
{
builder.UseLocalhostClustering();
})
.ConfigureServices(services =>
{
services.AddHostedService<ClusterClientHostedService>();
})
.RunConsoleAsync();
Przykład
Oto rozszerzona wersja poprzedniego przykładu przedstawiająca aplikację kliencką, która łączy się z Orleans, znajduje konto gracza, subskrybuje aktualizacje do sesji gry, w której gracz uczestniczy, korzystając z obserwatora, i drukuje powiadomienia do momentu ręcznego zakończenia programu.
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);
}
}