Compartilhar via


Migrar do Orleans 3.x para o 7.0

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

Migração

Devido a alterações na forma como Orleans identifica grãos e fluxos, a migração de aplicativos existentes usando lembretes, fluxos ou persistência de grãos para Orleans 7.0 no momento não é simples.

Não é possível atualizar sem problemas os aplicativos que executam versões anteriores Orleans via uma atualização gradual para Orleans 7.0. Portanto, use uma estratégia de atualização diferente, como implantar um novo cluster e desativar o anterior. Orleans O 7.0 altera o protocolo de comunicação de forma incompatível, o que significa que os clusters não podem conter uma combinação de Orleans hosts 7.0 e hosts executando versões anteriores Orleans .

Essas mudanças interruptivas têm sido evitadas por muitos anos, mesmo em grandes lançamentos. Por que agora? Há dois motivos principais: identidades e serialização. Em relação às identidades, as identidades de granulação e fluxo agora consistem em cadeias de caracteres. Isso permite que os grãos codificam informações de tipo genérico corretamente e facilita o mapeamento de fluxos para o domínio do aplicativo. Anteriormente, Orleans identificava tipos de grãos usando uma complexa estrutura de dados que não podia representar grãos genéricos, levando a casos extremos. Os fluxos foram identificados por um string namespace e uma Guid chave, que foi eficiente, mas difícil de mapear para o domínio do aplicativo. A serialização agora é tolerante a versões. Isso significa que os tipos podem ser modificados de determinadas maneiras compatíveis, seguindo um conjunto de regras, com confiança de que o aplicativo pode ser atualizado sem erros de serialização. Essa funcionalidade é especialmente útil quando os tipos de aplicativo persistem em fluxos ou armazenamento de grãos. As seções a seguir detalham as principais alterações e as discutem ainda mais.

Alterações de pacote

Ao atualizar um projeto para Orleans 7.0, execute as seguintes ações:

  • Todos os clientes devem referenciar Microsoft.Orleans.Client.
  • Todos os silos (servidores) devem referenciar Microsoft.Orleans.Server.
  • Todos os outros pacotes devem referenciar Microsoft.Orleans.Sdk.
  • Remova todas as referências a Microsoft.Orleans.CodeGenerator.MSBuild e Microsoft.Orleans.OrleansCodeGenerator.Build.
  • Remova todas as referências a Microsoft.Orleans.OrleansRuntime.
  • Remova chamadas para ConfigureApplicationParts. As Partes do Aplicativo foram removidas. O C# Source Generator para Orleans é adicionado a todos os pacotes (incluindo o cliente e o servidor) e gera automaticamente o equivalente a Partes da Aplicação.
  • Substitua referências a Microsoft.Orleans.OrleansServiceBus por Microsoft.Orleans.Streaming.EventHubs.
  • Se estiver usando lembretes, adicione uma referência à Microsoft.Orleans.Reminders.
  • Se estiver usando fluxos, adicione uma referência à Microsoft.Orleans. Streaming.

Dica

Todos os exemplos do Orleans foram atualizados para o Orleans 7.0 e podem ser usados como referência para as alterações que forem feitas. Para mais informações, consulte o Problema do Orleans #8035, que especifica as alterações feitas a cada exemplo.

Orleans diretivas de uso global

Todos os projetos do Orleans referenciam direta ou indiretamente o pacote NuGet Microsoft.Orleans.Sdk. Quando um Orleans projeto é configurado para habilitar usings implícitos (por exemplo, <ImplicitUsings>enable</ImplicitUsings>), o projeto implicitamente utiliza tanto os namespaces Orleans quanto Orleans.Hosting. Isso significa que o código do aplicativo não precisa dessas using diretivas.

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

Hospedagem

O tipo ClientBuilder é substituído pelo método de extensão UseOrleansClient em IHostBuilder. O tipo IHostBuilder vem do pacote NuGet Microsoft.Extensions.Hosting. Isso significa que um Orleans cliente pode ser adicionado a um host existente sem criar um contêiner de injeção de dependência separado. O cliente se conecta ao cluster durante a inicialização. Depois de IHost.StartAsync concluído, o cliente se conecta automaticamente. Os serviços adicionados ao IHostBuilder começam na ordem de registro. A chamada de UseOrleansClient antes de ConfigureWebHostDefaults, por exemplo, garante que Orleans inicie antes do ASP.NET Core, permitindo o acesso imediato ao cliente pelo aplicativo ASP.NET Core.

Para emular o comportamento anterior ClientBuilder , crie um separado HostBuilder e configure-o com um Orleans cliente. Um IHostBuilder pode ser configurado com um Orleans cliente ou um Orleans silo. Todos os silos registram uma instância de IGrainFactory e IClusterClient que o aplicativo pode usar, portanto, configurar um cliente separadamente é desnecessário e sem suporte.

alteração de assinatura OnActivateAsync e OnDeactivateAsync

O Orleans permite que as granularidades executem o código durante a ativação e a desativação. Use esse recurso para executar tarefas como ler o estado do armazenamento ou registrar mensagens de ciclo de vida em log. No Orleans 7.0, a assinatura desses métodos de ciclo de vida mudou:

  • OnActivateAsync() agora aceita um parâmetro CancellationToken. Quando CancellationToken for cancelado, abandone o processo de ativação.
  • OnDeactivateAsync() agora aceita um parâmetro DeactivationReason e um parâmetro CancellationToken. O DeactivationReason indica por que a ativação está sendo desativada. Use essas informações para fins de registro em log e diagnóstico. Quando o CancellationToken for cancelado, conclua o processo de desativação prontamente. Observe que, como qualquer host pode falhar a qualquer momento, não é recomendável contar com OnDeactivateAsync para executar ações importantes, como persistir o estado crítico.

Considere o seguinte exemplo de uma granularidade 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;
}

Grãos POCO e IGrainBase

As granularidades no Orleans não precisam mais herdar da classe base Grain nem de qualquer outra classe. Essa funcionalidade é conhecida como granularidades POCO. Para acessar métodos de extensão, tais como qualquer um dos seguintes:

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

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 os grãos participem 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 pesada no Orleans 7.0 é a introdução do serializador tolerante a versão. Essa alteração foi feita porque os aplicativos tendem a evoluir, o que levou a uma armadilha significativa para os desenvolvedores, uma vez que o serializador anterior não podia tolerar a adição de propriedades aos tipos existentes. Por outro lado, o serializador anterior era flexível, permitindo a representação da maioria dos tipos do .NET sem modificação, incluindo recursos como genéricos, polimorfismo e acompanhamento de referência. Uma substituição estava há muito atrasada, mas a representação de alta fidelidade dos tipos ainda é necessária. Portanto, Orleans a 7.0 apresenta um serializador de substituição que dá suporte à representação de alta fidelidade de tipos .NET e, ao mesmo tempo, permite que os tipos evoluam. O novo serializador é muito mais eficiente do que o anterior, resultando em até 170% maior taxa de transferência de ponta a ponta.

Para obter mais informações, confira os seguintes artigos relacionados ao Orleans 7.0:

Identidades de granularidade

Cada um dos grãos tem uma identidade exclusiva composta pelo tipo do grão e sua chave. As versões anteriores Orleans usavam um tipo composto para GrainIds para dar suporte a chaves de grãos de qualquer dos tipos:

Essa abordagem envolve alguma complexidade ao lidar com chaves de grãos. As identidades de granularidade 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érico.

As identidades de grãos agora assumem a forma type/key, onde tanto type quanto key são cadeias de caracteres. A interface de chave de grãos mais usada é IGrainWithStringKey. Isso simplifica muito o funcionamento da identidade de granularidade e melhora o suporte para tipos genéricos de granularidade.

As interfaces grain agora também são representadas usando um nome fácil de ler, em vez de uma combinação de um código hash e uma representação textual de qualquer parâmetro de tipo genérico.

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

  • GrainTypeAttribute(String) em um grão, class especifica a parte Type de sua identificação de grão.
  • DefaultGrainTypeAttribute(String) em um grão interface especifica o tipo do grão que por padrão IGrainFactory deve ser resolvido 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 mencionado acima especificado.
  • GrainInterfaceTypeAttribute(String) permite substituir o nome da interface. Especificar um nome explicitamente usando esse mecanismo permite renomear o tipo de interface sem interromper a compatibilidade com referências de grãos existentes. Observe que a interface também deve ter o AliasAttribute no caso, pois sua identidade pode ser serializada. Para mais informações sobre como especificar um alias de tipo, consulte a seção sobre serialização.

Conforme mencionado acima, substituir os nomes de classe e interface padrão para tipos permite renomear os tipos subjacentes sem interromper a compatibilidade com as implantações existentes.

Identidades de fluxo

Quando os fluxos do Orleans foram lançados, eles só podiam ser identificados usando um Guid. Essa abordagem foi eficiente em termos de alocação de memória, mas dificultou a criação de 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 são identificados usando cadeias de caracteres. O Orleans.Runtime.StreamIdstruct contém três propriedades: StreamId.Namespace, StreamId.Keye StreamId.FullKey. Esses valores de propriedade são cadeias de caracteres UTF-8 codificadas. Por exemplo, veja StreamId.Create(String, String).

Substituição de SimpleMessageStreams por BroadcastChannel

SimpleMessageStreams (também chamado de SMS) é removido na versão 7.0. O SMS tinha a mesma interface Orleans.Providers.Streams.PersistentStreams, mas seu comportamento era muito diferente porque se baseava em chamadas diretas de grão a grão. Para evitar confusão, o SMS foi removido e uma nova substituição chamada Orleans.BroadcastChannel foi introduzida.

BroadcastChannel só dá suporte a assinaturas implícitas e pode ser um substituto direto nesse caso. Se assinaturas explícitas forem necessárias ou a PersistentStream interface precisar ser usada (por exemplo, se o SMS foi usado em testes enquanto EventHub foi usado na produção), então MemoryStream é o melhor candidato.

BroadcastChannel tem os mesmos comportamentos que o SMS, enquanto MemoryStream se comporta como outros provedores de fluxo. Considere o seguinte exemplo de uso do Broadcast Channel:

// 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 é mais fácil, pois só é necessário alterar a configuração. Considere a configuração MemoryStream a seguir:

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 é atualizado em Orleans 7.0 e o sistema anterior é removido em favor de APIs .NET padronizadas, como Métricas do .NET para métricas e ActivitySource para rastreamento.

Como parte disso, os pacotes existentes Microsoft.Orleans.TelemetryConsumers.* são removidos. Um novo conjunto de pacotes está sendo considerado para simplificar a integração de métricas emitidas pela Orleans solução de monitoramento escolhida. Como sempre, comentários e contribuições são bem-vindos.

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

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

Da mesma forma, adicione as métricas Microsoft.Orleans ao OpenTelemetry, conforme mostrado no seguinte código:

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

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

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 está configurado para monitorar:

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

Para propagar a atividade, chame AddActivityPropagation:

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

Refatorar recursos do pacote principal em pacotes separados

Na versão Orleans 7.0, as extensões foram incorporadas em pacotes separados que não dependem de Orleans.Core. Ou seja, Orleans.Streaminge Orleans.RemindersOrleans.Transactions foram separados do núcleo. Isso significa que esses pacotes são totalmente pagos conforme o que é utilizado, e nenhum código no Orleans núcleo é dedicado a essas funcionalidades. Essa abordagem reduz a superfície da API principal e o tamanho do assembly, simplifica o núcleo e melhora o desempenho. Em relação ao desempenho, as transações em Orleans anteriormente exigiam a execução de algum código para cada método, a fim de coordenar possíveis transações. Essa lógica de coordenação agora é movida para uma base por método.

Essa é uma alteração interruptiva da compilação. O código existente que interage com lembretes ou fluxos chamando métodos anteriormente definidos na classe base Grain pode ter problemas de funcionamento porque agora são métodos de extensão. Atualize essas chamadas que não especificam this (por exemplo, GetReminders) para incluir this (por exemplo, this.GetReminders()) porque os métodos de extensão devem ser qualificados. Ocorrerá um erro de compilação se essas chamadas não forem atualizadas e a alteração de código necessária não for óbvia sem saber o que foi alterado.

Cliente de transação

Orleans 7.0 apresenta uma nova abstração para coordenar transações: Orleans.ITransactionClient. Anteriormente, somente os grãos podiam coordenar transações. Com ITransactionClient, disponível por meio da injeção de dependência, os clientes também podem coordenar transações sem precisar de um grão intermediário. O exemplo a seguir retira créditos de uma conta e os deposita em outra dentro de uma única transação. Chame esse código de dentro de um grão ou de um cliente externo que recuperou o ITransactionClient 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 a configuração:

clientBuilder.UseTransactions();

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

Reentrância da cadeia de chamadas

As granularidades são de thread único e processam solicitações uma a uma do início à conclusão por padrão. Em outras palavras, as granularidades não são reentrantes por padrão. Adicionar a ReentrantAttribute uma classe de "grain" permite que ele processe várias solicitações simultaneamente de forma intercalada, enquanto ainda opera em um único encadeamento. Essa funcionalidade pode ser útil para grãos que não contêm nenhum estado interno ou executam muitas operações assíncronas, como emitir chamadas HTTP ou gravar em um banco de dados. Cuidados extras são necessários quando as solicitações podem intercalar-se: é possível que o estado de um grão observado antes de uma instrução await mude até que a operação assíncrona seja concluída e o método retome a execução.

Por exemplo, a granularidade a seguir representa um contador. Está marcado Reentrant, permitindo que várias chamadas se intercalem. O método Increment() deve incrementar o contador interno e retornar o valor observado. No entanto, como o Increment() corpo do método observa o estado do grão antes de um await ponto e o atualiza posteriormente, várias execuções intercalares de Increment() podem resultar em um _value menor que o número total de chamadas Increment() recebidas. Esse é um erro introduzido pelo uso inadequado da reentrância.

Remover o ReentrantAttribute é suficiente para corrigir esse 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 esses erros, as granularidades não são reentrantes por padrão. A desvantagem é a redução da taxa de transferência para grãos que executam operações assíncronas em sua implementação, pois o grão não pode processar outras solicitações enquanto aguarda a conclusão de uma operação assíncrona. Para aliviar isso, o Orleans oferece várias opções para permitir a reentrância em determinados casos:

  • Para uma classe inteira: colocar o ReentrantAttribute grão permite que qualquer solicitação ao grão intercale com qualquer outra solicitação.
  • Para um subconjunto de métodos: colocar o AlwaysInterleaveAttribute no método de interface de grãos permite que as solicitações para esse método se intercalem com qualquer outra solicitação e permite que qualquer outra solicitação se intercale solicitações para esse método.
  • Para um subconjunto de métodos: colocar o ReadOnlyAttribute na interface do método de grãos permite que as solicitações para esse método se intercalem com qualquer outra ReadOnly solicitação e permite que qualquer outra ReadOnly solicitação se intercale com as solicitações para esse método. Nesse sentido, é uma forma mais restrita de AlwaysInterleave.
  • Para qualquer solicitação dentro de uma cadeia de chamadas: RequestContext.AllowCallChainReentrancy() e RequestContext.SuppressCallChainReentrancy() permitem optar por participar ou não, permitindo que as solicitações downstream voltem a entrar no componente. Ambas as chamadas retornam um valor que deve ser descartado ao sair da solicitação. Portanto, use-os da seguinte maneira:
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;

Adesão para reentrância de chamadas por unidade, por cadeia de chamadas. Por exemplo, considere dois grãos, A e B. Se o grão A habilitar a reentrância da cadeia de chamadas antes de chamar o grão B, o grão B poderá chamar de volta para o grão A nessa chamada. No entanto, o grão A não pode chamar de volta o grão B se o grão B não tiver também habilitado a reentrância da cadeia de chamadas. Ele está habilitado granularmente, por cadeia de chamadas.

As granularidades também podem suprimir as informações de reentrância da cadeia de chamadas de fluir para baixo em uma cadeia de chamadas usando using var _ = RequestContext.SuppressCallChainReentrancy(). Isso impede que chamadas subsequentes sejam reiniciadas.

Scripts de migração ADO.NET

Para garantir a compatibilidade futura com o agrupamento, a persistência e os lembretes que dependem de ADO.NET, é necessário o script de migração SQL apropriado.

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