
Partager:
Aaron était défenseur des développeurs chez Nexmo. Ingénieur logiciel chevronné et artiste numérique en herbe, Aaron est souvent amené à créer des choses avec du code ou de l'électronique, parfois les deux. On peut généralement savoir qu'il travaille sur quelque chose de nouveau grâce à l'odeur de composants brûlés qui flotte dans l'air.
Respecter les limites de débit de l'API avec un backoff
Temps de lecture : 14 minutes
Cet article a été mis à jour en avril 2025
Lorsque vous travaillez avec les Vonage Communication APIs-ou n'importe quelle API-vous devez être conscient de leurs limites de taux. Les limites de débit sont l'une des façons dont les fournisseurs de services peuvent réduire la charge sur leurs serveurs, prévenir les activités malveillantes ou s'assurer qu'un seul utilisateur ne monopolise pas les ressources disponibles.
Dans cet article, nous verrons comment vous pouvez gérer au mieux vos appels API pour vous assurer que vous êtes un "bon citoyen API". Nous verrons comment vous pouvez respecter les limites de débit de l'API de Vonage Communication. de Vonage Communication tout en étant efficace et en effectuant vos appels API aussi rapidement que possible.
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.
Pour acheter un numéro de téléphone virtuel, rendez-vous sur votre tableau de bord API et suivez les étapes ci-dessous.
Purchase a phone number
Accédez à votre tableau de bord API
Naviguez vers CONSTRUIRE & GERER > Numbers > Acheter des Numbers.
Choisissez les attributs nécessaires et cliquez sur Rechercher
Cliquez sur le bouton Acheter à côté du numéro désiré et validez votre achat
Pour confirmer que vous avez acheté le numéro virtuel, allez dans le menu de navigation de gauche, sous CONSTRUIRE & GÉRER, cliquez sur Numéros, puis sur Vos Numéros
Que signifie être un bon citoyen de l'API ?
Lorsque nous travaillons avec des API externes, nous devrions toujours essayer de maintenir notre débit à un niveau acceptable. Mais des erreurs se produisent, il peut y avoir une augmentation soudaine de l'utilisation et nous finissons par dépasser la limite de débit, ce qui entraîne l'échec de notre appel à l'API.
Lorsque cela se produit, il peut être tentant de réessayer immédiatement, mais cela va à l'encontre du but recherché. Si vos appels à l'API échouent parce que vous avez atteint la limite de débit, c'est que le service vous demande de ralentir. Réessayer immédiatement la même requête ne constitue pas un ralentissement et peut vous conduire à être banni de certains services. Au lieu de cela, vous devez "faire marche arrière" et marquer une pause avant de réessayer.
Retarder les appels API avec Backoff
Un backoff est un délai d'attente avant d'entreprendre une action. Le temps d'attente peut être calculé à l'aide de nombreuses stratégies différentes, mais les plus courantes sont les suivantes :
Constant : attendre un temps constant entre chaque tentative. Par exemple, si nous avons un délai constant de 1 seconde, nos tentatives auront lieu à 1s, 2s, 3s, 4s, 5s, 6s, 7s, etc.
Fibonaccial : on utilise ici le nombre de Fibonacci correspondant à la tentative en cours, ce qui fait que nos délais sont de 1s, 1s, 2s, 3s, 5s, 8s, 13s, etc
Exponentiel : le délai est calculé comme 2 à la puissance du nombre de tentatives infructueuses qui ont été faites. Par exemple, le délai est de 2 à la puissance 2 du nombre de tentatives infructueuses :
2^1 = 2 = 2
2^2 = 2 * 2 = 4
2^3 = 2 2 2 = 8
2^4 = 2 2 2 * 2 = 16
2^5 = 2 2 2 2 2 = 32
2^6 = 2 2 2 2 2 * 2 = 64
2^7 = 2 2 2 2 2 2 2 = 128
Il existe d'autres stratégies - fixe, linéaire, polynomiale - mais pour les besoins de cet article, nous allons nous en tenir à la stratégie de backoff exponentielle fournie par le paquetage Python backoff package.
Essai de rétroactivité
Je ne veux pas déclencher la limite de taux de l'API de Vonage juste pour démontrer le paquet Backoff. Au lieu de cela, créons un code fictif avec asyncio.
import asyncio
from datetime import datetime
import backoff
import uvloop
start_time = datetime.now().timestamp()
@backoff.on_predicate(backoff.constant, max_time=300, jitter=lambda x: x)
async def slow_operation():
with open("./attempts.log", "a") as f:
f.write(f"{datetime.now().timestamp() - start_time}\n")
return False
async def main(loop):
for x in range(0, 500):
asyncio.ensure_future(slow_operation(), loop=loop)
if __name__ == "__main__":
loop = uvloop.new_event_loop()
loop.create_task(main(loop))
loop.run_forever()Dans cet exemple, la fonction slow_operation() enregistre les millisecondes écoulées depuis l'époque et renvoie ensuite Falsece qui permet de s'assurer que le décorateur backoff s'exécute à chaque fois que nous appelons la fonction. Backoff continuera à s'exécuter slow_operation() jusqu'à ce que le délai atteigne le seuil de max_time de 300 secondes, après quoi il abandonnera.
Pour générer un grand nombre de points de données pour le graphique, nous mettons en file d'attente la fonction slow_operation() 500 fois dans notre boucle asynchrone.
Si nous représentons graphiquement le nombre d'appels de fonction tentés par seconde, voici à quoi cela ressemble lorsque nous utilisons une stratégie constante :
A constant traffic pattern visualised
Il existe une bande épaisse entre 40 et 60 appels de fonction, de sorte qu'un backoff constant n'est pas adapté à nos besoins. Chaque seconde, nous inondons l'API de requêtes, ce qui maintient notre débit à un niveau beaucoup trop élevé, et il est probable que nous continuerons à être limités en termes de taux.
Mais si nous exécutons le même code avec la stratégie exponentielle, nous obtenons un graphique très différent.
A visualisation of a traffic pattern using exponential backoff
Ce graphique est bien meilleur. Nous pouvons voir où la stratégie de backoff a augmenté le délai, réduisant le débit et, espérons-le, nous donnant assez de temps pour mettre fin à la limitation du débit. Mais nous avons maintenant un autre problème.
Dans le graphique, nous pouvons voir que les appels se regroupent maintenant autour de la fin des délais. Nous pourrions nous retrouver dans une situation où ces groupes d'appels continueraient à déclencher à nouveau la limitation de débit. Pour empêcher la formation de ces groupes, nous utilisons la gigue.
Créer une charge de travail plus équitablement répartie grâce au hasard
La gigue ajoute un facteur aléatoire au calcul de la durée du retard dans notre backoff.
sleep = random.uniform(0, delay)Le paquet Python backoff inclut cette gigue par défaut. Dans les exemples de code ci-dessus, je l'enlève avec une fonction lambda, alors générons à nouveau le graphique exponentiel, mais cette fois avec la gigue.
An exponential backoff traffic pattern visualised
Comme nous utilisons toujours une stratégie exponentielle, nous pouvons constater que le nombre d'appels diminue très rapidement, mais grâce au caractère aléatoire de la gigue, nous ne constatons pas de regroupement. Au contraire, les appels de fonction par seconde sont faibles et répartis de manière plus uniforme.
Traitement des files d'attente SMS avec backoff
Les limites tarifaires varient en fonction de l'API de Vonage Communications que vous utilisez. Par exemple, les API Redact et Applications API ont une limite de débit de 170 requêtes par seconde.. Cependant, en raison des restrictions imposées par les opérateurs, la limite de débit pour les SMS sortants peut être aussi basse qu'une demande par seconde. pour les SMS sortants peut être aussi basse qu'une demande par seconde.. Le SMS est donc le candidat idéal pour l'application des techniques de backoff que nous avons examinées plus haut.
Files d'attente et courtiers
Python dispose d'un grand nombre de files d'attente de tâches parmi lesquelles choisir-Celeryet [huey²], RQ, Kuyruk, Taskmaster, Dramatiq, WorQ-et presque autant de courtiersMongoDB, Redis, RabbitMQ, SQS. Certaines de ces files d'attente sont dotées d'un support intégré pour le backoff, mais elles ajoutent également beaucoup de complexité, ce qui les rend hors de portée pour cet article.
Cependant, une fois que vous êtes à l'aise avec les techniques sous-jacentes et le raisonnement derrière l'utilisation d'une file d'attente de tâches, backoff, jitter, et ainsi de suite, je vous recommande de revisiter les liens vers les files d'attente de tâches ci-dessus. Les exemples de code que nous verrons dans le reste de cet article sont intentionnellement succincts afin que nous puissions nous concentrer uniquement sur la gestion du débit, alors que les paquets ci-dessus sont beaucoup plus robustes et prêts pour la production.
Envoi de SMS de manière asynchrone avec les API de communication de Vonage
Pour éviter que la latence du réseau sur une requête ne bloque l'ensemble de notre application, nous allons envoyer nos SMS de manière asynchrone. Mais cela signifie que que nous ne pouvons pas utiliser le SDK Python de Vonage de Vonage pour envoyer notre SMS. Le SDK Python n'est pas asynchrone car il utilise des Requests, qui bloque.
Nous pouvons consulter la demande d'exemple de l'API Demande d'exemple de l'API Messages de la documentation pour avoir une idée de ce que le SDK Python de Vonage fait pour nous :
Dans cet extrait de code, nous pouvons voir que nous envoyons une requête POST au point de terminaison https://api.nexmo.com/v0.1/message. La requête contient des informations sur le type de contenu que nous envoyons et attendons en réponse. Mais les parties essentielles à noter sont l'en-tête Authorization et l'option data (-d).
La demande est autorisée à l'aide de jetons Web JSON (JWT). JWT est une norme industrielle ouverte, et il existe plusieurs paquets Python disponibles pour aider à leur génération. Mais, heureusement, le SDK Python de Vonage dispose déjà d'une fonction que nous pouvons appeler pour créer un JWT valide pour la demande. Comme la génération de JWT est rapide, qu'elle ne nécessite pas d'E/S réseau et qu'elle n'est exécutée qu'une seule fois au début du script, le fait qu'elle ne soit pas asynchrone n'a pas d'importance.
Création de l'application
Installez le CLI de Vonage globalement avec cette commande :
npm install @vonage/cli -gEnsuite, configurez le CLI avec votre clé et votre secret API Vonage. Vous pouvez trouver ces informations dans le tableau de bord du développeur.
vonage config:set --apiKey=VONAGE_API_KEY --apiSecret=VONAGE_API_SECRETCréez un nouveau répertoire pour votre projet et placez-y un CD :
mkdir my_project
CD my_projectMaintenant, utilisez le CLI pour créer une application Vonage.
vonage apps:create
✔ Application Name … my_project
✔ Select App Capabilities › Messages
✔ Create messages webhooks? … no
✔ Allow use of data for AI training? noCette commande stockera votre clé privée dans le fichier mon_projet.key. Nous en aurons besoin lors de la génération de notre JWT, ainsi que de l'identifiant de l'application. L'identifiant de l'application est affiché dans le terminal lorsque vous exécutez la commande app:create ou vous pouvez le trouver sur votre tableau de bord Vonage.
Vous avez maintenant besoin d'un numéro pour pouvoir recevoir des appels. Vous pouvez en louer un en utilisant la commande suivante (en remplaçant le code du pays par votre code). Par exemple, si vous êtes aux États-Unis, remplacez GB par US:
Reliez maintenant le numéro à votre application :
vonage apps:link --number=VONAGE_NUMBER APP_ID Envoi du SMS
import vonage
vonage_client = vonage.Client(
application_id=os.environ["VONAGE_APPLICATION_ID"],
private_key=os.environ["VONAGE_PRIVATE_KEY"],
)
jwt = vonage_client.generate_application_jwt()
@backoff.on_predicate(backoff.expo, max_time=300)
async def send_sms(recipient, message):
async with httpx.AsyncClient() as httpx_client:
response = await httpx_client.post(
"https://api.nexmo.com/v0.1/messages",
headers={
"Authorization": b"Bearer " + jwt,
"Content-Type": "application/json",
"Accept": "application/json",
},
json={
"from": {"type": "sms", "number": os.environ["VONAGE_NUMBER"]},
"to": {"type": "sms", "number": recipient},
"message": {"content": {"type": "text", "text": message,}},
},
)
return response.status_code == 202En haut de notre script, en dehors de la fonction asynchrone, nous instancions notre client Vonage avec l'identifiant de l'application et la clé privée. Je les ai stockées dans des variables d'environnement, de sorte qu'elles ne sont pas codées en dur dans mon script.
La fonction send_sms effectue la demande d'API, et c'est donc dans cette fonction que se trouve notre décorateur backoff. Nous utilisons on_predicatede sorte que si la fonction renvoie Falsela fonction tentera à nouveau de l'utiliser. J'ai maintenu la valeur de max_time à 300 secondes, mais nous pourrions également fixer une limite de max_attempt ou les deux !
Pour effectuer la requête POST asynchrone, nous utilisons httpx. httpx est un client HTTP pour Python 3 avec une interface très similaire à Requests, mais il supporte l'asynchronisme. J'ai structuré la requête httpx aussi près que possible de l'exemple cURL que nous avons vu plus haut. Nous avons les en-têtes avec les informations sur le type de contenu ainsi que l'en-tête Authorization, qui inclut le JWT généré pour nous par le SDK Python de Vonage.
Notre charge utile est une chaîne JSON contenant le numéro de départ, le numéro du destinataire et notre message.
Enfin, nous vérifions le code d'état HTTP renvoyé par l'API Messages à la demande. Si le code n'est pas 202 Accepted, la fonction renvoie False, ce qui déclenche une nouvelle tentative.
Mise en file d'attente du SMS dans une boucle
Dans mon script d'exemple, j'ai codé en dur une liste de destinataires.
async def main(loop):
recipients = [
"13055550157",
"15615550134",
]
message = "✨✨✨Hello! This is an SMS from the Vonage Communication APIs Messages API using exponential backoff and jitter 😄"
for recipient in recipients:
asyncio.ensure_future(send_sms(recipient, message), loop=loop)
if __name__ == "__main__":
loop = uvloop.new_event_loop()
loop.create_task(main(loop))
loop.run_forever()Mais c'est là que vous pourriez utiliser une file d'attente de tâches ou un courtier. Par ailleurs, je ne suis pas non plus un bon citoyen de l'API ! Je sais que l'API Message a une limite de débit de 1 message par seconde pour l'envoi de messages à l'intérieur des États-Unis, mais je n'ai pas de délai dans ma boucle !
Mon script tentera d'effectuer les appels à l'API sans délai entre eux, ce qui déclenchera très rapidement la limitation du débit. Bien que le backoff nous aide à gérer les cas où nous dépassons le débit maximal autorisé, il ne doit être utilisé qu'en dernier recours. Idéalement, pour être le plus efficace possible, nous voulons nous rapprocher de la limite de débit, mais sans la dépasser. L'ajout d'une courte période de sommeil lors de l'ajout de tâches à la boucle devrait permettre d'atteindre cet objectif.
for recipient in recipients:
asyncio.ensure_future(send_sms(recipient, message), loop=loop)
await asyncio.sleep(1) La mise en place de l'ensemble
Dans cet enregistrement, j'ai supprimé le sommeil et modifié l'exemple pour qu'il tente d'effectuer plusieurs centaines de requêtes à la fois, ce qui déclenche presque instantanément l'étranglement par Vonage. Mais regardez ce qui se passe après quelques secondes.
Traffic to an API being backed off over time until blocked requests end
Presque aussitôt que le script commence, nous voyons qu'il dépasse la limite de débit de l'API Messages et que le point de terminaison commence à renvoyer un statut HTTP 429 "Too Many Requests" (trop de demandes). Le script commence donc à s'arrêter. Au début, le nombre de requêtes échouées semble rester à peu près le même, mais comme le délai augmente de façon exponentielle, le nombre de requêtes échouées chute en quelques secondes, et notre script peut recommencer à envoyer.
Essayez vous-même
Sans charge de production, il peut être assez difficile de générer suffisamment de requêtes pour déclencher la limitation de taux. Vous pouvez consulter le script d'exemple de ce tutoriel ainsi que les instructions d'utilisation sur GitHub.
Veuillez noter que l'envoi de messages débitera votre Account. Si vous envoyez régulièrement suffisamment de messages pour que votre tarif soit limité, vous risquez d'enfreindre les conditions de service de Vonage ; en outre, les opérateurs ne verront pas d'un bon œil que vous envoyiez le même message des centaines de fois ! Je vous recommande donc, si vous souhaitez essayer cette méthode, de ne pas la tester avec l'API Messages en direct, mais plutôt de de la simuler. Il existe plusieurs paquets pour httpx afin de faciliter ce processus, notamment pytest-httpx et respx.
Quelle est la prochaine étape ?
Nous n'avons abordé qu'une partie des fonctionnalités disponibles avec Python backoff. Consultez la documentation pour plus d'informations sur la fourniture de différentes stratégies de backoff pour différents types d'exceptionsou les divers événements que le backoff émet. Essayez de modifier le code de l'exemple de sorte que si le backoff exécute le gestionnaire on_giveup le script utilise l'API Voice API de Vonage de Vonage pour appeler l'ingénieur de garde.
Vous avez une question ou souhaitez partager ce que vous construisez ?
Rejoignez la conversation sur le Communauté Vonage Slack
S'abonner à la Bulletin d'information du développeur
Suivez-nous sur X (anciennement Twitter) pour les mises à jour
Regardez les tutoriels sur notre chaîne YouTube
Connectez-vous avec nous sur la page Vonage Developer sur LinkedIn
Restez connecté et tenez-vous au courant des dernières nouvelles, astuces et événements concernant les développeurs.
Pour en savoir plus
Full Stack Python - Les files d'attente de tâches AWS Architecture Blog - Exponential Backoff and Jitter (en anglais)
Partager:
Aaron était défenseur des développeurs chez Nexmo. Ingénieur logiciel chevronné et artiste numérique en herbe, Aaron est souvent amené à créer des choses avec du code ou de l'électronique, parfois les deux. On peut généralement savoir qu'il travaille sur quelque chose de nouveau grâce à l'odeur de composants brûlés qui flotte dans l'air.
