Compartilhar via


Persistência de grãos ADO.NET

O código de back-end do armazenamento relacional no Orleans é baseado na funcionalidade do ADO.NET genérica e, consequentemente, é independente do fornecedor de banco de dados. O layout de armazenamento de dados do Orleans já foi explicado em tabelas de runtime. A configuração das cadeias de conexão é feita como explicado no Guia de Configuração do Orleans.

Para fazer com que o código do Orleans funcione com um determinado back-end de banco de dados relacional, o seguinte é necessário:

  1. A biblioteca do ADO.NET apropriada precisa ser carregada no processo. Isso deve ser definido como de costume, por exemplo, por meio do elemento DbProviderFactories na configuração do aplicativo.
  2. Configure o ADO.NET invariável por meio da propriedade Invariant nas opções.
  3. O banco de dados precisa existir e ser compatível com o código. Isso é feito executando um script de criação de banco de dados específico do fornecedor. Para obter mais informações, consulte Configuração do ADO.NET.

O provedor de armazenamento de grãos do ADO .NET permite armazenar o estado de grãos em bancos de dados relacionais. Atualmente, os bancos de dados a seguir são compatíveis:

  • SQL Server
  • MySQL/MariaDB
  • PostgreSQL
  • Oracle

Primeiro, instale o pacote base:

Install-Package Microsoft.Orleans.Persistence.AdoNet

Leia o artigo de configuração ADO.NET para obter informações sobre como configurar seu banco de dados, incluindo o invariante do ADO.NET correspondente e os scripts de instalação.

Veja a seguir um exemplo de como configurar um provedor de armazenamento ADO.NET pelo ISiloHostBuilder:

var siloHostBuilder = new HostBuilder()
    .UseOrleans(c =>
    {
        c.AddAdoNetGrainStorage("OrleansStorage", options =>
        {
            options.Invariant = "<Invariant>";
            options.ConnectionString = "<ConnectionString>";
            options.UseJsonFormat = true;
        });
    });

Essencialmente, você só precisa definir a cadeia de conexão específica do fornecedor de banco de dados e um Invariant (consulte Configuração do ADO.NET) que identifique o fornecedor. Você também pode escolher o formato no qual os dados são salvos, que podem ser binários (padrão), JSON ou XML. Embora o binário seja a opção mais compacta, ele é opaco e você não poderá ler ou trabalhar com os dados. Essa é a opção JSON recomendada.

Defina as seguintes propriedades pelo AdoNetGrainStorageOptions:

/// <summary>
/// Options for AdoNetGrainStorage
/// </summary>
public class AdoNetGrainStorageOptions
{
    /// <summary>
    /// Define the property of the connection string
    /// for AdoNet storage.
    /// </summary>
    [Redact]
    public string ConnectionString { get; set; }

    /// <summary>
    /// Set the stage of the silo lifecycle where storage should
    /// be initialized.  Storage must be initialized prior to use.
    /// </summary>
    public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
    /// <summary>
    /// Default init stage in silo lifecycle.
    /// </summary>
    public const int DEFAULT_INIT_STAGE =
        ServiceLifecycleStage.ApplicationServices;

    /// <summary>
    /// The default ADO.NET invariant will be used for
    /// storage if none is given.
    /// </summary>
    public const string DEFAULT_ADONET_INVARIANT =
        AdoNetInvariants.InvariantNameSqlServer;

    /// <summary>
    /// Define the invariant name for storage.
    /// </summary>
    public string Invariant { get; set; } =
        DEFAULT_ADONET_INVARIANT;

    /// <summary>
    /// Determine whether the storage string payload should be formatted in JSON.
    /// <remarks>If neither <see cref="UseJsonFormat"/> nor <see cref="UseXmlFormat"/> is set to true, then BinaryFormatSerializer will be configured to format the storage string payload.</remarks>
    /// </summary>
    public bool UseJsonFormat { get; set; }
    public bool UseFullAssemblyNames { get; set; }
    public bool IndentJson { get; set; }
    public TypeNameHandling? TypeNameHandling { get; set; }

    public Action<JsonSerializerSettings> ConfigureJsonSerializerSettings { get; set; }

    /// <summary>
    /// Determine whether storage string payload should be formatted in Xml.
    /// <remarks>If neither <see cref="UseJsonFormat"/> nor <see cref="UseXmlFormat"/> is set to true, then BinaryFormatSerializer will be configured to format storage string payload.</remarks>
    /// </summary>
    public bool UseXmlFormat { get; set; }
}

A persistência ADO.NET tem a funcionalidade para dados de versão e define (des)serializadores arbitrários com regras arbitrárias de aplicativo e streaming, mas, atualmente, não há nenhum método para expô-los ao código do aplicativo.

Lógica de persistência do ADO.NET

Os princípios para armazenamento de persistência apoiados pelo ADO.NET são:

  1. Mantenha os dados críticos para os negócios seguros e acessíveis enquanto os dados, o formato dos dados e o código evoluem.
  2. Aproveite a funcionalidade específica do fornecedor e do armazenamento.

Na prática, isso significa aderir às metas de implementação ADO.NET e alguma lógica de implementação adicionada em provedores de armazenamento específicos ADO.NET que permitem evoluir a forma dos dados no armazenamento.

Além dos recursos usuais do provedor de armazenamento, o provedor de ADO.NET tem capacidade interna para:

  1. Altere os dados de armazenamento de um formato para outro (por exemplo, de JSON para binário) quando o estado de tropeço redondo.
  2. Formate o tipo a ser salvo ou lido do armazenamento de maneiras arbitrárias. Isso permite que a versão do estado evolua.
  3. Transmitir dados para fora do banco de dados.

1. e 2. podem ser aplicados com base em parâmetros de decisão arbitrários, como ID de grãos, tipo de grão, dados de carga.

Esse é o caso para que você possa escolher um formato de serialização, por exemplo, SBE (Codificação Binária Simples) e implementa IStorageDeserializer e IStorageSerializer. Os serializadores internos foram criados usando este método:

Quando os serializadores tiverem sido implementados, eles precisarão ser adicionados à propriedade StorageSerializationPicker em AdoNetGrainStorage. Esta é uma implementação de IStorageSerializationPicker. Por padrão, StorageSerializationPicker será usado. Um exemplo de alteração do formato de armazenamento de dados ou do uso de serializadores pode ser visto em RelationalStorageTests.

Atualmente, não há nenhum método para expor o seletor de serialização ao aplicativo Orleans, pois não há nenhum método para acessar o AdoNetGrainStorage criado pelo framework.

Metas do design

1. Permitir o uso de qualquer back-end que tenha um provedor de ADO.NET

Isso deve abranger o conjunto mais amplo possível de back-ends disponíveis para o .NET, que é um fator em instalações locais. Alguns provedores são listados na Visão geral do ADO.NET, mas nem todos estão listados, como Teradata.

2. Manter o potencial de ajustar consultas e estrutura de banco de dados conforme apropriado, mesmo enquanto uma implantação estiver em execução

Em muitos casos, os servidores e bancos de dados são hospedados por terceiros em relação contratual com o cliente. Não é uma situação incomum encontrar um ambiente de hospedagem virtualizado e onde o desempenho flutua devido a fatores imprevistos, como vizinhos barulhentos ou hardware defeituoso. Talvez não seja possível alterar e implantar novamente binários do Orleans (por razões contratuais) ou até mesmo binários de aplicativo, mas geralmente é possível ajustar os parâmetros de implantação do banco de dados. Alterar componentes padrão, como binários do Orleans, requer um procedimento mais longo para a otimização em uma determinada situação.

3. Permite que você use habilidades específicas do fornecedor e da versão

Os fornecedores implementaram diferentes extensões e recursos nos seus produtos. É sensato usar esses recursos quando disponíveis. São recursos como UPSERT nativo ou PipelineDB no PostgreSQL e PolyBase ou tabelas compiladas nativamente e procedimentos armazenados no SQL Server.

4. Possibilitear a otimização dos recursos de hardware

Ao criar um aplicativo, geralmente é possível prever quais dados precisam ser inseridos mais rapidamente do que outros e quais podem ser colocados no armazenamento frio, o que é mais barato (por exemplo, dividir dados entre SSD e HDD). Considerações adicionais incluem o local físico dos dados (alguns dados podem ser mais caros, por exemplo, RAID SSD viz HDD RAID, ou mais protegidos) ou alguma outra base de decisão. Relacionado ao ponto 3., alguns bancos de dados oferecem esquemas especiais de particionamento, como Tabelas e índices particionados do SQL Server.

Esses princípios se aplicam ao longo do ciclo de vida do aplicativo. Considerando que um dos princípios do Orleans é a alta disponibilidade, deve ser possível ajustar o sistema de armazenamento sem interrupção para a implantação do Orleans, ou ajustar as consultas de acordo com dados e outros parâmetros de aplicativo. Um exemplo de alterações dinâmicas pode ser visto na postagem no blog de Brian Harry:

Quando uma tabela é pequena, quase não importa qual é o plano de consulta. Quando é média, um plano de consulta OK é bom, mas quando é enorme (milhões e milhões ou bilhões de linhas), até mesmo uma pequena variação no plano de consulta pode ser excessiva. Por esse motivo, sugerimos muito nossas consultas confidenciais.

5. Nenhuma suposição sobre quais ferramentas, bibliotecas ou processos de implantação são usadas em organizações

Muitas organizações conhecem bem um determinado conjunto de ferramentas de banco de dados, por exemplo, Dacpac ou Red Gate. Pode ser que a implantação de um banco de dados exija permissão ou uma pessoa, como alguém em uma função DBA, para fazê-lo. Normalmente, isso significa também ter o layout do banco de dados de destino e um esboço aproximado das consultas que o aplicativo produzirá para uso na estimativa da carga. Pode haver processos, talvez influenciados pelos padrões do setor, que exigem a implantação baseada em script. Ter as consultas e estruturas de banco de dados em um script externo torna isso possível.

6. Usar o conjunto mínimo de funcionalidades de interface necessárias para carregar as bibliotecas e a funcionalidade do ADO.NET

Isso é rápido e tem menos superfície exposta às discrepâncias de implementação da biblioteca ADO.NET.

7. Tornar o design fragmentável

Quando fizer sentido, por exemplo, em um provedor de armazenamento relacional, torne o design prontamente fragmentável. Por exemplo, isso significa não usar dados dependentes do banco de dados (ex.: IDENTITY). As informações que distinguem os dados de linha devem ser baseadas apenas em dados dos parâmetros reais.

8. Tornar o design fácil de testar

Criar um novo back-end deve ser idealmente tão fácil quanto traduzir um dos scripts de implantação atuais para o dialeto SQL do back-end que você está tentando direcionar, adicionando uma nova cadeia de conexão aos testes (supondo parâmetros padrão), verificando se um determinado banco de dados está instalado e executando os testes nele.

9. Levando em conta os pontos anteriores, tornar os scripts de portabilidade para novos back-ends e modificar os scripts de back-end já implantados o mais transparente possível

Realização das metas

A estrutura do Orleans não conhece o hardware específico da implantação (qual hardware pode mudar durante a implantação ativa), a alteração de dados durante o ciclo de vida da implantação ou determinados recursos específicos do fornecedor que só podem ser usados em determinadas situações. Por esse motivo, a interface entre o banco de dados e o Orleans deve aderir ao conjunto mínimo de abstrações e regras para atender a essas metas, torná-la robusta contra o uso indevido e facilitar o teste, se necessário. Tabelas de runtime, gerenciamento de cluster e a implementação concreta do protocolo de associação. Além disso, a implementação do SQL Server contém ajuste específico à edição do SQL Server. O contrato de interface entre o banco de dados e o Orleans é definido da seguinte maneira:

  1. A ideia geral é que os dados sejam lidos e gravados por meio de consultas específicas do Orleans. O Orleans opera em nomes e tipos de coluna ao ler e em nomes e tipos de parâmetro ao gravar.
  2. As implementações precisam preservar nomes e tipos de entrada e saída. O Orleans usa esses parâmetros para ler os resultados da consulta por nome e tipo. É permitido o ajuste específico do fornecedor e da implantação e as contribuições são incentivadas, desde que o contrato de interface seja mantido.
  3. A implementação entre scripts específicos do fornecedor deve preservar os nomes de restrição. Isso simplifica a solução de problemas em virtude da nomenclatura uniforme em implementações concretas.
  4. Versão – ou ETag no código do aplicativo – para o Orleans, isso representa uma versão exclusiva. O tipo de implementação real não é importante, desde que represente uma versão exclusiva. Na implementação, o código do Orleans espera um inteiro de 32 bits assinado.
  5. Para ser claro e remover ambiguidades, o Orleans espera que algumas consultas retornem TRUE como o valor > 0 ou FALSE como o valor = 0. Ou seja, o número de linhas afetadas ou retornadas não importa. Se um erro for gerado ou uma exceção for gerada, a consulta precisará garantir que toda a transação seja revertida e possa retornar FALSE ou propagar a exceção.
  6. Atualmente, todas, com exceção de uma consulta, são inserções ou atualizações de linha única (observação, pode-se substituir consultas UPDATE por INSERT, desde que as consultas SELECT associadas executaram a última gravação).

Os mecanismos de banco de dados são compatíveis com a programação no banco de dados. Isso é semelhante à ideia de carregar um script executável e invocá-lo para executar operações de banco de dados. No pseudocódigo, ele pode ser descrito como:

const int Param1 = 1;
const DateTime Param2 = DateTime.UtcNow;
const string queryFromOrleansQueryTableWithSomeKey =
    "SELECT column1, column2 "+
    "FROM <some Orleans table> " +
    "WHERE column1 = @param1 " +
    "AND column2 = @param2;";
TExpected queryResult =
    SpecificQuery12InOrleans<TExpected>(query, Param1, Param2);

Esses princípios também estão incluídos nos scripts de banco de dados.

Algumas ideias sobre como aplicar scripts personalizados

  1. Altere os scripts em OrleansQuery para a persistência de grãos com IF ELSE, para que algum estado seja salvo usando o padrão INSERT, enquanto alguns estados de grãos podem usar tabelas com otimização de memória. As consultas SELECT precisam ser alteradas adequadamente.
  2. A ideia em 1. pode ser usada para aproveitar outros aspectos específicos de implantação ou fornecedor, como dividir dados entre SSD ou HDD, colocar alguns dados em tabelas criptografadas ou inserir dados de estatísticas por meio do SQL Server para Hadoop ou até mesmo servidores vinculados.

Os scripts alterados podem ser testados executando o conjunto de testes do Orleans ou diretamente no banco de dados usando, por exemplo, o Projeto de teste de unidade do SQL Server.

Diretrizes para adicionar novos provedores do ADO.NET

  1. Adicione um novo script de configuração de banco de dados de acordo com a seção Realização de metas acima.
  2. Adicione o nome invariável do ADO do fornecedor para AdoNetInvariants e os dados específicos ao provedor ADO.NET para DbConstantsStore. Eles são (potencialmente) usados em algumas operações de consulta. por exemplo, para selecionar o modo de inserção de estatísticas corretas (ou seja, com UNION ALL ou sem FROM DUAL).
  3. O Orleans tem testes abrangentes para todos os repositórios do sistema: associação, lembretes e estatísticas. A adição de testes para o novo script de banco de dados é feita copiando classes de teste existentes e alterando o nome invariável do ADO. Além disso, derive de RelationalStorageForTesting para definir a funcionalidade de teste para o invariável ADO.