Verlassen von Tarantool-Netzwerken. Knotensynchronisation beim Filtern des Datenverkehrs

Bild

Variti ist auf den Schutz vor Bots und DDoS-Angriffen spezialisiert und führt auch Stress- und Lasttests durch. Da wir als internationaler Dienst arbeiten, ist es für uns äußerst wichtig, einen unterbrechungsfreien Informationsaustausch zwischen Servern und Clustern in Echtzeit sicherzustellen. Auf der Saint HighLoad ++ 2019-Konferenz erklärte der Variti-Entwickler Anton Barabanov, wie wir UDP und Tarantool verwenden, warum wir so viel genommen haben und wie wir das Tarantool-Modul von Lua nach C umschreiben mussten.

Sie können die Zusammenfassung des Berichts auch über den Link lesen und das folgende Video unter dem Spoiler sehen.

Video melden


Als wir mit der Erstellung eines Verkehrsfilterdienstes begannen, entschieden wir uns sofort, uns nicht mit IP-Transit zu befassen, sondern HTTP-, API- und Spieledienste zu schützen. Daher beenden wir den Verkehr auf L7-Ebene im TCP-Protokoll und geben ihn weiter. Der gleichzeitige Schutz von L3 und 4 erfolgt automatisch. Das folgende Diagramm zeigt das Servicediagramm: Anforderungen von Personen durchlaufen einen Cluster, dh Server und Netzwerkgeräte, und Bots (als Geist dargestellt) werden gefiltert.



Zum Filtern ist es erforderlich, den Datenverkehr in separate Anforderungen aufzuteilen, die Sitzung genau und schnell zu analysieren. Da wir nicht nach IP-Adressen blockieren, definieren Sie Bots und Personen innerhalb der Verbindung von derselben IP-Adresse aus.

Was passiert im Cluster?


Innerhalb des Clusters haben wir unabhängige Filterknoten, dh jeder Knoten arbeitet für sich und nur mit seinem eigenen Datenverkehr. Der Datenverkehr wird zufällig auf die Knoten verteilt: Wenn beispielsweise 10 Verbindungen von einem Benutzer empfangen werden, unterscheiden sich alle auf verschiedenen Servern.

Wir haben sehr hohe Leistungsanforderungen, da sich unsere Kunden in verschiedenen Ländern befinden. Und wenn beispielsweise ein Benutzer aus der Schweiz eine französische Site besucht, ist er aufgrund einer Zunahme der Verkehrsroute bereits mit einer Netzwerkverzögerung von 15 Millisekunden konfrontiert. Daher sind wir nicht berechtigt, weitere 15 bis 20 Millisekunden in unserem Verarbeitungszentrum hinzuzufügen - die Anfrage wird noch sehr lange dauern. Wenn wir jede HTTP-Anforderung für 15 bis 20 Millisekunden verarbeiten, summiert sich ein einfacher Angriff von 20.000 RPS auf den gesamten Cluster. Dies ist natürlich nicht akzeptabel.

Eine weitere Anforderung für uns war nicht nur die Verfolgung der Anfrage, sondern auch das Verständnis des Kontexts. Angenommen, ein Benutzer öffnet eine Webseite und sendet eine Schrägstrichanforderung. Danach wird die Seite geladen, und wenn es sich um HTTP / 1.1 handelt, öffnet der Browser 10 Verbindungen zum Backend und fordert in 10 Streams Statik und Dynamik an, stellt Ajax-Anfragen und Unterabfragen. Wenn Sie beim Zurückgeben der Seite nicht eine Unterabfrage vertreten, sondern mit dem Browser interagieren und versuchen, ihm beispielsweise JS Challenge für die Unterabfrage zu geben, wird die Seite höchstwahrscheinlich unterbrochen. Bei der allerersten Anfrage können Sie CAPTCHA- (obwohl dies schlecht ist) oder JS-Herausforderungen stellen, eine Weiterleitung vornehmen, und dann verarbeitet jeder Browser alles korrekt. Nach dem Testen müssen Informationen zu allen Clustern verbreitet werden, für die die Sitzung legitim ist. Wenn zwischen den Clustern kein Informationsaustausch stattfindet, empfangen die anderen Knoten die Sitzung von der Mitte und wissen nicht, ob sie übersprungen werden sollen oder nicht.

Es ist auch wichtig, schnell auf alle Laststöße und Änderungen im Verkehr zu reagieren. Wenn etwas auf einen Knoten gesprungen ist, tritt nach 50-100 Millisekunden ein Sprung auf allen anderen Knoten auf. Daher ist es besser, wenn die Knoten die Änderungen im Voraus kennen und die Schutzparameter im Voraus einstellen, damit auf allen anderen Knoten kein Sprung auftritt.
Ein zusätzlicher Dienst zum Schutz vor Bots war der Post-Markup-Dienst: Wir haben ein Pixel auf der Website platziert, Bot- / Personeninformationen geschrieben und diese Daten über die API gesendet. Diese Urteile müssen irgendwo aufbewahrt werden. Das heißt, wenn wir früher über die Synchronisation innerhalb eines Clusters gesprochen haben, fügen wir jetzt auch die Synchronisation von Informationen zwischen Clustern hinzu. Nachfolgend zeigen wir das Schema des Dienstes auf L7-Ebene.



Zwischen Clustern


Nachdem wir den Cluster erstellt hatten, begannen wir mit der Skalierung. Wir arbeiten mit BGP anycast, dh unsere Subnetze werden von allen Clustern angekündigt und der Datenverkehr kommt zum nächsten. Einfach ausgedrückt, eine Anfrage wird von Frankreich an einen Cluster in Frankfurt und von St. Petersburg an einen Cluster in Moskau gesendet. Cluster sollten unabhängig sein. Netzwerkströme sind unabhängig voneinander zulässig.

Warum ist das wichtig? Angenommen, eine Person fährt Auto, arbeitet mit einer Website aus dem mobilen Internet und überquert ein bestimmtes Rubikon. Danach wechselt der Datenverkehr plötzlich zu einem anderen Cluster. Oder ein anderer Fall: Die Verkehrsroute wurde neu erstellt, weil irgendwo der Switch oder Router durchgebrannt ist, etwas heruntergefallen ist und das Netzwerksegment getrennt wurde. In diesem Fall stellen wir dem Browser (z. B. in Cookies) ausreichende Informationen zur Verfügung, damit beim Wechsel zu einem anderen Cluster die erforderlichen Parameter über die bestandenen oder fehlgeschlagenen Tests informiert werden können.
Darüber hinaus müssen Sie den Schutzmodus zwischen Clustern synchronisieren. Dies ist wichtig bei Angriffen mit geringem Volumen, die am häufigsten unter dem Deckmantel von Überschwemmungen durchgeführt werden. Da Angriffe parallel ausgeführt werden, glauben die Benutzer, dass ihre Website die Flut bricht, und sehen keinen Angriff mit geringem Volumen. Für den Fall, dass ein Cluster nur eine geringe Lautstärke aufweist und ein anderer überflutet wird, ist eine Synchronisierung des Schutzmodus erforderlich.

Und wie bereits erwähnt, synchronisieren wir zwischen den Clustern genau die Urteile, die sich ansammeln und von der API gegeben werden. In diesem Fall kann es viele Urteile geben, die zuverlässig synchronisiert werden müssen. Im Schutzmodus können Sie etwas innerhalb des Clusters verlieren, jedoch nicht zwischen den Clustern.

Es ist erwähnenswert, dass zwischen den Clustern eine große Latenz besteht: Im Fall von Moskau und Frankfurt sind dies 20 Millisekunden. Synchrone Anforderungen können hier nicht gestellt werden, alle Interaktionen müssen im asynchronen Modus erfolgen.

Nachfolgend zeigen wir die Interaktion zwischen den Clustern. M, l, p sind einige technische Parameter für einen Austausch. U1, u2 ist ein Benutzer-Markup als unzulässig und legitim.



Interne Interaktion zwischen Knoten


Zu Beginn des Dienstes wurde die Filterung auf L7-Ebene nur auf einem Knoten gestartet. Dies funktionierte gut für zwei Kunden, aber nicht mehr. Bei der Skalierung wollten wir maximale Reaktionsfähigkeit und minimale Latenz erreichen.

Es war wichtig, die für die Verarbeitung von Paketen aufgewendeten CPU-Ressourcen zu minimieren, damit eine Interaktion über beispielsweise HTTP nicht geeignet wäre. Es war auch notwendig, einen minimalen Overhead-Verbrauch von nicht nur Rechenressourcen, sondern auch der Paketrate sicherzustellen. Trotzdem geht es um das Filtern von Angriffen, und dies sind Situationen, in denen offensichtlich nicht genügend Leistung vorhanden ist. Normalerweise reicht beim Erstellen eines Webprojekts x3 oder x4 für die Last aus, aber wir haben immer x1, da immer ein groß angelegter Angriff erfolgen kann.

Eine weitere Voraussetzung für die Interaktionsschnittstelle ist das Vorhandensein eines Ortes, an dem wir Informationen schreiben und von dem aus wir dann berechnen können, in welchem ​​Zustand wir uns gerade befinden. Es ist kein Geheimnis, dass C ++ häufig zur Entwicklung von Filtersystemen verwendet wird. Leider stürzen in C ++ geschriebene Programme manchmal ab. Manchmal müssen solche Programme neu gestartet werden, um sie zu aktualisieren, oder zum Beispiel, weil die Konfiguration nicht erneut gelesen wurde. Und wenn wir den angegriffenen Knoten neu starten, müssen wir irgendwo den Kontext nehmen, in dem dieser Knoten existiert hat. Das heißt, der Dienst sollte nicht staatenlos sein, sondern sich daran erinnern, dass es eine bestimmte Anzahl von Personen gibt, die wir blockiert haben und die wir überprüfen. Es muss dieselbe interne Kommunikation vorhanden sein, damit der Dienst einen primären Satz von Informationen empfangen kann. Wir hatten Gedanken, in die Nähe einer bestimmten Datenbank zu gehen, zum Beispiel SQLite, aber wir haben eine solche Lösung schnell verworfen, da es seltsam ist, Input-Output auf jeden Server zu schreiben. Dies funktioniert schlecht im Speicher.

Tatsächlich arbeiten wir mit nur drei Operationen. Die erste Funktion ist "Senden" an alle Knoten. Dies gilt beispielsweise für Nachrichten zur Synchronisierung der aktuellen Last: Jeder Knoten muss die Gesamtlast der Ressource innerhalb des Clusters kennen, um Spitzenwerte verfolgen zu können. Die zweite Operation ist das „Speichern“, es handelt sich um Überprüfungsurteile. Und die dritte Operation ist eine Kombination aus "An alle senden" und "Speichern". Hier geht es um Statusänderungsnachrichten, die wir an alle Knoten senden und dann speichern, um subtrahieren zu können. Unten sehen Sie das resultierende Interaktionsschema, in dem wir Parameter zum Speichern hinzufügen müssen.



Optionen und Ergebnis


Welche Möglichkeiten zur Bewahrung von Urteilen haben wir geprüft? Zunächst haben wir über die Klassiker RabbitMQ, RedisMQ und unseren eigenen TCP-basierten Service nachgedacht. Wir haben diese Entscheidungen abgelehnt, weil sie langsam funktionieren. Das gleiche TCP erhöht die Paketrate um x2. Wenn wir eine Nachricht von einem Knoten an alle anderen senden, benötigen wir entweder viele Sendeknoten, oder dieser Knoten kann 1/16 der Nachrichten vergiften, die 16 Computer an ihn senden können. Es ist klar, dass dies nicht akzeptabel ist.

Aus diesem Grund haben wir UDP-Multicast verwendet, da das Sendezentrum in diesem Fall eine Netzwerkausrüstung ist, deren Leistung nicht eingeschränkt ist und die es Ihnen ermöglicht, Probleme mit der Sende- und Empfangsgeschwindigkeit vollständig zu lösen. Es ist klar, dass wir bei UDP nicht an Textformate denken, sondern Binärdaten senden.

Zusätzlich haben wir sofort Verpackung und eine Datenbank hinzugefügt. Wir haben Tarantool gewählt, weil zum einen alle drei Gründer des Unternehmens Erfahrung mit dieser Datenbank hatten und zum anderen ist sie so flexibel wie möglich, dh es handelt sich auch um eine Art Anwendungsservice. Darüber hinaus verfügt Tarantool über CAPI, und die Fähigkeit, in C zu schreiben, ist für uns eine Grundsatzfrage, da zum Schutz vor DDoS ein maximaler Schutz erforderlich ist. Im Gegensatz zu C kann keine interpretierte Sprache eine ausreichende Leistung erbringen.

In der folgenden Abbildung haben wir eine Datenbank innerhalb des Clusters hinzugefügt, in der die Zustände für die interne Kommunikation gespeichert sind.



Datenbank hinzufügen


In der Datenbank speichern wir den Status in Form eines Anrufprotokolls. Als wir herausfanden, wie Informationen gespeichert werden können, gab es zwei Möglichkeiten. Es war möglich, einen Status mit ständiger Aktualisierung und Änderung zu speichern, aber die Implementierung ist ziemlich schwierig. Deshalb haben wir einen anderen Ansatz gewählt.

Tatsache ist, dass die Struktur der über UDP gesendeten Daten einheitlich ist: Es gibt Timing, eine Art Code, drei oder vier Datenfelder. Also haben wir begonnen, diese Struktur in Space Tarantool zu schreiben und dort einen TTL-Datensatz hinzugefügt, der deutlich macht, dass die Struktur veraltet ist und gelöscht werden muss. Somit wird in Tarantool ein Nachrichtenprotokoll akkumuliert, das wir mit dem angegebenen Timing löschen. Um alte Daten zu löschen, haben wir zunächst expirationd verwendet. Anschließend mussten wir es aufgeben, da es bestimmte Probleme verursachte, die wir weiter unten diskutieren werden. Bisher das Schema: Darauf wurden zwei Datenbanken zu unserer Struktur hinzugefügt.



Wie bereits erwähnt, müssen neben der Speicherung von Clusterzuständen auch Urteile synchronisiert werden. Urteile synchronisieren wir Intercluster. Dementsprechend musste eine zusätzliche Installation von Tarantool hinzugefügt werden. Es wäre seltsam, eine andere Lösung zu verwenden, da Tarantool bereits vorhanden ist und sich ideal für unseren Service eignet. In der neuen Installation haben wir begonnen, Urteile zu schreiben und sie mit anderen Clustern zu replizieren. In diesem Fall verwenden wir nicht Master / Slave, sondern Master / Master. Jetzt gibt es in Tarantool nur einen asynchronen Master / Master, was in vielen Fällen nicht geeignet ist, aber für uns ist dieses Modell optimal. Bei minimaler Latenz zwischen Clustern würde die synchrone Replikation im Weg stehen, während die asynchrone Replikation keine Probleme verursacht.

Die Probleme


Aber wir hatten viele Probleme. Der erste Komplexitätsblock bezieht sich auf UDP : Es ist kein Geheimnis, dass das Protokoll Pakete schlagen und verlieren kann. Wir haben diese Probleme mit der Straußmethode gelöst, dh wir haben einfach unsere Köpfe im Sand versteckt. Trotzdem ist eine Paketbeschädigung und Neuordnung ihrer Orte bei uns nicht möglich, da die Kommunikation im Rahmen eines einzelnen Switches erfolgt und es keine instabilen Verbindungen und instabilen Netzwerkgeräte gibt.

Es kann ein Problem mit dem Paketverlust auftreten, wenn ein Computer einfriert, irgendwo eine Eingabe / Ausgabe auftritt oder ein Knoten überlastet ist. Wenn ein solcher Hang für einen kurzen Zeitraum, beispielsweise 50 Millisekunden, aufgetreten ist, ist dies schrecklich, wird jedoch durch erhöhte Sysctl-Warteschlangen gelöst. Das heißt, wir nehmen sysctl, konfigurieren die Größe der Warteschlangen und erhalten einen Puffer, in dem alles liegt, bis der Knoten wieder funktioniert. Wenn ein längeres Einfrieren auftritt, ist das Problem nicht der Verlust der Konnektivität, sondern ein Teil des Datenverkehrs, der zum Knoten geht. Bisher hatten wir einfach keine solchen Fälle.

Asynchrone Replikationsprobleme von Tarantool waren viel komplexer. Anfangs haben wir nicht Master / Master genommen, sondern ein traditionelleres Modell für den Betrieb von Master / Slave. Und alles funktionierte genau so lange, bis der Slave lange Zeit die Hauptlast übernahm. Infolgedessen funktionierte expirationd und löschte Daten auf dem Master, auf dem Slave jedoch nicht. Dementsprechend sammelten sich beim mehrmaligen Wechsel von Master zu Slave und zurück so viele Daten auf dem Slave an, dass irgendwann alles kaputt ging. Für eine vollständige Fehlertoleranz musste ich also auf asynchrone Master / Master-Replikation umsteigen.

Und auch hier traten wieder Schwierigkeiten auf. Erstens können sich Schlüssel zwischen verschiedenen Replikaten schneiden. Angenommen, wir haben innerhalb des Clusters Daten an einen Master geschrieben. Zu diesem Zeitpunkt wurde die Verbindung unterbrochen. Wir haben alles an den zweiten Master geschrieben. Nachdem wir die asynchrone Replikation durchgeführt hatten, stellte sich heraus, dass derselbe Primärschlüssel im Speicher und die Replikation auseinander fielen.

Wir haben dieses Problem einfach gelöst: Wir haben ein Modell genommen, in dem der Primärschlüssel unbedingt den Namen des Tarantool-Knotens enthält, auf den wir schreiben. Aufgrund dessen traten keine Konflikte mehr auf, aber eine Situation ist möglich geworden, wenn Benutzerdaten dupliziert werden. Dies ist ein äußerst seltener Fall, daher haben wir ihn erneut einfach vernachlässigt. Wenn häufig Duplikationen auftreten, verfügt Tarantool über viele verschiedene Indizes, sodass Sie jederzeit eine Deduplizierung durchführen können.

Ein weiteres Problem betrifft die Bewahrung von Urteilen und tritt auf, wenn die auf einem Master aufgezeichneten Daten noch nicht auf einem anderen Master erschienen sind und beim ersten Master bereits eine Anfrage eingegangen ist. Um ehrlich zu sein, haben wir dieses Problem noch nicht gelöst und verzögern lediglich das Urteil. Wenn dies nicht akzeptabel ist, werden wir eine Art Push zur Datenbereitschaft organisieren. So haben wir mit der Master / Master-Replikation und ihren Problemen umgegangen.

Es gab eine Reihe von Problemen, die direkt mit Tarantool , seinen Treibern und dem Ablaufmodul zusammenhängen . Einige Zeit nach dem Start kamen jeden Tag Angriffe auf uns zu. Die Anzahl der Nachrichten, die wir zur Synchronisierung und Speicherung des Kontexts in der Datenbank speichern, ist sehr groß geworden. Und während des Strippens wurden so viele Daten gelöscht, dass der Garbage Collector die Bewältigung stoppte. Wir haben dieses Problem gelöst, indem wir in C unser eigenes Ablaufmodul namens IExpire geschrieben haben.

Mit expirationd gibt es jedoch eine weitere Schwierigkeit, mit der wir noch nicht fertig geworden sind, und die darin liegt, dass expirationd nur auf einem Master funktioniert. Wenn der Knoten expirationd ausfällt, verliert der Cluster wichtige Funktionen. Angenommen, wir bereinigen alle Daten, die älter als eine Stunde sind. Wenn ein Knoten beispielsweise fünf Stunden liegt, beträgt die Datenmenge x5 gegenüber dem üblichen Wert. Und wenn in diesem Moment ein großer Angriff erfolgt, dh zwei schlimme Fälle zusammenfallen, fällt der Cluster. Wir wissen noch nicht, wie wir damit umgehen sollen.

Schließlich gab es weiterhin Schwierigkeiten mit dem Tarantool-Treiber für C. Als wir den Dienst ausfielen (z. B. aufgrund der Rennbedingungen), dauerte es lange, den Grund und das Debugging zu finden. Deshalb haben wir gerade unseren Tarantool-Treiber geschrieben. Wir haben fünf Tage gebraucht, um das Protokoll zusammen mit dem Testen, Debuggen und Ausführen in der Produktion zu implementieren, aber wir hatten bereits unseren eigenen Code für die Arbeit mit dem Netzwerk.

Probleme draußen


Denken Sie daran, dass wir die Tarantool-Replikation bereits bereit haben. Wir wissen bereits, wie Urteile synchronisiert werden, aber es gibt keine Infrastruktur für die Übertragung von Nachrichten über Angriffe oder Probleme zwischen Clustern.
Wir hatten viele verschiedene Gedanken über die Infrastruktur, einschließlich des Gedankens, unseren eigenen TCP-Dienst zu schreiben. Trotzdem gibt es ein Tarantool-Warteschlangenmodul vom Tarantool-Team. Darüber hinaus hatten wir bereits Tarantool mit clusterübergreifender Replikation. Die „Löcher“ waren verdreht, dh es war nicht erforderlich, zu den Administratoren zu gehen und nach Ports zu fragen oder den Datenverkehr zu steuern. Auch hier war die Integration in die Softwarefiltration bereit.

Es gab eine Schwierigkeit mit dem Hostknoten. Angenommen, ein Cluster enthält n unabhängige Knoten, und Sie müssen den Knoten auswählen, der mit der Schreibwarteschlange interagiert. Andernfalls werden 16 Nachrichten gesendet oder 16 Mal dieselbe Nachricht von der Warteschlange abgezogen. Wir haben dieses Problem einfach gelöst: Wir registrieren einen verantwortlichen Knoten im Raum Tarantool, und wenn der Knoten abbrennt, ändern wir einfach den Raum, wenn wir nicht vergessen. Aber wenn wir vergessen, dann ist dies ein Problem, das wir auch in Zukunft lösen wollen.

Unten sehen Sie ein bereits detailliertes Diagramm eines Clusters mit einer Interaktionsschnittstelle.



Was ich verbessern und hinzufügen möchte


-, open source IExpire. , , , expirationd, overhead. , tuple. , Tarantool — “”, - . CAPI, .

, , . , expirationd , expirationd . , . , , Tarantool.

Tarantool. , Tarantool Queue “--”. , , , , 100, , , , - . -, , Tarantool .

Schlussfolgerungen


UDP multicast Tarantool. Multicast , — , . , , 50 , . , , . UDP multicast , .

— Tarantool. go, php , Tarantool . , . , : Oracle, PostgeSQL.

, , , , : Redis , go, python . . , , open source, , , , , . , . Tarantool, , , Redis, .

Source: https://habr.com/ru/post/de459264/


All Articles