Slashdot-Effekt in Nginx erkennen


10

Gibt es eine Möglichkeit, Nginx dazu zu bringen, mich zu benachrichtigen, wenn Treffer eines Empfehlers einen Schwellenwert überschreiten?

Beispiel: Wenn meine Website bei Slashdot vorgestellt wird und plötzlich in einer Stunde 2.000 Treffer eingehen, möchte ich benachrichtigt werden, wenn mehr als 1.000 Treffer pro Stunde erzielt werden.

Wird es möglich sein, dies in Nginx zu tun? Möglicherweise ohne lua? (da mein prod nicht lua kompiliert ist)


4
Was ist "Slashdot"?
ewwhite

Ich habe so etwas gemacht, um ddos ​​auf ngix zu erkennen. Ich habe es erreicht, indem ich das Zugriffsprotokoll analysiert habe. Ich habe einen Cron-Job ausgeführt, um das Zugriffsprotokoll zu analysieren und eindeutige IP-Verbindungen pro Stunde zu zählen.
Hex

8
Sie möchten, dass Nginx erkennen kann, ob Sie von Dice gekauft wurden?
MDMarra

1
@ Hex Das (und vielleicht ein paar Ausschnitte aus Ihrem Skript) würde eine ausgezeichnete Antwort auf diese Frage geben :)
voretaq7

3
Wahrscheinlich brauchen Sie sich keine Sorgen mehr zu machen, ob Sie Slashdotted bekommen. Ihr Webserver sollte in der Lage sein, 4 zusätzliche Verbindungen pro Stunde zu verarbeiten. Vielleicht möchten Sie sich Sorgen machen, dass Sie erneut entlassen werden ...
HopelessN00b

Antworten:


3

Die effizienteste Lösung könnte darin bestehen, einen Daemon zu schreiben, würde tail -fdas access.log, und Spur des halten $http_refererFeld.

Eine schnelle und schmutzige Lösung wäre jedoch, eine zusätzliche access_logDatei hinzuzufügen , nur die $http_refererVariable mit einem benutzerdefinierten Protokoll zu protokollieren log_formatund das Protokoll automatisch alle X Minuten zu drehen.

  • Dies kann mithilfe von Standard-Logrotate-Skripten erreicht werden, die möglicherweise ordnungsgemäße Neustarts von Nginx durchführen müssen, damit die Dateien erneut geöffnet werden können (z. B. die Standardprozedur, sehen Sie sich für eine einfache Zeit / a / 15183322 auf SO an. basiertes Skript)…

  • Oder indem Sie Variablen innerhalb verwenden access_log, möglicherweise indem Sie die Minutenangabe $time_iso8601mithilfe der mapoder einer ifDirektive herausholen (je nachdem, wo Sie Ihre platzieren möchten access_log).

Mit dem oben Gesagten können Sie also 6 Protokolldateien haben, die jeweils einen Zeitraum von 10 Minuten abdecken http_referer.Txx{0,1,2,3,4,5}x.log, z. B. indem Sie die erste Ziffer der Minute abrufen, um jede Datei zu unterscheiden.

Jetzt müssen Sie nur noch ein einfaches Shell-Skript caterstellen, das alle 10 Minuten ausgeführt werden kann. Alle oben genannten Dateien werden zusammen geleitet, sortan uniq -c, an sort -rn, an head -16und weitergeleitet, und Sie haben eine Liste der 16 häufigsten RefererVariationen - Sie können frei entscheiden, ob Kombinationen von Zahlen und Feldern Ihre Kriterien überschreiten, und eine Benachrichtigung durchführen.

Anschließend können Sie nach einer einzigen erfolgreichen Benachrichtigung alle diese 6 Dateien entfernen und in nachfolgenden Läufen keine Benachrichtigung ausgeben, es sei denn, alle sechs Dateien sind vorhanden (und / oder eine bestimmte andere Nummer, wie Sie es für richtig halten).


Das sieht super nützlich aus. Ich könnte zu viel verlangen, aber wie bei der früheren Antwort, würde es Ihnen etwas ausmachen, mit einem Skript zu helfen?
Quintin Par

@QuintinPar Das klingt außerhalb des Lehrplans! ;-) Wenn du willst, stehe ich für Miete und Beratung zur Verfügung; Meine E-Mail lautet cnst++@FreeBSD.org, ebenfalls bei Constantine.SU
cnst

Verstehe vollkommen. Vielen Dank für all die Hilfe bis jetzt. Hoffe ich kann es dir eines Tages leisten :-)
Quintin Par

1
@QuintinPar du bist willkommen! Keine Sorge, es sollte ein ziemlich einfaches Skript mit der obigen Spezifikation sein. im Grunde nur eine Frage des Testens, Konfigurierens und Packens. :)
cnst

1
Du bist ein Superheld!
Quintin Par

13

Ich denke, das wäre mit Logtail und Grep weitaus besser. Selbst wenn es möglich ist, mit lua inline zu arbeiten, möchten Sie diesen Overhead nicht für jede Anfrage und insbesondere nicht, wenn Sie einen Slashdotted erhalten haben.

Hier ist eine 5-Sekunden-Version. Stecke es in ein Skript und füge etwas lesbareren Text hinzu, und du bist golden.

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

Das ignoriert natürlich völlig reddit.com und facebook.com und all die Millionen anderer Websites, die Ihnen viel Verkehr schicken könnten. Ganz zu schweigen von 100 verschiedenen Websites, die Ihnen jeweils 20 Besucher senden. Sie sollten wahrscheinlich nur einen einfachen alten Verkehrsschwellenwert haben , der dazu führt, dass eine E-Mail an Sie gesendet wird, unabhängig vom Referrer.


1
Das Problem ist, proaktiv zu sein. Ich muss von jeder Seite wissen. Eine andere Frage ist, wo ich die Schwelle setzen soll. Meinten Sie eine zusätzliche Protokollanalyse? Außerdem habe ich –o in fourmilab.ch/webtools/logtail
Quintin Par

Der Schwellenwert hängt davon ab, wie viel Verkehr Ihre Server verarbeiten können. Nur Sie können das einstellen. Wenn Sie eine schnellere Benachrichtigung wünschen, führen Sie diese alle fünf Minuten statt stündlich aus und teilen Sie den Schwellenwert durch 12. Die -o Option gilt für eine Offset-Datei, damit sie weiß, wo sie beim nächsten Mal mit dem Lesen beginnen soll.
Ladadadada

@Ladadadada, ich bin nicht der Meinung, dass der Overhead erheblich wäre, siehe meine Lösung - serverfault.com/a/870537/110020 - Ich glaube, der Overhead wäre ziemlich gering, wenn dies ordnungsgemäß implementiert wird, insbesondere (1), wenn Ihr Backend vorhanden ist sehr langsam, dann wäre dieser Overhead vernachlässigbar, oder (2) wenn Ihr Backend bereits ziemlich schnell ist und / oder ordnungsgemäß zwischengespeichert wird, sollten Sie in erster Linie kleine Probleme mit der Verkehrsabwicklung haben und ein wenig zusätzliche Last gewinnen. ' t eine Delle machen. Insgesamt scheint diese Frage zwei Anwendungsfälle zu haben: (1) nur informiert zu werden und (2) automatische Skalierung.
cnst

4

Die Direktive nginx limit_req_zone kann ihre Zonen auf einer beliebigen Variablen basieren, einschließlich $ http_referrer.

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

    ...

    server {

        ...

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

Sie sollten jedoch auch etwas tun, um den auf dem Webserver erforderlichen Status zu begrenzen, da die Referrer-Header sehr lang und unterschiedlich sein können und möglicherweise eine unendliche Vielfalt angezeigt wird. Mit der Funktion nginx split_clients können Sie eine Variable für alle Anforderungen festlegen , die auf dem Hash des Referrer-Headers basiert. Das folgende Beispiel verwendet nur 10 Böcke, aber Sie können es genauso einfach mit 1000 machen. Wenn Sie also einen Schrägstrich erhalten, werden auch Personen blockiert, deren Verweis zufällig in denselben Bucket wie die Slashdot-URL gehasht wurde. Sie können dies jedoch auf 0,1% der Besucher beschränken, indem Sie 1000 Buckets in split_clients verwenden.

Es würde ungefähr so ​​aussehen (völlig ungetestet, aber richtungsrichtig):

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;
    }

Dies ist ein interessanter Ansatz; Ich glaube jedoch, dass es sich bei der Frage um eine automatische Warnung handelt, wenn der Slashdot-Effekt auftritt. Ihre Lösung scheint sich zu lösen, indem etwa 10% der Benutzer zufällig blockiert werden. Darüber hinaus glaube ich, dass Ihre Argumentation für die Verwendung split_clientsmöglicherweise falsch informiert ist - limit_reqbasiert auf einem "undichten Eimer", was bedeutet, dass der Gesamtzustand niemals die Größe der angegebenen Zone überschreiten sollte.
cnst

2

Ja, natürlich ist das in NGINX möglich!

Sie können den folgenden DFA implementieren :

  1. Implementieren Sie eine Ratenbegrenzung, basierend auf der $http_refererVerwendung von Regex durch a map, um die Werte zu normalisieren. Wenn das Limit überschritten wird, wird eine interne Fehlerseite ausgelöst, die Sie gemäß einer verwandten Frage über einen error_pageHandler abrufen können und die als interne Umleitung an einen neuen internen Speicherort wechselt (für den Client nicht sichtbar).

  2. An der oben genannten Stelle für überschrittene Grenzwerte führen Sie eine Warnanforderung aus, sodass die externe Logik die Benachrichtigung ausführt. Diese Anfrage wird anschließend zwischengespeichert, um sicherzustellen, dass Sie nur 1 eindeutige Anfrage pro Zeitfenster erhalten.

  3. Fangen Sie den HTTP-Statuscode der vorherigen Anforderung ab (indem Sie einen Statuscode ≥ 300 zurückgeben und proxy_intercept_errors onden nicht standardmäßig nicht erstellten Standardcode verwenden auth_requestoder alternativ add_after_bodyeine "kostenlose" Unteranforderung erstellen) und schließen Sie die ursprüngliche Anforderung so ab, als ob Der vorherige Schritt war nicht beteiligt. Beachten Sie, dass wir die rekursive error_pageBehandlung aktivieren müssen, damit dies funktioniert.

Hier ist mein PoC und ein MVP, ebenfalls unter 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;
}

Beachten Sie, dass dies wie erwartet funktioniert:

% 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"
%

Sie können sehen, dass die erste Anforderung erwartungsgemäß zu einem Front-End- und einem Back-End-Treffer führt (ich musste dem Speicherort ein Dummy-Back-End hinzufügen , bei dem ein echtes Back -End nicht erforderlich ist limit_req, da a return 200Vorrang vor den Grenzwerten hat für den Rest der Handhabung).

Die zweite Anforderung liegt über dem Grenzwert, daher senden wir die Warnung (Abrufen 200) und zwischenspeichern sie und geben sie zurück 429(dies ist aufgrund der oben genannten Einschränkung erforderlich, dass Anforderungen unter 300 nicht abgefangen werden können), die anschließend vom Front-End abgefangen wird , die jetzt frei ist, frei zu tun, was sie will.

Die dritte Anforderung überschreitet immer noch das Limit, aber wir haben die Warnung bereits gesendet, sodass keine neue Warnung gesendet wird.

Erledigt! Vergiss nicht, es auf GitHub zu teilen!


Können zwei Ratenbegrenzungsbedingungen zusammenarbeiten? Ich benutze dies gerade: serverfault.com/a/869793/26763
Quintin Par

@QuintinPar :-) Ich denke, es hängt davon ab, wie Sie es verwenden - das offensichtliche Problem wäre, an einem einzigen Ort zu unterscheiden, von welchem ​​Grenzwert die Bedingung eingeführt wurde; aber wenn dieser ein ist limit_reqund der andere ein ist limit_conn, dann benutze einfach den limit_req_status 429obigen (erfordert sehr neuen Nginx), und ich denke, du solltest golden sein; Möglicherweise gibt es andere Optionen (eine, die sicher funktioniert, ist die Verkettung von Nginx mit set_real_ip_from, aber je nachdem, was genau Sie tun möchten, gibt es möglicherweise effizientere Optionen).
cnst

@QuintinPar Wenn etwas in meiner Antwort fehlt, lass es mich wissen. Übrigens: Wenn das Limit erreicht ist und Ihr Skript aufgerufen werden soll, bis dieses Skript von nginx ordnungsgemäß zwischengespeichert wird, kann sich Ihr Inhalt verzögern. Beispielsweise möchten Sie das Skript möglicherweise asynchron mit etwas wie implementieren golangoder die Timeout-Optionen für Upstreams prüfen. Vielleicht möchten Sie auch verwenden proxy_cache_lock onund möglicherweise eine Fehlerbehandlung hinzufügen, um zu tun, was zu tun ist, wenn das Skript fehlschlägt (z. error_pageB. auch und proxy_intercept_errorserneut). Ich vertraue darauf, dass mein POC ein guter Start ist. :)
cnst

Vielen Dank, dass Sie dies versucht haben. Ein Hauptproblem für mich ist immer noch, dass ich limit_req und limit_conn bereits auf http-Ebene verwende und es für alle Websites gilt, die ich habe. Ich kann es nicht überschreiben. Diese Lösung verwendet also eine Funktionalität, die für etwas anderes gedacht ist. Gibt es einen anderen Ansatz für diese Lösung?
Quintin Par

@QuintinPar Was ist mit verschachtelten Nginx-Instanzen, bei denen jede einen einzelnen Satz von limit_req/ verwendet limit_conn? Stellen Sie beispielsweise die obige Konfiguration einfach vor Ihren aktuellen Front-End-Server. Sie können set_real_ip_fromin Upstream-Nginx verwenden, um sicherzustellen, dass IPs auf der ganzen Linie korrekt berücksichtigt werden. Sonst, wenn es immer noch nicht passt, müssen Sie Ihre genauen Einschränkungen und die Spezifikation anschaulicher formulieren - über welches Verkehrsniveau sprechen wir? Wie oft muss die Statistik ausgeführt werden (1 Minute / 5 Minuten / 1 Stunde)? Was ist los mit der alten logtailLösung?
cnst
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.