Nakon jednog od mojih najnovijih intervjua za posao, bio sam iznenađen kada sam shvatio da kompanija za koju sam se prijavio još uvijek koristi Laravel, PHP framework koji sam isprobao prije otprilike deset godina. Bilo je pristojno za to vrijeme, ali ako postoji jedna konstanta u tehnologiji i modi, to je stalna promjena i ponovno pojavljivanje stilova i koncepata. Ako ste JavaScript programer, vjerovatno vam je poznat ovaj stari vic
Programer 1: "Ne sviđa mi se ovaj novi JavaScript okvir!"
Programer 2: "Nema potrebe da brinete. Samo pričekajte šest mjeseci i doći će još jedan koji će ga zamijeniti!"
Iz radoznalosti, odlučio sam da vidim šta se tačno dešava kada stavimo staro i novo na test. Naravno, web je pun mjerila i tvrdnji, od kojih je vjerovatno najpopularniji TechEmpower Web Framework Benchmarks ovdje . Ipak, danas nećemo raditi ništa tako komplikovano kao oni. Održat ćemo stvari lijepim i jednostavnim kako se ovaj članak ne bi pretvorio u Rat i mir , i da ćete imati malu šansu da ostanete budni do trenutka kada završite s čitanjem. Primjenjuju se uobičajena upozorenja: ovo možda neće raditi isto na vašoj mašini, različite verzije softvera mogu utjecati na performanse, a Schrödingerova mačka je zapravo postala zombi mačka koja je bila napola živa i napola mrtva u isto vrijeme.
Za ovaj test, koristiću svoj laptop naoružan slabašnim i5 koji pokreće Manjaro Linux kao što je prikazano ovdje.
╰─➤ 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
Naš kod će imati tri jednostavna zadatka za svaki zahtjev:
Kakav je to idiotski test, pitate se? Pa, ako pogledate mrežne zahtjeve za ovu stranicu, primijetit ćete jedan koji se zove sessionvars.js koji radi potpuno istu stvar.
Vidite, moderne web stranice su komplikovana stvorenja, a jedan od najčešćih zadataka je keširanje složenih stranica kako bi se izbjeglo prekomjerno opterećenje servera baze podataka.
Ako ponovo generiramo složenu stranicu svaki put kada je korisnik zatraži, tada možemo opsluživati samo oko 600 korisnika u sekundi.
╰─➤ 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
Ali ako keširamo ovu stranicu kao statičnu HTML datoteku i pustimo Nginx da je brzo izbaci kroz prozor korisniku, tada možemo opsluživati 32.000 korisnika u sekundi, povećavajući performanse za faktor od 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
Statički index.en.html je dio koji ide svima, a samo dijelovi koji se razlikuju po korisniku šalju se u sessionvars.js. Ovo ne samo da smanjuje opterećenje baze podataka i stvara bolje iskustvo za naše korisnike, već i smanjuje kvantne vjerovatnoće da će naš server spontano ispariti u proboju warp jezgra kada napadnu Klingonci.
Vraćeni kod za svaki okvir imat će jedan jednostavan zahtjev: pokazati korisniku koliko puta je osvježio stranicu govoreći "Broj je x". Da stvari budu jednostavne, za sada ćemo se kloniti redova redova Redis, Kubernetes komponenti ili AWS Lambda.
Podaci o sesiji svakog korisnika biće sačuvani u PostgreSQL bazi podataka.
I ova tabela baze podataka će biti skraćena prije svakog testa.
Jednostavan, a efikasan je moto Pafere... ionako izvan najmračnije vremenske linije...
U redu, sad konačno možemo početi prljati ruke. Preskočićemo podešavanje za Laravel jer je to samo gomila kompozitora i zanatlija komande.
Prvo ćemo postaviti postavke naše baze podataka u .env datoteci
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=sessiontest
DB_USERNAME=sessiontest
DB_PASSWORD=sessiontest
Zatim ćemo postaviti jednu rezervnu rutu koja šalje svaki zahtjev našem kontroleru.
Route::fallback(SessionController::class);
I podesite kontroler da prikazuje broj. Laravel, po defaultu, pohranjuje sesije u bazi podataka. Takođe obezbeđuje session()
funkciju za povezivanje s našim podacima o sesiji, tako da je sve što je bilo potrebno bilo je nekoliko redova koda da se prikaže naša stranica.
class SessionController extends Controller
{
public function __invoke(Request $request)
{
$count = session('count', 0);
$count += 1;
session(['count' => $count]);
return 'Count is ' . $count;
}
}
Nakon postavljanja php-fpm-a i Nginx-a, naša stranica izgleda prilično dobro...
╰─➤ 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
Barem dok zaista ne vidimo rezultate testa...
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
Ne, to nije greška u kucanju. Naša mašina za testiranje je otišla sa 600 zahtjeva u sekundi renderirajući složenu stranicu... na 21 zahtjev u sekundi pri renderiranju "Broj je 1".
Šta je pošlo po zlu? Da li nešto nije u redu sa našom PHP instalacijom? Da li se Nginx nekako usporava kada se povezuje sa php-fpmom?
Hajde da ponovimo ovu stranicu u čistom PHP kodu.
<?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'];
Sada smo koristili 98 linija koda da uradimo ono što su četiri reda koda (i čitav niz konfiguracionih radova) u Laravelu uradile. (Naravno, ako bismo radili pravilno rukovanje greškama i poruke koje su okrenute korisniku, ovo bi bilo otprilike dvostruko veći broj redova.) Možda bismo mogli postići 30 zahtjeva u sekundi?
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
Vau! Izgleda da ipak nema ništa loše u našoj PHP instalaciji. Čista PHP verzija radi 700 zahtjeva u sekundi.
Ako ništa nije u redu sa PHP-om, možda smo pogrešno konfigurisali Laravel?
Nakon pretraživanja weba u potrazi za konfiguracijskim problemima i savjetima o performansama, dvije od najpopularnijih tehnika bile su keširanje podataka o konfiguraciji i rutiranju kako bi se izbjegla njihova obrada za svaki zahtjev. Stoga ćemo poslušati njihove savjete i isprobati ove savjete.
╰─➤ php artisan config:cache
INFO Configuration cached successfully.
╰─➤ php artisan route:cache
INFO Routes cached successfully.
Sve izgleda dobro na komandnoj liniji. Hajde da ponovimo benchmark.
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1
Running 10s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.13s 543.50ms 1.98s 61.90%
Req/Sec 25.45 13.39 50.00 55.77%
289 requests in 10.04s, 242.15KB read
Socket errors: connect 0, read 0, write 0, timeout 247
Requests/sec: 28.80
Transfer/sec: 24.13KB
Pa, sada smo povećali performanse sa 21.04 na 28.80 zahtjeva u sekundi, dramatično povećanje od skoro 37%! Ovo bi bilo prilično impresivno za bilo koji softverski paket... osim činjenice da još uvijek radimo samo 1/24 od broja zahtjeva čiste PHP verzije.
Ako mislite da nešto mora da nije u redu sa ovim testom, trebalo bi da razgovarate sa autorom Lucinda PHP framework-a. U svojim rezultatima testa jeste Lucinda pobijedi Laravela za 36x za HTML zahtjeve i 90x za JSON zahtjeve.
Nakon testiranja na vlastitoj mašini sa Apacheom i Nginxom, nemam razloga da sumnjam u njega. Laravel je zaista pravedan to polako! PHP sam po sebi nije tako loš, ali kada jednom dodate svu dodatnu obradu koju Laravel dodaje svakom zahtjevu, onda mi je vrlo teško preporučiti Laravel kao izbor u 2023. godini.
PHP/Wordpress računi za oko 40% svih web stranica na webu , što ga čini daleko najdominantnijim okvirom. Osobno, ipak, smatram da se popularnost ne mora nužno pretvoriti u kvalitetu više nego što osjećam da imam iznenadnu nekontrolisanu potrebu za tom izvanrednom gurmanskom hranom iz najpopularniji restoran na svijetu ... McDonald's. Pošto smo već testirali čisti PHP kod, nećemo testirati sam Wordpress, jer bi sve što uključuje Wordpress nesumnjivo bilo manje od 700 zahteva u sekundi koje smo primetili sa čistim PHP-om.
Django je još jedan popularan okvir koji postoji već duže vrijeme. Ako ste ga koristili u prošlosti, vjerovatno se rado sećate njegovog spektakularnog interfejsa za administraciju baze podataka, kao i koliko je dosadno bilo konfigurisati sve baš onako kako ste želeli. Hajde da vidimo koliko dobro Django radi u 2023. godini, posebno sa novim ASGI interfejsom koji je dodao od verzije 4.0.
Postavljanje Djanga je izuzetno slično postavljanju Laravela, jer su oba bila iz doba u kojem su MVC arhitekture bile elegantne i ispravne. Preskočićemo dosadnu konfiguraciju i ići direktno na podešavanje prikaza.
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}")
Četiri linije koda su iste kao kod Laravel verzije. Pogledajmo kako se ponaša.
╰─➤ 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
Uopšte nije loše sa 355 zahtjeva u sekundi. To je samo polovina performansi čiste PHP verzije, ali je takođe 12 puta veća od Laravel verzije. Čini se da Django protiv Laravela uopšte nije takmičenje.
Osim većih okvira za sve-uključujući-kuhinjski sudoper, postoje i manji okviri koji samo rade neke osnovne postavke, dok vam dozvoljavaju da se nosite s ostalim. Jedan od najboljih za korištenje je Flask i njegov ASGI pandan Quart. Moje PaferaPy Framework je izgrađen na vrhu Flask-a, tako da mi je dobro poznato koliko je lako obaviti stvari uz održavanje performansi.
#!/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}'
Kao što vidite, Flask skripta je kraća od čiste PHP skripte. Smatram da je od svih jezika koje sam koristio, Python vjerovatno najizrazitiji jezik u smislu pritiska na tipke. Nedostatak zagrada i zagrada, razumijevanja liste i diktata, te blokiranje zasnovano na uvlačenju, a ne na tački i zarezu, čine Python prilično jednostavnim, ali moćnim u svojim mogućnostima.
Nažalost, Python je takođe najsporiji jezik opšte namene, uprkos tome koliko je softvera napisano u njemu. Broj dostupnih Python biblioteka je oko četiri puta veći od sličnih jezika i pokriva ogromnu količinu domena, ali niko ne bi rekao da je Python brz niti efikasan izvan niša kao što je NumPy.
Pogledajmo kako je naša verzija Flask u usporedbi s našim prethodnim okvirima.
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
Naša Flask skripta je zapravo brža od naše čiste PHP verzije!
Ako ste iznenađeni ovim, trebali biste shvatiti da naša Flask aplikacija radi svu svoju inicijalizaciju i konfiguraciju kada pokrenemo gunicorn server, dok PHP ponovo izvršava skriptu svaki put kada stigne novi zahtjev. ;ekvivalentno je da je Flask mladi, željni taksista koji je već upalio auto i čeka pored puta, dok je PHP stari vozač koji ostaje u svojoj kući čekajući poziv da uđe i tek onda vozi doći po tebe. Budući da sam čovjek iz stare škole i dolazi iz vremena kada je PHP bio divna promjena u običnim HTML i SHTML fajlovima, pomalo je tužno shvatiti koliko je vremena prošlo, ali razlike u dizajnu zaista otežavaju PHP-u takmičite se sa Python, Java i Node.js serverima koji samo ostaju u memoriji i rješavaju zahtjeve s lakoćom žonglera.
Flask je možda naš najbrži okvir do sada, ali to je zapravo prilično stari softver. Python zajednica je prešla na novije asihrone ASGI servere pre nekoliko godina, i naravno, i ja sam se prebacio zajedno sa njima.
Najnovija verzija Pafera Framework-a, PaferaPyAsync , baziran je na Starlette. Iako postoji ASGI verzija Flask-a pod nazivom Quart, razlike u performansama između Quart-a i Starlette-a bile su dovoljne da umjesto toga rebaziram svoj kod na Starlette-u.
Asihrono programiranje može biti zastrašujuće za mnoge ljude, ali to zapravo nije težak koncept zahvaljujući Node.js momcima koji su popularizirali koncept prije više od deset godina.
Nekada smo se borili protiv istovremenosti sa višenitnim, multiprocesiranjem, distribuiranim računarstvom, ulančavanjem obećanja i svim onim zabavnim vremenima koja su prerano ostarila i isušila mnoge veterane programera. Sada samo kucamo async
ispred naših funkcija i await
ispred bilo kojeg koda za koji bi moglo biti potrebno neko vrijeme da se izvrši. Zaista je opširniji od običnog koda, ali je mnogo manje dosadan za korištenje od potrebe da se bavimo primitivima sinhronizacije, prosljeđivanjem poruka i rješavanjem obećanja.
Naš Starlette fajl izgleda ovako:
#!/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",
)
Kao što možete vidjeti, prilično je kopiran i zalijepljen iz naše Flask skripte sa samo nekoliko promjena rutiranja i async/await
ključne riječi.
Koliko nam poboljšanja zaista mogu dati kopirani i lijepljeni kodovi?
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
Imamo novog šampiona, dame i gospodo! Naš prethodni maksimum je bila naša čista PHP verzija sa 704 zahtjeva u sekundi, koju je potom pretekla naša Flask verzija sa 1080 zahtjeva u sekundi. Naša Starlette skripta uništava sve prethodne kandidate sa 4562 zahteva u sekundi, što znači 6x poboljšanje u odnosu na čisti PHP i 4x poboljšanje u odnosu na Flask.
Ako još niste promijenili svoj WSGI Python kod u ASGI, sada je možda dobro vrijeme za početak.
Do sada smo pokrili samo PHP i Python okvire. Međutim, veliki dio svijeta zapravo koristi Javu, DotNet, Node.js, Ruby on Rails i druge slične tehnologije za svoje web stranice. Ovo nikako nije sveobuhvatan pregled svih svjetskih ekosistema i bioma, tako da ćemo izbjeći programiranje ekvivalenta organskoj hemiji, izabrati samo okvire za koje je najlakše ukucati kod. od kojih Java definitivno nije.
Osim ako se niste skrivali ispod svoje kopije K&R C ili Knuth's Umetnost kompjuterskog programiranja u posljednjih petnaest godina vjerovatno ste čuli za Node.js. Oni od nas koji postoje od početka JavaScripta su ili neverovatno uplašeni, zadivljeni ili oboje stanjem modernog JavaScripta, ali ne može se poreći da je JavaScript postao sila s kojom se treba računati i na serverima kao pretraživači. Na kraju krajeva, čak imamo i izvorne 64-bitne cijele brojeve sada u jeziku! To je daleko bolje od svega što je pohranjeno u 64-bitnim floatovima!
ExpressJS je vjerovatno najlakši Node.js server za korištenje, tako da ćemo napraviti brzu i prljavu aplikaciju Node.js/ExpressJS koja će služiti našem brojaču.
/**********************************************************************
* 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}`));
Ovaj kod je zapravo bilo lakše napisati od Python verzija, iako izvorni JavaScript postaje prilično nezgrapan kada aplikacije postanu veće, a svi pokušaji da se ovo ispravi, kao što je TypeScript, brzo postaju opširniji od Pythona.
Hajde da vidimo kako se ovo radi!
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
Možda ste čuli drevne (ionako drevne po internet standardima...) narodne priče o Node.js' brzine, a te priče su uglavnom istinite zahvaljujući spektakularnom radu koji je Google uradio sa V8 JavaScript motorom. Međutim, u ovom slučaju, iako naša brza aplikacija nadmašuje Flask skriptu, njena jednostruka priroda je poražena od strane četiri asinhronizirana procesa kojima upravlja Starlette Knight koji kaže "Ni!".
Potražimo još pomoć!
╰─➤ 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 │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
U redu! Sada je bitka čak četiri na četiri! Hajde da mjerimo!
╰─➤ 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
Još uvijek nije sasvim na nivou Starlette, ali nije loše za brzi petominutni JavaScript hak. Iz mog vlastitog testiranja, ova skripta je zapravo malo zadržana na nivou sučelja baze podataka jer node-postgres nije ni približno tako efikasan kao psycopg za Python. Prebacivanje na sqlite kao drajver baze podataka daje preko 3000 zahtjeva u sekundi za isti ExpressJS kod.
Glavna stvar koju treba napomenuti je da uprkos sporoj brzini izvršavanja Pythona, ASGI okviri zapravo mogu biti konkurentni Node.js rješenjima za određena radna opterećenja.
Dakle, sada smo sve bliže vrhu planine, a pod planinom mislim na najviše rezultate koje su zabilježili i miševi i muškarci.
Ako pogledate većinu referentnih vrijednosti okvira dostupnih na webu, primijetit ćete da postoje dva jezika koja imaju tendenciju da dominiraju na vrhu: C++ i Rust. Radio sam sa C++ od 90-ih, i čak sam imao svoj Win32 C++ okvir prije nego što je MFC/ATL postojao, tako da imam dosta iskustva sa jezikom. Nije baš zabavno raditi s nečim kada to već znate, pa ćemo umjesto toga napraviti Rust verziju. ;)
Rust je relativno nov što se tiče programskih jezika, ali postao je predmet znatiželje za mene kada je Linus Torvalds najavio da će prihvatiti Rust kao programski jezik jezgra Linuxa. Za nas starije programere, to je otprilike isto kao da kažemo da će ova nova moderna new age hipi stvarčica biti novi amandman na Ustav SAD-a.
Sada, kada ste iskusan programer, ne uskačete tako brzo kao mlađi ljudi, inače biste se mogli opeći brzim promjenama jezika ili biblioteka. (Svako ko je koristio prvu verziju AngularJS-a znat će o čemu govorim.) Rust je još uvijek donekle u toj eksperimentalnoj fazi razvoja, a smiješno mi je što toliki primjeri koda na webu čak ni ne kompajlirajte više sa trenutnim verzijama paketa.
Međutim, performanse koje pokazuju Rust aplikacije ne mogu se poreći. Ako nikada niste probali ripgrep ili fd-find na velikim stablima izvornog koda, svakako biste ih trebali isprobati. Oni su čak dostupni za većinu Linux distribucija jednostavno iz upravitelja paketa. Razmjenjujete opširnost za performanse sa Rustom... a puno opširnosti za a puno performansi.
Kompletan kod za Rust je malo velik, tako da ćemo ovdje samo pogledati relevantne rukovaoce:
// =====================================================================
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
)))
}
Ovo je mnogo komplikovanije od Python/Node.js verzija...
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
I mnogo učinkovitije!
Naš Rust server koji koristi Actix/deadpool_postgres lako nadmašuje našeg prethodnog šampiona Starlette za +125%, ExpressJS za +362%, i čisti PHP za +1366%. (Ostaviću deltu performansi uz Laravel verziju kao vježbu za čitaoca.)
Otkrio sam da je učenje samog Rust jezika bilo teže od drugih jezika jer ima mnogo više problema od bilo čega što sam vidio izvan 6502 Assembly, ali ako vaš Rust server može preuzeti 14x veći broj korisnika kao vaš PHP server, onda možda ipak postoji nešto što se može dobiti sa prebacivanjem tehnologija. Zato će sljedeća verzija Pafera Framework-a biti bazirana na Rustu. Krivulja učenja je mnogo veća od skriptnih jezika, ali učinak će biti vrijedan toga. Ako ne možete odvojiti vrijeme da naučite Rust, onda ni baziranje vašeg tehničkog niza na Starlette ili Node.js nije loša odluka.
U posljednjih dvadeset godina prešli smo sa jeftinih statičnih hosting lokacija na dijeljeni hosting sa LAMP stackovima do iznajmljivanja VPS-a na AWS, Azure i druge usluge u oblaku. Danas su mnoge kompanije zadovoljne donošenjem dizajnerskih odluka na osnovu onoga koga mogu naći da je dostupan ili najjeftiniji jer je pojava praktičnih usluga u oblaku olakšala bacanje više hardvera na spore servere i aplikacije. To im je dalo velike kratkoročne dobitke po cijenu dugoročnog tehničkog duga.
Prije 70 godina bila je velika svemirska utrka između Sovjetskog Saveza i Sjedinjenih Država. Sovjeti su osvojili većinu prvih prekretnica. Imali su prvi satelit u Sputnjiku, prvog psa u svemiru u Lajki, prvu svemirsku letjelicu na Mjesecu u Luni 2, prvog muškarca i ženu u svemiru u Jurija Gagarina i Valentine Tereškove, i tako dalje...
Ali polako su gomilali tehnički dug.
Iako su Sovjeti bili prvi u svakom od ovih dostignuća, njihovi inženjerski procesi i ciljevi su ih naveli da se fokusiraju na kratkoročne izazove, a ne na dugoročnu izvodljivost. Pobjeđivali su svaki put kada su skočili, ali su postajali sve umorniji i sporiji dok su njihovi protivnici nastavili ujednačenim koracima ka cilju.
Jednom kada je Neil Armstrong napravio svoje istorijske korake na Mjesecu na televiziji uživo, Amerikanci su preuzeli vodstvo, a onda su ostali tamo dok je sovjetski program posustajao. Ovo se ne razlikuje od današnjih kompanija koje su se fokusirale na sljedeću veliku stvar, sljedeću veliku isplatu ili sljedeću veliku tehnologiju, a ne uspijevaju razviti odgovarajuće navike i strategije na duge staze.
Biti prvi na tržištu ne znači da ćete postati dominantni igrač na tom tržištu. Alternativno, odvajanje vremena da stvari uradite kako treba ne garantuje uspeh, ali svakako povećava vaše šanse za dugoročna postignuća. Ako ste tehnološki lider za svoju kompaniju, odaberite pravi smjer i alate za svoje radno opterećenje. Ne dozvolite da popularnost zamijeni performanse i efikasnost.
Želite da preuzmete 7z datoteku koja sadrži Rust, ExpressJS, Flask, Starlette i Pure PHP skripte?
O autoru |
|
![]() |
Jim se bavi programiranjem otkako je vratio IBM PS/2 tokom 90-ih. I dan-danas preferira pisanje HTML-a i SQL-a ručno, te se fokusira na efikasnost i korektnost u svom radu. |