Da, Virdžinija, postoji * a Deda Mraz Razlika između web okvira u 2023

Putovanje jednog prkosnog programera da pronađe brzi web serverski kod prije nego što podlegne pritisku tržišta i tehničkim dugovima
2023-03-24 11:52:06
👁️ 796
💬 0

Sadržaj

  1. Uvod
  2. Test
  3. PHP/Laravel
  4. Čisti PHP
  5. Revisiting Laravel
  6. Django
  7. Flask
  8. Starlette
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. Tehnički dug
  12. Resursi

Uvod

Nakon jednog od mojih najnovijih intervjua za posao, bio sam iznenađen kada sam shvatio da kompanija za koju sam se prijavio još uvijek koristi Laravel, PHP framework koji sam isprobao prije otprilike deset godina. Bilo je pristojno za to vrijeme, ali ako postoji jedna konstanta u tehnologiji i modi, to je stalna promjena i ponovno pojavljivanje stilova i koncepata. Ako ste JavaScript programer, vjerovatno vam je poznat ovaj stari vic

Programer 1: "Ne sviđa mi se ovaj novi JavaScript okvir!"

Programer 2: "Nema potrebe da brinete. Samo pričekajte šest mjeseci i doći će još jedan koji će ga zamijeniti!"

Iz radoznalosti, odlučio sam da vidim šta se tačno dešava kada stavimo staro i novo na test. Naravno, web je pun mjerila i tvrdnji, od kojih je vjerovatno najpopularniji TechEmpower Web Framework Benchmarks ovdje . Ipak, danas nećemo raditi ništa tako komplikovano kao oni. Održat ćemo stvari lijepim i jednostavnim kako se ovaj članak ne bi pretvorio u Rat i mir , i da ćete imati malu šansu da ostanete budni do trenutka kada završite s čitanjem. Primjenjuju se uobičajena upozorenja: ovo možda neće raditi isto na vašoj mašini, različite verzije softvera mogu utjecati na performanse, a Schrödingerova mačka je zapravo postala zombi mačka koja je bila napola živa i napola mrtva u isto vrijeme.

Test

Testing Environment

Za ovaj test, koristiću svoj laptop naoružan slabašnim i5 koji pokreće Manjaro Linux kao što je prikazano ovdje.

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

Zadatak pri ruci

Naš kod će imati tri jednostavna zadatka za svaki zahtjev:

  1. Pročitajte ID sesije trenutnog korisnika iz kolačića
  2. Učitajte dodatne informacije iz baze podataka
  3. Vratite te informacije korisniku

Kakav je to idiotski test, pitate se? Pa, ako pogledate mrežne zahtjeve za ovu stranicu, primijetit ćete jedan koji se zove sessionvars.js koji radi potpuno istu stvar.

Sadržaj sessionvars.js

Vidite, moderne web stranice su komplikovana stvorenja, a jedan od najčešćih zadataka je keširanje složenih stranica kako bi se izbjeglo prekomjerno opterećenje servera baze podataka.

Ako ponovo generiramo složenu stranicu svaki put kada je korisnik zatraži, tada možemo opsluživati ​​samo oko 600 korisnika u sekundi.

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

Ali ako keširamo ovu stranicu kao statičnu HTML datoteku i pustimo Nginx da je brzo izbaci kroz prozor korisniku, tada možemo opsluživati ​​32.000 korisnika u sekundi, povećavajući performanse za faktor od 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čki index.en.html je dio koji ide svima, a samo dijelovi koji se razlikuju po korisniku šalju se u sessionvars.js. Ovo ne samo da smanjuje opterećenje baze podataka i stvara bolje iskustvo za naše korisnike, već i smanjuje kvantne vjerovatnoće da će naš server spontano ispariti u proboju warp jezgra kada napadnu Klingonci.

Zahtjevi koda

Vraćeni kod za svaki okvir imat će jedan jednostavan zahtjev: pokazati korisniku koliko puta je osvježio stranicu govoreći "Broj je x". Da stvari budu jednostavne, za sada ćemo se kloniti redova redova Redis, Kubernetes komponenti ili AWS Lambda.

Prikazuje koliko ste puta posjetili stranicu

Podaci o sesiji svakog korisnika biće sačuvani u PostgreSQL bazi podataka.

Tabela korisničkih sesija

I ova tabela baze podataka će biti skraćena prije svakog testa.

Tabela nakon skraćenja

Jednostavan, a efikasan je moto Pafere... ionako izvan najmračnije vremenske linije...

Stvarni rezultati testa

PHP/Laravel

U redu, sad konačno možemo početi prljati ruke. Preskočićemo podešavanje za Laravel jer je to samo gomila kompozitora i zanatlija komande.

Prvo ćemo postaviti postavke naše baze podataka u .env datoteci

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

Zatim ćemo postaviti jednu rezervnu rutu koja šalje svaki zahtjev našem kontroleru.

Route::fallback(SessionController::class);

I podesite kontroler da prikazuje broj. Laravel, po defaultu, pohranjuje sesije u bazi podataka. Takođe obezbeđuje session() funkciju za povezivanje s našim podacima o sesiji, tako da je sve što je bilo potrebno bilo je nekoliko redova koda da se prikaže naša stranica.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

Nakon postavljanja php-fpm-a i Nginx-a, naša stranica izgleda prilično 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

Barem dok zaista ne vidimo rezultate testa...

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 nije greška u kucanju. Naša mašina za testiranje je otišla sa 600 zahtjeva u sekundi renderirajući složenu stranicu... na 21 zahtjev u sekundi pri renderiranju "Broj je 1".

Šta je pošlo po zlu? Da li nešto nije u redu sa našom PHP instalacijom? Da li se Nginx nekako usporava kada se povezuje sa php-fpmom?

Čisti PHP

Hajde da ponovimo ovu stranicu u čistom PHP kodu.

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

Sada smo koristili 98 linija koda da uradimo ono što su četiri reda koda (i čitav niz konfiguracionih radova) u Laravelu uradile. (Naravno, ako bismo radili pravilno rukovanje greškama i poruke koje su okrenute korisniku, ovo bi bilo otprilike dvostruko veći broj redova.) Možda bismo mogli postići 30 zahtjeva u sekundi?

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! Izgleda da ipak nema ništa loše u našoj PHP instalaciji. Čista PHP verzija radi 700 zahtjeva u sekundi.

Ako ništa nije u redu sa PHP-om, možda smo pogrešno konfigurisali Laravel?

Revisiting Laravel

Nakon pretraživanja weba u potrazi za konfiguracijskim problemima i savjetima o performansama, dvije od najpopularnijih tehnika bile su keširanje podataka o konfiguraciji i rutiranju kako bi se izbjegla njihova obrada za svaki zahtjev. Stoga ćemo poslušati njihove savjete i isprobati ove savjete.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Sve izgleda dobro na komandnoj liniji. Hajde da ponovimo 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

Pa, sada smo povećali performanse sa 21.04 na 28.80 zahtjeva u sekundi, dramatično povećanje od skoro 37%! Ovo bi bilo prilično impresivno za bilo koji softverski paket... osim činjenice da još uvijek radimo samo 1/24 od broja zahtjeva čiste PHP verzije.

Ako mislite da nešto mora da nije u redu sa ovim testom, trebalo bi da razgovarate sa autorom Lucinda PHP framework-a. U svojim rezultatima testa jeste Lucinda pobijedi Laravela za 36x za HTML zahtjeve i 90x za JSON zahtjeve.

Nakon testiranja na vlastitoj mašini sa Apacheom i Nginxom, nemam razloga da sumnjam u njega. Laravel je zaista pravedan to polako! PHP sam po sebi nije tako loš, ali kada jednom dodate svu dodatnu obradu koju Laravel dodaje svakom zahtjevu, onda mi je vrlo teško preporučiti Laravel kao izbor u 2023. godini.

Django

PHP/Wordpress računi za oko 40% svih web stranica na webu , što ga čini daleko najdominantnijim okvirom. Osobno, ipak, smatram da se popularnost ne mora nužno pretvoriti u kvalitetu više nego što osjećam da imam iznenadnu nekontrolisanu potrebu za tom izvanrednom gurmanskom hranom iz najpopularniji restoran na svijetu ... McDonald&#x27;s. Pošto smo već testirali čisti PHP kod, nećemo testirati sam Wordpress, jer bi sve što uključuje Wordpress nesumnjivo bilo manje od 700 zahteva u sekundi koje smo primetili sa čistim PHP-om.

Django je još jedan popularan okvir koji postoji već duže vrijeme. Ako ste ga koristili u prošlosti, vjerovatno se rado sećate njegovog spektakularnog interfejsa za administraciju baze podataka, kao i koliko je dosadno bilo konfigurisati sve baš onako kako ste želeli. Hajde da vidimo koliko dobro Django radi u 2023. godini, posebno sa novim ASGI interfejsom koji je dodao od verzije 4.0.

Postavljanje Djanga je izuzetno slično postavljanju Laravela, jer su oba bila iz doba u kojem su MVC arhitekture bile elegantne i ispravne. Preskočićemo dosadnu konfiguraciju i ići direktno na podešavanje prikaza.

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

Četiri linije koda su iste kao kod Laravel verzije. Pogledajmo kako se ponaša.

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

Uopšte nije loše sa 355 zahtjeva u sekundi. To je samo polovina performansi čiste PHP verzije, ali je takođe 12 puta veća od Laravel verzije. Čini se da Django protiv Laravela uopšte nije takmičenje.

Flask

Osim većih okvira za sve-uključujući-kuhinjski sudoper, postoje i manji okviri koji samo rade neke osnovne postavke, dok vam dozvoljavaju da se nosite s ostalim. Jedan od najboljih za korištenje je Flask i njegov ASGI pandan Quart. Moje PaferaPy Framework je izgrađen na vrhu Flask-a, tako da mi je dobro poznato koliko je lako obaviti stvari uz održavanje performansi.

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

Kao što vidite, Flask skripta je kraća od čiste PHP skripte. Smatram da je od svih jezika koje sam koristio, Python vjerovatno najizrazitiji jezik u smislu pritiska na tipke. Nedostatak zagrada i zagrada, razumijevanja liste i diktata, te blokiranje zasnovano na uvlačenju, a ne na tački i zarezu, čine Python prilično jednostavnim, ali moćnim u svojim mogućnostima.

Nažalost, Python je takođe najsporiji jezik opšte namene, uprkos tome koliko je softvera napisano u njemu. Broj dostupnih Python biblioteka je oko četiri puta veći od sličnih jezika i pokriva ogromnu količinu domena, ali niko ne bi rekao da je Python brz niti efikasan izvan niša kao što je NumPy.

Pogledajmo kako je naša verzija Flask u usporedbi s našim prethodnim okvirima.

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ša Flask skripta je zapravo brža od naše čiste PHP verzije!

Ako ste iznenađeni ovim, trebali biste shvatiti da naša Flask aplikacija radi svu svoju inicijalizaciju i konfiguraciju kada pokrenemo gunicorn server, dok PHP ponovo izvršava skriptu svaki put kada stigne novi zahtjev. ;ekvivalentno je da je Flask mladi, željni taksista koji je već upalio auto i čeka pored puta, dok je PHP stari vozač koji ostaje u svojoj kući čekajući poziv da uđe i tek onda vozi doći po tebe. Budući da sam čovjek iz stare škole i dolazi iz vremena kada je PHP bio divna promjena u običnim HTML i SHTML fajlovima, pomalo je tužno shvatiti koliko je vremena prošlo, ali razlike u dizajnu zaista otežavaju PHP-u takmičite se sa Python, Java i Node.js serverima koji samo ostaju u memoriji i rješavaju zahtjeve s lakoćom žonglera.

Starlette

Flask je možda naš najbrži okvir do sada, ali to je zapravo prilično stari softver. Python zajednica je prešla na novije asihrone ASGI servere pre nekoliko godina, i naravno, i ja sam se prebacio zajedno sa njima.

Najnovija verzija Pafera Framework-a, PaferaPyAsync , baziran je na Starlette. Iako postoji ASGI verzija Flask-a pod nazivom Quart, razlike u performansama između Quart-a i Starlette-a bile su dovoljne da umjesto toga rebaziram svoj kod na Starlette-u.

Asihrono programiranje može biti zastrašujuće za mnoge ljude, ali to zapravo nije težak koncept zahvaljujući Node.js momcima koji su popularizirali koncept prije više od deset godina.

Nekada smo se borili protiv istovremenosti sa višenitnim, multiprocesiranjem, distribuiranim računarstvom, ulančavanjem obećanja i svim onim zabavnim vremenima koja su prerano ostarila i isušila mnoge veterane programera. Sada samo kucamo async ispred naših funkcija i await ispred bilo kojeg koda za koji bi moglo biti potrebno neko vrijeme da se izvrši. Zaista je opširniji od običnog koda, ali je mnogo manje dosadan za korištenje od potrebe da se bavimo primitivima sinhronizacije, prosljeđivanjem poruka i rješavanjem obećanja.

Naš Starlette fajl izgleda ovako:

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

Kao što možete vidjeti, prilično je kopiran i zalijepljen iz naše Flask skripte sa samo nekoliko promjena rutiranja i async/await ključne riječi.

Koliko nam poboljšanja zaista mogu dati kopirani i lijepljeni kodovi?

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 novog šampiona, dame i gospodo! Naš prethodni maksimum je bila naša čista PHP verzija sa 704 zahtjeva u sekundi, koju je potom pretekla naša Flask verzija sa 1080 zahtjeva u sekundi. Naša Starlette skripta uništava sve prethodne kandidate sa 4562 zahteva u sekundi, što znači 6x poboljšanje u odnosu na čisti PHP i 4x poboljšanje u odnosu na Flask.

Ako još niste promijenili svoj WSGI Python kod u ASGI, sada je možda dobro vrijeme za početak.

Node.js/ExpressJS

Do sada smo pokrili samo PHP i Python okvire. Međutim, veliki dio svijeta zapravo koristi Javu, DotNet, Node.js, Ruby on Rails i druge slične tehnologije za svoje web stranice. Ovo nikako nije sveobuhvatan pregled svih svjetskih ekosistema i bioma, tako da ćemo izbjeći programiranje ekvivalenta organskoj hemiji, izabrati samo okvire za koje je najlakše ukucati kod. od kojih Java definitivno nije.

Osim ako se niste skrivali ispod svoje kopije K&amp;R C ili Knuth's Umetnost kompjuterskog programiranja u posljednjih petnaest godina vjerovatno ste čuli za Node.js. Oni od nas koji postoje od početka JavaScripta su ili neverovatno uplašeni, zadivljeni ili oboje stanjem modernog JavaScripta, ali ne može se poreći da je JavaScript postao sila s kojom se treba računati i na serverima kao pretraživači. Na kraju krajeva, čak imamo i izvorne 64-bitne cijele brojeve sada u jeziku! To je daleko bolje od svega što je pohranjeno u 64-bitnim floatovima!

ExpressJS je vjerovatno najlakši Node.js server za korištenje, tako da ćemo napraviti brzu i prljavu aplikaciju Node.js/ExpressJS koja će služiti našem brojaču.

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

Ovaj kod je zapravo bilo lakše napisati od Python verzija, iako izvorni JavaScript postaje prilično nezgrapan kada aplikacije postanu veće, a svi pokušaji da se ovo ispravi, kao što je TypeScript, brzo postaju opširniji od Pythona.

Hajde da vidimo kako se ovo radi!

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žda ste čuli drevne (ionako drevne po internet standardima...) narodne priče o Node.js&#x27; brzine, a te priče su uglavnom istinite zahvaljujući spektakularnom radu koji je Google uradio sa V8 JavaScript motorom. Međutim, u ovom slučaju, iako naša brza aplikacija nadmašuje Flask skriptu, njena jednostruka priroda je poražena od strane četiri asinhronizirana procesa kojima upravlja Starlette Knight koji kaže "Ni!".

Potražimo još 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 │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

U redu! Sada je bitka čak četiri na četiri! Hajde da mjerimo!

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

Još uvijek nije sasvim na nivou Starlette, ali nije loše za brzi petominutni JavaScript hak. Iz mog vlastitog testiranja, ova skripta je zapravo malo zadržana na nivou sučelja baze podataka jer node-postgres nije ni približno tako efikasan kao psycopg za Python. Prebacivanje na sqlite kao drajver baze podataka daje preko 3000 zahtjeva u sekundi za isti ExpressJS kod.

Glavna stvar koju treba napomenuti je da uprkos sporoj brzini izvršavanja Pythona, ASGI okviri zapravo mogu biti konkurentni Node.js rješenjima za određena radna opterećenja.

Rust/Actix

Dakle, sada smo sve bliže vrhu planine, a pod planinom mislim na najviše rezultate koje su zabilježili i miševi i muškarci.

Ako pogledate većinu referentnih vrijednosti okvira dostupnih na webu, primijetit ćete da postoje dva jezika koja imaju tendenciju da dominiraju na vrhu: C++ i Rust. Radio sam sa C++ od 90-ih, i čak sam imao svoj Win32 C++ okvir prije nego što je MFC/ATL postojao, tako da imam dosta iskustva sa jezikom. Nije baš zabavno raditi s nečim kada to već znate, pa ćemo umjesto toga napraviti Rust verziju. ;)

Rust je relativno nov što se tiče programskih jezika, ali postao je predmet znatiželje za mene kada je Linus Torvalds najavio da će prihvatiti Rust kao programski jezik jezgra Linuxa. Za nas starije programere, to je otprilike isto kao da kažemo da će ova nova moderna new age hipi stvarčica biti novi amandman na Ustav SAD-a.

Sada, kada ste iskusan programer, ne uskačete tako brzo kao mlađi ljudi, inače biste se mogli opeći brzim promjenama jezika ili biblioteka. (Svako ko je koristio prvu verziju AngularJS-a znat će o čemu govorim.) Rust je još uvijek donekle u toj eksperimentalnoj fazi razvoja, a smiješno mi je što toliki primjeri koda na webu čak ni ne kompajlirajte više sa trenutnim verzijama paketa.

Međutim, performanse koje pokazuju Rust aplikacije ne mogu se poreći. Ako nikada niste probali ripgrep ili fd-find na velikim stablima izvornog koda, svakako biste ih trebali isprobati. Oni su čak dostupni za većinu Linux distribucija jednostavno iz upravitelja paketa. Razmjenjujete opširnost za performanse sa Rustom... a puno opširnosti za a puno performansi.

Kompletan kod za Rust je malo velik, tako da ćemo ovdje samo pogledati relevantne rukovaoce:

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

Ovo je mnogo komplikovanije od Python/Node.js verzija...

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

I mnogo učinkovitije!

Naš Rust server koji koristi Actix/deadpool_postgres lako nadmašuje našeg prethodnog šampiona Starlette za +125%, ExpressJS za +362%, i čisti PHP za +1366%. (Ostaviću deltu performansi uz Laravel verziju kao vježbu za čitaoca.)

Otkrio sam da je učenje samog Rust jezika bilo teže od drugih jezika jer ima mnogo više problema od bilo čega što sam vidio izvan 6502 Assembly, ali ako vaš Rust server može preuzeti 14x veći broj korisnika kao vaš PHP server, onda možda ipak postoji nešto što se može dobiti sa prebacivanjem tehnologija. Zato će sljedeća verzija Pafera Framework-a biti bazirana na Rustu. Krivulja učenja je mnogo veća od skriptnih jezika, ali učinak će biti vrijedan toga. Ako ne možete odvojiti vrijeme da naučite Rust, onda ni baziranje vašeg tehničkog niza na Starlette ili Node.js nije loša odluka.

Tehnički dug

U posljednjih dvadeset godina prešli smo sa jeftinih statičnih hosting lokacija na dijeljeni hosting sa LAMP stackovima do iznajmljivanja VPS-a na AWS, Azure i druge usluge u oblaku. Danas su mnoge kompanije zadovoljne donošenjem dizajnerskih odluka na osnovu onoga koga mogu naći da je dostupan ili najjeftiniji jer je pojava praktičnih usluga u oblaku olakšala bacanje više hardvera na spore servere i aplikacije. To im je dalo velike kratkoročne dobitke po cijenu dugoročnog tehničkog duga.

Upozorenje generalnog hirurga iz Kalifornije: Ovo nije pravi svemirski pas.

Prije 70 godina bila je velika svemirska utrka između Sovjetskog Saveza i Sjedinjenih Država. Sovjeti su osvojili većinu prvih prekretnica. Imali su prvi satelit u Sputnjiku, prvog psa u svemiru u Lajki, prvu svemirsku letjelicu na Mjesecu u Luni 2, prvog muškarca i ženu u svemiru u Jurija Gagarina i Valentine Tereškove, i tako dalje...

Ali polako su gomilali tehnički dug.

Iako su Sovjeti bili prvi u svakom od ovih dostignuća, njihovi inženjerski procesi i ciljevi su ih naveli da se fokusiraju na kratkoročne izazove, a ne na dugoročnu izvodljivost. Pobjeđivali su svaki put kada su skočili, ali su postajali sve umorniji i sporiji dok su njihovi protivnici nastavili ujednačenim koracima ka cilju.

Jednom kada je Neil Armstrong napravio svoje istorijske korake na Mjesecu na televiziji uživo, Amerikanci su preuzeli vodstvo, a onda su ostali tamo dok je sovjetski program posustajao. Ovo se ne razlikuje od današnjih kompanija koje su se fokusirale na sljedeću veliku stvar, sljedeću veliku isplatu ili sljedeću veliku tehnologiju, a ne uspijevaju razviti odgovarajuće navike i strategije na duge staze.

Biti prvi na tržištu ne znači da ćete postati dominantni igrač na tom tržištu. Alternativno, odvajanje vremena da stvari uradite kako treba ne garantuje uspeh, ali svakako povećava vaše šanse za dugoročna postignuća. Ako ste tehnološki lider za svoju kompaniju, odaberite pravi smjer i alate za svoje radno opterećenje. Ne dozvolite da popularnost zamijeni performanse i efikasnost.

Resursi

Želite da preuzmete 7z datoteku koja sadrži Rust, ExpressJS, Flask, Starlette i Pure PHP skripte?

O autoru

Jim se bavi programiranjem otkako je vratio IBM PS/2 tokom 90-ih. I dan-danas preferira pisanje HTML-a i SQL-a ručno, te se fokusira na efikasnost i korektnost u svom radu.