След едно от последните ми интервюта за работа бях изненадан да разбера, че компанията, за която кандидатствах, все още използва Laravel, PHP рамка, която пробвах преди около десетилетие. Беше прилично за времето, но ако има една константа както в технологиите, така и в модата, това е непрестанната промяна и повторно появяване на стилове и концепции. Ако сте JavaScript програмист, вероятно сте запознати с този стар виц
Програмист 1: "Не ми харесва тази нова рамка на JavaScript!"
Програмист 2: "Няма нужда да се притеснявате. Просто изчакайте шест месеца и ще има друг, който да го замени!"
От любопитство реших да видя какво точно се случва, когато тестваме старо и ново. Разбира се, мрежата е пълна с бенчмаркове и твърдения, от които най-популярният вероятно е TechEmpower Web Framework Benchmarks тук . Въпреки това днес няма да правим нещо толкова сложно, колкото тях. Ще поддържаме нещата хубави и прости, така че тази статия да не се превърне в Война и мир , и че ще имате малък шанс да останете будни до момента, в който приключите с четенето. Прилагат се обичайните предупреждения: това може да не работи по същия начин на вашата машина, различните версии на софтуера могат да повлияят на производителността и котката на Шрьодингер всъщност се превърна в котка-зомби, която беше наполовина жива и наполовина мъртва по едно и също време.
За този тест ще използвам лаптопа си, въоръжен с слаб i5, работещ с Manjaro Linux, както е показано тук.
╰─➤ 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
Нашият код ще има три прости задачи за всяка заявка:
Що за идиотски тест е това, може да попитате? Е, ако погледнете мрежовите заявки за тази страница, ще забележите една, наречена sessionvars.js, която прави абсолютно същото.
Виждате ли, съвременните уеб страници са сложни същества и една от най-честите задачи е кеширането на сложни страници, за да се избегне прекомерното натоварване на сървъра на базата данни.
Ако изобразяваме повторно сложна страница всеки път, когато потребител я поиска, тогава можем да обслужваме само около 600 потребители в секунда.
╰─➤ 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
Но ако кешираме тази страница като статичен HTML файл и позволим на Nginx бързо да я хвърли през прозореца към потребителя, тогава можем да обслужваме 32 000 потребители в секунда, увеличавайки производителността с коефициент 50 пъти.
╰─➤ 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
Статичният index.en.html е частта, която отива на всички, и само частите, които се различават по потребител, се изпращат в sessionvars.js. Това не само намалява натоварването на базата данни и създава по-добро изживяване за нашите потребители, но също така намалява квантовите вероятности нашият сървър спонтанно да се изпари при пробив във варп ядрото, когато клингоните атакуват.
Върнатият код за всяка рамка ще има едно просто изискване: покажете на потребителя колко пъти са опреснявали страницата, като кажете "Count is x". За да опростим нещата, засега ще стоим далеч от опашките на Redis, компонентите на Kubernetes или AWS Lambdas.
Данните за сесията на всеки потребител ще бъдат записани в база данни на PostgreSQL.
И тази таблица на базата данни ще бъде съкратена преди всеки тест.
Просто, но ефективно е мотото на Pafera... така или иначе извън най-тъмната времева линия...
Добре, сега най-накрая можем да започнем да си цапаме ръцете. Ще пропуснем настройката за Laravel, тъй като това е просто куп композитор и занаятчия команди.
Първо, ще настроим настройките на нашата база данни във файла .env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=sessiontest
DB_USERNAME=sessiontest
DB_PASSWORD=sessiontest
След това ще зададем един резервен маршрут, който изпраща всяка заявка до нашия контролер.
Route::fallback(SessionController::class);
И настройте контролера да показва броя. Laravel по подразбиране съхранява сесии в базата данни. Той също така осигурява session()
функция за взаимодействие с нашите данни от сесията, така че бяха необходими само няколко реда код, за да изобразим нашата страница.
class SessionController extends Controller
{
public function __invoke(Request $request)
{
$count = session('count', 0);
$count += 1;
session(['count' => $count]);
return 'Count is ' . $count;
}
}
След настройката на php-fpm и Nginx страницата ни изглежда доста добре...
╰─➤ 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
Поне докато не видим резултатите от теста...
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
Не, това не е печатна грешка. Нашата тестова машина премина от 600 заявки в секунда, изобразяващи сложна страница... до 21 заявки в секунда, изобразявайки "Count is 1".
И така, какво се обърка? Нещо не е наред с нашата PHP инсталация? Nginx по някакъв начин забавя ли се при взаимодействие с php-fpm?
Нека повторим тази страница в чист 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'];
Вече сме използвали 98 реда код, за да направим това, което направиха четири реда код (и цял куп конфигурационна работа) в Laravel. (Разбира се, ако направихме правилна обработка на грешки и съобщения, изправени пред потребителите, това ще бъде около два пъти повече от броя на редовете.) Може би можем да достигнем до 30 заявки в секунда?
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
Уау! Изглежда, че в края на краищата няма нищо лошо в нашата PHP инсталация. Чистата PHP версия прави 700 заявки в секунда.
Ако няма нищо лошо в PHP, може би неправилно сме конфигурирали Laravel?
След претърсване на мрежата за проблеми с конфигурацията и съвети за производителност, две от най-популярните техники бяха да се кешират данните за конфигурацията и маршрута, за да се избегне обработката им за всяка заявка. Затова ще приемем съветите им и ще изпробваме тези съвети.
╰─➤ php artisan config:cache
INFO Configuration cached successfully.
╰─➤ php artisan route:cache
INFO Routes cached successfully.
Всичко изглежда добре на командния ред. Нека повторим бенчмарка.
╰─➤ 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
Е, сега увеличихме производителността от 21,04 на 28,80 заявки в секунда, драматично увеличение от почти 37%! Това би било доста впечатляващо за всеки софтуерен пакет... с изключение на факта, че ние все още изпълняваме само 1/24 от броя заявки на чистата PHP версия.
Ако смятате, че нещо не е наред с този тест, трябва да говорите с автора на Lucinda PHP framework. Според резултатите от теста той има Лусинда побеждава Laravel с 36x за HTML заявки и 90x за JSON заявки.
След като тествах на собствената си машина с Apache и Nginx, нямам причина да се съмнявам в него. Laravel е наистина справедлив че бавно! PHP сам по себе си не е толкова лош, но след като добавите цялата допълнителна обработка, която Laravel добавя към всяка заявка, тогава ми е много трудно да препоръчам Laravel като избор през 2023 г.
PHP/Wordpress сметки за около 40% от всички уебсайтове в мрежата , което го прави най-доминиращата рамка. Лично аз обаче откривам, че популярността не се превръща непременно в качество, както и че изпитвам внезапно неконтролируемо желание за тази необикновена гурме храна от най-популярният ресторант в света ... McDonald's. Тъй като вече сме тествали чист PHP код, няма да тестваме самия Wordpress, тъй като всичко, включващо Wordpress, несъмнено би било по-ниско от 700 заявки в секунда, които наблюдавахме с чист PHP.
Django е друга популярна рамка, която съществува от дълго време. Ако сте го използвали в миналото, вероятно с умиление си спомняте грандиозния му интерфейс за администриране на бази данни, както и колко досадно беше да конфигурирате всичко точно както искате. Нека да видим колко добре работи Django през 2023 г., особено с новия интерфейс ASGI, който е добавен от версия 4.0.
Настройката на Django е удивително подобна на настройката на Laravel, тъй като и двете бяха от времето, когато MVC архитектурите бяха стилни и правилни. Ще пропуснем скучната конфигурация и ще преминем направо към настройката на изгледа.
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}")
Четири реда код са същите като при версията на Laravel. Нека да видим как се представя.
╰─➤ 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
Изобщо не е лошо с 355 заявки в секунда. Това е само половината от производителността на чистата версия на PHP, но също така е 12 пъти по-висока от тази на версията на Laravel. Django срещу Laravel изглежда изобщо не се състезава.
Освен по-големите рамки за всичко-включително-кухненската мивка, има и по-малки рамки, които правят само някои основни настройки, като ви позволяват да се справите с останалото. Един от най-добрите за използване е Flask и неговият аналог ASGI Quart. Моята собствена PaferaPy Framework е изграден върху Flask, така че съм добре запознат с това колко лесно е да се вършат нещата, като се поддържа производителност.
#!/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}'
Както можете да видите, скриптът на Flask е по-кратък от чистия PHP скрипт. Откривам, че от всички езици, които съм използвал, Python е може би най-изразителният език по отношение на въведените натискания на клавиши. Липсата на фигурни скоби и скоби, разбиране на списъци и диктовки и блокиране въз основа на отстъп, а не на точка и запетая, правят Python доста прост, но мощен в своите възможности.
За съжаление Python е и най-бавният език с общо предназначение, въпреки колко софтуер е написан на него. Броят на наличните библиотеки на Python е около четири пъти повече от подобни езици и обхваща огромно количество домейни, но никой не би казал, че Python е бърз или ефективен извън ниши като NumPy.
Нека да видим как нашата версия на Flask се сравнява с предишните ни рамки.
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
Нашият Flask скрипт всъщност е по-бърз от нашата чиста PHP версия!
Ако сте изненадани от това, трябва да разберете, че нашето приложение Flask извършва цялата си инициализация и конфигурация, когато стартираме сървъра gunicorn, докато PHP изпълнява повторно скрипта всеки път, когато влезе нова заявка. То' ;е еквивалентно на това, че Flask е младият, нетърпелив шофьор на такси, който вече е запалил колата и чака до пътя, докато PHP е старият шофьор, който остава в къщата си, чака обаждане и едва тогава шофира за да те взема. Като човек от старата школа и идващ от дните, когато PHP беше прекрасна промяна към обикновените HTML и SHTML файлове, е малко тъжно да осъзнаем колко време е минало, но разликите в дизайна наистина затрудняват PHP да конкурирайте се със сървъри на Python, Java и Node.js, които просто остават в паметта и обработват заявки с пъргавата лекота на жонгльор.
Flask може да е най-бързата ни рамка досега, но всъщност е доста стар софтуер. Общността на Python премина към по-новите асинхронни ASGI сървъри преди няколко години и, разбира се, аз самият преминах заедно с тях.
Най-новата версия на Pafera Framework, PaferaPyAsync , е базиран на Starlette. Въпреки че има ASGI версия на Flask, наречена Quart, разликите в производителността между Quart и Starlette бяха достатъчни, за да пребазирам кода си на Starlette вместо това.
Асинхронното програмиране може да бъде плашещо за много хора, но всъщност не е трудна концепция, благодарение на момчетата от Node.js, които популяризираха концепцията преди повече от десетилетие.
Преди се борехме с паралелността с многонишковост, многопроцесорност, разпределени изчисления, обещаваща верига и всички онези забавни времена, които преждевременно състариха и изсушиха много ветерани програмисти. Сега просто пишем async
пред нашите функции и await
пред всеки код, чието изпълнение може да отнеме известно време. Той наистина е по-подробен от обикновения код, но много по-малко досаден за използване, отколкото да се налага да се справяте с примитиви за синхронизиране, предаване на съобщения и разрешаване на обещания.
Нашият файл Starlette изглежда така:
#!/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",
)
Както можете да видите, той е почти копиран и поставен от нашия Flask скрипт само с няколко промени в маршрутизирането и async/await
ключови думи.
Колко подобрения наистина може да ни даде копираният и поставен код?
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
Имаме нов шампион, дами и господа! Предишният ни връх беше нашата чиста PHP версия със 704 заявки в секунда, която след това беше изпреварена от нашата версия на Flask с 1080 заявки в секунда. Нашият скрипт Starlette смазва всички предишни конкуренти с 4562 заявки в секунда, което означава 6x подобрение спрямо чист PHP и 4x подобрение спрямо Flask.
Ако все още не сте променили кода си на WSGI Python към ASGI, сега може да е добър момент да започнете.
Досега сме обхванали само рамки на PHP и Python. Въпреки това, голяма част от света всъщност използва Java, DotNet, Node.js, Ruby on Rails и други подобни технологии за своите уебсайтове. Това в никакъв случай не е изчерпателен преглед на всички световни екосистеми и биоми, така че, за да избегнем извършването на програмен еквивалент на органичната химия, ние ще изберем само рамките, които са най-лесни за въвеждане на код. ., от които Java определено не е.
Освен ако не сте се крили под вашето копие на K&R C или Knuth' Изкуството на компютърното програмиране през последните петнадесет години вероятно сте чували за Node.js. Тези от нас, които са наоколо от началото на JavaScript, са или невероятно уплашени, или изумени, или и двете от състоянието на съвременния JavaScript, но не може да се отрече, че JavaScript се е превърнал в сила, с която трябва да се съобразяват и на сървърите като браузъри. В края на краищата дори имаме собствени 64-битови цели числа в езика! Това е много по-добре, отколкото всичко да се съхранява в 64-битови плаващи файлове!
ExpressJS е може би най-лесният за използване сървър на Node.js, така че ще направим бързо и мръсно приложение Node.js/ExpressJS, което да обслужва нашия брояч.
/**********************************************************************
* 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}`));
Този код всъщност беше по-лесен за писане от версиите на Python, въпреки че естественият JavaScript става доста тромав, когато приложенията станат по-големи, и всички опити да се коригира това, като TypeScript, бързо стават по-подробни от Python.
Нека да видим как се представя това!
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
Може да сте чували древни (все пак древни по стандартите на Интернет...) народни приказки за Node.js' скорост и тези истории са най-вече верни благодарение на грандиозната работа, която Google свърши с двигателя V8 JavaScript. В този случай обаче, въпреки че нашето бързо приложение превъзхожда скрипта на Flask, неговата еднонишкова природа е победена от четирите асинхронни процеса, управлявани от Starlette Knight, който казва "Ni!".
Нека получим още помощ!
╰─➤ 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 │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
добре! Сега това е равностойна битка четири на четири! Нека направим бенчмарк!
╰─➤ 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
Все още не е съвсем на нивото на Starlette, но не е лошо за бърз петминутен JavaScript хак. От моето собствено тестване, този скрипт всъщност е задържан малко на ниво интерфейс на базата данни, защото node-postgres не е толкова ефективен, колкото е psycopg за Python. Преминаването към sqlite като драйвер на базата данни дава над 3000 заявки в секунда за един и същ ExpressJS код.
Основното нещо, което трябва да се отбележи, е, че въпреки бавната скорост на изпълнение на Python, ASGI рамките всъщност могат да бъдат конкурентни на Node.js решенията за определени натоварвания.
Така че сега се доближаваме до върха на планината, а под планина имам предвид най-високите резултати от бенчмарк, записани както от мишки, така и от хора.
Ако погледнете повечето от бенчмарковете на рамката, налични в мрежата, ще забележите, че има два езика, които са склонни да доминират на върха: C++ и Rust. Работя с C++ от 90-те години на миналия век и дори имах своя собствена Win32 C++ рамка преди MFC/ATL да стане нещо, така че имам много опит с езика. Не е много забавно да работите с нещо, когато вече го знаете, така че вместо това ще направим версия на Rust. ;)
Rust е сравнително нов, що се отнася до езиците за програмиране, но стана обект на любопитство за мен, когато Линус Торвалдс обяви, че ще приеме Rust като език за програмиране на ядрото на Linux. За нас, по-възрастните програмисти, това е почти същото като да кажем, че това ново модно хипи нещо от новата епоха ще бъде нова поправка в конституцията на САЩ.
Сега, когато сте опитен програмист, нямате склонност да скачате толкова бързо, колкото по-младите хора, или в противен случай може да се изгорите от бързи промени в езика или библиотеките. (Всеки, който е използвал първата версия на AngularJS, ще разбере за какво говоря.) Rust все още е донякъде в този експериментален етап на разработка и намирам за смешно, че толкова много примери за код в мрежата дори не са компилирайте вече с текущи версии на пакети.
Въпреки това, производителността, показана от Rust приложенията, не може да бъде отречена. Ако никога не сте опитвали ripgrep или fd-намери на големи дървета на изходния код, определено трябва да ги завъртите. Те дори са достъпни за повечето дистрибуции на Linux просто от мениджъра на пакети. Разменяте многословие за производителност с Rust... a много на многословие за a много на изпълнение.
Пълният код за Rust е малко голям, така че просто ще разгледаме съответните манипулатори тук:
// =====================================================================
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
)))
}
Това е много по-сложно от версиите на 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
И много по-ефективен!
Нашият Rust сървър, използващ Actix/deadpool_postgres, победи с лекота предишния ни шампион Starlette с +125%, ExpressJS с +362% и чистия PHP с +1366%. (Ще оставя делтата на производителността с версията на Laravel като упражнение за читателя.)
Открих, че изучаването на самия език Rust е по-трудно от другите езици, тъй като има много повече проблеми от всичко, което съм виждал извън 6502 Assembly, но ако вашият Rust сървър може да поеме 14 пъти повече от потребители като вашия PHP сървър, тогава може би все пак има какво да се спечели с технологиите за превключване. Ето защо следващата версия на Pafera Framework ще бъде базирана на Rust. Кривата на обучение е много по-висока от скриптовите езици, но производителността ще си заслужава. Ако не можете да отделите време, за да научите Rust, тогава базирането на вашия технически стек на Starlette или Node.js също не е лошо решение.
През последните двадесет години преминахме от евтини статични хостинг сайтове към споделен хостинг с LAMP стекове до отдаване под наем на VPS на AWS, Azure и други облачни услуги. В днешно време много компании се задоволяват с вземането на дизайнерски решения въз основа на това, когото могат да намерят, което е налично или най-евтино, тъй като появата на удобни облачни услуги улесни хвърлянето на повече хардуер в бавните сървъри и приложения. Това им даде големи краткосрочни печалби на цената на дългосрочен технически дълг.
Преди 70 години имаше голяма космическа надпревара между Съветския съюз и Съединените щати. Съветите спечелиха повечето от ранните етапи. Те имаха първия сателит в Спутник, първото куче в космоса в Лайка, първия лунен космически кораб в Луна 2, първите мъж и жена в космоса в Юрий Гагарин и Валентина Терешкова и така нататък...
Но те бавно натрупваха технически дългове.
Въпреки че Съветите бяха първи във всяко от тези постижения, техните инженерни процеси и цели ги караха да се съсредоточат върху краткосрочни предизвикателства, а не върху дългосрочна осъществимост. Те печелеха всеки път, когато скачаха, но ставаха все по-уморени и по-бавни, докато опонентите им продължаваха да правят последователни крачки към финалната линия.
След като Нийл Армстронг направи историческите си стъпки на Луната по телевизията на живо, американците поеха водещата роля и след това останаха там, докато съветската програма се поколеба. Това не е по-различно от днешните компании, които са се фокусирали върху следващото голямо нещо, следващата голяма печалба или следващата голяма технология, като същевременно не са успели да развият правилни навици и стратегии за дългосрочен план.
Да сте първи на пазара не означава, че ще станете доминиращият играч на този пазар. Като алтернатива, отделянето на време, за да направите нещата както трябва, не гарантира успех, но със сигурност увеличава шансовете ви за дългосрочни постижения. Ако вие сте технологичният лидер за вашата компания, изберете правилната посока и инструменти за вашето работно натоварване. Не позволявайте на популярността да замени производителността и ефективността.
Искате ли да изтеглите 7z файл, съдържащ PHP скриптовете Rust, ExpressJS, Flask, Starlette и Pure?
За автора |
|
![]() |
Джим програмира, откакто получи IBM PS/2 през 90-те години. И до днес той все още предпочита да пише HTML и SQL на ръка и се фокусира върху ефективността и коректността в работата си. |