Après l’un de mes derniers entretiens d’embauche, j’ai été surpris de constater que l’entreprise pour laquelle j’avais postulé utilisait toujours Laravel, un framework PHP que j’avais essayé il y a une dizaine d’années. C’était correct pour l’époque, mais s’il y a une constante dans la technologie comme dans la mode, c’est le changement continuel et le renouvellement des styles et des concepts. Si vous êtes un programmeur JavaScript, vous connaissez probablement cette vieille blague
Programmeur 1 : « Je n’aime pas ce nouveau framework JavaScript ! »
Programmeur 2 : « Ne vous inquiétez pas. Attendez six mois et il y en aura un autre pour le remplacer ! »
Par curiosité, j'ai décidé de voir exactement ce qui se passe lorsque nous mettons à l'épreuve l'ancien et le nouveau. Bien sûr, le Web regorge de tests et d'affirmations, dont le plus populaire est probablement le Références du framework Web TechEmpower ici . Nous n’allons cependant pas faire quelque chose d’aussi compliqué qu’eux aujourd’hui. Nous allons garder les choses simples et agréables pour que cet article ne se transforme pas en Guerre et paix , et que vous aurez une petite chance de rester éveillé au moment où vous aurez fini de lire. Les mises en garde habituelles s'appliquent : cela peut ne pas fonctionner de la même manière sur votre machine, différentes versions de logiciels peuvent affecter les performances et le chat de Schrödinger est en fait devenu un chat zombie à moitié vivant et à moitié mort exactement au même moment.
Pour ce test, j'utiliserai mon ordinateur portable équipé d'un petit i5 exécutant Manjaro Linux comme indiqué ici.
╰─➤ uname -a
Linux jimsredmi 5.10.174-1-MANJARO #1 SMP PREEMPT Tuesday Mar 21 11:15:28 UTC 2023 x86_64 GNU/Linux
╰─➤ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 126
model name : Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
stepping : 5
microcode : 0xb6
cpu MHz : 990.210
cache size : 6144 KB
Notre code aura trois tâches simples pour chaque requête :
Vous vous demandez peut-être quel genre de test idiot est-ce ? Eh bien, si vous regardez les requêtes réseau pour cette page, vous remarquerez qu'il y en a une appelée sessionvars.js qui fait exactement la même chose.
Vous voyez, les pages Web modernes sont des créatures complexes, et l’une des tâches les plus courantes est la mise en cache de pages complexes pour éviter une charge excessive sur le serveur de base de données.
Si nous réaffichons une page complexe à chaque fois qu'un utilisateur la demande, nous ne pouvons servir qu'environ 600 utilisateurs par seconde.
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1/system/index.en.html
Running 10s test @ http://127.0.0.1/system/index.en.html
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 186.83ms 174.22ms 1.06s 81.16%
Req/Sec 166.11 58.84 414.00 71.89%
6213 requests in 10.02s, 49.35MB read
Requests/sec: 619.97
Transfer/sec: 4.92MB
Mais si nous mettons en cache cette page sous forme de fichier HTML statique et laissons Nginx le renvoyer rapidement à l'utilisateur, nous pouvons alors servir 32 000 utilisateurs par seconde, augmentant ainsi les performances d'un facteur 50x.
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1/system/index.en.html
Running 10s test @ http://127.0.0.1/system/index.en.html
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.03ms 511.95us 6.87ms 68.10%
Req/Sec 8.20k 1.15k 28.55k 97.26%
327353 requests in 10.10s, 2.36GB read
Requests/sec: 32410.83
Transfer/sec: 238.99MB
L'index statique.en.html est la partie qui s'adresse à tout le monde, et seules les parties qui diffèrent selon les utilisateurs sont envoyées dans sessionvars.js. Cela réduit non seulement la charge de la base de données et crée une meilleure expérience pour nos utilisateurs, mais diminue également les probabilités quantiques que notre serveur se vaporise spontanément dans une brèche du noyau de distorsion lorsque les Klingons attaquent.
Le code renvoyé pour chaque framework aura une exigence simple : montrer à l’utilisateur combien de fois il a actualisé la page en disant « Le nombre est x ». Pour simplifier les choses, nous éviterons pour l’instant les files d’attente Redis, les composants Kubernetes ou AWS Lambdas.
Les données de session de chaque utilisateur seront enregistrées dans une base de données PostgreSQL.
Et cette table de base de données sera tronquée avant chaque test.
Simple mais efficace est la devise de Pafera... en dehors de la chronologie la plus sombre en tout cas...
Bon, nous pouvons enfin commencer à nous salir les mains. Nous allons ignorer la configuration de Laravel car il s'agit simplement d'un ensemble de commandes de compositeur et d'artisan.
Tout d’abord, nous allons configurer nos paramètres de base de données dans le fichier .env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=sessiontest
DB_USERNAME=sessiontest
DB_PASSWORD=sessiontest
Nous définirons ensuite une seule route de secours qui envoie chaque requête à notre contrôleur.
Route::fallback(SessionController::class);
Et définissez le contrôleur pour afficher le nombre. Laravel, par défaut, stocke les sessions dans la base de données. Il fournit également le session()
fonction d'interface avec nos données de session, il n'a donc fallu que quelques lignes de code pour rendre notre page.
class SessionController extends Controller
{
public function __invoke(Request $request)
{
$count = session('count', 0);
$count += 1;
session(['count' => $count]);
return 'Count is ' . $count;
}
}
Après avoir configuré php-fpm et Nginx, notre page est plutôt belle...
╰─➤ php -v
PHP 8.2.2 (cli) (built: Feb 1 2023 08:33:04) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.2, Copyright (c) Zend Technologies
with Xdebug v3.2.0, Copyright (c) 2002-2022, by Derick Rethans
╰─➤ sudo systemctl restart php-fpm
╰─➤ sudo systemctl restart nginx
Du moins jusqu'à ce que nous voyions réellement les résultats des tests...
PHP/Laravel
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1
Running 10s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.08s 546.33ms 1.96s 65.71%
Req/Sec 12.37 7.28 40.00 56.64%
211 requests in 10.03s, 177.21KB read
Socket errors: connect 0, read 0, write 0, timeout 176
Requests/sec: 21.04
Transfer/sec: 17.67KB
Non, ce n'est pas une erreur de frappe. Notre machine de test est passée de 600 requêtes par seconde pour le rendu d'une page complexe... à 21 requêtes par seconde pour le rendu « Count is 1 ».
Alors, qu'est-ce qui s'est passé ? Est-ce qu'il y a un problème avec notre installation PHP ? Est-ce que Nginx ralentit d'une manière ou d'une autre lors de l'interfaçage avec php-fpm ?
Refaisons cette page en pur code PHP.
<?php
// ====================================================================
function uuid4()
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
// ====================================================================
function Query($db, $query, $params = [])
{
$s = $db->prepare($query);
$s->setFetchMode(PDO::FETCH_ASSOC);
$s->execute(array_values($params));
return $s;
}
// ********************************************************************
session_start();
$sessionid = 0;
if (isset($_SESSION['sessionid']))
{
$sessionid = $_SESSION['sessionid'];
}
if (!$sessionid)
{
$sessionid = uuid4();
$_SESSION['sessionid'] = $sessionid;
}
$db = new PDO('pgsql:host=127.0.0.1 dbname=sessiontest user=sessiontest password=sessiontest');
$data = 0;
try
{
$result = Query(
$db,
'SELECT data FROM usersessions WHERE uid = ?',
[$sessionid]
)->fetchAll();
if ($result)
{
$data = json_decode($result[0]['data'], 1);
}
} catch (Exception $e)
{
echo $e;
Query(
$db,
'CREATE TABLE usersessions(
uid TEXT PRIMARY KEY,
data TEXT
)'
);
}
if (!$data)
{
$data = ['count' => 0];
}
$data['count']++;
if ($data['count'] == 1)
{
Query(
$db,
'INSERT INTO usersessions(uid, data)
VALUES(?, ?)',
[$sessionid, json_encode($data)]
);
} else
{
Query(
$db,
'UPDATE usersessions
SET data = ?
WHERE uid = ?',
[json_encode($data), $sessionid]
);
}
echo 'Count is ' . $data['count'];
Nous avons maintenant utilisé 98 lignes de code pour faire ce que quatre lignes de code (et tout un tas de travail de configuration) dans Laravel ont fait. (Bien sûr, si nous faisions une gestion des erreurs et des messages destinés aux utilisateurs, cela représenterait environ deux fois plus de lignes.) Peut-être pourrions-nous atteindre 30 requêtes par seconde ?
PHP/Pure PHP
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1
Running 10s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 140.79ms 27.88ms 332.31ms 90.75%
Req/Sec 178.63 58.34 252.00 61.01%
7074 requests in 10.04s, 3.62MB read
Requests/sec: 704.46
Transfer/sec: 369.43KB
Ouah ! Il semblerait que notre installation PHP ne présente aucun problème. La version PHP pure effectue 700 requêtes par seconde.
S’il n’y a rien de mal avec PHP, peut-être avons-nous mal configuré Laravel ?
Après avoir parcouru le Web à la recherche de problèmes de configuration et de conseils sur les performances, deux des techniques les plus populaires consistaient à mettre en cache les données de configuration et de routage pour éviter de les traiter à chaque requête. Par conséquent, nous allons suivre leurs conseils et essayer ces conseils.
╰─➤ php artisan config:cache
INFO Configuration cached successfully.
╰─➤ php artisan route:cache
INFO Routes cached successfully.
Tout semble correct sur la ligne de commande. Reprenons le benchmark.
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1
Running 10s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.13s 543.50ms 1.98s 61.90%
Req/Sec 25.45 13.39 50.00 55.77%
289 requests in 10.04s, 242.15KB read
Socket errors: connect 0, read 0, write 0, timeout 247
Requests/sec: 28.80
Transfer/sec: 24.13KB
Eh bien, nous avons maintenant augmenté les performances de 21,04 à 28,80 requêtes par seconde, soit une augmentation spectaculaire de près de 37 % ! Cela serait assez impressionnant pour n'importe quel progiciel... sauf que nous ne faisons toujours que 1/24ème du nombre de requêtes de la version PHP pure.
Si vous pensez que quelque chose ne va pas avec ce test, vous devriez en parler avec l'auteur du framework PHP Lucinda. Dans ses résultats de test, il a Lucinda bat Laravel par 36x pour les requêtes HTML et 90x pour les requêtes JSON.
Après avoir testé sur ma propre machine avec Apache et Nginx, je n'ai aucune raison de douter de lui. Laravel est vraiment juste que lent ! PHP en soi n'est pas si mal, mais une fois que vous ajoutez tout le traitement supplémentaire que Laravel ajoute à chaque requête, alors je trouve très difficile de recommander Laravel comme choix en 2023.
Comptes PHP/Wordpress pour environ 40 % de tous les sites Web sur le Web , ce qui en fait de loin le cadre le plus dominant. Personnellement, je trouve que la popularité ne se traduit pas nécessairement par la qualité, pas plus que je ne me retrouve à ressentir une envie soudaine et incontrôlable pour cet extraordinaire aliment gastronomique de le restaurant le plus populaire du monde ... McDonald's. Puisque nous avons déjà testé du code PHP pur, nous n'allons pas tester Wordpress lui-même, car tout ce qui implique Wordpress serait sans aucun doute inférieur aux 700 requêtes par seconde que nous avons observées avec du PHP pur.
Django est un autre framework populaire qui existe depuis longtemps. Si vous l’avez déjà utilisé, vous vous souvenez probablement avec tendresse de son interface d’administration de base de données spectaculaire et de la difficulté à tout configurer comme vous le souhaitiez. Voyons comment Django fonctionne en 2023, en particulier avec la nouvelle interface ASGI qu’il a ajoutée à partir de la version 4.0.
La configuration de Django est remarquablement similaire à celle de Laravel, car ils datent tous deux de l'époque où les architectures MVC étaient élégantes et correctes. Nous allons ignorer la configuration ennuyeuse et passer directement à la configuration de la vue.
from django.shortcuts import render
from django.http import HttpResponse
# =====================================================================
def index(request):
count = request.session.get('count', 0)
count += 1
request.session['count'] = count
return HttpResponse(f"Count is {count}")
Quatre lignes de code sont les mêmes que dans la version Laravel. Voyons comment cela fonctionne.
╰─➤ python --version
Python 3.10.9
Python/Django
╰─➤ gunicorn --access-logfile - -k uvicorn.workers.UvicornWorker -w 4 djangotest.asgi
[2023-03-21 15:20:38 +0800] [2886633] [INFO] Starting gunicorn 20.1.0
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000/sessiontest/
Running 10s test @ http://127.0.0.1:8000/sessiontest/
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 277.71ms 142.84ms 835.12ms 69.93%
Req/Sec 91.21 57.57 230.00 61.04%
3577 requests in 10.06s, 1.46MB read
Requests/sec: 355.44
Transfer/sec: 148.56KB
Pas mal du tout avec 355 requêtes par seconde. C'est seulement la moitié des performances de la version PHP pure, mais c'est aussi 12 fois plus que la version Laravel. Django vs. Laravel ne semble pas être une compétition du tout.
Outre les frameworks plus volumineux qui permettent de tout faire, il existe également des frameworks plus petits qui se contentent de faire quelques configurations de base tout en vous laissant gérer le reste. L'un des meilleurs à utiliser est Flask et son homologue ASGI Quart. Le mien Cadre PaferaPy est construit sur Flask, donc je sais très bien à quel point il est facile de faire avancer les choses tout en maintenant les performances.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Session benchmark test
import json
import psycopg
import uuid
from flask import Flask, session, redirect, url_for, request, current_app, g, abort, send_from_directory
from flask.sessions import SecureCookieSessionInterface
app = Flask('pafera')
app.secret_key = b'secretkey'
dbconn = 0
# =====================================================================
@app.route('/', defaults={'path': ''}, methods = ['GET', 'POST'])
@app.route('/<path:path>', methods = ['GET', 'POST'])
def index(path):
"""Handles all requests for the server.
We route all requests through here to handle the database and session
logic in one place.
"""
global dbconn
if not dbconn:
dbconn = psycopg.connect('dbname=sessiontest user=sessiontest password=sessiontest')
cursor = dbconn.execute('''
CREATE TABLE IF NOT EXISTS usersessions(
uid TEXT PRIMARY KEY,
data TEXT
)
''')
cursor.close()
dbconn.commit()
sessionid = session.get('sessionid', 0)
if not sessionid:
sessionid = uuid.uuid4().hex
session['sessionid'] = sessionid
cursor = dbconn.execute("SELECT data FROM usersessions WHERE uid = %s", [sessionid])
row = cursor.fetchone()
count = json.loads(row[0])['count'] if row else 0
count += 1
newdata = json.dumps({'count': count})
if count == 1:
cursor.execute("""
INSERT INTO usersessions(uid, data)
VALUES(%s, %s)
""",
[sessionid, newdata]
)
else:
cursor.execute("""
UPDATE usersessions
SET data = %s
WHERE uid = %s
""",
[newdata, sessionid]
)
cursor.close()
dbconn.commit()
return f'Count is {count}'
Comme vous pouvez le voir, le script Flask est plus court que le script PHP pur. Je trouve que parmi tous les langages que j'ai utilisés, Python est probablement le langage le plus expressif en termes de frappes de touches. L'absence d'accolades et de parenthèses, la compréhension des listes et des dictionnaires, et le blocage basé sur l'indentation plutôt que sur les points-virgules rendent Python plutôt simple mais puissant dans ses capacités.
Malheureusement, Python est aussi le langage polyvalent le plus lent qui existe, malgré le nombre de logiciels qui y ont été écrits. Le nombre de bibliothèques Python disponibles est environ quatre fois supérieur à celui des langages similaires et couvre un grand nombre de domaines, mais personne ne dira que Python est rapide ou performant en dehors de niches comme NumPy.
Voyons comment notre version Flask se compare à nos frameworks précédents.
Python/Flask
╰─➤ gunicorn --access-logfile - -w 4 flasksite:app
[2023-03-21 15:32:49 +0800] [2856296] [INFO] Starting gunicorn 20.1.0
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 91.84ms 11.97ms 149.63ms 86.18%
Req/Sec 272.04 39.05 380.00 74.50%
10842 requests in 10.04s, 3.27MB read
Requests/sec: 1080.28
Transfer/sec: 333.37KB
Notre script Flask est en fait plus rapide que notre version PHP pure !
Si cela vous surprend, sachez que notre application Flask effectue toute son initialisation et sa configuration lorsque nous démarrons le serveur gunicorn, tandis que PHP réexécute le script à chaque fois qu'une nouvelle requête arrive. C'est l'équivalent de Flask, le jeune chauffeur de taxi impatient qui a déjà démarré la voiture et attend au bord de la route, tandis que PHP est le vieux chauffeur qui reste chez lui en attendant un appel et qui ne conduit qu'ensuite pour venir vous chercher. Étant un gars de la vieille école et venant de l'époque où PHP était un merveilleux changement par rapport aux fichiers HTML et SHTML simples, il est un peu triste de réaliser combien de temps s'est écoulé, mais les différences de conception font qu'il est vraiment difficile pour PHP de rivaliser avec les serveurs Python, Java et Node.js qui restent simplement en mémoire et gèrent les requêtes avec la facilité agile d'un jongleur.
Flask est peut-être notre framework le plus rapide jusqu'à présent, mais il s'agit en fait d'un logiciel assez ancien. La communauté Python est passée aux nouveaux serveurs ASGI asynchrones il y a quelques années, et bien sûr, j'ai moi-même fait la transition avec eux.
La dernière version du framework Pafera, PaferaPyAsync , est basé sur Starlette. Bien qu'il existe une version ASGI de Flask appelée Quart, les différences de performances entre Quart et Starlette ont été suffisantes pour que je base mon code sur Starlette à la place.
La programmation asynchrone peut être effrayante pour beaucoup de gens, mais ce n’est en fait pas un concept difficile grâce aux gars de Node.js qui ont popularisé le concept il y a plus de dix ans.
Nous avions l'habitude de lutter contre la concurrence avec le multithreading, le multitraitement, le calcul distribué, le chaînage de promesses et tous ces moments amusants qui ont prématurément vieilli et desséché de nombreux programmeurs chevronnés. Maintenant, nous tapons simplement async
devant nos fonctions et await
devant tout code qui pourrait prendre un certain temps à s'exécuter. Il est en effet plus verbeux que le code normal, mais beaucoup moins ennuyeux à utiliser que de devoir gérer des primitives de synchronisation, le passage de messages et la résolution de promesses.
Notre fichier Starlette ressemble à ceci :
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Session benchmark test
import json
import uuid
import psycopg
from starlette.applications import Starlette
from starlette.responses import Response, PlainTextResponse, JSONResponse, RedirectResponse, HTMLResponse
from starlette.routing import Route, Mount, WebSocketRoute
from starlette_session import SessionMiddleware
dbconn = 0
# =====================================================================
async def index(R):
global dbconn
if not dbconn:
dbconn = await psycopg.AsyncConnection.connect('dbname=sessiontest user=sessiontest password=sessiontest')
cursor = await dbconn.execute('''
CREATE TABLE IF NOT EXISTS usersessions(
uid TEXT PRIMARY KEY,
data TEXT
)
''')
await cursor.close()
await dbconn.commit()
sessionid = R.session.get('sessionid', 0)
if not sessionid:
sessionid = uuid.uuid4().hex
R.session['sessionid'] = sessionid
cursor = await dbconn.execute("SELECT data FROM usersessions WHERE uid = %s", [sessionid])
row = await cursor.fetchone()
count = json.loads(row[0])['count'] if row else 0
count += 1
newdata = json.dumps({'count': count})
if count == 1:
await cursor.execute("""
INSERT INTO usersessions(uid, data)
VALUES(%s, %s)
""",
[sessionid, newdata]
)
else:
await cursor.execute("""
UPDATE usersessions
SET data = %s
WHERE uid = %s
""",
[newdata, sessionid]
)
await cursor.close()
await dbconn.commit()
return PlainTextResponse(f'Count is {count}')
# *********************************************************************
app = Starlette(
debug = True,
routes = [
Route('/{path:path}', index, methods = ['GET', 'POST']),
],
)
app.add_middleware(
SessionMiddleware,
secret_key = 'testsecretkey',
cookie_name = "pafera",
)
Comme vous pouvez le voir, il s'agit en quelque sorte d'un copié-collé de notre script Flask avec seulement quelques modifications de routage et le async/await
mots-clés.
Dans quelle mesure le code copié et collé peut-il réellement nous apporter des améliorations ?
Python/Starlette
╰─➤ gunicorn --access-logfile - -k uvicorn.workers.UvicornWorker -w 4 starlettesite:app 130 ↵
[2023-03-21 15:42:34 +0800] [2856220] [INFO] Starting gunicorn 20.1.0
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 21.85ms 10.45ms 67.29ms 55.18%
Req/Sec 1.15k 170.11 1.52k 66.00%
45809 requests in 10.04s, 13.85MB read
Requests/sec: 4562.82
Transfer/sec: 1.38MB
Mesdames et messieurs, nous avons un nouveau champion ! Notre précédent record était notre version PHP pure avec 704 requêtes par seconde, qui a ensuite été dépassée par notre version Flask avec 1080 requêtes par seconde. Notre script Starlette écrase tous les concurrents précédents avec 4562 requêtes par seconde, ce qui signifie une amélioration de 6x par rapport à PHP pure et de 4x par rapport à Flask.
Si vous n’avez pas encore changé votre code Python WSGI en ASGI, c’est peut-être le bon moment pour commencer.
Jusqu’à présent, nous n’avons abordé que les frameworks PHP et Python. Cependant, une grande partie du monde utilise Java, DotNet, Node.js, Ruby on Rails et d’autres technologies similaires pour ses sites Web. Il ne s’agit en aucun cas d’un aperçu complet de tous les écosystèmes et biomes du monde. Par conséquent, pour éviter de faire l’équivalent en programmation de la chimie organique, nous choisirons uniquement les frameworks pour lesquels il est le plus facile de taper du code… dont Java n’est certainement pas un exemple.
À moins que vous ne vous cachiez sous votre exemplaire de K&R C ou de Knuth L'art de la programmation informatique Depuis quinze ans, vous avez probablement entendu parler de Node.js. Ceux d’entre nous qui ont été là depuis le début de JavaScript sont soit incroyablement effrayés, soit étonnés, ou les deux, par l’état du JavaScript moderne, mais il est indéniable que JavaScript est devenu une force avec laquelle il faut compter sur les serveurs ainsi que sur les navigateurs. Après tout, nous avons même maintenant des entiers natifs de 64 bits dans le langage ! C’est de loin bien mieux que tout ce qui est stocké dans des flottants de 64 bits !
ExpressJS est probablement le serveur Node.js le plus simple à utiliser, nous allons donc créer une application Node.js/ExpressJS rapide et simple pour servir notre compteur.
/**********************************************************************
* Simple session test using ExpressJS.
**********************************************************************/
var L = console.log;
var uuid = require('uuid4');
var express = require('express');
var session = require('express-session');
var MemoryStore = require('memorystore')(session);
var { Client } = require('pg')
var db = 0;
var app = express();
const PORT = 8000;
//session middleware
app.use(
session({
secret: "secretkey",
saveUninitialized: true,
resave: false,
store: new MemoryStore({
checkPeriod: 1000 * 60 * 60 * 24 // prune expired entries every 24h
})
})
);
app.get('/',
async function(req,res)
{
if (!db)
{
db = new Client({
user: 'sessiontest',
host: '127.0.0.1',
database: 'sessiontest',
password: 'sessiontest'
});
await db.connect();
await db.query(`
CREATE TABLE IF NOT EXISTS usersessions(
uid TEXT PRIMARY KEY,
data TEXT
)`,
[]
);
};
var session = req.session;
if (!session.sessionid)
{
session.sessionid = uuid();
}
var row = 0;
let queryresult = await db.query(`
SELECT data::TEXT
FROM usersessions
WHERE uid = $1`,
[session.sessionid]
);
if (queryresult && queryresult.rows.length)
{
row = queryresult.rows[0].data;
}
var count = 0;
if (row)
{
var data = JSON.parse(row);
data.count += 1;
count = data.count;
await db.query(`
UPDATE usersessions
SET data = $1
WHERE uid = $2
`,
[JSON.stringify(data), session.sessionid]
);
} else
{
await db.query(`
INSERT INTO usersessions(uid, data)
VALUES($1, $2)`,
[session.sessionid, JSON.stringify({count: 1})]
);
count = 1;
}
res.send(`Count is ${count}`);
}
);
app.listen(PORT, () => console.log(`Server Running at port ${PORT}`));
Ce code était en fait plus facile à écrire que les versions Python, bien que JavaScript natif devienne plutôt difficile à manier lorsque les applications deviennent plus grandes, et toutes les tentatives pour corriger cela, comme TypeScript, deviennent rapidement plus verbeuses que Python.
Voyons comment cela fonctionne !
Node.js/ExpressJS
╰─➤ node --version v19.6.0
╰─➤ NODE_ENV=production node nodejsapp.js 130 ↵
Server Running at port 8000
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 90.41ms 7.20ms 188.29ms 85.16%
Req/Sec 277.15 37.21 393.00 81.66%
11018 requests in 10.02s, 3.82MB read
Requests/sec: 1100.12
Transfer/sec: 390.68KB
Vous avez peut-être entendu des histoires anciennes (anciennes selon les standards Internet en tout cas...) sur la vitesse de Node.js, et ces histoires sont en grande partie vraies grâce au travail spectaculaire que Google a réalisé avec le moteur JavaScript V8. Dans ce cas cependant, bien que notre application rapide surpasse le script Flask, sa nature monothread est vaincue par les quatre processus asynchrones maniés par le chevalier Starlette qui dit « Ni ! ».
Allons chercher de l’aide supplémentaire !
╰─➤ pm2 start nodejsapp.js -i 4
[PM2] Spawning PM2 daemon with pm2_home=/home/jim/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/jim/projects/paferarust/nodejsapp.js in cluster_mode (4 instances)
[PM2] Done.
┌────┬──────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ nodejsapp │ default │ N/A │ cluster │ 37141 │ 0s │ 0 │ online │ 0% │ 64.6mb │ jim │ disabled │
│ 1 │ nodejsapp │ default │ N/A │ cluster │ 37148 │ 0s │ 0 │ online │ 0% │ 64.5mb │ jim │ disabled │
│ 2 │ nodejsapp │ default │ N/A │ cluster │ 37159 │ 0s │ 0 │ online │ 0% │ 56.0mb │ jim │ disabled │
│ 3 │ nodejsapp │ default │ N/A │ cluster │ 37171 │ 0s │ 0 │ online │ 0% │ 45.3mb │ jim │ disabled │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Ok ! Maintenant, c'est une bataille à égalité à quatre contre quatre ! Faisons un benchmark !
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 45.09ms 19.89ms 176.14ms 60.22%
Req/Sec 558.93 97.50 770.00 66.17%
22234 requests in 10.02s, 7.71MB read
Requests/sec: 2218.69
Transfer/sec: 787.89KB
Pas encore au niveau de Starlette, mais ce n'est pas mal pour un hack JavaScript rapide de cinq minutes. D'après mes propres tests, ce script est en fait un peu freiné au niveau de l'interface de base de données car node-postgres est loin d'être aussi efficace que psycopg pour Python. Le passage à sqlite comme pilote de base de données génère plus de 3000 requêtes par seconde pour le même code ExpressJS.
La principale chose à noter est que malgré la lenteur d’exécution de Python, les frameworks ASGI peuvent en fait être compétitifs avec les solutions Node.js pour certaines charges de travail.
Nous nous rapprochons donc désormais du sommet de la montagne, et par montagne, j’entends les scores de référence les plus élevés enregistrés par les souris et les hommes.
Si vous regardez la plupart des benchmarks de frameworks disponibles sur le Web, vous remarquerez que deux langages ont tendance à dominer le top : C++ et Rust. Je travaille avec C++ depuis les années 90, et j'ai même eu mon propre framework Win32 C++ avant que MFC/ATL ne soit une chose, j'ai donc beaucoup d'expérience avec le langage. Ce n'est pas très amusant de travailler avec quelque chose que vous connaissez déjà, nous allons donc faire une version Rust à la place. ;)
Rust est un langage de programmation relativement nouveau, mais il est devenu un objet de curiosité pour moi lorsque Linus Torvalds a annoncé qu’il accepterait Rust comme langage de programmation du noyau Linux. Pour nous, les programmeurs plus âgés, c’est à peu près la même chose que de dire que ce nouveau truc hippie new age allait être un nouvel amendement à la Constitution américaine.
Maintenant, quand vous êtes un programmeur expérimenté, vous avez tendance à ne pas sauter dans le train en marche aussi vite que les plus jeunes, sinon vous risquez de vous faire avoir par les changements rapides du langage ou des bibliothèques. (Quiconque a utilisé la première version d'AngularJS saura de quoi je parle.) Rust est encore quelque peu dans cette phase de développement expérimental, et je trouve amusant que de nombreux exemples de code sur le Web ne compilent même plus avec les versions actuelles des packages.
Cependant, les performances affichées par les applications Rust ne peuvent être niées. Si vous n'avez jamais essayé ripgrep ou fd-trouver sur de grands arbres de code source, vous devriez certainement les essayer. Ils sont même disponibles pour la plupart des distributions Linux simplement à partir du gestionnaire de paquets. Vous échangez la verbosité contre les performances avec Rust... parcelle de verbosité pour un parcelle de performance.
Le code complet de Rust est un peu volumineux, nous allons donc simplement jeter un œil aux gestionnaires pertinents ici :
// =====================================================================
pub async fn RunQuery(
db: &web::Data<Pool>,
query: &str,
args: &[&(dyn ToSql + Sync)]
) -> Result<Vec<tokio_postgres::row::Row>, tokio_postgres::Error>
{
let client = db.get().await.unwrap();
let statement = client.prepare_cached(query).await.unwrap();
client.query(&statement, args).await
}
// =====================================================================
pub async fn index(
req: HttpRequest,
session: Session,
db: web::Data<Pool>,
) -> Result<HttpResponse, Error>
{
let mut count = 1;
if let Some(sessionid) = session.get::<String>("sessionid")?
{
let rows = RunQuery(
&db,
"SELECT data
FROM usersessions
WHERE uid = $1",
&[&sessionid]
).await.unwrap();
if rows.is_empty()
{
let jsondata = serde_json::json!({
"count": 1,
}).to_string();
RunQuery(
&db,
"INSERT INTO usersessions(uid, data)
VALUES($1, $2)",
&[&sessionid, &jsondata]
).await
.expect("Insert failed!");
} else
{
let jsonstring:&str = rows[0].get(0);
let countdata: CountData = serde_json::from_str(jsonstring)?;
count = countdata.count;
count += 1;
let jsondata = serde_json::json!({
"count": count,
}).to_string();
RunQuery(
&db,
"UPDATE usersessions
SET data = $1
WHERE uid = $2
",
&[&jsondata, &sessionid]
).await
.expect("Update failed!");
}
} else
{
let sessionid = Uuid::new_v4().to_string();
let jsondata = serde_json::json!({
"count": 1,
}).to_string();
RunQuery(
&db,
"INSERT INTO usersessions(uid, data)
VALUES($1, $2)",
&[&sessionid, &jsondata]
).await
.expect("Insert failed!");
session.insert("sessionid", sessionid)?;
}
Ok(HttpResponse::Ok().body(format!(
"Count is {:?}",
count
)))
}
C'est beaucoup plus compliqué que les versions Python/Node.js...
Rust/Actix
╰─➤ cargo run --release
[2023-03-21T23:37:25Z INFO actix_server::builder] starting 4 workers
Server running at http://127.0.0.1:8888/
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8888
Running 10s test @ http://127.0.0.1:8888
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 9.93ms 3.90ms 77.18ms 94.87%
Req/Sec 2.59k 226.41 2.83k 89.25%
102951 requests in 10.03s, 24.59MB read
Requests/sec: 10267.39
Transfer/sec: 2.45MB
Et bien plus performant !
Notre serveur Rust utilisant Actix/deadpool_postgres bat haut la main notre précédent champion Starlette de +125%, ExpressJS de +362% et PHP pur de +1366%. (Je laisse le delta de performance avec la version Laravel comme exercice pour le lecteur.)
J’ai trouvé que l’apprentissage du langage Rust lui-même était plus difficile que celui d’autres langages, car il comporte beaucoup plus de pièges que tout ce que j’ai vu en dehors de l’assembleur 6502, mais si votre serveur Rust peut prendre en charge 14 fois plus d’utilisateurs que votre serveur PHP, alors peut-être qu’il y a quelque chose à gagner à changer de technologie après tout. C’est pourquoi la prochaine version du framework Pafera sera basée sur Rust. La courbe d’apprentissage est bien plus élevée que celle des langages de script, mais les performances en valent la peine. Si vous ne pouvez pas consacrer le temps nécessaire à l’apprentissage de Rust, alors baser votre pile technologique sur Starlette ou Node.js n’est pas non plus une mauvaise décision.
Au cours des vingt dernières années, nous sommes passés de sites d’hébergement statiques bon marché à l’hébergement partagé avec des piles LAMP, en passant par la location de VPS sur AWS, Azure et d’autres services cloud. De nos jours, de nombreuses entreprises se contentent de prendre des décisions de conception en fonction de ce qu’elles peuvent trouver de disponible ou de moins cher, car l’avènement de services cloud pratiques a facilité l’ajout de plus de matériel sur des serveurs et des applications lents. Cela leur a permis de réaliser d’importants gains à court terme au prix d’une dette technique à long terme.
Il y a 70 ans, l'Union soviétique et les États-Unis se livraient à une grande course à l'espace. Les Soviétiques ont remporté la plupart des premières étapes. Ils ont eu le premier satellite Spoutnik, le premier chien dans l'espace Laïka, le premier vaisseau spatial lunaire Luna 2, le premier homme et la première femme dans l'espace Youri Gagarine et Valentina Terechkova, etc.
Mais ils accumulaient peu à peu une dette technique.
Bien que les Soviétiques aient été les premiers à réaliser ces exploits, leurs processus d'ingénierie et leurs objectifs les ont poussés à se concentrer sur les défis à court terme plutôt que sur la faisabilité à long terme. Ils ont gagné à chaque fois qu'ils ont fait un bond en avant, mais ils étaient de plus en plus fatigués et lents tandis que leurs adversaires continuaient à avancer à grands pas vers la ligne d'arrivée.
Après que Neil Armstrong eut posé le pied sur la Lune en direct à la télévision, les Américains prirent les devants et y restèrent, tandis que le programme soviétique s’essoufflait. Ce n’est pas différent des entreprises d’aujourd’hui qui se concentrent sur la prochaine grande nouveauté, la prochaine grande rentabilité ou la prochaine grande technologie, sans parvenir à développer les bonnes habitudes et stratégies à long terme.
Être le premier sur le marché ne signifie pas que vous deviendrez l’acteur dominant sur ce marché. Par ailleurs, prendre le temps de faire les choses correctement ne garantit pas le succès, mais augmente certainement vos chances de réussite à long terme. Si vous êtes le responsable technique de votre entreprise, choisissez la bonne direction et les bons outils pour votre charge de travail. Ne laissez pas la popularité remplacer la performance et l’efficacité.
Vous souhaitez télécharger un fichier 7z contenant les scripts Rust, ExpressJS, Flask, Starlette et Pure PHP ?
À propos de l'auteur |
|
![]() |
Jim programme depuis qu'il a reçu une IBM PS/2 dans les années 90. Aujourd'hui encore, il préfère écrire du HTML et du SQL à la main et se concentre sur l'efficacité et la précision dans son travail. |