Пасля аднаго з маіх апошніх гутарак я са здзіўленнем зразумеў, што кампанія, у якую я падаў заяўку, усё яшчэ выкарыстоўвае Laravel, фрэймворк PHP, які я апрабаваў каля дзесяці гадоў таму. Для таго часу гэта было прыстойна, але калі і ёсць адна канстанта ў тэхналогіях і модзе, дык гэта пастаяннае змяненне і пераўтварэнне стыляў і канцэпцый. Калі вы праграміст на JavaScript, вы, напэўна, знаёмыя з гэтым старым жартам
Праграміст 1: "Мне не падабаецца гэты новы фрэймворк JavaScript!"
Праграміст 2: "Не трэба хвалявацца. Проста пачакайце шэсць месяцаў, і будзе іншы, каб замяніць яго!"
З цікаўнасці я вырашыў паглядзець, што менавіта адбываецца, калі мы выпрабоўваем старое і новае. Вядома, Інтэрнэт напоўнены тэстамі і прэтэнзіямі, з якіх, напэўна, самым папулярным з'яўляецца Тэсты TechEmpower Web Framework тут . Аднак сёння мы не збіраемся рабіць нічога такога складанага, як яны. Мы будзем рабіць усё прыгожа і проста, каб гэты артыкул не ператварыўся ў Вайна і мір , і што ў вас будзе невялікая верагоднасць не спаць да таго часу, калі вы скончыце чытаць. Прымяняюцца звычайныя засцярогі: гэта можа не працаваць аднолькава на вашай машыне, розныя версіі праграмнага забеспячэння могуць уплываць на прадукцыйнасць, і кот Шродзінгера фактычна стаў котам-зомбі, які быў напалову жывы і напалову мёртвы адначасова.
Для гэтага тэсту я буду выкарыстоўваць свой ноўтбук, узброены мізэрным 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. Гэта не толькі зніжае нагрузку на базу дадзеных і стварае лепшы вопыт для нашых карыстальнікаў, але таксама памяншае квантавую верагоднасць таго, што наш сервер самаадвольна выпарыцца ў выніку ўзлому варп-ядра пры атацы клінгонаў.
Вернуты код для кожнай структуры будзе мець адно простае патрабаванне: паказаць карыстальніку, колькі разоў яны абнаўлялі старонку, сказаўшы "Ліч роўна х". Каб зрабіць усё прасцей, пакуль мы будзем трымацца далей ад чэргаў Redis, кампанентаў Kubernetes або AWS Lambda.
Даныя сесіі кожнага карыстальніка будуць захоўвацца ў базе даных 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. У яго выніках тэстаў, ён мае Люсінда перамагае Laravel у 36 разоў для запытаў HTML і ў 90 разоў для запытаў 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 пабудаваны на аснове 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 запыты ў секунду, што азначае 6-кратнае паляпшэнне ў параўнанні з чыстым PHP і 4-кратнае паляпшэнне ў параўнанні з Flask.
Калі вы яшчэ не змянілі свой код WSGI Python на ASGI, цяпер можа быць добры час, каб пачаць.
Дагэтуль мы разглядалі толькі фрэймворкі PHP і Python. Тым не менш, значная частка свету фактычна выкарыстоўвае Java, DotNet, Node.js, Ruby on Rails і іншыя падобныя тэхналогіі для сваіх вэб-сайтаў. Гэта ні ў якім разе не ўсёабдымны агляд усіх сусветных экасістэм і біёмаў, таму, каб пазбегнуць праграмнага эквіваленту арганічнай хіміі, мы абярэм толькі тыя структуры, для якіх прасцей за ўсё ўводзіць код. ., якім Java дакладна не з'яўляецца.
Калі толькі вы не хаваліся пад сваёй копіяй K&R C або Knuth's Мастацтва праграмавання за апошнія пятнаццаць гадоў вы напэўна чулі пра 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 шмат шматслоўнасці для а шмат прадукцыйнасці.
Поўны код для 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, але калі ваш сервер Rust можа прыняць у 14 разоў большую колькасць карыстальнікаў у якасці вашага PHP-сервера, то, магчыма, у рэшце рэшт, можна нешта атрымаць з пераключэннем тэхналогій. Вось чаму наступная версія Pafera Framework будзе заснавана на Rust. Крывая навучання нашмат вышэй, чым мовы сцэнарыяў, але прадукцыйнасць таго вартая. Калі вы не можаце выдаткаваць час на вывучэнне Rust, то заснаваць свой тэхнічны стэк на Starlette або Node.js таксама не будзе дрэнным рашэннем.
За апошнія дваццаць гадоў мы перайшлі ад танных сайтаў статычнага хостынгу да агульнага хостынгу са стэкамі LAMP і да арэнды VPS для AWS, Azure і іншых воблачных сэрвісаў. У наш час многія кампаніі задавальняюцца прыняццем дызайнерскіх рашэнняў у залежнасці ад таго, каго яны знойдуць даступным або самым танным, паколькі з'яўленне зручных воблачных сэрвісаў дазволіла лёгка выкарыстоўваць больш апаратнага забеспячэння для павольных сервераў і прыкладанняў. Гэта дало ім вялікую кароткатэрміновую выгаду за кошт доўгатэрміновай тэхнічнай запазычанасці.
70 гадоў таму адбылася вялікая касмічная гонка паміж Савецкім Саюзам і ЗША. Саветы выйгралі большасць першых этапаў. У іх быў першы спадарожнік у Sputnik, першы сабака ў космасе ў Лайка, першы касмічны карабель на месяц у Луна-2, першыя мужчына і жанчына ў космасе Юрый Гагарын і Валянціна Церашкова, і гэтак далей ...
Але яны паціху назапашвалі тэхнічную запазычанасць.
Нягледзячы на тое, што Саветы былі першымі ў кожным з гэтых дасягненняў, іх інжынерныя працэсы і мэты прымушалі іх засяроджвацца на кароткатэрміновых задачах, а не на доўгатэрміновых мэтазгоднасцях. Яны перамагалі кожны раз, калі скакалі, але яны станавіліся ўсё больш стомленымі і павольнымі, у той час як іх супернікі працягвалі рабіць паслядоўныя крокі да фінішу.
Пасля таго, як Ніл Армстранг зрабіў свае гістарычныя крокі па Месяцы ў прамым эфіры, амерыканцы захапілі ініцыятыву, а потым засталіся там, бо савецкая праграма хісталася. Гэта нічым не адрозніваецца ад сучасных кампаній, якія засяродзіліся на наступнай вялікай справе, наступнай буйной выгадзе або наступнай буйной тэхніцы, але не змаглі выпрацаваць правільныя звычкі і стратэгіі на доўгатэрміновую перспектыву.
Быць першым на рынку не азначае, што вы станеце дамінуючым гульцом на гэтым рынку. У якасці альтэрнатывы, знаходзячы час, каб зрабіць усё правільна, не гарантуе поспеху, але, безумоўна, павялічвае вашы шанцы на доўгатэрміновыя дасягненні. Калі вы тэхнічны кіраўнік сваёй кампаніі, абярыце правільны кірунак і інструменты для сваёй працоўнай нагрузкі. Не дазваляйце папулярнасці замяніць прадукцыйнасць і эфектыўнасць.
Хочаце загрузіць файл 7z, які змяшчае скрыпты Rust, ExpressJS, Flask, Starlette і Pure PHP?
Пра аўтара |
|
![]() |
Джым займаецца праграмаваннем з таго часу, як атрымаў IBM PS/2 яшчэ ў 90-я гады. Па гэты дзень ён па-ранейшаму аддае перавагу напісанню HTML і SQL ад рукі, а ў сваёй працы робіць стаўку на эфектыўнасць і карэктнасць. |