Jā, Virdžīnija, tur *Ir* a Ziemassvētku vecītis Atšķirība starp tīmekļa ietvariem 2023. gadā

Viena izaicinoša programmētāja ceļojums, lai atrastu ātras darbības tīmekļa servera kodu, pirms viņš pakļaujas tirgus spiedienam un tehniskajiem parādiem
2023-03-24 11:52:06
👁️ 800
💬 0

Saturs

  1. Ievads
  2. Pārbaude
  3. PHP/Laravel
  4. Tīrs PHP
  5. Atkārtoti apmeklējot Laravelu
  6. Django
  7. Kolba
  8. Starlette
  9. Node.js/ExpressJS
  10. Rūsa/Aktix
  11. Tehniskais parāds
  12. Resursi

Ievads

Pēc vienas no savām pēdējām darba intervijām es biju pārsteigts, kad sapratu, ka uzņēmums, kuram pieteicos, joprojām izmanto Laravel — PHP sistēmu, kuru izmēģināju pirms aptuveni desmit gadiem. Tajā laikā tas bija pieklājīgs, taču, ja ir viena nemainīga tehnoloģija un mode, tā ir nepārtraukta stilu un koncepciju maiņa un atjaunošanās. Ja esat JavaScript programmētājs, iespējams, esat iepazinies ar šo veco joku.

Programmētājs 1: "Man nepatīk šī jaunā JavaScript ietvars!"

2. programmētājs: “Nav jāuztraucas. Vienkārši pagaidiet sešus mēnešus, un to nomainīs cits!"

Ziņkārības pēc nolēmu paskatīties, kas tieši notiek, kad mēs pārbaudām veco un jauno. Protams, tīmeklis ir piepildīts ar etaloniem un apgalvojumiem, no kuriem vispopulārākais, iespējams, ir TechEmpower Web Framework etaloni šeit . Tomēr mēs nedarīsim neko tik sarežģītu kā viņi šodien. Mēs saglabāsim lietas jaukas un vienkāršas, lai šis raksts nepārvērstos par Karš un miers , un ka jums būs neliela iespēja palikt nomodā, kad pabeigsit lasīt. Ir spēkā parastie brīdinājumi: tas var nedarboties vienādi jūsu datorā, dažādas programmatūras versijas var ietekmēt veiktspēju, un Šrēdingera kaķis faktiski kļuva par zombiju kaķi, kurš bija pa pusei dzīvs un pa pusei miris tieši tajā pašā laikā.

Pārbaude

Testēšanas vide

Šim testam es izmantošu savu klēpjdatoru, kas ir bruņots ar niecīgu i5, kurā darbojas Manjaro Linux, kā parādīts šeit.

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

Pašreizējais uzdevums

Mūsu kodā katram pieprasījumam būs trīs vienkārši uzdevumi:

  1. Lasīt pašreizējā lietotāja sesijas ID no sīkfaila
  2. Ielādējiet papildu informāciju no datu bāzes
  3. Atgrieziet šo informāciju lietotājam

Kas tas par idiotisku testu, jūs varētu jautāt? Ja skatāties uz tīkla pieprasījumus šai lapai, pamanīsit vienu ar nosaukumu sessionvars.js, kas veic tieši to pašu.

Sesijasvars.js saturs

Redziet, mūsdienu tīmekļa lapas ir sarežģītas būtnes, un viens no visizplatītākajiem uzdevumiem ir sarežģītu lapu saglabāšana kešatmiņā, lai izvairītos no pārmērīgas datu bāzes servera slodzes.

Ja mēs atkārtoti renderējam sarežģītu lapu katru reizi, kad lietotājs to pieprasa, mēs varam apkalpot tikai aptuveni 600 lietotāju 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

Bet, ja mēs saglabājam šo lapu kešatmiņā kā statisku HTML failu un ļaujam Nginx ātri izmest to lietotājam pa logu, mēs varam apkalpot 32 000 lietotāju sekundē, palielinot veiktspēju 50 reizes.

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

Statiskā index.en.html ir daļa, kas tiek izmantota visiem, un failā sessionvars.js tiek nosūtītas tikai tās daļas, kas atšķiras atkarībā no lietotāja. Tas ne tikai samazina datu bāzes noslodzi un rada labāku pieredzi mūsu lietotājiem, bet arī samazina kvantu varbūtību, ka mūsu serveris spontāni iztvaiko deformācijas kodola pārkāpuma gadījumā, kad klingoni uzbrūk.

Koda prasības

Katrai ietvaram atgrieztajam kodam būs viena vienkārša prasība: parādiet lietotājam, cik reižu viņš ir atsvaidzinājis lapu, sakot: "Count ir x". Lai lietas būtu vienkāršas, mēs pagaidām izvairīsimies no Redis rindām, Kubernetes komponentiem vai AWS Lambdas.

Tiek rādīts, cik reižu esat apmeklējis lapu

Katra lietotāja sesijas dati tiks saglabāti PostgreSQL datu bāzē.

Lietotāju sesiju tabula

Un šī datu bāzes tabula tiks saīsināta pirms katras pārbaudes.

Tabula pēc saīsināšanas

Vienkāršs, bet efektīvs ir Pafera devīze... jebkurā gadījumā ārpus tumšākās laika skalas...

Faktiskie testa rezultāti

PHP/Laravel

Labi, tagad mēs beidzot varam sākt smērēt rokas. Mēs izlaidīsim Laravel iestatīšanu, jo tas ir tikai komponistu un amatnieku bars. komandas.

Pirmkārt, mēs iestatīsim datu bāzes iestatījumus .env failā

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

Pēc tam mēs iestatīsim vienu rezerves maršrutu, kas katru pieprasījumu nosūta mūsu kontrolierim.

Route::fallback(SessionController::class);

Un iestatiet kontrolieri, lai parādītu skaitu. Laravel pēc noklusējuma saglabā sesijas datu bāzē. Tas arī nodrošina session() funkciju, lai saskartos ar mūsu sesijas datiem, tāpēc mūsu lapas renderēšanai bija nepieciešamas tikai dažas koda rindiņas.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

Pēc php-fpm un Nginx iestatīšanas mūsu lapa izskatās diezgan labi...

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

Vismaz līdz brīdim, kad mēs redzēsim testa rezultātus...

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

Nē, tā nav drukas kļūda. Mūsu testa iekārta ir mainījusies no 600 pieprasījumiem sekundē, renderējot sarežģītu lapu... līdz 21 pieprasījumiem sekundē, renderējot "Skaits ir 1".

Kas tad nogāja greizi? Vai kaut kas nav kārtībā ar mūsu PHP instalāciju? Vai Nginx kaut kā palēninās, saskaroties ar php-fpm?

Tīrs PHP

Pārtaisīsim šo lapu tīrā 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'];

Tagad mēs esam izmantojuši 98 koda rindiņas, lai paveiktu to, ko Laravel darīja četras koda rindiņas (un vesela virkne konfigurācijas darbu). (Protams, ja mēs veiktu pareizu kļūdu apstrādi un lietotāju ziņojumus, tas būtu apmēram divreiz lielāks rindu skaits.) Varbūt mēs varam sasniegt 30 pieprasījumus 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

Čau! Šķiet, ka ar mūsu PHP instalāciju nav nekā slikta. Tīrā PHP versija veic 700 pieprasījumus sekundē.

Ja ar PHP nav nekā slikta, varbūt mēs nepareizi konfigurējām Laravel?

Atkārtoti apmeklējot Laravelu

Pēc konfigurācijas problēmu un veiktspējas padomu meklēšanas tīmeklī divi no populārākajiem paņēmieniem bija konfigurācijas un maršruta datu saglabāšana kešatmiņā, lai izvairītos no to apstrādes katram pieprasījumam. Tāpēc mēs ņemsim vērā viņu padomus un izmēģināsim šos padomus.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Komandrindā viss izskatās labi. Pārtaisīsim etalonu.

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

Tagad esam palielinājuši veiktspēju no 21.04 līdz 28.80 pieprasījumiem sekundē, kas ir dramatisks pieaugums par gandrīz 37%! Tas būtu diezgan iespaidīgi jebkurai programmatūras pakotnei... izņemot to, ka mēs joprojām izpildām tikai 1/24 daļa no tīrās PHP versijas pieprasījumu skaita.

Ja domājat, ka ar šo testu kaut kas nav kārtībā, jums vajadzētu runāt ar Lucinda PHP ietvara autoru. Savā testa rezultātos viņš ir Lūsinda pārspēj Laravelu par 36x HTML pieprasījumiem un 90x JSON pieprasījumiem.

Pēc testēšanas savā mašīnā gan ar Apache, gan ar Nginx, man nav iemesla par viņu šaubīties. Laravels patiešām ir taisnīgs ka lēni! PHP pati par sevi nav tik slikta, taču, pievienojot visu papildu apstrādi, ko Laravel pievieno katram pieprasījumam, man ir ļoti grūti ieteikt Laravel kā izvēli 2023. gadā.

Django

PHP/Wordpress konti aptuveni 40% no visām tīmekļa vietnēm , padarot to par dominējošo sistēmu. Tomēr personīgi es uzskatu, ka popularitāte ne vienmēr izpaužas kvalitātē, tāpat kā man ir pēkšņa nekontrolējama vēlme pēc šī neparastā gardēžu ēdiena. populārākais restorāns pasaulē ... McDonald&#x27; Tā kā mēs jau esam pārbaudījuši tīru PHP kodu, mēs nepārbaudīsim pašu Wordpress, jo viss, kas saistīts ar Wordpress, neapšaubāmi būtu mazāks par 700 pieprasījumiem sekundē, ko novērojām ar tīru PHP.

Django ir vēl viens populārs ietvars, kas pastāv jau ilgu laiku. Ja esat to izmantojis agrāk, iespējams, ar prieku atceraties tās iespaidīgo datu bāzes administrēšanas saskarni, kā arī to, cik kaitinoši bija visu konfigurēt tieši tā, kā vēlaties. Apskatīsim, cik labi Django darbojas 2023. gadā, īpaši ar jauno ASGI saskarni, kas ir pievienota no 4.0. versijas.

Django iestatīšana ir ļoti līdzīga Laravel iestatīšanai, jo abi bija no laikmeta, kad MVC arhitektūras bija stilīgas un pareizas. Mēs izlaidīsim garlaicīgo konfigurāciju un pāriesim uzreiz pie skata iestatīšanas.

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

Četras koda rindiņas ir tādas pašas kā Laravel versijai. Redzēsim, kā tas darbojas.

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

Nav slikti ar 355 pieprasījumiem sekundē. Tā ir tikai puse no tīrās PHP versijas veiktspējas, taču tā ir arī 12 reizes lielāka nekā Laravel versijai. Šķiet, ka Django pret Laravelu vispār nav sacensība.

Kolba

Papildus lielākajiem visu veidu ietvariem, tostarp virtuves izlietnes karkasiem, ir arī mazāki ietvari, kas veic tikai dažus pamata iestatījumus, vienlaikus ļaujot jums rīkoties ar pārējo. Viens no labākajiem lietošanai ir Flask un tā ASGI līdzinieks Quart. Manas pašas PaferaPy ietvars ir uzcelta uz Flask, tāpēc es labi pārzinu, cik viegli ir paveikt lietas, vienlaikus saglabājot veiktspēju.

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

Kā redzat, Flask skripts ir īsāks nekā tīrais PHP skripts. Es atklāju, ka no visām manis izmantotajām valodām Python, iespējams, ir izteiksmīgākā valoda ievadīto taustiņu nospiešanas ziņā. Iekavu un iekavu, sarakstu un diktātu izpratnes trūkums un bloķēšana, pamatojoties uz atkāpi, nevis semikolu, padara Python diezgan vienkāršu, bet tajā pašā laikā jaudīgu.

Diemžēl Python ir arī lēnākā vispārējas nozīmes valoda, neskatoties uz to, cik daudz programmatūras tajā ir rakstīts. Pieejamo Python bibliotēku skaits ir aptuveni četras reizes lielāks nekā līdzīgās valodās un aptver milzīgu daudzumu domēnu, tomēr neviens nevarētu teikt, ka Python ir ātrs vai veiktspēja ārpus tādām nišām kā NumPy.

Apskatīsim, kā mūsu Flask versija ir salīdzināma ar mūsu iepriekšējiem ietvariem.

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

Mūsu Flask skripts patiesībā ir ātrāks nekā mūsu tīrā PHP versija!

Ja esat par to pārsteigts, jums vajadzētu saprast, ka mūsu lietotne Flask veic visu tās inicializāciju un konfigurēšanu, kad mēs startējam gunicorn serveri, savukārt PHP atkārtoti izpilda skriptu katru reizi, kad tiek saņemts jauns pieprasījums. Tas ir līdzvērtīgs tam, ka Flask ir jaunais, dedzīgs taksists, kurš jau ir iedarbinājis automašīnu un gaida blakus ceļam, savukārt PHP ir vecs šoferis, kurš paliek savā mājā un gaida zvanu un tikai tad brauc. aizbraukt, lai tevi uzņemtu. Esot vecskolas puisis un nāk no laikiem, kad PHP bija brīnišķīga pārmaiņa uz vienkāršiem HTML un SHTML failiem, ir mazliet skumji apzināties, cik daudz laika ir pagājis, taču dizaina atšķirības patiešām apgrūtina PHP konkurē ar Python, Java un Node.js serveriem, kuri vienkārši paliek atmiņā un apstrādā pieprasījumus ar žongliera veiklo vieglumu.

Starlette

Flask varētu būt mūsu līdz šim ātrākā sistēma, taču patiesībā tā ir diezgan veca programmatūra. Python kopiena dažus gadus atpakaļ pārgāja uz jaunākiem asinhronajiem ASGI serveriem, un, protams, arī es pats esmu pārgājis ar tiem.

Pafera Framework jaunākā versija, PaferaPyAsync , ir balstīta uz Starlette. Lai gan ir Flask ASGI versija ar nosaukumu Quart, ar veiktspējas atšķirībām starp Quart un Starlette pietika, lai es tā vietā izmantotu savu kodu Starlette.

Asinhronā programmēšana var būt biedējoša daudziem cilvēkiem, taču patiesībā tā nav grūta koncepcija, pateicoties Node.js puišiem, kuri šo koncepciju popularizēja pirms vairāk nekā desmit gadiem.

Mēs cīnījāmies pret vienlaicīgumu ar daudzpavedienu izmantošanu, vairāku apstrādi, izkliedētu skaitļošanu, solījumu ķēdi un visiem tiem jautrajiem laikiem, kas priekšlaicīgi novecoja un izžuva daudzus programmētājus veterānus. Tagad mēs vienkārši ierakstām async mūsu funkciju priekšā un await jebkura koda priekšā, kura izpilde var aizņemt kādu laiku. Tas patiešām ir daudz detalizētāks nekā parastais kods, taču to lietošana ir daudz mazāk kaitinoša nekā sinhronizācijas primitīvas problēmas, ziņojumu nodošana un solījumu izpilde.

Mūsu Starlette fails izskatās šādi:

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

Kā redzat, tas ir diezgan daudz kopēts un ielīmēts no mūsu Flask skripta, veicot tikai dažas maršrutēšanas izmaiņas un async/await atslēgvārdi.

Cik daudz uzlabojumu mums var sniegt kopēts un ielīmēts kods?

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

Mums ir jauns čempions, dāmas un kungi! Mūsu iepriekšējais maksimums bija mūsu tīrā PHP versija ar 704 pieprasījumiem sekundē, ko pēc tam apsteidza mūsu Flask versija ar ātrumu 1080 pieprasījumi sekundē. Mūsu Starlette skripts sagrauj visus iepriekšējos pretendentus ar 4562 pieprasījumiem sekundē, kas nozīmē 6x uzlabojumu salīdzinājumā ar tīru PHP un 4x uzlabojumu salīdzinājumā ar Flask.

Ja vēl neesat mainījis savu WSGI Python kodu uz ASGI, tagad varētu būt piemērots laiks sākt.

Node.js/ExpressJS

Līdz šim mēs esam aptvēruši tikai PHP un Python ietvarus. Tomēr liela daļa pasaules savās vietnēs faktiski izmanto Java, DotNet, Node.js, Ruby on Rails un citas līdzīgas tehnoloģijas. Šis nekādā gadījumā nav visaptverošs pārskats par visām pasaules ekosistēmām un biomiem, tāpēc, lai izvairītos no organiskās ķīmijas programmēšanas ekvivalenta, mēs izvēlēsimies tikai tos ietvarus, kuriem ir visvieglāk ievadīt kodu. no kuriem Java noteikti nav.

Ja vien neesat slēpies zem sava K&R C vai Knuth&#x27;s kopijas. Datorprogrammēšanas māksla pēdējos piecpadsmit gadus jūs droši vien esat dzirdējuši par Node.js. Tie no mums, kas ir bijuši kopš JavaScript sākuma, ir vai nu neticami nobijušies, pārsteigti vai abi par mūsdienu JavaScript stāvokli, taču nevar noliegt, ka JavaScript ir kļuvis par spēku, ar kuru jārēķinās arī serveros. kā pārlūkprogrammas. Galu galā mums pat tagad valodā ir vietējie 64 bitu veseli skaitļi! Tas ir daudz labāk nekā viss, kas tiek glabāts 64 bitu pludiņos!

ExpressJS, iespējams, ir visvieglāk lietojamais Node.js serveris, tāpēc mēs izveidosim ātru un netīru lietotni Node.js/ExpressJS, lai apkalpotu mūsu skaitītāju.

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

Šo kodu patiesībā bija vieglāk rakstīt nekā Python versijas, lai gan vietējais JavaScript kļūst diezgan smagnējs, kad lietojumprogrammas kļūst lielākas, un visi mēģinājumi to labot, piemēram, TypeScript, ātri kļūst daudz detalizētāki nekā Python.

Redzēsim, kā tas darbojas!

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

Jūs, iespējams, esat dzirdējuši senas (senas pēc interneta standartiem tik un tā...) tautas pasakas par Node.js&#x27; ātrumu, un šie stāsti lielākoties ir patiesi, pateicoties iespaidīgajam darbam, ko Google ir paveicis ar V8 JavaScript dzinēju. Tomēr šajā gadījumā, lai gan mūsu ātrā lietotne pārspēj Flask skriptu, tās vienotā pavediena raksturu pārspēj četri asinhronie procesi, ko izmanto Starlette Knight, kurš saka: "Ni!".

Saņemsim vēl kādu palīdzību!

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

Labi! Tagad tā ir vienāda cīņa četri pret četri! Veiksim etalonu!

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

Joprojām nav gluži Starlette līmenī, taču tas nav slikti ātrai piecu minūšu JavaScript uzlaušanai. No manas pārbaudes izriet, ka šis skripts faktiski ir nedaudz aizkavēts datu bāzes saskarnes līmenī, jo node-postgres ne tuvu nav tik efektīvs kā psycopg Python. Pārejot uz sqlite kā datu bāzes draiveri, tiek iegūti vairāk nekā 3000 pieprasījumu sekundē vienam ExpressJS kodam.

Galvenais, kas jāņem vērā, ir tas, ka, neraugoties uz Python lēno izpildes ātrumu, ASGI ietvari var būt konkurētspējīgi ar Node.js risinājumiem noteiktām darba slodzēm.

Rūsa/Aktix

Tagad mēs tuvojamies kalna virsotnei, un ar kalnu es domāju augstākos etalona rādītājus, ko reģistrējušas gan peles, gan vīrieši.

Apskatot lielāko daļu tīmeklī pieejamo ietvara etalonu, jūs ievērosiet, ka ir divas valodas, kas dominē populārākajās valodās: C++ un Rust. Es strādāju ar C++ kopš 90. gadiem, un man pat bija savs Win32 C++ ietvars vēl pirms MFC/ATL, tāpēc man ir liela pieredze ar valodu. Nav īpaši jautri strādāt ar kaut ko, ja jūs to jau zināt, tāpēc mēs tā vietā izveidosim Rust versiju. ;)

Rust ir salīdzinoši jauns programmēšanas valodu ziņā, taču tas man kļuva par ziņkārības objektu, kad Linuss Torvalds paziņoja, ka pieņems Rust kā Linux kodola programmēšanas valodu. Mums, gados vecākiem programmētājiem, tas ir aptuveni tas pats, kas teikt, ka šī jaunā, izdomātā jaunā laikmeta hipiju lieta būs jauns grozījums ASV konstitūcijā.

Tagad, kad esat pieredzējis programmētājs, jums ir tendence neiespringt tik ātri, kā to dara jaunāki cilvēki, pretējā gadījumā jūs varat apdegt strauju valodas vai bibliotēku izmaiņu dēļ. (Ikviens, kurš izmantoja pirmo AngularJS versiju, zinās, par ko es runāju.) Rust joprojām ir nedaudz eksperimentālās izstrādes stadijā, un man šķiet smieklīgi, ka daudzi koda piemēri tīmeklī nav pat tādi. vairs kompilējiet ar pašreizējām pakotņu versijām.

Tomēr Rust lietojumprogrammu parādīto veiktspēju nevar noliegt. Ja jūs nekad neesat mēģinājis ripgrep vai fd-atrast izmantojot lielus pirmkoda kokus, jums noteikti vajadzētu tos izmēģināt. Tie pat ir pieejami lielākajai daļai Linux izplatījumu, vienkārši izmantojot pakotņu pārvaldnieku. Jūs apmaināt runīgumu pret sniegumu ar Rust... a daudz no daudzvārdības a daudz veiktspēju.

Pilns Rust kods ir nedaudz liels, tāpēc mēs apskatīsim atbilstošos apstrādātājus šeit:

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

Tas ir daudz sarežģītāk nekā Python/Node.js versijās...

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

Un vēl daudz rezultatīvāks!

Mūsu Rust serveris, kas izmanto Actix/deadpool_postgres, pārspēj mūsu iepriekšējo čempionu Starlette par +125%, ExpressJS par +362%, un tīro PHP par +1366%. (Es atstāšu veiktspējas delta ar Laravel versiju kā vingrinājumu lasītājam.)

Esmu atklājis, ka pašas Rust valodas apguve ir bijusi sarežģītāka nekā citās valodās, jo tajā ir daudz vairāk problēmu nekā jebkurā citā, ko esmu redzējis ārpus 6502 Assembly, taču, ja jūsu Rust serveris var uzņemt 14 reizes vairāk lietotājiem kā jūsu PHP serverim, tad varbūt tomēr ir ko iegūt, mainot tehnoloģijas. Tāpēc nākamā Pafera Framework versija būs balstīta uz Rust. Mācīšanās līkne ir daudz augstāka nekā skriptu valodām, taču veiktspēja būs tā vērta. Ja nevarat atvēlēt laiku Rust apguvei, arī savu tehnoloģiju kopuma balstīšana uz Starlette vai Node.js nav slikts lēmums.

Tehniskais parāds

Pēdējo divdesmit gadu laikā mēs esam pārgājuši no lētām statiskām mitināšanas vietnēm uz dalītu mitināšanu ar LAMP skursteņiem un beidzot ar VPS nomu AWS, Azure un citiem mākoņpakalpojumiem. Mūsdienās daudzi uzņēmumi ir apmierināti ar dizaina lēmumu pieņemšanu, pamatojoties uz to, kurš ir pieejams vai lētākais, jo ērto mākoņpakalpojumu parādīšanās ir ļāvusi viegli izmest vairāk aparatūras lēnos serveros un lietojumprogrammās. Tas viņiem ir devis lielus īstermiņa ieguvumus uz ilgtermiņa tehnisko parādu rēķina.

Kalifornijas ķirurga ģenerālis brīdinājums: šis nav īsts kosmosa suns.

Pirms 70 gadiem notika liela kosmosa sacīkste starp Padomju Savienību un ASV. Padomju vara uzvarēja lielāko daļu agrīno atskaites punktu. Viņiem bija pirmais satelīts Sputnik, pirmais suns kosmosā Laikā, pirmais mēness kosmosa kuģis Luna 2, pirmais vīrietis un sieviete kosmosā Jurijs Gagarins un Valentīna Tereškova un tā tālāk...

Bet viņiem lēnām krājās tehniskie parādi.

Lai gan padomju vara bija pirmā, kas sasniedza katru no šiem sasniegumiem, to inženiertehniskie procesi un mērķi lika viņiem koncentrēties uz īstermiņa izaicinājumiem, nevis ilgtermiņa iespējamību. Viņi uzvarēja katru reizi, kad lēca, taču viņi kļuva arvien noguruši un lēnāki, kamēr viņu pretinieki turpināja spert konsekventus soļus uz finiša līniju.

Kad Nīls Ārmstrongs veica savus vēsturiskos soļus uz Mēness televīzijas tiešraidē, amerikāņi pārņēma vadību un pēc tam palika tur, jo padomju programma kliboja. Tas neatšķiras no mūsdienu uzņēmumiem, kuri ir koncentrējušies uz nākamo lielo lietu, nākamo lielo atdevi vai nākamo lielo tehnoloģiju, vienlaikus nespējot izstrādāt atbilstošus ieradumus un stratēģijas ilgtermiņā.

Būt pirmajam tirgū nenozīmē, ka jūs kļūsit par dominējošo spēlētāju šajā tirgū. Alternatīvi, veltot laiku lietu pareizai veikšanai, tas negarantē panākumus, bet noteikti palielina jūsu izredzes uz ilgtermiņa sasniegumiem. Ja esat sava uzņēmuma tehnoloģiju vadītājs, izvēlieties savai darba slodzei pareizo virzienu un rīkus. Neļaujiet popularitātei aizstāt veiktspēju un efektivitāti.

Resursi

Vai vēlaties lejupielādēt 7z failu, kurā ir Rust, ExpressJS, Flask, Starlette un Pure PHP skripti?

Par Autoru

Džims nodarbojas ar programmēšanu, kopš 90. gados ieguva IBM PS/2. Līdz pat šai dienai viņš dod priekšroku HTML un SQL rakstīšanai ar roku, un savā darbā koncentrējas uz efektivitāti un pareizību.