Removendo um Gargalo: Uma Ordem de Leitura
Entre 29 de agosto e 4 de setembro levei um serviço de notificações push por um incidente, uma mitigação rejeitada, uma decisão de arquitetura, um deploy em sombra e um cutover em produção. Estes oito posts saíram disso. Foram escritos conforme o trabalho acontecia, então cada um começa de onde as coisas tinham chegado.
Este é o começo.
O serviço ingere eventos esportivos por gRPC, deduplica, e envia notificações push para cerca de 5.000 dispositivos. Ele fazia isso criando um tópico AWS SNS por partida, por tipo de notificação, por idioma, e inscrevendo os endpoints dos dispositivos naqueles tópicos. Escrevi sobre construí-lo em dezembro, e sobre reescrever o antecessor em Python antes disso.
O design colocou duas APIs do SNS no caminho crítico. Publish tem cota de conta de 30.000 por segundo e carregava cerca de 1,2 eventos por segundo. Subscribe tem cota de 100 por segundo, documentada como não ajustável, e carregava todo o crescimento e toda a rotatividade do sistema. Em 29 de agosto uma importação em massa upstream criou cerca de 32.000 tópicos em nove horas, cada um querendo cerca de 2.400 inscrições de dispositivo, e aquela assimetria de 300 vezes virou um incidente de 48 horas.
A decisão que veio depois foi parar de andar devagar embaixo do teto e removê-lo do design: consultar o nosso próprio Postgres no momento da publicação e enviar direto para o Firebase, para que Subscribe deixe de aparecer na arquitetura. Isso entrou em estágios entre 1 e 4 de setembro, e o throttling que ele existia para eliminar caiu por um fator de cerca de 500.
O incidente
- O Teto Que Você Não Pode Levantar: como uma chamada throttled que não escreve linha nenhuma se transforma numa espiral que não se recupera sozinha, e por que ninguém percebeu por dois dias enquanto a task registrava
status: successatravés de 50.000 inscrições falhas por 20 minutos. Comece aqui. - Concorrência Não É Throughput: um comentário afirmando que 50 workers deixavam folga sob uma cota de 100 por segundo, quando 50 chamadas concorrentes a 30ms cada dão cerca de 1.600. Também por que o rate limiter que já existia no código não poderia ter limitado nada, e onde um limiter precisa ser cobrado para que os retries internos do SDK sejam contados.
A decisão
- Remova o Gargalo, Não Ande Devagar Embaixo Dele: quatro opções, e por que a pergunta que eu levei (Kafka ou RabbitMQ) era a errada: o SNS fazia três trabalhos separáveis e um broker substitui exatamente um. Inclui por que Kafka é a resposta errada a uma mensagem por segundo.
- A Mitigação Que Especificamos e Jogamos Fora: um orçamento de taxa com prioridade, especificado, levado até o checkpoint de aprovação e deliberadamente abandonado, porque três das quatro funções que ele protegia são apagadas pela correção real.
A migração
- O Que uma Sombra Mede: rodar o fan-out novo ao lado do antigo não é um teste simétrico: os dois lados leem tabelas diferentes escritas em momentos diferentes, então a divergência é a medição. Sobre um veredito assimétrico, um instrumento que nunca roda, e fazer cada ausência explicar a si mesma.
- A Cauda É Uma Requisição Travada: produção discordou do modelo nas duas direções. O maior fan-out foi 1.023 destinatários e não 308, e os dois fan-outs mais lentos não foram os maiores. Por que o timeout ficou como estava.
- A Metade do Cutover Que Ninguém Agenda: a entrega mudou e o provisionamento ficou, então o serviço passou um dia construindo um parque de tópicos SNS para notificações que ele nunca publicaria ali. Também uma chave de deduplicação que apontava para um recurso a ponto de deixar de existir.
O registro
- Um Registro de Decisão Que Discute Consigo Mesmo: seis correções datadas inline acima do raciocínio que elas derrubam, incluindo um estágio de migração planejado que seria código morto, e uma previsão errada numa direção útil.
Contexto anterior
Dois posts antigos cobrem o sistema que este arco substitui, e um cobre os conceitos por baixo dele:
- Construindo um Serviço de Notificações Push de Alta Performance em Go
- Reescrevendo um Serviço de Notificações Python em Go
- Como os Conceitos de Stream Processing do DDIA se Aplicam a Sistemas de Notificação em Tempo Real
Existe uma série adjacente sobre um serviço diferente na mesma empresa, extraindo assinaturas e pagamentos de um monolito Django: A Extração de Assinaturas. Os dois projetos compartilham uma forma, que é serem migrações em que as implementações antiga e nova rodam ao mesmo tempo e a engenharia interessante está em tornar a comparação confiável.