Pour utiliser le serveur JMAP de cyrus dans notre application de webmail, nous devons nous authentifier. Nous voyons qu’il n’y a pas d’autres mécanismes que de faire du basic auth HTTP. C’est un peu embêtant car cela oblige à envoyer les informations (utilisateur/mot de passe) de connexion pour chaque requête JMAP. Nous nous décidons alors à faire un petit proxy (comme ils l’indiquent dans l’issue) qui ajoutera les informations côté serveur.

C’est très simple de le faire en python puisque notre backend de connexion est en python/pyramid. La vérification de signature du JWT token, la lecture des informations dans la base Redis et le chiffrement/déchiffrement en AES est fait en 10 lignes de code. Le souci c’est que toutes les requêtes relatives aux mails (lister les mails, afficher un mail, etc.) passeront par ce proxy. C’est impactant en terme de sollicitation de nos serveurs. Nous faisons l’hypothèse qu’un proxy en Rust/hyper sera meilleur que notre backend python.

Alors nous implémentons un proxy en Rust. Il va juste aller chercher les credentials dans la base Redis, les décoder et les ajouter encodées en base64 dans le header Authorization. Nous choisissons de le faire en asynchrone avec hyper pour qu’il soit le plus efficace possible.

hyper auth proxy

A présent nous pouvons comparer notre proxy en python avec celui en Rust pour vérifier notre hypothèse. Pour cela nous faisons tourner un serveur nginx sur une machine, et sur une autre nous lançons notre proxy. L’injecteur ab tourne également sur la machine du proxy.

Ce test de performance nous intéresse car c’est typiquement un problème I/O bound (relatifs aux entrées sorties réseau) : le proxy reçoit des requêtes HTTP, va chercher (via le réseau encore) une valeur dans la base de données, et fait une requête vers le serveur nginx. Or nous savons que pour les problèmes CPU bound Rust est très performant. Qu’en est-il dans le cas de notre proxy ?

Nous faisons 7 tirs de 10000 requêtes:

  • un tir de référence avec une concurrence de 1 (un utilisateur), directement sur nginx pour voir le temps de traitement des requêtes. Facile, il sert à 1ms
  • un tir sur le proxy rust monothreadé (concurrence de 1) en mode debug
  • un tir sur le proxy rust en compilation optimisée (nous avions oublié dans le tir précédent de faire un build optimisé avec l’option --release)
  • un tir sur le proxy rust en multithreadé (un thread par coeur)
  • un tir sur le proxy python (concurrence de 1)
  • un tir sur le proxy monothreadé rust avec une concurrence de 8 (8 utilisateurs en parallèle)
  • un tir sur le proxy python avec une concurrence de 8

Voici les résultats obtenus :

diagramme

Ce que nous voyons :

  1. dans les mêmes conditions (monothreadé, concurrence de 1) rust est 4,2 fois plus efficace que python
  2. rust en compilation non-optimisée est 2,6 fois moins efficace (comparativement à la version optimisée)
  3. la version multithreadée (p8t) fait à peine mieux que la version monothreadée
  4. avec une concurrence de 8 les performances de python s’effondrent. C’est probablement lié au fait que notre serveur est synchrone, il ne peut pas paralléliser les requêtes
  5. résultat que nous avons du mal à expliquer : rust fait mieux avec 8 utilisateurs parallèles qu’avec un seul

Ok c’est très fort pour rust. Cela dit la comparaison n’est pas tout à fait juste :

  1. nous vérifions la signature du JWT en SHA-512 donc il y a aussi de la CPU dans ce calcul (sans doute fait avec binding C pour python)
  2. nous décodons les crédentials en AES-256 ce qui est aussi de la crypto qui utilise la CPU (également avec un binding C pour python)
  3. le serveur python n’est pas asynchrone alors que le proxy rust l’est

Alors testons un pur reverse proxy HTTP asynchrone en monothreadé. Nous récupérons ce proxy asyncio python, et nous modifions le code de notre proxy pour qu’il ne fasse que relayer la requête vers le backend (juste la partie hyper-reverse-proxy).

Nous faisons plusieurs tirs de 100K requêtes. Voici les résultats:

diagramme

Nous constatons que le proxy rust est toujours meilleur alors que nous sommes dans des situations comparables. Il ajoute 0,5ms au temps de réponse de nginx. Le proxy python répond très bien en ajoutant 1ms, mais c’est deux fois plus.

Conclusion, même sur des performances I/0 bound rust est quand même bien plus efficace que python. Nous avons déployé notre proxy en production. Notre proxy a également été référencé sur le site de JMAP.