Da, Virdžinija, postoji * a deda Mraz Razlika između veb okvira u 2023

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

Sadržaj

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

Uvod

Posle jednog od mojih najnovijih intervjua za posao, bio sam iznenađen kada sam shvatio da kompanija za koju sam se prijavio još uvek koristi Laravel, PHP okvir koji sam isprobao pre otprilike deceniju. Bilo je pristojno za to vreme, ali ako postoji jedna konstanta u tehnologiji i modi, to je stalna promena i obnavljanje stilova i koncepata. Ako ste JavaScript programer, verovatno vam je poznat ovaj stari vic

Programer 1: &kuot;Ne sviđa mi se ovaj novi JavaScript okvir!&kuot;

Programer 2: &kuot;Nema potrebe da brinete. Samo sačekajte šest meseci i biće još jedan da ga zameni!&kuot;

Iz radoznalosti, odlučio sam da vidim šta se tačno dešava kada stavimo staro i novo na test. Naravno, veb je pun merila i tvrdnji, od kojih je verovatno najpopularniji TechEmpover Veb Framevork Benchmarks ovde . Ipak, nećemo raditi ništa tako komplikovano kao oni danas. Održaćemo stvari lepim 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 sa čitanjem. Važe uobičajena upozorenja: ovo možda neće funkcionisati isto na vašoj mašini, različite verzije softvera mogu da utiču na performanse, a Šredingerova mačka je zapravo postala zombi mačka koja je bila pola živa i polumrtva u isto vreme.

The Test

Testing Environment

Za ovaj test, koristiću svoj laptop opremljen slabim i5 koji koristi Manjaro Linuk kao što je prikazano ovde.

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

  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 zahteve za ovu stranicu, primetićete jedan koji se zove sessionvars.js koji radi potpuno istu stvar.

Sadržaj sessionvars.js

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

Ako ponovo prikažemo složenu stranicu svaki put kada korisnik to zatraži, možemo da opslužujemo 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 dozvolimo Ngink-u da je brzo baci kroz prozor korisniku, onda možemo opsluživati 32.000 korisnika u sekundi, povećavajući performanse za faktor od 50 puta.

╰─➤  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 indek.en.html je deo koji ide svima, a samo delovi koji se razlikuju po korisniku se šalju u sessionvars.js. Ovo ne samo da smanjuje opterećenje baze podataka i stvara bolje iskustvo za naše korisnike, već i smanjuje kvantne verovatnoće da će naš server spontano ispariti u proboju varp jezgra kada Klingonci napadnu.

Zahtevi koda

Vraćeni kod za svaki okvir će imati jedan jednostavan zahtev: pokazati korisniku koliko puta je osvežio stranicu tako što ćete reći „Broj je k“. Da stvari budu jednostavne, za sada ćemo se kloniti Redis redova, Kubernetes komponenti ili AVS Lambda.

Prikazuje koliko puta ste posetili stranicu

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

Tabela korisničkih sesija

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

Tabela nakon skraćenja

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

Stvarni rezultati testa

PHP/Laravel

U redu, sada konačno možemo da počnemo da prljamo ruke. Preskočićemo podešavanje za Laravel pošto je to samo gomila kompozitora i zanatlija komande.

Prvo ćemo podesiti podešavanja naše baze podataka u datoteci .env

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 zahtev našem kontroloru.

Route::fallback(SessionController::class);

I podesite kontroler da prikaže broj. Laravel, podrazumevano, čuva sesije u bazi podataka. Takođe obezbeđuje session() funkciju za povezivanje sa našim podacima o sesiji, tako da je sve što je bilo potrebno bilo nekoliko redova koda da bi se prikazala 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 podešavanja php-fpm i Ngink-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

Bar 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 štamparska greška. Naša mašina za testiranje je otišla sa 600 zahteva u sekundi pri prikazivanju složene stranice... na 21 zahtev u sekundi pri prikazivanju "Broj je 1".

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

Č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 uradile četiri linije koda (i čitav niz konfiguracionih radova) u Laravelu. (Naravno, ako bismo pravilno obrađivali greške i poruke koje su okrenute korisniku, ovo bi bilo otprilike duplo veći broj redova.) Možda bismo mogli da dostignemo 30 zahteva 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 zahteva u sekundi.

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

Revisiting Laravel

Nakon pretraživanja veba u potrazi za problemima sa konfiguracijom i savetima o performansama, dve od najpopularnijih tehnika bile su keširanje podataka o konfiguraciji i rutiranju kako bi se izbegla njihova obrada za svaki zahtev. Zato ćemo poslušati njihov savet i isprobati ove savete.

╰─➤  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 benčmark.

╰─➤  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 zahteva u sekundi, što je dramatično povećanje od skoro 37%! Ovo bi bilo prilično impresivno za bilo koji softverski paket... osim činjenice da još uvek radimo samo 1/24 od broja zahteva čiste PHP verzije.

Ako mislite da nešto mora da nije u redu sa ovim testom, trebalo bi da razgovarate sa autorom Lucinda PHP okvira. U svojim rezultatima testa, on ima Lucinda pobedila Laravela za 36k za HTML zahteve i 90k za JSON zahteve.

Nakon testiranja na sopstvenoj mašini sa Apache-om i Ngink-om, 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 zahtevu, onda mi je veoma teško preporučiti Laravel kao izbor u 2023.

Django

PHP/Vordpress računi za oko 40% svih veb lokacija na vebu , što ga čini daleko najdominantnijim okvirom. Lično, ipak, smatram da se popularnost ne mora nužno pretvoriti u kvalitet više nego što imam iznenadnu nekontrolisanu želju za tom izuzetnom gurmanskom hranom iz najpopularniji restoran na svetu ... McDonald&#k27;s. Pošto smo već testirali čisti PHP kod, nećemo testirati sam Vordpress, jer bi sve što uključuje Vordpress 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 vreme. Ako ste ga koristili u prošlosti, verovatno 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 2023. godine, posebno sa novim ASGI interfejsom koji je dodao od verzije 4.0.

Podešavanje Django-a je izuzetno slično postavljanju Laravela, jer su oboje bili iz doba kada su MVC arhitekture bile elegantne i ispravne. Preskočićemo dosadnu konfiguraciju i preć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. Hajde da vidimo 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 zahteva u sekundi. To je samo polovina performansi čiste PHP verzije, ali je takođe 12 puta veća od Laravel verzije. Django protiv Laravela izgleda da uopšte nije takmičenje.

Flask

Osim većih okvira za sve-uključujući-kuhinjski sudoper, postoje i manji okviri koji samo rade neka osnovna podešavanja dok vam dozvoljavaju da se nosite sa ostalim. Jedan od najboljih za upotrebu je Flask i njegov ASGI kolega Kuart. Moje PaferaPi Framevork 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, Pithon verovatno najizrazitiji jezik u smislu pritiska na tastere. Nedostatak zagrada i zagrada, razumevanje liste i diktata i blokiranje zasnovano na uvlačenju, a ne na tački i zarezu, čine Pithon prilično jednostavnim, ali moćnim u svojim mogućnostima.

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

Hajde da vidimo kako se naša verzija Flask upoređuje sa 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, trebalo bi da shvatite 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 zahtev. 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 da te pokupim. Budući da je stari đak i dolazi iz vremena kada je PHP bio divna promena u obične HTML i SHTML datoteke, pomalo je tužno shvatiti koliko je vremena prošlo, ali razlike u dizajnu zaista otežavaju PHP-u da takmiče se sa Pithon, Java i Node.js serverima koji samo ostaju u memoriji i obrađuju zahteve sa lakoćom žonglera.

Starlette

Flask je možda naš najbrži okvir do sada, ali to je zapravo prilično stari softver. Pithon 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 okvira, PaferaPiAsinc , zasnovan je na Starlette. Iako postoji ASGI verzija Flask-a koja se zove Kuart, razlike u performansama između Kuart-a i Starlette-a bile su dovoljne da ponovo baziram 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 popularizovali koncept pre više od jedne decenije.

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 programere veterane. Sada samo kucamo async ispred naših funkcija i await ispred bilo kog koda koji bi mogao da potraje da se izvrši. Zaista je opširniji od običnog koda, ali je mnogo manje dosadan za korišćenje od potrebe da se bavite primitivima za sinhronizaciju, prosleđivanjem poruka i rešavanjem obećanja.

Naša Starlette datoteka 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 vidite, prilično je kopirano i nalepljeno iz naše Flask skripte sa samo nekoliko promena rutiranja i async/await ključne reči.

Koliko poboljšanja zaista može da nam pruži kopiranje i lepljenje koda?

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 zahteva u sekundi, koju je zatim pretekla naša Flask verzija sa 1080 zahteva u sekundi. Naša Starlette skripta uništava sve prethodne kandidate sa 4562 zahteva u sekundi, što znači 6k poboljšanje u odnosu na čisti PHP i 4k poboljšanje u odnosu na Flask.

Ako još niste promenili svoj VSGI Pithon kod u ASGI, sada je možda dobro vreme da počnete.

Node.js/EkpressJS

Do sada smo pokrili samo PHP i Pithon okvire. Međutim, veliki deo sveta zapravo koristi Java, DotNet, Node.js, Rubi on Rails i druge takve tehnologije za svoje veb stranice. Ovo nikako nije sveobuhvatan pregled svih svetskih ekosistema i bioma, tako da ćemo da izbegnemo programiranje ekvivalenta organskoj hemiji, izabrati samo okvire za koje je najlakše ukucati kod. od kojih Java definitivno nije.

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

EkpressJS je verovatno najlakši Node.js server za korišćenje, tako da ćemo napraviti brzu i prljavu aplikaciju Node.js/EkpressJS 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 nego u verzijama Pithon-a, iako izvorni JavaScript postaje prilično glomazan kada aplikacije postanu veće, a svi pokušaji da se ovo ispravi, kao što je TipeScript, brzo postaju opširniji od Pithon-a.

Hajde da vidimo kako ovo funkcioniše!

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 (po Internet standardima ionako drevne...) narodne priče o Node.js&#k27; brzina, 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 asinhronizovana procesa kojima upravlja Starlette Knight koji kaže „Ni!“.

Hajde da dobijemo 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 │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

OK! Sada je bitka četiri na četiri! Hajde da merimo!

╰─➤  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š uvek nije sasvim na nivou Starlette, ali nije loše za brzi petominutni JavaScript hak. Na osnovu mog sopstvenog testiranja, ova skripta se zapravo pomalo zadržava na nivou interfejsa baze podataka jer node-postgres nije ni blizu tako efikasan kao psicopg za Pithon. Prelazak na sklite kao drajver baze podataka daje preko 3000 zahteva u sekundi za isti EkpressJS kod.

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

Rust/Actik

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

Ako pogledate većinu referentnih vrednosti okvira dostupnih na vebu, primetić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 Vin32 C++ okvir pre nego što je MFC/ATL postojao, tako da imam dosta iskustva sa jezikom. Nije baš zabavno raditi sa nečim kada to već znate, pa ćemo umesto toga napraviti Rust verziju. ;)

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

Sada, kada ste iskusan programer, skloni ste da ne uskačete tako brzo kao mlađi ljudi, inače biste mogli da se opečete brzim promenama jezika ili biblioteka. (Svako ko je koristio prvu verziju AngularJS-a znaće o čemu govorim.) Rust je još uvek donekle u toj eksperimentalnoj fazi razvoja, i smešno mi je što toliki primeri koda na vebu č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 Linuk distribucija jednostavno iz menadžera paketa. Razmenjujete opširnost za performanse sa Rustom... a lot od mnogoslovlja za a lot performansi.

Kompletan kod za Rust je malo veliki, tako da ćemo ovde 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 verzija Pithon/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

I mnogo efikasnije!

Naš Rust server koji koristi Actik/deadpool_postgres lako je pobedio našeg prethodnog šampiona Starlette za +125%, EkpressJS za +362% i čisti PHP za +1366%. (Ostaviću deltu performansi sa Laravel verzijom kao vež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 video van 6502 Assembli, ali ako vaš Rust server može da preuzme 14 puta veći broj korisnika kao vaš PHP server, onda se možda ipak nešto može dobiti sa prebacivanjem tehnologija. Zato će sledeća verzija Pafera okvira biti zasnovana na Rustu. Krivulja učenja je mnogo veća od jezika za skriptovanje, ali učinak će biti vredan toga. Ako ne možete da odvojite vreme da naučite Rust, onda ni zasnivanje svoje tehničke grupe na Starlette ili Node.js nije loša odluka.

Tehnički dug

U poslednjih dvadeset godina, prešli smo sa jeftinih statičnih hosting lokacija na deljeni hosting sa LAMP stekovima do iznajmljivanja VPS-a na AVS, Azure i druge usluge u oblaku. Danas su mnoge kompanije zadovoljne donošenjem odluka o dizajnu na osnovu onoga koga mogu naći da je dostupan ili najjeftiniji jer je pojava pogodnih usluga u oblaku olakšala bacanje više hardvera na spore servere i aplikacije. Ovo im je dalo velike kratkoročne dobiti po cenu dugoročnog tehničkog duga.

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

Pre 70 godina bila je velika svemirska trka 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 letelicu na Mesecu u Luni 2, prvog čoveka i ženu u svemiru u Juriju Gagarinu i Valentini Tereškovoj, i tako dalje...

Ali oni su polako 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. Pobeđivali su svaki put kada su skočili, ali su postajali sve umorniji i sporiji dok su njihovi protivnici nastavili doslednim koracima ka cilju.

Jednom kada je Nil Armstrong napravio svoje istorijske korake na Mesecu na televiziji uživo, Amerikanci su preuzeli vođstvo, a zatim ostali tamo dok je sovjetski program posustajao. Ovo se ne razlikuje od današnjih kompanija koje su se fokusirale na sledeću veliku stvar, sledeću veliku isplatu ili sledeću veliku tehnologiju dok ne uspevaju da razviju 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 dostignuća. Ako ste tehnološki lider za svoju kompaniju, izaberite pravi pravac i alate za svoje radno opterećenje. Ne dozvolite da popularnost zameni performanse i efikasnost.

Resursi

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

O autoru

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