Po jednej z moich ostatnich rozmów kwalifikacyjnych byłem zaskoczony, gdy zdałem sobie sprawę, że firma, do której aplikowałem, nadal używała Laravela, frameworka PHP, którego próbowałem około dekady temu. Był niezły jak na tamte czasy, ale jeśli jest coś stałego w technologii i modzie, to ciągła zmiana i ponowne pojawianie się stylów i koncepcji. Jeśli jesteś programistą JavaScript, prawdopodobnie znasz ten stary dowcip
Programista 1: „Nie podoba mi się ten nowy framework JavaScript!”
Programista 2: „Nie ma się czym martwić. Poczekaj sześć miesięcy, a pojawi się ktoś inny, kto go zastąpi!”
Z ciekawości postanowiłem sprawdzić dokładnie, co się stanie, gdy wystawimy na próbę stare i nowe. Oczywiście sieć jest pełna benchmarków i twierdzeń, z których najpopularniejszym jest prawdopodobnie Testy porównawcze TechEmpower Web Framework tutaj . Dzisiaj nie będziemy robić niczego tak skomplikowanego jak oni. Będziemy trzymać się prostych i przyjemnych rzeczy, aby ten artykuł nie zamienił się w Wojna i pokój , i że będziesz mieć niewielką szansę na pozostanie przytomnym, gdy skończysz czytać. Obowiązują zwykłe zastrzeżenia: to może nie działać tak samo na twoim komputerze, różne wersje oprogramowania mogą wpływać na wydajność, a kot Schrödingera faktycznie stał się kotem zombie, który był w połowie żywy i w połowie martwy w dokładnie tym samym czasie.
Do tego testu użyję mojego laptopa wyposażonego w słaby procesor i5, na którym działa system Manjaro Linux, jak pokazano tutaj.
╰─➤ 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
Nasz kod będzie miał trzy proste zadania dla każdego żądania:
Co to za idiotyczny test, możesz zapytać? Cóż, jeśli spojrzysz na żądania sieciowe dla tej strony, zauważysz jedno o nazwie sessionvars.js, które robi dokładnie to samo.
Widzisz, współczesne strony internetowe są skomplikowanymi tworami i jednym z najczęstszych zadań jest buforowanie złożonych stron w celu uniknięcia nadmiernego obciążenia serwera bazy danych.
Jeśli będziemy renderować złożoną stronę za każdym razem, gdy użytkownik o nią poprosi, będziemy w stanie obsłużyć tylko około 600 użytkowników na sekundę.
╰─➤ 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
Jeśli jednak zapiszemy tę stronę w postaci statycznego pliku HTML i pozwolimy Nginx szybko przesłać ją użytkownikowi, wówczas będziemy mogli obsłużyć 32 000 użytkowników na sekundę, co zwiększy wydajność 50-krotnie.
╰─➤ 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
Statyczny index.en.html to część, która trafia do wszystkich, a tylko części różniące się w zależności od użytkownika są wysyłane w sessionvars.js. To nie tylko zmniejsza obciążenie bazy danych i tworzy lepsze doświadczenie dla naszych użytkowników, ale także zmniejsza prawdopodobieństwo kwantowe, że nasz serwer spontanicznie wyparuje w wyniku naruszenia rdzenia warp, gdy zaatakują Klingoni.
Zwrócony kod dla każdego frameworka będzie miał jedno proste wymaganie: pokaż użytkownikowi, ile razy odświeżył stronę, mówiąc „Liczba wynosi x”. Aby zachować prostotę, na razie będziemy trzymać się z dala od kolejek Redis, komponentów Kubernetes lub AWS Lambdas.
Dane sesji każdego użytkownika zostaną zapisane w bazie danych PostgreSQL.
A ta tabela bazy danych będzie obcinana przed każdym testem.
Proste, a zarazem skuteczne – takie jest motto Pafery... przynajmniej pomijając najciemniejsze ramy czasowe...
Okej, więc teraz możemy wreszcie zacząć brudzić sobie ręce. Pominiemy konfigurację Laravela, ponieważ to tylko kilka poleceń composer i artisan.
Najpierw skonfigurujemy ustawienia bazy danych w pliku .env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=sessiontest
DB_USERNAME=sessiontest
DB_PASSWORD=sessiontest
Następnie ustawimy jedną trasę zapasową, która będzie wysyłać każde żądanie do naszego kontrolera.
Route::fallback(SessionController::class);
I ustaw kontroler tak, aby wyświetlał liczbę. Laravel domyślnie przechowuje sesje w bazie danych. Zapewnia również session()
funkcję umożliwiającą komunikację z danymi sesji, więc do wyświetlenia naszej strony wystarczyło kilka linijek kodu.
class SessionController extends Controller
{
public function __invoke(Request $request)
{
$count = session('count', 0);
$count += 1;
session(['count' => $count]);
return 'Count is ' . $count;
}
}
Po skonfigurowaniu php-fpm i Nginx nasza strona wygląda całkiem nieźle...
╰─➤ 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
Przynajmniej do czasu, aż zobaczymy wyniki testów...
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
Nie, to nie literówka. Nasza maszyna testowa przeszła z 600 żądań na sekundę renderujących złożoną stronę... do 21 żądań na sekundę renderujących "Liczba wynosi 1".
Co więc poszło nie tak? Czy coś jest nie tak z naszą instalacją PHP? Czy Nginx jakoś zwalnia podczas łączenia się z php-fpm?
Stwórzmy tę stronę ponownie, używając czystego kodu 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'];
Użyliśmy już 98 linii kodu, aby zrobić to, co w Laravelu wymagało czterech linii kodu (i całej masy pracy konfiguracyjnej). (Oczywiście, gdybyśmy odpowiednio obsługiwali błędy i wyświetlali użytkownikom komunikaty, liczba linii byłaby około dwukrotnie większa). Może moglibyśmy dojść do 30 żądań na sekundę?
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
Wow! Wygląda na to, że jednak nie ma nic złego w naszej instalacji PHP. Czysta wersja PHP wykonuje 700 żądań na sekundę.
Jeśli z PHP wszystko jest w porządku, to może źle skonfigurowaliśmy Laravel?
Po przeszukaniu sieci w poszukiwaniu problemów z konfiguracją i wskazówek dotyczących wydajności, dwiema najpopularniejszymi technikami było buforowanie danych konfiguracji i trasowania, aby uniknąć przetwarzania ich przy każdym żądaniu. Dlatego skorzystamy z ich rady i wypróbujemy te wskazówki.
╰─➤ php artisan config:cache
INFO Configuration cached successfully.
╰─➤ php artisan route:cache
INFO Routes cached successfully.
Wszystko wygląda dobrze na wierszu poleceń. Powtórzmy test porównawczy.
╰─➤ 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
Cóż, teraz zwiększyliśmy wydajność z 21,04 do 28,80 żądań na sekundę, co stanowi drastyczny wzrost o prawie 37%! Byłoby to całkiem imponujące dla każdego pakietu oprogramowania... z wyjątkiem faktu, że nadal wykonujemy tylko 1/24 liczby żądań w czystej wersji PHP.
Jeśli myślisz, że coś musi być nie tak z tym testem, powinieneś porozmawiać z autorem frameworka PHP Lucinda. W wynikach testu napisał: Lucinda pokonuje Laravela o 36x dla żądań HTML i 90x dla żądań JSON.
Po przetestowaniu na własnym komputerze zarówno z Apache, jak i Nginx, nie mam powodu, aby w niego wątpić. Laravel jest naprawdę po prostu To wolno! PHP samo w sobie nie jest takie złe, ale gdy dodasz do tego całe dodatkowe przetwarzanie, które Laravel dodaje do każdego żądania, to uważam, że Laravel jest bardzo trudny do polecenia jako wybór w 2023 roku.
Konta PHP/Wordpress dla około 40% wszystkich stron internetowych w sieci , co czyni go zdecydowanie najbardziej dominującym frameworkiem. Osobiście jednak uważam, że popularność niekoniecznie przekłada się na jakość, tak samo jak nie odczuwam nagłej, niekontrolowanej potrzeby zjedzenia tego niezwykłego, wykwintnego jedzenia z najpopularniejsza restauracja na świecie ... McDonald's. Ponieważ testowaliśmy już czysty kod PHP, nie będziemy testować samego WordPressa, ponieważ wszystko, co dotyczy WordPressa, byłoby niewątpliwie niższe niż 700 żądań na sekundę, które zaobserwowaliśmy w czystym PHP.
Django to kolejny popularny framework, który istnieje od dawna. Jeśli korzystałeś z niego w przeszłości, prawdopodobnie z rozrzewnieniem wspominasz jego spektakularny interfejs administracyjny bazy danych, a także to, jak irytujące było konfigurowanie wszystkiego dokładnie tak, jak chciałeś. Zobaczmy, jak dobrze Django działa w 2023 r., zwłaszcza z nowym interfejsem ASGI, który dodano od wersji 4.0.
Konfigurowanie Django jest zadziwiająco podobne do konfigurowania Laravel, ponieważ oba pochodzą z czasów, gdy architektury MVC były stylowe i poprawne. Pominiemy nudną konfigurację i przejdziemy bezpośrednio do konfigurowania widoku.
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}")
Cztery linijki kodu są takie same jak w wersji Laravel. Zobaczmy, jak to działa.
╰─➤ 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
Wcale nie jest źle przy 355 żądaniach na sekundę. To tylko połowa wydajności wersji czystego PHP, ale jest też 12 razy większa niż w przypadku wersji Laravel. Django kontra Laravel nie wydaje się być wcale konkurencją.
Oprócz większych frameworków typu everything-including-the-kitchen-sink, istnieją również mniejsze frameworki, które wykonują tylko podstawowe ustawienia, pozwalając ci zająć się resztą. Jednym z najlepszych jest Flask i jego odpowiednik ASGI Quart. Mój własny Struktura PaferaPy jest zbudowany na bazie Flask, więc doskonale wiem, jak łatwo można w nim wykonywać zadania, zachowując przy tym wydajność.
#!/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 widać, skrypt Flask jest krótszy niż czysty skrypt PHP. Uważam, że ze wszystkich języków, których używałem, Python jest prawdopodobnie najbardziej ekspresyjnym językiem pod względem naciśnięć klawiszy. Brak nawiasów klamrowych i okrągłych, list i dict comprehensions oraz blokowanie oparte na wcięciach, a nie średnikach, sprawiają, że Python jest dość prosty, ale ma potężne możliwości.
Niestety, Python jest również najwolniejszym językiem ogólnego przeznaczenia, pomimo tego, ile oprogramowania zostało w nim napisane. Liczba dostępnych bibliotek Pythona jest około cztery razy większa niż w podobnych językach i obejmuje ogromną liczbę domen, ale nikt nie powiedziałby, że Python jest szybki ani wydajny poza niszami takimi jak NumPy.
Porównajmy naszą wersję Flask z naszymi poprzednimi rozwiązaniami.
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
Nasz skrypt Flask jest faktycznie szybszy niż nasza wersja napisana w czystym PHP!
Jeśli to Cię zaskoczyło, powinieneś zdać sobie sprawę, że nasza aplikacja Flask wykonuje całą swoją inicjalizację i konfigurację, gdy uruchamiamy serwer gunicorn, podczas gdy PHP ponownie uruchamia skrypt za każdym razem, gdy pojawia się nowe żądanie. To odpowiednik Flaska jako młodego, chętnego taksówkarza, który już uruchomił samochód i czeka przy drodze, podczas gdy PHP jest starym kierowcą, który zostaje w domu, czekając na telefon, a dopiero potem przyjeżdża, aby Cię odebrać. Będąc starym facetem i pochodzącym z czasów, gdy PHP było wspaniałą zmianą w stosunku do zwykłych plików HTML i SHTML, jest trochę smutno zdać sobie sprawę, ile czasu minęło, ale różnice w projekcie naprawdę utrudniają PHP konkurowanie z serwerami Python, Java i Node.js, które po prostu pozostają w pamięci i obsługują żądania z zwinnością żonglera.
Flask może być naszym najszybszym frameworkiem do tej pory, ale tak naprawdę jest to dość stare oprogramowanie. Społeczność Pythona przeszła na nowsze, asymetryczne serwery ASGI kilka lat temu, a ja oczywiście przeszedłem wraz z nimi.
Najnowsza wersja Pafera Framework, PaferaPyAsync , jest oparty na Starlette. Chociaż istnieje wersja ASGI Flask o nazwie Quart, różnice w wydajności między Quart i Starlette były wystarczające, abym ponownie oparł swój kod na Starlette.
Programowanie asynchroniczne może przerażać wiele osób, ale tak naprawdę nie jest to trudna koncepcja dzięki ludziom pracującym nad Node.js, którzy spopularyzowali ją ponad dekadę temu.
Kiedyś walczyliśmy ze współbieżnością za pomocą wielowątkowości, przetwarzania wieloprocesowego, przetwarzania rozproszonego, łańcuchowania obietnic i wszystkich tych zabawnych czasów, które przedwcześnie starzeją i wysuszają wielu doświadczonych programistów. Teraz po prostu wpisujemy async
przed naszymi funkcjami i await
przed jakimkolwiek kodem, którego wykonanie może zająć trochę czasu. Jest to rzeczywiście bardziej rozwlekłe niż zwykły kod, ale znacznie mniej irytujące w użyciu niż konieczność radzenia sobie z prymitywami synchronizacji, przekazywaniem wiadomości i rozwiązywaniem obietnic.
Nasz plik Starlette wygląda następująco:
#!/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 widać, jest to w zasadzie skopiowane i wklejone z naszego skryptu Flask, z kilkoma zmianami w routingu i async/await
słowa kluczowe.
Jak wiele ulepszeń może nam dać kopiowanie i wklejanie kodu?
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
Mamy nowego mistrza, panie i panowie! Nasz poprzedni rekord to czysta wersja PHP z 704 żądaniami na sekundę, którą następnie prześcignęła nasza wersja Flask z 1080 żądaniami na sekundę. Nasz skrypt Starlette miażdży wszystkich poprzednich konkurentów z 4562 żądaniami na sekundę, co oznacza 6-krotną poprawę w stosunku do czystego PHP i 4-krotną poprawę w stosunku do Flask.
Jeśli jeszcze nie zmieniłeś swojego kodu WSGI Python na ASGI, teraz może być dobry moment, aby to zrobić.
Do tej pory omawialiśmy tylko frameworki PHP i Python. Jednak duża część świata faktycznie używa Java, DotNet, Node.js, Ruby on Rails i innych podobnych technologii na swoich stronach internetowych. To w żadnym wypadku nie jest kompleksowy przegląd wszystkich ekosystemów i biomów świata, więc aby uniknąć programowania równoważnego chemii organicznej, wybierzemy tylko te frameworki, dla których najłatwiej jest pisać kod... a Java zdecydowanie do nich nie należy.
Chyba że ukrywałeś się pod swoim egzemplarzem K&R C lub Knutha Sztuka programowania komputerowego przez ostatnie piętnaście lat, prawdopodobnie słyszałeś o Node.js. Ci z nas, którzy są w pobliżu od początku JavaScript, są albo niesamowicie przestraszeni, albo zdumieni, albo jedno i drugie stanem nowoczesnego JavaScript, ale nie da się zaprzeczyć, że JavaScript stał się siłą, z którą trzeba się liczyć na serwerach, jak i przeglądarkach. W końcu mamy teraz w języku natywne 64-bitowe liczby całkowite! To o wiele lepsze niż wszystko przechowywane w 64-bitowych liczbach zmiennoprzecinkowych!
ExpressJS jest prawdopodobnie najłatwiejszym w użyciu serwerem Node.js, więc szybko i sprawnie stworzymy aplikację Node.js/ExpressJS do obsługi naszego licznika.
/**********************************************************************
* 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}`));
Tak naprawdę napisanie tego kodu było łatwiejsze niż napisanie wersji w Pythonie, choć natywny JavaScript staje się dość nieporęczny, gdy aplikacje stają się większe, a wszelkie próby skorygowania tego problemu, takie jak w przypadku TypeScript, szybko stają się bardziej rozwlekłe niż w przypadku Pythona.
Zobaczmy, jak to działa!
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
Być może słyszałeś starożytne (starożytne według standardów internetowych...) opowieści ludowe o szybkości Node.js, a historie te są w większości prawdziwe dzięki spektakularnej pracy, jaką Google wykonało z silnikiem JavaScript V8. W tym przypadku jednak, chociaż nasza szybka aplikacja przewyższa skrypt Flask, jej jednowątkowa natura zostaje pokonana przez cztery procesy asynchroniczne obsługiwane przez Starlette Knight, która mówi „Ni!”.
Potrzebna nam dodatkowa 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 │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Okej! Teraz to wyrównana walka czterech na czterech! Zróbmy 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
Nadal nie jest to poziom Starlette, ale nie jest źle jak na szybki, pięciominutowy hack JavaScript. Z moich własnych testów wynika, że ten skrypt jest trochę powstrzymywany na poziomie interfejsu bazy danych, ponieważ node-postgres nie jest wcale tak wydajny jak psycopg dla Pythona. Przejście na sqlite jako sterownik bazy danych generuje ponad 3000 żądań na sekundę dla tego samego kodu ExpressJS.
Należy przede wszystkim zauważyć, że pomimo powolnej prędkości wykonywania kodu w Pythonie, frameworki ASGI mogą w przypadku niektórych obciążeń dorównywać rozwiązaniom Node.js.
Zbliżamy się więc do szczytu góry, a przez górę mam na myśli najwyższe wyniki testów laboratoryjnych odnotowane zarówno przez myszy, jak i ludzi.
Jeśli przyjrzysz się większości dostępnych w sieci testów porównawczych frameworków, zauważysz, że na szczycie są dwa języki: C++ i Rust. Pracowałem z C++ od lat 90. i miałem nawet własny framework Win32 C++, zanim jeszcze MFC/ATL stało się czymś powszechnym, więc mam duże doświadczenie z tym językiem. Praca z czymś, co już znasz, nie jest zbyt przyjemna, więc zamiast tego zrobimy wersję Rust. ;)
Rust jest stosunkowo nowym językiem programowania, ale stał się obiektem mojej ciekawości, gdy Linus Torvalds ogłosił, że zaakceptuje Rust jako język programowania jądra Linuksa. Dla nas, starszych programistów, to mniej więcej tak, jakby powiedzieć, że ta nowomodna, hipisowska nowość będzie nową poprawką do Konstytucji Stanów Zjednoczonych.
Teraz, gdy jesteś doświadczonym programistą, nie wskakujesz do pociągu tak szybko, jak robią to młodsi, bo możesz się poparzyć szybkimi zmianami w języku lub bibliotekach. (Każdy, kto używał pierwszej wersji AngularJS, wie, o czym mówię.) Rust jest wciąż w fazie eksperymentalnego rozwoju i uważam za zabawne, że tak wiele przykładów kodu w sieci nie kompiluje się już nawet z obecnymi wersjami pakietów.
Jednakże wydajność aplikacji Rust nie może być zaprzeczona. Jeśli nigdy nie próbowałeś ripgrep Lub fd-znajdź na dużych drzewach kodu źródłowego, zdecydowanie powinieneś je wypróbować. Są nawet dostępne dla większości dystrybucji Linuksa po prostu z menedżera pakietów. Wymieniasz gadatliwość na wydajność z Rust... działka wielosłowności dla działka wydajności.
Cały kod dla Rust jest trochę długi, więc przyjrzymy się tylko odpowiednim procedurom obsługi:
// =====================================================================
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 jest o wiele bardziej skomplikowane niż wersje 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
I o wiele bardziej wydajny!
Nasz serwer Rust wykorzystujący Actix/deadpool_postgres z łatwością pokonuje naszego poprzedniego mistrza Starlette o +125%, ExpressJS o +362% i czysty PHP o +1366%. (Deltę wydajności pozostawię na przykładzie wersji Laravel jako ćwiczenie dla czytelnika.)
Odkryłem, że nauka samego języka Rust jest trudniejsza niż innych języków, ponieważ ma o wiele więcej pułapek niż cokolwiek, co widziałem poza 6502 Assembly, ale jeśli twój serwer Rust może obsłużyć 14 razy więcej użytkowników niż twój serwer PHP, to być może jest coś do zyskania ze zmiany technologii. Dlatego kolejna wersja Pafera Framework będzie oparta na Rust. Krzywa uczenia się jest o wiele wyższa niż w przypadku języków skryptowych, ale wydajność będzie tego warta. Jeśli nie możesz poświęcić czasu na naukę Rust, to oparcie swojego stosu technologicznego na Starlette lub Node.js również nie jest złą decyzją.
W ciągu ostatnich dwudziestu lat przeszliśmy od tanich statycznych witryn hostingowych do hostingu współdzielonego ze stosami LAMP, do wynajmu VPS-ów do AWS, Azure i innych usług w chmurze. Obecnie wiele firm zadowala się podejmowaniem decyzji projektowych na podstawie tego, kogo mogą znaleźć, kto jest dostępny lub najtańszy, ponieważ pojawienie się wygodnych usług w chmurze ułatwiło wrzucanie większej ilości sprzętu do wolnych serwerów i aplikacji. Dało im to duże krótkoterminowe zyski kosztem długoterminowego długu technicznego.
70 lat temu miał miejsce wielki wyścig kosmiczny między Związkiem Radzieckim a Stanami Zjednoczonymi. Sowieci odnieśli większość wczesnych kamieni milowych. Mieli pierwszego satelitę w Sputniku, pierwszego psa w kosmosie w Łajce, pierwszy statek kosmiczny na Księżyc w Łunie 2, pierwszego mężczyznę i kobietę w kosmosie w Juriju Gagarinie i Walentynie Tierieszkowej i tak dalej...
Jednak powoli gromadzili dług techniczny.
Chociaż Sowieci byli pierwsi w każdym z tych osiągnięć, ich procesy inżynieryjne i cele powodowały, że koncentrowali się na krótkoterminowych wyzwaniach, a nie na długoterminowej wykonalności. Wygrywali za każdym razem, gdy skakali, ale byli coraz bardziej zmęczeni i wolniejsi, podczas gdy ich przeciwnicy nadal stawiali stałe kroki w kierunku mety.
Kiedy Neil Armstrong postawił swoje historyczne kroki na księżycu w telewizji na żywo, Amerykanie objęli prowadzenie, a następnie pozostali tam, gdy radziecki program się załamał. Nie różni się to niczym od dzisiejszych firm, które skupiają się na kolejnej wielkiej rzeczy, kolejnej wielkiej wypłacie lub kolejnej wielkiej technologii, nie rozwijając jednocześnie właściwych nawyków i strategii na dłuższą metę.
Bycie pierwszym na rynku nie oznacza, że staniesz się dominującym graczem na tym rynku. Z drugiej strony poświęcenie czasu na robienie rzeczy właściwie nie gwarantuje sukcesu, ale z pewnością zwiększa szanse na długoterminowe osiągnięcia. Jeśli jesteś liderem technicznym w swojej firmie, wybierz właściwy kierunek i narzędzia do swojego obciążenia pracą. Nie pozwól, aby popularność zastąpiła wydajność i efektywność.
Chcesz pobrać plik 7z zawierający skrypty Rust, ExpressJS, Flask, Starlette i Pure PHP?
O autorze |
|
![]() |
Jim programuje odkąd dostał IBM PS/2 w latach 90. Do dziś woli pisać HTML i SQL ręcznie, a w swojej pracy skupia się na wydajności i poprawności. |