Arquitetura real · Sênior
Fury Streams: distribuir permits para não derrubar o destino
Exercício clássico com contexto real: um destino de 100k RPM e mais de 800 instâncias efêmeras. A divisão igual de permits processa só 70k; a segunda iteração distribui por uso real.
Resposta curta
Exercício clássico com contexto real: um destino de 100k RPM e mais de 800 instâncias efêmeras. A divisão igual de permits processa só 70k; a segunda iteração distribui por uso real. O serviço de streams da plataforma Fury, uma ferramenta interna do Mercado Libre para desenvolvedores, transporta informação em tempo real de muitas origens para destinos como filas, armazenamento chave-valor…
01 · CONTEXTO
Um destino que não escala
O serviço de streams da plataforma Fury, uma ferramenta interna do Mercado Libre para desenvolvedores, transporta informação em tempo real de muitas origens para destinos como filas, armazenamento chave-valor e bancos relacionais. O relato fala de mais de 30 milhões de mensagens, vindas de mais de 12 mil origens e ligadas a mais de 15 mil destinos.
O problema nasce de um desenho comum: a aplicação processa informação e entrega o resultado a um serviço complementar, como um banco ou uma aplicação externa. Diante de um pico, ela opta por escalar horizontalmente. O desafio aparece quando o volume passa da capacidade do destino, que pode ser um banco herdado ou uma aplicação externa fora do controle do time, e então escalar o destino não é viável.
Várias instâncias da aplicação processam informação e enviam o resultado a um serviço complementar.
O destino tem uma capacidade que não dá para ampliar escalando horizontalmente.
Quando o volume somado das instâncias passa da capacidade do destino, o limite é ultrapassado, e o número de instâncias flutua o tempo todo.
A pergunta do exercício é: se o destino aguenta 100 mil requisições por minuto, como a aplicação mantém o total abaixo disso com um número de instâncias que muda? No serviço real, a entrega saía de mais de 800 instâncias que flutuavam constantemente.
Ao descrever o impacto do problema no time de Streams, o relato menciona cerca de 600 alertas nos últimos meses de 2023, cada um exigindo em média 30 minutos de atenção de uma pessoa.
02 · PRIMEIRA ITERAÇÃO
Dividir os permits por igual
Na primeira iteração, o time equilibrou a carga por instância levando em conta quantas estavam ativas, e dividiu o total de permits de forma igual. Com um destino de 100 mil requisições por minuto e dez instâncias ativas, cada uma recebia 10 mil permits.
O sistema tinha três peças: uma fila de mensagens, um banco e uma aplicação adicional que compila e guarda os dados, o worker-balancer. Cada vez que uma instância nova começava a processar, enviava um sinal de vida pela fila. O sinal era gravado no banco com um tempo de vida (TTL) limitado. Depois, a aplicação worker consultava o balancer para saber quantas instâncias estavam vivas e repartia os permits proporcionalmente.
Cada instância que começa a processar envia um sinal de vida pela fila de mensagens.
O worker-balancer compila os sinais e os grava no banco com um TTL limitado.
A aplicação worker consulta o balancer para saber quantas instâncias estão vivas e reparte os permits proporcionalmente.
Com a divisão repartida, o destino não é sobrecarregado por pedidos em excesso.
O ponto fraco é a premissa: a estratégia supunha que toda instância executaria a mesma carga de processamento, o que nem sempre acontece. O relato aponta o desempenho do load balancer e a distribuição do tráfego como fontes de disparidade, e lembra que tarefas diferentes custam tempos diferentes: uma nota fiscal com milhares de linhas leva mais tempo que uma com poucas.
O exemplo numérico do relato mostra o efeito. Com 100 mil requisições por minuto e duas instâncias, A e B, cada uma recebe 50 mil. Se A só processa 20 mil e B processa 80 mil, o modelo igual deixa processar 70 mil no total, e 30 mil por minuto ficam sem processar, gerando atraso. O destino fica protegido, mas a capacidade da aplicação é subutilizada.
| Instância | Permits recebidos | Capacidade real | Processado |
|---|---|---|---|
| Instância A | 50 mil RPM | 20 mil RPM | 20 mil RPM |
| Instância B | 50 mil RPM | 80 mil RPM | 50 mil RPM |
| Total | 100 mil RPM | — | 70 mil RPM |
03 · SEGUNDA ITERAÇÃO
Reportar o uso sem atrapalhar o trabalho
Com essas conclusões, o time reestruturou a aplicação para informar a disponibilidade de cada host e detalhes do estado de carga atual de cada um. O worker-balancer foi ajustado para analisar essas informações e funcionar como centro de distribuição da carga. Entre os dados enviados estão o identificador da instância, o instante e os permits designados.
Enviar informação sincronizada durante o trabalho não é prático nem possível, porque exigiria mais cálculo e atrapalharia o processamento. O time adotou então uma abordagem parecida com a de bibliotecas de monitoramento como DataDog ou NewRelic: uma estrutura de dados em memória, com fila assíncrona, que compila, consolida e envia as informações em uma thread assíncrona.
No fluxo, os detalhes de processamento e os permits se acumulam em um buffer em memória; uma thread dedicada comprime e esvazia o buffer a cada minuto; e a informação comprimida é despachada para a fila de mensagens.
Cada instância acumula detalhes de processamento e permits em um buffer em memória.
Uma thread dedicada comprime e esvazia o buffer a cada minuto e despacha o conteúdo para a fila de mensagens.
O balancer recebe da fila e faz um upsert em lote no banco, gravando conforme o instante de cada tupla, o que alivia a fila e elimina a dependência da ordem das mensagens.
O balancer avalia a distribuição a cada minuto: o excedente de permits de um host que usa menos é realocado a outro host que usa todos os seus.
A distribuição avaliada é guardada de forma persistente em armazenamento de longo prazo, o Amazon S3.
Quando chega um pedido de distribuição de host, o arquivo é lido e o resultado volta ao cliente com tempo de resposta mínimo.
04 · DISTRIBUIÇÃO E DISPONIBILIDADE
Guardar proporções e servir arquivos prontos
A distribuição guarda o percentual de permits de cada host, e não um valor absoluto, o que evita reprocessar tudo quando o número de permits muda. Com 100 mil requisições por minuto divididas em 80% para o host A e 20% para o B, a proporção se mantém mesmo se o total subir para 200 mil permits, com reflexo rápido nas instâncias.
O algoritmo é simples: se um host usa menos permits do que recebeu, o excedente é realocado a outro host que usa todos os seus. A avaliação roda minuto a minuto, o que permite ajustes contínuos até chegar à distribuição ideal.
Consultar os permits de cada instância a cada pedido seria lento, então o processamento da distribuição ocorre a cada minuto e o resultado é guardado no Amazon S3. Quando um pedido chega, o arquivo é lido e devolvido com tempo de resposta mínimo.
Na persistência, o balancer recebe os dados da fila e faz um UPSERT em lote, gravando só se o instante da tupla for menor que o do novo elemento. Isso alivia a fila e elimina a dependência da ordem das mensagens. Os elementos têm um TTL de três minutos, útil por exemplo quando um host para.
| Aspecto | Primeira iteração | Segunda iteração |
|---|---|---|
| O que a instância informa | Um sinal de que está viva | Identificador, instante e permits designados |
| Como os permits são repartidos | Por igual entre as instâncias vivas | O excedente de quem usa menos vai a quem usa tudo |
| Premissa sobre a carga | Supõe a mesma carga em toda instância | Distribuição reavaliada minuto a minuto |
05 · RESULTADO E LIMITES
O que mudou e o que levar para a entrevista
O relato apresenta a reestruturação como resultado de várias iterações, com uma queda de 95% nos alertas de lag e um ambiente mais amigável para quem desenvolve na plataforma.
O serviço Fury Streams aparece também em outro artigo da equipe, sobre a migração para virtual threads. Nele, o componente pusher lê dos streams do Kinesis e entrega a vários destinos, desacoplando os consumidores de qualquer tecnologia específica, e usa milhares de threads para paralelizar consumo e entrega. Para restringir tarefas concorrentes, o artigo sugere trocar o pool de threads de tamanho limitado por um método baseado em semáforo.
Os dois artigos tratam de limites em níveis diferentes: o semáforo restringe concorrência dentro de um processo, enquanto os permits distribuídos limitam a vazão contra o destino somando todas as instâncias.
Perguntas frequentes
O que é Fury Streams: distribuir permits para não derrubar o destino?
Exercício clássico com contexto real: um destino de 100k RPM e mais de 800 instâncias efêmeras. A divisão igual de permits processa só 70k; a segunda iteração distribui por uso real.
O que o design de Fury Streams: distribuir permits para não derrubar o destino cobre?
Ele está organizado nestas seções: Um destino que não escala, Dividir os permits por igual, Reportar o uso sem atrapalhar o trabalho, Guardar proporções e servir arquivos prontos e O que mudou e o que levar para a entrevista.
Quanto tempo leva Fury Streams: distribuir permits para não derrubar o destino e qual é o nível?
Leva cerca de 17 minutos para ler e tem nível Sênior.
Agora projete você mesmo
Rode este caso como uma entrevista na escola de System Design: você toma as decisões, o tutor as questiona e você vê a nota do seu desenho.