Da, Virginia, *je* a Božiček Razlika med spletnimi ogrodji leta 2023

Potovanje enega kljubovalnega programerja, da najde hitro delujočo kodo spletnega strežnika, preden podleže pritisku trga in tehničnemu dolgu
2023-03-24 11:52:06
👁️ 770
💬 0

Vsebina

  1. Uvod
  2. Test
  3. PHP/Laravel
  4. Čisti PHP
  5. Ponovno obiskovanje Laravela
  6. Django
  7. Bučka
  8. Starlette
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. Tehnični dolg
  12. Viri

Uvod

Po enem od mojih zadnjih razgovorov za službo sem bil presenečen ugotovil, da podjetje, za katerega sem se prijavil, še vedno uporablja Laravel, PHP okvir, ki sem ga preizkusil pred približno desetletjem. Za tisti čas je bilo spodobno, a če obstaja ena stalnica v tehnologiji in modi, je to nenehno spreminjanje in ponovno pojavljanje stilov in konceptov. Če ste programer JavaScript, ste verjetno seznanjeni s to staro šalo

Programer 1: "Ni mi všeč to novo ogrodje JavaScript!"

Programer 2: "Ni vam treba skrbeti. Samo počakajte šest mesecev in prišel bo drug, ki ga bo nadomestil!"

Zaradi radovednosti sem se odločil natančno videti, kaj se zgodi, ko preizkusimo staro in novo. Seveda je splet poln meril uspešnosti in trditev, med katerimi je verjetno najbolj priljubljena Primerjalna merila spletnega ogrodja TechEmpower tukaj . Vendar danes ne bomo naredili ničesar tako zapletenega kot oni. Stvari bomo ohranili lepe in enostavne, tako da se ta članek ne bo spremenil v Vojna in mir , in da boste imeli majhno možnost, da ostanete budni, ko končate z branjem. Veljajo običajna opozorila: to morda ne bo delovalo enako na vašem računalniku, različne različice programske opreme lahko vplivajo na zmogljivost in Schrödingerjeva mačka je dejansko postala mačka zombi, ki je bila napol živa in napol mrtva ob istem času.

Preizkus

Testno okolje

Za ta preizkus bom uporabljal svoj prenosnik, oborožen s slabim i5, ki poganja Manjaro Linux, kot je prikazano tukaj.

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

Naloga pri roki

Naša koda bo imela tri preproste naloge za vsako zahtevo:

  1. Preberi ID seje trenutnega uporabnika iz piškotka
  2. Naloži dodatne informacije iz baze podatkov
  3. Te informacije vrnite uporabniku

Kakšen idiotski test je to, se boste morda vprašali? No, če pogledate omrežne zahteve za to stran, boste opazili eno, imenovano sessionvars.js, ki dela popolnoma isto stvar.

Vsebina sessionvars.js

Vidite, sodobne spletne strani so zapletena bitja in ena najpogostejših nalog je predpomnjenje zapletenih strani, da se izognete prekomerni obremenitvi strežnika baze podatkov.

Če znova upodobimo zapleteno stran vsakič, ko jo uporabnik zahteva, potem lahko oskrbimo samo približno 600 uporabnikov na sekundo.

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

Če pa to stran shranimo v predpomnilnik kot statično datoteko HTML in pustimo, da jo Nginx uporabniku hitro vrže skozi okno, potem lahko oskrbimo 32.000 uporabnikov na sekundo, s čimer povečamo zmogljivost za faktor 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

Statični index.en.html je del, ki je namenjen vsem, v sessionvars.js pa so poslani le deli, ki se razlikujejo glede na uporabnika. To ne le zmanjša obremenitev baze podatkov in ustvari boljšo izkušnjo za naše uporabnike, ampak tudi zmanjša kvantno verjetnost, da bo naš strežnik spontano izhlapel v vdoru v warp jedro, ko Klingonci napadejo.

Zahteve kode

Vrnjena koda za vsako ogrodje bo imela eno preprosto zahtevo: pokažite uporabniku, kolikokrat je osvežil stran, tako da rečete "Štetje je x". Da bi stvari poenostavili, se bomo za zdaj izogibali čakalnim vrstam Redis, komponentam Kubernetes ali AWS Lambdas.

Prikazuje, kolikokrat ste obiskali stran

Podatki o seji vsakega uporabnika bodo shranjeni v bazi podatkov PostgreSQL.

Tabela uporabniških sej

In ta tabela baze podatkov bo okrnjena pred vsakim preizkusom.

Tabela po prirezovanju

Enostaven, a učinkovit je moto Pafera ... zunaj najtemnejše časovne osi ...

Dejanski rezultati testa

PHP/Laravel

V redu, zdaj si lahko končno začnemo umazati roke. Preskočili bomo nastavitev za Laravel, ker je le skupek skladateljev in rokodelcev ukazi.

Najprej bomo v datoteki .env nastavili nastavitve baze podatkov

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

Nato bomo nastavili eno samo nadomestno pot, ki pošlje vsako zahtevo našemu krmilniku.

Route::fallback(SessionController::class);

In nastavite krmilnik, da prikaže štetje. Laravel privzeto shranjuje seje v bazo podatkov. Zagotavlja tudi session() funkcija za vmesnik z našimi podatki o seji, tako da je bilo potrebnih le nekaj vrstic kode za upodobitev naše strani.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

Po nastavitvi php-fpm in Nginx je naša stran videti precej dobro ...

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

Vsaj dokler dejansko ne vidimo rezultatov testov ...

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 ni tipkarska napaka. Naš testni stroj je prešel s 600 zahtev na sekundo za upodabljanje zapletene strani... na 21 zahtev na sekundo za upodabljanje "Count is 1".

Torej, kaj je šlo narobe? Je kaj narobe z našo namestitvijo PHP? Ali se Nginx nekako upočasnjuje pri povezovanju s php-fpm?

Čisti PHP

Ponovimo to stran v čisti kodi 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'];

Zdaj smo uporabili 98 vrstic kode za to, kar so naredile štiri vrstice kode (in cel kup konfiguracijskega dela) v Laravelu. (Seveda bi bilo to približno dvakratno število vrstic, če bi pravilno obravnavali napake in sporočila, ki bi jih posredovali uporabnikom.) Morda lahko dosežemo 30 zahtev na sekundo?

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

Vau! Videti je, da z našo namestitvijo PHP kljub vsemu ni nič narobe. Čista različica PHP naredi 700 zahtev na sekundo.

Če s PHP-jem ni nič narobe, smo morda napačno konfigurirali Laravel?

Ponovno obiskovanje Laravela

Po brskanju po spletu za težavami s konfiguracijo in nasveti o zmogljivosti sta bili dve izmed najbolj priljubljenih tehnik predpomnilnik podatkov o konfiguraciji in usmerjanju, da bi se izognili njihovi obdelavi za vsako zahtevo. Zato bomo upoštevali njihov nasvet in preizkusili te nasvete.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

V ukazni vrstici je vse videti dobro. Ponovimo merilo uspešnosti.

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

No, zdaj smo povečali zmogljivost z 21,04 na 28,80 zahtev na sekundo, dramatično povečanje za skoraj 37 %! To bi bilo precej impresivno za kateri koli programski paket ... razen dejstva, da še vedno izvajamo le 1/24 števila zahtev čiste različice PHP.

Če menite, da je s tem testom nekaj narobe, se pogovorite z avtorjem ogrodja Lucinda PHP. Po njegovih rezultatih testov je Lucinda premaga Laravel za 36x za zahteve HTML in 90x za zahteve JSON.

Po testiranju na lastnem računalniku z Apache in Nginxom nimam razloga dvomiti vanj. Laravel je res pravi to počasi! PHP sam po sebi ni tako slab, a ko dodate vso dodatno obdelavo, ki jo Laravel doda vsaki zahtevi, se mi zdi zelo težko priporočiti Laravel kot izbiro v letu 2023.

Django

PHP/Wordpress računi za približno 40 % vseh spletnih mest na spletu , zaradi česar je daleč najbolj prevladujoč okvir. Osebno pa ugotavljam, da se priljubljenost ne prevede nujno v kakovost, prav tako ne ugotovim, da imam nenadoma neobvladljivo željo po tej izjemni gurmanski hrani iz najbolj priljubljena restavracija na svetu ... McDonald&#x27;s. Ker smo že preizkusili čisto kodo PHP, ne bomo testirali samega Wordpressa, saj bi bilo vse, kar vključuje Wordpress, nedvomno nižje od 700 zahtev na sekundo, ki smo jih opazili pri čistem PHP.

Django je še eno priljubljeno ogrodje, ki obstaja že dolgo časa. Če ste ga že uporabljali v preteklosti, se verjetno z veseljem spominjate njegovega spektakularnega skrbniškega vmesnika baze podatkov in tega, kako nadležno je bilo vse konfigurirati tako, kot ste želeli. Poglejmo, kako dobro bo Django deloval leta 2023, zlasti z novim vmesnikom ASGI, ki ga je dodal od različice 4.0.

Nastavitev Djanga je izjemno podobna nastavitvi Laravela, saj sta oba iz obdobja, ko so bile arhitekture MVC elegantne in pravilne. Preskočili bomo dolgočasno konfiguracijo in šli naravnost k nastavitvi pogleda.

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

Štiri vrstice kode so enake kot pri različici Laravel. Poglejmo, kako se obnese.

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

Sploh ni slabo pri 355 zahtevah na sekundo. To je samo polovica zmogljivosti čiste različice PHP, vendar je tudi 12-krat večja od različice Laravel. Zdi se, da Django proti Laravelu sploh ni tekmovanje.

Bučka

Poleg večjih ogrodij vse-vključno-kuhinjsko-pomivalno korito obstajajo tudi manjša ogrodja, ki opravijo samo nekaj osnovnih nastavitev, medtem ko vam omogočajo, da poskrbite za ostalo. Eden najboljših za uporabo je Flask in njegov dvojnik ASGI Quart. Mojega Ogrodje PaferaPy je zgrajen na vrhu Flaska, zato sem dobro seznanjen s tem, kako preprosto je opraviti stvari in hkrati ohraniti učinkovitost.

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

Kot lahko vidite, je skript Flask krajši od čistega skripta PHP. Ugotavljam, da je izmed vseh jezikov, ki sem jih uporabljal, Python verjetno najbolj izrazit jezik v smislu vnesenih tipk. Zaradi pomanjkanja oklepajev in oklepajev, razumevanja seznamov in narekov ter blokiranja, ki temelji na zamiku namesto podpičja, je Python precej preprost, a zmogljiv v svojih zmogljivostih.

Na žalost je Python tudi najpočasnejši splošni jezik, ne glede na to, koliko programske opreme je bilo napisanega v njem. Število razpoložljivih knjižnic Python je približno štirikrat večje od podobnih jezikov in pokriva ogromno področij, vendar nihče ne bi rekel, da je Python hiter ali zmogljiv zunaj niš, kot je NumPy.

Poglejmo, kakšna je naša različica Flask v primerjavi z našimi prejšnjimi okviri.

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

Naš skript Flask je dejansko hitrejši od naše čiste različice PHP!

Če ste nad tem presenečeni, se morate zavedati, da naša aplikacija Flask opravi vso svojo inicializacijo in konfiguracijo, ko zaženemo strežnik gunicorn, medtem ko PHP znova izvede skript vsakič, ko pride nova zahteva. To&#x27 ;je enakovredno temu, da je Flask mlad, nestrpen taksist, ki je že zagnal avto in čaka ob cesti, medtem ko je PHP stari voznik, ki ostane doma in čaka na klic in šele nato odpelje da te poberem. Ker sem človek stare šole in prihajam iz časov, ko je bil PHP čudovita sprememba navadnih datotek HTML in SHTML, je nekoliko žalostno ugotoviti, koliko časa je minilo, toda razlike v oblikovanju za PHP resnično otežujejo tekmujejo s strežniki Python, Java in Node.js, ki samo ostanejo v pomnilniku in obravnavajo zahteve z okretno lahkotnostjo žonglerja.

Starlette

Flask je morda naše najhitrejše ogrodje doslej, vendar je pravzaprav precej stara programska oprema. Skupnost Python je pred nekaj leti prešla na novejše asinhrone strežnike ASGI in seveda sem tudi sam prešel skupaj z njimi.

Najnovejša različica Pafera Framework, PaferaPyAsync , temelji na Starlette. Čeprav obstaja ASGI različica Flaska z imenom Quart, so bile razlike v zmogljivosti med Quartom in Starlette dovolj, da sem svojo kodo namesto tega preoblikoval na Starlette.

Asinhrono programiranje je lahko za veliko ljudi prestrašeno, vendar pravzaprav ni težak koncept, zahvaljujoč fantom iz Node.js, ki so ta koncept popularizirali pred več kot desetletjem.

Proti sočasnosti smo se borili z večnitnostjo, večprocesiranjem, porazdeljenim računalništvom, veriženjem obljub in vsemi tistimi zabavnimi časi, ki so prezgodaj postarali in izsušili mnoge veteranske programerje. Zdaj samo tipkamo async pred našimi funkcijami in await pred vsako kodo, katere izvedba lahko traja nekaj časa. Res je bolj natančen kot običajna koda, vendar je veliko manj moteč za uporabo, kot če bi se morali ukvarjati s primitivi za sinhronizacijo, posredovanjem sporočil in razreševanjem obljub.

Naša datoteka Starlette izgleda takole:

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

Kot lahko vidite, je v veliki meri kopiran in prilepljen iz našega skripta Flask z le nekaj spremembami usmerjanja in async/await ključne besede.

Koliko izboljšav nam lahko dejansko omogoči kopiranje in lepljenje kode?

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

Imamo novega prvaka, dame in gospodje! Naš prejšnji rekord je bila naša čista različica PHP s 704 zahtevami na sekundo, ki jo je nato prehitela naša različica Flask s 1080 zahtevami na sekundo. Naš skript Starlette premaga vse prejšnje tekmece s 4562 zahtevami na sekundo, kar pomeni 6-kratno izboljšavo v primerjavi s čistim PHP in 4-kratno izboljšavo v primerjavi s Flaskom.

Če svoje kode WSGI Python še niste spremenili v ASGI, je zdaj morda pravi čas, da začnete.

Node.js/ExpressJS

Doslej smo obravnavali le okvira PHP in Python. Vendar pa velik del sveta dejansko uporablja Javo, DotNet, Node.js, Ruby on Rails in druge podobne tehnologije za svoja spletna mesta. To nikakor ni izčrpen pregled vseh svetovnih ekosistemov in biomov, zato bomo, da bi se izognili programiranju, ki je enakovredno organski kemiji, izbrali le okvire, za katere je najlažje vnesti kodo. ., kar Java zagotovo ni.

Razen če ste se skrivali pod svojo kopijo K&amp;R C ali Knuth&#x27;s Umetnost računalniškega programiranja v zadnjih petnajstih letih ste verjetno že slišali za Node.js. Tisti med nami, ki smo prisotni od začetka JavaScripta, smo nad stanjem sodobnega JavaScripta bodisi neverjetno prestrašeni, presenečeni ali oboje, vendar ni mogoče zanikati, da je JavaScript postal sila, s katero je treba računati tudi na strežnikih. kot brskalniki. Navsezadnje imamo zdaj v jeziku celo domača 64-bitna cela števila! To je daleč bolje kot vse, kar je shranjeno v 64-bitnih plavajočih!

ExpressJS je verjetno najlažji strežnik Node.js za uporabo, zato bomo naredili hitro in umazano aplikacijo Node.js/ExpressJS, ki bo služila našemu števcu.

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

To kodo je bilo dejansko lažje napisati kot različice Pythona, čeprav postane izvorni JavaScript precej okoren, ko aplikacije postanejo večje, in vsi poskusi popravka tega, kot je TypeScript, hitro postanejo bolj podrobni kot Python.

Poglejmo, kako se to obnese!

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

Morda ste slišali starodavne (itak starodavne po internetnih standardih...) ljudske pravljice o Node.js&#x27; hitrosti in te zgodbe so večinoma resnične zahvaljujoč spektakularnemu delu, ki ga je Google opravil z motorjem V8 JavaScript. Čeprav v tem primeru naša hitra aplikacija prekaša skript Flask, njeno naravo z eno nitjo premagajo štirje asinhroni procesi, ki jih upravlja Starlette Knight, ki reče »Ni!«.

Poiščimo še pomoč!

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

V redu! Zdaj je to izenačena bitka štiri na štiri! Opravimo primerjavo!

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

Še vedno ni povsem na ravni Starlette, vendar ni slabo za hiter petminutni vdor v JavaScript. Iz mojega lastnega testiranja je ta skript dejansko nekoliko zadržan na ravni vmesnika baze podatkov, ker node-postgres ni niti približno tako učinkovit kot psycopg za Python. Preklop na sqlite kot gonilnik baze podatkov prinaša več kot 3000 zahtev na sekundo za isto kodo ExpressJS.

Glavna stvar, ki jo je treba opozoriti, je, da so kljub nizki hitrosti izvajanja Pythona okviri ASGI dejansko lahko konkurenčni rešitvam Node.js za določene delovne obremenitve.

Rust/Actix

Zdaj smo vse bližje vrhu gore, z goro pa mislim na najvišje primerjalne rezultate, ki so jih zabeležili miši in ljudje.

Če pogledate večino meril uspešnosti ogrodja, ki so na voljo v spletu, boste opazili, da dva jezika prevladujeta na vrhu: C++ in Rust. S C++ delam že od 90. let prejšnjega stoletja in sem imel celo lastno ogrodje Win32 C++, preden je bil MFC/ATL stvar, tako da imam veliko izkušenj z jezikom. Ni zabavno delati z nečim, če to že poznate, zato bomo namesto tega naredili različico Rust. ;)

Rust je razmeroma nov, kar se tiče programskih jezikov, vendar me je zanimalo, ko je Linus Torvalds objavil, da bo sprejel Rust kot programski jezik jedra Linuxa. Za nas starejše programerje je to približno enako, kot če bi rekli, da bo ta novodobna hipijevska stvar nove dobe nov amandma k ustavi ZDA.

Zdaj, ko ste izkušen programer, se ne nagibate k temu, da bi skakali tako hitro kot mlajši, sicer se lahko opečete zaradi hitrih sprememb jezika ali knjižnic. (Vsakdo, ki je uporabljal prvo različico AngularJS, bo vedel, o čem govorim.) Rust je še vedno nekoliko v tej eksperimentalni razvojni fazi in zdi se mi smešno, da toliko primerov kode na spletu sploh ne prevajati s trenutnimi različicami paketov.

Vendar zmogljivosti, ki jo kažejo aplikacije Rust, ni mogoče zanikati. Če še nikoli niste poskusili ripgrep oz fd-najdi na velikih drevesih izvorne kode, bi jih vsekakor morali preizkusiti. Na voljo so celo za večino distribucij Linuxa preprosto prek upravitelja paketov. Z Rustom zamenjate besedičnost za uspešnost ... a veliko besednosti za a veliko uspešnosti.

Celotna koda za Rust je nekoliko velika, zato si bomo tukaj samo ogledali ustrezne upravljalnike:

// =====================================================================
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 veliko bolj zapleteno kot različice 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

In veliko bolj zmogljiv!

Naš strežnik Rust, ki uporablja Actix/deadpool_postgres, hitro premaga našega prejšnjega prvaka Starlette za +125%, ExpressJS za +362% in čisti PHP za +1366%. (Delto zmogljivosti bom pustil pri različici Laravel kot vajo za bralca.)

Ugotovil sem, da je bilo učenje samega jezika Rust težje kot drugih jezikov, saj ima veliko več težav kot kar koli drugega, kar sem videl zunaj 6502 Assembly, vendar če lahko vaš strežnik Rust prenese 14-krat večje število uporabnikov kot vaš strežnik PHP, potem boste morda vendarle lahko kaj pridobili s preklopnimi tehnologijami. Zato bo naslednja različica Pafera Framework temeljila na Rustu. Krivulja učenja je veliko višja kot pri skriptnih jezikih, vendar bo zmogljivost vredna tega. Če si ne morete vzeti časa za učenje Rusta, potem tudi osnovanje tehnološkega sklada na Starlette ali Node.js ni slaba odločitev.

Tehnični dolg

V zadnjih dvajsetih letih smo od poceni statičnih spletnih mest za gostovanje prešli na skupno gostovanje s skladi LAMP do najema VPS-jev za AWS, Azure in druge storitve v oblaku. Dandanes so mnoga podjetja zadovoljna s sprejemanjem oblikovalskih odločitev glede na to, koga najdejo in je na voljo ali je najcenejša, saj je pojav priročnih storitev v oblaku olajšal vlaganje več strojne opreme v počasne strežnike in aplikacije. To jim je prineslo velike kratkoročne dobičke na račun dolgoročnega tehničnega dolga.

Opozorilo kalifornijskega generalnega kirurga: To ni pravi vesoljski pes.

Pred 70 leti je med Sovjetsko zvezo in ZDA potekala velika vesoljska tekma. Sovjeti so osvojili večino prvih mejnikov. Imeli so prvi satelit v Sputniku, prvega psa v vesolju v Laiki, prvo lunino vesoljsko plovilo v Luni 2, prvega moškega in žensko v vesolju v Juriju Gagarinu in Valentini Tereškovi in ​​tako naprej ...

Toda počasi so si nabirali tehnični dolg.

Čeprav so bili Sovjeti prvi pri vsakem od teh dosežkov, so se njihovi inženirski procesi in cilji osredotočali na kratkoročne izzive in ne na dolgoročno izvedljivost. Zmagali so ob vsakem skoku, vendar so postajali vse bolj utrujeni in počasnejši, medtem ko so se njihovi nasprotniki enakomerno približevali cilju.

Ko je Neil Armstrong na televiziji v živo naredil svoje zgodovinske korake na Luni, so Američani prevzeli vodstvo, nato pa ostali tam, ko je sovjetski program zatajil. To se ne razlikuje od današnjih podjetij, ki so se osredotočila na naslednjo veliko stvar, naslednji veliki izkupiček ali naslednjo veliko tehnologijo, medtem ko jim ni uspelo razviti pravilnih navad in dolgoročnih strategij.

Biti prvi na trgu ne pomeni, da boste postali prevladujoči igralec na tem trgu. Druga možnost je, da si vzamete čas, da naredite stvari pravilno, ne zagotavlja uspeha, zagotovo pa poveča vaše možnosti za dolgoročne dosežke. Če ste tehnični vodja svojega podjetja, izberite pravo smer in orodja za svojo delovno obremenitev. Ne dovolite, da priljubljenost nadomesti zmogljivost in učinkovitost.

Viri

Ali želite prenesti datoteko 7z, ki vsebuje skripte PHP Rust, ExpressJS, Flask, Starlette in Pure?

O avtorju

Jim programira, odkar je dobil IBM PS/2 v 90-ih. Še danes najraje ročno piše HTML in SQL, pri svojem delu pa se osredotoča na učinkovitost in korektnost.