Na een van mijn meest recente sollicitatiegesprekken was ik verrast toen ik erachter kwam dat het bedrijf waar ik op solliciteerde nog steeds Laravel gebruikte, een PHP-framework dat ik ongeveer tien jaar geleden heb geprobeerd. Het was behoorlijk voor die tijd, maar als er รฉรฉn constante is in technologie en mode, dan is het wel de voortdurende verandering en het opnieuw opduiken van stijlen en concepten. Als je een JavaScript-programmeur bent, ben je waarschijnlijk bekend met deze oude mop
Programmeur 1: "Ik vind dit nieuwe JavaScript-framework niet leuk!"
Programmeur 2: "Geen zorgen. Wacht gewoon zes maanden en er komt er nog een om hem te vervangen!"
Uit nieuwsgierigheid besloot ik om eens te kijken wat er precies gebeurt als we oud en nieuw op de proef stellen. Natuurlijk staat het web vol met benchmarks en claims, waarvan de populairste waarschijnlijk de TechEmpower Web Framework Benchmarks hier . We gaan vandaag echter niet iets doen dat ook maar in de buurt komt van wat zij doen. We houden het lekker simpel, zodat dit artikel niet verandert in Oorlog en vrede , en dat je een kleine kans hebt om wakker te blijven tegen de tijd dat je klaar bent met lezen. De gebruikelijke kanttekeningen zijn van toepassing: dit werkt mogelijk niet hetzelfde op jouw machine, verschillende softwareversies kunnen de prestaties beรฏnvloeden en Schrรถdingers kat werd in feite een zombiekat die half levend en half dood was op exact hetzelfde moment.
Voor deze test gebruik ik mijn laptop, uitgerust met een nietige i5 met Manjaro Linux, zoals hier afgebeeld.
โฐโโค 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
Onze code bevat drie eenvoudige taken voor elk verzoek:
Wat is dat voor een idiote test, vraagt u zich misschien af? Nou, als u kijkt naar de netwerkverzoeken voor deze pagina, ziet u er een genaamd sessionvars.js die precies hetzelfde doet.
Moderne webpagina's zijn complexe wezens en een van de meest voorkomende taken is het cachen van complexe pagina's om overmatige belasting van de databaseserver te voorkomen.
Als we een complexe pagina elke keer opnieuw renderen als een gebruiker deze opvraagt, kunnen we slechts ongeveer 600 gebruikers per seconde bedienen.
โฐโโค 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
Maar als we deze pagina cachen als een statisch HTML-bestand en Nginx het snel naar de gebruiker laten sturen, dan kunnen we 32.000 gebruikers per seconde bedienen, wat de prestaties met een factor 50 verhoogt.
โฐโโค 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
De statische index.en.html is het deel dat naar iedereen gaat, en alleen de delen die per gebruiker verschillen worden verzonden in sessionvars.js. Dit vermindert niet alleen de databasebelasting en creรซert een betere ervaring voor onze gebruikers, maar vermindert ook de kwantumwaarschijnlijkheden dat onze server spontaan zal verdampen in een warp core breach wanneer de Klingons aanvallen.
De geretourneerde code voor elk framework heeft รฉรฉn simpele vereiste: laat de gebruiker zien hoe vaak ze de pagina hebben vernieuwd door te zeggen "Aantal is x". Om het simpel te houden, blijven we voorlopig weg van Redis-wachtrijen, Kubernetes-componenten of AWS Lambdas.
De sessiegegevens van elke gebruiker worden opgeslagen in een PostgreSQL-database.
Deze databasetabel wordt voor elke test afgekapt.
Simpel maar effectief is het motto van Pafera... in ieder geval buiten de donkerste tijdlijnen...
Okรฉ, nu kunnen we eindelijk onze handen vuil maken. We slaan de setup voor Laravel over, want het is gewoon een stel composer- en artisan-commando's.
Eerst zullen we onze database-instellingen in het .env-bestand instellen
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=sessiontest
DB_USERNAME=sessiontest
DB_PASSWORD=sessiontest
Vervolgens stellen we รฉรฉn enkele fallback-route in die elk verzoek naar onze controller stuurt.
Route::fallback(SessionController::class);
En stel de controller in om het aantal weer te geven. Laravel slaat standaard sessies op in de database. Het biedt ook de session()
functie om te communiceren met onze sessiegegevens, dus het kostte slechts een paar regels code om onze pagina te renderen.
class SessionController extends Controller
{
public function __invoke(Request $request)
{
$count = session('count', 0);
$count += 1;
session(['count' => $count]);
return 'Count is ' . $count;
}
}
Nadat we php-fpm en Nginx hadden ingesteld, zag onze pagina er behoorlijk goed uit...
โฐโโค 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
In ieder geval totdat we de testresultaten daadwerkelijk zien...
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
Nee, dat is geen typefout. Onze testmachine is van 600 verzoeken per seconde gegaan om een complexe pagina te renderen... naar 21 verzoeken per seconde om "Count is 1" te renderen.
Dus wat ging er mis? Is er iets mis met onze PHP-installatie? Wordt Nginx op de een of andere manier langzamer bij het communiceren met php-fpm?
Laten we deze pagina opnieuw maken in pure PHP-code.
<?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'];
We hebben nu 98 regels code gebruikt om te doen wat vier regels code (en een heleboel configuratiewerk) in Laravel deden. (Als we de foutverwerking en de berichten voor de gebruiker goed zouden regelen, zou dit natuurlijk ongeveer twee keer zoveel regels zijn.) Misschien kunnen we 30 verzoeken per seconde halen?
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
Whoa! Het lijkt erop dat er toch niks mis is met onze PHP-installatie. De pure PHP-versie doet 700 verzoeken per seconde.
Als er niets mis is met PHP, hebben we Laravel misschien verkeerd geconfigureerd?
Na het web te hebben doorzocht naar configuratieproblemen en prestatietips, waren twee van de populairste technieken om de configuratie te cachen en gegevens te routeren om te voorkomen dat ze voor elke aanvraag werden verwerkt. Daarom zullen we hun advies ter harte nemen en deze tips uitproberen.
โฐโโค php artisan config:cache
INFO Configuration cached successfully.
โฐโโค php artisan route:cache
INFO Routes cached successfully.
Alles ziet er goed uit op de opdrachtregel. Laten we de benchmark opnieuw doen.
โฐโโค 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
Nou, we hebben de performance nu verhoogd van 21,04 naar 28,80 verzoeken per seconde, een dramatische stijging van bijna 37%! Dit zou behoorlijk indrukwekkend zijn voor elk softwarepakket... ware het niet dat we nog steeds maar 1/24e van het aantal verzoeken van de pure PHP-versie doen.
Als u denkt dat er iets mis moet zijn met deze test, moet u met de auteur van het Lucinda PHP-framework praten. In zijn testresultaten heeft hij Lucinda verslaat Laravel met 36x voor HTML-verzoeken en 90x voor JSON-verzoeken.
Na het testen op mijn eigen machine met zowel Apache als Nginx, heb ik geen reden om aan hem te twijfelen. Laravel is echt gewoon Dat langzaam! PHP op zichzelf is niet zo slecht, maar als je alle extra verwerking toevoegt die Laravel aan elke aanvraag toevoegt, dan vind ik het erg moeilijk om Laravel aan te bevelen als een keuze in 2023.
PHP/Wordpress-accounts voor ongeveer 40% van alle websites op het web , waardoor het veruit het meest dominante raamwerk is. Persoonlijk vind ik echter dat populariteit niet per se in kwaliteit resulteert, net zo min als ik een plotselinge oncontroleerbare behoefte voel aan dat buitengewone gastronomische eten van het populairste restaurant ter wereld ... McDonald's. Omdat we al pure PHP-code hebben getest, gaan we Wordpress zelf niet testen, aangezien alles wat met Wordpress te maken heeft ongetwijfeld lager zou zijn dan de 700 verzoeken per seconde die we met pure PHP hebben waargenomen.
Django is een ander populair framework dat al heel lang bestaat. Als je het in het verleden hebt gebruikt, denk je waarschijnlijk met plezier terug aan de spectaculaire databasebeheerinterface en hoe vervelend het was om alles precies zo te configureren als je wilde. Laten we eens kijken hoe goed Django in 2023 werkt, vooral met de nieuwe ASGI-interface die het heeft toegevoegd in versie 4.0.
Het instellen van Django lijkt opvallend veel op het instellen van Laravel, aangezien ze allebei uit de tijd kwamen waarin MVC-architecturen stijlvol en correct waren. We slaan de saaie configuratie over en gaan direct naar het instellen van de weergave.
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}")
Vier regels code is hetzelfde als bij de Laravel-versie. Laten we eens kijken hoe het presteert.
โฐโโค 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
Helemaal niet slecht met 355 verzoeken per seconde. Het is maar de helft van de prestaties van de pure PHP-versie, maar het is ook 12x zo goed als die van de Laravel-versie. Django vs. Laravel lijkt helemaal geen wedstrijd te zijn.
Naast de grotere alles-inclusief-de-keuken-gootsteen-frameworks, zijn er ook kleinere frameworks die alleen wat basisinstellingen doen en jou de rest laten regelen. Een van de beste om te gebruiken is Flask en zijn ASGI-tegenhanger Quart. Mijn eigen PaferaPy-framework is gebouwd op Flask, dus ik weet heel goed hoe gemakkelijk het is om dingen gedaan te krijgen en tegelijkertijd de prestaties te behouden.
#!/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}'
Zoals u kunt zien, is het Flask-script korter dan het pure PHP-script. Ik vind dat van alle talen die ik heb gebruikt, Python waarschijnlijk de meest expressieve taal is in termen van getypte toetsaanslagen. Het ontbreken van accolades en haakjes, lijst- en dictbegrip en blokkering op basis van inspringing in plaats van puntkomma's maken Python vrij eenvoudig, maar krachtig in zijn mogelijkheden.
Helaas is Python ook de langzaamste algemene taal die er is, ondanks hoeveel software erin is geschreven. Het aantal beschikbare Python-bibliotheken is ongeveer vier keer groter dan vergelijkbare talen en bestrijkt een groot aantal domeinen, maar niemand zou zeggen dat Python snel of performant is buiten niches als NumPy.
Laten we eens kijken hoe onze Flask-versie zich verhoudt tot onze vorige frameworks.
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
Ons Flask-script is zelfs sneller dan onze pure PHP-versie!
Als je hierdoor verrast bent, moet je je realiseren dat onze Flask-app al zijn initialisatie en configuratie uitvoert wanneer we de gunicorn-server opstarten, terwijl PHP het script opnieuw uitvoert telkens wanneer er een nieuw verzoek binnenkomt. Het is vergelijkbaar met Flask als de jonge, enthousiaste taxichauffeur die de auto al heeft gestart en langs de weg staat te wachten, terwijl PHP de oude chauffeur is die thuis blijft wachten op een telefoontje en pas dan naar je toe rijdt om je op te halen. Als old school guy en afkomstig uit de tijd dat PHP een geweldige verandering was voor gewone HTML- en SHTML-bestanden, is het een beetje triest om te beseffen hoeveel tijd er is verstreken, maar de ontwerpverschillen maken het echt moeilijk voor PHP om te concurreren met Python-, Java- en Node.js-servers die gewoon in het geheugen blijven en verzoeken afhandelen met het gemak van een jongleur.
Flask is misschien wel ons snelste framework tot nu toe, maar het is eigenlijk behoorlijk oude software. De Python-community is een paar jaar geleden overgestapt op de nieuwere asychrone ASGI-servers, en ik ben natuurlijk ook met ze meegegaan.
De nieuwste versie van het Pafera Framework, PaferaPyAsync , is gebaseerd op Starlette. Hoewel er een ASGI-versie van Flask is genaamd Quart, waren de prestatieverschillen tussen Quart en Starlette genoeg voor mij om mijn code opnieuw op Starlette te baseren.
Asychroon programmeren kan voor veel mensen beangstigend zijn, maar het is eigenlijk geen moeilijk concept, dankzij de Node.js-mensen die het concept al ruim tien jaar geleden populair hebben gemaakt.
Vroeger vochten we tegen gelijktijdigheid met multithreading, multiprocessing, distributed computing, promise chaining en al die leuke tijden die veel ervaren programmeurs voortijdig verouderden en uitdroogden. Nu typen we gewoon async
voor onze functies en await
voor elke code die een tijdje kan duren om uit te voeren. Het is inderdaad meer omslachtig dan normale code, maar veel minder vervelend om te gebruiken dan te moeten omgaan met synchronisatieprimitieven, berichtpassing en het oplossen van beloftes.
Ons Starlette-bestand ziet er als volgt uit:
#!/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",
)
Zoals u kunt zien, is het vrijwel gekopieerd en geplakt uit ons Flask-script met slechts een paar routeringswijzigingen en de async/await
Trefwoorden.
Hoeveel verbetering kan het kopiรซren en plakken van code ons nu werkelijk opleveren?
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
We hebben een nieuwe kampioen, dames en heren! Onze vorige high was onze pure PHP-versie met 704 verzoeken per seconde, die vervolgens werd ingehaald door onze Flask-versie met 1080 verzoeken per seconde. Ons Starlette-script verplettert alle eerdere concurrenten met 4562 verzoeken per seconde, wat een verbetering van 6x betekent ten opzichte van pure PHP en een verbetering van 4x ten opzichte van Flask.
Als u uw WSGI Python-code nog niet naar ASGI hebt omgezet, is dit misschien een goed moment om daarmee te beginnen.
Tot nu toe hebben we alleen PHP- en Python-frameworks behandeld. Een groot deel van de wereld gebruikt echter Java, DotNet, Node.js, Ruby on Rails en andere dergelijke technologieรซn voor hun websites. Dit is absoluut geen uitgebreid overzicht van alle ecosystemen en biomen ter wereld, dus om te voorkomen dat we het programmeerequivalent van organische chemie gebruiken, kiezen we alleen de frameworks waarvoor het het gemakkelijkst is om code te typen... waarvan Java er zeker geen is.
Tenzij je je onder je exemplaar van K&R C of Knuth's hebt verstopt De kunst van computerprogrammering de afgelopen vijftien jaar heb je waarschijnlijk gehoord van Node.js. Degenen onder ons die er al sinds het begin van JavaScript bij zijn, zijn ofwel ongelooflijk bang, verbaasd, of allebei over de staat van modern JavaScript, maar het valt niet te ontkennen dat JavaScript een kracht is geworden om rekening mee te houden op servers en browsers. We hebben nu zelfs native 64-bits integers in de taal! Dat is veel beter dan dat alles in 64-bits floats wordt opgeslagen!
ExpressJS is waarschijnlijk de makkelijkste Node.js-server om te gebruiken, dus we maken een snelle en eenvoudige Node.js/ExpressJS-app om onze teller te bedienen.
/**********************************************************************
* 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}`));
Deze code was eigenlijk makkelijker te schrijven dan de Python-versies, hoewel native JavaScript nogal onhandig wordt naarmate de applicaties groter worden. Pogingen om dit te corrigeren, zoals TypeScript, worden bovendien al snel omslachtiger dan Python.
Laten we eens kijken hoe dit presteert!
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
Je hebt misschien oude (oud volgens internetnormen in ieder geval...) volksverhalen gehoord over de snelheid van Node.js, en die verhalen zijn grotendeels waar dankzij het spectaculaire werk dat Google heeft verricht met de V8 JavaScript-engine. In dit geval presteert onze snelle app beter dan het Flask-script, maar de single-threaded aard ervan wordt verslagen door de vier async-processen die worden gehanteerd door de Starlette Knight die "Ni!" zegt.
Laten we nog wat hulp krijgen!
โฐโโค 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รฉ! Nu is het een gelijkspel van vier tegen vier! Laten we de benchmark doen!
โฐโโค 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
Nog steeds niet helemaal op het niveau van Starlette, maar het is niet slecht voor een snelle JavaScript-hack van vijf minuten. Uit mijn eigen tests blijkt dat dit script eigenlijk een beetje wordt tegengehouden op het niveau van de database-interface, omdat node-postgres lang niet zo efficiรซnt is als psycopg voor Python. Overschakelen naar sqlite als databasedriver levert meer dan 3000 verzoeken per seconde op voor dezelfde ExpressJS-code.
Het belangrijkste om op te merken is dat ondanks de lage uitvoeringssnelheid van Python, ASGI-frameworks voor bepaalde workloads daadwerkelijk kunnen concurreren met Node.js-oplossingen.
We komen nu dichter bij de top van de berg. En met berg bedoel ik de hoogste scores die ooit door muizen en mensen zijn gemeten.
Als je kijkt naar de meeste framework benchmarks die beschikbaar zijn op het web, zul je merken dat er twee talen zijn die de boventoon voeren: C++ en Rust. Ik werk al met C++ sinds de jaren 90 en ik had zelfs mijn eigen Win32 C++ framework voordat MFC/ATL een ding was, dus ik heb veel ervaring met de taal. Het is niet zo leuk om met iets te werken als je het al kent, dus we gaan in plaats daarvan een Rust-versie maken. ;)
Rust is relatief nieuw als het om programmeertalen gaat, maar het werd een object van nieuwsgierigheid voor mij toen Linus Torvalds aankondigde dat hij Rust zou accepteren als een Linux kernel programmeertaal. Voor ons oudere programmeurs is dat ongeveer hetzelfde als zeggen dat dit nieuwe hippie dingetje een nieuw amendement op de Amerikaanse grondwet zou worden.
Als je een ervaren programmeur bent, spring je niet zo snel op de kar als jongere mensen, anders loop je het risico dat je je verbrandt aan de snelle veranderingen in de taal of bibliotheken. (Iedereen die de eerste versie van AngularJS heeft gebruikt, weet waar ik het over heb.) Rust bevindt zich nog steeds in die experimentele ontwikkelingsfase en ik vind het grappig dat zoveel codevoorbeelden op het web niet eens meer compileren met huidige versies van pakketten.
De prestaties die Rust-applicaties laten zien, kunnen echter niet worden ontkend. Als u het nog nooit hebt geprobeerd ripgrep of fd-vinden op grote broncodebomen, moet je ze zeker eens proberen. Ze zijn zelfs beschikbaar voor de meeste Linux-distributies, gewoon via de pakketbeheerder. Je ruilt breedsprakigheid in voor prestaties met Rust... een kavel van woordrijkheid voor een kavel van prestatie.
De volledige code voor Rust is wat groot, dus we zullen hier alleen de relevante handlers bekijken:
// =====================================================================
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
)))
}
Dit is veel ingewikkelder dan de Python/Node.js versies...
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
En veel performanter!
Onze Rust-server met Actix/deadpool_postgres verslaat onze vorige kampioen Starlette met gemak met +125%, ExpressJS met +362% en pure PHP met +1366%. (Ik laat het prestatieverschil bij de Laravel-versie als oefening voor de lezer.)
Ik heb ontdekt dat het leren van de Rust-taal zelf moeilijker is dan andere talen, omdat het veel meer addertjes onder het gras heeft dan alles wat ik buiten 6502 Assembly heb gezien, maar als je Rust-server 14x zoveel gebruikers aankan als je PHP-server, dan is er misschien toch iets te winnen met het wisselen van technologieรซn. Daarom zal de volgende versie van het Pafera Framework op Rust gebaseerd zijn. De leercurve is veel hoger dan bij scripttalen, maar de prestaties zullen het waard zijn. Als je geen tijd kunt steken in het leren van Rust, dan is het baseren van je tech stack op Starlette of Node.js ook geen slechte beslissing.
In de afgelopen twintig jaar zijn we gegaan van goedkope statische hostingsites naar gedeelde hosting met LAMP-stacks naar het verhuren van VPS'en aan AWS, Azure en andere cloudservices. Tegenwoordig zijn veel bedrijven tevreden met het maken van ontwerpbeslissingen op basis van wie ze kunnen vinden die beschikbaar of het goedkoopst is, aangezien de komst van handige cloudservices het gemakkelijk heeft gemaakt om meer hardware op langzame servers en applicaties te gooien. Dit heeft hen grote kortetermijnwinsten opgeleverd ten koste van technische schulden op de lange termijn.
70 jaar geleden was er een grote ruimterace tussen de Sovjet-Unie en de Verenigde Staten. De Sovjets wonnen de meeste vroege mijlpalen. Ze hadden de eerste satelliet in Spoetnik, de eerste hond in de ruimte in Laika, het eerste maanruimteschip in Luna 2, de eerste man en vrouw in de ruimte in Yuri Gagarin en Valentina Tereshkova, enzovoort...
Maar ze bouwden langzaam maar zeker een technische schuld op.
Hoewel de Sovjets de eersten waren in elk van deze prestaties, zorgden hun engineeringprocessen en doelen ervoor dat ze zich meer richtten op uitdagingen op de korte termijn dan op haalbaarheid op de lange termijn. Ze wonnen elke keer dat ze sprongen, maar ze werden steeds vermoeider en langzamer, terwijl hun tegenstanders consistente stappen richting de finishlijn bleven zetten.
Toen Neil Armstrong zijn historische stappen op de maan live op televisie zette, namen de Amerikanen het voortouw en bleven ze daar toen het Sovjetprogramma wankelde. Dit is niet anders dan bedrijven vandaag de dag die zich hebben gericht op het volgende grote ding, de volgende grote uitbetaling of de volgende grote technologie, maar er niet in slagen om de juiste gewoontes en strategieรซn voor de lange termijn te ontwikkelen.
Als eerste op de markt zijn betekent niet dat u de dominante speler in die markt wordt. De tijd nemen om dingen goed te doen garandeert daarentegen geen succes, maar vergroot zeker uw kansen op prestaties op de lange termijn. Als u de tech lead bent voor uw bedrijf, kies dan de juiste richting en tools voor uw werklast. Laat populariteit niet de plaats innemen van prestaties en efficiรซntie.
Wilt u een 7z-bestand downloaden met de scripts Rust, ExpressJS, Flask, Starlette en Pure PHP?
Over de auteur |
|
![]() |
Jim programmeert al sinds hij in de jaren 90 een IBM PS/2 terugkreeg. Tot op de dag van vandaag schrijft hij HTML en SQL nog steeds het liefst met de hand, en richt hij zich op efficiรซntie en correctheid in zijn werk. |