Reliability em bancos de dados e filas para sistemas pequenos
Como projetar confiabilidade em bancos de dados e filas sem adicionar complexidade desnecessária a sistemas pequenos, com práticas de operação, monitoramento e recuperação.
Reliability não exige uma arquitetura grande
Reliability em bancos de dados e filas é a capacidade de o sistema continuar entregando o comportamento esperado, ou se recuperar de modo previsível, quando ocorrem falhas. Em um produto pequeno, isso costuma significar algo bem concreto: não perder dados importantes, não processar uma operação duas vezes sem necessidade, informar o usuário quando uma ação está pendente e restaurar o serviço com rapidez depois de um incidente.
O erro mais comum é tratar confiabilidade como sinônimo de múltiplas regiões, vários clusters ou uma coleção de serviços especializados. Essas escolhas podem ser adequadas em outro estágio, mas também aumentam custo, superfície operacional e formas de falhar. Para uma equipe pequena, uma aplicação simples com banco gerenciado, uma fila gerenciada e procedimentos testados frequentemente é mais confiável do que uma arquitetura distribuída que ninguém consegue operar sob pressão.
O ponto de partida é definir o que precisa ser confiável. Cadastro e cobrança podem exigir persistência confirmada antes da resposta ao cliente. Já o envio de e-mail, a geração de miniatura e a atualização de um índice de busca podem ser assíncronos. A prioridade não é tornar tudo instantâneo; é deixar explícito o que pode esperar, o que pode ser repetido e o que nunca pode desaparecer.
Essa disciplina se aproxima do princípio de projetar para falhas: dependências ficam indisponíveis, processos são interrompidos, redes expiram e operadores cometem enganos. A documentação do pilar de confiabilidade do Google Cloud enfatiza objetivos de recuperação, gestão de falhas e operação como elementos do desenho do sistema. Para sistemas pequenos, traduzir esses princípios em poucas decisões claras tem mais valor do que copiar um desenho complexo.
Comece pelos riscos e pelos objetivos de recuperação
Antes de escolher tecnologia, liste os fluxos que mudam estado: criar pedido, registrar pagamento, alterar permissão, importar arquivo, reservar estoque ou publicar conteúdo. Para cada fluxo, responda: qual dado é a fonte de verdade; qual perda é aceitável; quanto tempo o serviço pode ficar degradado; e como a equipe saberá que houve falha. Essa conversa produz requisitos operacionais reais, não apenas uma lista de componentes.
Dois conceitos ajudam a tornar a discussão objetiva. O RPO, objetivo de ponto de recuperação, indica quanto dado pode ser perdido após uma falha. Um RPO de uma hora, por exemplo, admite perder alterações feitas desde o último ponto recuperável. O RTO, objetivo de tempo de recuperação, define em quanto tempo o serviço deve voltar a operar. Eles não são promessas automáticas do provedor: dependem de backup, acesso, procedimento, testes e decisões tomadas durante o incidente.
Uma loja interna que aceita ficar indisponível por algumas horas pode ter objetivos diferentes de uma plataforma que recebe pagamentos. Da mesma forma, perder um e-mail de confirmação pode ser tolerável se houver reconciliação posterior; perder o registro de um pagamento não é. Classificar operações por impacto evita tratar cada tabela e cada mensagem com o mesmo grau de proteção.
Defina também o modo degradado. Se a fila parar, a aplicação pode gravar o pedido e mostrar “processamento em andamento”, em vez de falhar a compra inteira? Se um serviço de relatório estiver lento, a tela pode exibir dados atrasados com data de atualização? O artigo sobre fallback e confiabilidade em sistemas pequenos oferece um bom complemento para decidir quando reduzir funcionalidade é preferível a negar todo o serviço.
Faça do banco de dados a fonte de verdade
Em muitos sistemas pequenos, o banco relacional é o lugar mais simples para manter o estado principal. Ele oferece transações para agrupar alterações que precisam ocorrer juntas, restrições para proteger regras básicas e consultas para reconciliar o que aconteceu. A confiabilidade melhora quando regras críticas, como chaves únicas, referências válidas e estados permitidos, não dependem apenas de uma verificação no código da aplicação.
Use transações para mudanças que precisam ser atômicas. Ao confirmar um pedido, por exemplo, a criação do pedido, dos itens e do registro de pagamento pendente devem terminar juntas ou não produzir efeito. Entretanto, não mantenha uma transação aberta enquanto chama um serviço externo: a rede pode demorar, conexões ficam presas e o resultado remoto não participa da transação local. Primeiro confirme o estado local; depois execute efeitos externos por um caminho controlado.
Backups automáticos são necessários, mas não bastam. Verifique retenção, frequência, localização, permissões e, principalmente, restauração. Uma cópia que nunca foi restaurada é uma hipótese. Teste periodicamente a recuperação em um ambiente separado: restaure uma cópia, valide a integridade esperada, meça quanto tempo o processo leva e registre os passos. Isso transforma RPO e RTO em números observáveis.
Proteja o banco contra problemas previsíveis de operação. Limite conexões por aplicação, use pool de conexões, imponha timeouts e revise consultas que fazem leituras amplas ou bloqueiam linhas por muito tempo. Acompanhe uso de CPU, memória ou capacidade disponível conforme o serviço usado, conexões, latência das consultas, erros e espaço de armazenamento. Um alerta sobre disco quase cheio ou falhas de autenticação pode evitar uma indisponibilidade maior.
Evite introduzir múltiplos bancos só para parecer moderno. A arquitetura de microserviços envolve descentralização de dados e automação de infraestrutura, mas essas características cobram maturidade operacional. Enquanto um banco atende bem ao domínio e à carga, separar dados sem necessidade pode tornar backup, consistência e investigação de incidentes mais difíceis.
Use filas para desacoplar, não para esconder falhas
Uma fila é útil quando o trabalho pode ser executado depois da resposta ao usuário ou quando um consumidor precisa absorver variações de carga. Processar uma imagem, enviar uma notificação ou sincronizar um sistema externo são exemplos típicos. A fila não elimina a necessidade de confiabilidade: ela desloca parte dela para a publicação, o consumo, as tentativas e a observação do atraso.
O contrato mais seguro para sistemas pequenos costuma ser simples: a mensagem descreve uma intenção ou referência estável, como o identificador de um pedido e o tipo do evento. O consumidor busca os dados necessários na fonte de verdade e registra o resultado. Evite colocar um retrato grande e mutável de toda a entidade na mensagem, pois reprocessamentos futuros podem operar sobre dados antigos sem que isso fique claro.
É importante assumir que uma mensagem pode ser entregue mais de uma vez. Muitas filas priorizam a entrega ao menos uma vez, o que favorece não perder trabalho, mas exige consumidores idempotentes. Idempotência significa que repetir a mesma operação produz o mesmo resultado final. Para isso, guarde uma chave de idempotência, aplique restrições únicas, registre o identificador da mensagem processada ou modele transições de estado que rejeitem repetições inválidas.
Também é possível haver mensagens fora de ordem, dependendo da tecnologia e da configuração. Se a ordem for realmente uma regra de negócio, modele-a explicitamente e aceite o custo de limitar concorrência ou particionar por uma chave relevante. Se não for, faça o consumidor tolerar reordenação. A melhor escolha é aquela que corresponde à regra do negócio, e não à suposição de que a fila sempre preservará uma sequência global.
Defina tentativas com limite e atraso crescente. Um erro transitório, como uma indisponibilidade breve de API, pode se resolver em nova tentativa. Um erro permanente, como dados inválidos, não deve consumir recursos indefinidamente. Após o limite, envie a mensagem para uma fila de mensagens com falha, muitas vezes chamada de dead-letter queue, e crie uma rotina para inspecionar, corrigir ou cancelar esses itens. A fila de falhas não é uma lixeira: itens nela sem responsável representam trabalho de negócio incompleto.
Evite a lacuna entre gravar dados e publicar eventos
Um caso clássico acontece quando a aplicação grava um pedido no banco e, em seguida, publica uma mensagem para a fila. Se o processo cair entre as duas etapas, o pedido existe, mas o e-mail, a cobrança ou a integração associada talvez nunca seja iniciada. Fazer as duas operações em sequência não cria atomicidade, porque banco e fila normalmente são sistemas independentes.
Para reduzir essa lacuna sem adotar uma plataforma complexa, use o padrão transactional outbox. Na mesma transação que altera o estado principal, grave um registro de evento em uma tabela de saída. Um processo separado lê registros pendentes, publica-os na fila e marca o envio. Se ele falhar, tenta de novo. Como a publicação pode acontecer mais de uma vez, o consumidor continua precisando ser idempotente.
Essa tabela também ajuda na auditoria e na recuperação: a equipe consegue ver quais eventos aguardam envio e reenviar um item específico com cuidado. Mantenha campos como identificador único, tipo de evento, referência da entidade, data de criação, número de tentativas e status. Não transforme a outbox em um sistema de eventos genérico sem necessidade; ela deve resolver o problema operacional concreto de entregar efeitos assíncronos a partir de uma alteração confirmada.
Quando APIs internas participam do fluxo, contratos claros são parte da reliability. Uma mudança de campo ou de semântica pode fazer consumidores falharem silenciosamente. Vale aplicar os princípios de arquitetura de sistemas: contratos e versionamento para APIs internas para documentar formatos, compatibilidade, códigos de erro e expectativas de repetição.
Monitore sintomas que levam a decisões
Monitorar não é acumular dashboards. É responder cedo a perguntas operacionais: usuários estão recebendo erros? O banco está próximo de um limite? O trabalho assíncrono está parado? Há mensagens que falham repetidamente? O sistema recuperou o atraso depois de uma instabilidade? A literatura de SRE do Google destaca sinais como latência, tráfego, erros e saturação para orientar o monitoramento de serviços.
Para a aplicação, acompanhe taxa de erros, latência e volume de requisições. Para o banco, acompanhe erros de conexão, latência de consultas, conexões ativas, bloqueios quando disponíveis e capacidade. Para a fila, os indicadores mais úteis normalmente são profundidade, idade da mensagem mais antiga, taxa de publicação, taxa de consumo, tentativas e quantidade na fila de falhas. A idade da mensagem mais antiga é especialmente valiosa: uma fila pequena pode esconder um consumidor parado há muito tempo.
Use logs estruturados com identificadores de correlação. Um pedido, uma solicitação HTTP, o evento da outbox e a mensagem na fila devem carregar referências que permitam acompanhar a mesma operação entre componentes. Registre erros com contexto suficiente para investigar, mas sem gravar senhas, tokens ou dados pessoais desnecessários. Métricas mostram que há um problema; logs e rastros, quando disponíveis, ajudam a descobrir por quê.
Alertas devem ser acionáveis. “CPU alta” isoladamente pode gerar ruído; “fila sem consumo por 15 minutos e idade máxima acima do limite” aponta uma condição e uma prioridade. Para cada alerta importante, mantenha um runbook curto: como confirmar o impacto, onde consultar dados, qual ação é segura, quando escalar e como verificar a recuperação. Um alerta sem procedimento tende a atrasar a resposta justamente quando a equipe está sob pressão.
Planeje recuperação, reconciliação e testes
A recuperação de um sistema com banco e filas raramente é só reiniciar um processo. Depois de uma queda, pode haver pedidos gravados sem evento publicado, mensagens consumidas cujo efeito não foi concluído, duplicidades causadas por tentativas ou integrações externas em estado desconhecido. Por isso, inclua rotinas de reconciliação no desenho: consultas e jobs capazes de comparar o estado esperado no banco com o resultado dos processamentos.
Um exemplo é buscar pedidos confirmados que ainda não possuem o registro de notificação enviada ou de integração concluída após um intervalo razoável. Outro é comparar transações recebidas de um provedor com registros locais e encaminhar diferenças para revisão. Reconciliação não substitui processamento correto, mas limita o dano de falhas inevitáveis e fornece uma rede de segurança verificável.
Teste cenários de falha em ambiente controlado. Interrompa um consumidor durante o processamento, force timeout de dependência externa, republique uma mensagem, restaure um backup e simule uma fila indisponível. O objetivo não é provar que o sistema nunca falha, e sim confirmar que a falha é detectada, que o efeito final permanece correto e que a recuperação cabe na capacidade da equipe.
Mantenha o procedimento de incidente proporcional ao sistema. Para um serviço pequeno, pode conter contatos, acesso ao painel do banco, como pausar consumidores, como verificar filas de falha, como restaurar em ambiente isolado e como comunicar degradação. Atualize o documento após incidentes e exercícios. A confiabilidade amadurece quando conhecimento deixa de estar apenas na memória de uma pessoa.
Uma sequência prática de adoção
Não é preciso implantar tudo de uma vez. Primeiro, identifique os dados críticos e ative backups com retenção compatível com o RPO. Em seguida, teste uma restauração e documente o tempo obtido. Depois, separe efeitos lentos ou dependentes de terceiros em uma fila, criando consumidores idempotentes e uma fila de falhas. Só então avance para outbox, reconciliação e alertas mais específicos conforme o volume e o impacto justificarem.
Uma base mínima confiável para muitos sistemas pequenos inclui: um banco gerenciado com backups testados; transações e restrições para dados críticos; uma fila para tarefas assíncronas; consumidores idempotentes; tentativas limitadas; monitoramento de erros, capacidade e idade da fila; e runbooks curtos. Essa base não torna o sistema invulnerável, mas reduz falhas silenciosas e torna a recuperação repetível.
A pergunta certa não é “qual arquitetura parece mais avançada?”, mas “qual falha é mais provável, qual dano ela causa e como esta equipe vai detectar e reparar o problema?”. Ao responder isso para banco de dados e filas, a reliability em bancos de dados e filas deixa de ser uma meta abstrata e passa a ser uma prática diária de projeto e operação.