Po, Virxhinia, Ka *A* a Santa Claus Dallimi midis Kornizave të Uebit në 2023

Udhëtimi i një programuesi sfidues për të gjetur kodin e serverit të uebit me performancë të shpejtë përpara se ai t'i nënshtrohet presionit të tregut dhe borxhit teknik
2023-03-24 11:52:06
👁️ 777
💬 0

Përmbajtja

  1. Hyrje
  2. Testi
  3. PHP/Laravel
  4. PHP e pastër
  5. Rishikimi i Laravel
  6. Xhango
  7. Balonë
  8. Starleta
  9. Nyja.js/ExpressJS
  10. Rust/Actix
  11. Borxhi Teknik
  12. Burimet

Hyrje

Pas një prej intervistave të mia të punës më të fundit, u befasova kur kuptova se kompania për të cilën aplikova po përdorte ende Laravel, një kornizë PHP që e provova rreth një dekadë më parë. Ishte e mirë për atë kohë, por nëse ekziston një konstante si në teknologji ashtu edhe në modë, është ndryshimi i vazhdueshëm dhe rishfaqja e stileve dhe koncepteve. Nëse jeni një programues JavaScript, me siguri jeni njohur me këtë shaka të vjetër

Programuesi 1: "Nuk më pëlqen kjo kornizë e re JavaScript!"

Programuesi 2: "Nuk ka nevojë për t'u shqetësuar. Prisni vetëm gjashtë muaj dhe do të ketë një tjetër për ta zëvendësuar!"

Nga kurioziteti, vendosa të shoh saktësisht se çfarë ndodh kur vëmë në provë të vjetrën dhe të renë. Sigurisht, ueb-faqja është e mbushur me standarde dhe pretendime, prej të cilave më e popullarizuara është ndoshta ajo Standardet e TechEmpower Web Framework këtu . Megjithatë, ne nuk do të bëjmë asgjë aq të komplikuar sa ata sot. Ne do t'i mbajmë gjërat bukur dhe të thjeshta të dyja, në mënyrë që ky artikull të mos kthehet në Lufta dhe Paqja , dhe se do të keni një shans të vogël për të qëndruar zgjuar deri në momentin që të përfundoni së lexuari. Zbatohen paralajmërimet e zakonshme: kjo mund të mos funksionojë njësoj në kompjuterin tuaj, versione të ndryshme të softuerit mund të ndikojnë në performancën dhe macja e Schrödinger-it në fakt u bë një mace zombie që ishte gjysmë e gjallë dhe gjysmë e vdekur në të njëjtën kohë.

Testi

Mjedisi i Testimit

Për këtë provë, unë do të përdor laptopin tim të armatosur me një i5 të vogël që funksionon Manjaro Linux siç tregohet këtu.

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

Detyra në dorë

Kodi ynë do të ketë tre detyra të thjeshta për secilën kërkesë:

  1. Lexoni ID-në e sesionit të përdoruesit aktual nga një cookie
  2. Ngarkoni informacion shtesë nga një bazë të dhënash
  3. Kthejeni atë informacion tek përdoruesi

Çfarë lloj testi idiot është ai, mund të pyesni? Epo, nëse shikoni kërkesat e rrjetit për këtë faqe, do të vini re një të quajtur sessionvars.js që bën të njëjtën gjë.

Përmbajtja e sessionvars.js

E shihni, faqet moderne të internetit janë krijesa të ndërlikuara dhe një nga detyrat më të zakonshme është ruajtja e faqeve komplekse për të shmangur ngarkesën e tepërt në serverin e bazës së të dhënave.

Nëse e ri-raportojmë një faqe komplekse sa herë që një përdorues e kërkon atë, atëherë mund t'u shërbejmë vetëm rreth 600 përdoruesve në sekondë.

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

Por nëse e ruajmë këtë faqe si një skedar statik HTML dhe lejojmë që Nginx ta hedhë shpejt nga dritarja te përdoruesi, atëherë ne mund t'i shërbejmë 32,000 përdoruesve në sekondë, duke rritur performancën me një faktor prej 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

Static index.en.html është pjesa që shkon te të gjithë dhe vetëm pjesët që ndryshojnë sipas përdoruesit dërgohen në sessionvars.js. Kjo jo vetëm që zvogëlon ngarkesën e bazës së të dhënave dhe krijon një përvojë më të mirë për përdoruesit tanë, por gjithashtu zvogëlon probabilitetet kuantike që serveri ynë të avullojë spontanisht në një deformim të bërthamës kur sulmi Klingons.

Kërkesat e kodit

Kodi i kthyer për çdo kornizë do të ketë një kërkesë të thjeshtë: t'i tregojë përdoruesit sa herë e kanë rifreskuar faqen duke thënë "Numërimi është x". Për t'i mbajtur gjërat të thjeshta, ne do të qëndrojmë larg radhëve të Redis, komponentëve të Kubernetes ose AWS Lambdas për momentin.

Duke treguar se sa herë e keni vizituar faqen

Të dhënat e sesionit të çdo përdoruesi do të ruhen në një bazë të dhënash PostgreSQL.

Tabela e sesioneve të përdoruesve

Dhe kjo tabelë e bazës së të dhënave do të shkurtohet përpara çdo testi.

Tabela pasi është cunguar

E thjeshtë por efektive është motoja e Pafera... gjithsesi jashtë afatit kohor më të errët...

Rezultatet aktuale të testit

PHP/Laravel

Në rregull, kështu që tani më në fund mund të fillojmë t'i bëjmë duart pis. Ne do të kalojmë konfigurimin për Laravel pasi është vetëm një grup kompozitorësh dhe artizanësh komandat.

Së pari, ne do të konfigurojmë cilësimet tona të bazës së të dhënave në skedarin .env

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

Më pas do të vendosim një rrugë të vetme kthyese që dërgon çdo kërkesë te kontrolluesi ynë.

Route::fallback(SessionController::class);

Dhe vendosni kontrolluesin që të shfaqë numërimin. Laravel, si parazgjedhje, ruan seancat në bazën e të dhënave. Ajo gjithashtu ofron session() funksioni për t'u ndërlidhur me të dhënat tona të sesionit, kështu që mjaftuan vetëm disa rreshta kodi për të dhënë faqen tonë.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

Pas konfigurimit të php-fpm dhe Nginx, faqja jonë duket mjaft e mirë...

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

Të paktën derisa të shohim rezultatet e testit...

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

Jo, nuk është një gabim shtypi. Makina jonë e provës ka shkuar nga 600 kërkesa për sekondë duke dhënë një faqe komplekse... në 21 kërkesa për sekondë duke dhënë "Numërimi është 1".

Pra, çfarë shkoi keq? A është diçka e gabuar me instalimin tonë të PHP? A po ngadalësohet disi Nginx kur ndërlidhet me php-fpm?

PHP e pastër

Le ta ribëjmë këtë faqe në kod të pastër PHP.

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

Tani kemi përdorur 98 rreshta kodi për të bërë atë që bënë katër rreshta kodi (dhe një grup i tërë konfigurimi) në Laravel. (Sigurisht, nëse do të bënim trajtimin e duhur të gabimeve dhe mesazhet përballë përdoruesve, ky do të ishte rreth dyfishi i numrit të rreshtave.) Ndoshta mund të arrijmë deri në 30 kërkesa në sekondë?

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

Ua! Duket sikur nuk ka asgjë të keqe me instalimin tonë të PHP në fund të fundit. Versioni i pastër PHP po bën 700 kërkesa në sekondë.

Nëse nuk ka asgjë të keqe me PHP, ndoshta e kemi konfiguruar gabim Laravel?

Rishikimi i Laravel

Pas pastrimit të uebit për problemet e konfigurimit dhe këshillat e performancës, dy nga teknikat më të njohura ishin ruajtja e të dhënave të konfigurimit dhe rrugës në memorie për të shmangur përpunimin e tyre për çdo kërkesë. Prandaj, ne do të marrim këshillat e tyre dhe do t'i provojmë këto këshilla.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Gjithçka duket mirë në vijën e komandës. Le të ribëjmë standardin.

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

Epo, tani kemi rritur performancën nga 21.04 në 28.80 kërkesë për sekondë, një ngritje dramatike prej gati 37%! Kjo do të ishte mjaft mbresëlënëse për çdo paketë softuerike... përveç faktit që ne ende po bëjmë vetëm 1/24 e numrit të kërkesave të versionit të pastër PHP.

Nëse po mendoni se diçka duhet të mos jetë në rregull me këtë test, duhet të flisni me autorin e kornizës së Lucinda PHP. Në rezultatet e testit të tij, ai ka Lucinda mund Laravel me 36x për kërkesat HTML dhe 90x për kërkesat JSON.

Pas testimit në makinën time me Apache dhe Nginx, nuk kam asnjë arsye të dyshoj në të. Laravel është me të vërtetë i drejtë se ngadalë! PHP në vetvete nuk është aq e keqe, por sapo të shtoni të gjithë përpunimin shtesë që Laravel i shton çdo kërkese, atëherë e kam shumë të vështirë të rekomandoj Laravel si një zgjedhje në 2023.

Xhango

Llogaritë PHP/Wordpress për rreth 40% e të gjitha faqeve të internetit në ueb , duke e bërë atë deri tani kornizën më dominuese. Megjithatë, personalisht, unë konstatoj se popullariteti nuk përkthehet domosdoshmërisht në cilësi më shumë sesa e gjej veten duke pasur një dëshirë të papritur të pakontrollueshme për atë ushqim të jashtëzakonshëm gustator nga restoranti më popullor në botë ... McDonald&#x27;s. Meqenëse ne kemi testuar tashmë kodin e pastër PHP, ne nuk do të testojmë vetë Wordpress, pasi çdo gjë që përfshin Wordpress do të ishte padyshim më e ulët se 700 kërkesat në sekondë që kemi vërejtur me PHP të pastër.

Django është një tjetër kornizë popullore që ka ekzistuar për një kohë të gjatë. Nëse e keni përdorur në të kaluarën, me siguri po kujtoni me dashuri ndërfaqen e saj spektakolare të administrimit të bazës së të dhënave, së bashku me atë se sa e bezdisshme ishte konfigurimi i gjithçkaje ashtu siç dëshironit. Le të shohim se sa mirë funksionon Django në 2023, veçanërisht me ndërfaqen e re ASGI që ka shtuar në versionin 4.0.

Vendosja e Django është jashtëzakonisht e ngjashme me konfigurimin e Laravel, pasi të dy ishin nga epoka ku arkitekturat MVC ishin elegante dhe korrekte. Do të kapërcejmë konfigurimin e mërzitshëm dhe do të shkojmë drejtpërdrejt te konfigurimi i pamjes.

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

Katër rreshta kodi janë të njëjta si me versionin Laravel. Le të shohim se si funksionon.

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

Aspak keq me 355 kërkesa në sekondë. Është vetëm gjysma e performancës së versionit të pastër PHP, por është gjithashtu 12 herë më i madh se versioni Laravel. Django kundër Laravel duket se nuk ka fare garë.

Balonë

Përveç kornizave më të mëdha të gjithçkaje, duke përfshirë kornizat e lavamanit të kuzhinës, ka edhe korniza më të vogla që thjesht bëjnë disa konfigurime bazë, ndërsa ju lejojnë të trajtoni pjesën tjetër. Një nga më të mirat për t'u përdorur është Flask dhe homologu i tij ASGI Quart. E imja Korniza PaferaPy është ndërtuar në majë të Flask, kështu që unë jam i njohur mirë se sa e lehtë është të kryhen gjërat duke ruajtur performancën.

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

Siç mund ta shihni, skripti Flask është më i shkurtër se skripti i pastër PHP. Unë zbuloj se nga të gjitha gjuhët që kam përdorur, Python është ndoshta gjuha më shprehëse për sa i përket shtypjes së tasteve. Mungesa e kllapave dhe kllapave, kuptimet e listave dhe diktateve, dhe bllokimi i bazuar në dhëmbëzim dhe jo në pikëpresje e bëjnë Python mjaft të thjeshtë por të fuqishëm në aftësitë e tij.

Fatkeqësisht, Python është gjithashtu gjuha më e ngadaltë për qëllime të përgjithshme atje, pavarësisht se sa shumë softuer është shkruar në të. Numri i bibliotekave Python të disponueshme është rreth katër herë më shumë se gjuhët e ngjashme dhe mbulon një sasi të madhe domenesh, megjithatë askush nuk do të thoshte se Python është i shpejtë dhe as performues jashtë niches si NumPy.

Le të shohim se si versioni ynë i Flask krahasohet me kornizat tona të mëparshme.

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

Skripti ynë Flask është në fakt më i shpejtë se versioni ynë i pastër PHP!

Nëse jeni të befasuar nga kjo, duhet të kuptoni se aplikacioni ynë Flask bën të gjithë inicializimin dhe konfigurimin e tij kur ne nisim serverin gunicorn, ndërsa PHP riekzekuton skriptin sa herë që vjen një kërkesë e re. Është&#x27 ;është ekuivalente me Flask që është taksisti i ri dhe i etur i cili tashmë ka nisur makinën dhe po pret buzë rrugës, ndërsa PHP është shoferi i vjetër që qëndron në shtëpinë e tij duke pritur një telefonatë për të hyrë brenda dhe vetëm më pas vozit. për të marrë ju. Duke qenë një djalë i vjetër i shkollës dhe duke ardhur nga ditët kur PHP ishte një ndryshim i mrekullueshëm për skedarët e thjeshtë HTML dhe SHTML, është pak e trishtueshme të kuptosh se sa kohë ka kaluar, por ndryshimet në dizajn e bëjnë vërtet të vështirë për PHP konkurroni me serverët Python, Java dhe Node.js të cilët thjesht qëndrojnë në kujtesë dhe trajtojnë kërkesat me lehtësinë e shkathët të një xhongleri.

Starleta

Flask mund të jetë kuadri ynë më i shpejtë deri më tani, por në të vërtetë është një softuer mjaft i vjetër. Komuniteti Python kaloi në serverët më të rinj asikronë ASGI disa vjet më parë, dhe natyrisht, unë vetë kam kaluar së bashku me ta.

Versioni më i ri i Pafera Framework, PaferaPyAsync , bazohet në Starlette. Megjithëse ekziston një version ASGI i Flask i quajtur Quart, dallimet e performancës midis Quart dhe Starlette ishin të mjaftueshme që unë të ribazoja kodin tim në Starlette.

Programimi asikron mund të jetë i frikshëm për shumë njerëz, por në të vërtetë nuk është një koncept i vështirë falë djemve të Node.js që popullarizuan konceptin më shumë se një dekadë më parë.

Ne e luftonim konkurrencën me multithreading, multiprocessing, informatikë të shpërndarë, zinxhir premtimesh dhe të gjitha ato kohë argëtuese që plaknin dhe thanë para kohe shumë programues veteranë. Tani, ne thjesht shtypim async përballë funksioneve tona dhe await përballë çdo kodi që mund të marrë pak kohë për t'u ekzekutuar. Është me të vërtetë më shumë fjalë se kodi i zakonshëm, por shumë më pak i bezdisshëm për t'u përdorur se sa duhet të merreni me primitivët e sinkronizimit, kalimin e mesazheve dhe zgjidhjen e premtimeve.

Skedari ynë Starlette duket si ky:

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

Siç mund ta shihni, është shumë e kopjuar dhe ngjitur nga skripti ynë Flask me vetëm disa ndryshime në rrugë dhe async/await fjalë kyçe.

Sa përmirësim mund të na japë vërtet kodi i kopjimit dhe ngjitjes?

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

Kemi një kampion të ri, zonja dhe zotërinj! Niveli ynë i mëparshëm ishte versioni ynë i pastër PHP me 704 kërkesa për sekondë, i cili më pas u tejkalua nga versioni ynë Flask me 1080 kërkesa për sekondë. Skripti ynë Starlette shtyp të gjithë pretendentët e mëparshëm me 4562 kërkesa për sekondë, që do të thotë një përmirësim 6 herë mbi PHP-në e pastër dhe 4 herë përmirësim mbi Flask.

Nëse ende nuk e keni ndryshuar kodin WSGI Python në ASGI, tani mund të jetë një kohë e mirë për të filluar.

Nyja.js/ExpressJS

Deri më tani, ne kemi mbuluar vetëm kornizat PHP dhe Python. Megjithatë, një pjesë e madhe e botës në fakt përdorin Java, DotNet, Node.js, Ruby on Rails dhe teknologji të tjera të tilla për faqet e tyre të internetit. Kjo nuk është aspak një pasqyrë gjithëpërfshirëse e të gjitha ekosistemeve dhe biomeve të botës, kështu që për të shmangur kryerjen e ekuivalentit të programimit të kimisë organike, ne do të zgjedhim vetëm kornizat që janë më të lehta për të shtypur kodin për.. nga të cilat Java nuk është definitivisht.

Nëse nuk keni qenë të fshehur nën kopjen tuaj të K&R C ose Knuth&#x27;s Arti i programimit kompjuterik për pesëmbëdhjetë vitet e fundit, me siguri keni dëgjuar për Node.js. Ata prej nesh që kanë qenë rreth e rrotull që nga fillimi i JavaScript janë ose tepër të frikësuar, të habitur ose të dyja me gjendjen e JavaScript-it modern, por nuk mund të mohohet se JavaScript është bërë një forcë për t'u llogaritur edhe në serverë. si shfletues. Në fund të fundit, ne kemi edhe numra të plotë 64 bit tani në gjuhë! Kjo është shumë më mirë se çdo gjë që ruhet në 64 bit float deri tani!

ExpressJS është ndoshta serveri më i lehtë Node.js për t'u përdorur, kështu që ne do të bëjmë një aplikacion të shpejtë dhe të ndotur Node.js/ExpressJS për t'i shërbyer sportelit tonë.

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

Ky kod ishte në të vërtetë më i lehtë për t'u shkruar sesa versionet e Python, megjithëse JavaScript-i vendas bëhet mjaft i pafuqishëm kur aplikacionet bëhen më të mëdha dhe të gjitha përpjekjet për ta korrigjuar këtë si TypeScript shpejt bëhen më të përfolura se Python.

Le të shohim se si funksionon kjo!

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

Ju mund të keni dëgjuar përralla të lashta (të lashta sipas standardeve të internetit...) për Node.js&#x27; shpejtësia, dhe ato histori janë kryesisht të vërteta falë punës spektakolare që Google ka bërë me motorin V8 JavaScript. Megjithatë, në këtë rast, megjithëse aplikacioni ynë i shpejtë e tejkalon skriptin Flask, natyra e tij e vetme me fillesë mposhtet nga katër proceset asinkronike të përdorura nga Starlette Knight i cili thotë "Ni!".

Le të marrim më shumë ndihmë!

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

në rregull! Tani është një betejë e barabartë katër me katër! Le të bëjmë pikë referimi!

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

Ende jo shumë në nivelin e Starlette, por nuk është keq për një hak të shpejtë të JavaScript pesë minutësh. Nga testimi im, ky skrip në fakt po mbahet pak në nivelin e ndërfaqes së bazës së të dhënave sepse node-postgres nuk është aq efikas sa është psycopg për Python. Kalimi në sqlite si drejtuesi i bazës së të dhënave jep mbi 3000 kërkesa në sekondë për të njëjtin kod ExpressJS.

Gjëja kryesore për t'u theksuar është se pavarësisht shpejtësisë së ngadaltë të ekzekutimit të Python, kornizat ASGI në fakt mund të jenë konkurruese me zgjidhjet Node.js për ngarkesa të caktuara pune.

Rust/Actix

Pra, tani, ne po afrohemi më shumë me majën e malit, dhe me malin, nënkuptoj rezultatet më të larta të standardeve të regjistruara nga minjtë dhe burrat.

Nëse shikoni shumicën e standardeve të kornizës të disponueshme në ueb, do të vini re se ka dy gjuhë që priren të dominojnë në krye: C++ dhe Rust. Unë kam punuar me C++ që nga vitet '90 dhe madje kisha kornizën time Win32 C++ përpara se MFC/ATL të ishte një gjë, kështu që kam shumë përvojë me gjuhën. Nuk është shumë argëtuese të punosh me diçka kur e di tashmë, kështu që ne do të bëjmë një version Rust në vend të kësaj. ;)

Rust është relativisht i ri për sa i përket gjuhëve të programimit, por u bë objekt kurioziteti për mua kur Linus Torvalds njoftoi se do të pranonte Rust si një gjuhë programimi të kernelit Linux. Për ne programuesit më të vjetër, kjo është pothuajse njësoj sikur të thuash se ky hipi i ri i ri i fangled epokës së re do të ishte një amendament i ri i Kushtetutës së SHBA.

Tani, kur ju jeni një programues me përvojë, ju prireni të mos hidheni aq shpejt sa të rinjtë, ose përndryshe mund të digjeni nga ndryshimet e shpejta në gjuhë ose biblioteka. (Çdokush që përdori versionin e parë të AngularJS do ta dijë se për çfarë po flas.) Rust është ende disi në atë fazë të zhvillimit eksperimental dhe më duket qesharake që shumë shembuj kodesh në ueb nuk përpiloni më me versionet aktuale të paketave.

Sidoqoftë, performanca e treguar nga aplikacionet Rust nuk mund të mohohet. Nëse nuk e keni provuar kurrë ripgrep ose fd-gjeni në pemët e mëdha të kodit burimor, patjetër që duhet t'u jepni atyre një rrotullim. Ato janë madje të disponueshme për shumicën e shpërndarjeve Linux thjesht nga menaxheri i paketave. Ju jeni duke e shkëmbyer fjalën me performancë me Rust... a shumë e fjalës për një shumë të performancës.

Kodi i plotë për Rust është paksa i madh, kështu që ne do t'i hedhim një sy mbajtësve përkatës këtu:

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

Kjo është shumë më e ndërlikuar sesa versionet 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

Dhe shumë më performuese!

Serveri ynë Rust duke përdorur Actix/deadpool_postgres mposht me lehtësi kampionin tonë të mëparshëm Starlette me +125%, ExpressJS me +362% dhe PHP të pastër me +1366%. (Unë do ta lë deltën e performancës me versionin Laravel si një ushtrim për lexuesin.)

Kam zbuluar se të mësuarit e gjuhës Rust në vetvete ka qenë më e vështirë se gjuhët e tjera pasi ajo ka shumë më tepër gabime se çdo gjë që kam parë jashtë 6502 Assembly, por nëse serveri juaj Rust mund të marrë 14 herë numrin e përdoruesit si serveri juaj PHP, atëherë ndoshta ka diçka për të fituar me ndërrimin e teknologjive në fund të fundit. Kjo është arsyeja pse versioni i ardhshëm i Pafera Framework do të bazohet në Rust. Kurba e të mësuarit është shumë më e lartë se gjuhët e shkrimit, por performanca do t'ia vlejë. Nëse nuk mund të jepni kohë për të mësuar Rust, atëherë bazimi i grupit tuaj të teknologjisë në Starlette ose Node.js nuk është gjithashtu një vendim i keq.

Borxhi Teknik

Në njëzet vitet e fundit, ne kemi kaluar nga faqet e lira të pritjes statike në pritje të përbashkëta me rafte LAMP në marrjen me qira të VPS-ve në AWS, Azure dhe shërbime të tjera cloud. Në ditët e sotme, shumë kompani janë të kënaqura me marrjen e vendimeve të projektimit bazuar në cilindo që mund ta gjejnë atë të disponueshëm ose më të lirë që nga ardhja e shërbimeve të përshtatshme cloud e kanë bërë të lehtë hedhjen e më shumë harduerit në serverët dhe aplikacionet e ngadalta. Kjo u ka dhënë atyre fitime të mëdha afatshkurtëra në kosto të borxhit teknik afatgjatë.

Paralajmërimi i Kirurgut të Përgjithshëm të Kalifornisë: Ky nuk është një qen i vërtetë hapësinor.

70 vjet më parë, pati një garë të madhe hapësinore midis Bashkimit Sovjetik dhe Shteteve të Bashkuara. Sovjetikët fituan shumicën e momenteve të hershme. Ata kishin satelitin e parë në Sputnik, qenin e parë në hapësirë ​​në Laika, anijen e parë kozmike të hënës në Luna 2, burrin dhe gruan e parë në hapësirë ​​në Yuri Gagarin dhe Valentina Tereshkova, e kështu me radhë...

Por ata po grumbullonin ngadalë borxhin teknik.

Edhe pse sovjetikët ishin të parët për secilën prej këtyre arritjeve, proceset dhe qëllimet e tyre inxhinierike po i bënin ata të përqendroheshin në sfidat afatshkurtra dhe jo në fizibilitetin afatgjatë. Ata fituan sa herë që kërcenin, por po lodheshin dhe po bëheshin më të ngadalshëm, ndërsa kundërshtarët e tyre vazhduan të bënin hapa të vazhdueshëm drejt vijës së finishit.

Sapo Neil Armstrong ndërmori hapat e tij historikë në Hënë në televizion drejtpërdrejt, amerikanët morën drejtimin dhe më pas qëndruan atje ndërsa programi sovjetik u lëkund. Kjo nuk është ndryshe nga kompanitë sot që janë përqendruar në gjënë tjetër të madhe, fitimin tjetër të madh ose teknologjinë tjetër të madhe, ndërsa nuk kanë arritur të zhvillojnë zakone dhe strategji të duhura për një kohë të gjatë.

Të jesh i pari në treg nuk do të thotë se do të bëhesh lojtari dominues në atë treg. Përndryshe, marrja e kohës për të bërë gjërat siç duhet nuk garanton sukses, por sigurisht rrit shanset tuaja për arritje afatgjata. Nëse jeni drejtuesi i teknologjisë për kompaninë tuaj, zgjidhni drejtimin dhe mjetet e duhura për ngarkesën tuaj të punës. Mos lejoni që popullariteti të zëvendësojë performancën dhe efikasitetin.

Burimet

Dëshironi të shkarkoni një skedar 7z që përmban skriptet Rust, ExpressJS, Flask, Starlette dhe Pure PHP?

Rreth Autorit

Jim ka qenë duke programuar që kur mori një IBM PS/2 gjatë viteve '90. Deri më sot, ai ende preferon të shkruajë HTML dhe SQL me dorë, dhe fokusohet në efikasitetin dhe korrektësinë në punën e tij.