Papers
Scaling your API with rate limiters
Paul Tarjan · Stripe Engineering Blog, 2017
O problema: proteger uma API compartilhada sem punir quem se comporta bem
O texto, publicado no blog de engenharia da Stripe, descreve como a empresa usa limitadores de taxa e mecanismos de load shedding para manter a API disponível para todos os usuários. As primeiras vezes em que o tráfego dispara, é razoável apenas adicionar capacidade para acomodar o crescimento de usuários; depois, uma API em produção precisa garantir que um único agente problemático, por acidente ou de propósito, não afete a disponibilidade dos demais. O autor lista cenários em que limitar ajuda: um usuário causa um pico de tráfego, um script com defeito envia requisições demais, um usuário manda muitas requisições de baixa prioridade que poderiam atrapalhar as críticas, ou algo falha internamente e é preciso descartar tráfego de baixa prioridade. Um rate limiter controla a velocidade do tráfego e é apropriado quando o cliente pode mudar o ritmo das chamadas sem alterar o resultado; se espaçar as requisições não é possível (tipicamente eventos em tempo real), o autor diz que na maioria das vezes o que falta é mais capacidade de infraestrutura. O artigo distingue ainda o load shedder: durante incidentes, ele descarta requisições de baixa prioridade e decide com base no estado do sistema inteiro, não no usuário que fez a chamada. A Stripe opera quatro tipos de limitadores em produção e recomenda começar pelo limitador de taxa de requisições.
Os dois primeiros limitadores: taxa de requisições e requisições concorrentes
O limitador de taxa de requisições restringe cada usuário a N requisições por segundo. O autor o chama de longe o mais importante e conta que ele dispara constantemente, tendo rejeitado milhões de requisições em um único mês, sobretudo em modo de teste, quando um script sai do controle. Os modos de teste e produção têm o mesmo comportamento de limitação, para que um script não encontre efeitos colaterais ao passar para produção. Depois de analisar o tráfego, a Stripe adicionou a possibilidade de estourar brevemente o teto em picos súbitos durante eventos em tempo real, como uma promoção relâmpago. O segundo é o limitador de requisições concorrentes, que em vez de limitar chamadas por segundo diz algo como: só pode haver 20 requisições em andamento ao mesmo tempo. Alguns endpoints consomem muito mais recursos que outros, e usuários frustrados com a espera tentam de novo, somando demanda a um recurso já sobrecarregado. Esse limitador dispara bem menos (12 mil requisições no mês citado), mas, segundo o autor, resolveu por completo a contenção de recursos nos endpoints mais caros. Ele pede um modelo de programação diferente do outro: disparar X tarefas que consomem uma fila, em vez de martelar a API e recuar ao receber HTTP 429. Cada API pode escolher o padrão que encaixa melhor com seus usuários.
Os load shedders: uso da frota e utilização dos workers
Os outros dois mecanismos decidem com base no estado do sistema, não no usuário. O load shedder de uso da frota garante que uma parcela da frota fique sempre disponível para as requisições mais importantes. A Stripe divide o tráfego em métodos críticos (o exemplo é criar cobranças) e não críticos (o exemplo é listar cobranças); um cluster Redis conta quantas requisições de cada tipo existem no momento. Com uma reserva de 20%, qualquer requisição não crítica acima da alocação de 80% é rejeitada com status 503. O autor diz que ele disparou para uma fração muito pequena das requisições no mês citado, mas que em outros meses evitou quedas. O load shedder de utilização dos workers é a última linha de defesa: se os workers começam a acumular requisições, ele passa a descartar tráfego de menor prioridade. O tráfego é dividido em quatro categorias: métodos críticos, POSTs, GETs e tráfego de modo de teste. A Stripe acompanha quantos workers têm capacidade livre; se uma máquina está ocupada demais, começa a descartar devagar o tráfego menos crítico, a começar pelo de teste, e escala para mais descarte se isso não bastar. Descartar e readmitir carga precisa ser lento, para não haver oscilação (flapping). Só cerca de 100 requisições foram rejeitadas por ele no mês citado; ele limita o impacto de incidentes já em curso, enquanto os três primeiros são mais preventivos.
Implementação e lançamento: token bucket, fail open, kill switches e dark launch
Para o algoritmo, a Stripe usa o token bucket: há um host central de baldes de onde se retira um token a cada requisição, enquanto novos tokens são repostos aos poucos; se o balde está vazio, a requisição é rejeitada. Cada usuário tem seu próprio balde. A implementação usa Redis, que pode ser operado pela própria equipe ou, na AWS, via serviço gerenciado como o ElastiCache. O texto dá quatro cuidados práticos. Primeiro, encaixar os limitadores no middleware com segurança: se houver bug no código de limitação ou o Redis cair, as requisições não podem ser afetadas, o que significa capturar exceções em todos os níveis para falhar aberto. Segundo, mostrar erros claros: decidir entre HTTP 429 (Too Many Requests) e HTTP 503 (Service Unavailable) conforme o que for mais preciso na situação, com mensagem acionável. Terceiro, ter kill switches e feature flags para desligar limitadores que disparem por engano, além de alertas e métricas sobre a frequência de disparo. Quarto, fazer um dark launch de cada limitador, observando o tráfego que ele bloquearia, para avaliar se a decisão está certa e ajustar os limiares sem afetar os padrões de uso existentes; o autor admite que isso pode exigir trabalhar com alguns usuários para mudarem o código deles. A conclusão é que nem todos os limitadores são necessários no primeiro dia e podem ser introduzidos gradualmente.
Veja as ideias em ação
Pratique na escola de System Design os desenhos que usam este paper.