Sí, Virginia, hi ha * a Pare Noel Diferència entre marcs web el 2023

El viatge d'un programador desafiant per trobar codi de servidor web de rendiment ràpid abans de sucumbir a la pressió del mercat i al deute tècnic
2023-03-24 11:52:06
👁️ 796
💬 0

Continguts

  1. Introducció
  2. La prova
  3. PHP/Laravel
  4. PHP pur
  5. Revisitant Laravel
  6. Django
  7. Flascó
  8. Starlette
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. Deute tècnic
  12. Recursos

Introducció

Després d'una de les meves entrevistes de feina més recents, em va sorprendre adonar-me que l'empresa a la qual vaig sol·licitar encara utilitzava Laravel, un framework PHP que vaig provar fa una dècada. Va ser decent per a l'època, però si hi ha una constant tant en tecnologia com en moda, és el canvi continu i el ressorgiment d'estils i conceptes. Si ets programador de JavaScript, probablement estàs familiaritzat amb aquesta vella broma

Programador 1: "No m'agrada aquest nou marc de JavaScript!"

Programador 2: "No cal preocupar-se. Només espereu sis mesos i n'hi haurà un altre per substituir-lo!"

Per curiositat, vaig decidir veure què passa exactament quan posem a prova vell i nou. Per descomptat, el web està ple de benchmarks i afirmacions, dels quals el més popular és probablement el Punts de referència del marc web de TechEmpower aquí . Tanmateix, avui no farem res tan complicat com ells. Mantendrem les coses agradables i senzilles ambdues perquè aquest article no es converteixi en Guerra i Pau , i que tindreu una lleugera possibilitat de romandre despert quan acabeu de llegir. S'apliquen les advertències habituals: és possible que això no funcioni igual a la vostra màquina, diferents versions de programari poden afectar el rendiment i el gat de Schrödinger es va convertir en un gat zombi que estava mig viu i mig mort al mateix temps.

La prova

Entorn de prova

Per a aquesta prova, faré servir el meu ordinador portàtil armat amb un petit i5 amb Manjaro Linux, tal com es mostra aquí.

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

La tasca a mà

El nostre codi tindrà tres tasques senzilles per a cada sol·licitud:

  1. Llegiu l'identificador de sessió de l'usuari actual des d'una galeta
  2. Carregueu informació addicional d'una base de dades
  3. Retorna aquesta informació a l'usuari

Quina mena de prova idiota és aquesta, us podeu preguntar? Bé, si mireu les sol·licituds de xarxa d'aquesta pàgina, notareu una anomenada sessionvars.js que fa exactament el mateix.

El contingut de sessionvars.js

Ja veieu, les pàgines web modernes són criatures complicades i una de les tasques més habituals és la memòria cau de pàgines complexes per evitar l'excés de càrrega al servidor de bases de dades.

Si tornem a renderitzar una pàgina complexa cada vegada que un usuari la sol·licita, només podrem servir uns 600 usuaris per segon.

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

Però si emmagatzemem aquesta pàgina a la memòria cau com a fitxer HTML estàtic i deixem que Nginx l'enviï ràpidament per la finestra a l'usuari, podrem servir 32.000 usuaris per segon, augmentant el rendiment en un factor de 50 vegades.

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

L'index.en.html estàtic és la part que va a tothom, i només les parts que difereixen per usuari s'envien a sessionvars.js. Això no només redueix la càrrega de la base de dades i crea una millor experiència per als nostres usuaris, sinó que també disminueix les probabilitats quàntiques que el nostre servidor es vaporitzi espontàniament en una incompliment del nucli deformat quan els klingons ataquen.

Requisits del codi

El codi retornat per a cada marc tindrà un requisit senzill: mostrar a l'usuari quantes vegades ha actualitzat la pàgina dient "El nombre és x". Per simplificar les coses, ara ens mantindrem allunyats de les cues de Redis, els components de Kubernetes o les Lambdas d'AWS.

Mostra quantes vegades has visitat la pàgina

Les dades de sessió de cada usuari es desaran en una base de dades PostgreSQL.

La taula de sessions d'usuari

I aquesta taula de base de dades es truncarà abans de cada prova.

La taula després de ser truncada

Simple però efectiu és el lema de Pafera... fora de la línia de temps més fosca de totes maneres...

Els resultats reals de la prova

PHP/Laravel

D'acord, ara per fi podem començar a embrutar-nos les mans. Ens saltarem la configuració de Laravel, ja que només és un grup de compositors i artesans. ordres.

Primer, configurarem la configuració de la nostra base de dades al fitxer .env

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

A continuació, establirem una única ruta alternativa que envia totes les sol·licituds al nostre controlador.

Route::fallback(SessionController::class);
PHP

I configureu el controlador per mostrar el recompte. Laravel, per defecte, emmagatzema les sessions a la base de dades. També proporciona el session() funció per interactuar amb les dades de la nostra sessió, de manera que només calia un parell de línies de codi per representar la nostra pàgina.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}
PHP

Després de configurar php-fpm i Nginx, la nostra pàgina té un aspecte força bo...

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

Almenys fins que vegem realment els resultats de la prova...

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
Shell

No, això no és una errada d'ortografia. La nostra màquina de prova ha passat de 600 sol·licituds per segon que representa una pàgina complexa... a 21 sol·licituds per segon que representa "El nombre és 1".

Aleshores, què va fallar? Hi ha alguna cosa malament amb la nostra instal·lació de PHP? Nginx s'està alentint d'alguna manera quan s'interfaça amb php-fpm?

PHP pur

Referem aquesta pàgina en codi PHP pur.

<?php

// ====================================================================
function uuid4() 
{
  return sprintf(
    '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
    mt_rand(0, 0xffff), mt_rand(0, 0xffff),
    mt_rand(0, 0xffff),
    mt_rand(0, 0x0fff) | 0x4000,
    mt_rand(0, 0x3fff) | 0x8000,
    mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
  );
}

// ====================================================================
function Query($db, $query, $params = [])
{
  $s  = $db->prepare($query);
  
  $s->setFetchMode(PDO::FETCH_ASSOC);
  $s->execute(array_values($params));
  
  return $s;
}

// ********************************************************************
session_start();

$sessionid  = 0;

if (isset($_SESSION['sessionid']))
{
  $sessionid  = $_SESSION['sessionid'];
}

if (!$sessionid)
{
  $sessionid              = uuid4();
  $_SESSION['sessionid']  = $sessionid;
}

$db   = new PDO('pgsql:host=127.0.0.1 dbname=sessiontest user=sessiontest password=sessiontest');
$data = 0;

try
{
  $result = Query(
    $db,
    'SELECT data FROM usersessions WHERE uid = ?',
    [$sessionid]
  )->fetchAll();
  
  if ($result)
  {
    $data = json_decode($result[0]['data'], 1);
  } 
} catch (Exception $e)
{
  echo $e;

  Query(
    $db,
    'CREATE TABLE usersessions(
      uid     TEXT PRIMARY KEY,
      data    TEXT
    )'
  );
}

if (!$data)
{
  $data = ['count'  => 0];
}

$data['count']++;

if ($data['count'] == 1)
{
  Query(
    $db,
    'INSERT INTO usersessions(uid, data)
    VALUES(?, ?)',
    [$sessionid, json_encode($data)]
  );
} else
{
  Query(
    $db,
    'UPDATE usersessions
      SET data = ?
      WHERE uid = ?',
    [json_encode($data), $sessionid]
  );
}

echo 'Count is ' . $data['count'];

PHP

Ara hem utilitzat 98 línies de codi per fer el que van fer quatre línies de codi (i un munt de treballs de configuració) a Laravel. (Per descomptat, si féssim un tractament adequat d'errors i missatges dirigits a l'usuari, això seria aproximadament el doble del nombre de línies.) Potser podrem arribar a 30 sol·licituds per segon?

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
Shell

Va! Sembla que no hi ha res dolent amb la nostra instal·lació de PHP després de tot. La versió pura de PHP fa 700 sol·licituds per segon.

Si no hi ha res dolent amb PHP, potser hem configurat malament Laravel?

Revisitant Laravel

Després de buscar al web problemes de configuració i consells de rendiment, dues de les tècniques més populars van ser emmagatzemar a la memòria cau les dades de configuració i encaminar per evitar processar-les per a cada sol·licitud. Per tant, seguirem els seus consells i provarem aquests consells.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  
Shell

Tot es veu bé a la línia d'ordres. Anem a refer la referència.

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

Bé, ara hem augmentat el rendiment de 21,04 a 28,80 sol·licituds per segon, un augment espectacular de gairebé un 37%! Això seria força impressionant per a qualsevol paquet de programari... excepte pel fet que encara estem fent només 1/24 del nombre de sol·licituds de la versió pura de PHP.

Si penseu que alguna cosa ha d'anar malament amb aquesta prova, hauríeu de parlar amb l'autor del framework Lucinda PHP. En els resultats de les seves proves, ho ha fet Lucinda guanyant a Laravel 36x per a sol·licituds HTML i 90x per a sol·licituds JSON.

Després de provar a la meva pròpia màquina tant amb Apache com amb Nginx, no tinc cap motiu per dubtar-ne. Laravel és realment just això lent! PHP per si sol no és tan dolent, però una vegada que afegiu tot el processament addicional que Laravel afegeix a cada sol·licitud, em resulta molt difícil recomanar Laravel com a opció el 2023.

Django

Comptes de PHP/Wordpress prop del 40% de tots els llocs web del web , convertint-lo amb diferència en el marc més dominant. Personalment, però, trobo que la popularitat no es tradueix necessàriament en qualitat més del que em trobo amb un impuls sobtat incontrolable per aquest extraordinari menjar gurmet de el restaurant més popular del món ... McDonald&#x27;s. Com que ja hem provat codi PHP pur, no provarem Wordpress en si mateix, ja que qualsevol cosa que inclogui Wordpress seria, sens dubte, inferior a les 700 sol·licituds per segon que vam observar amb PHP pur.

Django és un altre framework popular que ha existit durant molt de temps. Si l'heu utilitzat en el passat, probablement recordeu amb afecte la seva espectacular interfície d'administració de bases de dades juntament amb el molest que era configurar-ho tot de la manera que volíeu. Vegem com funciona Django el 2023, especialment amb la nova interfície ASGI que ha afegit a partir de la versió 4.0.

Configurar Django és molt semblant a configurar Laravel, ja que tots dos eren de l'època en què les arquitectures MVC eren elegants i correctes. Ens saltarem la configuració avorrida i passarem directament a configurar la vista.

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

Quatre línies de codi són les mateixes que amb la versió de Laravel. Vegem com funciona.

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

No està gens malament amb 355 peticions per segon. Només és la meitat del rendiment de la versió de PHP pura, però també és 12 vegades superior al de la versió de Laravel. Django vs. Laravel sembla que no hi ha cap competició.

Flascó

A part dels marcs més grans, inclosos els de la pica de la cuina, també hi ha marcs més petits que només fan una configuració bàsica mentre us permeten gestionar la resta. Un dels millors per utilitzar és Flask i el seu homòleg ASGI Quart. El meu Marc PaferaPy està construït a sobre de Flask, de manera que conec bé com de fàcil és fer les coses mantenint el rendiment.

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

Com podeu veure, l'script Flask és més curt que l'script PHP pur. Trobo que de tots els idiomes que he utilitzat, Python és probablement el llenguatge més expressiu pel que fa a les tecles escrites. La manca de claus i parèntesis, la comprensió de llistes i dictats i el bloqueig basat en sagnats en lloc de punts i coma fan que Python sigui bastant senzill però potent en les seves capacitats.

Malauradament, Python també és el llenguatge de propòsit general més lent que hi ha, malgrat la quantitat de programari que s'hi hagi escrit. El nombre de biblioteques de Python disponibles és aproximadament quatre vegades més gran que els idiomes similars i cobreix una gran quantitat de dominis, però ningú diria que Python és ràpid ni eficient fora de nínxols com NumPy.

Vegem com es compara la nostra versió de Flask amb els nostres frameworks anteriors.

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
Python

El nostre script Flask és realment més ràpid que la nostra versió de PHP pura!

Si us sorprèn això, hauríeu d'adonar-vos que la nostra aplicació Flask fa tota la seva inicialització i configuració quan iniciem el servidor gunicorn, mentre que PHP torna a executar l'script cada vegada que arriba una nova sol·licitud.&#x27; equival a que Flask és el taxista jove i impacient que ja ha posat en marxa el cotxe i està esperant al costat de la carretera, mentre que PHP és el vell conductor que es queda a casa seva esperant que arribi una trucada i només després condueix. passar a recollir-te. Com que és un noi de la vella escola i venint dels dies en què PHP va ser un canvi meravellós als fitxers HTML i SHTML senzills, és una mica trist adonar-se de quant de temps ha passat, però les diferències de disseny realment fan que PHP sigui difícil. competir contra els servidors Python, Java i Node.js que només es queden a la memòria i gestionen les sol·licituds amb l'àgil facilitat d'un malabarista.

Starlette

Flask pot ser el nostre marc més ràpid fins ara, però en realitat és un programari força antic. La comunitat Python va canviar als nous servidors ASGI asíncrons fa un parell d'anys i, per descomptat, jo mateix he canviat amb ells.

La versió més recent del Marc Pafera, PaferaPyAsync , es basa en Starlette. Tot i que hi ha una versió ASGI de Flask anomenada Quart, les diferències de rendiment entre Quart i Starlette van ser suficients perquè em tornés a basar el meu codi en Starlette.

La programació asíncrona pot fer por a molta gent, però en realitat no és un concepte difícil gràcies als nois de Node.js que van popularitzar el concepte fa més d'una dècada.

Solíem lluitar contra la concurrència amb multiprocés, multiprocessament, informàtica distribuïda, encadenament de promeses i tots aquells moments divertits que van envellir i dessecar prematurament molts programadors veterans. Ara, només escrivim async davant les nostres funcions i await davant de qualsevol codi que pugui trigar una estona a executar-se. De fet, és més detallat que el codi normal, però molt menys molest d'utilitzar que haver de fer front a les primitives de sincronització, el pas de missatges i la resolució de promeses.

El nostre fitxer Starlette té aquest aspecte:

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

Com podeu veure, està pràcticament copiat i enganxat del nostre script Flask amb només un parell de canvis d'encaminament i el async/await paraules clau.

Quina millora ens pot aportar realment el codi copiar i enganxar?

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
Shell

Tenim un nou campió, senyores i senyors! El nostre màxim anterior va ser la nostra versió PHP pura amb 704 sol·licituds per segon, que després va ser superada per la nostra versió de Flask a 1080 sol·licituds per segon. El nostre script Starlette aixafa tots els contendents anteriors a 4562 sol·licituds per segon, la qual cosa significa una millora de 6 vegades respecte a PHP pur i una millora de 4 vegades respecte a Flask.

Si encara no heu canviat el vostre codi WSGI Python a ASGI, ara pot ser un bon moment per començar.

Node.js/ExpressJS

Fins ara, només hem cobert els frameworks PHP i Python. Tanmateix, una gran part del món utilitza Java, DotNet, Node.js, Ruby on Rails i altres tecnologies per als seus llocs web. No es tracta de cap manera d'una visió general exhaustiva de tots els ecosistemes i biomes del món, de manera que per evitar fer l'equivalent de programació de la química orgànica, triarem només els marcs que siguin més fàcils d'escriure el codi. . del qual Java definitivament no.

Tret que us hàgiu amagat sota la vostra còpia de K&R C o Knuth&#x27;s L'art de la programació d'ordinadors durant els darrers quinze anys, probablement heu sentit parlar de Node.js. Aquells de nosaltres que hem existit des del començament de JavaScript estem increïblement espantats, sorpresos o tots dos davant l'estat del JavaScript modern, però no es pot negar que JavaScript també s'ha convertit en una força a tenir en compte als servidors. com a navegadors. Després de tot, fins i tot tenim nombres enters natius de 64 bits ara a l'idioma! Això és molt millor que tot el que s'emmagatzema en flotadors de 64 bits!

ExpressJS és probablement el servidor Node.js més fàcil d'utilitzar, així que farem una aplicació Node.js/ExpressJS ràpida i bruta per servir el nostre comptador.

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

En realitat, aquest codi era més fàcil d'escriure que les versions de Python, tot i que JavaScript natiu es fa més aviat difícil de manejar quan les aplicacions es fan més grans, i tots els intents de corregir-ho, com ara TypeScript, es tornen ràpidament més detallats que Python.

Vegem com funciona això!

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
Shell

És possible que hagis escoltat contes populars antics (antigues segons els estàndards d'Internet de totes maneres...) sobre Node.js&#x27; velocitat, i aquestes històries són majoritàriament certes gràcies a l'espectacular treball que ha fet Google amb el motor JavaScript V8. En aquest cas, però, tot i que la nostra aplicació ràpida supera l'script Flask, la seva naturalesa d'un sol fil és derrotada pels quatre processos asíncrons utilitzats per Starlette Knight que diu "Ni!".

Anem a rebre més ajuda!

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

D'acord! Ara és una batalla igualada de quatre contra quatre! Fem un punt de referència!

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

Encara no al nivell de Starlette, però no està malament per a un ràpid hack de JavaScript de cinc minuts. A partir de les meves pròpies proves, aquest script s'està retardant una mica a nivell d'interfície de base de dades perquè node-postgres no és tan eficient com psycopg per a Python. El canvi a sqlite com a controlador de base de dades produeix més de 3000 sol·licituds per segon per al mateix codi ExpressJS.

El més important a tenir en compte és que, malgrat la velocitat d'execució lenta de Python, els marcs ASGI poden ser competitius amb les solucions Node.js per a determinades càrregues de treball.

Rust/Actix

Així que ara, ens estem apropant al cim de la muntanya, i per muntanya, em refereixo a les puntuacions de referència més altes registrades per ratolins i homes per igual.

Si mireu la majoria dels punts de referència del marc disponibles al web, notareu que hi ha dos idiomes que solen dominar la part superior: C++ i Rust. He treballat amb C++ des dels anys 90, i fins i tot tenia el meu propi marc Win32 C++ abans que MFC/ATL fos una cosa, així que tinc molta experiència amb el llenguatge. No és gaire divertit treballar amb alguna cosa quan ja ho saps, així que farem una versió Rust. ;)

Rust és relativament nou pel que fa als llenguatges de programació, però es va convertir en un objecte de curiositat per a mi quan Linus Torvalds va anunciar que acceptaria Rust com a llenguatge de programació del nucli de Linux. Per a nosaltres, els programadors més antics, això és més o menys el mateix que dir que aquesta nova cosa hippie de la nova era seria una nova esmena a la Constitució dels Estats Units.

Ara, quan ets un programador experimentat, acostumes a no pujar al carro tan ràpid com ho fan els més joves, o en cas contrari et pots cremar pels canvis ràpids de l'idioma o de les biblioteques. (Qualsevol que utilitzi la primera versió d'AngularJS sabrà de què parlo.) Rust encara es troba una mica en aquesta fase de desenvolupament experimental, i em sembla curiós que tants exemples de codi a la web ni tan sols compilar més amb les versions actuals dels paquets.

Tanmateix, no es pot negar el rendiment que mostren les aplicacions Rust. Si no ho has provat mai ripgrep o fd-trobar a grans arbres de codi font, definitivament hauríeu de donar-los una volta. Fins i tot estan disponibles per a la majoria de distribucions de Linux simplement des del gestor de paquets. Estàs intercanviant verbositat per rendiment amb Rust... a lot de verbositat per a lot de rendiment.

El codi complet de Rust és una mica gran, així que només farem una ullada als controladors rellevants aquí:

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

Això és molt més complicat que les versions de 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
Shell

I molt més rendible!

El nostre servidor Rust que utilitza Actix/deadpool_postgres supera amb facilitat el nostre anterior campió Starlette en +125%, ExpressJS en +362% i PHP pur en +1366%. (Deixaré el delta d'actuació amb la versió de Laravel com a exercici per al lector.)

He descobert que aprendre el llenguatge Rust en si ha estat més difícil que altres idiomes, ja que té molts més problemes que qualsevol cosa que he vist fora de 6502 Assembly, però si el vostre servidor Rust pot agafar 14 vegades el nombre de usuaris com el vostre servidor PHP, llavors potser hi ha alguna cosa a guanyar amb canvis de tecnologia després de tot. És per això que la propera versió del Pafera Framework es basarà en Rust. La corba d'aprenentatge és molt superior a la dels llenguatges de script, però el rendiment valdrà la pena. Si no podeu dedicar el temps a aprendre Rust, basar la vostra pila tecnològica en Starlette o Node.js tampoc no és una mala decisió.

Deute tècnic

En els darrers vint anys, hem passat de llocs d'allotjament estàtic barats a allotjament compartit amb piles LAMP i a llogar VPS a AWS, Azure i altres serveis al núvol. Avui en dia, moltes empreses estan satisfetes amb la presa de decisions de disseny basant-se en qui trobin que estigui disponible o més barat, ja que l'arribada dels còmodes serveis al núvol han fet que sigui fàcil llançar més maquinari a servidors i aplicacions lents. Això els ha donat grans guanys a curt termini a costa del deute tècnic a llarg termini.

Advertència del cirurgià general de Califòrnia: aquest no és un gos espacial real.

Fa 70 anys, hi va haver una gran carrera espacial entre la Unió Soviètica i els Estats Units. Els soviètics van guanyar la majoria de les primeres fites. Van tenir el primer satèl·lit a Sputnik, el primer gos a l'espai a Laika, la primera nau espacial lunar a Luna 2, el primer home i dona a l'espai a Yuri Gagarin i Valentina Tereshkova, etc.

Però a poc a poc van anar acumulant deute tècnic.

Tot i que els soviètics van ser els primers en cadascun d'aquests èxits, els seus processos d'enginyeria i objectius els feien centrar-se en reptes a curt termini més que en la viabilitat a llarg termini. Guanyaven cada vegada que saltaven, però anaven cada cop més cansats i més lents mentre els seus oponents continuaven fent passos constants cap a la meta.

Una vegada que Neil Armstrong va fer els seus passos històrics a la Lluna a la televisió en directe, els nord-americans van prendre el lideratge i després es van quedar allà mentre el programa soviètic va trontollar. Això no és diferent de les empreses actuals que s'han centrat en la propera gran cosa, la propera gran recompensa o la propera gran tecnologia sense desenvolupar hàbits i estratègies adequats a llarg termini.

Ser el primer al mercat no vol dir que us convertireu en el jugador dominant en aquest mercat. Alternativament, prendre el temps per fer les coses bé no garanteix l'èxit, però sens dubte augmenta les teves possibilitats d'assoliments a llarg termini. Si sou el líder tecnològic de la vostra empresa, trieu la direcció i les eines adequades per a la vostra càrrega de treball. No deixis que la popularitat substitueixi el rendiment i l'eficiència.

Recursos

Voleu descarregar un fitxer 7z que contingui els scripts Rust, ExpressJS, Flask, Starlette i Pure PHP?

Sobre l'autor

Jim ha estat programant des que va recuperar un IBM PS/2 durant els anys 90. A dia d'avui, encara prefereix escriure HTML i SQL a mà, i se centra en l'eficiència i la correcció en el seu treball.