Так, Вірджинія, *Є* а Дід Мороз Різниця між веб-фреймворками у 2023 році

Подорож одного зухвалого програміста, щоб знайти швидкий код веб-сервера, перш ніж він піддасться тиску ринку та технічним боргам
2023-03-24 11:52:06
👁️ 779
💬 0

Зміст

  1. вступ
  2. Тест
  3. PHP/Laravel
  4. Чистий PHP
  5. Перегляд Laravel
  6. Джанго
  7. Колба
  8. Зірочка
  9. Node.js/ExpressJS
  10. Іржа/Actix
  11. Технічний борг
  12. Ресурси

вступ

Після однієї з останніх співбесід я з подивом усвідомив, що компанія, в яку я подав заявку, все ще використовує 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

Поставлене завдання

Наш код матиме три прості завдання для кожного запиту:

  1. Прочитати ідентифікатор поточного сеансу користувача з файлу cookie
  2. Завантажити додаткову інформацію з бази даних
  3. Поверніть цю інформацію користувачеві

Ви можете запитати, що це за ідіотський тест? Що ж, якщо ви подивіться на мережеві запити для цієї сторінки, ви помітите запит під назвою sessionvars.js, який виконує те саме.

Вміст 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 простий, але ефективний... поза межами найтемнішої шкали часу...

Фактичні результати тесту

PHP/Laravel

Гаразд, тепер ми можемо нарешті почати бруднити руки. Ми пропустимо налаштування для 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.

<?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?

Перегляд 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&#x27;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 повторно виконує сценарій щоразу, коли надходить новий запит. Це&#x27 ;еквівалентно тому, що 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, можливо, саме час почати.

Node.js/ExpressJS

Поки що ми розглядали лише фреймворки PHP і Python. Однак значна частина світу фактично використовує Java, DotNet, Node.js, Ruby on Rails та інші подібні технології для своїх веб-сайтів. Це аж ніяк не повний огляд усіх світових екосистем і біомів, тому, щоб уникнути програмування, еквівалента органічної хімії, ми виберемо лише ті структури, для яких найпростіше вводити код. .з яких Java точно не є.

Якщо ви не ховалися під своєю копією K&R C або Knuth&#x27;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&#x27; швидкість, і ці історії здебільшого правдиві завдяки вражаючій роботі, яку 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 для певних робочих навантажень.

Іржа/Actix

Тож тепер ми наближаємось до вершини гори, а під горою я маю на увазі найвищі показники, зафіксовані як мишами, так і людьми.

Якщо ви подивитесь на більшість доступних в Інтернеті тестів фреймворків, ви помітите, що є дві мови, які мають тенденцію домінувати у верхній частині: 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 вручну, і зосереджується на ефективності і коректності в своїй роботі.