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.
Orleans 7.0 presenta varios cambios beneficiosos, incluidas mejoras en el hospedaje, serialización personalizada, inmutabilidad y abstracciones de granos.
Migración
Debido a cambios en la forma Orleans en que identifica granos y flujos, la migración de aplicaciones existentes mediante recordatorios, secuencias o persistencia de grano a Orleans 7.0 no es actualmente sencillo.
No es posible actualizar sin problemas las aplicaciones que ejecutan versiones anteriores Orleans a través de una actualización gradual a Orleans la versión 7.0. Por lo tanto, use una estrategia de actualización diferente, como implementar un nuevo clúster y retirar el anterior. Orleans 7.0 cambia el protocolo de comunicación de forma incompatible, lo que significa que los clústeres no pueden contener una combinación de hosts de Orleans 7.0 y hosts que ejecutan versiones anteriores Orleans.
Estos cambios importantes se han evitado durante muchos años, incluso en las versiones principales. ¿Por qué se aplican ahora? Hay dos motivos principales: las identidades y la serialización. En cuanto a las identidades, las identidades de grano y de flujo ahora constan de cadenas. Esto permite que los granos codifiquen correctamente la información de tipo genérico y facilitan la asignación de flujos al dominio de aplicación. Anteriormente, Orleans identificaba tipos de granos mediante una estructura de datos compleja que no podía representar granos genéricos, lo que provocaba casos excepcionales. Los flujos se identificaron mediante un string espacio de nombres y una Guid clave, que era eficaz pero difícil de asignar al dominio de aplicación. La serialización ahora es tolerante a versiones. Esto significa que los tipos se pueden modificar de ciertas maneras compatibles, siguiendo un conjunto de reglas, con la confianza de que la aplicación se puede actualizar sin errores de serialización. Esta funcionalidad es especialmente útil cuando los tipos de aplicación se conservan en corrientes o almacenamiento de granos. En las secciones siguientes se detallan los principales cambios y se describen aún más.
Cambios de empaquetado
Al actualizar un proyecto a Orleans la versión 7.0, realice las siguientes acciones:
- Todos los clientes deben hacer referencia a Microsoft.Orleans.Client.
- Todos los silos (servidores) deben hacer referencia a Microsoft.Orleans.Server.
- Todos los demás paquetes deben hacer referencia a Microsoft.Orleans.Sdk.
- Los paquetes de cliente y servidor incluyen una referencia a Microsoft.Orleans.Sdk.
- Quite todas las referencias a
Microsoft.Orleans.CodeGenerator.MSBuildyMicrosoft.Orleans.OrleansCodeGenerator.Build.- Reemplace los usos de
KnownAssemblypor GenerateCodeForDeclaringAssemblyAttribute. - El paquete
Microsoft.Orleans.Sdkhace referencia al paquete generador de código fuente de C# (Microsoft.Orleans.CodeGenerator).
- Reemplace los usos de
- Quite todas las referencias a
Microsoft.Orleans.OrleansRuntime.- Los paquetes Microsoft.Orleans.Server hacen referencia a su reemplazo,
Microsoft.Orleans.Runtime.
- Los paquetes Microsoft.Orleans.Server hacen referencia a su reemplazo,
- Quite las llamadas a
ConfigureApplicationParts. Han sido eliminados los elementos de la aplicación. El generador de origen de C# para Orleans se agrega a todos los paquetes (incluido el cliente y el servidor) y genera automáticamente el equivalente de elementos de aplicación. - Reemplace las referencias a
Microsoft.Orleans.OrleansServiceBuspor Microsoft.Orleans. Streaming.EventHubs. - Si usa recordatorios, agregue una referencia a Microsoft.Orleans. Recordatorios.
- Si usa flujos, agregue una referencia a Microsoft.Orleans.Streaming.
Sugerencia
Todos los ejemplos de Orleans se han actualizado a Orleans 7.0 y se pueden usar como referencia para los cambios realizados. Para más información, consulte el problema de Orleans n.° 8035 que detalla los cambios realizados en cada ejemplo.
Orleans directivas using globales
Todos los proyectos de Orleans hacen referencia directa o indirecta al paquete NuGet Microsoft.Orleans.Sdk. Cuando se configura un Orleans proyecto para habilitar los usos implícitos (por ejemplo, <ImplicitUsings>enable</ImplicitUsings>), el proyecto usa implícitamente los Orleans y los Orleans.Hosting. Esto significa que el código de la aplicación no necesita estas using directivas.
Para más información, consulte ImplicitUsings y dotnet/orleans/src/Orleans.Sdk/build/Microsoft.Orleans.Sdk.targets.
Hospedar aplicaciones de WPF
El ClientBuilder tipo se reemplaza con el método de extensión UseOrleansClient en IHostBuilder. El tipo IHostBuilder procede del paquete NuGet Microsoft.Extensions.Hosting. Esto significa que un Orleans cliente se puede agregar a un host existente sin crear un contenedor independiente de inserción de dependencias. El cliente se conecta al clúster durante el inicio. Una vez IHost.StartAsync completado, el cliente se conecta automáticamente. Servicios añadidos al IHostBuilder inician en el orden de registro. Llamar UseOrleansClient antes de llamar a ConfigureWebHostDefaults, por ejemplo, garantiza que Orleans se inicie antes de que ASP.NET Core, lo que permite el acceso inmediato al cliente desde la aplicación ASP.NET Core.
Para emular el comportamiento anterior ClientBuilder , cree un elemento independiente HostBuilder y configúrelo con un Orleans cliente. Un IHostBuilder se puede configurar con un Orleans cliente o un Orleans silo. Todos los silos registran una instancia de IGrainFactory y IClusterClient que la aplicación puede usar, por lo que la configuración de un cliente por separado no es necesaria y no es compatible.
Cambio de firma OnActivateAsync y OnDeactivateAsync
Orleans permite que los granos ejecuten código durante la activación y desactivación. Use esta funcionalidad para realizar tareas como leer el estado del almacenamiento o registrar mensajes del ciclo de vida. En Orleans 7.0, la firma de estos métodos de ciclo de vida cambió:
- OnActivateAsync() ahora acepta un parámetro CancellationToken. Cuando se cancela el CancellationToken, se abandona el proceso de activación.
-
OnDeactivateAsync() ahora acepta un parámetro DeactivationReason y un parámetro
CancellationToken.DeactivationReasonindica por qué se desactiva la activación. Use esta información con fines de registro y diagnóstico. Cuando se canceleCancellationToken, complete rápidamente el proceso de desactivación. Tenga en cuenta que, dado que cualquier host puede producir un error en cualquier momento, no se recomienda confiar enOnDeactivateAsyncpara realizar acciones importantes, como conservar el estado crítico.
Considere el ejemplo siguiente de un grano que reemplaza estos nuevos métodos:
public sealed class PingGrain : Grain, IPingGrain
{
private readonly ILogger<PingGrain> _logger;
public PingGrain(ILogger<PingGrain> logger) =>
_logger = logger;
public override Task OnActivateAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("OnActivateAsync()");
return Task.CompletedTask;
}
public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
{
_logger.LogInformation("OnDeactivateAsync({Reason})", reason);
return Task.CompletedTask;
}
public ValueTask Ping() => ValueTask.CompletedTask;
}
Granos de POCO y IGrainBase
Los granos de Orleans ya no necesitan heredar de la clase base de Grain ni de ninguna otra clase. Esta funcionalidad se conoce como granos POCO. Para acceder a métodos de extensión como cualquiera de los siguientes:
- DeactivateOnIdle
- AsReference
- Cast
- GetPrimaryKey
- GetReminder
- GetReminders
- RegisterOrUpdateReminder
- UnregisterReminder
- GetStreamProvider
El grano debe implementar IGrainBase o heredar de Grain. A continuación, se muestra un ejemplo de cómo implementar IGrainBase en una clase de grano:
public sealed class PingGrain : IGrainBase, IPingGrain
{
public PingGrain(IGrainContext context) => GrainContext = context;
public IGrainContext GrainContext { get; }
public ValueTask Ping() => ValueTask.CompletedTask;
}
IGrainBase también define OnActivateAsync y OnDeactivateAsync con implementaciones predeterminadas, lo que permite que el grano participe en su ciclo de vida si lo desea:
public sealed class PingGrain : IGrainBase, IPingGrain
{
private readonly ILogger<PingGrain> _logger;
public PingGrain(IGrainContext context, ILogger<PingGrain> logger)
{
_logger = logger;
GrainContext = context;
}
public IGrainContext GrainContext { get; }
public Task OnActivateAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("OnActivateAsync()");
return Task.CompletedTask;
}
public Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
{
_logger.LogInformation("OnDeactivateAsync({Reason})", reason);
return Task.CompletedTask;
}
public ValueTask Ping() => ValueTask.CompletedTask;
}
Serialización
El cambio más complicado en Orleans 7.0 es la introducción del serializador tolerante a versiones. Este cambio se realizó porque las aplicaciones tienden a evolucionar, lo que provocó un problema significativo para los desarrolladores, ya que el serializador anterior no podía tolerar la adición de propiedades a los tipos existentes. Por otro lado, el serializador anterior era flexible, lo que permite la representación de la mayoría de los tipos de .NET sin modificaciones, incluidas características como genéricos, polimorfismo y seguimiento de referencias. Hacía tiempo que se necesitaba un reemplazo, pero todavía es necesaria una representación de alta fidelidad de los tipos. Por lo tanto, Orleans 7.0 presenta un serializador de reemplazo que admite la representación de alta fidelidad de los tipos de .NET, a la vez que permite que los tipos evolucionen. El nuevo serializador es mucho más eficaz que el anterior, lo que da como resultado hasta 170% mayor rendimiento de un extremo a otro.
Para más información, consulte estos artículos relacionados con Orleans 7.0:
- Información general de la serialización
- Configuración de la serialización
- Personalización de la serialización
Identidades de los granos
Cada grano tiene una identidad única formada por el tipo del grano y su clave. Las versiones anteriores Orleans usaron un tipo compuesto para GrainIds para admitir claves de grano de cualquiera de los siguientes tipos:
Este enfoque implica cierta complejidad al tratar con claves de grano. Las identidades de grano constan de dos componentes: un tipo y una clave. El componente de tipo constaba anteriormente de un código de tipo numérico, una categoría y 3 bytes de información de tipo genérico.
Las identidades de grano ahora tienen el formato type/key, donde tanto type como key son cadenas. La interfaz de clave de grano más usada es IGrainWithStringKey. Esto simplifica considerablemente el funcionamiento de la identidad de los granos y mejora la compatibilidad con tipos de grano genéricos.
Las interfaces de grano ahora también se representan mediante un nombre legible, en lugar de una combinación de código hash y una representación de cadena de cualquier parámetro de tipo genérico.
El nuevo sistema es más personalizable y estas personalizaciones se pueden controlar con atributos.
-
GrainTypeAttribute(String) en un grano
classespecifica la parte tipo de su identificador de grano. -
DefaultGrainTypeAttribute(String) en un grano
interfaceespecifica el tipo del grano que IGrainFactory se debe resolver de forma predeterminada al obtener una referencia de grano. Por ejemplo, al llamar aIGrainFactory.GetGrain<IMyGrain>("my-key"), el generador de granos devuelve una referencia al grano"my-type/my-key"siIMyGraintiene especificado el atributo mencionado anteriormente. - GrainInterfaceTypeAttribute(String) permite invalidar el nombre de la interfaz. Especificar un nombre explícitamente mediante este mecanismo permite cambiar el nombre del tipo de interfaz sin interrumpir la compatibilidad con las referencias de grano existentes. Tenga en cuenta que la interfaz también debe tener en este caso AliasAttribute, ya que su identidad podría ser serializada. Para obtener más información sobre cómo especificar un alias de tipo, consulte la sección sobre serialización.
Como se mencionó anteriormente, invalidar los nombres de interfaz y clase de grano predeterminados para los tipos permite cambiar el nombre de los tipos subyacentes sin interrumpir la compatibilidad con las implementaciones existentes.
Identidades de flujo
Cuando los flujos de Orleans se lanzaron por primera vez, los flujos solo se podían identificar mediante Guid. Este enfoque era eficaz en términos de asignación de memoria, pero dificultaba la creación de identidades de flujo significativas, lo que a menudo requería cierta codificación o direccionamiento indirecto para determinar la identidad de flujo adecuada para un propósito determinado.
En Orleans la versión 7.0, los flujos se identifican mediante cadenas.
Orleans.Runtime.StreamId
struct contiene tres propiedades: StreamId.Namespace, StreamId.Keyy StreamId.FullKey. Estos valores de propiedad son cadenas UTF-8 codificadas. Por ejemplo, vea StreamId.Create(String, String).
Reemplazo de SimpleMessageStreams con BroadcastChannel
SimpleMessageStreams (también denominado SMS) se quita en la versión 7.0. SMS tenía la misma interfaz que Orleans.Providers.Streams.PersistentStreams, pero su comportamiento era muy diferente porque se basaba en llamadas directas de grano a grano. Para evitar confusiones, se quitó SMS y se introdujo un nuevo reemplazo llamado Orleans.BroadcastChannel .
BroadcastChannel solo admite suscripciones implícitas y puede ser un reemplazo directo en este caso. Si se necesitan suscripciones explícitas o se debe usar la PersistentStream interfaz (por ejemplo, si se usó SMS en pruebas mientras EventHub se usó en producción), entonces MemoryStream es el mejor candidato.
BroadcastChannel tiene los mismos comportamientos que SMS, mientras que MemoryStream se comporta como otros proveedores de flujos. Tenga en cuenta el siguiente ejemplo de uso del canal de difusión:
// Configuration
builder.AddBroadcastChannel(
"my-provider",
options => options.FireAndForgetDelivery = false);
// Publishing
var grainKey = Guid.NewGuid().ToString("N");
var channelId = ChannelId.Create("some-namespace", grainKey);
var stream = provider.GetChannelWriter<int>(channelId);
await stream.Publish(1);
await stream.Publish(2);
await stream.Publish(3);
// Simple implicit subscriber example
[ImplicitChannelSubscription]
public sealed class SimpleSubscriberGrain : Grain, ISubscriberGrain, IOnBroadcastChannelSubscribed
{
// Called when a subscription is added to the grain
public Task OnSubscribed(IBroadcastChannelSubscription streamSubscription)
{
streamSubscription.Attach<int>(
item => OnPublished(streamSubscription.ChannelId, item),
ex => OnError(streamSubscription.ChannelId, ex));
return Task.CompletedTask;
// Called when an item is published to the channel
static Task OnPublished(ChannelId id, int item)
{
// Do something
return Task.CompletedTask;
}
// Called when an error occurs
static Task OnError(ChannelId id, Exception ex)
{
// Do something
return Task.CompletedTask;
}
}
}
La migración a MemoryStream es más fácil, ya que solo la configuración necesita cambiar. Tenga en cuenta la siguiente configuración de MemoryStream:
builder.AddMemoryStreams<DefaultMemoryMessageBodySerializer>(
"in-mem-provider",
_ =>
{
// Number of pulling agent to start.
// DO NOT CHANGE this value once deployed, if you do rolling deployment
_.ConfigurePartitioning(partitionCount: 8);
});
OpenTelemetry
El sistema de telemetría se actualiza en Orleans la versión 7.0 y el sistema anterior se elimina a favor de las API estandarizadas de .NET, como .NET Metrics para métricas y ActivitySource para trazado.
Como parte de esto, se quitan los paquetes existentes Microsoft.Orleans.TelemetryConsumers.* . Se está considerando un nuevo conjunto de paquetes para simplificar la integración de las métricas emitidas por Orleans en la solución de supervisión que prefiera. Como siempre, cualquier comentario o contribución serán bienvenidos.
La herramienta dotnet-counters cuenta con supervisión del rendimiento diseñada para la supervisión ad hoc del estado y la investigación de rendimiento de primer nivel. Para supervisar los Orleans contadores, use la herramienta dotnet-counters.
dotnet counters monitor -n MyApp --counters Microsoft.Orleans
Del mismo modo, añada las Microsoft.Orleans mediciones a las métricas de OpenTelemetry, como se indica en el siguiente código.
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddPrometheusExporter()
.AddMeter("Microsoft.Orleans"));
Para habilitar el seguimiento distribuido, configure OpenTelemetry como se muestra en el código siguiente:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService(serviceName: "ExampleService", serviceVersion: "1.0"));
tracing.AddAspNetCoreInstrumentation();
tracing.AddSource("Microsoft.Orleans.Runtime");
tracing.AddSource("Microsoft.Orleans.Application");
tracing.AddZipkinExporter(options =>
{
options.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
});
});
En el código anterior, OpenTelemetry está configurado para supervisar:
Microsoft.Orleans.RuntimeMicrosoft.Orleans.Application
Para propagar la actividad, llame a AddActivityPropagation:
builder.Host.UseOrleans((_, clientBuilder) =>
{
clientBuilder.AddActivityPropagation();
});
Refactorización de características del paquete principal en paquetes independientes
En Orleans la versión 7.0, las extensiones se factorizaron en paquetes independientes que no dependen de Orleans.Core. Es decir, Orleans.Streaming, Orleans.Remindersy Orleans.Transactions se separaron del núcleo. Esto significa que estos paquetes se pagan por completo por lo que se usa y no hay código en el Orleans núcleo dedicado a estas características. Este enfoque reduce el tamaño del ensamblado y la superficie de la API principal, simplifica el núcleo y mejora el rendimiento. Con respecto al rendimiento, las transacciones en Orleans antes requerían que se ejecutara código para cada método para coordinar posibles transacciones. Esa lógica de coordinación ahora se mueve a una base por método.
Se trata de un cambio importante en la compilación. El código existente que interactúa con recordatorios o flujos mediante una llamada a métodos que anteriormente estaban definidos en la Grain clase base podría dejar de funcionar porque ahora se han convertido en métodos de extensión. Actualice estas llamadas que no especifiquen this (por ejemplo, GetReminders) para incluir this (por ejemplo, this.GetReminders()) porque los métodos de extensión deben estar calificados. Se produce un error de compilación si estas llamadas no se actualizan y es posible que el cambio de código necesario no sea obvio sin saber qué ha cambiado.
Cliente de transacción
Orleans 7.0 presenta una nueva abstracción para coordinar transacciones: Orleans.ITransactionClient. Anteriormente, solo los granos podían coordinar las transacciones. Con ITransactionClient, disponible a través de la inyección de dependencias, los clientes también pueden coordinar transacciones sin necesidad de un componente intermediario. En el ejemplo siguiente se retiran créditos de una cuenta y se depositan en otra dentro de una sola transacción. Llame a este código desde un grano o desde un cliente externo que haya recuperado el ITransactionClient del contenedor de inyección de dependencias.
await transactionClient.RunTransaction(
TransactionOption.Create,
() => Task.WhenAll(from.Withdraw(100), to.Deposit(100)));
Para las transacciones coordinadas por el cliente, el cliente debe agregar los servicios necesarios durante la configuración:
clientBuilder.UseTransactions();
En el ejemplo BankAccount se muestra el uso de ITransactionClient. Para más información, consulte Transacciones de Orleans.
Reentrada de la cadena de llamadas
De forma predeterminada, los granos son de un solo subproceso y procesan solicitudes de una en una desde el comienzo hasta la finalización. En otras palabras, los granos no son reentrantes de forma predeterminada. Agregar la ReentrantAttribute a una clase de grano permite que el grano procese varias solicitudes simultáneamente de forma intercalada mientras sigue siendo monohilo. Esta funcionalidad puede ser útil para los granos que no contienen ningún estado interno o realizan muchas operaciones asincrónicas, como emitir llamadas HTTP o escribir en una base de datos. Se necesita atención adicional cuando las solicitudes pueden intercalarse: es posible que el estado de un grano, observado antes de una instrucción await, cambie para cuando se completa la operación asincrónica y el método reanuda la ejecución.
Por ejemplo, el siguiente grano representa un contador. Está marcado como Reentrant, lo que permite que varias llamadas se intercalen. El método Increment() debe incrementar el contador interno y devolver el valor observado. Sin embargo, dado que el Increment() cuerpo del método observa el estado del grano antes de un await punto y lo actualiza después, varias ejecuciones entrelazadas de Increment() pueden dar lugar a un número de _value menor que el total de llamadas de Increment() recibidas. Se trata de un error introducido por el uso incorrecto de la reentrada.
Quitar el ReentrantAttribute elemento es suficiente para corregir este problema.
[Reentrant]
public sealed class CounterGrain : Grain, ICounterGrain
{
int _value;
/// <summary>
/// Increments the grain's value and returns the previous value.
/// </summary>
public Task<int> Increment()
{
// Do not copy this code, it contains an error.
var currentVal = _value;
await Task.Delay(TimeSpan.FromMilliseconds(1_000));
_value = currentVal + 1;
return currentValue;
}
}
Para evitar estos errores, los granos no son reentrantes de forma predeterminada. El inconveniente es reducir el rendimiento de los granos que realizan operaciones asincrónicas en su implementación, ya que el grano no puede procesar otras solicitudes mientras espera a que se complete una operación asincrónica. Para contrarrestar esto, Orleans ofrece varias opciones para permitir la reentrada en determinados casos:
- Para una clase completa: colocar el ReentrantAttribute en el grano permite que cualquier solicitud al grano se mezcle con cualquier otra solicitud.
- Para un subconjunto de métodos: colocar el método de interfaz de grano en AlwaysInterleaveAttribute permite que las solicitudes a ese método se superpongan con cualquier otra solicitud y que cualquier otra solicitud se superponga con las solicitudes a ese método.
- Para un subconjunto de métodos: Colocar el ReadOnlyAttribute en el método de interfaz de grano permite que las solicitudes a ese método se intercalen con cualquier otra solicitud, y permite que cualquier otra solicitud se intercale con las solicitudes a ese método. En este sentido, es una forma más restringida de
AlwaysInterleave. - Para cualquier solicitud dentro de una cadena de llamadas: RequestContext.AllowCallChainReentrancy() y RequestContext.SuppressCallChainReentrancy() permiten optar por permitir o denegar que las solicitudes posteriores reingresen al grano. Ambas llamadas devuelven un valor que se debe eliminar al salir de la solicitud. Por lo tanto, úselas de la siguiente manera:
public Task<int> OuterCall(IMyGrain other)
{
// Allow call-chain reentrancy for this grain, for the duration of the method.
using var _ = RequestContext.AllowCallChainReentrancy();
await other.CallMeBack(this.AsReference<IMyGrain>());
}
public Task CallMeBack(IMyGrain grain)
{
// Because OuterCall allowed reentrancy back into that grain, this method
// will be able to call grain.InnerCall() without deadlocking.
await grain.InnerCall();
}
public Task InnerCall() => Task.CompletedTask;
Activación voluntaria de la reentrada de la cadena de llamadas por grano, por cadena de llamadas. Por ejemplo, considere dos granos, A y B. Si el grano A habilita la reentrada de la cadena de llamadas antes de llamar al grano B, el grano B puede volver a llamar al grano A en esa llamada. Sin embargo, el grano A no puede realizar una llamada de regreso al grano B si el grano B no ha habilitado también la reentrada de la cadena de llamadas. Está habilitado a nivel de granularidad y por cadena de llamadas.
Los granos también pueden suprimir la información de reentrada de la cadena de llamadas para que no fluya por una cadena de llamadas mediante using var _ = RequestContext.SuppressCallChainReentrancy(). Esto evita que las llamadas posteriores vuelvan a entrar.
Scripts de migración de ADO.NET
Para garantizar la compatibilidad a futuro con la agrupación en clústeres, la persistencia y los recordatorios basados en ADO.NET, se necesita el script adecuado de migración de SQL.
Seleccione los archivos de las bases de datos utilizadas y aplíquelos en orden.