Blog

Déployer un routeur de trafic à l'épreuve du futur basé sur OpenResty

Chargement...

7 min de lecture

Déployer un routeur de trafic à l'épreuve du futur basé sur OpenResty

Des milliers d'applications sont hébergées sur Scalingo, elles peuvent être déployées, redémarrées ou mises à l'échelle à tout moment. Chacune de ces opérations impacte la topologie interne de notre infrastructure. Les routeurs de trafic gèrent les requêtes entrantes et les acheminent dynamiquement dans l'infrastructure. Depuis le début, nous avons compté sur Nginx.

How Scalingo makes migrating from Heroku easy

Des milliers d'applications sont hébergées sur Scalingo, elles peuvent être déployées, redémarrées ou mises à l'échelle à tout moment. Chacune de ces opérations impacte la topologie interne de notre infrastructure. Les routeurs de trafic gèrent les demandes entrantes et les dirigent dynamiquement dans l'infrastructure. Depuis le début, nous avons compté sur Nginx pour alimenter ces serveurs. Avec notre croissance, cette solution est devenue insatisfaisante en termes de performance, fiabilité et flexibilité, restreignant l'implémentation de nouvelles fonctionnalités. Nous devions trouver un moyen d'améliorer ou de remplacer ce logiciel pour continuer à croître. Dans cet article, nous vous proposons un voyage dans les profondeurs de l'infrastructure de Scalingo, vous racontant comment nous avons fini par utiliser OpenResty.

Dans cet article, nous aborderons les principaux inconvénients de la solution actuelle conduisant au déploiement d'un tout nouveau reverse proxy étincelant. Ensuite, nous montrerons comment cette nouvelle version critique a été testée et publiée pour minimiser le risque pour les applications de nos clients.

Le but d'un reverse proxy

Notre infrastructure est composée d'une multitude de serveurs exécutant les applications de nos clients. Ces serveurs ne sont pas accessibles depuis le monde extérieur sauvage, ils sont cachés derrière des serveurs frontaux appelés reverse proxies. Un reverse proxy est un serveur HTTP qui reçoit des demandes et les transmet aux conteneurs d'application. Lorsqu'une demande est reçue, le proxy examine le nom de domaine ciblé grâce à l'en-tête HTTP Host pour une simple demande HTTP ou à l'extension SNI dans le cas d'une connexion HTTPS sécurisée. Une fois le nom de domaine défini, la connexion est transférée au(x) conteneur(s) de l'application correspondant à ce nom de domaine. Dans un deuxième temps, le conteneur d'application traite la demande et envoie la réponse au client via le reverse proxy.



The journey of a request on Scalingo



La configuration d'un reverse proxy se fait généralement dans un ou plusieurs fichiers de configuration afin de configurer la destination des demandes en fonction du nom de domaine ciblé, parfois appelé hôtes virtuels. Lorsque le reverse proxy démarre, il lit les fichiers de configuration et crée des structures de données, il construit une table de routage qui est stockée en mémoire. Cette table n'est pas modifiée après l'initialisation pour accélérer le traitement des demandes. Dans notre cas, une modification de la table de routage est obligatoire dans différents cas : le déploiement d'une application, le redémarrage d'une application, la mise à l'échelle d'une application, etc. Lorsque de tels événements se produisent, nous devons recharger la configuration en lisant à nouveau tous les fichiers et reconstruire la table de routage en mémoire. Cette méthode pour gérer la configuration est limitante lorsque vous avez des milliers de sites web à servir.

Les inconvénients de la solution actuelle

Depuis le début de Scalingo, ces reverse proxies étaient alimentés par de vieilles instances de Nginx avec des milliers de fichiers de configuration, un pour chaque application hébergée. Les inconvénients d'une telle solution sont devenus trop importants pour être ignorés :

  • Les itinéraires ne sont pas instantanément mis à jour lorsqu'ils sont nécessaires. Par exemple, lorsqu'une nouvelle application est déployée, en raison du grand nombre de domaines à gérer, le délai pour recharger la configuration variait de 3 à 8 secondes avec une moyenne de 4 secondes. Ce problème a causé un retard perceptible pour nos clients après une mise à jour de la configuration d'une application.

  • Les connexions durables comme les WebSockets pouvaient être brutalement interrompues si la configuration était rechargée trop de fois durant leur existence : lorsque Nginx recharge sa configuration, le serveur crée une copie de lui-même (fork/exec), et le fils Nginx lit les nouveaux fichiers de configuration et commence à répondre aux requêtes. Le problème est ce qui arrive au processus parent : il est maintenu en vie jusqu'à la dernière connexion terminée. Le problème évident est qu'avec des connexions de longue durée, cela n'arrive jamais. Enfin, nous avons dû collecter des orphelins d'anciennes instances de Nginx pour libérer de la mémoire sur nos serveurs frontaux, rompant les plus anciennes connexions.

Tous ces problèmes sont devenus plus fréquents avec le nombre de modifications de configuration augmentant avec le temps.

Pour ces raisons, nous avons enquêté sur des solutions pour rendre la configuration d'un reverse proxy plus dynamique.

Voici le Saint Graal du reverse proxy

Dans notre quête pour trouver un bon reverse proxy dynamique, nous avons découvert OpenResty, une plateforme web dynamique basée sur Nginx et LuaJIT. Utilisée par de grandes entreprises comme Cloudflare, nous sommes confiants quant à la sérieuse et la durabilité de ce projet. OpenResty n'est pas une solution prête à l'emploi. Elle nécessite encore un certain développement en Lua pour s'adapter parfaitement à notre infrastructure. Trois composants principaux doivent être développés pour que notre configuration OpenResty puisse remplacer entièrement l'ancien Nginx :

  1. Certificat / Clé TLS : chaque application hébergée sur Scalingo a un certificat associé. Avec notre intégration OpenResty, les certificats sont maintenant stockés dans une base de données Redis et sont récupérés dynamiquement lorsqu'une demande atteint un reverse proxy. De plus, pour éviter de frapper Redis à chaque requête HTTPS entrante et réduire la latence, un cache LRU en mémoire a été développé. Lorsque la configuration change, le cache est invalidé à l'aide de Redis Pub/Sub.

  2. Localiser l'application dans l'infrastructure de Scalingo : chaque application est disponible sur un ou plusieurs serveurs dans l'infrastructure de Scalingo. Le reverse proxy doit connaître la destination de toutes les demandes entrantes. Ces informations sont également stockées dans Redis et mises en cache pour obtenir les meilleures performances possibles.

  3. Gestion des logs : lors de l'utilisation de notre ancien setup Nginx, il était configuré pour stocker les logs dans un fichier pour chaque application. Aucune agrégation n'était possible, et c'était une manière vraiment statique d'enregistrer les informations de connexion. Étant donné la nature dynamique d'OpenResty, beaucoup plus de possibilités s'ouvrent à nous. Un micro-service écrit en Go a été créé, qui vise à recevoir des flux de logs et à gérer ces logs exactement comme nous le souhaitons (système de fichiers, indexation, file d'attente de messages, etc.)

Tester OpenResty pour s'assurer que ce n'est pas un faux Graal

La plus grande partie du développement de ce nouveau reverse proxy était la partie test. Nous voulions nous assurer qu'OpenResty surpasse vraiment l'ancien setup Nginx en ce qui concerne les inconvénients évoqués. Les nouvelles et anciennes configurations ont été largement testées afin de les comparer. Dans cette section, nous expliquerons les expériences que nous avons menées et fournirons les résultats pour comparer en profondeur OpenResty avec Nginx.

Dans le reste de ce document, nous appellerons Legacy l'ancien setup avec uniquement Nginx et OpenResty le nouveau.

Tests de Charge

Les tests de charge étaient les premières expériences réalisées sur les deux reverse proxies. L'outil Vegeta v6.1.1 a été utilisé pour envoyer un grand nombre de requêtes HTTP à chaque reverse proxy. Quatre paramètres varient pour cette expérience :

  • Protocole web : HTTP et HTTPS. En production, nous avons actuellement trois fois plus de connexions HTTPS que HTTP. Les requêtes HTTPS nécessitent plus de calculs de la part du reverse proxy.

  • Durée d'une expérience : 1, 2, 5 et 10 minutes.

  • Taux de requêtes : 100, 250, 500 et 750 requêtes par seconde.

  • Avec ou sans rechargement de la configuration. Comme expliqué ci-dessus, recharger la configuration de Nginx est problématique lorsqu'un grand nombre d'applications sont configurées. Un tel rechargement se produit en moyenne 11 fois par heure, avec des pics de plusieurs recharges chaque minute. (Comme vous pouvez vous y attendre, il y a plus de recharges pendant les heures de bureau européennes que les dimanches à minuit CET). Le pire des scénarios sera testé en rechargement de la configuration toutes les 20 secondes.

Métriques

Pour comparer les deux solutions, les métriques suivantes ont été définies :

  • Temps de réponse moyen : le temps de réponse est le temps entre le début de l'envoi de la demande et la fin de la réception de la réponse associée.

  • 99e percentile pour le temps de réponse : 99 % des temps de réponse des requêtes sont inférieurs au 99e percentile.

Résultats

Sur les images suivantes, le temps de réponse moyen et le 99e percentile sont affichés. Cela concerne les expériences sans rechargement de la configuration. Les résultats sont affichés pour l'expérience d'une minute car il n'y avait pas de différence notable entre les différentes durées. L'axe des x représente le nombre de requêtes par seconde et l'axe des y représente le temps de réponse moyen. Les barres roses montrent les résultats pour les expériences avec Legacy et les barres bleues montrent les résultats pour OpenResty. Chaque barre affiche une ligne en haut pour montrer le 99e percentile. Son but est de nous aider à comprendre comment les valeurs sont réparties derrière la moyenne. En résumé : plus la ligne est longue, plus les valeurs sont dispersées (c'est mauvais !).



Load tests with HTTP on OpenResty and Legacy for 1 minute without reloading the configuration





Load tests with HTTPS on OpenResty and Legacy for 1 minute without reloading the configuration



Les résultats HTTP sont vraiment proches entre OpenResty et Legacy. À 500 requêtes par seconde, le temps de réponse moyen fluctue de 22 ms à 27 ms. À 750 requêtes par seconde, le temps de réponse moyen est légèrement plus élevé. Mais surtout, certaines requêtes prennent beaucoup plus de temps pour recevoir une réponse avec un 99e percentile de 45 ms. À 1000 requêtes par seconde, la variation du temps de réponse devient déraisonnable avec un 99e percentile de 246 ms.

En utilisant HTTPS, les résultats sont légèrement défavorables pour Legacy avec un temps de réponse déraisonnable à partir de 500 requêtes par seconde (187 ms en moyenne) tandis que le temps de réponse avec OpenResty est de 50 ms en moyenne.

Regardons maintenant les expériences suivantes dans lesquelles la configuration est rechargée fréquemment. Les résultats sont toujours en faveur d'OpenResty. Cependant, dans les deux cas (OpenResty et Legacy), les performances se dégradent en raison du rechargement fréquent de la configuration.



Load tests with HTTP on OpenResty and Legacy for 1 minute with a reload the configuration





Load tests with HTTPS on OpenResty and Legacy for 1 minute with a reload the configuration



Concernant ce premier lot d'expériences, nous pouvons conclure qu'OpenResty et Legacy fournissent tous deux des résultats similaires avec un léger avantage pour OpenResty lors de la gestion des requêtes HTTPS, ce qui est un bon point car, comme mentionné précédemment, trois quarts des demandes entrantes utilisent HTTPS. Cependant, au-delà de 500 requêtes par seconde, le reverse proxy ne répond pas dans un délai raisonnable dans le pire des scénarios. À ce jour, en production, nos reverse proxies reçoivent de 80 à 170 requêtes par seconde. Ce résultat est un bon indicateur pour nous permettre de détecter quand des proxies supplémentaires doivent être déployés.

Tests WebSockets

Après avoir validé que la configuration OpenResty peut gérer la charge actuelle de Scalingo, nous souhaitons nous assurer que certains des problèmes soulevés lors de l'utilisation de Legacy disparaissent avec OpenResty. L'un de ces problèmes vraiment ennuyeux affecte les connexions WebSocket. Les WebSockets sont des canaux full-duplex souvent utilisés pour faciliter la communication en temps réel entre un client et un serveur.

Lors de l'utilisation de Legacy, la mise à jour de la table de routage nécessite de recharger les fichiers de configuration. Nginx engendre de nouveaux workers qui lisent les fichiers de configuration puis tuent le worker précédent lorsque les nouveaux sont prêts à accepter des connexions. Cette méthode garantit l'absence de temps d'arrêt dans le processus car Nginx ne rompt pas les connexions existantes. Si l'un des anciens workers a toujours une connexion WebSocket ouverte, il passe à l'état shutting down : il ne ferme pas les connexions existantes mais n'accepte pas de nouvelles. Lorsque toutes les connexions existantes sont fermées, le worker peut s'arrêter gracieusement. En attendant, ces workers consomment des ressources : 400 Mo de mémoire en moyenne. Si un grand nombre de recharges de configuration se produit dans un court laps de temps, les ressources utilisées par ces workers ne sont pas négligeables. Par conséquent, nous avons développé un script qui vise à fermer les connexions WebSocket pour libérer les ressources. En production, ce script doit être exécuté toutes les 15 minutes aux heures de pointe. Cette coupure brutale de connexion a conduit à des problèmes pour certaines applications et à de nombreuses questions à notre équipe de support.

Grâce à sa nature dynamique, la configuration d'OpenResty n'est pas stockée dans des fichiers de configuration mais dans une base de données Redis. Ainsi, les fichiers de configuration n'ont pas besoin d'être rechargés : Nginx n'a pas besoin de générer de nouveaux workers lorsque la table de routage est mise à jour. Les workers Nginx ne devraient pas être dans l'état shutting down et les connexions WebSocket en cours n'ont pas besoin d'être brutalement interrompues pour libérer les ressources.

Les expériences suivantes visent à confirmer que les connexions WebSocket de longue durée ne sont pas affectées par une mise à jour de la table de routage de Nginx. Pour réaliser ces expériences, une connexion WebSocket est ouverte à chaque reverse proxy (Legacy et OpenResty) à l'aide de l'outil en ligne de commande wscat. La connexion peut être maintenue ouverte longtemps si elle envoie un message keepalive à intervalles réguliers. (Les connexions inactives sont fermées après 30 secondes). La commande est :

> wscat --connect ws://{openresty,legacy}.scalingo.com/sockjs/6l78edj/websocket --keepalive
> wscat --connect ws://{openresty,legacy}.scalingo.com/sockjs/6l78edj/websocket --keepalive
> wscat --connect ws://{openresty,legacy}.scalingo.com/sockjs/6l78edj/websocket --keepalive
> wscat --connect ws://{openresty,legacy}.scalingo.com/sockjs/6l78edj/websocket --keepalive

Ensuite, la table de routage est mise à jour en changeant le nombre de conteneurs associés à l'application ciblée (sample-ruby-sinatra) :

> SCALINGO_API_URL=https://api-staging.scalingo.com scalingo --app sample-ruby-sinatra scale web:+1
> SCALINGO_API_URL=https://api-staging.scalingo.com scalingo --app sample-ruby-sinatra scale web:+1
> SCALINGO_API_URL=https://api-staging.scalingo.com scalingo --app sample-ruby-sinatra scale web:+1
> SCALINGO_API_URL=https://api-staging.scalingo.com scalingo --app sample-ruby-sinatra scale web:+1

Comme prévu, le worker Nginx ne s'arrête pas immédiatement avec Legacy car il attend que la connexion WebSocket se ferme. Les commandes suivantes mettent en évidence ce comportement :

> ps aux | grep nginx
www-data 13377  3.9  6.4 [...] 15:04 0:06 nginx: worker process is shutting down
www-data 13740  8.2  6.4 [...] 15:05 0:08 nginx

> ps aux | grep nginx
www-data 13377  3.9  6.4 [...] 15:04 0:06 nginx: worker process is shutting down
www-data 13740  8.2  6.4 [...] 15:05 0:08 nginx

> ps aux | grep nginx
www-data 13377  3.9  6.4 [...] 15:04 0:06 nginx: worker process is shutting down
www-data 13740  8.2  6.4 [...] 15:05 0:08 nginx

> ps aux | grep nginx
www-data 13377  3.9  6.4 [...] 15:04 0:06 nginx: worker process is shutting down
www-data 13740  8.2  6.4 [...] 15:05 0:08 nginx

Le processus 13377 attend que la connexion WebSocket se ferme. Le processus 13740 est le nouveau processus engendré après le rechargement de la configuration. Tuer le processus 13377 fermera la connexion.

Du côté d'OpenResty, aucun worker n'est dans l'état shutting down :

> ps aux | grep nginx
root      4044  0.0  0.0 [...] Feb06 0:00   nginx: master process openresty
www-data  4046 10.8  3.0 [...] Feb06 414:38 nginx

> ps aux | grep nginx
root      4044  0.0  0.0 [...] Feb06 0:00   nginx: master process openresty
www-data  4046 10.8  3.0 [...] Feb06 414:38 nginx

> ps aux | grep nginx
root      4044  0.0  0.0 [...] Feb06 0:00   nginx: master process openresty
www-data  4046 10.8  3.0 [...] Feb06 414:38 nginx

> ps aux | grep nginx
root      4044  0.0  0.0 [...] Feb06 0:00   nginx: master process openresty
www-data  4046 10.8  3.0 [...] Feb06 414:38 nginx

Les connexions WebSocket ne sont pas arrêtées par une mise à jour de la table de routage.

Test de Charge du Service de Logs

Le dernier cas à tester concerne les logs. Lors de l'utilisation de Legacy, les logs étaient configurés pour être stockés dans un fichier par application. Étant donné la nature dynamique d'OpenResty, la même configuration simple n'est plus utilisable. Un micro-service écrit en Go a été construit, qui vise à recevoir des flux de logs et à les gérer comme nous le souhaitons (écriture sur disque, envoi au service d'indexation, etc.). OpenResty a été configuré pour envoyer les logs par paquets de 64 Ko. Nous devons nous assurer que ce service gérera la charge.

Le premier backend de stockage de logs développé a été le système de fichiers : lors de la réception de logs, le logger ouvre un fichier pour les logs de chaque application. Il garde en interne une structure de tous les descripteurs de fichiers ouverts. À un moment donné, la rotation des logs devient obligatoire pour prévenir un fichier de log trop volumineux. Notre service doit fermer l'ancien descripteur de fichier après une rotation des logs pour acquérir le descripteur de fichier du nouveau fichier de log. Comme cette procédure utilise des ressources, nous devons nous assurer que la performance ne se dégrade pas trop rapidement avec un rechargement fréquent des descripteurs de fichiers.

Pour cette expérience, un petit programme a été écrit pour envoyer des logs par paquets de 64 Ko au service logger. Deux paramètres varient pour ces tests :

  • Le nombre de paquets de logs envoyés chaque seconde : 100, 250, 312, 375 et 500.

  • Avec ou sans rechargement des descripteurs de fichiers toutes les 20 secondes.

La durée de l'expérience est de 1 minute.

Métriques

Nous voulons nous assurer que le service traite les logs entrants plus rapidement qu'ils n'arrivent. À la fin de l'expérience, le nombre de lignes de logs restantes est affiché. Si celui-ci est supérieur à 0, cela signifie que ce service ne traite pas les lignes de logs assez rapidement.

Résultats

Les résultats affichés ici sont ceux du pire scénario (c'est-à-dire avec un rechargement des descripteurs de fichiers) dans le tableau suivant.

Paquets de logs par seconde

Lignes de log restantes

100

0

250

0

312

4096

375

4096

Ce service gère jusqu'à 250 paquets de 64 Ko de logs par seconde dans le pire des scénarios. Ainsi, le logger traite un maximum de \(250\times 64 = 16000~Ko/s\). Nous avons noté dans une section précédente que chaque reverse proxy ne peut pas gérer plus de 500 requêtes par seconde. Ce service serait surchargé si chaque ligne de log pèse \(\frac{16\,000}{500} = 32~Ko\). Comme cela semble déraisonnablement élevé, ce service est prêt pour une utilisation en production !

Conclusion

De nouveaux reverse proxies ont été déployés il y a quelques semaines sans que vous ne vous en rendiez compte (presque ;-) et nous sommes très heureux des résultats. Avec ce nouveau routeur de trafic basé sur OpenResty en place, nous sommes prêts à soutenir la croissance de Scalingo !

Dans le processus, les tests que nous avons réalisés sur les reverse proxies ont enrichi nos connaissances sur la résilience de l'infrastructure.

Grâce à ce dynamisme nouvellement acquis, le déploiement de nouvelles fonctionnalités a été grandement facilité. Cela nous a déjà permis de déployer le merveilleux intégration Let's Encrypt. Restez à l'écoute, de nouvelles fonctionnalités arrivent !

Etienne Michon, Scalingo

Étienne Michon

Docteur en informatique, Étienne Michon occupe actuellement le poste d'ingénieur R&D chez Scalingo. Il était l'un des premiers employés de Scalingo et il contribue grandement à faire grandir ce blog grâce à ses articles techniques de qualité.

Restez informé

Recevez des articles et des mises à jour de la plateforme dans votre boîte de réception.

Prêt à déployer en toute confiance ?

Découvrez des déploiements sans temps d'arrêt, une mise à l'échelle automatique intelligente et une infrastructure entièrement gérée. Commencez à déployer vos applications sur Scalingo dès aujourd'hui.

Aucune carte de crédit requise • Déployez en quelques minutes • Annulez à tout moment

Dégradé arrière-plan section

Déployez une application ou base de données

Commencez à déployer

Rejoignez les équipes qui misent sur une plateforme conçue pour livrer rapidement, opérer sereinement, avec des valeurs européennes et un support humain.

Dégradé arrière-plan section

Déployez une application ou base de données

Commencez à déployer

Rejoignez les équipes qui misent sur une plateforme conçue pour livrer rapidement, opérer sereinement, avec des valeurs européennes et un support humain.

Dégradé arrière-plan section

Déployez une application ou base de données

Commencez à déployer

Rejoignez les équipes qui misent sur une plateforme conçue pour livrer rapidement, opérer sereinement, avec des valeurs européennes et un support humain.