Vários sistemas que precisam de funcionar em conjunto, mesmo quando um deles falha.
Concebemos e operamos plataformas de vários serviços, com barramento de mensagens, novas tentativas limitadas, chaves de idempotência, circuit breakers e publicação com reversão automática. Cada padrão abaixo corre num sistema próprio, não num diagrama.
Já construídoOperamos quatro plataformas próprias com arquiteturas diferentes e podemos abrir todas. Os números são medidos hoje com comandos, não retirados da documentação: aichat tem 28 diretórios de serviço, 235 modelos de dados em quatro esquemas e uma malha de mensagens com 297 filas e 17.525 ligações; Kallina tem 472 funções edge, 559 migrações e 28 tarefas agendadas na base; MEGA CRM tem um backend de 9.516 linhas com 99 rotas e 179 políticas de acesso em linha; Taskin corre 21 tarefas agendadas sem qualquer Docker. A reserva que dizemos nós: em dois lugares a documentação própria ficou atrás do código — uma afirmação sobre o número de linhas estava desatualizada em 16%, e um diagrama de infraestrutura descrevia um servidor em que já não corremos nada. Usámos a medição, não o documento.
«Arquitetura complexa» não significa muitas caixas num desenho. Significa que você sabe o que acontece quando uma delas não responde. Um sistema com três serviços que se chamam de forma síncrona, sem limite de novas tentativas e sem idempotência, é mais frágil do que um monólito — porque cada nova ligação é uma nova forma de falha, e as formas de falha multiplicam-se mais depressa do que as funcionalidades.
O nosso trabalho parte da separação de responsabilidades e termina no que se vê quando algo cede. Numa das plataformas próprias, as mensagens não passam diretamente entre serviços, mas por um barramento com 297 filas, 9 exchanges e 17.525 ligações, das quais 278 filas são geradas por cliente, não escritas à mão. Oito filas têm exchange de dead letter e oito têm tempo de vida da mensagem de cinco minutos — ou seja, uma mensagem que não pode ser processada chega a um sítio onde pode ser vista, não desaparece.
Os padrões que implementamos são poucos e repetem-se: novas tentativas com limite escrito em código, chaves de idempotência que tornam inofensiva a segunda entrega do mesmo evento, um interruptor que pára os pedidos a um serviço que falha repetidamente, um gate de consumo antes das ações que custam dinheiro, e alertas com limiar de repetição para que uma avaria intermitente não enterre o resto.
A última parte, a que se observa: a publicação. Uma das plataformas é publicada por sincronização de ficheiros, sem Docker, com a verificação de que a página servida contém exatamente o identificador do pacote acabado de publicar — não apenas que o servidor responde 200 — e com reversão automática para a versão anterior se a verificação falhar. A regra surgiu depois de um incidente real em que uma verificação superficial anulou uma publicação boa.
Qué incluye
El trabajo, por componentes
Separamos os serviços pelo que pode estragar-se de forma independente
Numa plataforma própria existem 28 diretórios de serviço, dos quais 16 têm ponto de entrada próprio, e em produção correm cinco processos sob um gestor de processos. Cada canal de comunicação tem o seu próprio serviço e a sua própria porta, precisamente para que uma falha num deles não pare o resto. Os dados são modelados em quatro esquemas separados, com 235 modelos no total — a separação não é apenas ao nível do processo, é também ao nível do esquema.
Colocamos um barramento de mensagens, não chamadas síncronas em cadeia
297 filas, 9 exchanges, 17.525 ligações. A nomeação das filas é por cliente e, em alguns casos, por tópico de conversa — ou seja, a topologia é gerada, não escrita à mão. Existe também uma exchange com entrega atrasada, para coisas que precisam de acontecer mais tarde, não agora. Oito filas têm exchange de dead letter configurada e oito têm tempo de vida da mensagem de cinco minutos.
Novas tentativas com limite, não até ao infinito
Três limites diferentes para três situações diferentes, todos escritos em código: cinco novas tentativas para mensagens agendadas, com o contador persistido, não mantido em memória; três novas tentativas ao nível da fila, contadas num cabeçalho da mensagem; e três novas tentativas com aumento exponencial da pausa para o envio de e-mail, em dois caminhos de fornecedor. Uma nova tentativa sem limite não é resiliência, é um ciclo.
Chaves de idempotência, para que a segunda entrega não estrague nada
Para eventos de terceiros usamos uma inserção que falha se o evento já tiver sido visto: a violação de unicidade da base de dados é tratada como «duplicado», não como erro, juntamente com uma verificação de frescura do momento. Para pagamentos, a chave é uma marca na transação de crédito, por isso o reenvio do mesmo webhook não credita duas vezes. Para mensagens, a deduplicação é feita por identificador, com tempo de vida e limpeza.
Um interruptor de circuito na integração frágil
A integração que depende da sessão de um site de terceiros tem um interruptor real, não apenas uma nota na documentação: após dez falhas consecutivas, os pedidos param durante dois minutos. Sem ele, um site que não responde transforma um canal em falha numa plataforma lenta, porque toda a gente espera no pedido.
Gate de consumo antes das ações que custam
Numa das plataformas, qualquer ação paga passa por uma função de verificação em quatro etapas, cada uma com o seu próprio motivo devolvido em claro: conta suspensa, limite mensal de conversas atingido, teto diário de despesa ultrapassado, saldo insuficiente. Em caso de exceção, devolve «não permitido», não «permitido» — a gate fecha-se, não se abre, quando algo corre mal. Por trás está um registo de eventos de consumo com custo por evento, discriminado por fornecedor, modelo e modalidade.
Limitação de taxa que sobrevive ao reinício
Janela deslizante mantida na base de dados, com um mapa em memória apenas como atalho dentro de uma invocação. O motivo está escrito logo no início do ficheiro: os processos que servem os pedidos são de curta duração, portanto a sua memória não pode ser a fonte de verdade. Quinze funções usam-na, e a quota de um fornecedor externo tem o seu próprio limitador, separado.
Observabilidade que distingue «respondeu» de «funcionou»
Uma resposta 200 não significa que a tarefa tenha tido sucesso. A embalagem com que executamos as tarefas agendadas percorre recursivamente a resposta à procura de listas de falhas ou erros e trata «200 com falhas» como um resultado distinto. Os alertas têm limiar de repetição — dez minutos num sistema, duas horas noutro — e a marca é apagada no primeiro sucesso, para que uma avaria recorrente possa voltar a alertar de imediato.
Publicação com verificação e reversão automática
A publicação verifica não só que o endereço responde 200, mas que a página servida contém exatamente o identificador do pacote acabado de ser publicado; caso contrário, repõe a versão anterior. Para o serviço de fundo, a publicação compara a árvore por somas de controlo e salta o reinício se nada tiver mudado — um reinício inútil mata um loop em execução.
Qué aspecto tiene
El recorrido, paso a paso.
01
Desenhamos o mapa dos modos de falha, não das caixas
Para cada ligação entre dois sistemas: o que acontece se o outro estiver lento, se estiver em baixo, se responder duas vezes, se responder de forma errada. Entregamos: a lista das ligações com o comportamento esperado em cada uma das quatro situações, mais a decisão síncrona/assíncrona para cada uma.
02
Colocamos o barramento e os contratos de mensagem
Filas, exchanges, cartas mortas, tempo de vida por mensagem, e a chave de idempotência para cada tipo de evento. Entregamos: a topologia, os contratos de mensagem e o comportamento documentado na reentrega.
03
Adicionamos as gates: taxa, consumo, interruptor
A limitação de taxa numa janela deslizante na base, a gate de consumo antes das ações que têm custo e o interruptor nas integrações fora do nosso controlo. Entregamos: os limiares definidos, os motivos devolvidos em claro e os testes para cada etapa de recusa.
04
Construímos a publicação e a reversão
Publicação com verificação de conteúdo, não apenas de código de resposta, versão anterior mantida ao lado e reversão automática. Entregamos: o procedimento de publicação, o procedimento de reversão exercitado pelo menos uma vez, e as sondas de saúde — uma superficial para «vive», uma profunda para «está mesmo a funcionar».
05
Entregamos com a documentação verificada face ao código
O documento é verificado face à medição antes da entrega, porque a documentação que fica para trás é mais perigosa do que a sua ausência. Entregamos: o diagrama, os procedimentos e a lista explícita dos pontos onde o documento e o código foram encontrados em desacordo, com o que corrigimos.
Os sistemas não são chamados em cadeia: entre eles existe uma barra de mensagens com filas por cliente, trocas separadas por tipo de tráfego e cartas mortas para o que não pode ser processado. Em torno dela, os gates — limitação de taxa, gate de consumo, disjuntor — e as sondas de saúde, uma superficial e outra profunda.
Os dados
Qué tocamos, dónde están y cuánto se quedan
Las preguntas que hace cualquiera que tenga un delegado de protección de datos — hechas aquí antes de que las haga él.
Onde estão os dados, por plataforma
Não existe uma resposta única e isso é bom: uma plataforma usa MySQL no seu próprio servidor, duas usam PostgreSQL através de Supabase — uma alojada, uma instalada na nossa infraestrutura —, e uma guarda o conteúdo como ficheiros estáticos, sem base de dados nenhuma. A escolha é feita com base nos requisitos, não no hábito.
O acesso aplica-se na base, não só na aplicação
Num dos nossos sistemas internos, a segurança ao nível da linha está ativa em 43 tabelas, através de 90 instruções, com 179 políticas escritas. A regra que seguimos: se uma chamada contorna a aplicação e atinge a base diretamente, ainda assim não deve ver as linhas de outra pessoa.
Registo de consumo
Uma linha por evento com tipo, fornecedor, quantidades separadas por modalidade — segundos de chamada, tokens de entrada, tokens de saída, caracteres —, mais custo em moeda com seis casas decimais e os créditos subtraídos. Daqui saem o teto diário de despesa, o custo por chamada e a projeção de consumo.
As migrações são o histórico, não a documentação
559 migrações numa plataforma, 80 noutra. O esquema muda através de migrações versionadas, por isso o estado da base pode ser reconstruído e lido cronologicamente. Quando o documento e a migração não concordam, a migração tem razão.
As tarefas agendadas ficam na base ou no cron, não no processo
28 tarefas agendadas correm no interior da base de dados numa plataforma; noutra, 21 tarefas correm através do cron do sistema. O motivo está escrito no ficheiro: os cronómetros dentro de um serviço reiniciam a cada reinício, por isso um ciclo de três horas num serviço que é republicado nunca se ativa.
Un caso
Uma topologia de mensagens que se gera sozinha para cada cliente
A situação
Uma plataforma com vários clientes, cada um com os seus próprios canais de comunicação, os seus próprios tópicos de conversa e as suas próprias notificações. A variante ingénua — uma fila comum e um filtro pelo identificador do cliente — faz com que um cliente com grande volume bloqueie os restantes, e um erro num tópico pare a fila para todos.
Qué construimos
A topologia é gerada por cliente, não escrita à mão: de 297 filas, 278 são do tipo «notificações para uma determinada conta», e algumas descem até ao nível de um único tópico de conversa. Por cima delas, nove exchanges separam os tipos de tráfego — notificações, tokens, webhooks, canais — e existe um exchange com entrega atrasada para o que tem de acontecer mais tarde. Oito filas têm exchange de mensagens mortas, oito têm tempo de vida por mensagem de cinco minutos. As regras de orquestração podem ser recarregadas a quente, através de um canal de publicação, sem reiniciar os consumidores.
Qué salió
Um cliente com grande volume não atrasa os outros clientes, e uma mensagem que não pode ser processada vai para um lugar onde pode ser vista e relançada, em vez de desaparecer ou bloquear a fila. O número de ligações no barramento — 17.525 — mostra exatamente porque a topologia tem de ser gerada: ninguém mantém isto manualmente.
Qué no dice el caso
O preço é operacional: um barramento desta dimensão precisa da sua própria monitorização e de um plano para as filas abandonadas, caso contrário cresce indefinidamente. E a geração por cliente pressupõe que a eliminação de um cliente elimina também a sua topologia — se essa etapa faltar, acumulam-se filas mortas.
Perguntas
Lo que nos pregunta la gente antes de llamar
Como sei que não me estão a vender complexidade de que não preciso?
Porque a primeira recomendação que damos muitas vezes é não separar. Cada novo serviço é um novo modo de falha, e os modos de falha multiplicam-se mais depressa do que as funcionalidades. Um exemplo dos nossos próprios sistemas: o backend de um deles é um único ficheiro de 9.516 linhas com 99 rotas. Não é elegante e dizemo-lo, mas é publicado numa só etapa e depurado num só lugar. A separação faz-se quando existe um motivo mensurável — um serviço que tem de escalar separadamente, uma equipa separada, um ritmo de entrega separado.
O que acontece quando um sistema na cadeia não responde?
Depende do que decidimos em conjunto na fase de mapeamento, e é essa a ideia. Nos nossos sistemas: a mensagem entra na fila e é reenviada um número limitado de vezes, depois chega à troca de cartas mortas, onde pode ser vista; a integração instável tem um interruptor que, após dez falhas consecutivas, pára durante dois minutos em vez de manter os pedidos em espera; e as ações que custam dinheiro são bloqueadas por uma gate que, em exceção, recusa, não permite.
Como evitam que o mesmo evento seja processado duas vezes?
Com uma chave de idempotência, não com uma verificação “já vi isto?” que tem race condition. Concretamente: inserimos uma linha com o identificador do evento e, se a base de dados recusar por violação de unicidade, tratamos esse código de erro como sinal de duplicado, não como falha. Para pagamentos, o marcador fica na transação de crédito, por isso o reenvio do mesmo webhook não credita duas vezes.
Usam Docker ou não?
Ambos, e a escolha é justificada sempre. Um sistema corre em contentor, mas sem volumes montados, o que significa que a publicação é uma cópia para o contentor mais reinício — um `docker rm` ali perde o estado, e isso está escrito no documento de operação exatamente assim. Outra plataforma não tem qualquer Docker: a publicação é sincronização de ficheiros, e os scripts de publicação são instalados manualmente com privilégios de administrador e expostos ao fluxo automático por exatamente dois caminhos permitidos. O motivo está escrito no código: um script que o fluxo pode reescrever é um script pelo qual o fluxo pode escalar.
Como sabe que uma tarefa agendada foi mesmo executada?
Por experiência negativa. Um relatório diário nosso esteve morto durante onze dias, entre 7 e 17 de agosto, enquanto a tarefa agendada era acionada todos os dias — o comando usado saía em silêncio em caso de erro e não escrevia nada. Desde então, cada tarefa corre num invólucro que percorre a resposta por listas de falhas, trata “200 com falhas” como resultado distinto, tem tempo máximo de execução e escreve uma linha estruturada por execução. Os alertas têm limiar de repetição, e o marcador é removido na primeira reússita.
Quanto importa a abstração do fornecedor?
Muito, se o fornecedor estiver numa área que se move depressa. Para voz temos três pontes separadas — uma para cada fornecedor de fala em tempo real — que implementam a mesma interface. A ponte faz a conversão de áudio entre a central telefónica e o fornecedor, numa porta dedicada, com o servidor de saúde ligado apenas à interface local, não exposta. A mudança de fornecedor torna-se uma decisão, não uma reescrita.
A vossa documentação está atualizada?
Não em todo o lado, e preferimos dizer onde. Na preparação desta página medimos dois sistemas e encontrámos o documento próprio em atraso: uma afirmação sobre o tamanho de um ficheiro estava cerca de 16% abaixo da realidade, e um diagrama de infraestrutura descrevia um servidor de onde já tínhamos mudado. A regra que aplicamos e que pedimos nos projetos: quando o documento e a medição não concordam, a medição tem razão, e o documento é corrigido no mesmo passo.
O que não fazem?
Não prometemos objetivos de disponibilidade sem medição — uma percentagem do tipo “99,9%” exige dados de terreno ao longo de um período, e onde não os temos não os afirmamos. Não projetamos sistemas que não possamos operar ou entregar: se o resultado é uma arquitetura que a vossa equipa não consegue manter, é uma arquitetura errada. E não fazemos certificações de conformidade — podemos construir os controlos, não podemos emitir o certificado.
Em que se baseiam as afirmações acima (23 fontes)
23 delas são código e ficheiros dos nossos repositórios. Não publicamos o nome nem a linha: juntos, numa única página, descreveriam com demasiada precisão como estão construídos sistemas que não são só nossos. Percorremo-los consigo, no repositório, a pedido — a verificação continua possível, só que se faz numa conversa.