Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Um cliente permite que código não granulado interaja com um Orleans cluster. Os clientes permitem que o código do aplicativo se comunique com grãos e fluxos hospedados em um cluster. Há duas maneiras de obter um cliente, dependendo de onde você hospeda o código do cliente: no mesmo processo que um silo ou em um processo separado. Este artigo discute ambas as opções, começando com a abordagem recomendada: co-hospedar o código do cliente no mesmo processo que o código grain.
Clientes co-hospedados
Se você hospedar o código do cliente no mesmo processo que o código grain, poderá obter diretamente o cliente do contêiner de injeção de dependência do aplicativo de hospedagem. Nesse caso, o cliente se comunica diretamente com o silo ao qual está conectado e pode aproveitar o conhecimento extra do silo sobre o cluster.
Essa abordagem oferece vários benefícios, incluindo sobrecarga de rede e CPU reduzida, latência reduzida e maior taxa de transferência e confiabilidade. O cliente usa o conhecimento do silo sobre a topologia e o estado do cluster e não precisa de um gateway separado. Isso evita uma passagem de rede e um ciclo completo de serialização e desserialização, aumentando assim a confiabilidade ao minimizar o número de nós necessários entre o cliente e o recurso específico. Se o grão for um grão de trabalhador sem estado ou for ativado no mesmo silo em que o cliente está hospedado, nenhuma serialização ou comunicação de rede será necessária, permitindo que o cliente obtenha ganhos adicionais de desempenho e confiabilidade. O cliente de co-hospedagem e o código grain também simplifica a implantação e a topologia do aplicativo, eliminando a necessidade de implantar e monitorar dois binários de aplicativos distintos.
Há também desvantagens nessa abordagem, principalmente que o código grain não está mais isolado do processo do cliente. Portanto, problemas no código do cliente, como bloqueio de E/S ou contenção de bloqueio causando inanição de thread, podem afetar o desempenho do código de grão. Mesmo sem tais defeitos de código, efeitos vizinhos barulhentos podem ocorrer simplesmente porque o código do cliente é executado no mesmo processador que o código grain, colocando pressão adicional no cache da CPU e aumentando a contenção por recursos locais. Além disso, identificar a origem desses problemas torna-se mais difícil porque os sistemas de monitoramento não conseguem distinguir logicamente entre o código do cliente e o código grain.
Apesar dessas desvantagens, co-hospedar código de cliente com código grain é uma opção popular e a abordagem recomendada para a maioria dos aplicativos. Os inconvenientes acima mencionados são muitas vezes mínimos na prática pelas seguintes razões:
- O código do cliente geralmente é muito fino (por exemplo, traduzindo solicitações HTTP recebidas em chamadas grain). Portanto, os efeitos de 'vizinho barulhento' são mínimos e comparáveis ao custo do gateway necessário.
- Se surgir um problema de desempenho, o seu fluxo de trabalho típico provavelmente envolve ferramentas como perfiladores de CPU e depuradores. Essas ferramentas permanecem eficazes na identificação rápida da origem do problema, mesmo com o cliente e o código grain sendo executados no mesmo processo. Em outras palavras, embora as métricas se tornem mais grosseiras e menos capazes de identificar com precisão a origem do problema, ferramentas mais detalhadas ainda são eficazes.
Obter um cliente de um host
Se você hospedar usando o .NET Generic Host, o cliente estará automaticamente disponível no contêiner de injeção de dependência do host. Você pode injetá-lo em serviços como controladores de ASP.NET ou IHostedService implementações.
Como alternativa, você pode obter uma interface de cliente como IGrainFactory ou IClusterClient de ISiloHost:
var client = host.Services.GetService<IClusterClient>();
await client.GetGrain<IMyGrain>(0).Ping();
Clientes externos
O código do cliente pode ser executado fora do cluster onde o Orleans código grain está hospedado. Nesse caso, um cliente externo atua como um conector ou canal para o cluster e todos os grãos do aplicativo. Normalmente, você usa clientes em servidores Web frontend para se conectar a um Orleans cluster que serve como uma camada intermediária, com grãos executando lógica de negócios.
Em uma configuração típica, um servidor Web frontend:
- Recebe uma solicitação da Web.
- Executa a autenticação necessária e a validação de autorização.
- Decide qual(is) grão(s) deve(m) processar o pedido.
- Usa o pacote NuGet Microsoft.Orleans.Client para fazer uma ou mais chamadas de método para os grãos.
- Lida com a conclusão bem-sucedida ou falhas das chamadas de grão e quaisquer valores retornados.
- Envia uma resposta ao pedido web.
Inicializar um cliente de grãos
Antes de poder usar um cliente grain para fazer chamadas para grãos hospedados em um Orleans cluster, você precisa configurá-lo, inicializá-lo e conectá-lo ao cluster.
Forneça configuração via UseOrleansClient e várias classes de opção suplementares contendo uma hierarquia de propriedades de configuração para configurar programaticamente um cliente. Para obter mais informações, consulte Configuração do cliente.
Considere o seguinte exemplo de uma configuração de cliente:
// 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();
Quando você inicia o host, o cliente é configurado e está disponível por meio de sua instância de provedor de serviços construída.
Forneça configuração via ClientBuilder e várias classes de opção suplementares contendo uma hierarquia de propriedades de configuração para configurar programaticamente um cliente. Para obter mais informações, consulte Configuração do cliente.
Exemplo de uma configuração de cliente:
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();
Finalmente, você precisa chamar o Connect() método no objeto cliente construído para conectá-lo ao Orleans cluster. É um método assíncrono que retorna um Task, então você precisa aguardar sua conclusão usando await ou .Wait().
await client.Connect();
Fazer chamadas para unidades de processamento
Fazer chamadas para grains de um cliente não é diferente de fazer essas chamadas de dentro do código de grain. Use o mesmo IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) método (onde T é a interface de destino do grão) em ambos os casos para obter referências de grão. A diferença está em qual objeto de fábrica invoca IGrainFactory.GetGrain. No código do cliente, você faz isso por meio do objeto de cliente conectado, como mostra o exemplo a seguir:
IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
Task joinGameTask = player.JoinGame(game)
await joinGameTask;
Uma chamada para um método grain retorna um Task ou Task<TResult>, conforme exigido pelas regras da interface grain. O cliente pode usar a await palavra-chave para aguardar de forma assíncrona o retorno Task sem bloquear o thread ou, em alguns casos, usar o Wait() método para bloquear o thread atual de execução.
A principal diferença entre fazer chamadas aos grains a partir do código do cliente e de dentro de outro grain é o modelo de execução de um único thread dos grains. O Orleans runtime limita os grãos a serem single-threaded, enquanto os clientes podem ser multi-threaded.
Orleans não fornece nenhuma garantia desse tipo no lado do cliente, então cabe ao cliente gerenciar sua simultaneidade usando construções de sincronização apropriadas para seu ambiente — bloqueios, eventos, Tasksetc.
Receber notificações
Às vezes, um padrão simples de solicitação-resposta não é suficiente, e o cliente precisa receber notificações assíncronas. Por exemplo, um usuário pode querer notificação quando alguém que ele segue publica uma nova mensagem.
O uso de Observadores é um mecanismo que permite a exposição de objetos do lado do cliente como alvos semelhantes a 'grain' que podem ser invocados por 'grains'. As chamadas para observadores não fornecem qualquer indicação de sucesso ou fracasso, uma vez que são enviadas como mensagens unidirecionais e de melhor esforço. Portanto, é responsabilidade do código da sua aplicação criar um mecanismo de confiabilidade de nível superior sobre os observadores, quando necessário.
Outro mecanismo para entregar mensagens assíncronas aos clientes é o Streams. Os fluxos expõem indícios de sucesso ou falha na entrega de mensagens individuais, permitindo uma comunicação confiável de volta ao cliente.
Conectividade do cliente
Há dois cenários em que um cliente de cluster pode enfrentar problemas de conectividade:
- Quando o cliente tenta se conectar a um silo.
- Ao fazer chamadas em referências de grãos obtidas de um cliente de cluster conectado.
No primeiro caso, o cliente tenta se conectar a um silo. Se o cliente não puder se conectar a nenhum silo, ele lançará uma exceção indicando o que deu errado. Você pode registrar um IClientConnectionRetryFilter para lidar com a exceção e decidir se deseja repetir novamente. Se você não fornecer nenhum filtro de repetição ou se o filtro de repetição retornar false, o cliente desiste permanentemente.
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;
}
}
Há dois cenários em que um cliente de cluster pode enfrentar problemas de conectividade:
- Quando o IClusterClient.Connect() método é chamado inicialmente.
- Ao fazer chamadas em referências de grãos obtidas de um cliente de cluster conectado.
No primeiro caso, o Connect método lança uma exceção indicando o que deu errado. Este é tipicamente (mas não necessariamente) um SiloUnavailableException. Se isso acontecer, a instância do cliente de cluster não poderá ser utilizada e deverá ser descartada. Opcionalmente, você pode fornecer uma função de filtro de repetição para o Connect método, que pode, por exemplo, esperar por uma duração especificada antes de fazer outra tentativa. Se você não fornecer nenhum filtro de repetição ou se o filtro de repetição retornar false, o cliente desiste permanentemente.
Se Connect retornar com êxito, o cliente de cluster terá a garantia de ser utilizável até ser descartado. Isso significa que, mesmo que o cliente tenha problemas de conexão, ele tenta se recuperar indefinidamente. Você pode configurar o comportamento exato de recuperação em um GatewayOptions objeto fornecido pelo ClientBuilder, por exemplo:
var client = new ClientBuilder()
// ...
.Configure<GatewayOptions>(
options => // Default is 1 min.
options.GatewayListRefreshPeriod = TimeSpan.FromMinutes(10))
.Build();
No segundo caso, quando ocorre um problema de conexão durante uma chamada de 'grain,' um SiloUnavailableException é gerado no lado do cliente. Você poderia lidar com isso assim:
IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
try
{
await player.JoinGame(game);
}
catch (SiloUnavailableException)
{
// Lost connection to the cluster...
}
A referência de grão não é invalidada nesta situação; Você pode tentar novamente a chamada na mesma referência mais tarde, quando uma conexão pode ter sido restabelecida.
Injeção de dependência
A maneira recomendada de criar um cliente externo num programa usando o .NET Generic Host é injetar uma IClusterClient instância singleton através de dependency injection. Essa instância pode então ser aceita como um parâmetro de construtor em serviços hospedados, controladores de ASP.NET, etc.
Observação
Ao co-hospedar um Orleans silo no mesmo processo que estará se conectando a ele, não é necessário criar manualmente um cliente, Orleans irá automaticamente fornecer um e gerenciar sua vida útil adequadamente.
Ao se conectar a um cluster em um processo diferente (em uma máquina diferente), um padrão comum é criar um serviço hospedado como este:
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();
}
}
Registe o serviço da seguinte forma:
await Host.CreateDefaultBuilder(args)
.UseOrleansClient(builder =>
{
builder.UseLocalhostClustering();
})
.ConfigureServices(services =>
{
services.AddHostedService<ClusterClientHostedService>();
})
.RunConsoleAsync();
Exemplo
Aqui está uma versão estendida do exemplo anterior mostrando um aplicativo cliente que se conecta ao Orleans, encontra a conta do jogador, se inscreve para atualizações para a sessão de jogo da qual o jogador faz parte usando um observador e imprime notificações até que o programa seja encerrado manualmente.
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);
}
}