Да, Вирџинија, има *Има* a Дедо Мраз Разлика помеѓу веб-рамките во 2023 година

Патувањето на еден пркосен програмер да најде брз код на веб-серверот пред да подлегне на притисокот на пазарот и техничкиот долг
2023-03-24 11:52:06
👁️ 779
💬 0

Содржини

  1. Вовед
  2. Тестот
  3. PHP/Laravel
  4. Чист PHP
  5. Повторна посета на Ларавел
  6. Џанго
  7. Колба
  8. Старлета
  9. Јазол.js/ExpressJS
  10. Рѓа/Актикс
  11. Технички долг
  12. Ресурси

Вовед

По едно од моите најнови интервјуа за работа, бев изненаден кога сфатив дека компанијата за која аплицирав сè уште користи Laravel, PHP рамка што ја пробав пред околу една деценија. Тоа беше пристојно за тоа време, но ако постои една константа во технологијата и модата, тоа е постојана промена и повторно појавување на стилови и концепти. Ако сте програмер на JavaScript, веројатно сте запознаени со оваа стара шега

Програмер 1: "Не ми се допаѓа оваа нова JavaScript рамка!"

Програмер 2: „Нема потреба да се грижите. Само почекајте шест месеци и ќе има уште еден да го замени!"

Од љубопитност решив да видам што точно ќе се случи кога ќе ги ставиме на тест старото и новото. Се разбира, мрежата е исполнета со репери и тврдења, од кои најпопуларна е веројатно онаа TechEmpower Web Framework одредници овде . Сепак, нема да направиме ништо ни приближно комплицирано како нив денес. Ќе ги одржуваме работите убави и едноставни, така што овој напис нема да се претвори во Војна и мир , и дека ќе имате мали шанси да останете будни додека не завршите со читањето. Важат вообичаените предупредувања: ова може да не работи исто на вашата машина, различните верзии на софтвер може да влијаат на перформансите, а мачката на Шредингер всушност стана мачка зомби која беше полужива и половина мртва точно во исто време.

Тестот

Околина за тестирање

За овој тест, ќе го користам мојот лаптоп вооружен со слаб i5 со Manjaro Linux како што е прикажано овде.

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

Задачата при рака

Нашиот код ќе има три едноставни задачи за секое барање:

  1. Прочитајте го ID на сесијата на тековниот корисник од колаче
  2. Вчитајте дополнителни информации од базата на податоци
  3. Вратете ги тие информации на корисникот

Каков идиотски тест е тоа, можеби ќе прашате? Па, ако ги погледнете мрежните барања за оваа страница, ќе забележите едно наречено sessionvars.js кое го прави истото.

Содржината на sessionvars.js

Гледате, модерните веб-страници се комплицирани суштества, а една од најчестите задачи е кеширање на сложени страници за да се избегне прекумерно оптоварување на серверот на базата на податоци.

Ако повторно прикажуваме сложена страница секогаш кога корисникот ја бара, тогаш можеме да опслужиме само околу 600 корисници во секунда.

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

Но, ако ја кешираме оваа страница како статична HTML-датотека и дозволиме Nginx брзо да ја фрли низ прозорецот до корисникот, тогаш можеме да опслужуваме 32.000 корисници во секунда, зголемувајќи ги перформансите за 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

Статичниот index.en.html е делот што оди кај сите, а само деловите што се разликуваат по корисник се испраќаат во sessionvars.js. Ова не само што го намалува оптоварувањето на базата на податоци и создава подобро искуство за нашите корисници, туку ги намалува и квантните веројатности нашиот сервер спонтано да испарува при пробивање на јадрото на искривување при нападот на Клингонците.

Барања за код

Вратениот код за секоја рамка ќе има едно едноставно барање: покажете му на корисникот колку пати ја освежил страницата со велејќи „Бројот е x“. За работите да бидат едноставни, засега ќе се држиме подалеку од редиците на Redis, компонентите на Kubernetes или AWS Lambdas.

Се прикажува колку пати сте ја посетиле страницата

Податоците за сесијата на секој корисник ќе бидат зачувани во базата на податоци PostgreSQL.

Табела за кориснички сесии

И оваа табела со база на податоци ќе биде скратена пред секој тест.

Табелата откако ќе биде скратена

Едноставно, но ефективно е мотото на Пафера... и онака надвор од најтемната временска линија...

Вистинските резултати од тестот

PHP/Laravel

Добро, па сега конечно можеме да почнеме да ги валкаме рацете. Ќе го прескокнеме поставувањето за Laravel бидејќи тоа е само еден куп композитори и занаетчии команди.

Прво, ќе ги поставиме поставките за нашата база на податоци во датотеката .env

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

Потоа ќе поставиме една единствена резервна рута што го испраќа секое барање до нашиот контролер.

Route::fallback(SessionController::class);

И поставете го контролорот да го прикажува броењето. Ларавел, стандардно, складира сесии во базата на податоци. Таа, исто така обезбедува на session() функцијата за интерфејс со нашите податоци од сесијата, па се што беа потребни беа неколку линии код за да се прикаже нашата страница.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

По поставувањето php-fpm и Nginx, нашата страница изгледа прилично добро...

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

Барем додека не ги видиме резултатите од тестот...

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

Не, тоа не е печатна грешка. Нашата машина за тестирање се зголеми од 600 барања во секунда за прикажување на сложена страница... на 21 барање во секунда за прикажување "Бројот е 1".

Значи, што тргна наопаку? Дали нешто не е во ред со нашата инсталација на PHP? Дали Nginx некако забавува кога се поврзува со php-fpm?

Чист PHP

Да ја повториме оваа страница во чист 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'];

Сега користевме 98 линии код за да го направиме она што го направија четири линии код (и цел куп конфигурациски работи) во Ларавел. (Се разбира, ако направиме правилно справување со грешките и пораките со кои се соочува корисникот, ова би било отприлика двојно повеќе од бројот на линии.) Можеби можеме да достигнеме 30 барања во секунда?

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

Уф! Се чини дека на крајот на краиштата нема ништо лошо во нашата инсталација на PHP. Чистата PHP верзија прави 700 барања во секунда.

Ако нема ништо лошо со PHP, можеби погрешно го конфигуриравме Ларавел?

Повторна посета на Ларавел

По пребарувањето на мрежата за проблеми со конфигурацијата и совети за изведба, две од најпопуларните техники беа кеширањето на конфигурацијата и податоците за насочување за да се избегне нивна обработка за секое барање. Затоа, ќе ги земеме нивните совети и ќе ги испробаме овие совети.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Сè изгледа добро на командната линија. Ајде да го повториме реперот.

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

Па, сега ги зголемивме перформансите од 21,04 на 28,80 барања во секунда, драматично зголемување од речиси 37%! Ова би било доста импресивно за секој софтверски пакет... освен фактот дека сè уште правиме само 1/24 од бројот на барања од чистата верзија на PHP.

Ако мислите дека нешто мора да не е во ред со овој тест, треба да разговарате со авторот на рамката Lucinda PHP. Во резултатите од тестот, тој има Лусинда го победи Ларавел за 36x за барања за HTML и 90x за барања за JSON.

По тестирањето на мојата сопствена машина и со Apache и со Nginx, немам причина да се сомневам во него. Ларавел е навистина праведен тоа бавно! PHP сам по себе не е толку лош, но штом ќе ја додадете целата дополнителна обработка што Ларавел ја додава на секое барање, тогаш ми е многу тешко да го препорачам Ларавел како избор во 2023 година.

Џанго

PHP/Wordpress сметки за околу 40% од сите веб-локации на веб , што го прави убедливо најдоминантна рамка. Лично, сепак, сметам дека популарноста не мора да значи квалитет повеќе отколку што имам ненадеен неконтролиран нагон за таа извонредна гурманска храна од најпопуларниот ресторан во светот ... McDonald&#x27; Бидејќи веќе тестиравме чист PHP код, нема да го тестираме самиот Wordpress, бидејќи сè што вклучува Wordpress несомнено би било помало од 700 барања во секунда што ги забележавме со чист PHP.

Django е уште една популарна рамка која постои долго време. Ако сте го користеле во минатото, веројатно со задоволство се сеќавате на неговиот спектакуларен интерфејс за администрирање на базата на податоци, како и колку е досадно да конфигурирате сè онака како што сакате. Ајде да видиме колку добро функционира Django во 2023 година, особено со новиот ASGI интерфејс што го додаде од верзијата 4.0.

Поставувањето на Django е неверојатно слично на поставувањето на Laravel, бидејќи и двајцата беа од времето кога MVC архитектурите беа стилски и точни. Ќе ја прескокнеме здодевната конфигурација и ќе одиме директно на поставување на приказот.

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

Четири линии код се исти како кај верзијата Ларавел. Ајде да видиме како функционира.

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

Воопшто не е лошо со 355 барања во секунда. Тоа е само половина од перформансите на чистата PHP верзија, но исто така е 12 пати поголема од онаа на верзијата Laravel. Џанго против Ларавел се чини дека воопшто нема натпревар.

Колба

Освен поголемите рамки за сè, вклучително и мијалникот во кујната, има и помали рамки кои само прават некои основни поставки додека ви дозволуваат да се справите со останатото. Еден од најдобрите за употреба е Flask и неговиот ASGI колега Quart. Моето Рамка на PaferaPy е изграден на врвот на Flask, така што добро сум запознаен со тоа колку е лесно да се завршат работите додека се одржуваат перформансите.

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

Како што можете да видите, скриптата Flask е пократка од чистата PHP скрипта. Сфатив дека од сите јазици што сум ги користел, Python е веројатно најизразениот јазик во однос на тастатурата на тастатурата. Недостатокот на загради и загради, разбирање на списоци и дикти, како и блокирањето врз основа на вовлекување наместо точка-запирка го прават Python прилично едноставен, но моќен во неговите способности.

За жал, Python е исто така најбавниот јазик за општа намена таму, и покрај тоа колку софтвер е напишан во него. Бројот на достапни библиотеки на Python е околу четири пати повеќе од сличните јазици и покрива огромна количина на домени, но никој не би рекол дека Python е брз ниту има перформанси надвор од ниши како NumPy.

Ајде да видиме како нашата верзија Flask се споредува со нашите претходни рамки.

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

Нашата скрипта Flask е всушност побрза од нашата чиста PHP верзија!

Ако сте изненадени од ова, треба да сфатите дека нашата апликација Flask ја прави целата своја иницијализација и конфигурација кога ќе го стартуваме серверот Gunicorn, додека PHP повторно ја извршува скриптата секој пат кога доаѓа ново барање. е еквивалентно на Flask да биде младиот, желен таксист кој веќе ја стартуваше колата и чека покрај патот, додека PHP е стариот возач кој останува во својата куќа чекајќи повик да влезе и дури потоа вози да те земам. Да се ​​биде човек од старата школа и доаѓа од деновите кога PHP беше прекрасна промена на обичните HTML и SHTML-датотеки, малку е тажно да се сфати колку време поминало, но разликите во дизајнот навистина го отежнуваат PHP се натпреваруваат со серверите на Python, Java и Node.js кои само остануваат во меморијата и се справуваат со барањата со пргав леснотија на жонглер.

Старлета

Flask можеби е нашата најбрза рамка досега, но всушност е прилично стар софтвер. Заедницата на Python се префрли на поновите асихрони ASGI сервери пред неколку години, и се разбира, јас самиот се префрлив заедно со нив.

Најновата верзија на Pafera Framework, PaferaPyAsync , се базира на Starlette. Иако постои ASGI верзија на Flask наречена Quart, разликите во перформансите помеѓу Quart и Starlette беа доволни за да го пребазирам мојот код на Starlette наместо тоа.

Асихроното програмирање може да биде застрашувачко за многу луѓе, но всушност не е тежок концепт благодарение на момците од Node.js што го популаризираа концептот пред повеќе од една деценија.

Порано се боревме со истовременост со повеќенишки, мултипроцесирање, дистрибуирано пресметување, синџир на ветувања и сите оние забавни времиња што прерано стареа и исушија многу ветерани програмери. Сега, само пишуваме async пред нашите функции и await пред кој било код за кој може да биде потребно извесно време да се изврши. Навистина е пообемно од обичниот код, но многу помалку досаден за користење отколку да се занимавате со примитивите за синхронизација, пренесување пораки и решавање на ветувања.

Нашата датотека Starlette изгледа вака:

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

Како што можете да видите, тој е прилично копиран и залепен од нашата скрипта Flask со само неколку промени во насочувањето и async/await клучни зборови.

Колку подобрување навистина може да ни даде копирањето и залепениот код?

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

Имаме нов шампион, дами и господа! Нашиот претходен врв беше нашата чиста PHP верзија со 704 барања во секунда, која потоа беше надмината од нашата верзија на Flask со 1080 барања во секунда. Нашата скрипта Starlette ги уништува сите претходни конкуренти со 4562 барања во секунда, што значи 6x подобрување во однос на чистиот PHP и 4x подобрување во однос на Flask.

Ако сè уште не сте го промениле вашиот WSGI Python код во ASGI, сега можеби е добро време да започнете.

Јазол.js/ExpressJS

Досега покривавме само PHP и Python рамки. Сепак, голем дел од светот всушност користи Java, DotNet, Node.js, Ruby on Rails и други такви технологии за нивните веб-локации. Ова во никој случај не е сеопфатен преглед на сите светски екосистеми и биоми, па за да избегнеме да го правиме програмскиот еквивалент на органската хемија, ќе ги избереме само рамките што најлесно се пишуваат код за.. од кои Java дефинитивно не е.

Освен ако не сте се криеле под вашата копија на K&R C или Knuth&#x27;s Уметноста на компјутерското програмирање во последните петнаесет години, веројатно сте слушнале за Node.js. Оние од нас кои постојат од почетокот на JavaScript се или неверојатно исплашени, изненадени или и двајцата од состојбата на модерниот JavaScript, но не може да се негира дека JavaScript стана сила со која треба да се смета и на серверите. како прелистувачи. На крајот на краиштата, сега имаме дури и природни 64 битни цели броеви на јазикот! Тоа е далеку подобро од сè што е складирано во 64-битни плови!

ExpressJS е веројатно најлесниот Node.js сервер за користење, така што ќе направиме брза и валкана апликација Node.js/ExpressJS за да го опслужиме нашиот бројач.

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

Овој код всушност беше полесен за пишување од верзиите на Python, иако мајчин JavaScript станува прилично незгоден кога апликациите стануваат поголеми и сите обиди да се поправи ова, како што е TypeScript, брзо стануваат пообемни од Python.

Ајде да видиме како функционира ова!

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

Можеби сте слушнале антички (антички според стандардите на Интернет...) народни приказни за Node.js&#x27; брзина, а тие приказни се главно вистинити благодарение на спектакуларната работа што Google ја направи со V8 JavaScript моторот. Меѓутоа, во овој случај, иако нашата брза апликација ја надминува скриптата Flask, нејзината природа со единечна нишка е поразена од четирите асинхронизирани процеси со кои управува витезот Старлета кој вели „Ni!“.

Ајде да добиеме уште некоја помош!

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

Во ред! Сега е дури четири на четири битка! Ајде да бидеме репер!

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

Сè уште не е сосема на ниво на Starlette, но не е лошо за брзо хакирање на JavaScript од пет минути. Од моето сопствено тестирање, оваа скрипта всушност се задржува малку на ниво на интерфејс на базата на податоци бидејќи node-postgres не е ни приближно ефикасен како psycopg за Python. Префрлувањето на sqlite како двигател на базата на податоци дава над 3000 барања во секунда за истиот код ExpressJS.

Главната работа што треба да се забележи е дека и покрај бавната брзина на извршување на Python, ASGI рамки всушност можат да бидат конкурентни со решенијата Node.js за одредени оптоварувања.

Рѓа/Актикс

Така, сега се доближуваме до врвот на планината, а под планина мислам на највисоките резултати забележани од глувци и мажи подеднакво.

Ако ги погледнете повеќето од рамковните репери достапни на веб, ќе забележите дека има два јазика кои имаат тенденција да доминираат на врвот: C++ и Rust. Работев со C++ од 90-тите, па дури и имав своја Win32 C++ рамка уште пред да биде нешто MFC/ATL, така што имам многу искуство со јазикот. Не е многу забавно да се работи со нешто кога веќе го знаете, па наместо тоа ќе направиме верзија на Rust. ;)

Rust е релативно нов што се однесува до програмските јазици, но стана предмет на љубопитност за мене кога Линус Торвалдс објави дека ќе го прифати Rust како програмски јазик на Linux кернелот. За нас постарите програмери, тоа е отприлика исто како да се каже дека овој нов хипик од Њу Ејџ ќе биде нов амандман на Уставот на САД.

Сега, кога сте искусен програмер, имате тенденција да не скокате толку брзо како помладите луѓе, или во спротивно може да се изгорите од брзите промени на јазикот или библиотеките. (Секој што ја користел првата верзија на AngularJS ќе знае за што зборувам.) Rust е сè уште малку во таа фаза на експериментален развој и ми е смешно што толку многу примери на код на веб не ни компајлирај повеќе со тековните верзии на пакети.

Сепак, перформансите прикажани од апликациите на Rust не можат да се негираат. Ако никогаш не сте пробале рипгреп или fd-најди надвор на големите дрвја на изворниот код, дефинитивно треба да им дадете спин. Тие се достапни дури и за повеќето дистрибуции на Linux едноставно од менаџерот на пакети. Разменувате зборливост за изведба со Rust... a многу на говорност за а многу на изведбата.

Целосниот код за Rust е малку голем, па ние само ќе ги погледнеме релевантните управувачи овде:

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

Ова е многу покомплицирано од верзиите 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

И многу повеќе перформанси!

Нашиот Rust сервер кој користи Actix/deadpool_postgres лесно го победи нашиот претходен шампион Starlette за +125%, ExpressJS за +362% и чистиот PHP за +1366%. (Ќе ја оставам делтата на изведбата со верзијата на Ларавел како вежба за читателот.)

Открив дека учењето на јазикот Rust сам по себе е потешко од другите јазици, бидејќи има многу повеќе грешки од сè што сум видел надвор од 6502 Assembly, но ако вашиот Rust сервер може да заземе 14 пати повеќе од корисници како ваш PHP сервер, тогаш можеби има нешто да се добие со префрлување технологии на крајот на краиштата. Тоа е причината зошто следната верзија на Pafera Framework ќе се базира на Rust. Кривата на учење е многу повисока од јазиците за скриптирање, но изведбата ќе вреди. Ако не можете да одвоите време за да го научите Rust, тогаш засновањето на технолошкиот куп на Starlette или Node.js не е ниту лоша одлука.

Технички долг

Во последните дваесет години, преминавме од евтини статични локации за хостирање до споделени хостинг со стекови LAMP до изнајмување VPS на AWS, Azure и други облак услуги. Во денешно време, многу компании се задоволни со донесување одлуки за дизајн врз основа на кој и да е достапно или најевтино, бидејќи појавата на практични облак услуги го олесни фрлањето повеќе хардвер на бавните сервери и апликации. Ова им даде големи краткорочни добивки по цена на долгорочен технички долг.

Предупредување на генералниот хирург од Калифорнија: Ова не е вистинско вселенско куче.

Пред 70 години имаше голема вселенска трка меѓу Советскиот Сојуз и САД. Советите ги освоија повеќето од раните пресвртници. Тие го имаа првиот сателит во Спутник, првото куче во вселената во Лајка, првото вселенско летало на месечината во Луна 2, првиот маж и жена во вселената во Јуриј Гагарин и Валентина Терешкова и така натаму...

Но, тие полека акумулираа технички долг.

Иако Советите беа први за секое од овие достигнувања, нивните инженерски процеси и цели ги натераа да се фокусираат на краткорочни предизвици наместо на долгорочна изводливост. Победуваа секој пат кога скокаа, но стануваа се поуморни и побавни додека нивните противници продолжија да чекорат кон финишот.

Откако Нил Армстронг ги направи своите историски чекори на Месечината на телевизија во живо, Американците го презедоа водството, а потоа останаа таму додека советската програма пропадна. Ова не се разликува од компаниите денес кои се фокусираа на следната голема работа, на следната голема исплата или на следната голема технологија, додека не успеаја да развијат соодветни навики и стратегии за долги патеки.

Да се ​​биде прв на пазарот не значи дека ќе станете доминантен играч на тој пазар. Алтернативно, одвојувањето време да ги правите работите правилно не гарантира успех, но секако ги зголемува вашите шанси за долгорочни достигнувања. Ако вие сте водечка технологија за вашата компанија, изберете ја вистинската насока и алатки за вашиот обем на работа. Не дозволувајте популарноста да ги замени перформансите и ефикасноста.

Ресурси

Сакате да преземете датотека 7z што ги содржи скриптите Rust, ExpressJS, Flask, Starlette и Pure PHP?

За авторот

Џим програмира откако доби IBM PS/2 назад во текот на 90-тите. До денес, тој сè уште претпочита рачно да пишува HTML и SQL и се фокусира на ефикасноста и исправноста во својата работа.