Po jednom z mých posledních pracovních pohovorů jsem si s překvapením uvědomil, že společnost, o kterou jsem se ucházel, stále používá Laravel, PHP framework, který jsem vyzkoušel asi před deseti lety. Na tehdejší dobu to bylo slušné, ale pokud existuje jedna konstanta v technologii a módě, je to neustálá změna a přestavování stylů a konceptů. Pokud jste programátor JavaScriptu, pravděpodobně tento starý vtip znáte
Programátor 1: "Nelíbí se mi tento nový rámec JavaScriptu!"
Programátor 2: „Nemusíte si dělat starosti. Stačí počkat šest měsíců a nahradí ho jiný!"
Ze zvědavosti jsem se rozhodl přesně vidět, co se stane, když testujeme staré a nové. Web je samozřejmě plný benchmarků a nároků, z nichž asi nejoblíbenější je TechEmpower Web Framework Benchmarks zde . Dnes však nebudeme dělat nic tak složitého jako oni. Uděláme věci hezké a jednoduché, aby se tento článek nezměnil v Válka a mír , a že budete mít malou šanci zůstat vzhůru, než budete číst. Platí obvyklá upozornění: na vašem počítači to nemusí fungovat stejně, různé verze softwaru mohou ovlivnit výkon a Schrödingerova kočka se ve skutečnosti stala kočkou zombie, která byla napůl živá a napůl mrtvá ve stejnou dobu.
Pro tento test budu používat svůj notebook vyzbrojený maličkým i5 se systémem Manjaro Linux, jak je znázorněno zde.
╰─➤ 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
Náš kód bude mít pro každý požadavek tři jednoduché úkoly:
Ptáte se, co je to za idiotský test? Když se podíváte na síťové požadavky pro tuto stránku, všimnete si jedné s názvem sessionvars.js, která dělá přesně to samé.
Víte, moderní webové stránky jsou komplikovaná stvoření a jedním z nejběžnějších úkolů je ukládání složitých stránek do mezipaměti, aby se zabránilo nadměrnému zatížení databázového serveru.
Pokud znovu vykreslíme složitou stránku pokaždé, když si to uživatel vyžádá, můžeme obsloužit pouze asi 600 uživatelů za sekundu.
╰─➤ 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
Ale pokud tuto stránku uložíme do mezipaměti jako statický soubor HTML a necháme Nginx, aby ji rychle vyhodil z okna uživateli, pak můžeme obsloužit 32 000 uživatelů za sekundu, což zvýší výkon faktorem 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
Statický index.en.html je část, která je určena všem, a v sessionvars.js se odesílají pouze části, které se liší podle uživatele. To nejen snižuje zatížení databáze a vytváří lepší zážitek pro naše uživatele, ale také snižuje kvantové pravděpodobnosti, že se náš server spontánně vypaří při porušení warp jádra, když Klingoni zaútočí.
Vrácený kód pro každý rámec bude mít jeden jednoduchý požadavek: ukažte uživateli, kolikrát obnovili stránku tím, že řeknete „Počet je x“. Abychom to zjednodušili, prozatím se budeme držet dál od front Redis, komponent Kubernetes nebo AWS Lambdas.
Údaje o relaci každého uživatele budou uloženy v databázi PostgreSQL.
A tato databázová tabulka bude před každým testem zkrácena.
Jednoduché, ale účinné je motto Pafery... každopádně mimo nejtemnější časovou osu...
Dobře, takže teď si konečně můžeme začít špinit ruce. Přeskočíme nastavení pro Laravel, protože je to jen skupina skladatelů a řemeslníků příkazy.
Nejprve nastavíme nastavení databáze v souboru .env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=sessiontest
DB_USERNAME=sessiontest
DB_PASSWORD=sessiontest
Potom nastavíme jednu jedinou záložní trasu, která odešle každý požadavek našemu kontroléru.
Route::fallback(SessionController::class);
A nastavte ovladač tak, aby zobrazoval počet. Laravel ve výchozím nastavení ukládá relace do databáze. Poskytuje také session()
funkce pro rozhraní s našimi daty relace, takže k vykreslení naší stránky stačilo jen pár řádků kódu.
class SessionController extends Controller
{
public function __invoke(Request $request)
{
$count = session('count', 0);
$count += 1;
session(['count' => $count]);
return 'Count is ' . $count;
}
}
Po nastavení php-fpm a Nginx naše stránka vypadá docela dobře...
╰─➤ 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
Alespoň do té doby, než skutečně uvidíme výsledky testů...
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
Ne, to není překlep. Náš testovací stroj přešel z 600 požadavků za sekundu při vykreslování složité stránky... na 21 požadavků za sekundu při vykreslování "Počet je 1".
Co se tedy pokazilo? Je něco špatně s naší instalací PHP? Zpomaluje se nějak Nginx při propojení s php-fpm?
Předělejme tuto stránku v čistém PHP kódu.
<?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'];
Nyní jsme použili 98 řádků kódu k tomu, co čtyři řádky kódu (a spoustu konfiguračních prací) v Laravelu. (Samozřejmě, pokud bychom správně ošetřili chyby a zobrazili zprávy pro uživatele, byl by to asi dvojnásobek počtu řádků.) Možná bychom mohli dosáhnout 30 požadavků za sekundu?
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
Páni! Zdá se, že na naší instalaci PHP nakonec není nic špatného. Čistá verze PHP dělá 700 požadavků za sekundu.
Pokud s PHP není nic špatného, možná jsme špatně nakonfigurovali Laravel?
Poté, co jsme prohledali web kvůli problémům s konfigurací a tipům na výkon, dvě z nejoblíbenějších technik byly ukládání konfiguračních a směrovacích dat do mezipaměti, aby se zabránilo jejich zpracování pro každý požadavek. Dáme proto na jejich rady a tyto tipy vyzkoušíme.
╰─➤ php artisan config:cache
INFO Configuration cached successfully.
╰─➤ php artisan route:cache
INFO Routes cached successfully.
Na příkazovém řádku vše vypadá dobře. Pojďme znovu provést 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
Nyní jsme zvýšili výkon z 21,04 na 28,80 požadavku za sekundu, což je dramatický nárůst o téměř 37 %! To by bylo docela působivé pro jakýkoli softwarový balík... kromě skutečnosti, že stále provádíme pouze 1/24 z počtu požadavků čisté verze PHP.
Pokud si myslíte, že s tímto testem musí být něco špatně, měli byste si promluvit s autorem PHP frameworku Lucinda. Ve výsledcích testů má Lucinda porazila Laravel 36x pro požadavky HTML a 90x pro požadavky JSON.
Po testování na vlastním stroji s Apache i Nginx nemám důvod o něm pochybovat. Laravel je opravdu spravedlivá že pomalý! PHP samo o sobě není tak špatné, ale jakmile přidáte veškeré další zpracování, které Laravel přidává ke každému požadavku, je pro mě velmi obtížné doporučit Laravel jako volbu v roce 2023.
PHP/Wordpress účty pro asi 40 % všech webových stránek na webu , což z něj dělá zdaleka nejdominantnější rámec. Osobně však zjišťuji, že popularita se nutně nepromítá do kvality o nic víc, než když zjišťuji, že mám náhlou neovladatelnou touhu po tomto mimořádném gurmánském jídle od nejoblíbenější restaurace na světě ... McDonald's. Vzhledem k tomu, že jsme již testovali čistý PHP kód, nebudeme testovat samotný Wordpress, protože cokoli zahrnující Wordpress by bylo nepochybně nižší než 700 požadavků za sekundu, které jsme pozorovali u čistého PHP.
Django je další populární framework, který existuje již dlouhou dobu. Pokud jste jej v minulosti používali, pravděpodobně si rádi vzpomínáte na jeho velkolepé rozhraní pro správu databází spolu s tím, jak otravné bylo konfigurovat vše tak, jak jste chtěli. Podívejme se, jak dobře Django funguje v roce 2023, zejména s novým rozhraním ASGI, které přidalo od verze 4.0.
Nastavení Djanga je pozoruhodně podobné nastavení Laravelu, protože oba pocházeli z doby, kdy byly architektury MVC stylové a správné. Přeskočíme nudnou konfiguraci a přejdeme rovnou k nastavení pohledu.
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}")
Čtyři řádky kódu jsou stejné jako u verze Laravel. Podívejme se, jak to funguje.
╰─➤ 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
Vůbec to není špatné s 355 požadavky za sekundu. Je pouze poloviční oproti čisté verzi PHP, ale je také 12x vyšší než u verze Laravel. Zdá se, že Django vs. Laravel není vůbec žádná soutěž.
Kromě větších rámců všeho druhu, včetně kuchyňských dřezů, existují také menší rámce, které provedou pouze základní nastavení a zbytek vám umožní zvládnout. Jedním z nejlépe použitelných je Flask a jeho ASGI protějšek Quart. Moje vlastní Rámec PaferaPy je postaven na flask, takze jsem dobre obeznámen s tím, jak snadné je dělat věci při zachování výkonu.
#!/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}'
Jak vidíte, skript Flask je kratší než čistý PHP skript. Zjistil jsem, že ze všech jazyků, které jsem použil, je Python pravděpodobně nejvýraznějším jazykem, pokud jde o psaní na klávesnici. Nedostatek složených závorek a závorek, porozumění seznamům a diktátům a blokování založené na odsazení spíše než na středníku činí Python poměrně jednoduchým, ale výkonným ve svých schopnostech.
Naneštěstí je Python také nejpomalejším jazykem pro obecné použití, a to navzdory tomu, kolik softwaru v něm bylo napsáno. Počet dostupných knihoven Pythonu je asi čtyřikrát větší než u podobných jazyků a pokrývá obrovské množství domén, ale nikdo by neřekl, že Python je rychlý a výkonný mimo výklenky, jako je NumPy.
Podívejme se, jak si naše verze Flask stojí v porovnání s našimi předchozími frameworky.
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
Náš skript Flask je ve skutečnosti rychlejší než naše čistá verze PHP!
Pokud vás to překvapuje, měli byste si uvědomit, že naše aplikace Flask provádí veškerou inicializaci a konfiguraci, když spustíme server gunicorn, zatímco PHP znovu spustí skript pokaždé, když přijde nový požadavek. ; je ekvivalentní tomu, že Flask je mladý, dychtivý taxikář, který už nastartoval auto a čeká u silnice, zatímco PHP je starý řidič, který zůstává ve svém domě a čeká na zavolání a teprve potom řídí abych tě vyzvedl. Být člověkem ze staré školy a pocházet z dob, kdy PHP bylo skvělou změnou na obyčejné HTML a SHTML soubory, je trochu smutné si uvědomit, kolik času uplynulo, ale rozdíly v designu opravdu ztěžují PHP. soutěžit se servery Python, Java a Node.js, které zůstávají v paměti a zpracovávají požadavky s hbitou lehkostí žongléra.
Flask může být zatím naším nejrychlejším frameworkem, ale ve skutečnosti je to docela starý software. Komunita Pythonu přešla před pár lety na novější asychronní servery ASGI a já sám jsem s nimi samozřejmě přešel.
Nejnovější verze Pafera Framework, PaferaPyAsync , je založen na Starlette. Ačkoli existuje ASGI verze Flask s názvem Quart, výkonnostní rozdíly mezi Quartem a Starlette byly dostatečné k tomu, abych svůj kód místo toho přestavěl na Starlette.
Asychronní programování může být pro mnoho lidí děsivé, ale ve skutečnosti to není obtížný koncept, protože kluci z Node.js tento koncept popularizovali před více než deseti lety.
Dříve jsme bojovali proti souběžnosti s multithreadingem, multiprocessingem, distribuovaným počítáním, řetězením slibů a všemi těmi zábavnými časy, které předčasně zestárly a vysušily mnoho zkušených programátorů. Teď jen píšeme async
před našimi funkcemi a await
před jakýmkoli kódem, jehož spuštění může chvíli trvat. Je to skutečně podrobnější než běžný kód, ale mnohem méně otravné, než když se musíte zabývat synchronizačními primitivy, předáváním zpráv a řešením slibů.
Náš soubor Starlette vypadá takto:
#!/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",
)
Jak můžete vidět, je do značné míry zkopírován a vložen z našeho skriptu Flask s pouze několika změnami směrování a async/await
klíčová slova.
Kolik vylepšení nám může kopírování a vkládání kódu skutečně přinést?
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
Máme nového šampiona, dámy a pánové! Naším předchozím maximem byla naše čistá verze PHP s rychlostí 704 požadavků za sekundu, kterou pak překonala naše verze Flask s rychlostí 1080 požadavků za sekundu. Náš skript Starlette rozdrtí všechny předchozí uchazeče rychlostí 4562 požadavků za sekundu, což znamená 6x zlepšení oproti čistému PHP a 4x zlepšení oproti Flasku.
Pokud jste ještě nezměnili svůj kód WSGI Python na ASGI, možná je nyní ten správný čas začít.
Doposud jsme se zabývali pouze frameworky PHP a Python. Velká část světa však ve skutečnosti používá pro své webové stránky Java, DotNet, Node.js, Ruby on Rails a další podobné technologie. Toto v žádném případě není komplexní přehled všech světových ekosystémů a biomů, takže abychom se vyhnuli programování ekvivalentu organické chemie, vybereme pouze rámce, pro které je nejjednodušší zadat kód. z nichž Java rozhodně není.
Pokud jste se neskrývali pod svou kopií K&R C nebo Knuth's Umění počítačového programování za posledních patnáct let jste pravděpodobně slyšeli o Node.js. Ti z nás, kteří existují od počátku JavaScriptu, jsou buď neuvěřitelně vyděšení, ohromeni, nebo obojím stavem moderního JavaScriptu, ale nelze popřít, že JavaScript se stal silou, se kterou je třeba počítat i na serverech. jako prohlížeče. Koneckonců, nyní máme v jazyce dokonce nativní 64bitová celá čísla! To je daleko lepší než všechno, co je uloženo v 64bitových plovácích!
ExpressJS je pravděpodobně nejsnáze použitelný server Node.js, takže vytvoříme rychlou a špinavou aplikaci Node.js/ExpressJS, která bude sloužit našemu počítadlu.
/**********************************************************************
* 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}`));
Tento kód bylo ve skutečnosti snazší napsat než verze Pythonu, ačkoli nativní JavaScript se stává poněkud nepraktickým, když se aplikace zvětší, a všechny pokusy o nápravu, jako je TypeScript, se rychle stanou podrobnějšími než Python.
Podívejme se, jak to funguje!
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
Možná jste slyšeli starodávné (stejně tak staré podle internetových standardů...) lidové pohádky o Node.js' rychlost a tyto příběhy jsou většinou pravdivé díky velkolepé práci, kterou Google odvedl s V8 JavaScript enginem. V tomto případě, ačkoli naše rychlá aplikace překonává skript Flask, její jednovláknová povaha je poražena čtyřmi asynchronními procesy, které ovládá Starlette Knight, který říká „Ni!“.
Pojďme získat další pomoc!
╰─➤ 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 │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Dobře! Nyní je to vyrovnaná bitva čtyři na čtyři! Pojďme srovnávat!
╰─➤ 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
Stále to není úplně na úrovni Starlette, ale na rychlý pětiminutový hack JavaScriptu to není špatné. Z mého vlastního testování je tento skript ve skutečnosti trochu zdržen na úrovni rozhraní databáze, protože node-postgres není zdaleka tak efektivní jako psycopg pro Python. Přepnutí na sqlite jako databázový ovladač přináší přes 3000 požadavků za sekundu pro stejný kód ExpressJS.
Hlavní věc, kterou je třeba poznamenat, je, že navzdory pomalé rychlosti provádění Pythonu mohou rámce ASGI skutečně konkurovat řešením Node.js pro určitá pracovní zatížení.
Nyní se tedy přibližujeme k vrcholu hory a horou mám na mysli nejvyšší srovnávací skóre zaznamenané myší i muži.
Pokud se podíváte na většinu benchmarků frameworků dostupných na webu, všimnete si, že existují dva jazyky, které mají tendenci dominovat na vrcholu: C++ a Rust. S C++ pracuji od 90. let a dokonce jsem měl svůj vlastní Win32 C++ framework ještě předtím, než MFC/ATL byla věc, takže s jazykem mám bohaté zkušenosti. Není moc zábavné s něčím pracovat, když to už znáte, takže místo toho uděláme verzi Rust. ;)
Rust je relativně nový, pokud jde o programovací jazyky, ale stal se pro mě předmětem zvědavosti, když Linus Torvalds oznámil, že přijme Rust jako programovací jazyk jádra Linuxu. Pro nás starší programátory je to asi totéž, jako kdybychom řekli, že tato nová fangled new age hippie věcička bude novým dodatkem k ústavě USA.
Nyní, když jste zkušený programátor, máte tendenci nenaskočit do rozjetého vlaku tak rychle jako mladší lidé, jinak byste se mohli popálit rychlými změnami jazyka nebo knihoven. (Každý, kdo používal první verzi AngularJS, bude vědět, o čem mluvím.) Rust je stále poněkud ve fázi experimentálního vývoje a připadá mi legrační, že tolik příkladů kódu na webu kompilovat již s aktuálními verzemi balíčků.
Výkon, který předvádějí aplikace Rust, však nelze upřít. Pokud jste to nikdy nezkusili ripgrep nebo fd-najít na velkých stromech zdrojového kódu byste je rozhodně měli vyzkoušet. Jsou dokonce k dispozici pro většinu distribucí Linuxu jednoduše ze správce balíčků. S Rustem vyměňujete upovídanost za výkon... a hodně výřečnosti pro a hodně výkonu.
Kompletní kód pro Rust je trochu velký, takže se zde jen podíváme na příslušné handlery:
// =====================================================================
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
)))
}
To je mnohem složitější než verze 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
A mnohem výkonnější!
Náš Rust server využívající Actix/deadpool_postgres obratně poráží našeho předchozího šampióna Starlette o +125 %, ExpressJS o +362 % a čisté PHP o +1366 %. (Ponechám deltu výkonu s verzí Laravel jako cvičení pro čtenáře.)
Zjistil jsem, že naučit se samotný jazyk Rust bylo obtížnější než jiné jazyky, protože má mnohem více problémů než cokoli, co jsem viděl mimo shromáždění 6502, ale pokud váš server Rust zvládne 14x více uživatelů jako váš PHP server, pak se možná přepínáním technologií přeci jen dá něco získat. To je důvod, proč další verze Pafera Framework bude založena na Rustu. Křivka učení je mnohem vyšší než u skriptovacích jazyků, ale výkon bude stát za to. Pokud nemůžete věnovat čas tomu, abyste se naučili Rust, pak založit svůj technologický stack na Starlette nebo Node.js také není špatné rozhodnutí.
V posledních dvaceti letech jsme přešli od levných statických hostingových webů přes sdílený hosting se zásobníky LAMP až po pronájem VPS pro AWS, Azure a další cloudové služby. V současné době se mnoho společností spokojuje s rozhodováním o návrhu na základě toho, koho považují za dostupného nebo nejlevnějšího, protože příchod pohodlných cloudových služeb usnadnil nahodit více hardwaru na pomalé servery a aplikace. To jim přineslo velké krátkodobé zisky za cenu dlouhodobého technického dluhu.
Před 70 lety proběhl velký vesmírný závod mezi Sovětským svazem a Spojenými státy. Sověti vyhráli většinu prvních milníků. Měli první satelit ve Sputniku, prvního psa ve vesmíru v Lajce, první měsíční kosmickou loď na Luně 2, prvního muže a ženu ve vesmíru v Juriji Gagarinovi a Valentině Těreškovové a tak dále...
Ale pomalu se jim hromadil technický dluh.
Ačkoli Sověti byli první v každém z těchto úspěchů, jejich inženýrské procesy a cíle způsobily, že se zaměřili na krátkodobé výzvy spíše než na dlouhodobou proveditelnost. Vyhráli pokaždé, když skočili, ale byli stále unavenější a pomalejší, zatímco jejich soupeři pokračovali v konzistentních krocích směrem k cílové čáře.
Jakmile Neil Armstrong podnikl své historické kroky na Měsíci v přímém televizním přenosu, Američané se ujali vedení a pak tam zůstali, zatímco sovětský program zakolísal. To se neliší od dnešních společností, které se soustředily na další velkou věc, další velkou výplatu nebo další velkou technologii, přičemž se jim nepodařilo vytvořit správné návyky a strategie na dlouhou trať.
Být první na trhu neznamená, že se stanete dominantním hráčem na tomto trhu. Alternativně, věnovat čas tomu, abyste dělali věci správně, nezaručuje úspěch, ale rozhodně zvyšuje vaše šance na dlouhodobé úspěchy. Pokud jste pro svou společnost technologickým lídrem, vyberte si správný směr a nástroje pro svou pracovní zátěž. Nedovolte, aby popularita nahradila výkon a efektivitu.
Chcete si stáhnout soubor 7z obsahující skripty Rust, ExpressJS, Flask, Starlette a Pure PHP?
O autorovi |
|
![]() |
Jim programuje od doby, kdy v 90. letech získal IBM PS/2. Dodnes stále preferuje ruční psaní HTML a SQL a ve své práci se zaměřuje na efektivitu a korektnost. |