Mais desenvolvedores deveriam saber isso sobre bancos de dados

Observação. trad.: Jaana Dogan é uma engenheira experiente do Google que atualmente trabalha na observabilidade dos serviços de produção da empresa escritos em Go. Neste artigo, que ganhou grande popularidade entre o público de língua inglesa, ela coletou em 17 pontos detalhes técnicos importantes sobre SGBDs (e às vezes sistemas distribuídos em geral) que são úteis para desenvolvedores de aplicações grandes/exigentes.

Mais desenvolvedores deveriam saber isso sobre bancos de dados

A grande maioria dos sistemas informáticos monitoriza o seu estado e, consequentemente, requer algum tipo de sistema de armazenamento de dados. Acumulei conhecimento sobre bancos de dados durante um longo período de tempo, cometendo erros de design que levaram à perda e interrupções de dados. Em sistemas que processam grandes volumes de informação, os bancos de dados estão no centro da arquitetura do sistema e atuam como um elemento-chave na escolha da solução ideal. Apesar de ser dada muita atenção ao trabalho do banco de dados, os problemas que os desenvolvedores de aplicativos tentam antecipar são muitas vezes apenas a ponta do iceberg. Nesta série de artigos, compartilho algumas ideias que serão úteis para desenvolvedores que não são especializados na área.

  1. Você terá sorte se 99,999% das vezes a rede não estiver causando problemas.
  2. ACID significa muitas coisas diferentes.
  3. Cada banco de dados possui seus próprios mecanismos para garantir consistência e isolamento.
  4. O bloqueio otimista vem em socorro quando é difícil manter o habitual.
  5. Existem outras anomalias além de leituras sujas e perda de dados.
  6. O banco de dados e o usuário nem sempre concordam sobre o curso de ação.
  7. A fragmentação no nível do aplicativo pode ser movida para fora do aplicativo.
  8. O incremento automático pode ser perigoso.
  9. Dados desatualizados podem ser úteis e não precisam ser bloqueados.
  10. As distorções são típicas de qualquer fonte de tempo.
  11. Atraso tem muitos significados.
  12. Os requisitos de desempenho devem ser avaliados para uma transação específica.
  13. As transações aninhadas podem ser perigosas.
  14. As transações não devem estar vinculadas ao estado do aplicativo.
  15. Os planejadores de consultas podem dizer muito sobre bancos de dados.
  16. A migração online é difícil, mas possível.
  17. Um aumento significativo na base de dados implica um aumento na imprevisibilidade.

Gostaria de agradecer a Emmanuel Odeke, Rein Henrichs e outros pelos seus comentários sobre uma versão anterior deste artigo.

Você terá sorte se 99,999% das vezes a rede não estiver causando problemas.

Permanece a questão sobre quão confiáveis ​​são as tecnologias de rede modernas e com que frequência os sistemas ficam inativos devido a falhas na rede. A informação sobre esta questão é escassa e a investigação é muitas vezes dominada por grandes organizações com redes, equipamentos e pessoal especializados.

Com uma taxa de disponibilidade de 99,999% para o Spanner (banco de dados distribuído globalmente do Google), o Google afirma que apenas 7,6% problemas estão relacionados à rede. Ao mesmo tempo, a empresa chama a sua rede especializada de “principal pilar” da alta disponibilidade. Estudar Bailis e Kingsbury, realizado em 2014, desafia um dos “equívocos sobre computação distribuída", que Peter Deutsch formulou em 1994. A rede é realmente confiável?

Pesquisas abrangentes fora de empresas gigantes, realizadas para a Internet em geral, simplesmente não existem. Também não existem dados suficientes dos principais intervenientes sobre a percentagem de problemas dos seus clientes que estão relacionados com a rede. Estamos bem cientes das interrupções na rede de grandes provedores de nuvem que podem derrubar uma parte inteira da Internet por várias horas, simplesmente porque são eventos de alto perfil que afetam um grande número de pessoas e empresas. As interrupções na rede podem causar problemas em muitos outros casos, mesmo que nem todos estejam no centro das atenções. Os clientes de serviços em nuvem também não sabem nada sobre as causas dos problemas. Se houver uma falha, é quase impossível atribuí-la a um erro de rede por parte do provedor de serviços. Para eles, os serviços de terceiros são caixas pretas. É impossível avaliar o impacto sem ser um grande prestador de serviços.

Dado o que os grandes players relatam sobre seus sistemas, é seguro dizer que você está com sorte se as dificuldades de rede forem responsáveis ​​por apenas uma pequena porcentagem dos possíveis problemas de tempo de inatividade. As comunicações de rede ainda sofrem com coisas mundanas como falhas de hardware, alterações de topologia, alterações de configuração administrativa e cortes de energia. Recentemente, fiquei surpreso ao saber que a lista de possíveis problemas foi adicionada mordidas de tubarão (sim, você ouviu direito).

ACID significa muitas coisas diferentes

A sigla ACID significa Atomicidade, Consistência, Isolamento, Confiabilidade. Estas propriedades das transações têm como objetivo garantir a sua validade em caso de falhas, erros, falhas de hardware, etc. Sem o ACID ou esquemas semelhantes, seria difícil para os desenvolvedores de aplicativos diferenciar entre o que eles são responsáveis ​​e o que o banco de dados é responsável. A maioria dos bancos de dados transacionais relacionais tenta ser compatível com ACID, mas novas abordagens como NoSQL deram origem a muitos bancos de dados sem transações ACID porque sua implementação é cara.

Quando entrei na indústria, nosso líder técnico falou sobre a relevância do conceito ACID. Para ser justo, o ACID é considerado uma descrição aproximada e não um padrão de implementação estrito. Hoje considero-o sobretudo útil porque levanta uma categoria específica de questões (e sugere uma série de soluções possíveis).

Nem todo DBMS é compatível com ACID; Ao mesmo tempo, as implementações de banco de dados que suportam ACID entendem o conjunto de requisitos de maneira diferente. Uma das razões pelas quais as implementações do ACID são irregulares é devido às muitas compensações que precisam ser feitas para implementar os requisitos do ACID. Os criadores podem apresentar seus bancos de dados como compatíveis com ACID, mas a interpretação de casos extremos pode variar drasticamente, assim como o mecanismo para lidar com eventos "improváveis". No mínimo, os desenvolvedores podem obter uma compreensão de alto nível das complexidades das implementações básicas para obter uma compreensão adequada de seu comportamento especial e das compensações de design.

O debate sobre se o MongoDB está em conformidade com os requisitos do ACID continua mesmo após o lançamento da versão 4. MongoDB não é suportado há muito tempo exploração madeireira, embora por padrão os dados fossem confirmados no disco no máximo uma vez a cada 60 segundos. Imagine o seguinte cenário: uma aplicação publica duas gravações (w1 e w2). O MongoDB armazena w1 com sucesso, mas w2 é perdido devido a uma falha de hardware.

Mais desenvolvedores deveriam saber isso sobre bancos de dados
Diagrama ilustrando o cenário. O MongoDB trava antes de poder gravar dados no disco

A confirmação no disco é um processo caro. Ao evitar commits frequentes, os desenvolvedores melhoram o desempenho da gravação em detrimento da confiabilidade. Atualmente, o MongoDB oferece suporte ao log, mas gravações sujas ainda podem afetar a integridade dos dados, pois os logs são capturados a cada 100 ms por padrão. Ou seja, um cenário semelhante ainda é possível para os logs e as alterações neles apresentadas, embora o risco seja muito menor.

Cada banco de dados possui seus próprios mecanismos de consistência e isolamento

Dos requisitos ACID, a consistência e o isolamento apresentam o maior número de implementações diferentes porque a gama de compensações é mais ampla. Deve ser dito que consistência e isolamento são funções bastante caras. Exigem coordenação e aumentam a concorrência pela consistência dos dados. A complexidade do problema aumenta significativamente quando é necessário dimensionar o banco de dados horizontalmente em vários data centers (especialmente se eles estiverem localizados em regiões geográficas diferentes). Alcançar um alto nível de consistência é muito difícil, pois também reduz a disponibilidade e aumenta a segmentação da rede. Para uma explicação mais geral deste fenômeno, aconselho você a consultar Teorema CAP. Também é importante notar que os aplicativos podem lidar com pequenas quantidades de inconsistências, e os programadores podem entender as nuances do problema bem o suficiente para implementar lógica adicional no aplicativo para lidar com a inconsistência sem depender muito do banco de dados para lidar com isso.

Os SGBDs geralmente fornecem diferentes níveis de isolamento. Os desenvolvedores de aplicativos podem escolher o mais eficaz com base em suas preferências. O baixo isolamento permite maior velocidade, mas também aumenta o risco de uma corrida de dados. O alto isolamento reduz essa probabilidade, mas retarda o trabalho e pode levar à competição, o que levará a tais freios na base que começarão as falhas.

Mais desenvolvedores deveriam saber isso sobre bancos de dados
Revisão dos modelos de simultaneidade existentes e relacionamentos entre eles

O padrão SQL define apenas quatro níveis de isolamento, embora na teoria e na prática existam muitos mais. Jepson.io oferece uma excelente visão geral dos modelos de simultaneidade existentes. Por exemplo, o Google Spanner garante serialização externa com sincronização de relógio e, embora seja uma camada de isolamento mais rigorosa, não está definida nas camadas de isolamento padrão.

O padrão SQL menciona os seguintes níveis de isolamento:

  • Serializável (mais rigoroso e caro): A execução serializável tem o mesmo efeito que alguma execução de transação sequencial. A execução sequencial significa que cada transação subsequente começa somente após a conclusão da anterior. Deve-se notar que o nível Serializável frequentemente implementado como o chamado isolamento de instantâneo (por exemplo, no Oracle) devido a diferenças na interpretação, embora o isolamento de instantâneo em si não seja representado no padrão SQL.
  • Leituras repetíveis: Os registros não confirmados na transação atual estão disponíveis para a transação atual, mas as alterações feitas por outras transações (como novas linhas) não visível.
  • Leitura confirmada: os dados não confirmados não estão disponíveis para transações. Nesse caso, as transações só podem ver os dados confirmados e podem ocorrer leituras fantasmas. Se uma transação inserir e confirmar novas linhas, a transação atual poderá vê-las quando consultada.
  • Leia sem compromisso (nível menos rigoroso e caro): leituras sujas são permitidas, as transações podem ver alterações não confirmadas feitas por outras transações. Na prática, este nível pode ser útil para estimativas aproximadas, como consultas COUNT(*) na mesa.

Nível Serializável minimiza o risco de corridas de dados, ao mesmo tempo que é o mais caro de implementar e resulta na maior carga competitiva no sistema. Outros níveis de isolamento são mais fáceis de implementar, mas aumentam a probabilidade de corridas de dados. Alguns SGBDs permitem definir um nível de isolamento personalizado, outros têm preferências fortes e nem todos os níveis são suportados.

O suporte para níveis de isolamento é frequentemente anunciado em um determinado SGBD, mas somente um estudo cuidadoso de seu comportamento pode revelar o que realmente está acontecendo.

Mais desenvolvedores deveriam saber isso sobre bancos de dados
Revisão de anomalias de simultaneidade em diferentes níveis de isolamento para diferentes SGBDs

Martin Kleppmann em seu projeto eremitério Compara diferentes níveis de isolamento, fala sobre anomalias de simultaneidade e se o banco de dados é capaz de aderir a um determinado nível de isolamento. A pesquisa de Kleppmann mostra como os desenvolvedores de bancos de dados pensam de maneira diferente sobre os níveis de isolamento.

O bloqueio otimista vem em socorro quando é difícil manter o habitual.

O bloqueio pode ser muito caro, não apenas porque aumenta a concorrência no banco de dados, mas também porque exige que os servidores de aplicativos se conectem constantemente ao banco de dados. A segmentação da rede pode exacerbar situações de bloqueio exclusivo e levar a impasses difíceis de identificar e resolver. Nos casos em que o bloqueio exclusivo não é adequado, o bloqueio otimista ajuda.

Bloqueio otimista é um método no qual ao ler uma string leva em consideração sua versão, checksum ou horário da última modificação. Isso permite garantir que não haja alteração de versão atômica antes de alterar uma entrada:

UPDATE products
SET name = 'Telegraph receiver', version = 2
WHERE id = 1 AND version = 1

Neste caso, atualizando a tabela products não será executado se outra operação tiver feito alterações anteriormente nesta linha. Se nenhuma outra operação foi realizada nesta linha, a alteração de uma linha ocorrerá e podemos dizer que a atualização foi bem-sucedida.

Existem outras anomalias além de leituras sujas e perda de dados

Quando se trata de consistência de dados, o foco está no potencial de condições de corrida que podem levar a leituras sujas e perda de dados. No entanto, as anomalias de dados não param por aí.

Um exemplo de tais anomalias é a distorção de gravação (escrever distorções). As distorções são difíceis de detectar porque geralmente não são procuradas ativamente. Eles não se devem a leituras sujas ou perda de dados, mas a violações de restrições lógicas impostas aos dados.

Por exemplo, vamos considerar um aplicativo de monitoramento que exige que um operador esteja de plantão o tempo todo:

BEGIN tx1;                      BEGIN tx2;
SELECT COUNT(*)
FROM operators
WHERE oncall = true;
0                               SELECT COUNT(*)
                                FROM operators
                                WHERE oncall = TRUE;
                                0
UPDATE operators                UPDATE operators
SET oncall = TRUE               SET oncall = TRUE
WHERE userId = 4;               WHERE userId = 2;
COMMIT tx1;                     COMMIT tx2;

Na situação acima, ocorrerá uma corrupção de registro se ambas as transações forem confirmadas com sucesso. Embora não tenha havido leituras sujas ou perda de dados, a integridade dos dados foi comprometida: agora duas pessoas são consideradas de plantão ao mesmo tempo.

O isolamento serializável, o design do esquema ou as restrições do banco de dados podem ajudar a eliminar a corrupção na gravação. Os desenvolvedores devem ser capazes de identificar tais anomalias durante o desenvolvimento para evitá-las na produção. Ao mesmo tempo, é extremamente difícil detectar distorções de gravação na base de código. Principalmente em grandes sistemas, quando diferentes equipes de desenvolvimento são responsáveis ​​pela implementação de funções baseadas nas mesmas tabelas e não concordam com as especificidades do acesso aos dados.

O banco de dados e o usuário nem sempre concordam sobre o que fazer

Uma das principais características dos bancos de dados é a garantia da ordem de execução, mas esta ordem em si pode não ser transparente para o desenvolvedor do software. Os bancos de dados executam transações na ordem em que são recebidas, e não na ordem pretendida pelos programadores. A ordem das transações é difícil de prever, especialmente em sistemas paralelos altamente carregados.

Durante o desenvolvimento, especialmente ao trabalhar com bibliotecas sem bloqueio, o estilo pobre e a baixa legibilidade podem fazer com que os usuários acreditem que as transações são executadas sequencialmente, quando na verdade elas poderiam chegar ao banco de dados em qualquer ordem.

À primeira vista, no programa abaixo, T1 e T2 são chamados sequencialmente, mas se essas funções não forem bloqueadoras e retornarem imediatamente o resultado no formato promessa, então a ordem das chamadas será determinada pelos momentos em que elas entraram no banco de dados:

result1 = T1() // resultados reais são promessas
resultado2 = T2()

Se a atomicidade for necessária (ou seja, todas as operações devem ser concluídas ou abortadas) e a sequência for importante, então as operações T1 e T2 deverão ser executadas em uma única transação.

A fragmentação no nível do aplicativo pode ser movida para fora do aplicativo

Sharding é um método de particionar horizontalmente um banco de dados. Alguns bancos de dados podem dividir automaticamente os dados horizontalmente, enquanto outros não podem ou não são muito bons nisso. Quando arquitetos/desenvolvedores de dados são capazes de prever exatamente como os dados serão acessados, eles podem criar partições horizontais no espaço do usuário em vez de delegar esse trabalho ao banco de dados. Este processo é chamado de "fragmentação em nível de aplicativo" (fragmentação em nível de aplicativo).

Infelizmente, esse nome muitas vezes cria o equívoco de que a fragmentação reside em serviços de aplicativos. Na verdade, ele pode ser implementado como uma camada separada na frente do banco de dados. Dependendo do crescimento dos dados e das iterações do esquema, os requisitos de fragmentação podem tornar-se bastante complexos. Algumas estratégias podem se beneficiar da capacidade de iterar sem precisar reimplantar servidores de aplicativos.

Mais desenvolvedores deveriam saber isso sobre bancos de dados
Um exemplo de arquitetura em que os servidores de aplicativos são separados do serviço de fragmentação

Mover a fragmentação para um serviço separado expande a capacidade de usar diferentes estratégias de fragmentação sem a necessidade de reimplantar aplicativos. Velocidades é um exemplo desse sistema de fragmentação no nível do aplicativo. Vitess fornece fragmentação horizontal para MySQL e permite que os clientes se conectem a ele por meio do protocolo MySQL. O sistema segmenta os dados em diferentes nós MySQL que nada sabem uns sobre os outros.

O incremento automático pode ser perigoso

AUTOINCREMENT é uma forma comum de gerar chaves primárias. Muitas vezes há casos em que bancos de dados são usados ​​como geradores de ID e o banco de dados contém tabelas projetadas para gerar identificadores. Existem vários motivos pelos quais a geração de chaves primárias usando incremento automático está longe de ser ideal:

  • Em um banco de dados distribuído, o incremento automático é um problema sério. Para gerar o ID, é necessário um bloqueio global. Em vez disso, você pode gerar um UUID: isso não requer interação entre diferentes nós do banco de dados. O incremento automático com bloqueios pode levar à contenção e reduzir significativamente o desempenho em inserções em situações distribuídas. Alguns SGBDs (por exemplo, MySQL) podem exigir configuração especial e atenção mais cuidadosa para organizar adequadamente a replicação mestre-mestre. E é fácil cometer erros na configuração, o que levará a falhas de gravação.
  • Alguns bancos de dados possuem algoritmos de particionamento baseados em chaves primárias. IDs consecutivos podem levar a pontos de acesso imprevisíveis e aumento de carga em algumas partições enquanto outras permanecem ociosas.
  • Uma chave primária é a maneira mais rápida de acessar uma linha em um banco de dados. Com melhores maneiras de identificar registros, os IDs sequenciais podem transformar a coluna mais importante das tabelas em uma coluna inútil cheia de valores sem sentido. Portanto, sempre que possível, escolha uma chave primária natural e globalmente única (por exemplo, nome de usuário).

Antes de decidir sobre uma abordagem, considere o impacto do incremento automático de IDs e UUIDs na indexação, particionamento e fragmentação.

Dados desatualizados podem ser úteis e não requerem bloqueio

O Multiversion Concurrency Control (MVCC) implementa muitos dos requisitos de consistência que foram brevemente discutidos acima. Alguns bancos de dados (por exemplo, Postgres, Spanner) usam MVCC para “alimentar” transações com snapshots – versões mais antigas do banco de dados. As transações instantâneas também podem ser serializadas para garantir consistência. Ao ler um instantâneo antigo, são lidos dados desatualizados.

A leitura de dados ligeiramente desatualizados pode ser útil, por exemplo, ao gerar análises a partir dos dados ou ao calcular valores agregados aproximados.

A primeira vantagem de trabalhar com dados legados é a baixa latência (especialmente se o banco de dados estiver distribuído em diferentes regiões geográficas). A segunda é que as transações somente leitura não têm bloqueio. Esta é uma vantagem significativa para aplicativos que leem muito, desde que possam lidar com dados desatualizados.

Mais desenvolvedores deveriam saber isso sobre bancos de dados
O servidor de aplicativos lê dados da réplica local que estão desatualizados em 5 segundos, mesmo que a versão mais recente esteja disponível do outro lado do Oceano Pacífico

Os SGBDs eliminam automaticamente as versões mais antigas e, em alguns casos, permitem que você faça isso mediante solicitação. Por exemplo, o Postgres permite que os usuários façam VACUUM mediante solicitação, e também realiza periodicamente esta operação de forma automática. O Spanner executa um coletor de lixo para se livrar de snapshots com mais de uma hora.

Sempre que as fontes estiverem sujeitas a distorção

O segredo mais bem guardado da ciência da computação é que todas as APIs de temporização mentem. Na verdade, nossas máquinas não sabem a hora exata atual. Os computadores contêm cristais de quartzo que geram vibrações usadas para marcar o tempo. No entanto, eles não são precisos o suficiente e podem estar adiantados/atrasados ​​em relação à hora exata. A mudança pode chegar a 20 segundos por dia. Portanto, a hora em nossos computadores deve ser sincronizada periodicamente com a da rede.

Servidores NTP são usados ​​para sincronização, mas o próprio processo de sincronização está sujeito a atrasos na rede. Até mesmo a sincronização com um servidor NTP no mesmo data center leva algum tempo. É claro que trabalhar com um servidor NTP público pode levar a distorções ainda maiores.

Os relógios atômicos e seus equivalentes GPS são melhores para determinar a hora atual, mas são caros e exigem configurações complexas, por isso não podem ser instalados em todos os carros. Por causa disso, os data centers usam uma abordagem em camadas. Os relógios atômicos e/ou GPS mostram a hora exata, após a qual ela é transmitida para outras máquinas através de servidores secundários. Isso significa que cada máquina experimentará um certo deslocamento em relação à hora exata.

A situação é agravada pelo facto de as aplicações e bases de dados estarem frequentemente localizadas em máquinas diferentes (se não em centros de dados diferentes). Assim, o tempo será diferente não apenas nos nós do banco de dados distribuídos em máquinas diferentes. Também será diferente no servidor de aplicativos.

O Google TrueTime adota uma abordagem completamente diferente. A maioria das pessoas acredita que o progresso do Google nessa direção é explicado pela transição banal para relógios atômicos e GPS, mas isso é apenas parte do quadro geral. Veja como funciona o TrueTime:

  • TrueTime usa duas fontes diferentes: GPS e relógios atômicos. Esses relógios possuem modos de falha não correlacionados. [veja a página 5 para detalhes aqui - Aproximadamente. trad.), portanto, seu uso conjunto aumenta a confiabilidade.
  • TrueTime possui uma API incomum. Ele retorna o tempo como um intervalo com erro de medição e incerteza incorporados. O momento real está em algum lugar entre os limites superior e inferior do intervalo. O Spanner, o banco de dados distribuído do Google, simplesmente espera até que seja seguro dizer que a hora atual está fora do intervalo. Este método introduz alguma latência no sistema, especialmente se a incerteza nos mestres for alta, mas garante a correção mesmo em uma situação distribuída globalmente.

Mais desenvolvedores deveriam saber isso sobre bancos de dados
Os componentes do Spanner usam TrueTime, onde TT.now() retorna um intervalo, então o Spanner simplesmente dorme até o ponto em que pode ter certeza de que o tempo atual passou de um certo ponto

A precisão reduzida na determinação da hora atual significa um aumento na duração das operações do Spanner e uma diminuição no desempenho. É por isso que é importante manter a maior precisão possível, mesmo que seja impossível obter um relógio totalmente preciso.

Atraso tem muitos significados

Se você perguntar a uma dúzia de especialistas sobre o que é um atraso, provavelmente obterá respostas diferentes. No SGBD, a latência costuma ser chamada de "latência do banco de dados" e é diferente do que é percebido pelo cliente. O fato é que o cliente observa a soma do atraso da rede e do banco de dados. A capacidade de isolar o tipo de latência é crítica ao depurar problemas crescentes. Ao coletar e exibir métricas, tente sempre ficar de olho nos dois tipos.

Os requisitos de desempenho devem ser avaliados para uma transação específica

Às vezes, as características de desempenho de um SGBD e suas limitações são especificadas em termos de taxa de transferência e latência de gravação/leitura. Isso fornece uma visão geral dos principais parâmetros do sistema, mas ao avaliar o desempenho de um novo SGBD, uma abordagem muito mais abrangente é avaliar separadamente as operações críticas (para cada consulta e/ou transação). Exemplos:

  • Taxa de transferência e latência de gravação ao inserir uma nova linha na tabela X (com 50 milhões de linhas) com restrições especificadas e preenchimento de linha em tabelas relacionadas.
  • Atraso na exibição de amigos de amigos de determinado usuário quando a média de amigos é 500.
  • Latência na recuperação das 100 principais entradas do histórico de um usuário quando o usuário segue 500 outros usuários com X entradas por hora.

A avaliação e a experimentação podem incluir esses casos críticos até que você tenha certeza de que o banco de dados atende aos requisitos de desempenho. Uma regra prática semelhante também leva esse detalhamento em consideração ao coletar métricas de latência e determinar SLOs.

Esteja ciente da alta cardinalidade ao coletar métricas para cada operação. Use logs, coleta de eventos ou rastreamento distribuído para obter dados de depuração de alta potência. No artigo "Quer depurar a latência?» você pode se familiarizar com metodologias de depuração de atraso.

Transações aninhadas podem ser perigosas

Nem todo SGBD suporta transações aninhadas, mas quando o fazem, tais transações podem resultar em erros inesperados que nem sempre são fáceis de detectar (ou seja, deve ser óbvio que existe algum tipo de anomalia).

Você pode evitar o uso de transações aninhadas usando bibliotecas de cliente que podem detectá-las e ignorá-las. Se as transações aninhadas não puderem ser abandonadas, tome cuidado especial em sua implementação para evitar situações inesperadas em que as transações concluídas sejam acidentalmente abortadas devido a transações aninhadas.

Encapsular transações em diferentes camadas pode levar a transações aninhadas inesperadas e, do ponto de vista da legibilidade do código, pode dificultar a compreensão das intenções do autor. Dê uma olhada no seguinte programa:

with newTransaction():
   Accounts.create("609-543-222")
   with newTransaction():
       Accounts.create("775-988-322")
       throw Rollback();

Qual será a saída do código acima? Irá reverter ambas as transações ou apenas a interna? O que acontece se confiarmos em múltiplas camadas de bibliotecas que encapsulam a criação de transações para nós? Seremos capazes de identificar e melhorar esses casos?

Imagine uma camada de dados com múltiplas operações (por exemplo newAccount) já está implementado em suas próprias transações. O que acontece se você executá-los como parte de uma lógica de negócios de nível superior executada em sua própria transação? Qual seria o isolamento e a consistência neste caso?

function newAccount(id string) {
  with newTransaction():
      Accounts.create(id)
}

Em vez de procurar respostas para perguntas tão intermináveis, é melhor evitar transações aninhadas. Afinal, sua camada de dados pode realizar facilmente operações de alto nível sem criar suas próprias transações. Além disso, a própria lógica de negócios é capaz de iniciar uma transação, realizar operações nela, confirmar ou abortar uma transação.

function newAccount(id string) {
   Accounts.create(id)
}
// In main application:
with newTransaction():
   // Read some data from database for configuration.
   // Generate an ID from the ID service.
   Accounts.create(id)
   Uploads.create(id) // create upload queue for the user.

As transações não devem estar vinculadas ao estado do aplicativo

Às vezes é tentador usar o estado do aplicativo em transações para alterar determinados valores ou ajustar parâmetros de consulta. A nuance crítica a considerar é o escopo correto de aplicação. Os clientes geralmente reiniciam as transações quando há problemas de rede. Se a transação depender de um estado que está sendo alterado por algum outro processo, ela poderá escolher o valor errado dependendo da possibilidade de uma corrida de dados. As transações devem considerar o risco de condições de corrida de dados no aplicativo.

var seq int64
with newTransaction():
    newSeq := atomic.Increment(&seq)
    Entries.query(newSeq)
    // Other operations...

A transação acima aumentará o número de sequência cada vez que for executada, independentemente do resultado final. Se a confirmação falhar devido a problemas de rede, a solicitação será executada com um número de sequência diferente quando você tentar novamente.

Os planejadores de consultas podem dizer muito sobre um banco de dados

Os planejadores de consultas determinam como uma consulta será executada em um banco de dados. Eles também analisam as solicitações e as otimizam antes de enviá-las. Os planejadores só podem fornecer algumas estimativas possíveis com base nos sinais à sua disposição. Por exemplo, qual é o melhor método de pesquisa para a consulta a seguir?

SELECT * FROM articles where author = "rakyll" order by title;

Os resultados podem ser recuperados de duas maneiras:

  • Verificação completa da tabela: você pode examinar cada entrada na tabela e retornar artigos com o nome do autor correspondente e, em seguida, ordená-los.
  • Varredura de índice: você pode usar um índice para encontrar IDs correspondentes, obter essas linhas e ordená-las.

O trabalho do planejador de consultas é determinar qual estratégia é melhor. Vale a pena considerar que os planejadores de consultas têm recursos preditivos limitados. Isso pode levar a decisões erradas. DBAs ou desenvolvedores podem usá-los para diagnosticar e ajustar consultas com baixo desempenho. Novas versões do SGBD podem configurar planejadores de consultas, e o autodiagnóstico pode ajudar na atualização do banco de dados se a nova versão levar a problemas de desempenho. Logs de consultas lentas, relatórios de problemas de latência ou estatísticas de tempo de execução podem ajudar a identificar consultas que precisam de otimização.

Algumas métricas apresentadas pelo planejador de consultas podem estar sujeitas a ruído (especialmente ao estimar a latência ou o tempo de CPU). Uma boa adição aos agendadores são as ferramentas para rastrear e rastrear o caminho de execução. Eles permitem diagnosticar esses problemas (infelizmente, nem todos os SGBDs fornecem essas ferramentas).

A migração online é difícil, mas possível

Migração online, migração em tempo real ou migração em tempo real significa passar de um banco de dados para outro sem tempo de inatividade ou corrupção de dados. A migração ao vivo é mais fácil de realizar se a transição ocorrer dentro do mesmo SGBD/mecanismo. A situação fica mais complicada quando é necessário migrar para um novo SGBD com diferentes requisitos de desempenho e esquema.

Existem diferentes modelos de migração online. Aqui está um deles:

  • Habilite a entrada dupla em ambos os bancos de dados. O novo banco de dados nesta fase não possui todos os dados, mas aceita apenas os dados mais recentes. Depois de ter certeza disso, você pode passar para a próxima etapa.
  • Habilite a leitura de ambos os bancos de dados.
  • Configure o sistema para que a leitura e a gravação sejam executadas principalmente no novo banco de dados.
  • Pare de gravar no banco de dados antigo enquanto continua lendo os dados dele. Nesta fase, a nova base de dados ainda está desprovida de alguns dados. Eles devem ser copiados do banco de dados antigo.
  • O banco de dados antigo é somente leitura. Copie os dados ausentes do banco de dados antigo para o novo. Após a conclusão da migração, alterne os caminhos para o novo banco de dados, interrompa o antigo e exclua-o do sistema.

Para informações adicionais, recomendo entrar em contato статье, que detalha a estratégia de migração do Stripe com base neste modelo.

Um aumento significativo na base de dados implica um aumento na imprevisibilidade

O crescimento da base de dados leva a problemas imprevisíveis associados à sua escala. Quanto mais sabemos sobre a estrutura interna de um banco de dados, melhor podemos prever como ele será dimensionado. Porém, alguns momentos ainda são impossíveis de prever.
À medida que a base cresce, as suposições e expectativas anteriores em relação ao volume de dados e aos requisitos de largura de banda da rede podem ficar desatualizadas. É aí que surge a questão de grandes revisões de design, melhorias operacionais em grande escala, repensar implantações ou migração para outros SGBDs para evitar problemas potenciais.

Mas não pense que um excelente conhecimento da estrutura interna da base de dados existente é a única coisa necessária. Novas escalas trarão consigo novas incógnitas. Pontos problemáticos imprevisíveis, distribuição desigual de dados, problemas inesperados de largura de banda e hardware, tráfego cada vez maior e novos segmentos de rede forçarão você a repensar sua abordagem de banco de dados, modelo de dados, modelo de implantação e tamanho do banco de dados.

...

Na época em que comecei a pensar em publicar este artigo, já havia mais cinco itens na minha lista original. Então veio um grande número Novas ideias sobre o que mais pode ser coberto. Portanto, o artigo aborda os problemas menos óbvios que requerem atenção máxima. Porém, isso não significa que o tema esteja esgotado e não voltarei mais a ele em meus materiais futuros e não farei alterações no atual.

PS

Leia também em nosso blog:

Fonte: habr.com

Adicionar um comentário