Fala dataholics, hoje o tema é sobre uma feature nova que traz a proposta de otimização do storage e veremos como ela funciona e se de fato temos essa otimização.
O que veremos nesse post:
Versão suportada pelo Deletion Vector
O que é o Deletion Vector
Como funciona internamente o Deletion Vector
Limpeza de versões do Deletion Vector
Deletion Vector é só para Deletes?
Performance?
Limitações
Considerações finais
O Deletion Vector está disponível a partir do Databricks Runtime 12.1, para usar o Deletion Vector sua tabela precisa estar na MinReaderVersion = 3 e MinWriterVersion = 7, então, geralmente você precisará fazer o upgrade da sua tabela.
Se você não entende como funciona esse esquema de Upgrade do Delta, falo um pouco nesse post sobre a feature Column Mapping.
Essa é a tabela de features por versão:
Note que a Deletion Vectors é praticamente a última versão.
A proposta principal do Deletion Vector é reduzir a escrita no storage utilizando um mapa de vetores para marcar registros alterados dentro de um PARQUET.
Calma, vai ficando mais simples.
Se você já leu meus posts anteriores falando sobre Delta engine, sabe que para cada alteração na tabela Delta, a engine precisa fazer várias escritas no storage, tanto para criar novos arquivos PARQUET (que é onde estão os dados de fato), quanto para registrar no Transaction Log (JSON e CRC) e garantir a integridade dos dados.
Ou seja, se faço Delete de 1 registro em uma tabela Delta com 5 mil registros, considerando que esses 5 mil registros estejam em 1 único arquivo PARQUET, basicamente a engine Delta lê esse arquivo, pega os 4999 registros e escreve 1 novo arquivo PARQUET, o antigo é marcado como removido da tabela e pode ficar para Time Travel.
Até aqui estamos juntos certo?
O Deletion Vector trabalha justamente nesses casos e veremos na prática, o que aconteceria se tivesse Deletion Vector habilitado nessa tabela seria o seguinte, a Delta Engine não leria o arquivo Parquet do Storage e nem gravaria 1 arquivo novo, ela simplesmente criaria um arquivo .bin que tem o tamanho de poucos bytes, nesse arquivo bin possui um bitmap informando quais registros no arquivo PARQUET foram excluídos, assim quando a engine for ler essa tabela, ela precisa ler esse bitmap para remover os registros deletados na hora de devolver para a query que solicitou.
Vamos para a prática e tudo fará sentido.
Vamos usar aquele exemplo clássico com dados de pacientes de covid disponibilizados no Sample do Databricks, sempre uso esses exemplos, pois, você pode reproduzir para estudar no seu ambiente.
Criando nossa tabela Delta para o exemplo, note temos 5165 registros.
Abaixo vemos a versão criada, por não utilizar nenhuma feature diferente na criação, vem com a versão básica MinReaderVersion = 1 e MinWriterVersion = 2.
Primeiro Delete sem Deletion Vector, sem novidades, acontecerá como mencionei.
Criado a versão 1 e você já pode ver na pasta do _delta_log os arquivos de logs JSON e CRC (esse é o Transaction Log do Delta).
Criado 1 novo arquivo PARQUET (arquivo onde estão os dados de fato).
E no Log (arquivo JSON) vemos exatamente como mencionei, ele marca o arquivo anterior com as 5165 linhas como removido e cria um novo arquivo com 5164 linhas.
Ok Reginaldo, tudo bem, e o que será otimizado então? Quanto custa essa operação?
Veja que precisamos escrever basicamente o mesmo arquivo (quase duplicado) no storage, com o tamanho de 52KB.
Não entrarei no quesito de calcular quanto isso custa $ no seu Storage, pois é complexo e pode depender de N variáveis, mas basicamente pagamos por armazenamento e transações (leitura, escrita entre outas operações), logo dependendo do tamanho do arquivo algumas transações podem ser quebradas em duas ou mais, aumentando custo do seu Storage.
Nesse exemplo, estamos então pagando pela transação e pelo armazenamento de um novo arquivo, quase que duplicado(1 registro a menos), lembre-se que é apenas um exemplo bem simplista, em ambientes de produção a quantidade de escrita nos Storages é bem mais agressiva e esse volume pode ser bem mais expressivo em ocupação de espaço no storage e transações.
Bom, então vamos habilitar o Deletion Vector e ver como funciona.
Note que sua tabela agora foi atualizada para a versão MinReaderVersion = 3 e MinWriterVersion = 7.
E agora executando outro Delete.
Uma nova versão da tabela foi gerada, mas note la no storage o que aconteceu, não foi adicionado nenhum novo arquivo PARQUET, agora foi adicionado um arquivo .bin de 43 bytes.
Concordamos que escrever 43 bytes é bem melhor do que escrever 52 Kb certo?
E como ficou o Log?
Olha que loucura, ele marca o arquivo part-00000-94242640-b08a-4f0b-8bd8-29c5a370d996.c000.snappy.parquet como removido(Remove.path) e em seguida o mesmo arquivo como adicionado (Add.path).
Porém, note que no ADD, agora temos um novo atributo chamado deletionVector.
Esse cara indica que nesse arquivo possui registros mapeados pelo bitmap (arquivo .bin) que foram excluídos da tabela e sempre que ler esse arquivo PARQUET precisa ler também o bitmap para saber quais são os registros que não fazem parte da tabela.
Confesso que fiquei um bom tempo tentando ler o arquivo .bin e interpretar o seu funcionamento, mas ficou muito complexo, o máximo que cheguei foi ver um vetor, mas sem nexo nenhum.
Mas se você é daqueles curiosos que gostam de entender como o Spark funciona e como ele aplica o Deletion Vector nesse cenário, pode pegar o código no github e dar uma olhada, fiz isso também, mas scala não é o meu forte rs.
Quando realizo a leitura da tabela novamente, o registro já não aparece mais na minha tabela, ele está lendo o arquivo com 5164 registros, mas removendo em tempo de execução o registro mapeado no bitmap.
E como limpar essas versões antigas do Delection Vector e limpar a sujeira dentro dos arquivos PARQUET? Imagina isso em produção, você terá muitos arquivos de bitmap e muitos arquivos PARQUET com "sujeira" dentro deles.
Para limpar os registros deletados dentro dos arquivos PARQUET e não precisar usar mais o Deletion Vector, você pode aplicar uma operação de OPTIMIZE na sua tabela, os arquivos serão reescritos, as linhas que foram apagadas serão definitivamente apagadas do PARQUET, não precisando mais do Deletion Vector.
Para limpar os arquivos .bin gerados pelo Delection Vector da pasta é com a operação de Vacuum.
Falo sobre rotina de Vacuum aqui:
Deletion Vector só funciona com Deletes?
Não, ele pode funcionar com Updates e Merge também, mas só se você estiver utilizando a engine Photon, pois ele consegue aplicar outra feature sensacional que falarei em outro post, predictive IO para Updates.
Então resumindo, cluster tradicional com Databricks Runtime 12.1 você pode usar o Deletion Vector para operações de Delete, utilizando o Databricks Runtime 12.1 e com Engine Photon você tem esse beneficio também para Updates e Merges.
E performance? Se você considerar que ele escreve arquivos menores a performance deveria melhor certo?
Bom não foi o que notei, é quase imperceptível em cenários pequenos.
Simulei alguns Deletes com e sem Delection Vector para comparar:
Esse levou 31 segundos com Deletion Vector habilitado.
Sem Deletion Vector levou 24 segundos.
Parece até que é mais rápido, contudo, aparentemente é o mesmo.
Em resumo a otimização que essa feature traz não é focada para performance e sim para o Storage.
Olhe agora no storage nossos Deletes com Deletion Vector:
104 KB.
Sem Deletion Vector:
621 KB
É uma boa diferença tanto para armazenamento quanto para transações dependendo do tamanho dos arquivos, logo, com certeza é uma boa otimização para o seu Storage.
Limitações:
São poucas limitações e basicamente para quem gera Manifestos para ler Delta com outras engines, exemplo Presto ou Trino.
Outro ponto que essa feature continua em Public Preview.
Bom espero que tenha gostado, é uma feature relativamente nova, continuo aplicando em alguns ambientes mais controlados, não tenho benchmarks mais elaborados nesse momento, mas com certeza pode ser mais uma ferramenta para o seu cinturão.
Link Github:
Fique bem e até a próxima.
Comentários