Partilhar via


Migrar de Orleans 3.x para 7.0

Orleans O 7.0 introduz várias alterações benéficas, incluindo melhorias na hospedagem, serialização personalizada, imutabilidade e abstrações de grãos.

Migração

Os aplicativos existentes que usam lembretes, fluxos ou persistência de grãos não podem ser facilmente migrados para a Orleans versão 7.0 devido a alterações na forma como Orleans identifica grãos e fluxos. Planejamos oferecer incrementalmente um caminho de migração para esses aplicativos.

Os aplicativos que executam versões anteriores do não podem ser atualizados sem problemas por meio de Orleans uma atualização contínua para a Orleans versão 7.0. Portanto, uma estratégia de atualização diferente deve ser usada, como implantar um novo cluster e desativar o cluster anterior. Orleans 7.0 altera o protocolo de conexão de forma incompatível, o que significa que os clusters não podem conter uma mistura de Orleans hosts 7.0 e hosts executando versões anteriores do Orleans.

Temos evitado tais mudanças por muitos anos, mesmo em grandes lançamentos, então por que agora? Há duas razões principais: identidades e serialização. Em relação às identidades, as identidades de grãos e fluxos agora são compostas por cadeias de caracteres, permitindo que os grãos codificem informações de tipo genéricas corretamente e permitindo que os fluxos sejam mapeados mais facilmente para o domínio do aplicativo. Os tipos de grãos foram previamente identificados usando uma estrutura de dados complexa que não poderia representar grãos genéricos, levando a casos de esquina. Os fluxos eram identificados por um string namespace e uma Guid chave, o que era difícil para os desenvolvedores mapear para seu domínio de aplicativo, por mais eficiente que fosse. A serialização agora é tolerante à versão, o que significa que você pode modificar seus tipos de determinadas maneiras compatíveis, seguindo um conjunto de regras, e ter certeza de que pode atualizar seu aplicativo sem erros de serialização. Isso era especialmente problemático quando os tipos de aplicação persistiam em fluxos ou armazenamento de grãos. As seções a seguir detalham as principais mudanças e as discutem mais detalhadamente.

Alterações de embalagem

Se você estiver atualizando um projeto para Orleans a versão 7.0, precisará executar as seguintes ações:

Gorjeta

Todas as Orleans amostras foram atualizadas para Orleans a versão 7.0 e podem ser usadas como referência para as alterações feitas. Para obter mais informações, consulte Orleans a edição #8035 que discrimina as alterações feitas em cada exemplo.

Orleansglobal using Diretivas

Todos os Orleans projetos fazem referência direta ou indireta ao Microsoft.Orleans.Sdk pacote NuGet. Quando um Orleans projeto é configurado para habilitar usos implícitos (por exemplo <ImplicitUsings>enable</ImplicitUsings>), os Orleans namespaces e Orleans.Hosting são usados implicitamente. Isso significa que o código do seu aplicativo não precisa dessas diretivas.

Para obter mais informações, consulte ImplicitUsings e dotnet/orleans/src/Orleans. Sdk/build/Microsoft.Orleans. Sdk.targets.

Alojamento

O ClientBuilder tipo foi substituído por um método de UseOrleansClient extensão em IHostBuilder. O IHostBuilder tipo vem do pacote NuGet Microsoft.Extensions.Hosting . Isso significa que você pode adicionar um Orleans cliente a um host existente sem ter que criar um contêiner de injeção de dependência separado. O cliente se conecta ao cluster durante a inicialização. Uma vez IHost.StartAsync concluído, o cliente será conectado automaticamente. Os serviços adicionados ao IHostBuilder são iniciados na ordem de registro, portanto, ligue UseOrleansClient antes de ligar ConfigureWebHostDefaults para garantir Orleans que seja iniciado antes que ASP.NET Core comece, por exemplo, permitindo que você acesse o cliente do seu aplicativo ASP.NET Core imediatamente.

Se desejar emular o comportamento anterior ClientBuilder , você pode criar um separado HostBuilder e configurá-lo com um Orleans cliente. IHostBuilder pode ter um Orleans cliente ou um Orleans silo configurado. Todos os silos registram uma instância de IGrainFactory e IClusterClient que o aplicativo pode usar, portanto, configurar um cliente separadamente é desnecessário e não tem suporte.

OnActivateAsync e OnDeactivateAsync alteração de assinatura

Orleans Permite que os grãos executem código durante a ativação e desativação. Isso pode ser usado para executar tarefas como ler o estado do armazenamento ou registrar mensagens do ciclo de vida. Na Orleans versão 7.0, a assinatura desses métodos de ciclo de vida mudou:

  • OnActivateAsync() agora aceita um CancellationToken parâmetro. Quando o CancellationToken é cancelado, o processo de ativação deve ser abandonado.
  • OnDeactivateAsync() agora aceita um DeactivationReason parâmetro e um CancellationToken parâmetro. O DeactivationReason indica por que a ativação está sendo desativada. Espera-se que os desenvolvedores usem essas informações para fins de registro e diagnóstico. Quando o CancellationToken é cancelado, o processo de desativação deve ser concluído imediatamente. Observe que, como qualquer host pode falhar a qualquer momento, não é recomendável confiar para OnDeactivateAsync executar ações importantes, como persistir o estado crítico.

Considere o seguinte exemplo de um grão substituindo esses novos 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;
}

POCO Grãos e IGrainBase

Os grãos não Orleans precisam mais herdar da Grain classe base ou de qualquer outra classe. Esta funcionalidade é conhecida como grãos POCO . Para acessar métodos de extensão, como qualquer um dos seguintes:

Seu grão deve implementar IGrainBase ou herdar de Grain. Aqui está um exemplo de implementação IGrainBase em uma classe de grão:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    public PingGrain(IGrainContext context) => GrainContext = context;

    public IGrainContext GrainContext { get; }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

IGrainBase também define OnActivateAsync e OnDeactivateAsync com implementações padrão, permitindo que seu grão participe de seu ciclo de vida, se desejado:

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;
}

Serialização

A mudança mais onerosa no Orleans 7.0 é a introdução do serializador tolerante à versão. Essa alteração foi feita porque os aplicativos tendem a evoluir e isso levou a uma armadilha significativa para os desenvolvedores, já que o serializador anterior não podia tolerar a adição de propriedades aos tipos existentes. Por outro lado, o serializador era flexível, permitindo que os desenvolvedores representassem a maioria dos tipos .NET sem modificação, incluindo recursos como genéricos, polimorfismo e rastreamento de referência. A substituição já deveria ter sido feita há muito tempo, mas os usuários ainda precisam da representação de alta fidelidade de seus tipos. Portanto, um serializador de substituição foi introduzido no 7.0 que suporta a representação de alta fidelidade de tipos .NET e, ao mesmo tempo Orleans , permite que os tipos evoluam. O novo serializador é muito mais eficiente do que o serializador anterior, resultando em uma taxa de transferência de ponta a ponta até 170% maior.

Para obter mais informações, consulte os seguintes artigos relacionados à Orleans versão 7.0:

Identidades de grãos

Cada grão tem uma identidade única que é composta pelo tipo do grão e sua chave. As versões anteriores do Orleans usavam um tipo composto para GrainIds para suportar chaves de grão de:

Isso envolve alguma complexidade quando se trata de lidar com chaves de grão. As identidades de grãos consistem em dois componentes: um tipo e uma chave. O componente de tipo consistia anteriormente em um código de tipo numérico, uma categoria e 3 bytes de informações de tipo genéricas.

As identidades de grãos agora assumem a forma type/key onde ambas e type key são cordas. A interface de chave de grão mais usada é o IGrainWithStringKey. Isso simplifica muito como a identidade de grãos funciona e melhora o suporte para tipos de grãos genéricos.

As interfaces de grãos também são agora representadas usando um nome legível por humanos, em vez de uma combinação de um código hash e uma representação de cadeia de caracteres de quaisquer parâmetros de tipo genéricos.

O novo sistema é mais personalizável e essas personalizações podem ser impulsionadas por atributos.

  • GrainTypeAttribute(String) em um grão class especifica a porção Tipo de seu id de grão.
  • DefaultGrainTypeAttribute(String) on a grain interface especifica o tipo do grão que IGrainFactory deve ser resolvido por padrão ao obter uma referência de grão. Por exemplo, ao chamar IGrainFactory.GetGrain<IMyGrain>("my-key"), a fábrica de grãos retornará uma referência ao grão "my-type/my-key" se IMyGrain tiver o atributo acima especificado acima.
  • GrainInterfaceTypeAttribute(String) Permite substituir o nome da interface. Especificar um nome explicitamente usando esse mecanismo permite renomear o tipo de interface sem quebrar a compatibilidade com as referências de grão existentes. Observe que sua interface também deve ter o AliasAttribute neste caso, uma vez que sua identidade pode ser serializada. Para obter mais informações sobre como especificar um alias de tipo, consulte a seção sobre serialização.

Como mencionado acima, substituir a classe de grão padrão e os nomes de interface para seus tipos permite que você renomeie os tipos subjacentes sem quebrar a compatibilidade com implantações existentes.

Identidades de fluxo

Quando Orleans os fluxos foram lançados pela primeira vez, os fluxos só podiam ser identificados usando um Guidarquivo . Isso foi eficiente em termos de alocação de memória, mas era difícil para os usuários criar identidades de fluxo significativas, muitas vezes exigindo alguma codificação ou indireção para determinar a identidade de fluxo apropriada para uma determinada finalidade.

Na Orleans versão 7.0, os fluxos agora são identificados usando strings. O Orleans.Runtime.StreamId struct contém três propriedades: a StreamId.Namespace, a StreamId.Keye a StreamId.FullKey. Esses valores de propriedade são cadeias de caracteres UTF-8 codificadas. Por exemplo, StreamId.Create(String, String).

Substituição de SimpleMessageStreams com BroadcastChannel

SimpleMessageStreams (também chamado de SMS) foi removido na versão 7.0. O SMS tinha a mesma interface Orleans.Providers.Streams.PersistentStreamsdo , mas seu comportamento era muito diferente, já que dependia de chamadas diretas grão-a-grão. Para evitar confusão, o SMS foi removido e um novo substituto chamado Orleans.BroadcastChannel foi introduzido.

BroadcastChannel suporta apenas subscrições implícitas e pode ser um substituto direto neste caso. Se você precisa de assinaturas explícitas ou precisa usar a PersistentStream interface (por exemplo, você estava usando SMS em testes enquanto usava EventHub na produção), então MemoryStream é o melhor candidato para você.

BroadcastChannel terá os mesmos comportamentos que o SMS, enquanto MemoryStream se comportará como outros provedores de fluxo. Considere o seguinte exemplo de uso do canal de transmissão:

// 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;
        }
    }
}

A migração para MemoryStream será mais fácil, uma vez que apenas a configuração precisa mudar. Considere a seguinte MemoryStream configuração:

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

O sistema de telemetria foi atualizado na Orleans versão 7.0 e o sistema anterior foi removido em favor de APIs .NET padronizadas, como .NET Metrics para métricas e ActivitySource rastreamento.

Como parte disso, os pacotes existentes Microsoft.Orleans.TelemetryConsumers.* foram removidos. Estamos considerando introduzir um novo conjunto de pacotes para agilizar o processo de integração das métricas emitidas pela Orleans sua solução de monitoramento de escolha. Como sempre, comentários e contribuições são bem-vindos.

A dotnet-counters ferramenta apresenta monitoramento de desempenho para monitoramento de integridade ad-hoc e investigação de desempenho de primeiro nível. Para Orleans contadores, a ferramenta dotnet-counters pode ser usada para monitorá-los:

dotnet counters monitor -n MyApp --counters Microsoft.Orleans

Da mesma forma, as métricas OpenTelemetry podem adicionar os Microsoft.Orleans medidores, conforme mostrado no código a seguir:

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddPrometheusExporter()
        .AddMeter("Microsoft.Orleans"));

Para habilitar o rastreamento distribuído, configure o OpenTelemetry conforme mostrado no código a seguir:

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");
        });
    });

No código anterior, OpenTelemetry é configurado para monitorar:

  • Microsoft.Orleans.Runtime
  • Microsoft.Orleans.Application

Para propagar a atividade, chame AddActivityPropagation:

builder.Host.UseOrleans((_, clientBuilder) =>
{
    clientBuilder.AddActivityPropagation();
});

Refatore recursos do pacote principal em pacotes separados

Na Orleans versão 7.0, fizemos um esforço para incluir extensões em pacotes separados que não dependem Orleans.Coredo . Ou seja, Orleans.Streaming, Orleans.Reminders, e Orleans.Transactions foram separados do núcleo. Isso significa que esses pacotes são inteiramente pagos pelo que você usa e nenhum código no núcleo do Orleans é dedicado a esses recursos. Isso reduz a superfície da API principal e o tamanho da montagem, simplifica o núcleo e melhora o desempenho. Em relação ao desempenho, as transações em Orleans anteriormente exigiam algum código que era executado para cada método para coordenar transações potenciais. Desde então, isso foi transferido para o método por método.

Esta é uma mudança que quebra a compilação. Você pode ter código existente que interage com lembretes ou fluxos chamando métodos que foram definidos anteriormente na classe base, Grain mas agora são métodos de extensão. Tais chamadas que não especificam this (por exemplo GetReminders) precisarão ser atualizadas para incluir this (por exemplo this.GetReminders()) porque os métodos de extensão devem ser qualificados. Haverá um erro de compilação se você não atualizar essas chamadas e a alteração de código necessária pode não ser óbvia se você não souber o que mudou.

Cliente da transação

Orleans 7.0 introduz uma nova abstração para coordenar transações, Orleans.ITransactionClient. Anteriormente, as transações só podiam ser coordenadas por grãos. Com ITransactionCliento , que está disponível por meio de injeção de dependência, os clientes também podem coordenar transações sem precisar de um intermediário. O exemplo a seguir retira créditos de uma conta e os deposita em outra em uma única transação. Esse código pode ser chamado de dentro de um grão ou de um cliente externo que recuperou o ITransactionClient do contêiner de injeção de dependência.

await transactionClient.RunTransaction(
  TransactionOption.Create,
  () => Task.WhenAll(from.Withdraw(100), to.Deposit(100)));

Para transações coordenadas pelo cliente, o cliente deve adicionar os serviços necessários durante o tempo de configuração:

clientBuilder.UseTransactions();

O exemplo BankAccount demonstra o uso do ITransactionClient. Para obter mais informações, consulte Orleans transações.

Reentrância da cadeia de chamadas

Os grãos são de thread único e processam solicitações uma a uma, do início à conclusão, por padrão. Em outras palavras, os grãos não são reentrantes por padrão. Adicionar o ReentrantAttribute a uma classe de grão permite que várias solicitações sejam processadas simultaneamente, de forma intercalada, enquanto ainda são single-threaded. Isso pode ser útil para grãos que não possuem estado interno ou executam muitas operações assíncronas, como emitir chamadas HTTP ou gravar em um banco de dados. Cuidado extra precisa ser tomado quando as solicitações podem intercalar: é possível que o estado de um grão seja observado antes que uma await instrução seja alterada no momento em que a operação assíncrona é concluída e o método retoma a execução.

Por exemplo, o grão a seguir representa um contador. Foi marcado Reentrant, permitindo que várias chamadas intercalem. O Increment() método deve incrementar o contador interno e retornar o valor observado. No entanto, como o corpo do Increment() método observa o estado do grão antes de um await ponto e o atualiza depois, é possível que várias execuções de intercalação possam Increment() resultar em um _value número menor do que o total de Increment() chamadas recebidas. Este é um erro introduzido pelo uso indevido de reentrancy.

Remover o ReentrantAttribute é suficiente para corrigir o 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 tais erros, os grãos não são reentrantes por padrão. A desvantagem disso é a taxa de transferência reduzida para grãos que executam operações assíncronas em sua implementação, uma vez que outras solicitações não podem ser processadas enquanto o grão aguarda a conclusão de uma operação assíncrona. Para aliviar isso, Orleans oferece várias opções para permitir a reentrância em certos casos:

  • Para uma classe inteira: colocar o ReentrantAttribute sobre o grão permite que qualquer pedido ao grão intercale com qualquer outro pedido.
  • Para um subconjunto de métodos: colocar o AlwaysInterleaveAttribute método de interface on the grain permite que as solicitações para esse método intercalem com qualquer outra solicitação e que as solicitações para esse método sejam intercaladas por qualquer outra solicitação.
  • Para um subconjunto de métodos: colocar o ReadOnlyAttribute método de interface on the grain permite que as solicitações para esse método intercalem com qualquer outra ReadOnly solicitação e que as solicitações para esse método sejam intercaladas por qualquer outra ReadOnly solicitação. Neste sentido, é uma forma mais restrita de AlwaysInterleave.
  • Para qualquer solicitação dentro de uma cadeia de chamadas: RequestContext.AllowCallChainReentrancy() e <xref:Orleans. Runtime.RequestContext.SuppressCallChainReentrancy?displayProperty=nameWithType permite ativar e desativar a permissão de solicitações downstream para reentrar no grão. As chamadas retornam um valor que deve ser descartado ao sair da solicitação. Portanto, o uso adequado é o seguinte:
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;

A reentrância da cadeia de chamadas deve ser optada por grão, por cadeia de chamada. Por exemplo, considere dois grãos, grão A ou grão B. Se o grão A permite a reentrância da cadeia de chamada antes de chamar o grão B, o grão B pode chamar de volta para o grão A nessa chamada. No entanto, o grão A não pode chamar de volta para o grão B se o grão B não tiver também habilitado a reentrância da cadeia de chamada. É por grão, por cadeia de chamada.

Os grãos também podem suprimir informações de reentrância da cadeia de chamada de fluir para baixo de uma cadeia de chamada usando using var _ = RequestContext.SuppressCallChainReentrancy(). Isso impede a reentrada de chamadas subsequentes.

ADO.NET scripts de migração

Para garantir a compatibilidade direta com Orleans clustering, persistência e lembretes que dependem de ADO.NET, você precisará do script de migração SQL apropriado:

Selecione os arquivos para o banco de dados usado e aplique-os em ordem.