Ja, Virginia, der *er* en Julemanden Forskellen mellem Web Frameworks i 2023

En trodsig programmørs rejse for at finde hurtigt fungerende webserverkode, før han bukker under for markedspres og teknisk gæld
2023-03-24 11:52:06
👁️ 781
💬 0

Indhold

  1. Indledning
  2. Testen
  3. PHP/Laravel
  4. Ren PHP
  5. Genbesøger Laravel
  6. Django
  7. Kolbe
  8. Stjernelette
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. Teknisk gæld
  12. Ressourcer

Indledning

Efter en af ​​mine seneste jobsamtaler blev jeg overrasket over at indse, at den virksomhed, jeg ansøgte om, stadig brugte Laravel, en PHP-ramme, som jeg prøvede for omkring et årti siden. Det var anstændigt for tiden, men hvis der er én konstant inden for teknologi og mode, så er det konstant ændring og genoplivning af stilarter og koncepter. Hvis du er en JavaScript-programmør, er du sikkert bekendt med denne gamle joke

Programmer 1: "Jeg kan ikke lide denne nye JavaScript-ramme!"

Programmer 2: "Ingen grund til bekymring. Bare vent seks måneder, og der vil være en anden til at erstatte den!"

Af nysgerrighed besluttede jeg at se præcis, hvad der sker, når vi sætter gammelt og nyt på prøve. Naturligvis er nettet fyldt med benchmarks og påstande, hvoraf den mest populære nok er TechEmpower Web Framework Benchmarks her . Vi kommer dog ikke til at gøre noget nær så kompliceret som dem i dag. Vi vil holde tingene pæne og enkle, både så denne artikel ikke bliver til Krig og Fred , og at du har en lille chance for at holde dig vågen, når du er færdig med at læse. De sædvanlige forbehold gælder: Dette fungerer muligvis ikke ens på din maskine, forskellige softwareversioner kan påvirke ydeevnen, og Schrödinger's kat blev faktisk en zombiekat, der var halvt levende og halvt død på nøjagtig samme tid.

Testen

Testmiljø

Til denne test vil jeg bruge min bærbare computer bevæbnet med en sølle i5, der kører Manjaro Linux som vist her.

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

Opgaven ved hånden

Vores kode vil have tre enkle opgaver for hver anmodning:

  1. Læs den aktuelle brugers sessions-id fra en cookie
  2. Indlæs yderligere information fra en database
  3. Returner disse oplysninger til brugeren

Hvad er det for en idiotisk test, spørger du måske? Tja, hvis du ser på netværksanmodningerne for denne side, vil du bemærke en kaldet sessionvars.js, der gør nøjagtig det samme.

Indholdet af sessionvars.js

Du kan se, moderne websider er komplicerede væsner, og en af ​​de mest almindelige opgaver er at cache komplekse sider for at undgå overbelastning på databaseserveren.

Hvis vi genrenderer en kompleks side hver gang en bruger anmoder om det, så kan vi kun betjene omkring 600 brugere pr. sekund.

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

Men hvis vi cacher denne side som en statisk HTML-fil og lader Nginx hurtigt smide den ud af vinduet til brugeren, så kan vi betjene 32.000 brugere i sekundet, hvilket øger ydeevnen med en faktor på 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

Det statiske index.en.html er den del, der går til alle, og kun de dele, der adskiller sig fra brugeren, sendes i sessionvars.js. Dette reducerer ikke kun databasebelastningen og skaber en bedre oplevelse for vores brugere, men mindsker også kvantesandsynligheden for, at vores server spontant vil fordampe i et warp-kernebrud, når Klingons angriber.

Kodekrav

Den returnerede kode for hver ramme vil have et enkelt krav: vis brugeren, hvor mange gange de har opdateret siden ved at sige "Tæller er x". For at gøre tingene enkle vil vi holde os væk fra Redis-køer, Kubernetes-komponenter eller AWS Lambdas indtil videre.

Viser, hvor mange gange du har besøgt siden

Hver brugers sessionsdata vil blive gemt i en PostgreSQL-database.

Tabellen brugersessioner

Og denne databasetabel vil blive trunkeret før hver test.

Tabellen efter at være blevet afkortet

Simpelt, men effektivt er Paferas motto... uden for den mørkeste tidslinje alligevel...

De faktiske testresultater

PHP/Laravel

Okay, så nu kan vi endelig begynde at få snavsede hænder. Vi springer opsætningen over for Laravel, da det kun er en flok komponister og håndværkere kommandoer.

Først opsætter vi vores databaseindstillinger i .env-filen

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

Så sætter vi én enkelt reserverute, der sender hver anmodning til vores controller.

Route::fallback(SessionController::class);

Og indstil controlleren til at vise tællingen. Laravel gemmer som standard sessioner i databasen. Det giver også session() funktion til at interface med vores sessionsdata, så det eneste, der skulle til, var et par linjer kode for at gengive vores side.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

Efter opsætning af php-fpm og Nginx ser vores side ret godt ud...

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

I hvert fald indtil vi rent faktisk ser testresultaterne...

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

Nej, det er ikke en tastefejl. Vores testmaskine er gået fra 600 anmodninger pr. sekund, der renderer en kompleks side... til 21 anmodninger pr. sekund, der renderer "Tæller er 1".

Så hvad gik galt? Er der noget galt med vores PHP-installation? Er Nginx på en eller anden måde langsommere, når den interfacerer med php-fpm?

Ren PHP

Lad os lave denne side om i ren PHP-kode.

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

Vi har nu brugt 98 linjer kode til at gøre, hvad fire linjer kode (og en hel masse konfigurationsarbejde) i Laravel gjorde. (Selvfølgelig, hvis vi gjorde korrekt fejlhåndtering og brugervendte meddelelser, ville dette være omkring det dobbelte af antallet af linjer.) Måske kan vi nå 30 anmodninger i sekundet?

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

Hov! Det ser ud til, at der ikke er noget galt med vores PHP-installation trods alt. Den rene PHP-version udfører 700 anmodninger i sekundet.

Hvis der ikke er noget galt med PHP, har vi måske fejlkonfigureret Laravel?

Genbesøger Laravel

Efter at have gennemsøgt nettet for konfigurationsproblemer og ydeevnetips, var to af de mest populære teknikker at cache konfigurations- og rutedataene for at undgå at behandle dem for hver anmodning. Derfor vil vi tage deres råd og prøve disse tips.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Alt ser godt ud på kommandolinjen. Lad os gentage 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

Nå, vi har nu øget ydeevnen fra 21,04 til 28,80 anmodninger pr. sekund, en dramatisk stigning på næsten 37 %! Dette ville være ret imponerende for enhver softwarepakke... bortset fra det faktum, at vi stadig kun udfører 1/24 af antallet af anmodninger i den rene PHP-version.

Hvis du tænker, at der må være noget galt med denne test, bør du tale med forfatteren af ​​Lucinda PHP-rammeværket. I sine testresultater har han Lucinda slår Laravel med 36x for HTML-anmodninger og 90x for JSON-anmodninger.

Efter at have testet på min egen maskine med både Apache og Nginx, har jeg ingen grund til at tvivle på ham. Laravel er virkelig bare at langsom! PHP i sig selv er ikke så slemt, men når du først tilføjer al den ekstra behandling, som Laravel tilføjer til hver anmodning, så finder jeg det meget svært at anbefale Laravel som et valg i 2023.

Django

PHP/Wordpress står for omkring 40 % af alle hjemmesider på nettet , hvilket gør det til langt den mest dominerende ramme. Personligt oplever jeg dog, at popularitet ikke nødvendigvis udmønter sig i kvalitet mere, end at jeg pludselig får en ukontrollabel trang til den ekstraordinære gourmetmad fra den mest populære restaurant i verden ... McDonald&#x27;s. Da vi allerede har testet ren PHP-kode, vil vi ikke teste selve Wordpress, da alt, der involverer Wordpress, utvivlsomt ville være lavere end de 700 anmodninger i sekundet, som vi observerede med ren PHP.

Django er en anden populær ramme, der har eksisteret i lang tid. Hvis du har brugt det tidligere, husker du sikkert med glæde dens spektakulære databaseadministrationsgrænseflade sammen med, hvor irriterende det var at konfigurere alt, lige som du ville. Lad os se, hvor godt Django fungerer i 2023, især med den nye ASGI-grænseflade, som den har tilføjet fra version 4.0.

Opsætning af Django ligner bemærkelsesværdigt opsætning af Laravel, da de begge var fra den alder, hvor MVC-arkitekturerne var stilfulde og korrekte. Vi springer den kedelige konfiguration over og går direkte til opsætning af visningen.

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

Fire linjer kode er det samme som med Laravel-versionen. Lad os se, hvordan det fungerer.

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

Slet ikke dårligt med 355 anmodninger i sekundet. Det er kun halvdelen af ​​ydeevnen af ​​den rene PHP-version, men den er også 12 gange så stor som Laravel-versionen. Django vs. Laravel ser ikke ud til at være nogen konkurrence overhovedet.

Kolbe

Udover de større alt-inklusive-køkkenvask-rammer, er der også mindre rammer, der bare laver nogle grundlæggende opsætninger, mens du lader dig klare resten. En af de bedste at bruge er Flask og dens ASGI-modstykke Quart. Min egen PaferaPy Framework er bygget oven på Flask, så jeg er godt bekendt med, hvor nemt det er at få tingene gjort og samtidig bevare ydeevnen.

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

Som du kan se, er Flask-scriptet kortere end det rene PHP-script. Jeg finder ud af, at af alle de sprog, jeg har brugt, er Python nok det mest udtryksfulde sprog med hensyn til tastetryk. Mangel på klammeparenteser og parenteser, liste- og diktatforståelser og blokering baseret på indrykning i stedet for semikolon gør Python ret simpel, men alligevel kraftfuld i sine muligheder.

Desværre er Python også det langsomste sprog til generelle formål derude, på trods af hvor meget software der er skrevet i det. Antallet af tilgængelige Python-biblioteker er omkring fire gange mere end tilsvarende sprog og dækker en stor mængde domæner, men ingen vil sige, at Python er hurtig eller performant uden for nicher som NumPy.

Lad os se, hvordan vores Flask-version sammenlignes med vores tidligere rammer.

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

Vores Flask-script er faktisk hurtigere end vores rene PHP-version!

Hvis du er overrasket over dette, bør du være klar over, at vores Flask-app udfører hele sin initialisering og konfiguration, når vi starter gunicorn-serveren, mens PHP genudfører scriptet, hver gang der kommer en ny anmodning. Det svarer til, at Flask er den unge, ivrige taxachauffør, der allerede har startet bilen og venter ved siden af ​​vejen, mens PHP er den gamle chauffør, der bliver hjemme hos ham og venter på, at der kommer et opkald og først derefter kører over for at hente dig. Som en old school fyr og kommer fra de dage, hvor PHP var en vidunderlig ændring til almindelige HTML- og SHTML-filer, er det lidt trist at indse, hvor meget tid der er gået, men designforskellene gør det virkelig svært for PHP at konkurrere mod Python-, Java- og Node.js-servere, der bare forbliver i hukommelsen og håndterer anmodninger med en jonglørs smidige lethed.

Stjernelette

Flask er måske vores hurtigste framework indtil videre, men det er faktisk ret gammel software. Python-fællesskabet skiftede til de nyere asynkrone ASGI-servere for et par år tilbage, og jeg har selvfølgelig selv skiftet sammen med dem.

Den nyeste version af Pafera Framework, PaferaPyAsync , er baseret på Starlette. Selvom der er en ASGI-version af Flask kaldet Quart, var præstationsforskellene mellem Quart og Starlette nok til, at jeg rebaserede min kode på Starlette i stedet.

Asynkron programmering kan være skræmmende for mange mennesker, men det er faktisk ikke et svært koncept takket være Node.js-fyrene, der populariserede konceptet for over ti år siden.

Vi plejede at bekæmpe samtidighed med multithreading, multiprocessing, distribueret computing, love chaining og alle de sjove tider, der for tidligt ældede og udtørrede mange veteranprogrammører. Nu skriver vi bare async foran vores funktioner og await foran enhver kode, der kan tage et stykke tid at udføre. Det er faktisk mere omfattende end almindelig kode, men meget mindre irriterende at bruge end at skulle beskæftige sig med synkroniseringsprimitiver, meddelelsesoverførsel og løsning af løfter.

Vores Starlette-fil ser sådan ud:

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

Som du kan se, er det stort set kopieret og indsat fra vores Flask-script med kun et par routingændringer og async/await søgeord.

Hvor meget forbedring kan kopiering og indsat kode virkelig give os?

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

Vi har en ny mester, mine damer og herrer! Vores tidligere højdepunkt var vores rene PHP-version med 704 anmodninger i sekundet, som derefter blev overhalet af vores Flask-version med 1080 anmodninger i sekundet. Vores Starlette-script knuser alle tidligere konkurrenter med 4562 anmodninger i sekundet, hvilket betyder en 6x forbedring i forhold til ren PHP og 4x forbedring i forhold til Flask.

Hvis du ikke har ændret din WSGI Python-kode til ASGI endnu, kan det være et godt tidspunkt at starte nu.

Node.js/ExpressJS

Indtil videre har vi kun dækket PHP- og Python-frameworks. Men en stor del af verden bruger faktisk Java, DotNet, Node.js, Ruby on Rails og andre sådanne teknologier til deres hjemmesider. Dette er på ingen måde en omfattende oversigt over alle verdens økosystemer og biomer, så for at undgå at lave programmeringsækvivalenten til organisk kemi, vælger vi kun de rammer, der er nemmest at skrive kode til. . som Java bestemt ikke er.

Medmindre du har gemt dig under din kopi af K&amp;R C eller Knuth&#x27;s Kunsten at programmere computer i de sidste femten år har du sikkert hørt om Node.js. De af os, der har eksisteret siden begyndelsen af ​​JavaScript, er enten utroligt bange, forbløffede eller begge dele over tilstanden af ​​moderne JavaScript, men der kan ikke benægtes, at JavaScript også er blevet en kraft, man skal regne med på servere som browsere. Når alt kommer til alt, har vi endda native 64 bit heltal nu i sproget! Det er langt bedre end alt, der er lagret i 64 bit flydere!

ExpressJS er nok den nemmeste Node.js-server at bruge, så vi laver en hurtig og beskidt Node.js/ExpressJS-app for at betjene vores tæller.

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

Denne kode var faktisk lettere at skrive end Python-versionerne, selvom native JavaScript bliver ret uhåndterligt, når applikationer bliver større, og alle forsøg på at rette dette, såsom TypeScript, bliver hurtigt mere omfattende end Python.

Lad os se, hvordan dette fungerer!

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

Du har måske hørt ældgamle (ældgamle efter internetstandarder alligevel...) folkeeventyr om Node.js&#x27; hastighed, og de historier er for det meste sande takket være det spektakulære arbejde, som Google har udført med V8 JavaScript-motoren. I dette tilfælde, selvom vores hurtige app udkonkurrerer Flask-scriptet, bliver dens enkelttrådede natur besejret af de fire asynkroniseringsprocesser, som udføres af Starlette Knight, der siger "Ni!".

Lad os få noget mere hjælp!

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

Okay! Nu er det en lige fire mod fire kamp! Lad os benchmarke!

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

Stadig ikke helt på niveau med Starlette, men det er ikke dårligt for et hurtigt fem minutters JavaScript-hack. Fra min egen test er dette script faktisk blevet holdt lidt tilbage på databasegrænsefladeniveauet, fordi node-postgres ikke er nær så effektiv som psycopg er for Python. Skift til sqlite som databasedriver giver over 3000 anmodninger pr. sekund for den samme ExpressJS-kode.

Det vigtigste at bemærke er, at på trods af den langsomme udførelseshastighed af Python, kan ASGI-frameworks faktisk være konkurrencedygtige med Node.js-løsninger til visse arbejdsbelastninger.

Rust/Actix

Så nu kommer vi tættere på toppen af ​​bjerget, og med bjerg mener jeg de højeste benchmark-score, der er registreret af både mus og mænd.

Hvis du ser på de fleste af referencerammerne, der er tilgængelige på nettet, vil du bemærke, at der er to sprog, der har tendens til at dominere toppen: C++ og Rust. Jeg har arbejdet med C++ siden 90'erne, og jeg havde endda mit eget Win32 C++ framework tilbage, før MFC/ATL var en ting, så jeg har stor erfaring med sproget. Det er ikke særlig sjovt at arbejde med noget, når du allerede ved det, så vi laver en Rust-version i stedet for. ;)

Rust er relativt nyt, hvad angår programmeringssprog, men det blev en genstand for nysgerrighed for mig, da Linus Torvalds annoncerede, at han ville acceptere Rust som et Linux-kerneprogrammeringssprog. For os ældre programmører er det omtrent det samme som at sige, at denne nye fanglede new age hippie-ting ville være en ny ændring af den amerikanske forfatning.

Nu, når du er en erfaren programmør, har du en tendens til ikke at hoppe med på vognen så hurtigt som de yngre mennesker gør, ellers kan du blive brændt af hurtige ændringer i sproget eller bibliotekerne. (Enhver, der brugte den første version af AngularJS, ved, hvad jeg taler om.) Rust er stadig lidt i det eksperimentelle udviklingsstadium, og jeg synes, det er sjovt, at så mange kodeeksempler på nettet ikke engang kompiler længere med aktuelle versioner af pakker.

Den ydeevne, som Rust-applikationer viser, kan dog ikke nægtes. Hvis du aldrig har prøvet ripgrep eller fd-find ude på store kildekodetræer, bør du helt sikkert give dem en tur. De er endda tilgængelige for de fleste Linux-distributioner blot fra pakkehåndteringen. Du udveksler ordlyd for ydeevne med Rust... a masse af ordlyd for en masse af ydeevne.

Den komplette kode for Rust er lidt stor, så vi tager lige et kig på de relevante handlere her:

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

Dette er meget mere kompliceret end Python/Node.js-versionerne...

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

Og meget mere performant!

Vores Rust-server, der bruger Actix/deadpool_postgres, slår vores tidligere mester Starlette med +125%, ExpressJS med +362% og ren PHP med +1366%. (Jeg forlader præstationsdeltaet med Laravel-versionen som en øvelse for læseren.)

Jeg har fundet ud af, at det har været sværere at lære Rust-sproget i sig selv end andre sprog, da det har mange flere gotchas end noget andet, jeg har set uden for 6502 Assembly, men hvis din Rust-server kan klare 14 gange antallet af brugere som din PHP-server, så er der måske alligevel noget at hente ved at skifte teknologi. Derfor vil den næste version af Pafera Framework være baseret på Rust. Indlæringskurven er meget højere end scriptsprog, men ydeevnen vil være det værd. Hvis du ikke kan bruge tid på at lære Rust, så er det heller ikke en dårlig beslutning at basere din tech-stack på Starlette eller Node.js.

Teknisk gæld

I de sidste tyve år er vi gået fra billige statiske hostingsider til delt hosting med LAMP-stacks til at leje VPS'er til AWS, Azure og andre cloud-tjenester. I dag er mange virksomheder tilfredse med at træffe designbeslutninger baseret på hvem de kan finde, der er tilgængeligt eller billigst, da fremkomsten af ​​praktiske cloud-tjenester har gjort det nemt at kaste mere hardware på langsomme servere og applikationer. Dette har givet dem store kortsigtede gevinster på bekostning af langsigtet teknisk gæld.

California Surgeon Generals advarsel: Dette er ikke en rigtig rumhund.

For 70 år siden var der et stort rumkapløb mellem Sovjetunionen og USA. Sovjet vandt de fleste af de tidlige milepæle. De havde den første satellit i Sputnik, den første hund i rummet i Laika, det første månerumfartøj i Luna 2, den første mand og kvinde i rummet i Yuri Gagarin og Valentina Tereshkova, og så videre...

Men de akkumulerede langsomt teknisk gæld.

Selvom Sovjet var først til hver af disse præstationer, fik deres tekniske processer og mål dem til at fokusere på kortsigtede udfordringer snarere end langsigtede gennemførlighed. De vandt hver gang de sprang, men de blev mere trætte og langsommere, mens deres modstandere fortsatte med at tage konsekvente skridt mod målstregen.

Da Neil Armstrong tog sine historiske skridt på månen på direkte tv, tog amerikanerne føringen og blev derefter der, mens det sovjetiske program vaklede. Dette er ikke anderledes end virksomheder i dag, der har fokuseret på den næste store ting, den næste store gevinst eller den næste store teknologi, mens de ikke har udviklet ordentlige vaner og strategier på længere sigt.

At være den første på markedet betyder ikke, at du bliver den dominerende spiller på det marked. Alternativt, at tage sig tid til at gøre tingene rigtigt garanterer ikke succes, men øger bestemt dine chancer for langsigtede præstationer. Hvis du er den tekniske leder for din virksomhed, skal du vælge den rigtige retning og værktøjer til din arbejdsbyrde. Lad ikke popularitet erstatte ydeevne og effektivitet.

Ressourcer

Vil du downloade en 7z-fil, der indeholder Rust-, ExpressJS-, Flask-, Starlette- og Pure PHP-scripts?

Om forfatteren

Jim har programmeret, siden han fik en IBM PS/2 tilbage i 90'erne. Den dag i dag foretrækker han stadig at skrive HTML og SQL i hånden og fokuserer på effektivitet og korrekthed i sit arbejde.