Détection de l'effet Slashdot dans nginx

10

Existe-t-il un moyen pour que Nginx me prévienne si les hits d'un référent dépassent un seuil?

Par exemple, si mon site Web est présenté à Slashdot et que tout à coup, j'ai 2K hits à venir dans une heure, je veux être averti quand va au-delà de 1K hits par heure.

Sera-t-il possible de le faire dans Nginx? Peut-être sans lua? (puisque ma prod n'est pas lua compilée)

Quintin Par
la source
4
Qu'est-ce que "Slashdot" ??
ewwhite
J'ai fait quelque chose comme ça pour détecter les ddos ​​sur ngix. Je l'ai atteint en analysant le journal d'accès. J'ai fait un travail cron pour analyser le journal d'accès et compter les connexions IP uniques par heure.
Hex
8
Vous voulez dire que vous voulez que nginx puisse détecter si vous avez été acheté par Dice?
MDMarra
1
@Hex That (et peut-être quelques extraits de votre script) ferait une excellente réponse à cette question :)
voretaq7
3
Vous n'avez probablement plus à vous soucier de devenir Slashdotted. Votre serveur Web devrait pouvoir gérer 4 connexions supplémentaires par heure. Je pourrais avoir peur de me faire redécouper, cependant ...
HopelessN00b

Réponses:

3

La solution la plus efficace pourrait être d'écrire un démon qui tail -fle access.log, et garder une trace du $http_refererchamp.

Cependant, une solution rapide et sale serait d'ajouter un access_logfichier supplémentaire , de ne journaliser que la $http_referervariable avec une coutume log_formatet de faire pivoter automatiquement le journal toutes les X minutes.

  • Cela peut être accompli à l'aide de scripts logrotate standard, qui pourraient avoir besoin de faire des redémarrages gracieux de nginx pour que les fichiers soient rouverts (par exemple, la procédure standard, jetez un œil à / a / 15183322 sur SO pour un temps simple- basé sur un script)…

  • Ou, en utilisant des variables à l'intérieur access_log, éventuellement en extrayant la spécification minute $time_iso8601à l'aide de la mapou d'une ifdirective (selon l'endroit où vous souhaitez mettre votre access_log).

Ainsi, avec ce qui précède, vous pouvez avoir 6 fichiers journaux, chacun couvrant une période de 10 minutes http_referer.Txx{0,1,2,3,4,5}x.log, par exemple, en obtenant le premier chiffre de la minute pour différencier chaque fichier.

Maintenant, tout ce que vous avez à faire est d'avoir un script shell simple qui pourrait s'exécuter toutes les 10 minutes, cattous les fichiers ci-dessus ensemble, le diriger vers, le diriger sortvers uniq -c, vers sort -rn, vers head -16, et vous avez une liste des 16 Referervariantes les plus courantes - libre de décider si des combinaisons de nombres et de champs dépassent vos critères et d'effectuer une notification.

Par la suite, après une seule notification réussie, vous pouvez supprimer tous ces 6 fichiers et, dans les exécutions suivantes, ne pas émettre de notification SAUF si les six fichiers sont présents (et / ou un certain autre numéro comme bon vous semble).

cnst
la source
Cela semble super utile. Je demande peut-être trop, mais comme la réponse précédente, cela vous dérangerait-il d'aider avec un script?
Quintin Par
@QuintinPar Cela semble extra-curriculum! ;-) Si vous le souhaitez, je suis disponible pour la location et le conseil; mon email est [email protected], également à Constantine.SU
cnst
Je comprends parfaitement. Merci beaucoup pour toute l'aide jusqu'à présent. J'espère pouvoir vous permettre un jour :-)
Quintin Par
1
@QuintinPar vous êtes les bienvenus! Pas de soucis, ce devrait être un script assez simple avec les spécifications ci-dessus; juste une question de test, de configuration et de conditionnement, essentiellement. :)
cnst
1
Tu es un super héros!
Quintin Par
13

Je pense que ce serait beaucoup mieux fait avec logtail et grep. Même s'il est possible de le faire avec lua inline, vous ne voulez pas cette surcharge pour chaque demande et surtout vous ne le voulez pas quand vous avez été Slashdotted.

Voici une version de 5 secondes. Collez-le dans un script et mettez-y du texte plus lisible et vous êtes en or.

5 * * * * logtail -f /var/log/nginx/access_log -o /tmp/nginx-logtail.offset | grep -c "http://[^ ]slashdot.org"

Bien sûr, cela ignore complètement reddit.com et facebook.com et tous les millions d'autres sites qui pourraient vous envoyer beaucoup de trafic. Sans oublier 100 sites différents vous envoyant chacun 20 visiteurs. Vous devriez probablement avoir simplement un ancien seuil de trafic qui provoque l'envoi d'un e-mail, quel que soit le référent.

Ladadadada
la source
1
Le problème est d'être proactif. J'ai besoin de savoir depuis n'importe quel site. Une autre question est de savoir où dois-je mettre le seuil? Voulez-vous dire une analyse de journal supplémentaire? De plus, je n'ai pas trouvé –o dans fourmilab.ch/webtools/logtail
Quintin Par
Le seuil dépendra de la quantité de trafic que vos serveurs peuvent gérer. Vous seul pouvez définir cela. Si vous souhaitez une notification plus rapide, exécutez-la toutes les cinq minutes au lieu de toutes les heures et divisez le seuil par 12. L' -o option est pour un fichier de décalage afin qu'il sache où commencer la lecture la prochaine fois.
Ladadadada
@Ladadadada, je ne suis pas d'accord sur le fait que les frais généraux seraient substantiels, voir ma solution - serverfault.com/a/870537/110020 - Je pense que les frais généraux seraient assez minimes si cela est correctement mis en œuvre, en particulier (1), si votre backend est vraiment lent, alors cette surcharge serait négligeable, ou, (2), si votre backend est déjà assez snippy et / ou mis en cache correctement, alors vous devriez avoir peu de problèmes avec la gestion du trafic en premier lieu, et un peu de charge supplémentaire gagnée '' t faire une bosse. Dans l'ensemble, il semble que cette question comporte deux cas d'utilisation, (1), juste d'être informé, et, (2), une mise à l'échelle automatique.
cnst
4

La directive nginx limit_req_zone peut baser ses zones sur n'importe quelle variable, y compris $ http_referrer.

http {
    limit_req_zone  $http_referrer  zone=one:10m   rate=1r/s;

    ...

    server {

        ...

        location /search/ {
            limit_req   zone=one  burst=5;
        }

Vous voudrez également faire quelque chose pour limiter la quantité d'état requise sur le serveur Web, car les en-têtes de référence peuvent être assez longs et variés et vous pouvez voir un variet infini. Vous pouvez utiliser la fonction nginx split_clients pour définir une variable pour toutes les demandes qui est basée sur le hachage de l'en-tête du référent. L'exemple ci-dessous utilise seulement 10 buckes, mais vous pouvez le faire avec 1000 tout aussi facilement. Donc, si vous deviez slashdotted, les personnes dont le référent s'est avéré être haché dans le même compartiment que l'URL slashdot seraient également bloquées, mais vous pouvez limiter cela à 0,1% des visiteurs en utilisant 1000 compartiments dans split_clients.

Cela ressemblerait à quelque chose comme ça (totalement non testé, mais directionnellement correct):

http {

split_clients $http_referrer $refhash {
               10%               x01;
               10%               x02;
               10%               x03;
               10%               x04;
               10%               x05;
               10%               x06;
               10%               x07;
               10%               x08;
               10%               x09;
               *                 x10;
               }

limit_req_zone  $refhash  zone=one:10m   rate=1r/s;

...

server {

    ...

    location /search/ {
        limit_req   zone=one  burst=5;
    }
rmalayter
la source
Il s'agit d'une approche intéressante; cependant, je crois que la question concerne une alerte automatique lorsque l'effet Slashdot se produit; votre solution semble résoudre le blocage aléatoire d'environ 10% des utilisateurs. De plus, je crois que votre raisonnement pour l'utilisation split_clientspeut être mal informé - limit_reqest basé sur un "seau qui fuit", ce qui signifie que l'état global ne doit jamais dépasser la taille de la zone spécifiée.
cnst
2

Oui, bien sûr, c'est possible dans NGINX!

Vous pouvez implémenter le DFA suivant :

  1. Implémentez la limitation de débit, basée sur $http_referer, éventuellement en utilisant une expression régulière via a mappour normaliser les valeurs. Lorsque la limite est dépassée, une page d'erreur interne s'affiche, que vous pouvez intercepter via un error_pagegestionnaire selon une question connexe , en allant vers un nouvel emplacement interne en tant que redirection interne (non visible pour le client).

  2. À l'emplacement ci-dessus pour les limites dépassées, vous effectuez une demande d'alerte, laissant la logique externe effectuer la notification; cette demande est ensuite mise en cache, ce qui garantit que vous n'obtiendrez qu'une seule demande par fenêtre de temps donnée.

  3. Attrapez le code d'état HTTP de la demande précédente (en renvoyant un code d'état ≥ 300 et en utilisant proxy_intercept_errors on, ou, alternativement, utilisez le non-construit par défaut auth_requestou add_after_bodypour faire une sous-demande "gratuite"), et complétez la demande d'origine comme si l'étape précédente n'était pas impliquée. Notez que nous devons activer la error_pagegestion récursive pour que cela fonctionne.

Voici mon PoC et un MVP, également sur https://github.com/cnst/StackOverflow.cnst.nginx.conf/blob/master/sf.432636.detecting-slashdot-effect-in-nginx.conf :

limit_req_zone $http_referer zone=slash:10m rate=1r/m;  # XXX: how many req/minute?
server {
    listen 2636;
    location / {
        limit_req zone=slash nodelay;
        #limit_req_status 429;  #nginx 1.3.15
        #error_page 429 = @dot;
        error_page 503 = @dot;
        proxy_pass http://localhost:2635;
        # an outright `return 200` has a higher precedence over the limit
    }
    recursive_error_pages on;
    location @dot {
        proxy_pass http://127.0.0.1:2637/?ref=$http_referer;
        # if you don't have `resolver`, no URI modification is allowed:
        #proxy_pass http://localhost:2637;
        proxy_intercept_errors on;
        error_page 429 = @slash;
    }
    location @slash {
        # XXX: placeholder for your content:
        return 200 "$uri: we're too fast!\n";
    }
}
server {
    listen 2635;
    # XXX: placeholder for your content:
    return 200 "$uri: going steady\n";
}
proxy_cache_path /tmp/nginx/slashdotted inactive=1h
        max_size=64m keys_zone=slashdotted:10m;
server {
    # we need to flip the 200 status into the one >=300, so that
    # we can then catch it through proxy_intercept_errors above
    listen 2637;
    error_page 429 @/.;
    return 429;
    location @/. {
        proxy_cache slashdotted;
        proxy_cache_valid 200 60s;  # XXX: how often to get notifications?
        proxy_pass http://localhost:2638;
    }
}
server {
    # IRL this would be an actual script, or
    # a proxy_pass redirect to an HTTP to SMS or SMTP gateway
    listen 2638;
    return 200 authorities_alerted\n;
}

Notez que cela fonctionne comme prévu:

% sh -c 'rm /tmp/slashdotted.nginx/*; mkdir /tmp/slashdotted.nginx; nginx -s reload; for i in 1 2 3; do curl -H "Referer: test" localhost:2636; sleep 2; done; tail /var/log/nginx/access.log'
/: going steady
/: we're too fast!
/: we're too fast!

127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.1" 200 16 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.0" 200 16 "test" "curl/7.26.0"

127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 200 20 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"

127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"
%

Vous pouvez voir que la première demande aboutit à un hit frontal et à un backend, comme prévu (j'ai dû ajouter un backend factice à l'emplacement qui a limit_req, car un return 200aurait priorité sur les limites, un vrai backend n'est pas nécessaire pour le reste de la manipulation).

La deuxième demande est au-dessus de la limite, donc, nous envoyons l'alerte (obtention 200), et la mettons en cache, en retournant 429(cela est nécessaire en raison de la limitation susmentionnée selon laquelle les demandes inférieures à 300 ne peuvent pas être interceptées), qui est ensuite interceptée par le frontal , qui est maintenant libre de faire ce qu'il veut.

La troisième demande dépasse toujours la limite, mais nous avons déjà envoyé l'alerte, donc aucune nouvelle alerte n'est envoyée.

Terminé! N'oubliez pas de le bifurquer sur GitHub!

cnst
la source
Deux conditions de limitation de taux peuvent-elles fonctionner ensemble? J'utilise ceci en ce moment: serverfault.com/a/869793/26763
Quintin Par
@QuintinPar :-) Je pense que cela dépendra de la façon dont vous l'utiliserez - le problème évident serait de distinguer en un seul endroit de quelle limite a introduit la condition; mais si celui-ci est un limit_req, et l'autre est un limit_conn, alors utilisez simplement ce qui limit_req_status 429précède (nécessite un tout nouveau nginx), et je pense que vous devriez être en or; il peut y avoir d'autres options (une pour travailler à coup sûr est de chaîner nginx avec set_real_ip_from, mais, selon ce que vous voulez exactement faire, il peut y avoir des choix plus efficaces).
cnst
@QuintinPar s'il manque quelque chose dans ma réponse, faites le moi savoir. BTW, notez qu'une fois la limite atteinte et que votre script doit être appelé, jusqu'à ce que ce script soit correctement mis en cache par nginx, votre contenu peut être retardé; par exemple, vous voudrez peut-être implémenter le script de manière asynchrone avec quelque chose comme golang, ou examiner les options de délai d'attente pour les amonts; peut également vouloir utiliser proxy_cache_lock onégalement, et éventuellement ajouter une gestion des erreurs pour savoir quoi faire en cas d'échec du script (par exemple, utiliser error_pageaussi bien qu'à proxy_intercept_errorsnouveau). J'espère que mon POC est un bon début. :)
cnst
Merci d'avoir tenté cela. Un problème majeur pour moi est toujours, j'utilise limit_req et limit_conn déjà au niveau http et cela s'applique à tous les sites Web que j'ai. Je ne peux pas passer outre cela. Cette solution utilise donc une fonctionnalité destinée à autre chose. Autre approche de cette solution?
Quintin Par
@QuintinPar Qu'en est-il d'avoir des instances nginx imbriquées, où chacune utilisera un seul ensemble de limit_req/ limit_conn? Par exemple, placez simplement la configuration ci-dessus devant votre serveur frontal actuel. Vous pouvez utiliser set_real_ip_fromdans nginx en amont pour vous assurer que les adresses IP sont correctement comptabilisées sur toute la ligne. Sinon, si cela ne correspond toujours pas, je pense que vous devez exprimer vos contraintes exactes et les spécifications de manière plus vivante - de quels niveaux de trafic parlons-nous? À quelle fréquence la statistique doit-elle s'exécuter (1 min / 5 min / 1 h)? Quel est le problème avec l'ancienne logtailsolution?
2017 à 18h18