Fundamentos de bancos de dados
(tontinton.com)- Começando com um armazenamento simples de chave-valor em Bash, mostra passo a passo por que bancos de dados reais precisam tratar durabilidade, atomicidade, isolamento e desempenho como problemas de projeto separados
fsync/fdatasync,flocke WAL são ferramentas básicas para proteger dados em cenários de falha e concorrência, mas quanto maior o nível de garantia, maior o custo de desempenho- Motores de armazenamento usam estruturas como B-tree e LSM tree para reduzir I/O de disco e custos de busca, e cada uma delas traz manutenções como vacuum e compaction
- Bancos de dados distribuídos ganham disponibilidade e escalabilidade horizontal, mas em troca assumem a complexidade de sistemas distribuídos, como teorema CAP, partições de rede, ajuste de consistência e resolução de conflitos
- Ao escolher ou implementar um banco de dados, é preciso ajustar à carga de trabalho as garantias ACID, o nível de isolamento, a estrutura de armazenamento, o método de replicação e os requisitos de consistência
Os problemas básicos de bancos de dados revelados pelo bashdb
bashdbé um armazenamento simples de chave-valor feito com duas funções Bashdb_setfaz append em um arquivo no formatochave,valordb_getlê o último valor combinandogrep,sedetail
- Embora seja simples para fins de aprendizado, essa implementação por si só já expõe os problemas que um banco de dados de produção precisa resolver
- Durability: se a máquina travar depois de
db_setter sucesso, dados ainda não flushed para o disco podem se perder - Atomicity: se houver falha durante a escrita, os dados podem ser gravados só parcialmente e ficar corrompidos
- Isolation: se leitura e escrita acessarem o mesmo item ao mesmo tempo, a leitura pode ver apenas parte dos dados
- Performance:
db_getfaz busca linha por linha no arquivo inteiro, então éO(n)
- Durability: se a máquina travar depois de
ACID e tentativas de melhorar o bashdb
- ACID é uma sigla que reúne propriedades que muitos bancos de dados tentam garantir
- Atomicity: se ocorrer uma falha durante a escrita, a transação inteira é cancelada ou revertida, sem deixar um estado de escrita parcial
- Consistency: transações inválidas não devem corromper o banco de dados
- Isolation: não deve haver race conditions em acessos simultâneos aos mesmos dados
- Durability: escritas bem-sucedidas devem permanecer mesmo após situações como falta de energia
- Nem toda transação de banco de dados precisa necessariamente garantir ACID; em alguns casos de uso, pode-se reduzir as garantias em troca de desempenho
-
Durabilidade e
fsync- A system call
writegrava buffers em um arquivo, mas isso não significa que eles foram imediatamente persistidos em armazenamento não volátil - O kernel pode armazenar os buffers como dirty pages no page cache e só depois fazer o flush para o disco
- Dispositivos de disco e sistemas RAID também podem ter seu próprio write cache
fsyncefdatasyncsão system calls para fazer flush de dirty pages para armazenamento permanentefdatasyncfaz flush do raw buffer passado porwritefsyncfaz flush não só dos dados, mas também de metadados do arquivo, comomtime- Adicionar
sync -d databaseapósdb_setpode aumentar a durabilidade com um comportamento próximo defdatasync, mas normalmente sync é mais lento que a própria escrita, então o desempenho piora - O sucesso de
fsync()significa que “todas as writes desde o último fsync chegaram ao disco”, e não apenas “as writes desde o último fsync bem-sucedido” - O PostgreSQL enfrentou esse problema em 2018 e mudou seu comportamento para entrar em panic em caso de falha de fsync, em vez de simplesmente tentar novamente
- O caso ficou conhecido como fsyncgate, com link para o artigo sobre falhas de fsync
- O MongoDB, por padrão, faz sync das escritas a cada 100ms, então não é 100% durável
- A system call
-
Isolamento e
flock- No
bashdb, a forma mais simples de isolamento entre múltiplos processos é aplicar lock no arquivo de armazenamento antes de ler ou escrever - O
flockno Linux bloqueia arquivos, e com a flag-sé possível usar shared lock para que vários readers leiam ao mesmo tempo - O
bashdbmelhorado usa exclusive lock para escritas e shared lock para leituras - A desvantagem é que, a cada escrita, ele bloqueia o banco de dados inteiro
- Garantir atomicidade de forma simples só com Bash é difícil; há a possibilidade de usar
mv -Tourename, mas isso não foi concluído - O
bashdbainda não resolve o problema de consultaO(n)
- No
O papel do motor de armazenamento e seus gargalos
- O motor de armazenamento fornece uma abstração para ler e gravar dados em armazenamento persistente, e seu objetivo principal é alta vazão e baixa latência
- A maior limitação vem das diferenças de velocidade do próprio disco
- Na tabela de latências de exemplo, uma referência à L1 cache é cerca de
0.5ns, uma leitura aleatória de 4KB em SSD é150,000ns, e um disk seek é10,000,000ns - Se uma referência à L1 cache fosse equivalente a um batimento cardíaco de cerca de 0,5 segundo, uma leitura sequencial de 1MB em SSD corresponderia a cerca de 12 dias, e uma leitura sequencial de 1MB em disco a cerca de 8 meses
- Na tabela de latências de exemplo, uma referência à L1 cache é cerca de
- Por isso, o projeto de motores de armazenamento evoluiu na direção de reduzir ao máximo o I/O de disco e os disk seeks
- Elementos comuns no projeto de um motor de armazenamento incluem
- a estrutura de dados básica usada para armazenar itens no disco
- transações ACID
- cache para reduzir leituras em disco
- uma camada de API, como SQL, documento ou grafo
- As estruturas de dados de motores de armazenamento podem ser divididas, em linhas gerais, entre estruturas mutáveis e imutáveis
- Estruturas mutáveis podem sobrescrever depois os dados já gravados no arquivo
- Estruturas imutáveis apenas relêem dados já gravados no arquivo
B-tree mutável
- Para manter bom desempenho mesmo com o crescimento dos dados, é preciso encontrar itens em tempo no máximo logarítmico, e não com busca linear como no
bashdb - Uma BST permite consultas em
O(log n), mas, se os nós estiverem muito distantes uns dos outros no disco, a navegação pode exigir muitos disk seeks - A B-tree é uma generalização da BST em que um nó pode ter mais de dois filhos, aproveitando spatial locality
- Normalmente lê-se uma page de 4KB ou 8KB do disco e depois vários nós dentro dela são comparados sequencialmente na memória e no CPU cache
- Como acessos à memória e ao CPU cache são várias ordens de grandeza mais rápidos que o disco, é importante aproveitar o máximo possível os bytes lidos do disco
- O acesso sequencial à memória pode ser muito poderoso graças a SIMD, instruction pipelining e prefetching
- A B+ tree armazena valores apenas nos leaf nodes e deixa os demais nós guardarem só chaves, permitindo comparar mais chaves em uma única page de disco
-
Recuperação de espaço e vacuum
- A B-tree precisa recuperar espaço vazio gerado por fragmentação de dados para otimizar o uso de espaço
- Ao atualizar com um valor maior, isso pode sobrescrever dados do próximo nó, então o item é movido para outro lugar e fica um buraco na page original
- Ao atualizar com um valor menor, fica um buraco no final
- Exclusões criam buracos no lugar em que o valor apagado estava
- Esse processo de recuperar espaço e reescrever pages pode ser chamado de vacuum, compaction, page defragmentation ou maintenance
- Em geral, isso é feito em background para evitar picos de latência nas requisições dos usuários
- O PostgreSQL permite configurar o auto vacuum daemon
- A B-tree é comumente usada como estrutura base de índices, como no índice padrão do PostgreSQL, e o DynamoDB já foi chamado em tom de brincadeira de “distributed B-tree”
LSM tree imutável
- A LSM tree é uma estrutura de dados append-only que surgiu da premissa de que o disk seek é caro
- Se os dados forem adicionados apenas ao fim do arquivo, a cabeça do disco precisa se mover menos até a próxima posição de escrita, o que favorece workloads com muitas gravações
Log Structured Merge tree, ou simplesmente LSM tree, é usada em mecanismos de armazenamento de bancos de dados modernos como RocksDB, Cassandra e ScyllaDB- O funcionamento básico é o seguinte
- As gravações são bufferizadas em uma estrutura de dados ordenável na memória
- Exemplos são
AVL tree,Red Black treeeSkip List - Ao atingir uma certa capacidade, os dados são descarregados em um arquivo ordenado chamado
Sorted String Table, ou SSTable
- A SSTable armazena dados ordenados, o que permite reduzir I/O de disco com binary search e sparse index
- Para garantir durabilidade, as operações escritas em memória também são registradas no Write-Ahead Log, ou WAL
- Ao iniciar o programa, o WAL é lido para restaurar o estado anterior ao encerramento ou crash
- Exclusões também são adicionadas por append como uma gravação comum, armazenando um tombstone no lugar do valor
- O tombstone é removido durante o processo de compaction
-
Leitura e compaction na LSM tree
- A leitura em uma LSM tree primeiro busca na estrutura em memória e, se não encontrar, percorre as SSTables em disco da mais recente para a mais antiga
- Quanto mais gravações houver, maior será o número de SSTables a verificar
- Mesmo que cada arquivo esteja ordenado, varrer muitos arquivos pequenos pode ser mais lento do que consultar um único arquivo grande
- A expressão de comparação é
log(num_files * table_size) < num_files * log(table_size) - Compaction é uma tarefa em background que combina várias SSTables pequenas em uma SSTable grande e remove tombstones
- O RocksDB implementa Leveled Compaction
- As SSTables recém-descarregadas ficam no level 0
- Quando a quantidade configurada de arquivos se acumula em um level, após a compaction o novo arquivo é promovido ao próximo level
- A remoção de tombstones exige cuidado
- Pode surgir o problema de data resurrection, em que itens excluídos reaparecem durante a compaction com arquivos mais antigos
- O RocksDB mantém os tombstones até a compaction que os promove ao último level
- Um exemplo real em Rust pode ser visto no código de LSM tree do dbeel
-
Bloom filter
- Bloom filter é uma estrutura de dados probabilística de conjunto que permite verificar com eficiência se um item não está em um conjunto
- O resultado da consulta tem dois casos
false: o item definitivamente não está no conjuntotrue: o item pode estar no conjunto
- O Bloom filter mapeia os resultados de várias hash functions para posições de bits em um bitmap e os define como 1
- A complexidade espacial é apresentada como
O(log n), em contraste com oO(n)de um set comum - É possível ajustar a “probabilidade de ter certeza de que não está presente” alocando mais memória para o bitmap e aumentando o número de hash functions, e há também uma calculadora
- A LSM tree armazena um Bloom filter para cada SSTable, permitindo pular a busca em SSTables onde foi confirmado que uma key não existe
WAL e garantias de transação
- WAL é uma forma de registrar todas as operações de transação em um arquivo especial para sobreviver a crashes repentinos
- Quando o processo do banco de dados é iniciado, ele lê o arquivo WAL e reconstrói o estado dos dados
- Transações sem commit log são ignoradas, obtendo assim atomicidade
- Se os dados de uma solicitação de escrita forem registrados e flushados no WAL antes de responder ao usuário, eles certamente poderão ser lidos na inicialização, garantindo durabilidade
- O WAL pode ser visto como uma forma de event sourcing para eventos de transação
Níveis de isolamento e controle de concorrência
- As formas de alcançar isolamento se dividem em três grandes categorias
- Lock pessimista: bloqueia o acesso a dados que estão sendo escritos no momento
- Lock otimista: modifica uma cópia dos dados e só faz commit se o original não tiver mudado durante a transação; caso contrário, faz retry
- MVCC: em vez de sobrescrever os dados, cria uma nova version para que cada usuário veja um snapshot de um ponto específico no tempo
- Nem toda aplicação precisa de isolamento completo, isto é, serializable isolation
- O ANSI/ISO SQL 92 classifica em três tipos os resultados que podem ocorrer quando outra transação altera os mesmos dados durante uma transação
- Dirty read: lê uma atualização de outra transação que ainda não foi commitada
- Non-repeatable read: entre duas leituras da mesma row, outra transação faz commit e o valor muda
- Phantom read: entre duas leituras de um conjunto de rows com a mesma condição, rows são adicionadas ou removidas
- Os níveis de isolamento ANSI/SQL 92, do mais alto para o mais baixo, são os seguintes
- Serializable: lê apenas dados commitados e evita phantom read, inclusive em escritas de múltiplas rows baseadas em intervalo
- Repeatable reads: phantom read é permitido
- Read committed: non-repeatable read é permitido
- Read uncommitted: dirty read é permitido
- Níveis de isolamento mais altos normalmente vêm com perda de desempenho
- Os níveis de isolamento ANSI/SQL 92 são criticados por não serem completos
- Muitas implementações de MVCC fornecem snapshot isolation, e não serializable isolation
- Como algoritmo rápido de MVCC serializable, recomenda-se o HyPer
Por que sistemas distribuídos são necessários e CAP
- Sistemas distribuídos adicionam muita complexidade, então devem ser evitados quando uma solução não distribuída for suficiente
- Há dois motivos comuns para distribuir dados entre várias máquinas
- Disponibilidade (Availability): mesmo que a máquina do banco de dados sofra crash ou perca a conexão com o usuário, as requisições podem ser enviadas a outra máquina
- Escalabilidade horizontal (Horizontal Scaling): em vez de vertical scaling para uma única máquina maior, várias máquinas conectadas em rede operam como se fossem uma só
- Sistemas distribuídos introduzem complexidade operacional e o problema de network partition
- O teorema CAP diz que um sistema só pode garantir duas das três propriedades a seguir
- Consistency: as leituras recebem a escrita mais recente
- Availability: toda requisição é bem-sucedida independentemente de falhas
- Partition Tolerance: o sistema continua funcionando mesmo com perda ou atraso de mensagens entre nós
- Um banco de dados em máquina única não tem network partition e é consistente, mas quando a máquina falha, novas requisições falham, violando availability
- Se duas máquinas com CPU, memória e disco separados estiverem conectadas por cabo, em caso de falha surge uma escolha
- Cancelar as requisições sacrifica availability e preserva consistency
- Continuar processando requisições apenas na máquina que segue funcionando sacrifica consistency e preserva availability
- Sistemas que sacrificam consistency e se ajustam depois são chamados de eventually consistent
- Como é preciso reunir dados espalhados pelo cluster, network partitions também dificultam
JOINeficiente, e o ecossistema NoSQL recomenda denormalization para aliviar isso
Replicação e o caso do Amazon Dynamo
- O artigo original do Dynamo, da Amazon, é apresentado como um caso em que a disponibilidade era considerada mais importante do que a consistência no carrinho de compras da amazon.com
- Se o usuário vir dois itens iguais no carrinho, basta remover um deles
- A avaliação foi de que isso é melhor do que uma situação em que a compra em si se torna impossível
- Para obter disponibilidade, não basta apenas vários nós dividirem os dados; cada item precisa ter pelo menos uma cópia adicional
- Os nós que armazenam cópias de um item são replicas, e o processo de cópia é a replication
- Aumentar o número de replicas eleva a disponibilidade, mas exige mais recursos para armazenar as cópias
- As cópias dos dados também podem não ser armazenadas integralmente, mas divididas com erasure coding e distribuídas entre vários nós; as características de latência relacionadas estão no artigo sobre erasure coding
Consistent Hashing e posicionamento de dados
- Quando há vários nós, é necessário um método de balanceamento de carga ou particionamento de dados para decidir qual nó processará uma requisição de armazenamento
- Um método simples é aplicar hash à primary key e então usar modulo pelo número de nós
- Quando um nó é adicionado ou removido,
len(nodes)muda e a mesma key passa a apontar para outro nó - Nesse caso, é preciso migrar quase todos os itens, o que tem alto custo
- Quando um nó é adicionado ou removido,
- Consistent Hashing posiciona os nós em um ring, em vez de um array, para reduzir a quantidade de itens que precisam ser movidos quando nós são adicionados ou removidos
- É usado em bancos de dados como Dynamo e Cassandra
- No consistent hashing, o hash do nome do nó é posicionado no ring, e o nó encontrado após o hash da key da requisição se torna o proprietário
- A seleção de replicas pode ser feita percorrendo o ring no sentido anti-horário e armazenando cópias nos nós seguintes
- Se o nó proprietário cair, um nó replica processa a requisição para manter a disponibilidade
- Esse método é chamado de Leaderless Replication e é usado em bancos de dados no estilo Dynamo, como o Cassandra
- Ao adicionar um nó, o número de keys que precisam ser movidas é, em média,
num_keys / num_nodes - Virtual node significa posicionar o mesmo nó físico várias vezes no ring para reduzir a chance de alguns nós ficarem com muito mais itens do que outros
- Um exemplo é adicionar um índice como sufixo ao nome do nó, como em
"half-0"e"half-1"
- Um exemplo é adicionar um índice como sufixo ao nome do nó, como em
- Há outras formas de escolher leader node e replica node, como leader election, mas isso não será tratado aqui
Leaderless Replication e ajuste de consistência
- Uma configuração leaderless obtém alta disponibilidade em troca de sacrificar consistência
- Se o nó proprietário estiver down no momento de uma requisição de write, a escrita vai para as replicas, e depois que o nó proprietário voltar, uma requisição de read pode ler dados antigos
- Se uma determinada requisição precisar de consistência, a requisição de read pode ser enviada em paralelo para várias replicas e para o nó proprietário, e o cliente escolhe os dados mais recentes
- Requisições de write normalmente são enviadas em paralelo para todas as replicas, mas espera-se apenas o acknowledgement de parte dos nós
- Para ajustar a consistência no nível da requisição, verifica-se
R + W > N/2 + 1N: número de nós que têm cópias dos dadosW: número de nós que precisam dar acknowledgement para o write ter sucessoR: número de nós que precisam responder para o read ter sucesso
- Quando
WouRéN/2 + 1, isso é chamado de quorum, ou maioria de nós -
Resolução de conflitos
- O processo de escolher o write mais recente é a Conflict Resolution
- Comparar apenas timestamps não é algo confiável em sistemas distribuídos
- Cada máquina tem seu próprio hardware clock, e o clock não é perfeitamente preciso, então ocorre drift
- O NTP recebe a hora de uma fonte mais precisa, mas como a própria requisição passa pela rede, não é possível saber com exatidão o tempo até a resposta
- O Cassandra usa timestamp, e a documentação relacionada está em Cassandra data versioning
- O Google Spanner alcançou garantias de consistência baseadas em clock com hardware especial de tempo de alta precisão e uma API que expõe uma faixa de incerteza do timestamp; o artigo relacionado é o Spanner paper
- Sistemas como o Dynamo reduzem parte dos conflitos com Version Vectors
- Cada version de um item recebe um par
(node, counter)para identificar relações causais entre versions - É possível identificar a version que certamente é mais recente e remover alguns valores antigos
- Como material mais detalhado, há Dotted Version Vectors
- Também é possível, como no Riak KV, retornar todos os valores em conflito para a aplicação, para que ela resolva com base no conhecimento que tem sobre os dados
- Em sistemas eventualmente consistentes, várias técnicas para reduzir conflitos normalmente são agrupadas sob o termo Anti Entropy
Técnicas de Anti Entropy
-
Read Repair
- Depois que o cliente escolhe o valor mais recente entre os resultados de read de vários nós, ele o envia novamente aos nós que ainda não o armazenam para fazer o repair
-
Hinted Handoff
- Se uma requisição de write não conseguir chegar ao nó de destino, ela é armazenada como hint em outro nó
- Quando o nó de destino volta a ficar available, o hint armazenado é entregue
- Em quorum write, esse método também é chamado de
Sloppy Quorume aumenta ainda mais a disponibilidade de requisições com quorum
-
Merkle Trees
- O read repair corrige apenas os dados consultados, então uma grande quantidade de dados pode permanecer inconsistente por muito tempo
- Fazer nós se sincronizarem para encontrar toda a diferença é caro, com custo
O(n), quando há muitos dados - A Merkle tree é uma estrutura hierárquica em que os hashes de faixas de dados ficam nas folhas, e cada pai armazena um hash combinado dos hashes dos filhos
- Se o hash da raiz for igual, os dados dos dois nós são iguais; se for diferente, compara-se recursivamente os hashes inferiores para encontrar os dados divergentes, tornando a sincronização mais rápida, em
O(log n)
-
Gossip Dissemination
- É uma forma simples e confiável de propagar eventos por todo o cluster
- Um nó envia uma mensagem para uma quantidade configurada de nós aleatórios, ou seja, o fanout, e os nós que a recebem a reenviam para
Nnós aleatórios - Quando um mesmo gossip message é visto o número configurado de vezes, ele deixa de ser broadcast
- Há um simulator para perceber a convergência dos dados
- Gossip messages normalmente são transmitidas via UDP
Áreas que podem ser exploradas mais a fundo
- Há muitos outros tópicos em bancos de dados além do que foi tratado aqui
- Ao escolher ou implementar um banco de dados, é preciso analisar junto como storage engine, ACID, níveis de isolamento, replicação distribuída e formas de resolução de conflitos se alinham aos requisitos reais
1 comentários
Comentários do Hacker News
Há um bug no método
compact: os tombstones só devem ser ignorados ao compactar o nível final, ou seja, o maior nível, e não removidos entre todos os níveisCaso contrário, os tombstones dos níveis superiores desaparecem durante a compactação, fazendo com que entradas que estavam em níveis inferiores voltem a aparecer
Em bancos de dados baseados em LSM, é uma característica que registros de exclusão/tombstones permaneçam por muito tempo, e alguns bancos de dados, como o RocksDB, incluem otimizações para tentar evitar isso
Conheço os recursos de exclusão por intervalo, mas não me lembro de ter lido muito sobre exclusão de chave única
Muita gente aprende bancos de dados estudando SQL, mas eu recomendaria aprender assistindo a aulas assim e entendendo árvores B
A maior parte dos prós e contras de um SGBD relacional fica clara quando se entende a árvore B e como ela afeta inserção de chaves, consultas e ordenação
Muita gente tenta acelerar o banco adicionando índices, mas no fim isso é só colocar outra árvore em cima da árvore, o que acaba escondendo o problema fundamental
Alguns problemas se encaixam bem em árvores B, mas muitos não
SQL é apenas uma interface de consulta para um sistema remoto de árvore B
Árvores B não são a única estratégia de indexação, e também é bem conhecido que índices são um mecanismo para melhorar desempenho de leitura em troca de sacrificar desempenho de escrita
Em geral, bancos de dados lidam com muito mais leituras do que escritas
Fico curioso sobre qual é exatamente o problema que “colocar outra árvore em cima da árvore” estaria escondendo, e como isso seria resolvido sem mexer nos índices
Para tabelas de tamanho razoável, índices são praticamente essenciais
É preciso aprender sobre árvores B e índices hash, a camada de E/S, o modelo de processos e coisas do tipo
Hoje em dia também vale a pena aprender as estratégias gerais dos bancos de dados colunares: materialização tardia de tuplas, execução preguiçosa, varredura linear e busca binária, pipeline de instruções e afins
Quando se ganha familiaridade com essas coisas, percebe-se que, no mundo real, às vezes um arquivo plano simples ou um banco embarcado como RocksDB já é suficiente, em vez de um SGBD completo
E, claro, também pode haver índices de cobertura
Quero fazer o contraponto ao conselho de “evite sistemas distribuídos se uma solução não distribuída for suficiente”
Todo sistema de produção minimamente não trivial é um sistema distribuído
No mínimo, se o banco de dados for um conjunto replicado, ele já é um sistema distribuído, então não aprender sistemas distribuídos é assumir um risco
Vale a pena ver https://jepsen.io/ e https://raft.github.io/
Isso não significa que seja aceitável introduzir isso em todo lugar, porque isso aumenta muito a complexidade além do necessário
Dizer isso dessa forma não rebate o conselho de evitar complexidade desnecessária. A questão central não é se algo é distribuído no sentido técnico, mas se isso é realmente necessário
Aprender sistemas distribuídos e usá-los são coisas diferentes
O importante é ter a disciplina de, mesmo depois de aprender, aplicá-los só onde fizer sentido
Hoje em dia, muitas vezes se gasta um enorme esforço para migrar sistemas simples e que funcionam bem para modelos distribuídos mais fortes, como se isso tivesse custo quase zero
Mas, olhando o problema e a escala que se queria resolver, fica claro que um único Postgres e um monólito já bastariam
Parece que o conselho do texto original aponta justamente nessa direção
Ou pelo menos não precisa ser
Eu ainda aconselharia escolher a solução simples
Mesmo em “armazenamentos triviais e simples”, muitos sistemas ainda falham em armazenar, fazer backup e restaurar corretamente o estado persistente
Tentar restaurar o estado de um armazenamento distribuído numa situação de recuperação de desastre é ainda mais difícil
Dá para adotar uma solução distribuída depois de primeiro ter uma solução de backup que funcione
Uma configuração com master e réplicas somente de leitura também não é o que as pessoas normalmente querem dizer com “distribuído”, porque a escrita não é distribuída
Na prática, distribuído geralmente significa que os dados são shardados, e essa é justamente a parte que se quer evitar a todo custo se não houver necessidade real
Foi uma leitura divertida por fazer um bom panorama de vários conceitos envolvidos na criação de bancos de dados
Vai de SIMD, para extrair desempenho de uma única máquina, até algoritmos de consenso
Já que estamos falando de bancos de dados, confiabilidade e sistemas distribuídos, também vale ler sobre métodos formais que podem ser aplicados a esse tipo de situação e à implementação interna de bancos de dados
Há um artigo interessante sobre a equipe do S3 modelando com TLA+
[0] Use of Formal Methods at Amazon Web Services
https://lamport.azurewebsites.net/tla/formal-methods-amazon....
[1] How Amazon Web Services uses formal methods
https://www.amazon.science/publications/how-amazon-web-servi...
Há consistência de banco de dados e consistência de aplicação
Por exemplo, é possível alcançar atomicidade, isolamento e durabilidade no nível de uma tabela, mas falhar em escritas que atravessam várias tabelas
A consistência passa a importar quando se começa a lidar com transações que atualizam várias tabelas ao mesmo tempo
Todas as tabelas precisam ser atualizadas ao mesmo tempo, ou então nenhuma deve ser atualizada
Uma arquitetura com “API de documentos como a do MongoDB, replicação leaderless como a do Cassandra e arquitetura de uma thread por núcleo como a do ScyllaDB” é muito legal
E, além disso, tudo foi feito em Rust
A etapa de “os livros despertaram minha curiosidade, então eu mesmo criei um pequeno banco de dados” parece ser algo por que muitos desenvolvedores passam pelo menos uma vez
Eu não tentaria impedir isso. Quando você faz por conta própria, aprende muito sobre o que não funciona
Se puder dedicar tempo a isso, é uma lição extremamente valiosa
Ter criado um banco de dados por conta própria foi o que mais aumentou meu respeito pelas soluções existentes
A parte difícil não é escrever e ler bytes no disco rapidamente
O realmente difícil é fazer com que funcione de forma estável por anos, dando suporte a casos de uso que você nunca imaginou
Que eficiência seria possível obter se projetássemos um DBMS especializado em domínio partindo do pressuposto de que casos de uso fora do domínio podem ser proibidos e ignorados?
Por exemplo, hoje usamos bancos de dados de propósito geral até para conjuntos de dados que são essencialmente append-only
E se houvesse um banco de dados sem qualquer conceito de atualização ou exclusão de linhas existentes, com apenas inserções e exclusão da tabela/conjunto de dados inteiro?
Um banco desses não precisaria implementar transações MVCC? Cada tabela poderia ser em si o write-ahead log, evitando um write-ahead log separado? Daria para armazenar de forma mais eficiente? A indexação poderia ter atomicidade por chunk em vez de atomicidade no nível da tabela inteira, reduzindo bloqueios?
A versão de atomicidade em Bash não poderia ser “simplesmente” alcançada copiando o arquivo para um arquivo temporário, modificando-o e depois usando
sync; mv; sync?grepreversoJá que estaria copiando mesmo, talvez desse para garantir ordenação também, mas fazer isso só com “bash” e utilitários básicos não parece muito fazer sentido
Para esse tipo de uso existe o CDB do DJB, isto é, cdbget, cdbmake etc.:
https://cr.yp.to/cdb.html
Ótimo texto
O livro Database Internals parece muito bom; existem mais livros parecidos que mergulhem fundo na implementação interna?
https://www.youtube.com/c/cmudatabasegroup
Há cursos introdutórios e avançados online, além de apresentações e aulas sobre produtos da indústria
É muito útil
Como material com uma perspectiva mais teórica de ciência da computação de nível mais alto, e menos focado em implementação física, o livro “Alice”, isto é, “Foundations of Databases”, é excelente
É muito denso e matemático, mas cobre álgebra relacional e Datalog, além da conversão de Datalog para álgebra relacional
A edição impressa está difícil de encontrar hoje em dia, e o exemplar usado que comprei chegou com a encadernação destruída e páginas se soltando, mas o livro inteiro está online: http://webdam.inria.fr/Alice/
https://dsf.berkeley.edu/papers/fntdb07-architecture.pdf
Só que o Database Internals é mais moderno
Gosto que o texto não mistifique “banco de dados” e comece mostrando uma implementação trivial com um one-liner em Bash
É uma ótima introdução