Padrão de limitação de taxa

Controla a taxa a que a tua aplicação envia pedidos para um serviço, mantendo-te dentro dos limites de limitação e da capacidade global do serviço. Esta abordagem ajuda-o a evitar ou minimizar erros de limitação da taxa e a prever com maior precisão a taxa de transferência.

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

Contexto e problema

Executar um grande número de operações num serviço com limitação de débito pode resultar num aumento do tráfego e numa redução da capacidade de processamento, porque é necessário monitorizar os pedidos rejeitados e voltar a tentar as operações. À medida que o número de operações aumenta, um limite de limitação pode exigir múltiplas passagens de reenvio de dados, o que resulta num impacto de desempenho maior.

Por exemplo, considere o seguinte processo problemático de repetição em caso de erro para introduzir dados no Azure Cosmos DB:

  1. Seu aplicativo precisa ingerir 10.000 registros no Azure Cosmos DB. Cada registo custa 10 unidades de pedido (RUs) para ingestão, pelo que são necessárias 100 000 RUs para concluir a tarefa.

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

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

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

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

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

  7. Você envia os 2.000 registros restantes para o Azure Cosmos DB. Todos são escritos com sucesso.

O trabalho de ingestão é concluído com sucesso, mas apenas após enviar 30.000 registos para o Azure Cosmos DB. Todo o conjunto de dados consiste em apenas 10.000 registos.

Existem outros fatores a considerar neste exemplo:

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

  • Como não conheces os limites de limitação do serviço de ingestão, não tens forma de definir expectativas sobre quanto tempo demora o processamento de dados. A limitação da taxa pode permitir calcular o tempo necessário para a ingestão.

Solução

A limitação da taxa pode reduzir o seu tráfego e potencialmente melhorar a taxa de transferência, reduzindo o número de registos enviados para um serviço durante um determinado período de tempo.

Um serviço pode limitar pedidos com base em diferentes métricas ao longo do tempo, tais 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 utilize para a limitação de taxa, a sua implementação de limitação de taxa consistirá em controlar o número e/ou a dimensão das operações enviadas para o serviço num determinado período de tempo. A limitação de taxa otimiza a utilização do serviço sem exceder a sua capacidade de limitação.

Em cenários em que as suas APIs conseguem lidar com pedidos mais rapidamente do que os serviços de ingestão limitados permitem, deve gerir a rapidez com que utiliza o serviço. Tratar a limitação apenas como uma incompatibilidade de velocidade de dados e bufferizar os pedidos de ingestão até que o serviço recupere cria um risco. Se a sua aplicação deixar de responder neste cenário, quaisquer dados em buffer podem 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 completa. (Serviços como o Hubs de Eventos do Azure podem gerir milhões de operações por segundo.) Pode então usar um ou mais processadores de tarefas para ler os registos do sistema de mensagens a uma taxa controlada dentro dos limites do serviço limitado. Submeter registos ao sistema de mensagens pode poupar memória interna, permitindo-lhe retirar da fila apenas os registos que podem ser processados durante um determinado intervalo de tempo.

O Azure fornece vários serviços de mensagens duráveis que você pode usar com esse padrão, incluindo:

Diagrama que mostra um fluxo de mensagens duradouro. Três processadores de tarefas ligam a um serviço limitado.

Quando envia registos, o período que utiliza para divulgar registos pode ser mais detalhado do que o período em que o serviço limita. Os sistemas costumam definir limites com base em intervalos de tempo que pode compreender e utilizar facilmente. No entanto, para o computador que executa um serviço, estes prazos podem ser muito longos comparados com a rapidez com que pode processar informação. Por exemplo, um sistema pode acelerar por segundo ou por minuto, mas normalmente o código é processado na ordem de nanossegundos ou milissegundos.

Embora não seja obrigatório, é frequentemente recomendado enviar um número menor de registos com mais frequência para melhorar o débito. Assim, em vez de tentares agrupar registos para libertação uma vez por segundo ou uma vez por minuto, podes fazê-lo de forma mais granular para manter o consumo de recursos (memória, CPU e rede) distribuído de forma mais uniforme. Esta abordagem previne potenciais gargalos causados por surtos súbitos de pedidos. Por exemplo, se um serviço permite 100 operações por segundo, a implementação de um limitador de taxa pode equilibrar os pedidos ao libertar 20 operações a cada 200 milissegundos, como mostrado no gráfico seguinte.

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

Além disso, às vezes é necessário que vários processos descoordenados compartilhem um serviço limitado. Para implementar limitação de taxa neste cenário, pode-se particionar logicamente a capacidade do serviço e depois usar um sistema distribuído de exclusão mútua para gerir bloqueios exclusivos nessas partições. Os processos descoordenados podem então competir por bloqueios nessas partições sempre que precisarem de capacidade. Para cada partição para a qual um processo mantém um bloqueio, é concedida uma certa quantidade de capacidade.

Por exemplo, se o sistema acelerado permitir 500 solicitações por segundo, você poderá criar 20 partições no valor de 25 solicitações por segundo cada. Se um processo precisar emitir 100 solicitações, ele pode solicitar ao sistema de exclusão mútua distribuído quatro partições. O sistema pode conceder duas partições por 10 segundos. O processo classificaria o limite para 50 solicitações por segundo, concluiria a tarefa em dois segundos e, em seguida, liberaria o bloqueio.

Uma forma de implementar este padrão é usar o Armazenamento do Azure. Nesse cenário, você cria um blob de 0 bytes por partição lógica em um contêiner. As suas aplicações podem então obter bloqueios exclusivos diretamente sobre esses blobs por um curto período de tempo (por exemplo, 15 segundos). Por cada concessão atribuída a uma aplicação, esta pode utilizar a quantidade de capacidade dessa partição. A candidatura precisa então de acompanhar o tempo do arrendamento para que, quando o prazo expirar, a candidatura deixe de usar a capacidade que lhe foi concedida. Quando implementas este padrão, muitas vezes vais querer que cada processo tente alugar uma partição aleatória quando precisar de capacidade.

Para reduzir ainda mais a latência, você pode alocar uma pequena quantidade de capacidade exclusiva para cada processo. Nesse caso, um processo só procuraria obter um contrato de locação de capacidade partilhada se fosse necessário exceder a sua capacidade reservada.

Diagrama que mostra vários processos a disputar arrendamentos exclusivos em partições de blobs no Armazenamento de Blobs do Azure.

Como alternativa ao Armazenamento do Azure, também pode implementar este tipo de sistema de gestão de arrendamentos usando tecnologias como ZooKeeper, etcd, e Redis/Redsync.

Problemas e considerações

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

  • Embora o padrão de limitação de taxa possa reduzir o número de erros de limitação, a sua aplicação ainda precisa de lidar corretamente com quaisquer erros de limitação que possam ocorrer.

  • Assegure que as tentativas são coordenadas com limitação de taxas. Retentativas cegas ou excessivamente agressivas podem aumentar a carga e criar tempestades de retentativas, por isso propague sinais de back-pressure (por exemplo, HTTP 429 com Retry-After) e utilize um número limitado de retentativas com pequenos atrasos aleatórios entre tentativas.

  • Se a sua aplicação tiver vários fluxos de trabalho que acedem ao mesmo serviço limitado, precisa de integrar todos eles na sua estratégia de limitação de taxas. Por exemplo, você pode oferecer suporte ao carregamento em massa de registros em um banco de dados, mas também consultar registros nesse mesmo banco de dados. Pode gerir a capacidade garantindo que todos os fluxos de trabalho são bloqueados pelo mesmo mecanismo de limitação de taxas. Como alternativa, você pode reservar pools separados de capacidade para cada fluxo de trabalho.

  • O serviço restringido pode ser usado em múltiplas aplicações. Em alguns casos, é possível coordenar essa utilização (como mostrado anteriormente neste artigo). Se começar a ver um número maior de erros de limitação do que o esperado, esse número aumentado pode indicar contenção entre aplicações que acedem a um serviço. Neste caso, poderá ser necessário ponderar reduzir temporariamente a taxa de transferência imposta pelo seu mecanismo de limitação de taxa até que a utilização por parte de outras aplicações diminua.

Quando utilizar este padrão

Utilize este padrão quando:

  • Precisa de reduzir os erros de limitação causados por um serviço com tarifa limitada.

  • Pretende minimizar o tráfego, em comparação com abordagens básicas de repetição em caso de erro.

  • É necessário reduzir o consumo de memória retirando os registos da fila apenas quando houver capacidade suficiente para os poder processar.

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

  • A operação requer conclusão imediata e síncrona com latência muito baixa e não tolera filas ou processamento diferido.

  • O principal gargalo não é a taxa de pedidos, mas sim a concorrência ou a contenção de recursos (por exemplo, saturação do CPU ou trabalho prolongado em voo). Nestes casos, controlos de escalabilidade ou concorrência são mais adequados.

Design da carga de trabalho

Avalie de que forma utilizar o padrão de limitação da taxa na conceção de uma carga de trabalho para dar resposta aos objetivos e princípios descritos nos pilares do Azure Well-Architected Framework. A tabela a seguir fornece orientação sobre como esse padrão suporta as metas de cada pilar.

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

- RE:07 Autopreservação

Se este 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 registo tem um processador de tarefas único que executa os seguintes passos:

  1. Validation
  2. Enriquecimento
  3. Inserção do registo na base de dados

Todos os componentes da aplicação (API, processador de tarefas A e processador de tarefas B) são processos separados que podem ser escalados de forma independente. Os processos não comunicam diretamente entre si.

Diagrama que mostra um fluxo multi-fila e multiprocessador com armazenamento de arrendamento particionado a escrever para uma base de dados limitada.

O diagrama mostra dois utilizadores a submeter registos através de uma API partilhada. Cada tipo de registo é encaminhado para uma fila separada, processado por um processador de trabalhos dedicado e escrito numa base de dados limitada a uma taxa controlada, regida por arrendamentos de partições blob no Armazenamento do Azure. Os dois utilizadores enviam cada um um lote de mensagens para o componente da API. Um utilizador envia 10.000 mensagens e o outro envia 5.000 mensagens. A API encaminha as 10.000 mensagens para a fila A e as 5.000 mensagens para a fila B. A fila A liga-se ao processador de trabalhos A, e a fila B liga-se ao processador de trabalhos B. O processador de tarefas A escreve na base de dados a uma taxa de 300 registos por segundo. O processador de trabalho B escreve na base de dados a 500 registos por segundo. Ambos os processadores de tarefas estão ligados a um componente de base de dados identificado com a indicação "limitado a 800 registos por segundo." Por baixo dos dois processadores de trabalho, o Armazenamento do Azure contém oito partições blob rotuladas de 0 a 7. Uma nota indica que cada partição corresponde a 100 registos por segundo. Várias setas ligam os processadores de tarefas às partições de blobs. Setas verdes apontam do processador de trabalho A para as partições 0, 4 e 6, o que indica que o processador detém contratos de arrendamento nessas três partições. Estas concessões explicam a sua taxa de 300 registos por segundo. Setas verdes apontam do processador de tarefas B para as partições 1, 2, 3, 5 e 7. Estas setas indicam que este processador tem concessões sobre cinco partições. Estes contratos representam a sua taxa de 500 registos por segundo. As tentativas falhadas de concessão são mostradas como setas vermelhas que se estendem até partições já atribuídas ao outro processador. Estas setas representam a resolução de conflitos. O diagrama mostra que a soma de todas as partições mantidas entre ambos os processadores é igual a 800 registos por segundo, o que corresponde ao limite de aceleração da base de dados.

Neste exemplo, cada arrendamento de blob representa uma quota fixa do débito permitido da base de dados. Um processador só pode desalinhar e escrever à taxa combinada dos arrendamentos que atualmente detém. À medida que os processadores ganham ou perdem arrendamentos ao longo do tempo, a taxa de escrita permitida muda, o que mantém o tráfego total da base de dados dentro do limite configurado, permitindo que todo o trabalho em fila progrida.

Este 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 registos na fila A.
  3. Um usuário envia 5.000 registros do tipo B para a API.
  4. A API coloca esses 5.000 registos na fila B.
  5. O processador de trabalhos A deteta que a fila A contém registos e tenta obter uma locação exclusiva sobre o blob 2.
  6. O processador de trabalhos B verifica que a fila B tem registos e tenta obter uma locação exclusiva sobre o blob 2.
  7. O processador de trabalhos A não consegue obter o contrato de arrendamento.
  8. O processador de tarefas B adquire o arrendamento do blob 2 durante 15 segundos. Agora, é possível limitar a taxa de pedidos para a base de dados a 100 por segundo.
  9. O processador de trabalhos B remove 100 registos da fila B e escreve-os.
  10. Um segundo passa.
  11. O processador de trabalhos A vê que a fila A tem mais registos e tenta obter um arrendamento exclusivo no blob 6.
  12. O processador de tarefas B deteta que a fila B tem mais registos e tenta obter uma concessão exclusiva sobre o blob 3.
  13. O processador de tarefas A adquire a concessão do bloco 6 durante 15 segundos. Agora, é possível limitar a taxa de pedidos para a base de dados a 100 por segundo.
  14. O processador de tarefas B adquire o arrendamento sobre o blob 3 por 15 segundos. Agora pode limitar a taxa de pedidos para a base de dados a 200 por segundo. (Também detém o contrato de arrendamento do blob 2.)
  15. O processador de trabalhos A remove 100 registos da fila A e escreve-os.
  16. O processador de trabalhos B remove 200 registos da fila B e escreve-os.
  17. Um segundo passa.
  18. O processador de trabalhos A deteta que a fila A tem mais registos e tenta obter uma locação exclusiva no blob 0.
  19. O processador de trabalhos B deteta que a fila B tem mais registos e tenta obter uma concessão exclusiva no blob 1.
  20. O processador de tarefas A obtém a concessão do blob 0 por 15 segundos. Agora pode limitar a taxa de pedidos para a base de dados a 200 por segundo. (É também titular do contrato de arrendamento do blob 6.)
  21. O processador de tarefas B adquire o arrendamento do blob 1 durante 15 segundos. Agora, pode limitar os pedidos à base de dados a 300 por segundo. (Também detém o contrato de arrendamento para as bolhas 2 e 3.)
  22. O processador de trabalhos A remove 200 registos da fila A e escreve-os.
  23. O processador de tarefas B retira 300 registos da fila B e escreve-os.
  24. E assim por diante.

Após 15 segundos, um ou ambos os trabalhos continuam sem ser concluídos. À medida que os contratos de arrendamento expiram, um processador deve também reduzir o número de pedidos que remove e escreve.

Implementações deste padrão estão disponíveis em diferentes linguagens de programação:

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

Passos seguintes

As seguintes orientações também podem ser relevantes ao implementar este padrão:

Os seguintes padrões e orientações também podem ser relevantes ao implementar este padrão:

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

  • Retry. Quando pedidos a um serviço limitado resultam em erros de limitação, é geralmente apropriado tentar novamente esses pedidos após um intervalo apropriado.

  • Nivelamento da Carga Baseado em Filas é semelhante ao padrão de limitação da taxa, mas difere em vários aspetos fundamentais:

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

    • O padrão Rate Limiting introduz o conceito de um sistema distribuído de exclusão mútua nas partições, que permite gerir a capacidade de múltiplos processos não coordenados que comunicam com o mesmo serviço limitado.

    • Um padrão de nivelamento de carga baseado em filas aplica-se sempre que houver uma discrepância de desempenho entre serviços ou quando se pretende melhorar a resiliência. Portanto, é um padrão mais amplo do que o Limite de Velocidade, que se preocupa mais especificamente com o acesso eficiente a um serviço limitado.