Variti est spécialisé dans la protection contre les bots et les attaques DDoS, et effectue également des tests de stress et de charge. Étant donné que nous travaillons en tant que service international, il est extrêmement important pour nous d'assurer un échange ininterrompu d'informations entre les serveurs et les clusters en temps réel. Lors de la conférence de Saint HighLoad ++ 2019, le développeur de Variti, Anton Barabanov, a expliqué comment nous utilisons UDP et Tarantool, pourquoi nous avons pris un tel groupe et comment nous avons dû réécrire le module Tarantool de Lua à C.Vous pouvez également lire le
résumé du rapport
via le lien, et voir la vidéo ci-dessous sous le spoiler.
Lorsque nous avons commencé à créer un service de filtrage du trafic, nous avons immédiatement décidé de ne pas traiter le transit IP, mais de protéger HTTP, l'API et les services de jeux. Ainsi, nous terminons le trafic au niveau L7 dans le protocole TCP et le transmettons. La protection sur L3 et 4 en même temps se produit automatiquement. Le diagramme ci-dessous montre le diagramme de service: les demandes des personnes passent par un cluster, c'est-à-dire que les serveurs et les équipements réseau et les robots (représentés comme des fantômes) sont filtrés.

Pour le filtrage, il est nécessaire de diviser le trafic en requêtes distinctes, d'analyser la session avec précision et rapidité, et comme nous ne bloquons pas par les adresses IP, définissez les bots et les personnes à l'intérieur de la connexion à partir de la même adresse IP.
Que se passe-t-il à l'intérieur du cluster
À l'intérieur du cluster, nous avons des nœuds de filtre indépendants, c'est-à-dire que chaque nœud fonctionne seul et uniquement avec son propre trafic. Le trafic est réparti de manière aléatoire entre les nœuds: si, par exemple, 10 connexions sont reçues d'un utilisateur, elles divergent toutes sur des serveurs différents.
Nous avons des exigences de performance très strictes car nos clients sont situés dans différents pays. Et si, par exemple, un internaute suisse visite un site français, il est déjà confronté à 15 millisecondes de retard sur le réseau en raison d'une augmentation de la circulation. Par conséquent, nous ne sommes pas autorisés à ajouter 15 à 20 millisecondes supplémentaires dans notre centre de traitement - la demande se poursuivra de manière critique. De plus, si nous traitons chaque requête HTTP pendant 15 à 20 millisecondes, une simple attaque de 20 000 RPS ajoutera l'ensemble du cluster. Ceci, bien sûr, est inacceptable.
Une autre exigence pour nous était non seulement de suivre la demande, mais aussi de comprendre le contexte. Supposons qu'un utilisateur ouvre une page Web et envoie une demande de barre oblique. Après cela, la page est chargée, et s'il s'agit de HTTP / 1.1, alors le navigateur ouvre 10 connexions au backend et dans 10 flux demande des statiques et dynamiques, fait des requêtes ajax et des sous-requêtes. Si, au lieu de mandater une sous-requête, au cours de la soumission de la page, vous commencez à interagir avec le navigateur et essayez de lui donner, disons, JS Challenge pour la sous-requête, alors vous casserez probablement la page. À la toute première demande, vous pouvez donner des défis CAPTCHA (bien que ce soit mauvais) ou JS, faire une redirection, puis n'importe quel navigateur traitera tout correctement. Après les tests, il est nécessaire de diffuser des informations sur tous les clusters que la session est légitime. S'il n'y a pas d'échange d'informations entre les clusters, les autres nœuds recevront la session du milieu et ne sauront pas si elle doit être ignorée ou non.
Il est également important de répondre rapidement à toutes les surcharges de charge et aux changements de trafic. Si quelque chose saute sur un nœud, alors, après 50 à 100 millisecondes, un saut se produit sur tous les autres nœuds. Par conséquent, il est préférable que les nœuds soient informés des modifications à l'avance et définissent les paramètres de protection à l'avance de sorte qu'aucun saut ne se produise sur tous les autres nœuds.
Un service supplémentaire pour se protéger contre les bots était le service post-balisage: nous mettons un pixel sur le site, écrivons des informations sur le bot / personne et envoyons ces données via l'API. Ces verdicts doivent être conservés quelque part. Autrement dit, si auparavant nous avons parlé de synchronisation au sein d'un cluster, nous ajoutons maintenant la synchronisation des informations entre les clusters également. Ci-dessous, nous montrons le schéma du service au niveau L7.

Entre les clusters
Après avoir créé le cluster, nous avons commencé à évoluer. Nous travaillons via BGP anycast, c'est-à-dire que nos sous-réseaux sont annoncés à partir de tous les clusters et le trafic arrive au plus proche. En termes simples, une demande est envoyée de la France à un cluster à Francfort et de Saint-Pétersbourg à un cluster à Moscou. Les clusters doivent être indépendants. Les flux de réseau sont indépendants indépendants.
Pourquoi est-ce important? Supposons qu'une personne conduise une voiture, travaille avec un site Web à partir d'Internet mobile et traverse un certain Rubicon, après quoi le trafic passe soudainement à un autre cluster. Ou un autre cas: la route de trafic a été reconstruite car quelque part le commutateur ou le routeur a brûlé, quelque chose est tombé, le segment de réseau s'est déconnecté. Dans ce cas, nous fournissons au navigateur (par exemple, dans les cookies) des informations suffisantes pour que lors du passage à un autre cluster, il soit possible d'informer les paramètres nécessaires sur les tests réussis ou échoués.
De plus, vous devez synchroniser le mode de protection entre les clusters. Ceci est important dans le cas d'attaques à faible volume, qui sont le plus souvent menées sous couvert d'inondations. Étant donné que les attaques se déroulent en parallèle, les gens pensent que leur site brise le déluge et ne voient pas d'attaque à faible volume. Dans le cas où un faible volume arrive dans un cluster et se répand dans un autre, la synchronisation du mode de protection est nécessaire.
Et comme déjà mentionné, nous synchronisons entre les clusters les verdicts mêmes qui s'accumulent et sont donnés par l'API. Dans ce cas, il peut y avoir de nombreux verdicts et ils doivent être synchronisés de manière fiable. En mode protection, vous pouvez perdre quelque chose à l'intérieur du cluster, mais pas entre les clusters.
Il est à noter qu'il existe une grande latence entre les clusters: dans le cas de Moscou et de Francfort, c'est 20 millisecondes. Les requêtes synchrones ne peuvent pas être effectuées ici; toutes les interactions doivent passer en mode asynchrone.
Ci-dessous, nous montrons l'interaction entre les clusters. M, l, p sont quelques paramètres techniques pour un échange. U1, u2 est un balisage utilisateur comme illégitime et légitime.

Interaction interne entre les nœuds
Initialement, lorsque nous avons rendu le service, le filtrage au niveau L7 a été démarré sur un seul nœud. Cela a bien fonctionné pour deux clients, mais pas plus. Lors de la mise à l'échelle, nous voulions atteindre une réactivité maximale et une latence minimale.
Il était important de minimiser les ressources CPU consacrées au traitement des paquets, de sorte que l'interaction via, par exemple, HTTP ne conviendrait pas. Il était également nécessaire d'assurer une consommation minimale de frais généraux non seulement pour les ressources informatiques, mais également pour le débit des paquets. Néanmoins, nous parlons de filtrage des attaques, et ce sont des situations dans lesquelles il n'y a évidemment pas assez de performances. Habituellement, lors de la construction d'un projet Web, x3 ou x4 est suffisant pour la charge, mais nous avons toujours x1, car une attaque à grande échelle peut toujours survenir.
Une autre exigence pour l'interface d'interaction est la présence d'un endroit où nous écrirons des informations et d'où nous pourrons ensuite calculer dans quel état nous sommes maintenant. Ce n'est un secret pour personne que le C ++ est souvent utilisé pour développer des systèmes de filtrage. Mais malheureusement, les programmes écrits en C ++ plantent parfois. Parfois, ces programmes doivent être redémarrés pour être mis à jour, ou, par exemple, parce que la configuration n'a pas été relue. Et si nous redémarrons le nœud attaqué, nous devons prendre quelque part le contexte dans lequel ce nœud existait. Autrement dit, le service ne doit pas être apatride, il faut se rappeler qu'il y a un certain nombre de personnes que nous avons bloquées, que nous contrôlons. Il doit y avoir la même communication interne pour que le service puisse recevoir un ensemble principal d'informations. Nous avons pensé à mettre à proximité d'une certaine base de données, par exemple SQLite, mais nous avons rapidement rejeté une telle solution, car il est étrange d'écrire des entrées-sorties sur chaque serveur, cela fonctionnera mal en mémoire.
En fait, nous travaillons avec seulement trois opérations. La première fonction est «envoyer» à tous les nœuds. Cela s'applique, par exemple, aux messages sur la synchronisation de la charge actuelle: chaque nœud doit connaître la charge totale sur la ressource au sein du cluster afin de suivre les pics. La deuxième opération consiste à «sauver», elle concerne les verdicts de vérification. Et la troisième opération est une combinaison de «envoyer à tout le monde» et de «sauvegarder». Ici, nous parlons de messages de changement d'état que nous envoyons à tous les nœuds puis que nous enregistrons afin de pouvoir soustraire. Vous trouverez ci-dessous le schéma d'interaction résultant, dans lequel nous devrons ajouter des paramètres pour l'enregistrement.

Options et résultat
Quelles options pour préserver les verdicts avons-nous envisagées? Tout d'abord, nous pensions aux classiques, RabbitMQ, RedisMQ et à notre propre service basé sur TCP. Nous avons rejeté ces décisions parce qu'elles fonctionnent lentement. Le même TCP ajoute x2 au débit de paquets. De plus, si nous envoyons un message d'un nœud à tous les autres, alors nous devons avoir beaucoup de nœuds d'envoi, ou ce nœud peut empoisonner 1/16 de ces messages que 16 machines peuvent lui envoyer. Il est clair que cela est inacceptable.
En conséquence, nous avons pris la multidiffusion UDP, car dans ce cas, le centre d'envoi est un équipement réseau, qui n'est pas limité dans les performances et vous permet de résoudre complètement les problèmes de vitesse d'envoi et de réception. Il est clair que dans le cas d'UDP, nous ne pensons pas aux formats de texte, mais envoyons des données binaires.
De plus, nous avons immédiatement ajouté un emballage et une base de données. Nous avons pris Tarantool, parce que, premièrement, les trois fondateurs de l'entreprise avaient de l'expérience avec cette base de données, et deuxièmement, elle est aussi flexible que possible, c'est-à-dire qu'il s'agit également d'une sorte de service d'application. De plus, Tarantool a CAPI, et la capacité d'écrire en C est une question de principe pour nous car une protection maximale est requise pour se protéger contre les DDoS. Aucun langage interprété ne peut fournir des performances suffisantes, contrairement à C.
Dans le diagramme ci-dessous, nous avons ajouté une base de données à l'intérieur du cluster, dans laquelle les états de communication interne sont stockés.

Ajouter une base de données
Dans la base de données, nous stockons l'état sous la forme d'un journal des appels. Lorsque nous avons trouvé comment enregistrer des informations, il y avait deux options. Il était possible de stocker un état avec une mise à jour et un changement constants, mais il est plutôt difficile à implémenter. Par conséquent, nous avons utilisé une approche différente.
Le fait est que la structure des données envoyées via UDP est unifiée: il y a un timing, une sorte de code, trois ou quatre champs de données. Nous avons donc commencé à écrire cette structure dans l'espace Tarantool et y avons ajouté un enregistrement TTL, ce qui indique clairement que la structure est obsolète et doit être supprimée. Ainsi, un journal des messages est accumulé dans Tarantool, que nous effaçons avec le timing spécifié. Pour supprimer les anciennes données, nous avons initialement pris expirationd. Par la suite, nous avons dû l'abandonner, car cela a causé certains problèmes, dont nous parlerons ci-dessous. Jusqu'à présent, le schéma: deux bases de données ont été ajoutées à notre structure.

Comme nous l'avons déjà mentionné, en plus de stocker les états de cluster, il est également nécessaire de synchroniser les verdicts. Verdicts nous synchronisons intercluster. En conséquence, il a été nécessaire d'ajouter une installation supplémentaire de Tarantool. Il serait étrange d'utiliser une autre solution, car Tarantool est déjà là et il est idéal pour notre service. Dans la nouvelle installation, nous avons commencé à rédiger des verdicts et à les reproduire avec d'autres clusters. Dans ce cas, nous utilisons non pas maître / esclave, mais maître / maître. Maintenant, à Tarantool, il n'y a qu'un maître / maître asynchrone, ce qui dans de nombreux cas ne convient pas, mais pour nous, ce modèle est optimal. Avec une latence minimale entre les clusters, la réplication synchrone serait gênante, tandis que la réplication asynchrone ne pose pas de problème.
Les problèmes
Mais nous avons eu beaucoup de problèmes.
Le premier bloc de complexité est lié à UDP : ce n'est un secret pour personne que le protocole peut battre et perdre des paquets. Nous avons résolu ces problèmes par la méthode de l'autruche, c'est-à-dire que nous avons simplement caché nos têtes dans le sable. Néanmoins, les dommages par paquets et le réarrangement de leurs emplacements sont impossibles avec nous, car la communication a lieu dans le cadre d'un commutateur, et il n'y a pas de connexions instables et d'équipements de réseau instables.
Il peut y avoir un problème de perte de paquets si une machine se fige, une entrée-sortie se produit quelque part ou un nœud est surchargé. Si un tel blocage s'est produit pendant une courte période, disons 50 millisecondes, cela est terrible, mais il est résolu par l'augmentation des files d'attente sysctl. Autrement dit, nous prenons sysctl, configurons la taille des files d'attente et obtenons un tampon dans lequel tout se trouve jusqu'à ce que le nœud recommence à fonctionner. Si un gel plus long se produit, le problème ne sera pas la perte de connectivité, mais une partie du trafic qui va au nœud. Jusqu'à présent, nous n'avons tout simplement pas eu de tels cas.
Les problèmes de réplication asynchrone de Tarantool étaient beaucoup plus complexes. Dans un premier temps, nous n'avons pas pris maître / maître, mais un modèle plus traditionnel de fonctionnement maître / esclave. Et tout a fonctionné exactement jusqu'à ce que l'esclave prenne la charge principale pendant longtemps. En conséquence, expirationd a fonctionné et supprimé les données sur le maître, mais sur l'esclave, ce n'était pas le cas. En conséquence, lorsque nous sommes passés plusieurs fois du maître à l'esclave et vice versa, tant de données se sont accumulées sur l'esclave qu'à un moment donné, tout s'est cassé. Donc, pour une tolérance aux pannes totale, j'ai dû passer à la réplication maître / maître asynchrone.
Et là encore, des difficultés sont apparues. Premièrement, les clés peuvent se croiser entre différentes répliques. Supposons que, dans le cluster, nous écrivions des données sur un maître, à ce moment la connexion s'est rompue, nous avons tout écrit sur le deuxième maître, et après avoir effectué la réplication asynchrone, il s'est avéré que la même clé primaire dans l'espace et la réplication s'effondraient.
Nous avons résolu ce problème simplement: nous avons pris un modèle dans lequel la clé primaire contient nécessairement le nom du nœud Tarantool sur lequel nous écrivons. Pour cette raison, des conflits ont cessé de se produire, mais une situation est devenue possible lorsque les données utilisateur sont dupliquées. Il s'agit d'un cas extrêmement rare, nous l'avons donc tout simplement négligé. Si la duplication se produit fréquemment, Tarantool a de nombreux index différents, vous pouvez donc toujours faire de la déduplication.
Un autre problème concerne la conservation des verdicts et se pose lorsque les données enregistrées sur un maître ne sont pas encore apparues sur un autre, et qu'une demande est déjà parvenue au premier maître. Pour être honnête, nous n'avons pas encore résolu ce problème et retardons simplement le verdict. Si cela est inacceptable, nous organiserons une sorte de promotion de la préparation des données. C’est ainsi que nous avons traité la réplication maître / maître et ses problèmes.
Il y avait un bloc de problèmes liés directement à Tarantool , à ses pilotes et au module expirationd. Quelque temps après le lancement, des attaques ont commencé à nous arriver chaque jour, respectivement, le nombre de messages que nous enregistrons dans la base de données pour la synchronisation et le stockage du contexte est devenu très important. Et pendant le décapage, tellement de données ont commencé à être supprimées que le garbage collector a cessé de faire face. Nous avons résolu ce problème en écrivant en C notre propre module expirationd appelé IExpire.
Cependant, avec expirationd, il y a encore une difficulté avec laquelle nous n'avons pas encore réussi et qui réside dans le fait que l'expirationd ne fonctionne que sur un seul maître. Et si le nœud expirationd tombe, le cluster perdra ses fonctionnalités critiques. Supposons que nous nettoyions toutes les données de plus d'une heure - il est clair que si un nœud se trouve, disons, cinq heures, alors la quantité de données sera x5 à l'habituel. Et si à ce moment une grande attaque survient, c'est-à-dire que deux mauvais cas coïncident, alors le cluster tombera. Nous ne savons pas encore comment y faire face.
Enfin, il restait des difficultés avec le pilote Tarantool pour C. Lorsque nous avons interrompu le service (par exemple, en raison de conditions de course), il a fallu beaucoup de temps pour trouver la raison et le débogage. Par conséquent, nous venons d'écrire notre pilote Tarantool. Il nous a fallu cinq jours pour mettre en œuvre le protocole ainsi que les tests, le débogage et l'exécution en production, mais nous avions déjà notre propre code pour travailler avec le réseau.
Problèmes à l'extérieur
Rappelons que nous avons déjà la réplication Tarantool prête, nous savons déjà synchroniser les verdicts, mais il n'y a pas encore d'infrastructure pour transmettre des messages sur les attaques ou les problèmes entre les clusters.
Nous avons eu beaucoup de réflexions différentes sur l'infrastructure, y compris l'idée d'écrire notre propre service TCP. Mais il y a toujours un module Tarantool Queue de l'équipe Tarantool. De plus, nous avions déjà Tarantool avec une réplication inter-cluster, des «trous» étaient tordus, c'est-à-dire qu'il n'était pas nécessaire d'aller aux administrateurs et de demander d'ouvrir des ports ou de générer du trafic. Là encore, l'intégration dans la filtration logicielle était prête.
Il y avait un problème avec le nœud hôte. Supposons qu'il existe n nœuds indépendants à l'intérieur d'un cluster et que vous devez choisir celui qui interagira avec la file d'attente d'écriture. Dans le cas contraire, 16 messages seront envoyés ou 16 fois le même message sera soustrait de la file d'attente. Nous avons résolu ce problème simplement: nous enregistrons un nœud responsable dans l'espace Tarantool, et si le nœud brûle, nous changeons simplement l'espace si nous n'oublions pas. Mais si nous oublions, c'est un problème que nous voulons également résoudre à l'avenir.
Vous trouverez ci-dessous un schéma déjà détaillé d'un cluster avec une interface d'interaction.

Ce que je veux améliorer et ajouter
Premièrement, nous voulons publier dans IExpire open source. Il nous semble que c'est un module utile, car il vous permet de tout faire de la même manière que expirationd, mais avec une surcharge presque nulle. Là, vous devez ajouter un index de tri pour supprimer uniquement le tuple le plus ancien. Jusqu'à présent, nous ne l'avons pas fait, car pour nous, l'opération principale de Tarantool est «l'écriture», et un index supplémentaire entraînera une charge supplémentaire en raison de son support. Nous voulons également réécrire la plupart des méthodes de CAPI pour éviter de replier la base de données.
La question reste du choix d'un maître logique, mais il semble que ce problème soit totalement impossible à résoudre. Autrement dit, si le nœud avec expirationd tombe, il ne reste plus qu'à sélectionner manuellement un autre nœud et à exécuter expirationd sur celui-ci. Il est peu probable que cela se produise automatiquement, car la réplication est asynchrone. Bien que nous consulterons probablement à ce sujet avec l'équipe Tarantool.
En cas de croissance exponentielle du cluster, nous devrons également demander de l'aide à l'équipe Tarantool. Le fait est que la réplication tout-à-tout est utilisée pour Tarantool Queue et la sauvegarde intercluster des verdicts. Cela fonctionne bien, alors qu'il y a trois clusters, par exemple, mais quand il y en a 100, le nombre de connexions à surveiller sera incroyablement important et quelque chose se cassera constamment. Deuxièmement, ce n'est pas un fait que Tarantool peut supporter une telle charge.
Conclusions
Les premières conclusions concernent la multidiffusion UDP et Tarantool. Multicast , — , . , , 50 , . , , . UDP multicast , .
— Tarantool. go, php , Tarantool . , . , : Oracle, PostgeSQL.
, , , , : Redis , go, python . . , , open source, , , , , . , . Tarantool, , , Redis, .