← Voltar ao blog

O problema dos backups consistentes em Kubernetes

Como transformei backups independentes de control plane e PVs em recovery sets com identidade exata e uma janela de consistência medida em 64 segundos.

kuberneteshomelabbackupdisaster-recovery

Depois de três rehearsals de Disaster Recovery no meu homelab K3s, eu já conseguia reconstruir o control plane, restaurar PersistentVolumes, carregar imagens OCI sem Internet e iniciar workloads em outra máquina.

Mas ainda havia um problema conceitual.

Eu possuía um backup do control plane e um backup dos volumes.

Isso não significava que eu possuía um estado consistente do sistema.

Dois backups corretos podem formar um restore incorreto

Imagine esta sequência:

sequenceDiagram
    participant CP as Control plane
    participant App as Aplicação
    participant PV as Persistent Volumes
    Note over CP: 10:00 backup
    App->>PV: 10:10 continua escrevendo
    Note over PV: 10:20 backup

Os dois backups podem estar tecnicamente íntegros. Mesmo assim, eles representam instantes diferentes.

Dependendo da aplicação, o Kubernetes pode acreditar em um estado enquanto o volume persistente contém outro.

No meu laboratório, essa diferença apareceu de forma prática quando Tempo precisou lidar com um bloco WAL incompleto durante um restore anterior. O workload conseguiu se recuperar, mas aquilo era evidência suficiente para eu parar de tratar uma cópia de filesystem como automaticamente consistente.

A ideia: bounded-consistency recovery set

Eu não precisava construir um snapshot distribuído atomicamente perfeito para esse laboratório.

Precisava de algo mais simples e verificável:

flowchart LR
    Q[Quiesce dos writers] --> CP[Backup control plane]
    CP --> PV[Backup dos PVs]
    PV --> Meta[Registrar IDs e timestamps]
    Meta --> Restore[Restaurar writers]
    Restore --> Verify[Verificação independente]

Passei a chamar isso de bounded-consistency recovery set.

A palavra “bounded” é importante. Eu não estou afirmando atomicidade distribuída. Estou afirmando que conheço e consigo medir o intervalo durante o qual os writers permaneceram quiesced enquanto os dois lados do backup foram capturados.

Quiesce sem brigar com operadores

Grafana, Tempo e Loki são StatefulSets que podem ser reduzidos de forma controlada.

Prometheus trouxe uma nuance: seu StatefulSet é gerenciado pelo Prometheus Operator.

Escalar diretamente o StatefulSet seria lutar contra o reconciliador.

A solução foi alterar temporariamente o spec.replicas do recurso Prometheus e deixar o próprio Operator produzir o estado desejado.

Essa pequena diferença resume uma regra importante de Kubernetes: quando existe um controller responsável por um recurso, converse com o controller, não com o efeito gerado por ele.

O processo também é fail-closed. Não force-deleto Pods writers de produção para conseguir um backup. Se o quiesce gracioso não ocorrer dentro do limite, o backup consistente falha.

Falhar antes de interromper writers

Outro cuidado foi mover problemas previsíveis para antes da janela de interrupção.

O orquestrador valida API K3s, ferramentas e principalmente o repositório Restic/R2 antes de começar o quiesce.

Se o storage off-host estiver indisponível ou houver um problema conhecido no repositório, não faz sentido parar aplicações apenas para descobrir isso depois.

Operações caras de retenção, prune e integridade também foram movidas para depois que os writers já voltaram.

O objetivo é manter a janela crítica limitada ao que realmente exige consistência.

Identidade exata dos artefatos

Uma das mudanças mais importantes foi abandonar a ideia de “restaure o backup mais recente”.

O recovery set registra IDs e timestamps exatos.

Para o control plane, o script cria um novo archive SQLite/token, verifica o archive localmente, envia exatamente aquele arquivo e checksum ao R2 e depois resolve o snapshot Restic que contém aquele archive específico.

Para os PVs, o snapshot Restic criado durante a mesma janela também tem ID e timestamp registrados.

O primeiro recovery set completo ficou assim:

backup_set_id=20260916T114220Z
quiesced_at=2026-09-16T11:42:34Z
control_plane_snapshot=6709a96774a79ded9e3435591074d9445d9f814127b0b1a4ba3041d6a39f3882
pv_snapshot=1830fedfe1c6f293cef2eb971f390f28fd761ba1e929c75b9e06c2a007da190e
completed_backup_window_at=2026-09-16T11:43:38Z
consistency_window_seconds=64
writers_restored_at=2026-09-16T11:44:21Z
result=PASS

A janela medida entre quiesce e conclusão das duas capturas foi de 64 segundos.

timeline
    title Recovery set 20260916T114220Z
    11h42m34s : Writers quiesced
    11h42m39s : Snapshot do control plane
    11h43m00s : Snapshot dos PVs
    11h43m38s : Janela de backup concluída — 64 s
    11h44m21s : Writers restaurados

Verificar o próprio verificador

O orquestrador terminar com PASS ainda não era evidência suficiente.

Criei um verificador independente que reabre o repositório off-host e confirma metadata do recovery set, os dois snapshot IDs exatos, timestamps reais, presença do archive de control plane no snapshot correto, root esperado dos volumes no snapshot PV e timestamps dentro da janela registrada.

Essa etapa também passou.

A diferença é sutil, mas importante: o mesmo processo que produz um artefato não deveria ser a única autoridade dizendo que o artefato está correto.

Concorrência de backups também é estado

O servidor continua executando o backup diário normal.

O recovery set consistente é semanal porque provoca uma pequena janela de quiesce.

Os dois fluxos compartilham o mesmo lock: /run/lock/guiosoft-k3s-backup.lock.

O wrapper semanal mantém o lock não apenas durante a captura, mas também durante verificação e manutenção Restic posterior. Assim outro processo de backup não entra no repositório no meio da sequência e altera o contexto que estou verificando.

Do recovery set para um bundle portátil

Depois de criar uma identidade formal para o par control-plane/PV, o bundle de DR também precisou mudar.

Antes, um builder poderia selecionar independentemente um snapshot recente de control plane e um snapshot recente de PV. Isso reintroduziria exatamente o problema que eu acabara de resolver.

O bundle v2 agora nasce de um backup_set_id específico.

flowchart LR
    Set[backup_set_id] --> CP[Snapshot CP exato]
    Set --> PV[Snapshot PV exato]
    CP --> Bundle[Bundle DR v2]
    PV --> Bundle
    OCI[Imagens OCI offline] --> Bundle
    Tools[Recovery tooling] --> Bundle
    Meta[Metadata + checksums] --> Bundle
    Bundle --> Verify[Verificação completa]

O materializador restaura os snapshot IDs exatos, verifica timestamps, cruza metadata do PV com o mesmo backup set e só então monta o artefato offline.

O primeiro bundle portátil v2 foi construído a partir do recovery set de 64 segundos e passou sua verificação completa.

O que mudou na minha definição de backup

No começo do projeto, backup significava algo próximo de “arquivo existe fora do servidor”.

Depois dos rehearsals, a definição ficou bem mais exigente:

  • artefato existe;
  • checksum confere;
  • snapshot exato é conhecido;
  • timestamp é conhecido;
  • relação CP/PV é conhecida;
  • restore foi exercitado;
  • imagens estão disponíveis offline;
  • tooling está disponível offline;
  • secrets possuem recuperação independente;
  • pós-condições são verificadas.

Isso obviamente é mais trabalho.

Mas também transforma backup de uma esperança em uma propriedade testável do sistema.

Onde o projeto está agora

O homelab que começou com “vamos instalar K3s” hoje possui:

  • infraestrutura do host declarada com Ansible;
  • recursos externos com Terraform;
  • workloads reconciliados por Flux;
  • secrets cifrados com SOPS + age;
  • métricas, logs e traces correlacionados;
  • backup off-host com Restic/R2;
  • rehearsals completos em uma segunda máquina;
  • recovery OCI offline;
  • RTO medido;
  • recovery sets com consistência limitada e mensurada;
  • bundle de DR portátil ligado a snapshots exatos;
  • guards fail-closed para operações destrutivas;
  • separação declarativa entre produção e DR.

Ainda há coisas para melhorar, e novos rehearsals certamente vão encontrar outras suposições erradas.

Hoje considero isso uma característica do projeto, não um defeito.

A parte mais interessante do laboratório deixou de ser o Kubernetes em si. Passou a ser usar um ambiente pequeno para exercitar as mesmas perguntas que aparecem em sistemas maiores:

o estado está declarado? é observável? é recuperável? conseguimos provar?

É essa última pergunta que pretendo continuar usando como critério para as próximas etapas.


Projeto: guionardo/guiosoft-k3s-lab.