
O log de transações é fundamental para entender o Delta Lake porque ele é o fio condutor que atravessa muitos de seus recursos mais importantes, incluindo transações ACID, tratamento de metadados escalonável, viagem no tempo e muito mais. Neste artigo, exploraremos o que é o log de transações do Delta Lake, como ele funciona em nível de arquivo e como ele oferece uma solução elegante para o problema de múltiplas leituras e gravações concorrentes.
O log de transações do Delta Lake (também conhecido como DeltaLog) é um registro ordenado de cada transação que já foi realizada em uma tabela Delta Lake desde sua criação.
O Delta Lake é construído sobre o Apache Spark™ para permitir que múltiplos leitores e escritores de uma determinada tabela trabalhem nela ao mesmo tempo. Para mostrar aos usuários visualizações corretas dos dados o tempo todo, o log de transações do Delta Lake serve como uma fonte única de verdade - o repositório central que rastreia todas as alterações que os usuários fazem na tabela.
Quando um usuário lê uma tabela Delta Lake pela primeira vez ou executa uma nova consulta em uma tabela aberta que foi modificada desde a última vez que foi lida, o Spark verifica o log de transações para ver quais novas transações foram postadas na tabela e, em seguida, atualiza a tabela do usuário final com essas novas alterações. Isso garante que a versão de uma tabela do usuário esteja sempre sincronizada com o registro mestre a partir da consulta mais recente e que os usuários não possam fazer alterações divergentes e conflitantes em uma tabela.
Uma das quatro propriedades das transações ACID, a atomicidade, garante que as operações (como um INSERT ou UPDATE) realizadas em seu data lake sejam concluídas totalmente ou não sejam concluídas de forma alguma. Sem essa propriedade, é muito fácil que uma falha de hardware ou um bug de software cause a gravação parcial de dados em uma tabela, resultando em dados confusos ou corrompidos.
O log de transações é o mecanismo pelo qual o Delta Lake oferece a garantia de atomicidade. Para todos os efeitos, se não estiver registrado no log de transações, nunca aconteceu. Ao registrar apenas transações que são executadas de forma completa e integral, e usando esse registro como a fonte única de verdade, o log de transações do Delta Lake permite que os usuários raciocinem sobre seus dados e tenham tranquilidade quanto à sua confiabilidade fundamental, em escala de petabytes.
Sempre que um usuário executa uma operação para modificar uma tabela (como INSERT, UPDATE ou DELETE), o Delta Lake divide essa operação em uma série de etapas discretas compostas por uma ou mais das ações abaixo.
Essas ações são então registradas no log de transações como unidades ordenadas e atômicas conhecidas como commits.
Por exemplo, suponha que um usuário crie uma transação para adicionar uma nova coluna a uma tabela e adicionar mais dados a ela. O Delta Lake dividiria essa transação em suas partes componentes e, uma vez que a transação fosse concluída, as adicionaria ao log de transações como os seguintes commits:
Quando um usuário cria uma tabela Delta Lake, o log de transações dessa tabela é criado automaticamente no subdiretório _delta_log. À medida que ele ou ela faz alterações nessa tabela, essas alterações são registradas como commits ordenados e atômicos no log de transações. Cada commit é gravado como um arquivo JSON, começando com 000000.json. Alterações adicionais na tabela geram arquivos JSON subsequentes em ordem numérica ascendente, de modo que o próximo commit é gravado como 000001.json, o seguinte como 000002.json, e assim por diante.
Assim, como exemplo, talvez adicionemos registros adicionais à nossa tabela a partir dos arquivos de dados 1.parquet e 2.parquet. Essa transação seria automaticamente adicionada ao log de transações, salva em disco como commit 000000.json. Então, talvez mudemos de ideia e decidamos remover esses arquivos e adicionar um novo arquivo em vez disso (3.parquet). Essas ações seriam registradas como o próximo commit no log de transações, como 000001.json, como mostrado abaixo.
Mesmo que 1.parquet e 2.parquet não façam mais parte da nossa tabela Delta Lake, sua adição e remoção ainda são registradas no log de transações porque essas operações foram realizadas em nossa tabela - apesar do fato de que elas acabaram se cancelando mutuamente. O Delta Lake ainda retém commits atômicos como esses para garantir que, caso precisemos auditar nossa tabela ou usar o "time travel" para ver como nossa tabela se parecia em um determinado ponto no tempo, possamos fazê-lo com precisão.
Além disso, o Spark não remove os arquivos do disco de forma proativa, mesmo que tenhamos removido os arquivos de dados subjacentes de nossa tabela. Os usuários podem excluir os arquivos que não são mais necessários usando VACUUM.
Depois de fazermos vários commits no log de transações, o Delta Lake salva um arquivo de checkpoint em formato Parquet no mesmo subdiretório _delta_log. O Delta Lake gera automaticamente checkpoints conforme necessário para manter um bom desempenho de leitura.
Esses arquivos de checkpoint salvam todo o estado da tabela em um determinado ponto no tempo - em formato Parquet nativo que é rápido e fácil para o Spark ler. Em outras palavras, eles oferecem ao leitor do Spark uma espécie de "atalho" para reproduzir completamente o estado de uma tabela, o que permite ao Spark evitar o reprocessamento de milhares de arquivos JSON pequenos e ineficientes.
Para se atualizar, o Spark pode executar uma operação listFrom para visualizar todos os arquivos no log de transações, pular rapidamente para o arquivo de checkpoint mais recente e processar apenas os commits JSON feitos desde que o arquivo de checkpoint mais recente foi salvo.
Para demonstrar como isso funciona, imagine que criamos commits até 000007.json, como mostrado no diagrama abaixo. O Spark está atualizado até este commit, tendo automaticamente armazenado em cache a versão mais recente da tabela na memória. Enquanto isso, no entanto, vários outros escritores (talvez seus colegas de equipe excessivamente ansiosos) escreveram novos dados na tabela, adicionando commits até 0000012.json.
Para incorporar essas novas transações e atualizar o estado de nossa tabela, o Spark executará uma operação listFrom version 7 para ver as novas alterações na tabela.
Em vez de processar todos os arquivos JSON intermediários, o Spark pode pular para o arquivo de checkpoint mais recente, pois ele contém todo o estado da tabela no commit nº 10. Agora, o Spark só precisa realizar o processamento incremental de 0000011.json e 0000012.json para ter o estado atual da tabela. O Spark então armazena em cache a versão 12 da tabela na memória. Ao seguir este fluxo de trabalho, o Delta Lake consegue usar o Spark para manter o estado de uma tabela sempre atualizado de forma eficiente.
Agora que entendemos como o log de transações do Delta Lake funciona em um nível geral, vamos falar sobre concorrência. Até agora, nossos exemplos cobriram principalmente cenários em que os usuários confirmam transações linearmente, ou pelo menos sem conflitos. Mas o que acontece quando o Delta Lake está lidando com múltiplas leituras e gravações concorrentes?
A resposta é simples. Como o Delta Lake é alimentado pelo Apache Spark, não é apenas possível que vários usuários modifiquem uma tabela ao mesmo tempo - é esperado. Para lidar com essas situações, o Delta Lake emprega controle de concorrência otimista.
O controle de concorrência otimista é um método para lidar com transações concorrentes que assume que as transações (alterações) feitas em uma tabela por diferentes usuários podem ser concluídas sem conflitar umas com as outras. É incrivelmente rápido porque, ao lidar com petabytes de dados, há uma alta probabilidade de que os usuários estejam trabalhando em partes diferentes dos dados, permitindo que eles concluam transações não conflitantes simultaneamente.
Por exemplo, imagine que você e eu estamos montando um quebra-cabeça juntos. Desde que ambos estejamos trabalhando em partes diferentes dele - você nos cantos e eu nas bordas, por exemplo - não há razão para não podermos trabalhar em nossa parte do quebra-cabeça maior ao mesmo tempo e terminar o quebra-cabeça duas vezes mais rápido. É apenas quando precisamos das mesmas peças, ao mesmo tempo, que surge um conflito. Isso é controle de concorrência otimista.
Claro, mesmo com o controle de concorrência otimista, às vezes os usuários tentam modificar as mesmas partes dos dados ao mesmo tempo. Felizmente, o Delta Lake tem um protocolo para isso.
Para oferecer transações ACID, o Delta Lake tem um protocolo para descobrir como os commits devem ser ordenados (conhecido como o conceito de serialização em bancos de dados) e determinar o que fazer no caso de dois ou mais commits serem feitos ao mesmo tempo. O Delta Lake lida com esses casos implementando uma regra de exclusão mútua, e então tenta resolver qualquer conflito de forma otimista. Este protocolo permite que o Delta Lake cumpra o princípio ACID de isolamento, que garante que o estado resultante da tabela após múltiplas gravações concorrentes seja o mesmo como se essas gravações tivessem ocorrido serialmente, isoladamente umas das outras.
Em geral, o processo prossegue da seguinte forma:
Para ver como tudo isso acontece em tempo real, vamos dar uma olhada no diagrama abaixo para ver como o Delta Lake gerencia conflitos quando eles surgem. Imagine que dois usuários leiam da mesma tabela, e então cada um tente adicionar alguns dados a ela.
000001.json.000001.json com sucesso. O commit do Usuário 1 é aceito, enquanto o do Usuário 2 é rejeitado.000002.json.Na grande maioria dos casos, essa reconciliação acontece silenciosamente, de forma integrada e bem-sucedida. No entanto, no caso de haver um problema irreconciliável que o Delta Lake não consiga resolver otimisticamente (por exemplo, se o Usuário 1 excluiu um arquivo que o Usuário 2 também excluiu), a única opção é gerar um erro.
Como nota final, como todas as transações feitas em tabelas Delta Lake são armazenadas diretamente em disco, este processo satisfaz a propriedade ACID de durabilidade, o que significa que persistirá mesmo em caso de falha do sistema.
Cada tabela é o resultado da soma total de todos os commits registrados no log de transações do Delta Lake - nem mais, nem menos. O log de transações fornece um guia de instruções passo a passo, detalhando exatamente como ir do estado original da tabela para seu estado atual.
Portanto, podemos recriar o estado de uma tabela em qualquer ponto no tempo começando com uma tabela original e processando apenas os commits feitos antes desse ponto. Essa habilidade poderosa é conhecida como “viagem no tempo” ou versionamento de dados, e pode ser uma salvação em diversas situações. Para mais informações, leia o post do blog Introducing Delta Time Travel for Large Scale Data Lakes, ou consulte a documentação de viagem no tempo do Delta Lake.
Como o registro definitivo de cada alteração já feita em uma tabela, o log de transações do Delta Lake oferece aos usuários uma linha do tempo de dados verificável que é útil para fins de governança, auditoria e conformidade. Ele também pode ser usado para rastrear a origem de uma alteração inadvertida ou um bug em um pipeline até a ação exata que a causou. Os usuários podem executar DESCRIBE HISTORY para ver metadados sobre as alterações que foram feitas.
Neste blog, exploramos os detalhes de como o log de transações do Delta Lake funciona, incluindo:
Artigos nesta série:
Mergulhando no Delta Lake #1: Desvendando o Log de Transações
Mergulhando no Delta Lake #2: Aplicação e Evolução de Esquema
Mergulhando no Delta Lake #3: Internos de DML (Update, Delete, Merge)
Artigos relacionados:
O Que é um Data Lake?
Produzindo Machine Learning com Delta Lake
(Esta publicação no blog foi traduzida utilizando ferramentas baseadas em inteligência artificial) Publicação original
