Em um mundo onde a comunicação digital se tornou essencial, plataformas como o Discord estão desafiando os limites da escalabilidade e performance. Recentemente, a equipe de engenharia do Discord revelou uma solução inovadora para um problema que muitos desenvolvedores enfrentam: como implementar tracing distribuído em arquiteturas baseadas em actors sem sacrificar a performance. Eles introduziram uma biblioteca chamada "Transport", que envolve o sistema de mensagens do Elixir com o contexto de rastreamento, permitindo uma visibilidade de ponta a ponta em sua infraestrutura de chat.
O Desafio do Tracing em Arquiteturas de Actors
A arquitetura de actor do Elixir, embora poderosa, apresenta um desafiu único em termos de rastreamento. Diferente dos microserviços baseados em HTTP, onde o contexto de rastreamento viaja em cabeçalhos, o Elixir passa mensagens arbitrárias entre processos sem uma camada de metadados embutida. Isso significa que o OpenTelemetry, uma solução padrão para rastreamento, não conseguia propagar o contexto entre os processos do Elixir. Para resolver esse dilema, o time do Discord identificou três requisitos cruciais: a solução precisava ser ergonômica, suportar tanto mensagens brutas quanto abstrações do GenServer, e permitir implementações sem downtime.
A Solução do Envelope
A solução encontrada foi a introdução de um primitvo chamado "Envelope", que envolve mensagens com o contexto de rastreamento. A implementação é simples, mas eficaz, consistindo em uma estrutura que contém a menssagen original e um portador de rastreamento serializado. Abaixo, um exemplo de como isso é feito:
defmodule Discord.Transport.Envelope do
defstruct [:message, trace_carrier: []]
def wrap_message(message) do
%__MODULE__{
message: message,
trace_carrier: :otel_propagator_text_map.inject([])
}
end
end
Essa biblioteca substitui as funções de chamada e envio do GenServer, automaticamente envolvendo as mensagens de saída. E na recepção, uma função handle_message normaliza tanto as mensagens tradicionais quanto as novas mensagens embrulhadas no Envelope, extraindo o contexto de rastreamento quando presente.
Dicas para Implementação Eficiente
- Gradualidade é chave: A transição não precisa ser abrupta. O Discord conseguiu lidar com mensagens de código não instrumentado e instrumentado ao mesmo tempo, permitindo uma migração suave.
- Sampling Dinâmico: Ao lidar com fanout, mensagens enviadas para um único destinatário mantêm 100% de decisão de amostragem. Já mensagens enviadas para 100 destinatários caem para 10%, e assim por diante. Essa estratégia evita sobrecarregar a infraestrutura de observabilidade.
- Otimização de Contexto: Apenas propague o contexto de rastreamento para operações amostradas. Isso economiza tempo de CPU e evita picos indesejados.
Reflexões Finais
A implementação do tracing distribuído pelo Discord em seu sistema baseado em Elixir é uma verdadeira aula de como é possível inovar sem perder a essência da arquitetura. Ao invés de tentar adaptar uma solução HTTP, eles encontraram uma maneira de manter a força de sua arquitetura enquanto ganhavam a visibilidade necessária. Isso não só melhorou a capacidade de resposta em incidentes, como também trouxe uma nova camada de insights sobre o comportamento dos usuários.
Se você trabalha com arquiteturas de actors, considere como a implementação de um sistema de rastreamento similar poderia beneficiar sua aplicação. Nunca subestime o poder de ter visibilidade — é um divisor de águas quando se trata de resolver problemas complexos.