O NameNode no Hadoop gerencia metadados do sistema de arquivos, mantendo uma árvore de diretórios na memória e persistindo informações no disco. Em ambientes de produção com cargas intensas, ferramentas como Flink, Spark ou HBase enviam operações de escrita em metadados de forma concorrente, alcançando taxas de 2000 a 3000 requisições por segundo, o que equivale a aproximadamente 300 MB de dados. No entanto, a gravação direta em disco se torna um gargalo de desempenho, devido à latência inerente a dispositivos de armazenamento secundário. Para mitigar esse problema, o Hadoop implementa um mecanismo de duplo buffer, que transforma operações de I/O de disco em operações de memória, viabilizando o processamento de alta concorrência. Funcionamento do Duplo Buffer O mecanismo utiliza dois buffers em memória: um para receber escritas e outro para realizar o flush para o disco. Inicialmente, os metadados são gravados no buffer corrente (bufferAtual). Quando condições específicas são atendidas, os buffers são trocados: o conteúdo do bufferAtual é movido para o bufferPronto, enquanto o bufferAtual fica vazio para novas escritas. O bufferPronto então executa o flush assíncrono para o disco, e após a conclusão, seu conteúdo é limpo. Esse ciclo se repete continuamente, garantindo que o buffer corrente sempre esteja disponível para escritas imediatas. Implementação via Código Fonte No código do Hadoop, a classe FSEditLog gerencia esse processo, controlando transações e buffers. A sincronização ocorre no método sincronizarLog(), que emprega travas segmentadas para garantir eficiência e segurança durante operações concorrentes. Exemplo simplificado da definição dos buffers:
private BufferTransacao bufferAtual; // buffer corrente para escrita de transações
private BufferTransacao bufferPronto; // buffer pronto para flush no disco
A troca de buffers é realizada pelo método prepararParaFlush():
public void prepararParaFlush() {
assert estaLimpo() : "dados anteriores não foram descarregados";
BufferTransacao temp = bufferPronto;
bufferPronto = bufferAtual;
bufferAtual = temp;
}
No método sincronizarLog(), verifica-se primeiro se a transação atual já foi concluída por outra thread, evitando trabalho redundante. Se necessário, inicia-se o processo de flush, trocando os buffers e invocando operações de I/O de forma assíncrona. Variáveis como isSincronizacaoExecutando e idTransacaoSincronizada controlam o estado, garantindo que múltiplas threads possam aguardar de forma segura durante a sincronização. Após a troca de buffers, o flush é executado sem a trava segmentada, permitindo que novas escritas ocorram no bufferAtual em paralelo. Ao concluir, o método atualiza os estados e notifica threads aguardando, finalizando o ciclo de persistência de metadados.