Da, Virginia, există *a Moș Crăciun Diferența dintre cadrele web în 2023

Călătoria unui programator sfidător pentru a găsi un cod de server web performant înainte de a ceda presiunii pieței și datoriei tehnice
2023-03-24 11:52:06
👁️ 800
💬 0

Cuprins

  1. Introducere
  2. Testul
  3. PHP/Laravel
  4. PHP pur
  5. Revizuind Laravel
  6. Django
  7. Balon
  8. Starlette
  9. Node.js/ExpressJS
  10. Rugina/Actix
  11. Datoria tehnică
  12. Resurse

Introducere

După unul dintre cele mai recente interviuri ale mele de angajare, am fost surprins să realizez că compania la care am aplicat încă folosea Laravel, un framework PHP pe care l-am încercat acum aproximativ un deceniu. Era decent pentru vremea respectivă, dar dacă există o constantă atât în ​​tehnologie, cât și în modă, aceasta este schimbarea continuă și reapariția stilurilor și conceptelor. Dacă ești un programator JavaScript, probabil că ești familiarizat cu această veche glumă

Programator 1: "Nu-mi place acest nou cadru JavaScript!"

Programator 2: „Nu trebuie să vă faceți griji. Așteptați doar șase luni și va mai fi unul care să-l înlocuiască!"

Din curiozitate, am decis să văd exact ce se întâmplă când punem la încercare vechiul și noul. Desigur, web-ul este plin de repere și revendicări, dintre care cea mai populară este probabil cea Puncte de referință ale cadrului web TechEmpower aici . Totuși, nu vom face nimic la fel de complicat ca ei astăzi. Vom păstra lucrurile frumoase și simple ambele, astfel încât acest articol să nu se transforme în Război și pace , și că veți avea o șansă ușoară să rămâneți treaz până când veți termina de citit. Se aplică avertismentele obișnuite: acest lucru s-ar putea să nu funcționeze la fel pe mașina dvs., diferite versiuni de software pot afecta performanța, iar pisica Schrödinger a devenit de fapt o pisică zombie care era pe jumătate vie și pe jumătate moartă în același timp.

Testul

Mediul de testare

Pentru acest test, îmi voi folosi laptopul înarmat cu un i5 slab care rulează Manjaro Linux, așa cum se arată aici.

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

Sarcina la îndemână

Codul nostru va avea trei sarcini simple pentru fiecare solicitare:

  1. Citiți ID-ul sesiunii utilizatorului curent dintr-un cookie
  2. Încărcați informații suplimentare dintr-o bază de date
  3. Returnează acele informații utilizatorului

Ce fel de test idiot este acela, te-ai putea întreba? Ei bine, dacă te uiți la solicitările de rețea pentru această pagină, vei observa una numită sessionvars.js care face exact același lucru.

Conținutul sessionvars.js

Vedeți, paginile web moderne sunt creaturi complicate, iar una dintre cele mai comune sarcini este stocarea în cache a paginilor complexe pentru a evita încărcarea excesivă pe serverul bazei de date.

Dacă redăm din nou o pagină complexă de fiecare dată când un utilizator o solicită, atunci putem servi doar aproximativ 600 de utilizatori pe secundă.

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

Dar dacă memorăm această pagină ca fișier HTML static și lăsăm pe Nginx să o arunce rapid pe fereastră utilizatorului, atunci putem servi 32.000 de utilizatori pe secundă, crescând performanța cu un factor de 50 de ori.

╰─➤  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 static este partea care merge la toată lumea și numai părțile care diferă în funcție de utilizator sunt trimise în sessionvars.js. Acest lucru nu numai că reduce încărcarea bazei de date și creează o experiență mai bună pentru utilizatorii noștri, dar scade și probabilitățile cuantice ca serverul nostru să se vaporizeze spontan într-o breșă warp core atunci când atacul Klingonilor.

Cerințe de cod

Codul returnat pentru fiecare cadru va avea o cerință simplă: arătați utilizatorului de câte ori a reîmprospătat pagina spunând „Numărul este x”. Pentru a menține lucrurile simple, vom sta departe de cozile Redis, componentele Kubernetes sau AWS Lambdas pentru moment.

Afișează de câte ori ați vizitat pagina

Datele sesiunii fiecărui utilizator vor fi salvate într-o bază de date PostgreSQL.

Tabelul usersessions

Și acest tabel al bazei de date va fi trunchiat înainte de fiecare test.

Tabelul după ce a fost trunchiat

Simplu, dar eficient, este motto-ul Pafera... oricum în afara celei mai întunecate cronologie...

Rezultatele reale ale testului

PHP/Laravel

Bine, așa că acum putem începe în sfârșit să ne murdărim mâinile. Vom sări peste configurarea pentru Laravel, deoarece este doar o grămadă de compozitori și artizani. comenzi.

Mai întâi, vom configura setările bazei de date în fișierul .env

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

Apoi, vom seta o singură rută alternativă care trimite fiecare cerere către controlerul nostru.

Route::fallback(SessionController::class);

Și setați controlerul să afișeze numărul. Laravel, implicit, stochează sesiunile în baza de date. De asemenea, oferă session() funcția pentru a interfața cu datele sesiunii noastre, așa că a fost nevoie doar de câteva linii de cod pentru a reda pagina noastră.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

După configurarea php-fpm și Nginx, pagina noastră arată destul de bine...

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

Cel puțin până când vom vedea rezultatele testelor...

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

Nu, nu este o greșeală de tipar. Mașina noastră de testare a trecut de la 600 de solicitări pe secundă care redă o pagină complexă... la 21 de solicitări pe secundă care redă „Numărul este 1”.

Deci, ce a mers prost? Este ceva în neregulă cu instalarea noastră PHP? Nginx încetinește cumva la interfața cu php-fpm?

PHP pur

Să refacem această pagină în cod PHP pur.

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

Acum am folosit 98 de linii de cod pentru a face ceea ce au făcut patru linii de cod (și o mulțime de lucrări de configurare) în Laravel. (Desigur, dacă am gestiona corect erorile și mesajele adresate utilizatorului, acesta ar fi de aproximativ două ori numărul de linii.) Poate că putem ajunge la 30 de solicitări pe secundă?

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

Vai! Se pare că, până la urmă, nu este nimic în neregulă cu instalarea noastră PHP. Versiunea PHP pură face 700 de solicitări pe secundă.

Dacă nu este nimic în neregulă cu PHP, poate că am configurat greșit Laravel?

Revizuind Laravel

După ce a căutat pe web probleme de configurare și sfaturi de performanță, două dintre cele mai populare tehnici au fost memorarea în cache a datelor de configurare și rutarea datelor pentru a evita procesarea lor pentru fiecare solicitare. Prin urmare, vom lua sfatul lor și vom încerca aceste sfaturi.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Totul arată bine pe linia de comandă. Să refacem benchmark-ul.

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

Ei bine, acum am crescut performanța de la 21,04 la 28,80 de solicitări pe secundă, o creștere dramatică de aproape 37%! Acest lucru ar fi destul de impresionant pentru orice pachet de software... cu excepția faptului că încă facem doar 1/24 din numărul de solicitări ale versiunii PHP pur.

Dacă vă gândiți că ceva trebuie să fie în neregulă cu acest test, ar trebui să discutați cu autorul framework-ului Lucinda PHP. În rezultatele testelor sale, el are Lucinda îl învinge pe Laravel de 36x pentru solicitările HTML și de 90x pentru solicitările JSON.

După ce am testat pe propria mea mașină atât cu Apache, cât și cu Nginx, nu am niciun motiv să mă îndoiesc de el. Laravel este într-adevăr drept lent! PHP în sine nu este chiar atât de rău, dar odată ce adăugați toate procesele suplimentare pe care Laravel o adaugă la fiecare solicitare, atunci mi se pare foarte dificil să recomand Laravel ca alegere în 2023.

Django

Conturi PHP/Wordpress pentru aproximativ 40% din toate site-urile web de pe web , făcându-l de departe cel mai dominant cadru. Totuși, personal, constat că popularitatea nu se traduce neapărat în calitate, mai mult decât mă simt cu o dorință necontrolată de acea mâncare gourmet extraordinară de la cel mai popular restaurant din lume ... McDonald&#x27;s. Deoarece am testat deja codul PHP pur, nu vom testa Wordpress în sine, deoarece orice lucru care implică Wordpress ar fi, fără îndoială, mai mic decât cele 700 de solicitări pe secundă pe care le-am observat cu PHP pur.

Django este un alt cadru popular care există de mult timp. Dacă l-ați folosit în trecut, probabil că vă amintiți cu drag de interfața sa spectaculoasă de administrare a bazei de date, precum și cât de enervant a fost să configurați totul așa cum v-ați dorit. Să vedem cât de bine funcționează Django în 2023, mai ales cu noua interfață ASGI pe care a adăugat-o începând cu versiunea 4.0.

Configurarea Django este remarcabil de similară cu configurarea Laravel, deoarece ambii au fost din epoca în care arhitecturile MVC erau elegante și corecte. Vom sări peste configurația plictisitoare și vom trece direct la configurarea vizualizării.

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

Patru linii de cod sunt identice cu versiunea Laravel. Să vedem cum funcționează.

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

Nu este rău deloc la 355 de solicitări pe secundă. Este doar jumătate din performanța versiunii PHP pur, dar este și de 12 ori mai mare decât a versiunii Laravel. Django vs. Laravel pare să nu fie deloc un concurs.

Balon

În afară de cadrele mai mari pentru totul, inclusiv pentru chiuveta de bucătărie, există și cadre mai mici care fac doar niște setări de bază, în timp ce vă permit să vă ocupați de restul. Unul dintre cele mai bune de utilizat este Flask și omologul său ASGI Quart. A mea Cadrul PaferaPy este construit pe Flask, așa că sunt bine familiarizat cu cât de ușor este să faci lucrurile în același timp menținând performanța.

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

După cum puteți vedea, scriptul Flask este mai scurt decât scriptul PHP pur. Am descoperit că dintre toate limbile pe care le-am folosit, Python este probabil cel mai expresiv limbaj în ceea ce privește tastele tastate. Lipsa acoladelor și a parantezelor, a înțelegerii listelor și a dictelor și blocarea bazată mai degrabă pe indentație decât pe punct și virgulă fac ca Python să fie destul de simplu, dar puternic în capabilitățile sale.

Din păcate, Python este, de asemenea, cel mai lent limbaj de uz general de acolo, în ciuda cât de mult software a fost scris în el. Numărul de biblioteci Python disponibile este de aproximativ patru ori mai mare decât limbile similare și acoperă o cantitate mare de domenii, dar nimeni nu ar spune că Python este rapid și nici performant în afara nișelor precum NumPy.

Să vedem cum se compară versiunea noastră Flask cu cadrele noastre anterioare.

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

Scriptul nostru Flask este de fapt mai rapid decât versiunea noastră PHP pură!

Dacă ești surprins de acest lucru, ar trebui să realizezi că aplicația noastră Flask își face toată inițializarea și configurarea atunci când pornim serverul gunicorn, în timp ce PHP reexecută scriptul de fiecare dată când apare o nouă solicitare. Echivalează cu că Flask este șoferul de taxi tânăr și nerăbdător care a pornit deja mașina și așteaptă lângă drum, în timp ce PHP este șoferul bătrân care stă la el acasă așteptând să vină un apel și abia apoi conduce să te ia. Fiind un tip de școală veche și venind din vremurile în care PHP a fost o schimbare minunată pentru fișierele simple HTML și SHTML, este puțin trist să realizezi cât de mult timp a trecut, dar diferențele de design îngreunează cu adevărat PHP. concurează cu serverele Python, Java și Node.js care rămân doar în memorie și gestionează cererile cu ușurința agilă a unui jongler.

Starlette

Flask ar putea fi cel mai rapid cadru al nostru de până acum, dar este de fapt un software destul de vechi. Comunitatea Python a trecut la noile servere ASGI asincrone cu câțiva ani în urmă și, bineînțeles, eu am trecut împreună cu ele.

Cea mai nouă versiune a Cadrului Pafera, PaferaPyAsync , se bazează pe Starlette. Deși există o versiune ASGI a Flask numită Quart, diferențele de performanță dintre Quart și Starlette au fost suficiente pentru ca eu să-mi rebazez codul pe Starlette.

Programarea asincronă poate fi înspăimântătoare pentru mulți oameni, dar de fapt nu este un concept dificil, datorită faptului că băieții Node.js au popularizat conceptul în urmă cu peste un deceniu.

Obișnuiam să luptăm cu concurența cu multithreading, multiprocesare, calcul distribuit, înlănțuire de promisiuni și toate acele vremuri distractive care au îmbătrânit prematur și i-au deshidratat pe mulți programatori veterani. Acum, scriem doar async în faţa funcţiilor noastre şi await în fața oricărui cod care ar putea dura ceva timp pentru a se executa. Este într-adevăr mai pronunțat decât codul obișnuit, dar mult mai puțin enervant de utilizat decât a avea de a face cu primitivele de sincronizare, transmiterea mesajelor și rezolvarea promisiunilor.

Fișierul nostru Starlette arată astfel:

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

După cum puteți vedea, este destul de copiat și lipit din scriptul nostru Flask cu doar câteva modificări de rutare și async/await cuvinte cheie.

Câtă îmbunătățire ne poate aduce cu adevărat codul copiat și lipit?

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

Avem un nou campion, doamnelor și domnilor! Maximul nostru anterior a fost versiunea noastră PHP pură la 704 de solicitări pe secundă, care a fost apoi depășită de versiunea noastră Flask la 1080 de solicitări pe secundă. Scriptul nostru Starlette zdrobește toți concurenții anteriori la 4562 de solicitări pe secundă, ceea ce înseamnă o îmbunătățire de 6 ori față de PHP pur și o îmbunătățire de 4 ori față de Flask.

Dacă nu v-ați schimbat încă codul WSGI Python în ASGI, acum ar putea fi un moment bun pentru a începe.

Node.js/ExpressJS

Până acum, am acoperit doar cadrele PHP și Python. Cu toate acestea, o mare parte a lumii utilizează de fapt Java, DotNet, Node.js, Ruby on Rails și alte astfel de tehnologii pentru site-urile lor web. Aceasta nu este în niciun caz o imagine de ansamblu cuprinzătoare a tuturor ecosistemelor și biomurilor lumii, așa că pentru a evita să facem echivalentul de programare al chimiei organice, vom alege doar cadrele pentru care sunt mai ușor de introdus cod. . din care Java cu siguranță nu este.

Cu excepția cazului în care v-ați ascuns sub copia dvs. de K&R C sau Knuth&#x27;s Arta programarii pe computer în ultimii cincisprezece ani, probabil că ați auzit de Node.js. Aceia dintre noi care există încă de la începuturile JavaScript sunt fie incredibil de speriați, uimiți, fie ambii de starea JavaScript-ului modern, dar nu se poate nega că JavaScript a devenit o forță de luat în considerare și pe servere. ca browsere. La urma urmei, avem chiar și numere întregi native pe 64 de biți acum în limbă! Este mult mai bun decât tot ce este stocat în float de 64 de biți!

ExpressJS este probabil cel mai ușor server Node.js de utilizat, așa că vom face o aplicație rapidă și murdară Node.js/ExpressJS pentru a servi contorul nostru.

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

Acest cod a fost de fapt mai ușor de scris decât versiunile Python, deși JavaScript nativ devine destul de dificil atunci când aplicațiile devin mai mari și toate încercările de a corecta acest lucru, cum ar fi TypeScript, devin rapid mai detaliate decât Python.

Să vedem cum funcționează!

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

Poate că ați auzit povești populare străvechi (oricum vechi după standardele de internet...) despre Node.js&#x27; viteză, iar acele povești sunt în mare parte adevărate datorită muncii spectaculoase pe care Google a făcut-o cu motorul JavaScript V8. În acest caz, totuși, deși aplicația noastră rapidă depășește scriptul Flask, natura sa cu un singur thread este învinsă de cele patru procese asincrone utilizate de Starlette Knight care spune „Ni!”.

Să primim mai mult ajutor!

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

Bine! Acum este o luptă egală patru la patru! Să facem benchmarking!

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

Încă nu este chiar la nivelul Starlette, dar nu este rău pentru un hack JavaScript rapid de cinci minute. Din propriile mele teste, acest script este de fapt reținut puțin la nivel de interfață a bazei de date, deoarece node-postgres nu este nici pe departe la fel de eficient ca psycopg pentru Python. Trecerea la sqlite ca driver de bază de date generează peste 3000 de solicitări pe secundă pentru același cod ExpressJS.

Principalul lucru de remarcat este că, în ciuda vitezei lente de execuție a lui Python, cadrele ASGI pot fi de fapt competitive cu soluțiile Node.js pentru anumite sarcini de lucru.

Rugina/Actix

Așa că acum, ne apropiem de vârful muntelui și, prin munte, mă refer la cele mai mari scoruri de referință înregistrate de șoareci și bărbați deopotrivă.

Dacă te uiți la majoritatea benchmark-urilor disponibile pe web, vei observa că există două limbi care tind să domine în top: C++ și Rust. Lucrez cu C++ din anii 90 și chiar aveam propriul cadru Win32 C++ înainte ca MFC/ATL să fie un lucru, așa că am multă experiență cu limbajul. Nu este foarte distractiv să lucrezi cu ceva când îl știi deja, așa că vom face în schimb o versiune Rust. ;)

Rust este relativ nou în ceea ce privește limbajele de programare, dar a devenit un obiect de curiozitate pentru mine când Linus Torvalds a anunțat că va accepta Rust ca limbaj de programare pentru nucleul Linux. Pentru noi, programatorii mai în vârstă, este aproximativ același lucru cu a spune că acest nou lucru hippie nou și modern va fi un nou amendament la Constituția SUA.

Acum, atunci când ești un programator cu experiență, ai tendința să nu te arunci la fel de repede ca cei mai tineri, altfel s-ar putea să fii ars de schimbările rapide ale limbii sau ale bibliotecilor. (Oricine a folosit prima versiune de AngularJS va ști despre ce vorbesc.) Rust este încă oarecum în stadiul de dezvoltare experimentală și mi se pare amuzant că atâtea exemple de cod de pe web nici măcar nu mai compilați cu versiunile curente ale pachetelor.

Cu toate acestea, performanța arătată de aplicațiile Rust nu poate fi refuzată. Dacă nu ai încercat niciodată ripgrep sau fd-găsește pe arbori mari de cod sursă, cu siguranță ar trebui să le învârtiți. Sunt disponibile chiar și pentru majoritatea distribuțiilor Linux din managerul de pachete. Schimbați verbozitatea cu performanță cu Rust... a lot de verbozitate pentru a lot de performanţă.

Codul complet pentru Rust este puțin mare, așa că vom arunca o privire la handlerele relevanți aici:

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

Acest lucru este mult mai complicat decât versiunile 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

Si mult mai performant!

Serverul nostru Rust care folosește Actix/deadpool_postgres învinge cu ușurință campioana noastră precedentă Starlette cu +125%, ExpressJS cu +362% și PHP pur cu +1366%. (Voi părăsi delta performanței cu versiunea Laravel ca exercițiu pentru cititor.)

Am descoperit că învățarea limbajului Rust în sine a fost mai dificilă decât alte limbi, deoarece are multe mai multe probleme decât orice am văzut în afara 6502 Assembly, dar dacă serverul dvs. Rust poate prelua de 14 ori numărul de utilizatorii ca server PHP, atunci poate că există ceva de câștigat cu schimbarea tehnologiilor, până la urmă. De aceea, următoarea versiune a Cadrului Pafera se va baza pe Rust. Curba de învățare este mult mai mare decât limbajele de scripting, dar performanța va merita. Dacă nu puteți dedica timp pentru a învăța Rust, atunci să vă bazați stiva tehnologică pe Starlette sau Node.js nu este o decizie rea.

Datoria tehnică

În ultimii douăzeci de ani, am trecut de la site-uri de găzduire statică ieftine la găzduire partajată cu stive LAMP la închirierea de VPS-uri către AWS, Azure și alte servicii cloud. În zilele noastre, multe companii sunt mulțumite să ia decizii de proiectare pe baza oricui pot găsi că este disponibil sau mai ieftin, deoarece apariția serviciilor cloud convenabile a făcut mai ușor să aruncați mai mult hardware la serverele și aplicațiile lente. Acest lucru le-a oferit câștiguri mari pe termen scurt cu prețul datoriei tehnice pe termen lung.

Avertismentul chirurgului general din California: acesta nu este un adevărat câine spațial.

În urmă cu 70 de ani, a existat o mare cursă spațială între Uniunea Sovietică și Statele Unite. Sovieticii au câștigat majoritatea etapelor timpurii. Au avut primul satelit în Sputnik, primul câine în spațiu în Laika, prima navă spațială lunară în Luna 2, primul bărbat și femeie în spațiu în Yuri Gagarin și Valentina Tereshkova și așa mai departe...

Dar acumuleau încet datorii tehnice.

Deși sovieticii au fost primii la fiecare dintre aceste realizări, procesele și obiectivele lor de inginerie i-au determinat să se concentreze pe provocări pe termen scurt, mai degrabă decât pe fezabilitate pe termen lung. Au câștigat de fiecare dată când au sărit, dar au devenit din ce în ce mai obosiți și mai încet, în timp ce adversarii lor continuau să facă pași consistenti către linia de sosire.

Odată ce Neil Armstrong și-a făcut pașii istorici pe Lună la televiziunea în direct, americanii au preluat conducerea și apoi au rămas acolo, în timp ce programul sovietic s-a clătinat. Acest lucru nu este diferit de companiile de astăzi care s-au concentrat pe următorul mare lucru, următorul mare profit sau următoarea mare tehnologie, fără să dezvolte obiceiuri și strategii adecvate pe termen lung.

A fi primul pe piață nu înseamnă că vei deveni jucătorul dominant pe acea piață. Alternativ, a-ți acorda timp pentru a face lucrurile corect nu garantează succesul, dar cu siguranță îți crește șansele de realizare pe termen lung. Dacă sunteți liderul tehnologic al companiei dvs., alegeți direcția și instrumentele potrivite pentru volumul dvs. de lucru. Nu lăsați popularitatea să înlocuiască performanța și eficiența.

Resurse

Doriți să descărcați un fișier 7z care conține scripturile Rust, ExpressJS, Flask, Starlette și Pure PHP?

Despre autor

Jim programează de când a primit un IBM PS/2 înapoi în anii '90. Până în prezent, el încă preferă să scrie manual HTML și SQL și se concentrează pe eficiență și corectitudine în munca sa.