Ja, Virginia, det *finns* en Jultomten Skillnaden mellan webbramar 2023

En trotsig programmerares resa för att hitta snabbpresterande webbserverkod innan han ger efter för marknadstrycket och tekniska skulder
2023-03-24 11:52:06
👁️ 791
💬 0

Innehåll

  1. Introduktion
  2. Testet
  3. PHP/Laravel
  4. Ren PHP
  5. Återbesök Laravel
  6. Django
  7. Kolv
  8. Starlette
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. Teknisk skuld
  12. Resurser

Introduktion

Efter en av mina senaste anställningsintervjuer blev jag förvånad över att inse att företaget som jag sökte till fortfarande använde Laravel, ett PHP-ramverk som jag provade för ungefär ett decennium sedan. Det var anständigt för den tiden, men om det finns en konstant i både teknik och mode, är det ständig förändring och återuppbyggnad av stilar och koncept. Om du är en JavaScript-programmerare är du förmodligen bekant med det här gamla skämtet

Programmerare 1: "Jag gillar inte det här nya JavaScript-ramverket!"

Programmerare 2: "Du behöver inte oroa dig. Vänta bara i sex månader så kommer det att finnas en till att ersätta den!"

Av nyfikenhet bestämde jag mig för att se exakt vad som händer när vi sätter gammalt och nytt på prov. Naturligtvis är webben fylld med riktmärken och påståenden, varav den mest populära förmodligen är TechEmpower Web Framework Benchmarks här . Vi kommer dock inte att göra något i närheten av så komplicerat som dem idag. Vi kommer att hålla saker och ting snyggt och enkelt både så att den här artikeln inte blir till Krig och fred , och att du har en liten chans att hålla dig vaken när du är klar med att läsa. De vanliga varningarna gäller: detta kanske inte fungerar likadant på din maskin, olika programvaruversioner kan påverka prestandan, och Schrödinger's katt blev faktiskt en zombiekatt som var halvt levande och halvdöd på exakt samma gång.

Testet

Testmiljö

För det här testet kommer jag att använda min bärbara dator beväpnad med en ynklig i5 som kör Manjaro Linux som visas här.

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

Uppgiften till hands

Vår kod kommer att ha tre enkla uppgifter för varje begäran:

  1. Läs den aktuella användarens sessions-ID från en cookie
  2. Ladda ytterligare information från en databas
  3. Returnera den informationen till användaren

Vad är det för ett idiotiskt test kanske du undrar? Tja, om du tittar på nätverksförfrågningarna för den här sidan kommer du att märka en som heter sessionvars.js som gör exakt samma sak.

Innehållet i sessionvars.js

Du förstår, moderna webbsidor är komplicerade varelser, och en av de vanligaste uppgifterna är att cachelagra komplexa sidor för att undvika överbelastning på databasservern.

Om vi ​​återrenderar en komplex sida varje gång en användare begär det, kan vi bara betjäna cirka 600 användare per 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 om vi cachelagrar den här sidan som en statisk HTML-fil och låter Nginx snabbt kasta ut den genom fönstret till användaren, då kan vi betjäna 32 000 användare per sekund, vilket ökar prestandan med en faktor 50x.

╰─➤  wrk -d 10s -t 4 -c 100 http://127.0.0.1/system/index.en.html
Running 10s test @ http://127.0.0.1/system/index.en.html
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.03ms  511.95us   6.87ms   68.10%
    Req/Sec     8.20k     1.15k   28.55k    97.26%
  327353 requests in 10.10s, 2.36GB read
Requests/sec:  32410.83
Transfer/sec:    238.99MB

Den statiska index.en.html är den del som går till alla, och endast de delar som skiljer sig åt per användare skickas i sessionvars.js. Detta minskar inte bara databasbelastningen och skapar en bättre upplevelse för våra användare, utan minskar också kvantsannolikheterna för att vår server spontant kommer att förångas i ett warp-kärnbrott när Klingons attackerar.

Kodkrav

Den returnerade koden för varje ramverk kommer att ha ett enkelt krav: visa användaren hur många gånger de har uppdaterat sidan genom att säga "Räkna är x". För att göra det enkelt håller vi oss borta från Redis-köer, Kubernetes-komponenter eller AWS Lambdas tills vidare.

Visar hur många gånger du har besökt sidan

Varje användares sessionsdata kommer att sparas i en PostgreSQL-databas.

Användarsessionstabellen

Och denna databastabell kommer att trunkeras före varje test.

Tabellen efter att ha trunkerats

Enkelt men effektivt är Paferas motto... utanför den mörkaste tidslinjen i alla fall...

De faktiska testresultaten

PHP/Laravel

Okej, så nu kan vi äntligen börja smutsa ner händerna. Vi hoppar över installationen för Laravel eftersom det bara är ett gäng kompositörer och hantverkare kommandon.

Först ställer vi in ​​våra databasinställningar i .env-filen

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

Sedan kommer vi att ställa in en enda reservrutt som skickar varje begäran till vår styrenhet.

Route::fallback(SessionController::class);

Och ställ in kontrollen för att visa antalet. Laravel lagrar som standard sessioner i databasen. Det ger också session() funktion för att samverka med vår sessionsdata, så allt som behövdes var ett par rader kod för att rendera vår sida.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

Efter att ha ställt in php-fpm och Nginx ser vår sida ganska bra ut...

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

Åtminstone tills vi faktiskt ser testresultaten...

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 är inget stavfel. Vår testmaskin har gått från 600 förfrågningar per sekund, vilket renderar en komplex sida... till 21 förfrågningar per sekund, vilket ger "Antalet är 1".

Så vad gick fel? Är det något fel med vår PHP-installation? Saknar Nginx på något sätt ner när det gränssnitt med php-fpm?

Ren PHP

Låt oss göra om den här sidan i ren PHP-kod.

<?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 använt 98 rader kod för att göra vad fyra rader kod (och en hel massa konfigurationsarbete) gjorde i Laravel. (Naturligtvis, om vi gjorde korrekt felhantering och användarinriktade meddelanden, skulle detta vara ungefär dubbelt så många rader.) Kanske kan vi göra det till 30 förfrågningar per sekund?

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

Oj! Det verkar som att det trots allt inte är något fel på vår PHP-installation. Den rena PHP-versionen gör 700 förfrågningar per sekund.

Om det inte är något fel med PHP, kanske vi har felkonfigurerat Laravel?

Återbesök Laravel

Efter att ha letat igenom webben efter konfigurationsproblem och prestandatips var två av de mest populära teknikerna att cachelagra konfigurations- och ruttdata för att undvika att bearbeta dem för varje begäran. Därför kommer vi att ta deras råd och prova dessa tips.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Allt ser bra ut på kommandoraden. Låt oss göra om riktmärket.

╰─➤  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åväl, vi har nu ökat prestandan från 21,04 till 28,80 begäran per sekund, en dramatisk ökning med nästan 37 %! Detta skulle vara ganska imponerande för alla programpaket... förutom det faktum att vi fortfarande bara gör 1/24 av antalet förfrågningar för den rena PHP-versionen.

Om du tänker att något måste vara fel med det här testet, bör du prata med författaren till Lucinda PHP-ramverket. I sina testresultat har han Lucinda slår Laravel med 36x för HTML-förfrågningar och 90x för JSON-förfrågningar.

Efter att ha testat på egen maskin med både Apache och Nginx har jag ingen anledning att tvivla på honom. Laravel är verkligen bara att långsam! PHP i sig är inte så illa, men när du väl lägger till all extra bearbetning som Laravel lägger till varje begäran, då har jag väldigt svårt att rekommendera Laravel som ett val 2023.

Django

PHP/Wordpress står för cirka 40 % av alla webbplatser på webben , vilket gör det till det överlägset mest dominerande ramverket. Personligen tycker jag dock att popularitet inte nödvändigtvis översätts till kvalitet lika mycket som att jag plötsligt får en okontrollerbar längtan efter den där extraordinära gourmetmaten från den mest populära restaurangen i världen ... McDonald&#x27;s. Eftersom vi redan har testat ren PHP-kod, kommer vi inte att testa själva Wordpress, eftersom allt som involverar Wordpress utan tvekan skulle vara lägre än de 700 förfrågningar per sekund som vi observerade med ren PHP.

Django är ett annat populärt ramverk som har funnits länge. Om du har använt det tidigare, minns du förmodligen med glädje dess spektakulära databasadministrationsgränssnitt tillsammans med hur irriterande det var att konfigurera allt precis som du ville. Låt oss se hur bra Django fungerar 2023, speciellt med det nya ASGI-gränssnittet som det har lagt till från och med version 4.0.

Att sätta upp Django är anmärkningsvärt likt att sätta upp Laravel, eftersom de båda var från en ålder där MVC-arkitekturer var snygga och korrekta. Vi hoppar över den tråkiga konfigurationen och går direkt till att ställa in vyn.

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

Fyra rader kod är samma som med Laravel-versionen. Låt oss se hur det fungerar.

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

Inte illa alls med 355 förfrågningar per sekund. Det är bara halva prestandan för den rena PHP-versionen, men den är också 12 gånger högre än Laravel-versionen. Django vs. Laravel verkar inte vara någon tävling alls.

Kolv

Förutom de större ramverken för allt-inklusive-diskbänken, finns det också mindre ramar som bara gör lite grundläggande inställningar samtidigt som du kan hantera resten. En av de bästa att använda är Flask och dess ASGI-motsvarighet Quart. Min egen PaferaPy Framework är byggd ovanpå Flask, så jag är väl insatt i hur lätt det är att få saker gjorda samtidigt som prestanda bibehålls.

#!/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 är Flask-skriptet kortare än det rena PHP-skriptet. Jag tycker att av alla språk som jag har använt är Python förmodligen det mest uttrycksfulla språket när det gäller tangenttryckningar. Brist på klammerparenteser och parenteser, list- och diktförståelser och blockering baserad på indrag snarare än semikolon gör Python ganska enkel men ändå kraftfull i sina möjligheter.

Tyvärr är Python också det långsammaste allmänna språket som finns, trots hur mycket mjukvara som har skrivits i det. Antalet tillgängliga Python-bibliotek är ungefär fyra gånger fler än liknande språk och täcker en stor mängd domäner, men ingen skulle säga att Python är snabb eller presterande utanför nischer som NumPy.

Låt oss se hur vår Flask-version kan jämföras med våra tidigare ramverk.

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

Vårt Flask-skript är faktiskt snabbare än vår rena PHP-version!

Om du är förvånad över detta bör du inse att vår Flask-app gör all sin initiering och konfiguration när vi startar upp gunicorn-servern, medan PHP kör om skriptet varje gång en ny begäran kommer in. Det motsvarar att Flask är den unga, ivriga taxichauffören som redan har startat bilen och väntar bredvid vägen, medan PHP är den gamla chauffören som stannar hemma och väntar på att ett samtal ska komma in och först därefter kör över för att hämta dig. Eftersom jag är en gammal skolkille och kommer från tiden då PHP var en underbar förändring till vanliga HTML- och SHTML-filer, är det lite tråkigt att inse hur mycket tid som har gått, men designskillnaderna gör det verkligen svårt för PHP att tävla mot Python-, Java- och Node.js-servrar som bara stannar i minnet och hanterar förfrågningar med en jonglörs smidiga lätthet.

Starlette

Flask kan vara vårt snabbaste ramverk hittills, men det är faktiskt ganska gammal programvara. Python-communityt bytte till de nyare asynkrona ASGI-servrarna för ett par år sedan, och självklart har jag själv bytt med dem.

Den senaste versionen av Pafera Framework, PaferaPyAsync , är baserad på Starlette. Även om det finns en ASGI-version av Flask som heter Quart, var prestandaskillnaderna mellan Quart och Starlette tillräckliga för att jag skulle bygga om min kod på Starlette istället.

Asynkron programmering kan vara skrämmande för många människor, men det är faktiskt inte ett svårt koncept tack vare att Node.js-killarna populariserade konceptet för över ett decennium sedan.

Vi brukade bekämpa samtidighet med multithreading, multiprocessing, distribuerad datoranvändning, löfteskedja och alla dessa roliga tider som i förtid åldrade och uttorkade många veteranprogrammerare. Nu skriver vi bara async framför våra funktioner och await framför någon kod som kan ta ett tag att köra. Det är verkligen mer utförligt än vanlig kod, men mycket mindre irriterande att använda än att behöva hantera synkroniseringsprimitiver, meddelandeförmedling och lösa löften.

Vår Starlette-fil ser ut så här:

#!/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 är det i stort sett kopierat och inklistrat från vårt Flask-skript med bara ett par routingändringar och async/await nyckelord.

Hur mycket förbättring kan kopiering och inklistrad kod verkligen ge oss?

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 mästare, mina damer och herrar! Vårt tidigare högsta var vår rena PHP-version med 704 förfrågningar per sekund, som sedan övertogs av vår Flask-version med 1080 förfrågningar per sekund. Vårt Starlette-skript krossar alla tidigare utmanare med 4562 förfrågningar per sekund, vilket innebär en 6x förbättring jämfört med ren PHP och 4x förbättring jämfört med Flask.

Om du inte har ändrat din WSGI Python-kod till ASGI än, kan det vara ett bra tillfälle att börja nu.

Node.js/ExpressJS

Hittills har vi bara täckt PHP- och Python-ramverk. Men en stor del av världen använder faktiskt Java, DotNet, Node.js, Ruby on Rails och andra sådana tekniker för sina webbplatser. Detta är inte på något sätt en heltäckande översikt över alla världens ekosystem och biomer, så för att undvika att programmera motsvarigheten till organisk kemi väljer vi bara de ramverk som är lättast att skriva kod för. . av vilket Java definitivt inte är.

Såvida du inte har gömt dig under ditt exemplar av K&R C eller Knuth&#x27;s Konsten att programmera under de senaste femton åren har du förmodligen hört talas om Node.js. De av oss som har funnits sedan början av JavaScript är antingen otroligt rädda, förvånade eller båda över tillståndet för modern JavaScript, men det går inte att förneka att JavaScript har blivit en kraft att räkna med på servrar också som webbläsare. När allt kommer omkring har vi till och med infödda 64-bitars heltal nu i språket! Det är mycket bättre än allt som lagras i 64-bitars flyter överlägset!

ExpressJS är förmodligen den enklaste Node.js-servern att använda, så vi kommer att göra en snabb och smutsig Node.js/ExpressJS-app för att betjäna vår disk.

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

Den här koden var faktiskt lättare att skriva än Python-versionerna, även om inbyggt JavaScript blir ganska otympligt när applikationer blir större, och alla försök att korrigera detta, såsom TypeScript, blir snabbt mer omfattande än Python.

Låt oss se hur detta fungerar!

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 kanske har hört uråldriga (urgamla med internetstandarder i alla fall...) folksagor om Node.js&#x27; hastighet, och de berättelserna är mest sanna tack vare det spektakulära arbete som Google har gjort med V8 JavaScript-motorn. Men i det här fallet, även om vår snabbapp överträffar Flask-skriptet, besegras dess enstrådade natur av de fyra asynkroniseringsprocesser som utövas av Starlette Knight som säger "Ni!".

Låt oss få lite mer 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 │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

Okej! Nu är det en jämn fyra mot fyra strid! Låt oss jämföra!

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

Fortfarande inte riktigt på Starlette-nivå, men det är inte illa för ett snabbt fem minuters JavaScript-hack. Från min egen testning hålls det här skriptet faktiskt tillbaka lite på databasgränssnittsnivån eftersom node-postgres inte är i närheten av så effektivt som psycopg är för Python. Att byta till sqlite som databasdrivrutin ger över 3000 förfrågningar per sekund för samma ExpressJS-kod.

Det viktigaste att notera är att trots den långsamma exekveringshastigheten för Python kan ASGI-ramverk faktiskt vara konkurrenskraftiga med Node.js-lösningar för vissa arbetsbelastningar.

Rust/Actix

Så nu närmar vi oss toppen av berget, och med berg menar jag de högsta riktmärken som registrerats av både möss och män.

Om du tittar på de flesta ramverksriktmärken som finns tillgängliga på webben kommer du att märka att det finns två språk som tenderar att dominera toppen: C++ och Rust. Jag har arbetat med C++ sedan 90-talet, och jag hade till och med mitt eget Win32 C++-ramverk innan MFC/ATL var en grej, så jag har mycket erfarenhet av språket. Det är inte så kul att jobba med något när du redan kan det, så vi ska göra en Rust-version istället. ;)

Rust är relativt nytt när det gäller programmeringsspråk, men det blev ett föremål för nyfikenhet för mig när Linus Torvalds meddelade att han skulle acceptera Rust som ett Linux-kärnprogrammeringsspråk. För oss äldre programmerare är det ungefär detsamma som att säga att den här nya nygamla hippiegrejen kommer att bli ett nytt tillägg till den amerikanska konstitutionen.

Nu, när du är en erfaren programmerare, tenderar du att inte hoppa på tåget så snabbt som de yngre gör, annars kan du bli bränd av snabba förändringar av språket eller biblioteken. (Alla som använde den första versionen av AngularJS kommer att veta vad jag pratar om.) Rust är fortfarande något i det experimentella utvecklingsstadiet, och jag tycker det är roligt att så många kodexempel på webben inte ens kompilera längre med aktuella versioner av paket.

Prestandan som visas av Rust-applikationer kan dock inte förnekas. Om du aldrig har provat ripgrep eller fd-hitta ute på stora källkodsträd bör du definitivt ge dem en snurr. De är till och med tillgängliga för de flesta Linux-distributioner helt enkelt från pakethanteraren. Du byter ut detaljnivå mot prestanda med Rust... a massa av mångfald för a massa av prestanda.

Den fullständiga koden för Rust är lite stor, så vi tar bara en titt på de relevanta hanterarna här:

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

Detta är mycket mer komplicerat än Python/Node.js-versionerna...

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

Och mycket mer presterande!

Vår Rust-server som använder Actix/deadpool_postgres slår vår tidigare mästare Starlette med +125%, ExpressJS med +362% och ren PHP med +1366%. (Jag lämnar prestationsdeltat med Laravel-versionen som en övning för läsaren.)

Jag har upptäckt att det har varit svårare att lära sig Rust-språket i sig än andra språk eftersom det har många fler gotchas än något annat jag har sett utanför 6502 Assembly, men om din Rust-server kan ta emot 14 gånger antalet användare som din PHP-server, så kanske det trots allt finns något att vinna med att byta teknik. Det är därför nästa version av Pafera Framework kommer att baseras på Rust. Inlärningskurvan är mycket högre än skriptspråk, men prestandan kommer att vara värd det. Om du inte kan lägga tid på att lära dig Rust, så är det inte heller ett dåligt beslut att basera din tekniska stack på Starlette eller Node.js.

Teknisk skuld

Under de senaste tjugo åren har vi gått från billiga statiska värdwebbplatser till delad värd med LAMP-stackar till att hyra VPS:er till AWS, Azure och andra molntjänster. Nuförtiden är många företag nöjda med att fatta designbeslut baserat på vem de kan hitta som är tillgänglig eller billigast eftersom tillkomsten av bekväma molntjänster har gjort det enkelt att kasta mer hårdvara på långsamma servrar och applikationer. Detta har gett dem stora kortsiktiga vinster på bekostnad av långfristiga tekniska skulder.

California Surgeon Generals varning: Det här är inte en riktig rymdhund.

För 70 år sedan var det en stor rymdkapplöpning mellan Sovjetunionen och USA. Sovjet vann de flesta av de tidiga milstolparna. De hade den första satelliten i Sputnik, den första hunden i rymden i Laika, den första månfarkosten i Luna 2, den första mannen och kvinnan i rymden i Yuri Gagarin och Valentina Tereshkova, och så vidare...

Men de ackumulerade sakta tekniska skulder.

Även om sovjeterna var först med var och en av dessa prestationer, fick deras tekniska processer och mål dem att fokusera på kortsiktiga utmaningar snarare än långsiktiga genomförbarhet. De vann varje gång de hoppade, men de blev tröttare och långsammare medan deras motståndare fortsatte att ta konsekventa steg mot mållinjen.

När Neil Armstrong väl tog sina historiska steg på månen i direktsänd tv tog amerikanerna ledningen och stannade sedan där när det sovjetiska programmet vacklade. Detta är inte annorlunda än företag idag som har fokuserat på nästa stora sak, nästa stora vinst eller nästa stora teknik samtidigt som de inte lyckats utveckla ordentliga vanor och strategier för det långa loppet.

Att vara först på marknaden betyder inte att du kommer att bli den dominerande aktören på den marknaden. Alternativt, att ta sig tid att göra saker rätt garanterar inte framgång, men ökar verkligen dina chanser till långsiktiga prestationer. Om du är den tekniska ledaren för ditt företag, välj rätt riktning och verktyg för din arbetsbörda. Låt inte popularitet ersätta prestanda och effektivitet.

Resurser

Vill du ladda ner en 7z-fil som innehåller skripten Rust, ExpressJS, Flask, Starlette och Pure PHP?

Om författaren

Jim har programmerat sedan han fick en IBM PS/2 på 90-talet. Än idag föredrar han fortfarande att skriva HTML och SQL för hand, och fokuserar på effektivitet och korrekthet i sitt arbete.