Padrão de limitação de taxa

Controle a taxa com que seu aplicativo envia solicitações a um serviço para que você fique dentro dos limites de limitação de taxa do serviço e de sua capacidade total. Essa abordagem ajuda você a evitar ou minimizar erros de limitação de taxa e a prever com mais precisão a taxa de transferência.

A limitação de taxa é apropriada em muitos cenários, mas é particularmente útil para tarefas automatizadas repetitivas e em larga escala, como processamento em lote.

Contexto e problema

Executar um grande número de operações em um serviço limitado pode resultar em maior tráfego e redução da taxa de transferência, pois você precisa rastrear solicitações rejeitadas e tentar novamente as operações. À medida que o número de operações aumenta, um limite de limitação pode exigir várias passagens de reenviamento de dados, o que resulta em um impacto maior no desempenho.

Por exemplo, considere o seguinte processo problemático de nova tentativa em caso de erro para a ingestão de dados no Azure Cosmos DB:

  1. Seu aplicativo precisa ingerir 10.000 registros no Azure Cosmos DB. Cada registro custa 10 RUs (unidades de solicitação) para ser ingerido, portanto são necessárias 100.000 RUs no total para concluir o trabalho.

  2. Sua instância do Azure Cosmos DB tem 20.000 RUs de capacidade provisionada.

  3. Você envia os 10.000 registros para o Azure Cosmos DB. 2.000 registros são gravados com êxito e 8.000 registros são rejeitados.

  4. Você envia os 8.000 registros restantes para o Azure Cosmos DB, dos quais 2.000 registros são gravados com sucesso e 6.000 são rejeitados.

  5. Você envia os 6.000 registros restantes para o Azure Cosmos DB, dos quais 2.000 registros são gravados com sucesso e 4.000 são rejeitados.

  6. Você envia os 4.000 registros restantes para o Azure Cosmos DB, dos quais 2.000 registros são gravados com sucesso e 2.000 são rejeitados.

  7. Por fim, você envia os 2.000 registros restantes ao Azure Cosmos DB. Todos os registros são gravados com êxito.

O trabalho de ingestão é concluído com êxito, mas somente depois de enviar 30.000 registros para Azure Cosmos DB. Todo o conjunto de dados consiste em apenas 10.000 registros.

Há outros fatores a serem considerados neste exemplo:

  • Um grande número de erros também pode resultar em trabalho extra para registrar esses erros e processar os dados de log resultantes. A abordagem anterior lida com 20.000 erros e registrar esses erros em log pode impor um custo de recurso de processamento, memória ou armazenamento.

  • Como você não sabe os limites de limitação do serviço de ingestão, não tem uma maneira de definir expectativas de quanto tempo leva o processamento de dados. O limite de taxa ajuda a calcular o tempo necessário para a ingestão.

Solução

A limitação de taxa pode reduzir o tráfego e, potencialmente, melhorar a taxa de transferência devido à redução do número de registros enviados a um serviço durante um determinado período de tempo.

Um serviço pode limitar solicitações com base em métricas diferentes ao longo do tempo, como:

  • O número de operações (por exemplo, 20 solicitações por segundo).
  • A quantidade de dados (por exemplo, 2 GiB por minuto).
  • O custo relativo das operações (por exemplo, 20.000 RUs por segundo).

Independentemente da métrica que você usa para limitação, sua implementação de limitação de taxa envolverá o controle do número e/ou do tamanho das operações enviadas ao serviço durante um período de tempo específico. A limitação de taxa ajuda a otimizar o uso do serviço sem exceder o limite de taxa do serviço.

Em cenários em que suas APIs podem lidar com solicitações mais rapidamente do que os serviços de ingestão limitados permitem, você deve gerenciar a rapidez com que usa o serviço. Tratar a limitação apenas como uma incompatibilidade de taxa de dados e armazenar em buffer as solicitações de ingestão até que o serviço se recupere apresenta um risco. Se o aplicativo parar de responder nesse cenário, todos os dados em buffer poderão ser perdidos.

Para evitar esse risco, considere enviar seus registros para um sistema de mensagens durável que possa lidar com sua taxa de ingestão total. (Serviços como Hubs de Eventos do Azure podem lidar com milhões de operações por segundo.) Em seguida, você pode usar um ou mais processadores de trabalho para ler os registros do sistema de mensagens a uma taxa controlada que está dentro dos limites do serviço limitado. O envio de registros para o sistema de mensagens pode ajudar a economizar a memória interna, pois permite que você desenfileire apenas os registros que podem ser processados durante um determinado intervalo de tempo.

O Azure fornece vários serviços de mensagens duráveis que podem ser usados com esse padrão, incluindo os seguintes:

Diagrama que mostra um fluxo de mensagens duráveis. Três processadores de tarefas chamam um serviço com taxa limitada.

Ao enviar registros, o intervalo de tempo utilizado para a liberação dos registros pode ser mais granular do que o período de limitação do serviço. Os sistemas geralmente definem limites com base em intervalos de tempo que você pode compreender e usar facilmente. No entanto, para o computador que executa um serviço, esses quadros de tempo podem ser muito longos em comparação com a rapidez com que ele pode processar informações. Por exemplo, um sistema pode aplicar a limitação por segundo ou minuto, mas o código normalmente está sendo processado na ordem de nanossegundos ou milissegundos.

Embora não seja necessário, geralmente é recomendável enviar um número menor de registros com mais frequência para melhorar a taxa de transferência. Portanto, em vez de tentar agrupar registros em lote para uma liberação uma vez por segundo ou uma vez por minuto, você pode trabalhar com uma granularidade maior para manter o consumo de recursos (memória, CPU e rede) distribuído de forma mais uniforme. Essa abordagem evita possíveis gargalos causados por explosões repentinas de solicitações. Por exemplo, se um serviço permitir 100 operações por segundo, a implementação de um limitador de taxa poderá empatar as solicitações liberando 20 operações a cada 200 milissegundos, conforme mostrado no grafo a seguir.

Gráfico que mostra um fluxo com taxa limitada ao longo do tempo.

Além disso, pode ocorrer de vários processos descoordenados compartilharem um serviço limitado. Para implementar a limitação de taxa nesse cenário, você pode particionar logicamente a capacidade do serviço e, em seguida, usar um sistema de exclusão mútua distribuída para gerenciar bloqueios exclusivos nessas partições. Assim, os processos descoordenados podem competir por bloqueios nessas partições sempre que precisarem de capacidade. Um processo recebe uma determinada quantidade de capacidade para cada partição em que mantém um bloqueio.

Por exemplo, se o sistema limitado permite 500 solicitações por segundo, é possível criar 20 partições com 25 solicitações por segundo cada. Se um processo precisasse emitir 100 requisições, ele poderia solicitar ao sistema de exclusão mútua distribuído quatro partições. O sistema pode conceder duas partições por 10 segundos. Então, o processo limitaria a taxa a 50 solicitações por segundo, concluiria a tarefa em dois segundos e, em seguida, liberaria o bloqueio.

Uma maneira de implementar esse padrão é usar Armazenamento do Azure. Nesse cenário, você cria um blob de 0 bytes por partição lógica em um contêiner. Seus aplicativos podem então obter arrendamentos exclusivos diretamente sobre esses blobs por um curto período de tempo (por exemplo, 15 segundos). Para cada concessão atribuída a um aplicativo, ele pode usar a capacidade dessa partição. Em seguida, o aplicativo precisa acompanhar o tempo de concessão para que, quando a hora expirar, o aplicativo possa parar de usar a capacidade concedida. Ao implementar esse padrão, você geralmente desejará que cada processo tente alugar uma partição aleatória quando precisar de capacidade.

Para reduzir ainda mais a latência, é possível alocar uma pequena quantidade de capacidade exclusiva para cada processo. Assim, um processo só buscaria uma concessão de capacidade compartilhada se fosse necessário exceder a capacidade reservada para ele.

Diagrama que mostra vários processos competindo por concessões exclusivas em partições de blob em Armazenamento de Blobs do Azure.

Como alternativa para Armazenamento do Azure, você também pode implementar esse tipo de sistema de gerenciamento de concessão usando tecnologias como ZooKeeper, etcd e Redis/Redsync.

Problemas e considerações

Considere os seguintes pontos ao decidir como implementar esse padrão:

  • Embora o padrão de limitação de taxa possa reduzir o número de erros de limitação, sua aplicação ainda precisa tratar adequadamente quaisquer erros desse tipo que possam ocorrer.

  • Verifique se as novas tentativas são coordenadas com limitação de taxa. Tentativas cegas ou excessivamente agressivas podem aumentar a carga e criar tempestades de novas tentativas; portanto, propague sinais de contrapressão (por exemplo, HTTP 429 com Retry-After) e use um número limitado de novas tentativas, com pequenos atrasos aleatórios entre elas.

  • Se o aplicativo tiver vários fluxos de trabalho que acessam o mesmo serviço limitado, você precisará integrar todos eles à sua estratégia de limitação de taxa. Por exemplo, é possível dar suporte tanto ao carregamento de registros em massa em um banco de dados quanto à consulta de registros nesse mesmo banco de dados. Você pode gerenciar a capacidade ao garantir que todos os fluxos de trabalho passem pelo mesmo mecanismo de limitação de taxa. Como alternativa, é possível reservar pools de capacidade separados para cada fluxo de trabalho.

  • O serviço limitado pode ser usado em vários aplicativos. Em alguns casos, é possível coordenar esse uso (conforme mostrado anteriormente neste artigo). Se você começar a ver um número maior do que o esperado de erros de limitação, esse número aumentado poderá indicar contenção entre aplicativos que acessam um serviço. Nesse caso, talvez seja necessário considerar reduzir temporariamente a vazão imposta pelo seu mecanismo de limitação de taxa até que o consumo por outros aplicativos diminua.

Quando usar esse padrão

Use esse padrão quando:

  • Você precisa reduzir os erros de limitação de taxa emitidos por um serviço com limite de taxa.

  • Você deseja minimizar o tráfego, em comparação com abordagens ingênuas de nova tentativa em caso de erro.

  • Você precisa reduzir o consumo de memória desqueando registros somente quando houver capacidade suficiente para processá-los.

Esse padrão pode não ser adequado quando:

  • A operação requer uma conclusão imediata e síncrona com latência muito baixa e não pode tolerar o processamento em fila ou adiado.

  • O principal gargalo não é a taxa de requisições, mas sim a concorrência ou a contenção de recursos (por exemplo, saturação da CPU ou trabalho em andamento de longa duração). Nesses casos, controles de escala ou simultaneidade são mais apropriados.

Design de carga de trabalho

Avalie como usar o padrão de limitação de taxa no projeto de uma carga de trabalho para atender às metas e aos princípios descritos nos pilares do Azure Well-Architected Framework. A tabela a seguir fornece diretrizes sobre como esse padrão dá suporte às metas de cada pilar.

Pilar Como esse padrão apoia os objetivos do pilar
As decisões de design de confiabilidade ajudam sua carga de trabalho a se tornar resiliente ao mau funcionamento e garantir que ela se recupere para um estado totalmente funcional após a ocorrência de uma falha. Essa tática protege o cliente reconhecendo e respeitando as limitações e os custos de comunicação com um serviço quando o serviço prefere evitar uso excessivo.

- RE:07 Autopreservação

Se esse padrão introduzir compensações dentro de um pilar, considere-as em relação aos objetivos dos outros pilares.

Example

O aplicativo de exemplo a seguir permite que os usuários enviem registros de vários tipos para uma API. Cada tipo de registro tem um processador de trabalho exclusivo que executa as seguintes etapas:

  1. Validação
  2. Enriquecimento
  3. Inserção do registro no banco de dados

Todos os componentes do aplicativo (API, processador de trabalho A e processador de trabalho B) são processos separados que podem ser dimensionados independentemente. Os processos não se comunicam diretamente entre si.

Diagrama que mostra um fluxo de vários processadores e de várias filas com gravação de armazenamento de concessão particionada em um banco de dados limitado.

O diagrama mostra dois usuários enviando registros por meio de uma API compartilhada. Cada tipo de registro é roteado para uma fila separada, processado por um processador de trabalho dedicado e gravado em um banco de dados limitado a uma taxa controlada regida por concessões de partição de blob em Armazenamento do Azure. Cada um dos dois usuários envia um lote de mensagens para o componente de API. Um usuário envia 10.000 mensagens e o outro envia 5.000 mensagens. A API roteia as 10.000 mensagens para a fila A e as 5.000 mensagens para a fila B. A fila A se conecta ao processador de trabalho A e a fila B se conecta ao processador de trabalho B. O processador de trabalho A grava no banco de dados a uma taxa de 300 registros por segundo. O processador de tarefas B grava 500 registros por segundo no banco de dados. Ambos os processadores de trabalho se conectam a um componente de banco de dados rotulado como "limitado a 800 registros por segundo". Abaixo dos dois processadores de trabalho, Armazenamento do Azure contém oito partições de blob rotuladas de 0 a 7. Uma observação indica que cada partição equivale a 100 registros por segundo. Várias setas conectam os processadores de tarefas às partições de blobs. As setas verdes apontam do processador de trabalho A para as partições 0, 4 e 6, o que indica que o processador mantém concessões nessas três partições. Essas concessões representam sua taxa de 300 registros por segundo. As setas verdes apontam do processador de trabalho B para as partições 1, 2, 3, 5 e 7. Essas setas indicam que esse processador mantém concessões em cinco partições. Essas concessões representam sua taxa de 500 registros por segundo. As tentativas malsucedidas de obter lease são mostradas como setas vermelhas que se estendem até partições que já estão em posse do outro processador. Essas setas representam a resolução de conflitos. O diagrama mostra que a soma de todas as partições mantidas em ambos os processadores é igual a 800 registros por segundo, o que corresponde ao limite de banco de dados.

Neste exemplo, cada concessão de blob representa um compartilhamento fixo da taxa de transferência de banco de dados permitida. Um processador só pode desativar e gravar à taxa combinada das concessões que ele mantém no momento. À medida que os processadores ganham ou perdem concessões ao longo do tempo, a taxa de gravação permitida muda, o que mantém o tráfego total do banco de dados dentro do limite configurado, ao mesmo tempo em que permite que todo o trabalho enfileirado continue avançando.

O diagrama incorpora o seguinte fluxo de trabalho:

  1. Um usuário envia 10.000 registros do tipo A para a API.
  2. A API coloca esses 10.000 registros na fila A.
  3. Um usuário envia 5.000 registros do tipo B para a API.
  4. A API enfileira esses 5.000 registros na fila B.
  5. O processador de trabalhos A detecta que a fila A contém registros e tenta obter uma concessão exclusiva para o blob 2.
  6. O processador de trabalhos B detecta que a fila B contém registros e tenta obter uma concessão exclusiva para o blob 2.
  7. O processador de tarefas A não consegue obter a locação.
  8. O processador de tarefas B obtém a concessão do blob 2 por 15 segundos. Agora, ele pode limitar as solicitações feitas ao banco de dados com uma taxa de 100 por segundo.
  9. O processador de tarefas B retira 100 registros da fila B e os grava.
  10. Um segundo passa.
  11. O processador de trabalho A vê que a fila A tem mais registros e tenta obter uma concessão exclusiva no blob 6.
  12. O processador de trabalhos B detecta que a fila B contém mais registros e tenta obter uma concessão exclusiva no blob 3.
  13. O processador de tarefas A adquire o lease do blob 6 por 15 segundos. Agora, ele pode limitar as solicitações feitas ao banco de dados com uma taxa de 100 por segundo.
  14. O processador de trabalhos B obtém a concessão do blob 3 por 15 segundos. Agora, ele pode limitar as solicitações feitas ao banco de dados com uma taxa de 200 por segundo. (ele também mantém a concessão do blob 2).
  15. O processador de tarefas A remove 100 registros da fila A e os grava.
  16. O processador de tarefas B desenfileira 200 registros da fila B e os grava.
  17. Um segundo passa.
  18. O processador de trabalho A detecta que a fila A tem mais registros e tenta obter uma concessão exclusiva no blob 0.
  19. O processador de trabalhos B detecta que a fila B contém mais registros e tenta obter uma concessão exclusiva no blob 1.
  20. O processador de trabalhos A obtém a concessão no blob 0 por 15 segundos. Agora, ele pode limitar as solicitações feitas ao banco de dados com uma taxa de 200 por segundo. (ele também mantém a concessão do blob 6).
  21. O processador de trabalhos B obtém a concessão no blob 1 por 15 segundos. Agora, ele pode limitar as solicitações feitas ao banco de dados com uma taxa de 300 por segundo (ele também mantém a concessão dos blobs 2 e 3).
  22. O processador de tarefas A retira 200 registros da fila A e os grava.
  23. O processador de tarefas B retira 300 registros da fila B e os grava.
  24. E assim por diante.

Após 15 segundos, um ou ambos os trabalhos ainda não serão concluídos. À medida que as concessões expiram, um processador também precisa reduzir o número de solicitações que ele retira da fila e grava.

As implementações desse padrão estão disponíveis em diferentes linguagens de programação:

  • Uma implementação Go está disponível no GitHub.
  • Uma implementação Java está disponível no GitHub.

Próximas Etapas 

As diretrizes a seguir também podem ser relevantes quando você implementa esse padrão:

Os seguintes padrões e diretrizes também podem ser relevantes quando você implementa esse padrão:

  • Throttling. O padrão de Limitação de Taxa normalmente é implementado em resposta a um serviço limitado.

  • Retry. Quando solicitações para um serviço limitado resultam em erros de limitação, geralmente é apropriado repetir essas solicitações após um intervalo apropriado.

  • Nivelamento de Carga Baseado em Filas é semelhante ao padrão de Limitação de Taxa, mas difere em vários aspectos importantes:

    • A limitação de taxa não precisa necessariamente usar filas para gerenciar a carga, mas precisa usar um serviço de mensagens durável. Por exemplo, um padrão de limitação de taxa pode usar serviços como Apache Kafka ou Hubs de Eventos.

    • O padrão de limitação de taxa introduz o conceito de um sistema distribuído de exclusão mútua baseado em partições, que permite gerenciar a capacidade de múltiplos processos não coordenados que se comunicam com o mesmo serviço com limitação de taxa.

    • Um padrão de Nivelamento de Carga Queue-Based é aplicável sempre que há uma incompatibilidade de desempenho entre os serviços ou você deseja melhorar a resiliência. Portanto, é um padrão mais amplo do que a Limitação de Taxa, que está mais especificamente preocupada em acessar com eficiência um serviço limitado.