Blog

Compactação de log de eventos: reduzir armazenamento sem perder a capacidade de reprocessar

O tópico de eventos passou de quatrocentos gigabytes para dois terabytes em sete meses e a conta de armazenamento deixou de ser detalhe de rodapé. Alguém propôs a solução óbvia: reduzir a retenção de trinta dias para sete. A proposta foi aprovada numa quinta-feira, aplicada na sexta, e na terça seguinte o time descobriu que o serviço de recomendação não conseguia mais reconstruir o próprio estado depois de um bug de cálculo, porque os eventos que ele precisava reler tinham vinte e dois dias. Este artigo mostra por que retenção por tempo e compactação por chave resolvem problemas diferentes e por que trocar uma pela outra é o erro mais caro dessa área, o que a compactação garante de verdade e o que ela destrói para sempre, por que a lápide é o único jeito de apagar uma chave e por que ela tem prazo de validade, como calcular a taxa de compactação real do seu tópico antes de ligar qualquer coisa, por que a chave do evento deixa de ser detalhe de particionamento e vira decisão de retenção, qual é a topologia de dois tópicos que preserva auditoria e reprocessamento ao mesmo tempo, e quais indicadores mostram que a compactação parou de funcionar antes de o disco encher.

2026-09-17 / Arquitetura / 18 min

01

Retenção por tempo e compactação por chave não são a mesma alavanca

A confusão começa porque as duas políticas aparecem lado a lado na mesma tela de configuração e as duas reduzem o tamanho do tópico. A semelhança termina aí. A retenção por tempo apaga segmentos inteiros com base na idade: passados os sete dias, tudo o que estava naquele arquivo some, sem que ninguém olhe o conteúdo. A compactação por chave preserva o último valor conhecido de cada chave para sempre e descarta apenas as versões anteriores daquela mesma chave. A primeira é uma política de esquecimento cronológico, a segunda é uma política de deduplicação por identidade.

A consequência prática é que as duas respondem a perguntas diferentes. Com retenção por tempo, a pergunta que o tópico responde é: o que aconteceu nos últimos N dias. Com compactação, a pergunta é: qual é o estado atual de cada entidade. Um serviço que precisa reconstruir um saldo somando todos os lançamentos depende da primeira. Um serviço que precisa saber apenas o endereço atual de cada cliente depende da segunda, e ficaria satisfeito com um tópico compactado de um centésimo do tamanho.

O erro do incidente descrito na abertura foi tratar as duas como intercambiáveis porque ambas apareciam como formas de reduzir disco. Reduzir a janela de retenção destruiu a capacidade de reprocessar, que era a propriedade que o time achava que estava comprando. Ligar compactação naquele tópico teria sido igualmente destrutivo por outro caminho: o histórico de lançamentos teria virado um saldo por conta, e a auditoria exigida em contrato sumiria em silêncio, sem nenhum erro, sem nenhum alerta, apenas com números que deixam de bater três meses depois.

PropriedadeRetenção por tempoCompactação por chaveConsequência prática
Critério de descarteIdade do segmento, independentemente do conteúdoExistência de versão mais recente da mesma chaveCompactar não libera espaço em tópico de chaves únicas
O que fica garantidoTodo evento dentro da janela, inclusive os intermediáriosO último valor de cada chave, sem garantia dos anterioresSó a retenção permite recontar a partir do zero
O que se perdeTudo o que é mais antigo que a janelaTodas as transições intermediárias de cada chaveCompactação destrói a trilha de auditoria de mudança
Tamanho em regime permanenteProporcional à taxa de eventos vezes a janelaProporcional ao número de chaves distintasCompactação limita crescimento, retenção limita idade
Eventos sem chaveTratados como qualquer outroNunca removidos, acumulam indefinidamenteUm produtor esquecido sem chave anula a economia toda
Apagar uma entidadeAcontece sozinho quando a janela passaExige lápide explícita e só depois someDireito ao esquecimento vira trabalho de aplicação

A quinta linha é a que produz o maior número de decepções em produção. Um tópico compactado onde metade dos produtores emite eventos sem chave não encolhe pela metade: ele encolhe apenas na parte com chave e continua crescendo linearmente na outra, porque o núcleo do mecanismo não tem nenhum critério para comparar dois registros sem identidade. O gráfico de tamanho depois de ligar a compactação fica com um degrau para baixo seguido da mesma inclinação de antes, e o time conclui erroneamente que a compactação não funciona.

02

O que a compactação garante é mais fraco do que a maioria assume

A garantia formal da compactação tem uma redação estreita e vale a pena enunciá-la com precisão, porque quase todo bug de arquitetura nessa área nasce de assumir mais do que ela promete. A garantia é: qualquer consumidor que leia o tópico do início até o fim verá, no mínimo, o último valor escrito para cada chave. Não está prometido que ele verá apenas esse valor, nem que ele verá os valores intermediários, nem que dois consumidores que leiam em momentos diferentes verão a mesma sequência.

Três consequências saem direto dessa redação. A primeira é que o consumidor precisa ser idempotente por construção, porque ele pode receber a mesma chave várias vezes: o segmento ativo ainda não foi compactado e pode conter três versões da mesma chave que uma releitura futura já não terá. A segunda é que qualquer lógica que dependa da sequência de transições de uma entidade está proibida de rodar sobre tópico compactado, porque a sequência é exatamente o que foi descartado. A terceira é que o resultado de um reprocessamento deixa de ser determinístico entre execuções: reprocessar hoje e reprocessar amanhã pode produzir históricos diferentes para a mesma entidade, mesmo sem nenhuma escrita nova, porque a compactação rodou no meio.

Essa terceira consequência é a que costuma quebrar times que usam o log como fonte de verdade para cálculos financeiros ou para modelos treinados a partir de histórico. Um relatório reconstruído em janeiro e reconstruído de novo em março sobre o mesmo tópico compactado não bate, e a diferença não aparece como erro em lugar nenhum: os dois valores estão corretos em relação ao que o tópico continha no momento de cada leitura. A investigação desse tipo de divergência costuma consumir semanas antes de alguém suspeitar da política de retenção.

Topico com particao unica, chaves A, B e C

Antes da compactacao (offsets 0 a 8):
  off 0  A=10
  off 1  B=20
  off 2  A=11      <- versao intermediaria de A
  off 3  C=30
  off 4  A=12      <- versao intermediaria de A
  off 5  B=21      <- versao intermediaria de B
  off 6  A=13      <- ultimo valor de A
  off 7  B=22      <- ultimo valor de B
  off 8  C=31      <- ultimo valor de C   [segmento ativo]

Depois da compactacao dos segmentos fechados (0 a 7):
  off 6  A=13
  off 7  B=22
  off 8  C=31      [segmento ativo, nunca compactado]

O que sobrevive: o ultimo valor de cada chave.
O que some: A=10, A=11, A=12, B=20, B=21, C=30.
Os offsets NAO sao renumerados: 0 a 5 simplesmente nao existem mais.
Um consumidor que peca o offset 3 recebe o proximo offset existente.

O detalhe dos offsets no fim do diagrama tem consequência operacional direta. Como a compactação abre buracos na sequência sem renumerar nada, qualquer código que assuma continuidade de offsets para calcular atraso, para estimar quantidade de mensagens pendentes ou para dividir trabalho entre consumidores vai produzir números errados em tópico compactado. A diferença entre o maior offset e o offset atual do consumidor deixa de ser a quantidade de mensagens a processar e passa a ser um limite superior que pode estar ordens de magnitude acima do valor real.

03

Apagar uma chave exige lápide, e a lápide tem prazo

Num tópico compactado a ausência de novidade significa permanência: o último valor de cada chave fica lá indefinidamente, e nenhuma passagem de tempo o remove. Isso cria um problema específico quando a entidade deixa de existir, seja porque o cliente foi excluído, porque o produto saiu do catálogo ou porque uma requisição de exclusão de dados pessoais precisa ser honrada. O mecanismo para isso é a lápide: um registro com a chave preenchida e o valor nulo, que a compactação interpreta como instrução de remover a chave inteira.

A parte que surpreende é que a lápide não some imediatamente e também não fica para sempre. Ela precisa permanecer visível por tempo suficiente para que todo consumidor que esteja lendo o tópico veja a instrução de apagar, e depois precisa ela mesma ser removida, senão o tópico acumularia uma lápide por entidade excluída até o fim dos tempos. Existe portanto uma janela de retenção específica para lápides, tipicamente configurada em vinte e quatro horas, e essa janela cria um risco silencioso: um consumidor que fique parado mais tempo do que ela, por um incidente longo, por um fim de semana prolongado ou por um processo de reconstrução demorado, volta a ler o tópico e nunca vê a lápide. Ele mantém no próprio estado uma entidade que foi deliberadamente apagada da origem.

// Producao de lapide e verificacao de janela antes de confiar na exclusao.
// A lapide so cumpre o papel se todos os consumidores leem dentro da janela.

/**
 * Emite a lapide que instrui a compactacao a remover a chave por completo.
 * O valor precisa ser nulo de verdade, nao string vazia nem objeto vazio:
 * a comparacao feita pelo compactador e com ausencia de payload.
 */
async function emitirLapide(produtor, topico, chave) {
  await produtor.send({
    topic: topico,
    messages: [{ key: chave, value: null }],
  });
}

/**
 * Verifica se e seguro confiar na lapide, comparando o atraso de cada grupo
 * consumidor com a janela de retencao de lapides do topico.
 *
 * @param {Array<{grupo: string, atrasoMs: number}>} gruposConsumidores
 * @param {number} janelaLapideMs retencao configurada para lapides
 * @param {number} margem fracao da janela reservada como folga (0.5 = metade)
 */
function avaliarSegurancaDaExclusao(gruposConsumidores, janelaLapideMs, margem = 0.5) {
  const limiteSeguro = janelaLapideMs * margem;

  const emRisco = gruposConsumidores.filter((g) => g.atrasoMs > limiteSeguro);

  return {
    seguro: emRisco.length === 0,
    limiteSeguroMs: limiteSeguro,
    // Grupos que podem nunca ver a lapide: precisam de reconstrucao completa
    // a partir de um snapshot, e nao de leitura incremental.
    gruposQuePrecisamReconstruir: emRisco.map((g) => g.grupo),
  };
}

// Uso tipico dentro do fluxo de exclusao de dados pessoais.
async function excluirEntidade({ produtor, topico, chave, gruposConsumidores, janelaLapideMs }) {
  const avaliacao = avaliarSegurancaDaExclusao(gruposConsumidores, janelaLapideMs);

  await emitirLapide(produtor, topico, chave);

  if (!avaliacao.seguro) {
    // A exclusao foi registrada, mas nao esta comprovada ponta a ponta.
    // O registro abaixo e o que transforma um risco invisivel em tarefa.
    return {
      lapideEmitida: true,
      exclusaoComprovada: false,
      acaoNecessaria: 'reconstruir estado dos grupos atrasados a partir de snapshot',
      grupos: avaliacao.gruposQuePrecisamReconstruir,
    };
  }

  return { lapideEmitida: true, exclusaoComprovada: true };
}

export { emitirLapide, avaliarSegurancaDaExclusao, excluirEntidade };

A função de avaliação existe porque a diferença entre uma exclusão registrada e uma exclusão comprovada é exatamente o tipo de coisa que ninguém percebe até uma auditoria perguntar. Emitir a lápide é trivial e sempre funciona do lado do produtor. Garantir que cada consumidor aplicou a remoção é um problema distribuído que depende do atraso de cada grupo, e a única forma honesta de tratá-lo é medir esse atraso no momento da exclusão e escalar para reconstrução completa quem estiver fora da janela.

Há ainda uma armadilha de implementação frequente o bastante para merecer menção explícita: emitir a lápide com valor de string vazia, com objeto vazio ou com um payload marcado como excluído não produz remoção nenhuma. Qualquer um desses casos é, para o compactador, um valor válido e recente como qualquer outro, e a chave permanece no tópico para sempre com esse conteúdo. O valor precisa ser nulo no sentido de ausência de payload, e vale verificar isso no serializador, porque várias bibliotecas convertem nulo para bytes vazios em silêncio.

04

A economia real depende da razão entre eventos e chaves distintas

Antes de ligar compactação em qualquer tópico vale calcular quanto ela vai economizar, porque o resultado varia entre noventa e oito por cento e zero dependendo de uma única razão: quantos eventos existem por chave distinta. Um tópico de atualização de posição de entregador, onde cada entregador emite um evento a cada cinco segundos, tem uma razão altíssima e compacta maravilhosamente. Um tópico de pedidos criados, onde cada pedido aparece exatamente uma vez com identificador único, tem razão igual a um e compacta exatamente nada, além de ficar mais caro porque o processo de compactação passa a consumir processador e memória sem devolver espaço.

A conta precisa considerar também que o segmento ativo nunca é compactado e que a maioria das configurações só compacta quando a proporção de registros obsoletos ultrapassa um limiar. O tamanho em regime permanente, portanto, não é o número de chaves vezes o tamanho médio do registro: é isso mais o segmento ativo mais a margem tolerada pelo limiar, e esses dois adicionais costumam responder por uma fração relevante do total em tópicos de volume alto.

// Estimativa de economia antes de ligar compactacao em um topico existente.
// Roda sobre uma amostra de offsets, nao sobre o topico inteiro.

/**
 * @param {object} p
 * @param {number} p.eventosNaAmostra     quantidade de registros lidos
 * @param {number} p.chavesDistintas      chaves unicas encontradas na amostra
 * @param {number} p.eventosSemChave      registros sem chave na amostra
 * @param {number} p.tamanhoAtualBytes    tamanho atual do topico em disco
 * @param {number} p.segmentoAtivoBytes   tamanho do segmento ativo por particao
 * @param {number} p.particoes            numero de particoes do topico
 * @param {number} p.limiarSujeira        fracao obsoleta tolerada antes de compactar
 */
export function estimarEconomiaDeCompactacao({
  eventosNaAmostra,
  chavesDistintas,
  eventosSemChave,
  tamanhoAtualBytes,
  segmentoAtivoBytes,
  particoes,
  limiarSujeira = 0.5,
}) {
  const comChave = eventosNaAmostra - eventosSemChave;
  const eventosPorChave = comChave / Math.max(chavesDistintas, 1);

  // Registros sem chave nunca sao removidos: eles sobrevivem inteiros.
  const fracaoSemChave = eventosSemChave / eventosNaAmostra;
  const bytesSemChave = tamanhoAtualBytes * fracaoSemChave;

  // Da parte com chave, sobrevive apenas o ultimo valor de cada chave,
  // extrapolado da amostra para a proporcao do topico inteiro.
  const fracaoComChave = 1 - fracaoSemChave;
  const bytesComChaveApos =
    eventosPorChave > 0 ? (tamanhoAtualBytes * fracaoComChave) / eventosPorChave : 0;

  // O segmento ativo de cada particao nunca entra na compactacao.
  const bytesSegmentoAtivo = segmentoAtivoBytes * particoes;

  // O compactador so roda quando a sujeira passa do limiar, entao em regime
  // permanente sobra sempre essa fracao de registros obsoletos no disco.
  const bytesUteis = bytesSemChave + bytesComChaveApos;
  const bytesSujeiraTolerada = bytesUteis * (limiarSujeira / (1 - limiarSujeira));

  const tamanhoEstimado = bytesUteis + bytesSujeiraTolerada + bytesSegmentoAtivo;
  const economia = 1 - tamanhoEstimado / tamanhoAtualBytes;

  return {
    eventosPorChave: Number(eventosPorChave.toFixed(2)),
    tamanhoEstimadoBytes: Math.round(tamanhoEstimado),
    economiaPercentual: Number((economia * 100).toFixed(1)),
    // Abaixo de tres eventos por chave a compactacao custa mais em
    // processamento do que devolve em disco.
    valeAPena: eventosPorChave >= 3 && economia > 0.2,
    alertaSemChave:
      fracaoSemChave > 0.1
        ? 'mais de dez por cento dos registros nao tem chave e nunca serao removidos'
        : null,
  };
}

O limiar de três eventos por chave no retorno da função não é arbitrário: abaixo dele, o trabalho de leitura, construção do mapa de chaves e reescrita dos segmentos custa mais processador e memória do que o espaço economizado justifica, e em disco de custo baixo essa troca raramente compensa. O alerta sobre registros sem chave está ali porque é o diagnóstico mais frequente para a pergunta que aparece semanas depois: por que o tópico compactado continua crescendo.

05

A chave do evento deixa de ser detalhe de roteamento e vira política de retenção

Em um tópico com retenção por tempo, a chave serve para uma coisa só: decidir em qual partição o registro cai, e com isso garantir ordem entre eventos da mesma entidade. Escolher a chave errada nesse cenário produz desequilíbrio de partição, que é um problema de desempenho, incômodo mas reversível. Em um tópico compactado a mesma escolha determina a granularidade do que sobrevive, e isso não é reversível: o que foi descartado por uma chave grosseira demais não volta.

O caso mais comum é o de um tópico de mudança de status de pedido. Se a chave for o identificador do pedido, a compactação preserva apenas o status atual de cada pedido, e a pergunta quanto tempo um pedido ficou em separação fica sem resposta para sempre. Se a chave for a combinação de pedido e status, cada transição vira uma chave própria, todas sobrevivem, e o tópico deixa de encolher de forma relevante porque a razão entre eventos e chaves cai para perto de um. As duas escolhas são defensáveis, mas elas compram coisas opostas, e a decisão precisa ser tomada olhando para as consultas que o sistema precisa responder e não para o tamanho do disco.

Chave escolhidaO que sobrevive à compactaçãoPergunta que continua respondívelPergunta que se perde
Identificador do pedidoUm registro por pedido, com o status atualQual é o status de cada pedido agoraQuanto tempo ficou em cada etapa
Pedido mais statusUm registro por transição ocorridaQuais etapas cada pedido percorreu e quandoNenhuma, mas quase não há economia de espaço
Identificador do clienteApenas o último pedido de cada clienteQual foi a última compra de cada clienteTodo o histórico de pedidos anteriores
Pedido mais diaUm registro por pedido por dia de atividadeComo o pedido evoluiu em granularidade diáriaTransições múltiplas dentro do mesmo dia
Sem chaveTudo, indefinidamenteTodasNenhuma, e o tópico nunca encolhe

A quarta linha merece atenção porque é a solução intermediária que raramente é considerada e que resolve bem uma classe inteira de casos. Compor a chave com um recorte temporal converte a compactação de deduplicação total em amostragem por período: preserva-se o estado de cada entidade ao fim de cada dia, o que basta para a maioria das análises de evolução, e descarta-se o ruído de alta frequência dentro do período. O custo em espaço é o número de chaves multiplicado pelo número de dias de atividade, o que costuma ser uma ordem de magnitude menor do que guardar tudo e uma ordem maior do que guardar só o estado atual.

06

A topologia de dois tópicos resolve a tensão entre auditoria e estado

Quando o mesmo fluxo precisa simultaneamente servir de trilha de auditoria completa e de fonte de estado atual, nenhuma política única atende, porque as exigências são contraditórias por construção. A saída é parar de tentar configurar um tópico para fazer as duas coisas e separar os papéis em dois tópicos com políticas próprias, alimentados pelo mesmo produtor ou por uma transformação entre eles.

                    +---------------------+
   produtor ------> | pedidos.eventos     |  retencao por tempo: 30 dias
                    | (log completo)      |  todas as transicoes
                    +----------+----------+
                               |
                               | transformacao: extrai ultimo estado
                               v
                    +---------------------+
                    | pedidos.estado      |  compactado, sem retencao
                    | (chave = pedidoId)  |  um registro por pedido
                    +----------+----------+
                               |
              +----------------+----------------+
              v                                 v
    +-------------------+             +-------------------+
    | servico de busca  |             | painel operacional|
    | reconstroi do zero|             | le so o atual     |
    +-------------------+             +-------------------+

   Arquivamento frio (fora do broker):
   pedidos.eventos --> objeto em armazenamento barato, particionado por dia
                       retencao de anos, leitura rara, custo por gigabyte
                       de uma a duas ordens de magnitude menor

   Reprocessamento historico le do arquivo frio, nao do broker.
   Reprocessamento recente le de pedidos.eventos, dentro dos 30 dias.
   Inicializacao de servico novo le de pedidos.estado, em minutos.

Essa topologia resolve três problemas de uma vez. O tópico de eventos mantém a janela de reprocessamento que o time realmente usa no dia a dia, que quase sempre é de dias e não de anos, e paga armazenamento caro de broker apenas por essa janela. O tópico compactado dá a qualquer serviço novo uma forma de chegar ao estado atual em minutos em vez de horas, porque ele lê um registro por entidade em vez de todo o histórico. E o arquivamento frio preserva o histórico completo para auditoria e para reprocessamento raro, num armazenamento cujo custo por gigabyte é uma a duas ordens de magnitude menor do que o do broker.

  1. Medir a razão entre eventos e chaves distintas em uma amostra do tópico atual, para saber se a compactação tem algo a economizar antes de qualquer mudança de configuração.
  2. Inventariar os consumidores e classificar cada um como dependente de histórico ou dependente apenas de estado atual, porque essa lista define quais precisam do tópico de eventos e quais podem migrar para o compactado.
  3. Criar o tópico compactado novo em vez de mudar a política do existente, já que a mudança de política é destrutiva e não tem volta.
  4. Escrever a transformação que alimenta o tópico compactado a partir do de eventos, validando que a chave escolhida preserva a granularidade que os consumidores de estado precisam.
  5. Ligar o arquivamento frio e verificar que um reprocessamento a partir do arquivo produz o mesmo resultado que um reprocessamento a partir do broker, antes de reduzir qualquer retenção.
  6. Só então reduzir a retenção do tópico de eventos, em um passo por vez, monitorando o atraso máximo dos consumidores a cada redução.

A ordem dos passos importa mais do que o conteúdo de cada um. A redução de retenção é o único passo irreversível da lista e por isso é o último, depois de a rota alternativa de reprocessamento já estar validada em execução real e não apenas planejada. O erro do incidente da abertura foi executar exclusivamente esse passo, que é o único que aparece como uma linha de configuração e por isso parece o mais barato.

  • Atraso máximo por grupo consumidor comparado com a janela de retenção de lápides: quando o primeiro passa de metade da segunda, exclusões deixam de ser comprovadas ponta a ponta.
  • Proporção de registros obsoletos por partição: se ela não volta para baixo do limiar depois de um ciclo, o compactador não está dando conta do volume e o tópico vai crescer sem limite.
  • Fração de registros sem chave produzidos no tópico compactado: qualquer valor acima de um por cento indica produtor novo mal configurado e anula a economia esperada.
  • Idade do registro mais antigo por partição no tópico de eventos, comparada com a janela contratada de reprocessamento: é esse número, e não a configuração, que diz até onde dá para voltar hoje.
  • Tempo de inicialização de um consumidor a partir do zero no tópico compactado: o crescimento desse número é o primeiro sinal de que a razão entre eventos e chaves mudou.

FAQ

Perguntas frequentes

Dá para ligar compactação em um tópico que já existe e está em produção, ou é preciso criar outro?

Tecnicamente dá, e é uma linha de configuração, e é exatamente por isso que essa mudança causa tanto dano. Assim que a política passa a incluir compactação, o compactador começa a processar os segmentos fechados já existentes, e todas as versões intermediárias de cada chave que estavam ali são descartadas de forma definitiva e sem confirmação. Não existe modo de simulação, não existe desfazer, e a única recuperação possível é a partir de um arquivamento externo que precisa ter sido criado antes. Por isso a recomendação prática é sempre criar um tópico novo com a política desejada e alimentar esse tópico a partir do original, mesmo quando isso significa manter os dois lado a lado por algumas semanas pagando armazenamento em dobro. O custo de manter o dobro por um mês é sempre menor do que o custo de descobrir, dois meses depois, que uma análise que ninguém tinha mapeado dependia do histórico intermediário. Existe uma exceção razoável: tópicos que já nasceram como projeção de estado, onde todo consumidor sempre leu apenas o valor atual e onde nenhum relatório histórico jamais foi construído a partir deles. Mesmo nesse caso vale executar antes a estimativa de economia, porque se a razão entre eventos e chaves for baixa a mudança traz o risco todo sem trazer o benefício. E vale documentar a mudança com data e responsável, porque quando alguém perguntar por que os dados antes daquela data têm granularidade diferente, essa anotação é a única resposta que vai existir.

Como fazer reprocessamento histórico depois que a retenção do broker foi reduzida?

A resposta curta é que o reprocessamento histórico deixa de sair do broker e passa a sair do arquivamento frio, e essa mudança precisa ser projetada e testada antes da redução da retenção, não depois. O desenho que funciona bem tem três partes. A primeira é o arquivamento contínuo: um consumidor dedicado que lê o tópico de eventos e escreve blocos em armazenamento de objetos, particionados por dia e por partição de origem, em formato colunar comprimido, o que costuma render uma redução de cinco a dez vezes em relação ao formato do broker e permite leitura seletiva por intervalo de data sem varrer tudo. A segunda é a camada de leitura unificada: uma biblioteca compartilhada que recebe um intervalo de tempo e decide sozinha se lê do arquivo frio, do broker, ou dos dois em sequência costurando o ponto de junção, para que o código de reprocessamento seja idêntico independentemente de onde o dado está. Essa camada é o que evita que cada time reimplemente a lógica de junção com um erro sutil diferente. A terceira é o teste de equivalência recorrente: um trabalho agendado que reprocessa uma janela que existe nos dois lugares e compara os resultados registro a registro, o que transforma uma suposição em verificação e detecta problemas de serialização, de fuso horário e de limite de partição antes que eles apareçam durante um incidente. Sobre desempenho, vale ajustar a expectativa: um reprocessamento a partir de armazenamento de objetos é tipicamente mais lento por registro do que a partir do broker, mas paraleliza muito melhor por ser leitura de arquivos independentes, então com dez leitores concorrentes costuma terminar antes.

Compactação ajuda ou atrapalha uma requisição de exclusão de dados pessoais?

Ajuda em um ponto específico e atrapalha em três, e a conta líquida depende de como o tópico foi desenhado. Ajuda porque a lápide dá um mecanismo explícito e verificável de remoção por chave, que em um tópico com retenção por tempo simplesmente não existe: lá a única forma de apagar um registro específico é reescrever o tópico inteiro ou esperar a janela passar. Atrapalha na primeira frente porque a remoção não é imediata nem tem prazo determinístico, já que ela só se concretiza quando o compactador processa aquele segmento, o que depende do limiar de sujeira e do volume de escrita, e pode levar de minutos a dias, o que é incompatível com um prazo legal expresso em dias corridos sem margem. Atrapalha na segunda frente por causa do segmento ativo, que nunca é compactado: se a chave a excluir teve escrita recente, o valor pessoal continua fisicamente no disco mesmo depois da lápide, até que o segmento rotacione, o que em tópico de volume baixo pode demorar. Atrapalha na terceira frente porque a lápide remove a chave mas não remove dados pessoais que estejam dentro do valor de outras chaves, e é comum um identificador ou um endereço aparecer no payload de eventos de entidades vizinhas. O desenho que resolve as três é separar o dado pessoal do fluxo de eventos desde o começo, mantendo no evento apenas um identificador opaco e guardando o dado sensível em um repositório com exclusão direta e prazo controlado. A exclusão então vira uma operação nesse repositório, imediata e comprovável, e o log de eventos permanece com identificadores que perdem qualquer capacidade de identificar alguém no momento em que a chave de referência some. Essa é a única topologia que responde com prazo confiável a uma requisição formal de exclusão.

Compactação é uma escolha sobre qual pergunta o log continuará respondendo

Reduzir armazenamento de log de eventos parece uma decisão de infraestrutura e é, na prática, uma decisão sobre quais perguntas o sistema continuará conseguindo responder daqui a um ano. Retenção por tempo e compactação por chave cortam o mesmo disco por caminhos opostos: uma apaga o passado inteiro, a outra apaga o caminho e preserva o destino. A escolha certa quase nunca é uma das duas isoladamente, e sim a topologia que separa o log de auditoria do tópico de estado e move o histórico raro para armazenamento frio antes de qualquer redução de janela. Posso levantar a razão entre eventos e chaves dos seus tópicos, mapear quais consumidores dependem de histórico e quais só precisam de estado atual, desenhar a separação e validar o reprocessamento pelo arquivo antes de tocar em qualquer configuração de retenção.