Cursos
Módulo
Creación de la primera aplicación de Orleans con ASP.NET Core 8.0 - Training
Aprenda a crear aplicaciones nativas de la nube y aplicaciones distribuidas con Orleans.
Este explorador ya no se admite.
Actualice a Microsoft Edge para aprovechar las características y actualizaciones de seguridad más recientes, y disponer de soporte técnico.
Un cliente permite que el código que no es de grano interactúe con un clúster de Orleans. Los clientes permiten que el código de la aplicación se comunique con granos y secuencias que se hospedan en un clúster. Existen dos maneras de obtener un cliente, según dónde se hospede el código de cliente: en el mismo proceso que un silo o en un proceso independiente. En este artículo se analizarán ambas opciones, primero la opción recomendada: hospedar de forma conjunta el código de cliente en el mismo proceso que el código de grano.
Si el código de cliente se hospeda en el mismo proceso que el código de grano, el cliente se puede obtener directamente del contenedor de inserción de dependencias de la aplicación de hospedaje. En este caso, el cliente se comunica directamente con el silo al que está adjunto y puede beneficiarse del conocimiento adicional que tiene el silo sobre el clúster.
Esto proporciona varias ventajas, entre las que se incluyen la reducción de la sobrecarga de la red y la CPU, así como la disminución de la latencia y el aumento del rendimiento y la confiabilidad. El cliente usa el conocimiento del silo de la topología y el estado del clúster y no necesita usar una puerta de enlace independiente. Esto evita un salto de red y un recorrido de ida y vuelta de serialización o deserialización. Por tanto, también aumenta la confiabilidad, ya que el número de nodos necesarios entre el cliente y el grano se minimiza. Si el grano es un grano de trabajo sin estado o, de lo contrario, se activa de otra forma en el silo en el que se hospeda el cliente, no es necesario realizar ninguna serialización o comunicación de red y el cliente puede beneficiarse de las mejoras adicionales de rendimiento y confiabilidad. El cliente de hospedaje conjunto y el código de grano también simplifican la implementación y la topología de aplicación al eliminar la necesidad de implementar y supervisar dos archivos binarios de aplicación distintos.
También hay detractores en este enfoque, principalmente que el código de grano ya no está aislado del proceso de cliente. Por tanto, los problemas en el código de cliente, como el bloqueo de E/S o la contención de bloqueos que provocan el agotamiento del subproceso pueden afectar al rendimiento del código de grano. Incluso sin defectos de código como los mencionados anteriormente, los efectos de vecinos ruidosos pueden producir simplemente que el código de cliente se ejecute en el mismo procesador que el código de grano, lo que ejerce presión adicional en la memoria caché de CPU y contención adicional para los recursos locales en general. Además, la identificación del origen de estos problemas ahora resulta más difícil, ya que los sistemas de supervisión no pueden distinguir lo que es lógicamente el código de cliente de código de grano.
A pesar de estos detractores, el código de cliente de hospedaje conjunto con código de grano es una opción popular y el enfoque recomendado para la mayoría de las aplicaciones. Para elaborarlo, los detractores mencionados anteriormente son mínimos en la práctica por los siguientes motivos:
Si hospeda mediante el host genérico de .NET, el cliente estará disponible automáticamente en el contenedor de inserción de dependencias del host y se puede insertar en servicios como controladores ASP.NET o implementaciones IHostedService.
Como alternativa, una interfaz de cliente como IGrainFactory o IClusterClient se puede obtener de ISiloHost:
var client = host.Services.GetService<IClusterClient>();
await client.GetGrain<IMyGrain>(0).Ping();
El código de cliente se puede ejecutar fuera del clúster de Orleans donde se hospeda el código de grano. Por tanto, un cliente externo actúa como conector o conducto para el clúster y todos los granos de la aplicación. Por lo general, los clientes se usan en los servidores web front-end para conectarse a un clúster de Orleans que actúa como un nivel intermedio con granos que ejecutan lógica de negocios.
En una configuración típica, un servidor web frontend:
Antes de que se pueda usar un cliente de grano para realizar llamadas a granos hospedados en un clúster de Orleans, debe configurarse, inicializarse y conectarse al clúster.
La configuración se proporciona a través de UseOrleansClient y varias clases de opciones complementarias que contienen una jerarquía de propiedades de configuración para configurar un cliente mediante programación. Para obtener más información, consulte Configuración del cliente.
Considere el ejemplo siguiente de una configuración 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();
Cuando se inicia el host
, se configurará el cliente y estará disponible a través de su instancia de proveedor de servicios construida.
La configuración se proporciona a través de ClientBuilder y varias clases de opciones complementarias que contienen una jerarquía de propiedades de configuración para configurar un cliente mediante programación. Para obtener más información, consulte Configuración del cliente.
Ejemplo de una configuración 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();
Por último, es necesario llamar al método Connect()
en el objeto de cliente construido para que se conecte al clúster de Orleans. Es un método asincrónico que devuelve un Task
. Por tanto, es necesario esperar a que finalice con un await
o.Wait()
.
await client.Connect();
La realización de llamadas al grano desde un cliente no es diferente de realizar dichas llamadas desde el código de grano. El mismo método IGrainFactory.GetGrain<TGrainInterface>(Type, Guid), donde T
es la interfaz de grano de destino, se usa en ambos casos para obtener referencias de grano. La diferencia es en qué objeto de fábrica se invoca IGrainFactory.GetGrain. En el código de cliente, se hace a través del objeto de cliente conectado, tal como se muestra en el ejemplo siguiente:
IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
Task joinGameTask = player.JoinGame(game)
await joinGameTask;
Una llamada a un método de grano devuelve Task o Task<TResult>según sea necesario para las reglas de interfaz de grano. El cliente puede usar la palabra clave await
para esperar de forma asincrónica que devuelva Task
sin bloquear el subproceso o, en algunos casos, el método Wait()
para bloquear el subproceso actual de ejecución.
La principal diferencia entre realizar llamadas a granos desde el código de cliente y desde dentro de otro grano es el modelo de ejecución de un solo subproceso de granos. Los granos están restringidos para que el entorno de ejecución de Orleans tenga un único subproceso, mientras que los clientes pueden tener varios subprocesos. Orleans no proporciona ninguna garantía de este tipo en el lado del cliente, por lo que es el cliente quien debe administrar su simultaneidad mediante cualquier construcción de sincronización adecuada para su entorno: bloqueos, eventos y Tasks
.
En algunas situaciones un patrón simple de solicitud-respuesta no es suficiente y el cliente debe recibir notificaciones asincrónicas. Por ejemplo, es posible que un usuario quiera recibir una notificación cuando alguien a quien sigue publique un nuevo mensaje.
El uso de observadores es uno de estos mecanismos que permite exponer objetos del lado del cliente como destinos similares al grano para que los granos los invoquen. Las llamadas a observadores no proporcionan ninguna indicación de éxito o error, ya que se envían como un mensaje de mejor trabajo unidireccional. Por tanto, es responsabilidad del código de la aplicación crear un mecanismo de confiabilidad de nivel superior sobre los observadores cuando sea necesario.
Otro mecanismo que se puede usar para entregar mensajes asincrónicos a los clientes es Secuencias. Las secuencias exponen indicaciones de éxito o error de entrega de mensajes individuales y, por tanto, permiten una comunicación confiable con el cliente.
Existen dos escenarios en los que un cliente de clúster puede experimentar problemas de conectividad:
En el primer caso, el cliente intentará conectarse a un silo. Si el cliente no puede hacerlo, se generará una excepción para indicar lo que salió mal. Puede registrar un IClientConnectionRetryFilter para controlar la excepción y decidir si desea reintentar o no la operación. Si no se proporciona ningún filtro de reintento o si el filtro de reintento devuelve false
, el cliente deja de ser correcto.
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;
}
}
Existen dos escenarios en los que un cliente de clúster puede experimentar problemas de conectividad:
En el primer caso, el método Connect
producirá una excepción para indicar lo que salió mal. Esto suele ser (pero no necesariamente) un SiloUnavailableException. Si esto sucede, la instancia de cliente de clúster no se podrá usar y deberá eliminarse. De forma opcional, se puede proporcionar una función de filtro de reintento al método Connect
que, por ejemplo, puede esperar un tiempo especificado antes de realizar otro intento. Si no se proporciona ningún filtro de reintento o si el filtro de reintento devuelve false
, el cliente deja de ser correcto.
Si Connect
se devuelve correctamente, se garantiza que el cliente de clúster se puede usar hasta que se elimine. Esto significa que incluso si el cliente experimenta problemas de conexión, intentará recuperarse indefinidamente. El comportamiento de recuperación exacto se puede configurar en un objeto GatewayOptions que proporciona ClientBuilder, por ejemplo:
var client = new ClientBuilder()
// ...
.Configure<GatewayOptions>(
options => // Default is 1 min.
options.GatewayListRefreshPeriod = TimeSpan.FromMinutes(10))
.Build();
En el segundo caso, donde se produce un problema de conexión durante una llamada de grano, se producirá una SiloUnavailableException en el lado cliente. Esto se puede controlar de la siguiente forma:
IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
try
{
await player.JoinGame(game);
}
catch (SiloUnavailableException)
{
// Lost connection to the cluster...
}
La referencia de grano no se invalida en esta situación; la llamada se puede reintentar más adelante en la misma referencia cuando se haya podido volver a establecer una conexión.
La forma recomendada de crear un cliente externo en un programa que usa el host genérico de .NET es insertar una instancia de singleton IClusterClient a través de la inserción de dependencias, que luego se puede aceptar como parámetro de constructor en servicios hospedados, controladores ASP.NET, etc.
Nota
Cuando se hospeda de forma conjunta un silo de Orleans en el mismo proceso que se conectará a él, no es necesario crear manualmente un cliente, porque Orleans proporcionará uno de manera automática y administrará su duración adecuadamente.
Al conectarse a un clúster en un proceso diferente (en un equipo diferente), un patrón común consiste en crear un servicio hospedado de la siguiente forma:
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();
}
}
A continuación, el servicio se registra de la siguiente forma:
await Host.CreateDefaultBuilder(args)
.UseOrleansClient(builder =>
{
builder.UseLocalhostClustering();
})
.ConfigureServices(services =>
{
services.AddHostedService<ClusterClientHostedService>();
})
.RunConsoleAsync();
Esta es una versión extendida del ejemplo anterior de una aplicación cliente que se conecta a Orleans, busca la cuenta del jugador, se suscribe a las actualizaciones de la sesión de juego de la que forma parte el jugador con un observador e imprime notificaciones hasta que el programa finaliza 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);
}
}
Comentarios de .NET
.NET es un proyecto de código abierto. Seleccione un vínculo para proporcionar comentarios:
Cursos
Módulo
Creación de la primera aplicación de Orleans con ASP.NET Core 8.0 - Training
Aprenda a crear aplicaciones nativas de la nube y aplicaciones distribuidas con Orleans.