Ano, Virginie, tam *Je* a Santa Claus Rozdíl mezi webovými rámci v roce 2023

Cesta jednoho vzdorného programátora k nalezení rychle výkonného kódu webového serveru, než podlehne tlaku trhu a technickému dluhu
2023-03-24 11:52:06
👁️ 764
💬 0

Obsah

  1. Zavedení
  2. Test
  3. PHP/Laravel
  4. Čisté PHP
  5. Opětovná návštěva Laravel
  6. Django
  7. Baňka
  8. Starlette
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. Technický dluh
  12. Zdroje

Zavedení

Po jednom z mých posledních pracovních pohovorů jsem si s překvapením uvědomil, že společnost, o kterou jsem se ucházel, stále používá Laravel, PHP framework, který jsem vyzkoušel asi před deseti lety. Na tehdejší dobu to bylo slušné, ale pokud existuje jedna konstanta v technologii a módě, je to neustálá změna a přestavování stylů a konceptů. Pokud jste programátor JavaScriptu, pravděpodobně tento starý vtip znáte

Programátor 1: "Nelíbí se mi tento nový rámec JavaScriptu!"

Programátor 2: „Nemusíte si dělat starosti. Stačí počkat šest měsíců a nahradí ho jiný!"

Ze zvědavosti jsem se rozhodl přesně vidět, co se stane, když testujeme staré a nové. Web je samozřejmě plný benchmarků a nároků, z nichž asi nejoblíbenější je TechEmpower Web Framework Benchmarks zde . Dnes však nebudeme dělat nic tak složitého jako oni. Uděláme věci hezké a jednoduché, aby se tento článek nezměnil v Válka a mír , a že budete mít malou šanci zůstat vzhůru, než budete číst. Platí obvyklá upozornění: na vašem počítači to nemusí fungovat stejně, různé verze softwaru mohou ovlivnit výkon a Schrödingerova kočka se ve skutečnosti stala kočkou zombie, která byla napůl živá a napůl mrtvá ve stejnou dobu.

Test

Testovací prostředí

Pro tento test budu používat svůj notebook vyzbrojený maličkým i5 se systémem Manjaro Linux, jak je znázorněno zde.

╰─➤  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

Úkol na dosah ruky

Náš kód bude mít pro každý požadavek tři jednoduché úkoly:

  1. Přečtěte si aktuální ID relace uživatele ze souboru cookie
  2. Načtěte další informace z databáze
  3. Vraťte tyto informace uživateli

Ptáte se, co je to za idiotský test? Když se podíváte na síťové požadavky pro tuto stránku, všimnete si jedné s názvem sessionvars.js, která dělá přesně to samé.

Obsah sessionvars.js

Víte, moderní webové stránky jsou komplikovaná stvoření a jedním z nejběžnějších úkolů je ukládání složitých stránek do mezipaměti, aby se zabránilo nadměrnému zatížení databázového serveru.

Pokud znovu vykreslíme složitou stránku pokaždé, když si to uživatel vyžádá, můžeme obsloužit pouze asi 600 uživatelů za sekundu.

╰─➤  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

Ale pokud tuto stránku uložíme do mezipaměti jako statický soubor HTML a necháme Nginx, aby ji rychle vyhodil z okna uživateli, pak můžeme obsloužit 32 000 uživatelů za sekundu, což zvýší výkon faktorem 50x.

╰─➤  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

Statický index.en.html je část, která je určena všem, a v sessionvars.js se odesílají pouze části, které se liší podle uživatele. To nejen snižuje zatížení databáze a vytváří lepší zážitek pro naše uživatele, ale také snižuje kvantové pravděpodobnosti, že se náš server spontánně vypaří při porušení warp jádra, když Klingoni zaútočí.

Požadavky na kód

Vrácený kód pro každý rámec bude mít jeden jednoduchý požadavek: ukažte uživateli, kolikrát obnovili stránku tím, že řeknete „Počet je x“. Abychom to zjednodušili, prozatím se budeme držet dál od front Redis, komponent Kubernetes nebo AWS Lambdas.

Zobrazuje, kolikrát jste stránku navštívili

Údaje o relaci každého uživatele budou uloženy v databázi PostgreSQL.

Tabulka uživatelských relací

A tato databázová tabulka bude před každým testem zkrácena.

Tabulka po oříznutí

Jednoduché, ale účinné je motto Pafery... každopádně mimo nejtemnější časovou osu...

Aktuální výsledky testu

PHP/Laravel

Dobře, takže teď si konečně můžeme začít špinit ruce. Přeskočíme nastavení pro Laravel, protože je to jen skupina skladatelů a řemeslníků příkazy.

Nejprve nastavíme nastavení databáze v souboru .env

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=sessiontest
DB_USERNAME=sessiontest
DB_PASSWORD=sessiontest

Potom nastavíme jednu jedinou záložní trasu, která odešle každý požadavek našemu kontroléru.

Route::fallback(SessionController::class);

A nastavte ovladač tak, aby zobrazoval počet. Laravel ve výchozím nastavení ukládá relace do databáze. Poskytuje také session() funkce pro rozhraní s našimi daty relace, takže k vykreslení naší stránky stačilo jen pár řádků kódu.

class SessionController extends Controller
{
  public function __invoke(Request $request)
  {
    $count  = session('count', 0);

    $count  += 1;

    session(['count' => $count]);

    return 'Count is ' . $count;
  }
}

Po nastavení php-fpm a Nginx naše stránka vypadá docela dobře...

╰─➤  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

Alespoň do té doby, než skutečně uvidíme výsledky testů...

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

Ne, to není překlep. Náš testovací stroj přešel z 600 požadavků za sekundu při vykreslování složité stránky... na 21 požadavků za sekundu při vykreslování "Počet je 1".

Co se tedy pokazilo? Je něco špatně s naší instalací PHP? Zpomaluje se nějak Nginx při propojení s php-fpm?

Čisté PHP

Předělejme tuto stránku v čistém PHP kódu.

<?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'];

Nyní jsme použili 98 řádků kódu k tomu, co čtyři řádky kódu (a spoustu konfiguračních prací) v Laravelu. (Samozřejmě, pokud bychom správně ošetřili chyby a zobrazili zprávy pro uživatele, byl by to asi dvojnásobek počtu řádků.) Možná bychom mohli dosáhnout 30 požadavků za sekundu?

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

Páni! Zdá se, že na naší instalaci PHP nakonec není nic špatného. Čistá verze PHP dělá 700 požadavků za sekundu.

Pokud s PHP není nic špatného, ​​možná jsme špatně nakonfigurovali Laravel?

Opětovná návštěva Laravel

Poté, co jsme prohledali web kvůli problémům s konfigurací a tipům na výkon, dvě z nejoblíbenějších technik byly ukládání konfiguračních a směrovacích dat do mezipaměti, aby se zabránilo jejich zpracování pro každý požadavek. Dáme proto na jejich rady a tyto tipy vyzkoušíme.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Na příkazovém řádku vše vypadá dobře. Pojďme znovu provést benchmark.

╰─➤  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

Nyní jsme zvýšili výkon z 21,04 na 28,80 požadavku za sekundu, což je dramatický nárůst o téměř 37 %! To by bylo docela působivé pro jakýkoli softwarový balík... kromě skutečnosti, že stále provádíme pouze 1/24 z počtu požadavků čisté verze PHP.

Pokud si myslíte, že s tímto testem musí být něco špatně, měli byste si promluvit s autorem PHP frameworku Lucinda. Ve výsledcích testů má Lucinda porazila Laravel 36x pro požadavky HTML a 90x pro požadavky JSON.

Po testování na vlastním stroji s Apache i Nginx nemám důvod o něm pochybovat. Laravel je opravdu spravedlivá že pomalý! PHP samo o sobě není tak špatné, ale jakmile přidáte veškeré další zpracování, které Laravel přidává ke každému požadavku, je pro mě velmi obtížné doporučit Laravel jako volbu v roce 2023.

Django

PHP/Wordpress účty pro asi 40 % všech webových stránek na webu , což z něj dělá zdaleka nejdominantnější rámec. Osobně však zjišťuji, že popularita se nutně nepromítá do kvality o nic víc, než když zjišťuji, že mám náhlou neovladatelnou touhu po tomto mimořádném gurmánském jídle od nejoblíbenější restaurace na světě ... McDonald&#x27;s. Vzhledem k tomu, že jsme již testovali čistý PHP kód, nebudeme testovat samotný Wordpress, protože cokoli zahrnující Wordpress by bylo nepochybně nižší než 700 požadavků za sekundu, které jsme pozorovali u čistého PHP.

Django je další populární framework, který existuje již dlouhou dobu. Pokud jste jej v minulosti používali, pravděpodobně si rádi vzpomínáte na jeho velkolepé rozhraní pro správu databází spolu s tím, jak otravné bylo konfigurovat vše tak, jak jste chtěli. Podívejme se, jak dobře Django funguje v roce 2023, zejména s novým rozhraním ASGI, které přidalo od verze 4.0.

Nastavení Djanga je pozoruhodně podobné nastavení Laravelu, protože oba pocházeli z doby, kdy byly architektury MVC stylové a správné. Přeskočíme nudnou konfiguraci a přejdeme rovnou k nastavení pohledu.

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}")

Čtyři řádky kódu jsou stejné jako u verze Laravel. Podívejme se, jak to funguje.

╰─➤  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

Vůbec to není špatné s 355 požadavky za sekundu. Je pouze poloviční oproti čisté verzi PHP, ale je také 12x vyšší než u verze Laravel. Zdá se, že Django vs. Laravel není vůbec žádná soutěž.

Baňka

Kromě větších rámců všeho druhu, včetně kuchyňských dřezů, existují také menší rámce, které provedou pouze základní nastavení a zbytek vám umožní zvládnout. Jedním z nejlépe použitelných je Flask a jeho ASGI protějšek Quart. Moje vlastní Rámec PaferaPy je postaven na flask, takze jsem dobre obeznámen s tím, jak snadné je dělat věci při zachování výkonu.

#!/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}'

Jak vidíte, skript Flask je kratší než čistý PHP skript. Zjistil jsem, že ze všech jazyků, které jsem použil, je Python pravděpodobně nejvýraznějším jazykem, pokud jde o psaní na klávesnici. Nedostatek složených závorek a závorek, porozumění seznamům a diktátům a blokování založené na odsazení spíše než na středníku činí Python poměrně jednoduchým, ale výkonným ve svých schopnostech.

Naneštěstí je Python také nejpomalejším jazykem pro obecné použití, a to navzdory tomu, kolik softwaru v něm bylo napsáno. Počet dostupných knihoven Pythonu je asi čtyřikrát větší než u podobných jazyků a pokrývá obrovské množství domén, ale nikdo by neřekl, že Python je rychlý a výkonný mimo výklenky, jako je NumPy.

Podívejme se, jak si naše verze Flask stojí v porovnání s našimi předchozími frameworky.

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

Náš skript Flask je ve skutečnosti rychlejší než naše čistá verze PHP!

Pokud vás to překvapuje, měli byste si uvědomit, že naše aplikace Flask provádí veškerou inicializaci a konfiguraci, když spustíme server gunicorn, zatímco PHP znovu spustí skript pokaždé, když přijde nový požadavek. ; je ekvivalentní tomu, že Flask je mladý, dychtivý taxikář, který už nastartoval auto a čeká u silnice, zatímco PHP je starý řidič, který zůstává ve svém domě a čeká na zavolání a teprve potom řídí abych tě vyzvedl. Být člověkem ze staré školy a pocházet z dob, kdy PHP bylo skvělou změnou na obyčejné HTML a SHTML soubory, je trochu smutné si uvědomit, kolik času uplynulo, ale rozdíly v designu opravdu ztěžují PHP. soutěžit se servery Python, Java a Node.js, které zůstávají v paměti a zpracovávají požadavky s hbitou lehkostí žongléra.

Starlette

Flask může být zatím naším nejrychlejším frameworkem, ale ve skutečnosti je to docela starý software. Komunita Pythonu přešla před pár lety na novější asychronní servery ASGI a já sám jsem s nimi samozřejmě přešel.

Nejnovější verze Pafera Framework, PaferaPyAsync , je založen na Starlette. Ačkoli existuje ASGI verze Flask s názvem Quart, výkonnostní rozdíly mezi Quartem a Starlette byly dostatečné k tomu, abych svůj kód místo toho přestavěl na Starlette.

Asychronní programování může být pro mnoho lidí děsivé, ale ve skutečnosti to není obtížný koncept, protože kluci z Node.js tento koncept popularizovali před více než deseti lety.

Dříve jsme bojovali proti souběžnosti s multithreadingem, multiprocessingem, distribuovaným počítáním, řetězením slibů a všemi těmi zábavnými časy, které předčasně zestárly a vysušily mnoho zkušených programátorů. Teď jen píšeme async před našimi funkcemi a await před jakýmkoli kódem, jehož spuštění může chvíli trvat. Je to skutečně podrobnější než běžný kód, ale mnohem méně otravné, než když se musíte zabývat synchronizačními primitivy, předáváním zpráv a řešením slibů.

Náš soubor Starlette vypadá takto:

#!/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",
)

Jak můžete vidět, je do značné míry zkopírován a vložen z našeho skriptu Flask s pouze několika změnami směrování a async/await klíčová slova.

Kolik vylepšení nám může kopírování a vkládání kódu skutečně přinést?

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

Máme nového šampiona, dámy a pánové! Naším předchozím maximem byla naše čistá verze PHP s rychlostí 704 požadavků za sekundu, kterou pak překonala naše verze Flask s rychlostí 1080 požadavků za sekundu. Náš skript Starlette rozdrtí všechny předchozí uchazeče rychlostí 4562 požadavků za sekundu, což znamená 6x zlepšení oproti čistému PHP a 4x zlepšení oproti Flasku.

Pokud jste ještě nezměnili svůj kód WSGI Python na ASGI, možná je nyní ten správný čas začít.

Node.js/ExpressJS

Doposud jsme se zabývali pouze frameworky PHP a Python. Velká část světa však ve skutečnosti používá pro své webové stránky Java, DotNet, Node.js, Ruby on Rails a další podobné technologie. Toto v žádném případě není komplexní přehled všech světových ekosystémů a biomů, takže abychom se vyhnuli programování ekvivalentu organické chemie, vybereme pouze rámce, pro které je nejjednodušší zadat kód. z nichž Java rozhodně není.

Pokud jste se neskrývali pod svou kopií K&R C nebo Knuth&#x27;s Umění počítačového programování za posledních patnáct let jste pravděpodobně slyšeli o Node.js. Ti z nás, kteří existují od počátku JavaScriptu, jsou buď neuvěřitelně vyděšení, ohromeni, nebo obojím stavem moderního JavaScriptu, ale nelze popřít, že JavaScript se stal silou, se kterou je třeba počítat i na serverech. jako prohlížeče. Koneckonců, nyní máme v jazyce dokonce nativní 64bitová celá čísla! To je daleko lepší než všechno, co je uloženo v 64bitových plovácích!

ExpressJS je pravděpodobně nejsnáze použitelný server Node.js, takže vytvoříme rychlou a špinavou aplikaci Node.js/ExpressJS, která bude sloužit našemu počítadlu.

/**********************************************************************
 * 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}`));

Tento kód bylo ve skutečnosti snazší napsat než verze Pythonu, ačkoli nativní JavaScript se stává poněkud nepraktickým, když se aplikace zvětší, a všechny pokusy o nápravu, jako je TypeScript, se rychle stanou podrobnějšími než Python.

Podívejme se, jak to funguje!

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

Možná jste slyšeli starodávné (stejně tak staré podle internetových standardů...) lidové pohádky o Node.js&#x27; rychlost a tyto příběhy jsou většinou pravdivé díky velkolepé práci, kterou Google odvedl s V8 JavaScript enginem. V tomto případě, ačkoli naše rychlá aplikace překonává skript Flask, její jednovláknová povaha je poražena čtyřmi asynchronními procesy, které ovládá Starlette Knight, který říká „Ni!“.

Pojďme získat další pomoc!

╰─➤  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 │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

Dobře! Nyní je to vyrovnaná bitva čtyři na čtyři! Pojďme srovnávat!

╰─➤  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

Stále to není úplně na úrovni Starlette, ale na rychlý pětiminutový hack JavaScriptu to není špatné. Z mého vlastního testování je tento skript ve skutečnosti trochu zdržen na úrovni rozhraní databáze, protože node-postgres není zdaleka tak efektivní jako psycopg pro Python. Přepnutí na sqlite jako databázový ovladač přináší přes 3000 požadavků za sekundu pro stejný kód ExpressJS.

Hlavní věc, kterou je třeba poznamenat, je, že navzdory pomalé rychlosti provádění Pythonu mohou rámce ASGI skutečně konkurovat řešením Node.js pro určitá pracovní zatížení.

Rust/Actix

Nyní se tedy přibližujeme k vrcholu hory a horou mám na mysli nejvyšší srovnávací skóre zaznamenané myší i muži.

Pokud se podíváte na většinu benchmarků frameworků dostupných na webu, všimnete si, že existují dva jazyky, které mají tendenci dominovat na vrcholu: C++ a Rust. S C++ pracuji od 90. let a dokonce jsem měl svůj vlastní Win32 C++ framework ještě předtím, než MFC/ATL byla věc, takže s jazykem mám bohaté zkušenosti. Není moc zábavné s něčím pracovat, když to už znáte, takže místo toho uděláme verzi Rust. ;)

Rust je relativně nový, pokud jde o programovací jazyky, ale stal se pro mě předmětem zvědavosti, když Linus Torvalds oznámil, že přijme Rust jako programovací jazyk jádra Linuxu. Pro nás starší programátory je to asi totéž, jako kdybychom řekli, že tato nová fangled new age hippie věcička bude novým dodatkem k ústavě USA.

Nyní, když jste zkušený programátor, máte tendenci nenaskočit do rozjetého vlaku tak rychle jako mladší lidé, jinak byste se mohli popálit rychlými změnami jazyka nebo knihoven. (Každý, kdo používal první verzi AngularJS, bude vědět, o čem mluvím.) Rust je stále poněkud ve fázi experimentálního vývoje a připadá mi legrační, že tolik příkladů kódu na webu kompilovat již s aktuálními verzemi balíčků.

Výkon, který předvádějí aplikace Rust, však nelze upřít. Pokud jste to nikdy nezkusili ripgrep nebo fd-najít na velkých stromech zdrojového kódu byste je rozhodně měli vyzkoušet. Jsou dokonce k dispozici pro většinu distribucí Linuxu jednoduše ze správce balíčků. S Rustem vyměňujete upovídanost za výkon... a hodně výřečnosti pro a hodně výkonu.

Kompletní kód pro Rust je trochu velký, takže se zde jen podíváme na příslušné handlery:

// =====================================================================
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
  )))
}

To je mnohem složitější než verze 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

A mnohem výkonnější!

Náš Rust server využívající Actix/deadpool_postgres obratně poráží našeho předchozího šampióna Starlette o +125 %, ExpressJS o +362 % a čisté PHP o +1366 %. (Ponechám deltu výkonu s verzí Laravel jako cvičení pro čtenáře.)

Zjistil jsem, že naučit se samotný jazyk Rust bylo obtížnější než jiné jazyky, protože má mnohem více problémů než cokoli, co jsem viděl mimo shromáždění 6502, ale pokud váš server Rust zvládne 14x více uživatelů jako váš PHP server, pak se možná přepínáním technologií přeci jen dá něco získat. To je důvod, proč další verze Pafera Framework bude založena na Rustu. Křivka učení je mnohem vyšší než u skriptovacích jazyků, ale výkon bude stát za to. Pokud nemůžete věnovat čas tomu, abyste se naučili Rust, pak založit svůj technologický stack na Starlette nebo Node.js také není špatné rozhodnutí.

Technický dluh

V posledních dvaceti letech jsme přešli od levných statických hostingových webů přes sdílený hosting se zásobníky LAMP až po pronájem VPS pro AWS, Azure a další cloudové služby. V současné době se mnoho společností spokojuje s rozhodováním o návrhu na základě toho, koho považují za dostupného nebo nejlevnějšího, protože příchod pohodlných cloudových služeb usnadnil nahodit více hardwaru na pomalé servery a aplikace. To jim přineslo velké krátkodobé zisky za cenu dlouhodobého technického dluhu.

Varování kalifornského chirurga: Toto není skutečný vesmírný pes.

Před 70 lety proběhl velký vesmírný závod mezi Sovětským svazem a Spojenými státy. Sověti vyhráli většinu prvních milníků. Měli první satelit ve Sputniku, prvního psa ve vesmíru v Lajce, první měsíční kosmickou loď na Luně 2, prvního muže a ženu ve vesmíru v Juriji Gagarinovi a Valentině Těreškovové a tak dále...

Ale pomalu se jim hromadil technický dluh.

Ačkoli Sověti byli první v každém z těchto úspěchů, jejich inženýrské procesy a cíle způsobily, že se zaměřili na krátkodobé výzvy spíše než na dlouhodobou proveditelnost. Vyhráli pokaždé, když skočili, ale byli stále unavenější a pomalejší, zatímco jejich soupeři pokračovali v konzistentních krocích směrem k cílové čáře.

Jakmile Neil Armstrong podnikl své historické kroky na Měsíci v přímém televizním přenosu, Američané se ujali vedení a pak tam zůstali, zatímco sovětský program zakolísal. To se neliší od dnešních společností, které se soustředily na další velkou věc, další velkou výplatu nebo další velkou technologii, přičemž se jim nepodařilo vytvořit správné návyky a strategie na dlouhou trať.

Být první na trhu neznamená, že se stanete dominantním hráčem na tomto trhu. Alternativně, věnovat čas tomu, abyste dělali věci správně, nezaručuje úspěch, ale rozhodně zvyšuje vaše šance na dlouhodobé úspěchy. Pokud jste pro svou společnost technologickým lídrem, vyberte si správný směr a nástroje pro svou pracovní zátěž. Nedovolte, aby popularita nahradila výkon a efektivitu.

Zdroje

Chcete si stáhnout soubor 7z obsahující skripty Rust, ExpressJS, Flask, Starlette a Pure PHP?

O autorovi

Jim programuje od doby, kdy v 90. letech získal IBM PS/2. Dodnes stále preferuje ruční psaní HTML a SQL a ve své práci se zaměřuje na efektivitu a korektnost.