Padrão de limitação de taxa

Barramento de Serviço do Azure
Armazenamento de Filas do Azure
Hubs de eventos do Azure

Muitos serviços usam um padrão de limitação para controlar os recursos consumidos por meio da imposição de limites à taxa de acesso de outros aplicativos ou serviços. É possível usar um padrão de limitação de taxa para evitar ou minimizar erros de limitação relacionados a esses limites e prever com mais precisão a taxa de transferência.

Um padrão de limitação de taxa é apropriado para muitos cenários, mas é particularmente útil em tarefas automatizadas e repetitivas em larga escala, como o processamento em lotes.

Contexto e problema

A execução de diversas operações usando um serviço limitado pode resultar em maior tráfego e taxa de transferência, pois é preciso controlar as solicitações rejeitadas e tentar realizar novamente essas operações. À medida que o número de operações aumenta, um limite pode exigir várias etapas de reenvio de dados, o que resulta em um impacto maior no desempenho.

Por exemplo, considere a seguinte tentativa precipitada de realizar novamente um processo com erro de ingestão de dados no Azure Cosmos DB:

  1. Seu aplicativo precisa ingerir 10.000 registros no Azure Cosmos DB. A ingestão de cada registro custa 10 RUs (Unidades de Solicitação), e é exigido um total de 100.000 RUs para a conclusão o trabalho.
  2. Sua instância do Azure Cosmos DB tem 20.000 RUs de capacidade provisionada.
  3. Você começa enviando todos os 10.000 registros para o Azure Cosmos DB, que grava 2.000 com êxito e rejeita 8.000.
  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 foi concluído com êxito, mas somente depois do envio de 30.000 registros ao Azure Cosmos DB, embora todo o conjunto de dados consistisse apenas em 10.000 registros.

Considere os seguintes fatores adicionais no exemplo acima:

  • Um grande número de erros também pode resultar em trabalho adicional relativo ao registro desses erros e ao processamento dos dados de log resultantes. Essa abordagem precipitada resulta na manipulação de 20.000 erros e o registro desses erros pode impor um custo de processamento, memória ou recurso de armazenamento.
  • Sem conhecimento dos limites do serviço de ingestão, a abordagem precipitada não tem como definir expectativas de quanto tempo levará para processar os 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 aplicar a limitação com base em métricas diferentes ao longo do tempo, como as seguintes:

  • 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 usada para limitação, sua implementação da limitação de taxa envolverá o controle do número e/ou tamanho das operações enviadas ao serviço durante um período de tempo específico, otimizando o uso do serviço sem exceder a capacidade de limitação.

Em cenários em que suas APIs podem lidar com solicitações mais rapidamente do que o permitido por qualquer serviço de ingestão limitada, você precisará gerenciar a rapidez de seu uso do serviço. No entanto, é arriscado tratar a limitação apenas como um problema de incompatibilidade de taxa de dados e simplesmente armazenar em buffer suas solicitações de ingestão até que o serviço limitado possa lidar com elas. Nesse cenário, se o aplicativo falha, você corre o risco de perder os dados armazenados em buffer.

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 os Hubs de Eventos do Azure podem lidar com milhões de operações por segundo). Assim, é possível usar um ou mais processadores de trabalho para ler os registros do sistema de mensagens com 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:

Um fluxo de mensagens durável com três processadores de trabalhos chamando um serviço limitado.

Ao enviar registros, o período de tempo usado 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 limitações com base em intervalos de tempo fáceis de compreender e utilizar. No entanto, para o computador que executa um serviço, esses períodos podem ser muito longos em comparação com a rapidez com que ele processa 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 quantidades menores de registros com uma frequência maior, a fim de melhorar a taxa de transferência. Portanto, em vez de criar lotes para liberações a cada segundo ou minuto, é possível ser mais granular e manter o fluxo de consumo de recursos (memória, CPU, rede, etc.) em uma taxa mais uniforme, a fim de evitar possíveis gargalos devido a disparo contínuos e repentinos de solicitações. Por exemplo, se um serviço permitir 100 operações por segundo, a implementação de um limitador de taxa poderá uniformizar as solicitações liberando 20 operações a cada 200 milissegundos, conforme mostrado no gráfico a seguir.

Um gráfico que mostra a limitação de taxa 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, particione logicamente a capacidade do serviço e use um sistema de exclusão mútua distribuído 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 precisou 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. Assim, o processo classifica o limite de taxa para 50 solicitações por segundo, conclui a tarefa em dois segundos e libera o bloqueio.

Uma maneira de implementar esse padrão seria usar o Armazenamento do Azure. Nesse cenário, você cria um blob de 0 bytes por partição lógica em um contêiner. Em seguida, seus aplicativos podem obter concessões exclusivas diretamente nesses blobs por um curto período de tempo (por exemplo, 15 segundos). Para cada concessão dada a um aplicativo, ele pode usar o valor de capacidade contido na partição. Em seguida, o aplicativo precisa acompanhar o tempo de concessão para que, quando ele expirar, seja possível parar de usar a capacidade concedida. Geralmente, ao implementar esse padrão, é definido que cada processo que precisar de capacidade tentará obter a concessão em uma partição aleatória.

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.

Partições de Blobs do Azure

Como alternativa ao Armazenamento do Azure, também é possível implementar esse tipo de sistema de gerenciamento de concessão usando tecnologias como o Zookeeper, o Consul, o etcd, o Redis/Redsync, entre outras.

Problemas e considerações

Considere o seguinte ao decidir como implementar o padrão:

  • Embora o padrão de limitação de taxa possa reduzir o número de erros de limitação, o aplicativo ainda precisa lidar corretamente com possíveis ocorrências desses erros.
  • Se o aplicativo tiver vários fluxos de trabalho que acessam o mesmo serviço limitado, será preciso integrar todos eles à 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. Assim, você pode gerenciar a capacidade garantindo que todos os fluxos de trabalho sejam limitados por meio do 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, mas não em todos, é possível coordenar esse uso (conforme mostrado acima). Se você começar a ver um número maior do que o esperado de erros de limitação, isso pode ser um sinal de contenção entre aplicativos que acessam um serviço. Nesse caso, talvez seja necessário considerar a redução temporária da taxa de transferência imposta pelo mecanismo de limitação de taxa até que o uso de outros aplicativos diminua.

Quando usar esse padrão

Use esse padrão para:

  • Reduzir os erros de limitação gerados por um serviço limitado.
  • Reduzir o tráfego em comparação com a abordagem de uma tentativa precipitada de reexecução diante de um erro.
  • Reduzir o consumo de memória desenfileirando registros somente quando há capacidade para processá-los.

Design de carga de trabalho

Um arquiteto deve avaliar como o padrão de Limitação de taxa pode ser usado no design das suas cargas de trabalho para abordar os objetivos e princípios dos pilares da estrutura bem arquitetada do Azure. Por exemplo:

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 a garantir 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 honrar as limitações e os custos de se comunicar com um serviço quando o serviço pretende evitar o uso excessivo.

- RE:07 Autopreservação

Tal como acontece com qualquer decisão de design, considere quaisquer compensações em relação aos objetivos dos outros pilares que possam ser introduzidos com este padrão.

Exemplo

O aplicativo de exemplo a seguir permite que os usuários enviem registros de vários tipos para uma API. Há um processador de trabalho exclusivo para cada tipo de registro 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 trabalhos A e processador de trabalhos B) são processos separados que podem ser dimensionados de maneira independente. Os processos não se comunicam diretamente uns com os outros.

Um fluxo de várias filas e vários processadores com armazenamento particionado para concessões realizando uma gravação em um banco de dados.

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 enfileira 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 vê que a fila A tem registros e tenta obter uma concessão exclusiva no blob 2.
  6. O processador de trabalhos B vê que a fila B tem registros e tenta obter uma concessão exclusiva no blob 2.
  7. O processador de trabalhos A não consegue obter a concessão.
  8. O processador de trabalhos B obtém a concessão no 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 trabalhos B desenfileira 100 registros da fila B e os grava.
  10. Um segundo passa.
  11. O processador de trabalhos 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 vê que a fila B tem mais registros e tenta obter uma concessão exclusiva no blob 3.
  13. O processador de trabalhos A obtém a concessão no 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 no 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 trabalhos A desenfileira 100 registros da fila A e os grava.
  16. O processador de trabalhos B desenfileira 200 registros da fila B e os grava.
  17. Um segundo passa.
  18. O processador de trabalhos A vê que a fila A tem mais registros e tenta obter uma concessão exclusiva no blob 0.
  19. O processador de trabalhos B vê que a fila B tem 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 trabalhos A desenfileira 200 registros da fila A e os grava.
  23. O processador de trabalhos B desenfileira 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 terão sido concluídos. À medida que as concessões expiram, um processador também precisa reduzir o número de solicitações desenfileiradas e gravadas.

Logotipo do GitHub 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.

Os padrões e diretrizes a seguir também podem ser relevantes ao implementar esse padrão:

  • Limitação. O padrão de limitação de taxa discutido aqui normalmente é implementado em resposta a um serviço limitado.
  • Repetição. Quando as solicitações feitas a um serviço limitado resultam em erros de limitação, é geralmente apropriado tentar realizar novamente essas solicitações após um tempo.

O nivelamento de carga baseado em fila é semelhante, mas difere do padrão de limitação de taxa das seguintes importantes maneiras:

  1. 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 o Apache Kafka ou os Hubs de Eventos do Azure.
  2. O padrão de limitação de taxa introduz o conceito de um sistema de exclusão mútua distribuído nas partições, o que permite o gerenciamento da capacidade de vários processos descoordenados que se comunicam com o mesmo serviço limitado.
  3. Um padrão de nivelamento de carga baseado em fila é aplicável para melhorar a resiliência ou sempre que há uma incompatibilidade de desempenho entre os serviços. Isso o torna um padrão mais amplo do que a limitação de taxa, que está mais especificamente concentrada em acessar com eficiência um serviço limitado.