После одного из последних собеседований я с удивлением обнаружил, что компания, в которую я подавал заявку, все еще использует 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. Это не только снижает нагрузку на базу данных и создает лучший опыт для наших пользователей, но и снижает квантовые вероятности того, что наш сервер спонтанно испарится при прорыве варп-ядра, когда атакуют клингоны.
Возвращаемый код для каждого фреймворка будет иметь одно простое требование: показать пользователю, сколько раз он обновил страницу, сказав «Count is x». Чтобы не усложнять, мы пока воздержимся от очередей Redis, компонентов Kubernetes или AWS Lambdas.
Данные сеанса каждого пользователя будут сохранены в базе данных PostgreSQL.
И эта таблица базы данных будет усекаться перед каждым тестом.
Простой, но эффективный девиз Pafera... во всяком случае, за пределами самой темной временной линии...
Хорошо, теперь мы наконец можем начать пачкать руки. Мы пропустим настройку Laravel, так как это просто куча команд composer и artisan.
Сначала мы настроим параметры нашей базы данных в файле .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. В результатах своего теста он Люсинда побеждает Ларавеля в 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.
Если вы еще не изменили свой код Python WSGI на ASGI, сейчас самое время начать.
До сих пор мы рассматривали только фреймворки PHP и Python. Однако большая часть мира на самом деле использует Java, DotNet, Node.js, Ruby on Rails и другие подобные технологии для своих веб-сайтов. Это ни в коем случае не всеобъемлющий обзор всех мировых экосистем и биомов, поэтому, чтобы избежать программирования, эквивалентного органической химии, мы выберем только фреймворки, для которых проще всего набирать код... к которым Java определенно не относится.
Если только вы не прятались под своей копией K&R C или Кнута Искусство программирования за последние пятнадцать лет вы, вероятно, слышали о 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 проделала с движком JavaScript V8. Однако в этом случае, хотя наше быстрое приложение превосходит скрипт Flask, его однопоточная природа терпит поражение из-за четырех асинхронных процессов, которыми владеет рыцарь-звездочка, говорящий «Ни!».
Давайте позовем еще немного помощи!
╰─➤ 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, отрицать нельзя. Если вы никогда не пробовали рипгреп или fd-найти на больших исходных кодах, вам определенно стоит их попробовать. Они даже доступны для большинства дистрибутивов Linux просто из менеджера пакетов. Вы обмениваете многословие на производительность с Rust... много многословия для много производительности.
Полный код для 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, содержащий скрипты Rust, ExpressJS, Flask, Starlette и Pure PHP?
об авторе |
|
![]() |
Джим программирует с тех пор, как в 90-х у него появился IBM PS/2. По сей день он предпочитает писать HTML и SQL вручную и фокусируется на эффективности и корректности в своей работе. |