Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Un cliente permite que el código no específico interactúe con un Orleans clúster. Los clientes permiten que el código de aplicación se comunique con granos y flujos hospedados en un clúster. Hay dos maneras de obtener un cliente, en función de dónde se aloje el código del cliente: dentro del mismo proceso que un silo o en un proceso independiente. En este artículo se describen ambas opciones, comenzando por el enfoque recomendado: co-hospedar el código del cliente en el mismo proceso que el código de grano.
Clientes hospedados conjuntamente
Si hospeda código de cliente en el mismo proceso que el código de grain, puede obtener directamente el cliente del contenedor de inyección de dependencias de la aplicación anfitriona. En este caso, el cliente se comunica directamente con el silo al que está asociado y puede aprovechar los conocimientos adicionales del silo sobre el clúster.
Este enfoque proporciona varias ventajas, como una menor sobrecarga de red y CPU, una menor latencia y un mayor rendimiento y confiabilidad. El cliente usa el conocimiento del silo de la topología y el estado del clúster y no necesita una puerta de enlace independiente. Esto evita un salto de red y un recorrido de ida y vuelta de serialización o deserialización, lo que aumenta la confiabilidad al minimizar el número de nodos necesarios entre el cliente y el grano. Si el grano es un grano de trabajo sin estado o se activa en el mismo silo donde se hospeda el cliente, no se necesita ninguna serialización o comunicación de red, lo que permite al cliente lograr mejoras de rendimiento y confiabilidad adicionales. El cliente de hospedaje conjunto y el código de grano también simplifican la implementación y la topología de la aplicación al eliminar la necesidad de desplegar y monitorizar dos binarios distintos de la aplicación.
También hay inconvenientes en este enfoque, principalmente que el código granular ya no está aislado del proceso del cliente. Por lo tanto, los problemas en el código del cliente, como el bloqueo de E/S o la contención de bloqueos que causan la inanición de subprocesos, pueden afectar al rendimiento del código de granos. Incluso sin tales defectos de código, los efectos vecinos ruidosos pueden producirse simplemente porque el código de cliente se ejecuta en el mismo procesador que el código de grano, poniendo presión adicional en la memoria caché de CPU y aumentando la contención para los recursos locales. Además, la identificación del origen de estos problemas resulta más difícil porque los sistemas de supervisión no pueden distinguir lógicamente entre el código de cliente y el código de grano.
A pesar de estos inconvenientes, el código de cliente alojado conjuntamente con el código grain es una opción popular y el enfoque recomendado para la mayoría de las aplicaciones. Los inconvenientes mencionados anteriormente suelen ser mínimos en la práctica por los siguientes motivos:
- El código de cliente suele ser muy fino (por ejemplo, traducir solicitudes HTTP entrantes en llamadas específicas). Por lo tanto, los efectos de los vecinos ruidosos son mínimos y el costo es comparable al de la puerta de enlace necesaria.
- Si surge un problema de rendimiento, es probable que el flujo de trabajo típico implique herramientas como generadores de perfiles de CPU y depuradores. Estas herramientas siguen siendo eficaces para identificar rápidamente el origen del problema, incluso con código de cliente y de grano que se ejecuta en el mismo proceso. Es decir, mientras que las métricas se vuelven más gruesas y menos capaces de identificar con precisión el origen del problema, las herramientas más detalladas siguen siendo eficaces.
Obtener un cliente desde un host
Si hospeda con el host genérico de .NET, el cliente estará disponible automáticamente en el contenedor de inserción de dependencias del host. Puede insertarlo en servicios como los controladores de ASP.NET o implementaciones de IHostedService.
Como alternativa, puede obtener una interfaz de cliente como IGrainFactory o IClusterClient desde ISiloHost:
var client = host.Services.GetService<IClusterClient>();
await client.GetGrain<IMyGrain>(0).Ping();
Clientes externos
El código de cliente se puede ejecutar fuera del Orleans clúster donde se hospeda el código de grano. En este caso, un cliente externo actúa como conector o conducto para el clúster y todas las unidades de la aplicación. Normalmente, se usan clientes en servidores web front-end para conectarse a un Orleans clúster que actúa como un nivel intermedio, con granos que ejecutan lógica de negocios.
En una configuración típica, un servidor web front-end:
- Recibe una solicitud web.
- Realiza la autenticación y la validación de autorización necesarias.
- Decide qué granos deben procesar la solicitud.
- Usa el Orleans para realizar una o varias llamadas de método a los granos.
- Controla la finalización correcta o los errores de las llamadas de grano y los valores devueltos.
- Envía una respuesta a la solicitud web.
Inicializar un cliente de granos
Para poder usar un cliente de granos para realizar llamadas a los granos alojados en un Orleans clúster, debe configurarlo, inicializarlo y conectarlo al clúster.
Proporcione la configuración a través de UseOrleansClient y varias clases de opciones complementarias que contengan una jerarquía de propiedades de configuración para configurar un cliente mediante programación. Para obtener más información, consulte Configuración de 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();
Al iniciar host, el cliente se configura y está disponible a través de la instancia construida de su proveedor de servicios.
Proporcione la configuración a través de ClientBuilder y varias clases de opciones complementarias que contengan una jerarquía de propiedades de configuración para configurar un cliente mediante programación. Para obtener más información, consulte Configuración de 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, debe llamar al Connect() método en el objeto cliente construido para conectarlo al Orleans clúster. Es un método asincrónico que devuelve un Task, por lo que debe esperar a su finalización mediante await o .Wait().
await client.Connect();
Realizar llamadas a granos
Realizar llamadas a silos desde un cliente no es diferente de realizar estas llamadas desde el código de silo. Use el mismo IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) método (donde T es la interfaz de grano de destino) en ambos casos para obtener referencias de grano. La diferencia radica en qué objeto de fábrica invoca IGrainFactory.GetGrain. En el código de cliente, lo hace a través del objeto de cliente conectado, 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 lo requieran las reglas de interfaz de grano. El cliente puede usar la palabra clave await para esperar de forma asincrónica el Task que se devuelve sin bloquear el subproceso, o en algunos casos, usar el método Wait() para bloquear el hilo actual de ejecución.
La principal diferencia entre hacer llamadas a granos desde el código del cliente y desde dentro de otro grano es el modelo de ejecución de un único hilo en los granos. El Orleans tiempo de ejecución restringe los granos para que sean de un solo subproceso, mientras que los clientes pueden ser multiproceso.
Orleans no proporciona ninguna garantía de este tipo en el lado cliente, por lo que depende del cliente administrar su simultaneidad mediante construcciones de sincronización adecuadas para su entorno: bloqueos, eventos, Tasks, etc.
Recibir notificaciones
A veces, un patrón simple de solicitud-respuesta no es suficiente y el cliente debe recibir notificaciones asincrónicas. Por ejemplo, un usuario podría querer notificaciones cuando alguien que sigue publica un nuevo mensaje.
El uso de observadores es un mecanismo que permite la exposición de objetos del lado del cliente como objetivos similares a granos que pueden ser invocados por otros granos. Las llamadas a observadores no proporcionan ninguna indicación de éxito o error, ya que se envían como mensajes unidireccionales de mejor esfuerzo. Por lo tanto, es responsabilidad del código de aplicación crear un mecanismo de confiabilidad de nivel superior sobre los observadores cuando sea necesario.
Otro mecanismo para entregar mensajes asincrónicos a los clientes es Streams. Los flujos exponen indicaciones de éxito o error para la entrega de mensajes individuales, lo que permite una comunicación confiable con el cliente.
Conectividad de clientes
Hay dos escenarios en los que un cliente de clúster puede experimentar problemas de conectividad:
- Cuando el cliente intenta conectarse a un silo.
- Al realizar llamadas a referencias de grano obtenidas de un cliente de clúster que está conectado.
En el primer caso, el cliente intenta conectarse a un silo. Si el cliente no puede conectarse a ningún silo, produce una excepción que indica lo que ha ido mal. Puede registrar un IClientConnectionRetryFilter para controlar la excepción y decidir si desea volver a intentarlo. Si no proporciona ningún filtro de reintento, o si el filtro de reintento devuelve false, el 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;
}
}
Hay dos escenarios en los que un cliente de clúster puede experimentar problemas de conectividad:
- IClusterClient.Connect() Cuando se llama al método inicialmente.
- Al realizar llamadas a referencias de grano obtenidas de un cliente de clúster que está conectado.
En el primer caso, el Connect método produce una excepción que indica lo que salió mal. Esto suele ser (pero no necesariamente) un SiloUnavailableException. Si esto sucede, la instancia de cliente del clúster no se puede usar y se debe eliminar. Opcionalmente, puede proporcionar una función de filtro de reintento al método Connect, que, por ejemplo, podría esperar una duración especificada antes de realizar otro intento. Si no proporciona ningún filtro de reintento, o si el filtro de reintento devuelve false, el cliente desiste permanentemente.
Si Connect devuelve correctamente, se garantiza que el cliente del clúster se pueda usar hasta que se elimine. Esto significa que incluso si el cliente experimenta problemas de conexión, intenta recuperarse indefinidamente. Puede configurar el comportamiento exacto de recuperación en un GatewayOptions objeto proporcionado por 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 a un proceso de grano, se lanza una SiloUnavailableException en el lado del cliente. Puede controlar esto de la siguiente manera:
IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
try
{
await player.JoinGame(game);
}
catch (SiloUnavailableException)
{
// Lost connection to the cluster...
}
La referencia de granulación no se invalida en esta situación; podría volver a intentar la llamada con la misma referencia más adelante cuando la conexión se haya restablecido.
Inserción de dependencia
La manera recomendada de crear un cliente externo en un programa mediante el .NET Generic Host es inyectar una instancia singleton a través de la inyección de dependencias. A continuación, esta instancia se puede aceptar como parámetro de constructor en servicios hospedados, ASP.NET controladores, etc.
Nota:
Al hospedar conjuntamente un Orleans silo en el mismo proceso que se conectará a él, no es necesario crear manualmente un cliente; Orleans proporcionará automáticamente uno y administrará su ciclo de vida correctamente.
Al conectarse a un clúster en un proceso diferente (en una máquina diferente), un patrón común es crear un servicio 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();
}
}
Registre el servicio de la siguiente manera:
await Host.CreateDefaultBuilder(args)
.UseOrleansClient(builder =>
{
builder.UseLocalhostClustering();
})
.ConfigureServices(services =>
{
services.AddHostedService<ClusterClientHostedService>();
})
.RunConsoleAsync();
Ejemplo
Esta es una versión extendida del ejemplo anterior que muestra una aplicación cliente que se conecta a Orleans, busca la cuenta del jugador, se suscribe a las actualizaciones de la sesión del juego a la que el jugador pertenece, utilizando un observador, e imprime las notificaciones hasta que el programa se termina 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);
}
}