Desenhando um único caminho de computação para uma métrica entre serviços
Dois serviços calculavam de forma independente a mesma métrica voltada ao cliente, causando dados inconsistentes. Redesenhei a arquitetura para que só um serviço fosse dono do cálculo enquanto todo outro serviço simplesmente sinalizava dados desatualizados.
Contexto
A plataforma da Dynamox deriva uma métrica de conclusão para rotas de inspeção industrial: quanto de uma rota foi inspecionado, por ativo, por cliente. A métrica é voltada ao cliente, aparecendo em dashboards que operadores usam para planejar trabalho.
Os fatos subjacentes vivem em eventos. Inspeções, edições e exclusões fluem através do Kafka para múltiplos serviços, cada um mantendo sua própria projeção dos dados.
Problema
Dois serviços computavam a métrica de forma independente, cada um a partir de sua própria projeção. Em condições normais, concordavam. Sob retries, entrega fora de ordem ou falhas parciais, divergiam, e clientes viam dois números diferentes para a mesma rota dependendo de qual tela abriam.
Cada divergência virava um ticket de suporte, uma reconciliação manual, e uma pequena perda de confiança do cliente. O time tinha construído um job de reconciliação para corrigir diferenças, o que tratava o sintoma e adicionava um terceiro componente que podia discordar.
03Restrições
04Alternativas Consideradas
Decisão
Exatamente um serviço é dono da métrica. É o único caminho de código na empresa autorizado a computá-la.
Todo outro serviço que toca dados subjacentes para de computar qualquer coisa. Em vez disso, emite um sinal leve de 'desatualizado': 'a rota X pode ter mudado'. O dono recomputa a métrica a partir da fonte da verdade, de forma idempotente, sempre que um sinal chega.
Foi aqui que um princípio pessoal se cristalizou: dados derivados eventualmente consistentes deveriam ter exatamente um caminho de computação. E seu corolário: se um valor sempre pode ser recomputado a partir da fonte, prefira recomputação a sincronização.
06Trade-offs
07Implementação
Sinais de desatualização viajam por um tópico Kafka dedicado, com chave por rota, então recomputações para a mesma rota serializam naturalmente. O dono consome com handlers idempotentes, então recomputar duas vezes é seguro por construção e retries não precisam de tratamento especial.
Um script de backfill recomputou cada métrica histórica a partir da fonte, migrando o sistema para uma baseline consistente antes do novo caminho ir ao ar. O cálculo antigo no segundo serviço foi deletado em vez de desativado, porque deixá-lo dormente convidava a uma ressurreição.
Impacto
09Lições Aprendidas
Debates de consistência terminam quando o ownership é explícito. A maior parte do trabalho de design foi conseguir concordância sobre a frase 'só este serviço computa este valor', não escrever código.
Deletar código é parte da arquitetura. A migração não estava concluída até que o segundo caminho de computação deixasse de existir fisicamente.
10Evidência
Este documento é a evidência primária das capacidades que reivindica: Sistemas Distribuídos (design de consistência orientado a eventos), Arquitetura (fronteiras de ownership), Senso de Dono (proposta até backfill até exclusão), Comunicação (a decisão sobreviveu porque foi escrita e acordada entre dois donos de serviço).