12,3 millions de WebSockets simultanés

Une chose à propos des WebSockets est que vous avez besoin de beaucoup de ressources côté client pour générer une charge suffisamment élevée pour que le serveur consomme réellement toutes les ressources CPU.


Il y a plusieurs défis à relever car le protocole WebSockets demande plus de CPU du côté client que du côté serveur. En même temps, vous avez besoin de beaucoup de RAM pour stocker des informations sur les connexions ouvertes si vous en avez des millions.


J'ai eu la chance d'avoir quelques nouveaux serveurs pour une période de temps limitée à ma disposition pour les tests de "burnout" matériel. J'ai donc décidé d'utiliser mon Lua Application Server - LAppS pour faire les deux tâches: tester le matériel et effectuer les tests LAppS à haute charge.


Voyons les détails


Défis


Tout d'abord, les WebSockets côté client nécessitent un RNG assez rapide pour le masquage du trafic, sauf si vous voulez le truquer. Je voulais que les tests soient réalistes, j'ai donc abandonné l'idée de truquer le RNG en le remplaçant par une constante. Cela laisse la seule option - beaucoup de puissance CPU. Comme je l'ai dit, j'ai deux serveurs: un avec deux processeurs Intel Gold 6148 (40 cœurs au total) 256 Go de RAM (DDR4-2666) et un avec quatre processeurs Intel Platinum 8180 (112 cœurs au total) 384 Go de RAM (DDR4-2666) . Je ferai référence à eux en tant que serveur A et serveur B ci-après.


Le deuxième défi - il n'y a pas de bibliothèques ou de suites de tests pour WebSockets qui sont assez rapides. C'est pourquoi j'ai été contraint de développer un module client WebSockets pour LAppS ( cws ).


La configuration des tests


Les deux serveurs ont des cartes 10 GbE à double port. Le port 1 de chaque carte est agrégé à une interface de liaison et ces ports des deux cartes sont connectés les uns aux autres en mode RR. Le port 2 de chaque carte est agrégé à une interface de liaison en mode Active-Backup, connecté via un commutateur Ethernet. Chaque serveur matériel exécutait RHEL 7.6. J'ai dû installer gcc-8.3 à partir des sources pour avoir un compilateur moderne au lieu du gcc-4.8.5 par défaut. Ce n'est pas la meilleure configuration pour les tests de performance, mais les mendiants ne peuvent pas être des sélecteurs.


Les processeurs sur le serveur A (2xGold 6148) ont une fréquence plus élevée que sur le serveur B (4xPlatinum 8180). En même temps, le serveur B a plus de cœurs, j'ai donc décidé d'exécuter un serveur d'écho sur le serveur A et les clients d'écho sur le serveur B.


Je voulais voir comment LAppS se comportait sous une charge élevée avec des millions de connexions et quelle quantité maximale de demandes d'écho par seconde il pouvait servir. Il s'agit en fait de deux ensembles de tests différents, car avec la croissance du nombre de connexions, le serveur et les clients font un travail de plus en plus complexe.


Avant cela, j'ai fait tous les tests sur mon PC personnel que j'utilise pour le développement. Les résultats de ces tests seront utilisés à des fins de comparaison. Mon ordinateur personnel dispose d'un processeur Intel Core i7-7700 à 3,6 GHz (4,0 GHz Turbo) avec 4 cœurs et 32 ​​Go de RAM DDR4-2400. Ce PC exécute Gentoo avec le noyau 4.14.72.


Niveaux des correctifs Spectre et Meltdown
  • PC à la maison
    /sys/devices/system/cpu/vulnerabilities/l1tf:Mitigation: PTE Inversion; VMX: conditional cache flushes, SMT vulnerable /sys/devices/system/cpu/vulnerabilities/meltdown:Mitigation: PTI /sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl and seccomp /sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: __user pointer sanitization /sys/devices/system/cpu/vulnerabilities/spectre_v2:Vulnerable: Minimal generic ASM retpoline, IBPB, IBRS_FW 
  • Serveurs A et B
     /sys/kernel/debug/x86/ibpb_enabled : 1 /sys/kernel/debug/x86/pti_enabled : 1 /sys/kernel/debug/x86/ssbd_enabled : 1 /sys/kernel/debug/x86/ibrs_enabled : 1 /sys/kernel/debug/x86/retp_enabled : 3 

Je ferai une note supplémentaire avec les résultats des tests des serveurs A et B, que ces correctifs soient activés ou non.


Sur mon Home PC, le niveau des correctifs n'est jamais modifié.


Installation de LAppS 0.8.1


Installation des prérequis et des LAppS

LAppS dépend du luajit-2.0.5 ou supérieur, des bibliothèques libcrypto ++ 8.2 et wolfSSL-3.15.7 et ils doivent être installés à partir de sources dans RHEL 7.6 et probablement dans toute autre distribution Linux.


Le préfixe d'installation est / usr / local. Voici la partie Dockerfile assez explicite pour l'installation de wolfSSL


 ADD https://github.com/wolfSSL/wolfssl/archive/v3.15.7-stable.tar.gz ${WORKSPACE} RUN tar xzvf v3.15.7-stable.tar.gz WORKDIR ${WORKSPACE}/wolfssl-3.15.7-stable RUN ./autogen.sh RUN ./configure CFLAGS="-pipe -O2 -march=native -mtune=native -fomit-frame-pointer -fstack-check -fstack-protector-strong -mfpmath=sse -msse2avx -mavx2 -ftree-vectorize -funroll-loops -DTFM_TIMING_RESISTANT -DECC_TIMING_RESISTANT -DWC_RSA_BLINDING" --prefix=/usr/local --enable-tls13 --enable-openssh --enable-aesni --enable-intelasm --enable-keygen --enable-certgen --enable-certreq --enable-curve25519 --enable-ed25519 --enable-intelasm --enable-harden RUN make -j40 all RUN make install 

Et voici la partie pour l'installation de libcrypto ++


 ADD https://github.com/weidai11/cryptopp/archive/CRYPTOPP_8_2_0.tar.gz ${WORKSPACE} RUN rm -rf ${WORKSPACE}/cryptopp-CRYPTOPP_8_2_0 RUN tar xzvf ${WORKSPACE}/CRYPTOPP_8_2_0.tar.gz WORKDIR ${WORKSPACE}/cryptopp-CRYPTOPP_8_2_0 RUN make CFLAGS="-pipe -O2 -march=native -mtune=native -fPIC -fomit-frame-pointer -fstack-check -fstack-protector-strong -mfpmath=sse -msse2avx -mavx2 -ftree-vectorize -funroll-loops" CXXFLAGS="-pipe -O2 -march=native -mtune=native -fPIC -fomit-frame-pointer -fstack-check -fstack-protector-strong -mfpmath=sse -msse2avx -mavx2 -ftree-vectorize -funroll-loops" -j40 libcryptopp.a libcryptopp.so RUN make install 

Et le luajit


 ADD http://luajit.org/download/LuaJIT-2.0.5.tar.gz ${WORKSPACE} WORKDIR ${WORKSPACE} RUN tar xzvf LuaJIT-2.0.5.tar.gz WORKDIR ${WORKSPACE}/LuaJIT-2.0.5 RUN env CFLAGS="-pipe -Wall -pthread -O2 -fPIC -march=native -mtune=native -mfpmath=sse -msse2avx -mavx2 -ftree-vectorize -funroll-loops -fstack-check -fstack-protector-strong -fno-omit-frame-pointer" make -j40 all RUN make install 

Une dépendance facultative de LAppS, qui peut être ignorée, est la nouvelle bibliothèque de Microsoft mimalloc . Cette bibliothèque améliore considérablement les performances (environ 1%) mais nécessite cmake-3.4 ou supérieur. Étant donné le temps limité pour les tests, j'ai décidé de sacrifier l'amélioration des performances mentionnée.


Sur mon PC personnel, je ne désactiverai pas mimalloc pendant les tests.


Permet d'extraire des LAppS du référentiel:


 WORKDIR ${WORKSPACE} RUN rm -rf ITCLib ITCFramework lar utils LAppS RUN git clone https://github.com/ITpC/ITCLib.git RUN git clone https://github.com/ITpC/utils.git RUN git clone https://github.com/ITpC/ITCFramework.git RUN git clone https://github.com/ITpC/LAppS.git RUN git clone https://github.com/ITpC/lar.git WORKDIR ${WORKSPACE}/LAppS 

Maintenant, nous devons supprimer "-lmimalloc" de tous les Makefiles du sous-répertoire nbproject , afin que nous puissions construire LAppS (en supposant que notre répertoire actuel est $ {WORKSPACE} / LAppS)


 # find ./nbproject -type f -name "*.mk" -exec sed -i.bak -e 's/-lmimalloc//g' {} \; # find ./nbproject -type f -name "*.bak" -exec rm {} \; 

Et maintenant, nous pouvons construire LAppS. LAppS fournit plusieurs configurations de build, qui peuvent ou non exclure certaines fonctionnalités côté serveur:


  • avec support SSL et avec support de collecte de statistiques
  • avec SSL et sans collecte de statistiques (bien que la collecte de statistiques minimale se poursuivra car elle est utilisée pour l'optimisation dynamique de LAppS lors de l'exécution)
  • sans SSL et sans collecte de statistiques.

Avant l'étape suivante, assurez-vous que vous êtes le propriétaire du répertoire / opt / lapps (ou exécutez make installs avec sudo)


Faisons deux types de binaires avec prise en charge SSL et collecte de statistiques et sans (en supposant que nous soyons dans le répertoire $ {WORKSPACE} / LAppS):


 # make clean # make CONF=Release.AVX2 install # make CONF=Release.AVX2.NO_STATS.NO_TLS install 

Les binaires résultants sont:


  • dist / Release.AVX2 / GNU-Linux / lapps.avx2
  • dist / Release.AVX2.NO_STATS.NO_TLS / GNU-Linux / lapps.avx2.nostats.notls

Ils seront installés dans / opt / lapps / bin.


Veuillez noter que le module client WebSockets pour Lua est toujours construit avec le support SSL. Qu'il soit activé ou non dépend de l'URI que vous utilisez pour la connexion (ws: // ou wss: //) lors de l'exécution.


Test 1. Performances optimales sur PC domestique. Configuration pour la ligne de base.


J'ai déjà établi que j'obtiens les meilleures performances lorsque je configure quatre instances de service de référence avec 100 connexions chacune. En même temps, je n'ai besoin que de trois IOWorkers et de quatre instances de service d'écho pour obtenir les meilleures performances. Tu te souviens? Je n'ai que 4 cœurs ici.


Le but de ce test est simplement d'établir une base de référence pour une comparaison ultérieure. Rien d'excitant ici vraiment.


Ci-dessous sont les étapes requises pour configurer LAppS pour les tests.


Certificats auto-signés


script certgen.sh
 #!/bin/bash openssl genrsa -out example.org.key 2048 openssl req -new -key example.org.key -out example.org.csr openssl genrsa -out ca.key 2048 openssl req -new -x509 -key ca.key -out ca.crt openssl x509 -req -in example.org.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out example.org.crt cat example.org.crt ca.crt > example.org.bundle.crt 

L'exécution de ce script à partir du répertoire / opt / lapps / conf / ssl créera tous les fichiers requis. Voici la sortie du script et ce que j'ai tapé:


sortie certgen.sh
 # certgen.sh Generating RSA private key, 2048 bit long modulus .................................................................................................................................................................+++++ .....................................+++++ e is 65537 (0x010001) You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:KZ State or Province Name (full name) [Some-State]:none Locality Name (eg, city) []:Almaty Organization Name (eg, company) [Internet Widgits Pty Ltd]:NOORG.DO.NOT.FORGET.TO.REMOVE.FROM.BROWSER Organizational Unit Name (eg, section) []: Common Name (eg server FQDN or YOUR name) []: Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: Generating RSA private key, 2048 bit long modulus ...+++++ ............................................................................+++++ e is 65537 (0x010001) You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:KZ State or Province Name (full name) [Some-State]:none Locality Name (eg, city) []:Almaty Organization Name (eg, company) [Internet Widgits Pty Ltd]:none Organizational Unit Name (eg, section) []: Common Name (eg server FQDN or YOUR name) []:*.example.org Email Address []: Signature ok subject=C = KZ, ST = none, L = Almaty, O = NOORG.DO.NOT.FORGET.TO.REMOVE.FROM.BROWSER Getting CA Private Key 

Configuration de LAppS


Voici le fichier de configuration WebSockets qui est défini pour TLS 1.3, inclut les certificats générés ci-dessus et configuré avec 3 IOWorkers (voir la description détaillée des variables dans le wiki LAppS).


/opt/lapps/etc/conf/ws.json
 { "listeners" : 2, "connection_weight": 1.0, "ip" : "0.0.0.0", "port" : 5083, "lapps_config_auto_save" : true , "workers" : { "workers": 3, "max_connections" : 40000, "auto_fragment" : false, "max_poll_events" : 256, "max_poll_wait_ms" : 10, "max_inbounds_skip" : 50, "input_buffer_size" : 2048 }, "acl" : { "policy" : "allow", "exclude" : [] }, "tls":true, "tls_server_version" : 4, "tls_client_version" : 4, "tls_certificates":{ "ca":"/opt/lapps/conf/ssl/ca.crt", "cert": "/opt/lapps/conf/ssl/example.org.bundle.crt", "key": "/opt/lapps/conf/ssl/example.org.key" } } 

Configuration des services: le serveur d'écho et le client d'écho (benchmark), chacun avec quatre instances.


/opt/lapps/etc/conf/lapps.json
 { "directories": { "app_conf_dir": "etc", "applications": "apps", "deploy": "deploy", "tmp": "tmp", "workdir": "workdir" }, "services": { "echo": { "auto_start": true, "instances": 4, "internal": false, "max_inbound_message_size": 16777216, "protocol": "raw", "request_target": "/echo", "acl" : { "policy" : "allow", "exclude" : [] } }, "benchmark": { "auto_start": true, "instances" : 4, "internal": true, "preload": [ "nap", "cws", "time" ], "depends" : [ "echo" ] } } } 

Le service d'écho est assez banal:


code source du service d'écho
 echo = {} echo.__index = echo; echo.onStart=function() print("echo::onStart"); end echo.onDisconnect=function() end echo.onShutdown=function() print("echo::onShutdown()"); end echo.onMessage=function(handler,opcode, message) local result, errmsg=ws:send(handler,opcode,message); if(not result) then print("echo::OnMessage(): "..errmsg); end return result; end return echo 

Le service de benchmark crée autant de benchmark.max_connections vers benchmark.target , puis s'exécute jusqu'à ce que vous arrêtiez le LAppS. Il n'y a pas de pause dans l'établissement des connexions ou d'écho de bombardements. L'API du module cws ressemble à l'API Web pour WebSockets. Une fois que tous les benchmark.max_connections sont établis, le benchmark imprime la quantité de Sockets connectés. Une fois la connexion établie, le benchmark envoie un benchmark.message au serveur. Une fois que le serveur a répondu, la méthode onmessage anonyme de l'objet cws est invoquée, qui renvoie simplement le même message au serveur.


code source du service de référence
 benchmark={} benchmark.__index=benchmark benchmark.init=function() end benchmark.messages_counter=0; benchmark.start_time=time.now(); benchmark.max_connections=100; benchmark.target="wss://127.0.0.1:5083/echo"; benchmark.message="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; benchmark.meter=function() benchmark.messages_counter=benchmark.messages_counter+1; local slice=time.now() - benchmark.start_time; if( slice >= 1000) then print(benchmark.messages_counter.." messages received per "..slice.."ms") benchmark.messages_counter=0; benchmark.start_time=time.now(); end end benchmark.run=function() local n=nap:new(); local counter=1; n:sleep(1); local array={}; local start=time.now(); while(#array < benchmark.max_connections) and (not must_stop()) do local sock, err_msg=cws:new(benchmark.target, { onopen=function(handler) local result, errstr=cws:send(handler,benchmark.message,2); if(not result) then print("Error on websocket send at handler "..handler..": "..errstr); end end, onmessage=function(handler,message,opcode) benchmark.meter(); cws:send(handler,message,opcode); end, onerror=function(handler, message) print("Client WebSocket connection is failed for socketfd "..handler..". Error: "..message); end, onclose=function(handler) print("Connection is closed for socketfd "..handler); end }); if(sock ~= nil) then table.insert(array,sock); else print(err_msg); err_msg=nil; collectgarbage("collect"); end -- poll events once per 10 outgoing connections -- this will improve the connection establishment speed if counter == 10 then cws:eventLoop(); counter=1 else counter = counter + 1; end end print("Sockets connected: "..#array); benchmark.start_time=time.now(); while not must_stop() do cws:eventLoop(); end for i=1,#array do array[i]:close(); cws:eventLoop(); end end return benchmark; 

Installation des services


Maintenant, nous devons placer les scripts de service dans /opt/lapps/apps//.lua:

  • /opt/lapps/apps/benchmark/benchmark.lua
  • /opt/lapps/apps/echo/echo.lua

Nous sommes prêts à exécuter notre référence maintenant. Exécutez simplement: rm -f lapps.log; /opt/lapps/bin/lapps.avx2 > log rm -f lapps.log; /opt/lapps/bin/lapps.avx2 > log partir du répertoire LAppS et attendez 5 minutes, puis appuyez une fois sur Ctrl-C, pour arrêter le LAppS (il ne s'arrêtera pas immédiatement, il fermera d'abord les connexions), ou deux fois (cela interrompra la séquence d'arrêt).


Ok, nous avons un fichier texte avec quelque chose comme ça à l'intérieur:


sortie de référence

echo :: onStart
echo :: onStart
echo :: onStart
echo :: onStart
courir
1 messages reçus par 3196ms
1 messages reçus par 3299ms
1 messages reçus par 3299ms
1 messages reçus par 3305ms
Prises connectées: 100
Prises connectées: 100
Prises connectées: 100
Prises connectées: 100
134597 messages reçus par 1000 ms
139774 messages reçus par 1000 ms
138521 messages reçus par 1000 ms
139404 messages reçus par 1000 ms
140162 messages reçus par 1000 ms
139337 messages reçus par 1000 ms
140088 messages reçus par 1000 ms
139946 messages reçus par 1000 ms
141204 messages reçus par 1000 ms
137988 messages reçus par 1000 ms
141805 messages reçus par 1000 ms
134733 messages reçus par 1000 ms
...


Nettoyons ce fichier journal comme ceci:


 echo -e ':%s/ms/^V^M/g\n:%g/^$/d\nGdd1G4/Sockets\n4\ndggZZ' | vi $i 

'^ V ^ M' est une représentation visible des touches clés suivantes: Ctrl-V Ctrl-V Ctrl-V Ctrl-M, il sera donc assez inutile de copier-coller simplement cette ligne bash. Brève explication:


  • nous devons remplacer les symboles 'ms' par une fin de ligne, car nous n'en avons pas besoin, ils gâcheront les calculs plus tard et 4 benchmarks travaillant en parallèle peuvent imprimer leurs résultats sur une seule ligne.
  • nous devons ensuite supprimer toutes les lignes vides
  • nous supprimons également la dernière ligne, car nous arrêtons le serveur
  • dans le fichier journal , il n'y aura que des lignes avant constituées de Sockets connectés: 100 (c'est parce que nous n'exécutons que quatre services de référence). Nous sautons donc 4 lignes au-delà de la dernière, et nous supprimons tout en haut du fichier.
  • enregistrer le fichier.

Le fichier est enregistré et vous êtes de retour dans le shell maintenant, et le fichier journal est prêt pour l'étape suivante:


 # awk -v avg=0 '{rps=($1/$5);avg+=rps;}END{print ((avg*1000)/NR)*4}' log 

Ce awk single-liner calcule la quantité de réponses d'écho par ms et accumule le résultat en variable moyenne . Une fois toutes les lignes du fichier journal traitées, il multiplie la moyenne à 1000 pour obtenir le nombre total de réponses d'écho par seconde, en divisant le nombre de lignes et en multipliant le nombre de services de référence. Cela nous donne le nombre moyen de réponses d'écho par seconde pour ce test.


Sur mon PC, ce nombre (ERps) est: 563854


Faisons de même sans support SSL et voyons la différence:


  • changer la valeur de la variable tls dans ws.json en false
  • changez benchmark.target dans benchmark.lua de wss: // en ws: //
  • exécutez rm -f lapps.log; /opt/lapps/bin/lapps.avx2.nostats.notls > log rm -f lapps.log; /opt/lapps/bin/lapps.avx2.nostats.notls > log partir du répertoire LAppS et répétez les étapes ci-dessus.

J'ai: 721236 réponses par seconde


La différence de performances avec SSL et sans SSL est d'environ 22%. Gardons ces chiffres à l'esprit pour référence future.


Avec la même configuration sur le serveur A, j'ai: 421905 ERpS avec SSL et 443145 ERpS sans SSL. Les correctifs pour Spectre et Meltdown ont été désactivés.
Sur le serveur B, j'ai 270996 ERpS avec SSL et 318522 ERpS sans SSL avec les correctifs activés. 385726 et 372126 sans et avec SSL. Les correctifs pour Spectre et Meltdown ont également été désactivés.


Les résultats sont pires avec la même configuration car les processeurs de ces serveurs ont une fréquence moindre.


Veuillez noter que les clients dépendent beaucoup de la disponibilité des données dans / dev / urandom. Cela peut prendre un certain temps avant que les clients ne démarrent réellement, si vous l'avez déjà exécuté une fois. Attendez donc qu'ils commencent à travailler, puis ils sont assez rapides dans ce qu'ils font. Surveillez simplement avec top si les instances de LAppS font vraiment du travail. Si / dev / urandom est épuisé, le LAppS ne dévorera pas vos CPU tant qu'il n'y aura pas de données disponibles.


Se préparer à de gros tests


Tout d'abord, nous devons apporter des modifications aux paramètres du noyau et ne pas oublier l'ulimit pour le nombre de fichiers ouverts. J'ai utilisé presque la même configuration que dans cet article .


Créez un fichier avec le contenu suivant


 : sysctl -w fs.file-max=14000000 sysctl -w fs.nr_open=14000000 ulimit -n 14000000 sysctl -w net.ipv4.tcp_mem="100000000 100000000 100000000" sysctl -w net.core.somaxconn=20000 sysctl -w net.ipv4.tcp_max_syn_backlog=20000 sysctl -w net.ipv4.ip_local_port_range="1025 65535" 

Ensuite, utilisez source ./filename sur les deux serveurs pour modifier les paramètres du noyau et ulimit.


Cette fois, mon but est de créer plusieurs millions de clients sur un serveur et de les connecter au second.


Le serveur A servira de serveur de service d'écho WebSockets côté serveur.
Le serveur B servira de côté client du service d'écho WebSockets.


Il y a une limitation dans LAppS imposée par LuaJIT. Vous ne pouvez utiliser que 2 Go de RAM par machine virtuelle LuaJIT. C'est la limite de RAM que vous pouvez utiliser dans une seule instance LAppS pour tous les services, car les services sont les threads liés à une instance libluajit. Si l'un de vos services qui s'exécute sous la seule instance LAppS dépasse cette limite, alors tous les services seront à court de mémoire.


J'ai découvert que par instance LAppS unique, vous ne pouvez pas établir plus de 2 464 000 WebSockets client avec une taille de message de 64 octets. La taille du message peut légèrement modifier cette limite, car LAppS transmet le message au service cws en allouant l'espace pour ce message dans le LuaJIT.


Cela implique que je dois démarrer plusieurs instances LAppS avec la même configuration sur le serveur B pour établir plus de 2,4 millions de WebSockets. Le serveur A (le serveur d'écho) n'utilise pas autant de mémoire côté LuaJIT, donc la seule instance du LAppS prendra en charge 12,3 millions de WebSockets sans aucun problème.


Préparons deux configurations différentes pour les serveurs A et B.


Serveur A ws.json
 { "listeners" : 224, "connection_weight": 1.0, "ip" : "0.0.0.0", "port" : 5084, "lapps_config_auto_save" : true , "workers" : { "workers": 40, "max_connections" : 2000000, "auto_fragment" : false, "max_poll_events" : 256, "max_poll_wait_ms" : 10, "max_inbounds_skip" : 50, "input_buffer_size" : 2048 }, "acl" : { "policy" : "allow", "exclude" : [] }, "tls": true, "tls_server_version" : 4, "tls_client_version" : 4, "tls_certificates":{ "ca":"/opt/lapps/conf/ssl/ca.crt", "cert": "/opt/lapps/conf/ssl/example.org.bundle.crt", "key": "/opt/lapps/conf/ssl/example.org.key" } } 

Serveur A lapps.json
 { "directories": { "app_conf_dir": "etc", "applications": "apps", "deploy": "deploy", "tmp": "tmp", "workdir": "workdir" }, "services": { "echo": { "auto_start": true, "instances": 40, "internal": false, "max_inbound_message_size": 16777216, "protocol": "raw", "request_target": "/echo", "acl" : { "policy" : "allow", "exclude" : [] } } } } 

Serveur B ws.json
 { "listeners" : 0, "connection_weight": 1.0, "ip" : "0.0.0.0", "port" : 5083, "lapps_config_auto_save" : true , "workers" : { "workers": 0, "max_connections" : 0, "auto_fragment" : false, "max_poll_events" : 2048, "max_poll_wait_ms" : 10, "max_inbounds_skip" : 50, "input_buffer_size" : 2048 }, "acl" : { "policy" : "deny", "exclude" : [] }, "tls": true, "tls_server_version" : 4, "tls_client_version" : 4, "tls_certificates":{ "ca":"/opt/lapps/conf/ssl/ca.crt", "cert": "/opt/lapps/conf/ssl/example.org.bundle.crt", "key": "/opt/lapps/conf/ssl/example.org.key" } } 

Le serveur A a deux interfaces:


  • bond0 - xx203.37
  • bond1 - xx23.10

L'un est plus rapide, l'autre est plus lent, mais cela n'a pas vraiment d'importance. Le serveur sera de toute façon sous forte charge.


Permet de préparer un modèle à partir de notre /opt/lapps/benchmark/benchmark.lua


benchmark.lua
 benchmark={} benchmark.__index=benchmark benchmark.init=function() end benchmark.messages_counter=0; benchmark.start_time=time.now(); benchmark.max_connections=10000; benchmark.target_port=0; benchmark.target_prefix="wss://IPADDR:"; benchmark.target_postfix="/echo"; benchmark.message="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; benchmark.meter=function() benchmark.messages_counter=benchmark.messages_counter+1; local slice=time.now() - benchmark.start_time; if( slice >= 1000) then print(benchmark.messages_counter.." messages received per "..slice.."ms") benchmark.messages_counter=0; benchmark.start_time=time.now(); end end benchmark.run=function() local n=nap:new(); local counter=1; n:sleep(1); local array={}; local start=time.now(); while(#array < benchmark.max_connections) and (not must_stop()) do benchmark.target_port=math.random(5084,5307); local sock, err_msg=cws:new(benchmark.target_prefix..benchmark.target_port..benchmark.target_postfix, { onopen=function(handler) local result, errstr=cws:send(handler,benchmark.message,2); if(not result) then print("Error on websocket send at handler "..handler..": "..errstr); end end, onmessage=function(handler,message,opcode) benchmark.meter(); cws:send(handler,message,opcode); end, onerror=function(handler, message) print("Client WebSocket connection is failed for socketfd "..handler..". Error: "..message); end, onclose=function(handler) print("Connection is closed for socketfd "..handler); end }); if(sock ~= nil) then table.insert(array,sock); else print(err_msg); err_msg=nil; collectgarbage("collect"); -- force garbage collection on connection failure. end -- poll events once per 10 outgoing connections -- this will improve the connection establishment speed if counter == 10 then cws:eventLoop(); counter=1 else counter = counter + 1; end end print("Sockets connected: "..#array); benchmark.start_time=time.now(); while not must_stop() do cws:eventLoop(); end for i=1,#array do array[i]:close(); cws:eventLoop(); end end return benchmark; 

Stockons les adresses IP du serveur A dans les fichiers IP1 et IP2 respectivement, puis:


 for i in 1 2 do mkdir -p /opt/lapps/apps/benchmark${i} sed -e "s/IPADDR/$(cat IP1)/g" /opt/lapps/apps/benchmark/benchmark.lua > /opt/lapps/apps/benchmark${i}/benchmark${i}.lua done 

Maintenant, nous modifions /opt/lapps/etc/conf/lapps.json sur le serveur B pour utiliser ces deux services de référence:


Serveur B lapps.json
 { "directories": { "app_conf_dir": "etc", "applications": "apps", "deploy": "deploy", "tmp": "tmp", "workdir": "workdir" }, "services": { "benchmark1": { "auto_start": true, "instances" : 112, "internal": true, "preload": [ "nap", "cws", "time" ] }, "benchmark2": { "auto_start": true, "instances" : 112, "internal": true, "preload": [ "nap", "cws", "time" ] } } } 

Sommes-nous prêts? Non, nous ne le sommes pas. Parce que nous avons l'intention de générer 2 240 000 sockets sortants vers seulement deux adresses et nous avons besoin de plus de ports côté serveur. Il est impossible de créer plus de 64k connexions vers la même paire ip: port (en fait un peu moins que 64k).


Sur le serveur A dans le fichier LAppS / include / wsServer.h, il y a une fonction void startListeners (). Dans cette fonction, nous remplacerons la ligne 126


 LAppSConfig::getInstance()->getWSConfig()["port"], 

avec ceci:


 static_cast<int>(LAppSConfig::getInstance()->getWSConfig()["port"])+i, 

Reconstruire des Laps:


 make CONF=Release.AVX2 install 

Exécution des tests pour 2 240 000 WebSockets client.


Démarrez le LAppS sur le serveur B, puis démarrez le LAppS sur le serveur B et redirigez la sortie vers un fichier comme celui-ci:


 /opt/lapps/bin/lapps.avx2 > log 

Edit, NB:


 if you want to run several LAppS instances, then create separate directory for each instnace (like run1,run2, etc) and run each instance from within these directories. This is required for lapps.log file and of course for resulting standard output, for not to be overlapped/overwritten by concurrent LAppS instances. 

Cela peut prendre un certain temps jusqu'à ce que toutes les connexions soient établies. Suivons les progrès.


N'utilisez pas netstat pour surveiller les connexions établies, cela ne sert à rien alors qu'il s'exécute indéfiniment après, comme 150 000 connexions qui s'établissent en quelques secondes. Regardez le lapps.log sur le serveur A, dans le répertoire où vous vous trouviez au démarrage de LAppS. Vous pouvez utiliser le one-liner suivant pour voir comment les connexions sont établies et comment sont-elles réparties entre les IOWorkers:


 date;awk '/ will be added to connection pool of worker/{a[$22]++}END{for(i in a){ print i, a[i]; sum+=a[i]} print sum}' lapps.log | sort -n;date 

Voici une idée de la rapidité avec laquelle ces connexions sont établies:


 375874 Sun Jul 21 20:33:07 +06 2019 650874 Sun Jul 21 20:34:42 +06 2019 2894 connections per second 1001874 Sun Jul 21 20:36:45 +06 2019 2974 connections per second 1182874 Sun Jul 21 20:37:50 +06 2019 2784 connections per second 1843874 Sun Jul 21 20:41:44 +06 2019 2824 connections per second 2207874 Sun Jul 21 20:45:43 +06 2019 3058 connections per second 

Sur le serveur B, nous pouvons vérifier le nombre de benchmarks finis d'établir leurs connexions:


 # grep Sockets log | wc -l 224 

Après avoir vu que le nombre est 224, laissez les serveurs fonctionner pendant un certain temps, surveillez l'utilisation du processeur et de la mémoire avec top. Vous pourriez voir quelque chose comme ça (serveur B à gauche et serveur A à droite):


image


Ou comme ceci (serveur B à gauche et serveur A à droite):



Il est clair que le serveur B, où les services clients de référence sont en cours d'exécution, est sous une lourde charge. Le serveur A est également sous forte charge mais pas toujours. Parfois, il se refroidit tandis que le serveur B a du mal avec la quantité de tâches à travailler. Le chiffrement du trafic sur TLS est très gourmand en ressources CPU.


Arrêtons les serveurs (appuyez plusieurs fois sur Ctrl-C) et modifions le journal comme auparavant, mais par rapport à la quantité modifiée de services de référence (224):


 echo -e ':%s/ms/^V^M/g\n:%g/^$/d\nGdd1G224/Sockets\n224\ndggZZ' | vi $i awk -v avg=0 '{rps=($1/$5);avg+=rps;}END{print ((avg*1000)/NR)*224}' log 

Vous souhaiterez peut-être également supprimer plusieurs dernières lignes pour tenir compte des impressions de l'arrêt des services de référence. Vous avez déjà une idée sur la façon de procéder. Je vais donc publier les résultats pour différents scénarios de test et poursuivre les problèmes que j'ai rencontrés.


Résultats pour tous les autres tests (4,9 millions et au-delà)



test # 4 charge


test # 5 charge


test # 5 équilibré pour un partage CPU égal


test n ° 6


test n ° 8


test # 12


test # 13


Problèmes.


Marqué dans le tableau ci-dessus avec du rouge.


L'exécution de plus de 224 instances de référence sur le serveur B s'est avérée être la mauvaise idée, car le serveur côté client était incapable de répartir uniformément le temps CPU entre les processus / threads. Les 224 premières instances de référence qui ont établi toutes leurs connexions ont consommé la plupart des ressources CPU et les autres instances de référence accusent un retard. Même l'utilisation de renice -20 n'aide pas beaucoup (448 instances de référence, 2 instances LAppS):


448 instances de référence


Le serveur B (côté gauche) est soumis à une charge très lourde et le serveur A dispose toujours de ressources CPU gratuites.


J'ai donc doublé benchmark.max_connections au lieu de démarrer davantage d'instances LAppS distinctes.


Toujours pour 12,3 millions de WebSockets à exécuter, j'ai démarré la 5ème instance de LAppS (tests 5 et 6) sans en arrêter quatre déjà en cours d'exécution. Et a joué le rôle de CFQ en suspendant et en redémarrant manuellement les processus prioritaires avec kill -STOP / -CONT ou / et renice leurs priorités. Vous pouvez utiliser le script de modèle suivant pour cela:


 while [ 1 ]; do kill -STOP <4 fast-processes pids> sleep 10 kill -CONT <4 fast-processes pids> sleep 5 done 

Bienvenue dans RHEL 7.6 2019! Honnêtement, j'ai utilisé la commande renice pour la première fois depuis 2009. Ce qui est pire, - je l'ai utilisée presque sans succès cette fois.


J'ai eu un problème avec le moteur de diffusion-collecte des cartes réseau. Je l'ai donc désactivé pour certains tests, sans marquer cet événement dans le tableau.


J'ai eu des interruptions de liaison partielles sous une charge élevée et le bug du pilote NIC, j'ai donc dû ignorer les résultats des tests associés.


Fin de l'histoire


Honnêtement, les tests se sont déroulés beaucoup plus facilement que je ne l'avais prévu.


Je suis convaincu que je n'ai pas réussi à charger LAppS sur le serveur A à son plein potentiel (sans SSL), car je n'avais pas assez de ressources CPU pour le côté client. Bien que TLS 1.3 soit activé, le LAppS sur le serveur A utilisait presque toutes les ressources CPU disponibles.


Je suis toujours convaincu que LAppS est le serveur open source WebSockets le plus évolutif et le plus rapide du marché et que le module client cws WebSockets est le seul du genre, fournissant le cadre pour les tests à haute charge.


Veuillez vérifier les résultats sur votre propre matériel.


Remarque: n'utilisez jamais nginx ou apache comme équilibreur de charge ou comme passe proxy pour WebSockets, sinon vous finirez par réduire les performances par ordre de grandeur. Ils ne sont pas conçus pour WebSockets.

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


All Articles