Після однієї з останніх співбесід я з подивом усвідомив, що компанія, в яку я подав заявку, все ще використовує 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 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 на 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 побудовано на основі 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, який каже «Ні!».
Отримаймо ще допомогу!
╰─➤ 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 вручну, і зосереджується на ефективності і коректності в своїй роботі. |