2009-09-07 5 views
4

Plusieurs de mes vues récupèrent des ressources externes. Je veux m'assurer que sous forte charge je ne fais pas exploser les sites distants (et/ou me faire bannir).Django: limitation de débit simple

Je n'ai que 1 chenille donc avoir un verrou central fonctionnera bien. Donc les détails: Je veux autoriser au plus 3 requêtes à un hôte par seconde, et avoir le reste du bloc pendant 15 secondes maximum. Comment pourrais-je faire cela (facilement)?

Quelques réflexions:

  • Utiliser le cache de django
    • Semble seulement avoir 1 seconde résolution
  • Utilisez un sémaphores basé fichier
    • Facile à faire serrures pour concurrency . Je ne sais pas comment m'assurer que seulement 3 fetches arrivent par seconde.
  • Utilisez un peu d'état de la mémoire partagée
    • Je préfère ne pas installer plus de choses, mais si je vais le faire.

Répondre

1

Une approche; créer une table comme ceci:

class Queries(models.Model): 
    site = models.CharField(max_length=200, db_index=True) 
    start_time = models.DateTimeField(null = True) 
    finished = models.BooleanField(default=False) 

Ce enregistre lorsque chaque requête a soit eu lieu ou aura lieu à l'avenir si la limitation empêche de se produire immédiatement. start_time est l'heure à laquelle l'action doit commencer; C'est dans le futur si l'action est actuellement bloquante. Au lieu de penser en termes de requêtes par seconde, pensons en termes de secondes par requête; dans ce cas, 1/3 seconde par requête.

Chaque fois qu'une action est à effectuer, procédez comme suit:

  • Créer une ligne pour l'action. q = Queries.objects.create (site = nom_site)
  • Sur l'objet que vous venez de créer (q.id), définissez atomiquement start_time sur la plus grande valeur de début de ce site plus 1/3 seconde. Si le plus grand est 10 secondes dans le futur, alors nous pouvons commencer notre action à 10 1/3 secondes. Si ce temps est passé, pincez-le maintenant().
  • Si l'heure de début qui vient d'être définie est dans le futur, dormez jusqu'à ce moment. Si c'est trop loin dans le futur (par exemple plus de 15 secondes), supprimez la ligne et l'erreur.
  • Lorsque la requête est terminée, définissez terminé à True, afin que la ligne puisse être purgée ultérieurement.

L'action atomique est ce qui est important. Vous ne pouvez pas simplement faire un agrégat sur les requêtes, puis enregistrez-le, car il va courir. Je ne sais pas si Django peut le faire en mode natif, mais il est assez facile dans SQL brute:

UPDATE site_queries 
SET start_time = MAX(now(), COALESCE(now(), (
    SELECT MAX(start_time) + 1.0/3 FROM site_queries WHERE site = site_name 
))) 
WHERE id = object_id 

Ensuite, recharger le modèle et le sommeil si nécessaire. Vous devrez également purger les anciennes lignes. Quelque chose comme Queries.objects.filter (site = site, terminé = True) .exclude (id = id) .delete() fonctionnera probablement: supprimer toutes les requêtes terminées à l'exception de celle que vous venez de faire. (De cette façon, vous ne supprimez jamais la dernière requête car les requêtes ultérieures doivent être planifiées.)

Enfin, assurez-vous que la mise à jour n'a pas lieu dans une transaction. Autocommit doit être activé pour que cela fonctionne. Sinon, le UPDATE ne sera pas atomique: il serait possible pour deux requêtes de UPDATE en même temps, et de recevoir le même résultat. Django et Python ont généralement un autocommit désactivé, vous devez donc l'activer puis le désactiver. Avec Postgres, il s'agit de connection.set_isolation_level (ISOLATION_LEVEL_AUTOCOMMIT) et de ISOLATION_LEVEL_READ_COMMITTED. Je ne sais pas comment faire cela avec MySQL.

(je considère que le défaut d'avoir autocommit désactivé dans DB-API Python pour être un sérieux défaut conception.)

L'avantage de cette approche est qu'il est tout à fait simple, avec l'état simple; Vous n'avez pas besoin de choses comme les écouteurs d'événements et les réveils, qui ont leurs propres problèmes.

Un problème possible est que si l'utilisateur annule la demande pendant le délai, que vous fassiez ou non l'action, le délai est toujours appliqué. Si vous ne lancez jamais l'action, les autres demandes ne seront pas déplacées dans l'espace de temps inutilisé.

Si vous ne parvenez pas à activer le fonctionnement automatique, une solution de contournement consiste à ajouter une contrainte UNIQUE à (site, start_time). (Je ne pense pas que Django le comprenne directement, donc vous auriez besoin d'ajouter la contrainte vous-même.) Ensuite, si la course arrive et que deux requêtes sur le même site se terminent en même temps, l'une d'entre elles lancera une contrainte exception que vous pouvez attraper, et vous pouvez simplement réessayer. Vous pouvez également utiliser un agrégat Django normal au lieu du SQL brut. Attraper des exceptions de contraintes n'est pas aussi robuste.

+0

Un autre avantage de ceci, en passant, est qu'il garde l'état à l'intérieur de la base de données, ce qui signifie que la limitation fonctionne même si vous avez plusieurs serveurs faisant toutes les demandes séparément. cas similaires. –

+0

A travaillé super. Je n'ai pas fait le commit atomique car une condition de course n'est pas trop mauvaise (frapper le site avec une requête supplémentaire) par rapport à la complexité accrue du programme. –

+1

Bon point. Parfois, le but habituel de l'exactitude peut manquer la signification pratique. –

1

Qu'en est-il en utilisant un processus différent pour gérer le grattage et une file d'attente pour la communication entre elle et Django?
De cette façon, vous pourrez facilement modifier le nombre de demandes simultanées et également garder automatiquement la trace des demandes, sans bloquer l'appelant.
Surtout, je pense que cela aiderait à réduire la complexité de l'application principale (dans Django).

+0

Cela nécessiterait de maintenir un autre processus de longue durée en dehors d'Apache. Soit dit en passant, un peu plus de complexité sysadmin ... hmm .... –

Questions connexes