Так, Вірджынія, *Ёсць* а Дзед Мароз Розніца паміж Web Frameworks у 2023 годзе

Падарожжа аднаго дзёрзкага праграміста, каб знайсці хуткадзейны код вэб-сервера, перш чым ён паддасца ціску рынку і тэхнічнай запазычанасці
2023-03-24 11:52:06
👁️ 775
💬 0

Змест

  1. Уводзіны
  2. Тэст
  3. PHP/Laravel
  4. Чысты PHP
  5. Перагляд Laravel
  6. Джанга
  7. Колба
  8. Старлетка
  9. Node.js/ExpressJS
  10. Іржа/Actix
  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. Счытванне ідэнтыфікатара сеанса бягучага карыстальніка з файла cookie
  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 карыстальнікаў у секунду, павялічваючы прадукцыйнасць у 50 разоў.

╰─➤  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. Гэта не толькі зніжае нагрузку на базу дадзеных і стварае лепшы вопыт для нашых карыстальнікаў, але таксама памяншае квантавую верагоднасць таго, што наш сервер самаадвольна выпарыцца ў выніку ўзлому варп-ядра пры атацы клінгонаў.

Патрабаванні да кодэкса

Вернуты код для кожнай структуры будзе мець адно простае патрабаванне: паказаць карыстальніку, колькі разоў яны абнаўлялі старонку, сказаўшы "Ліч роўна х". Каб зрабіць усё прасцей, пакуль мы будзем трымацца далей ад чэргаў Redis, кампанентаў Kubernetes або AWS Lambda.

Паказвае, колькі разоў вы наведалі старонку

Даныя сесіі кожнага карыстальніка будуць захоўвацца ў базе даных PostgreSQL.

Табліца сесій карыстальнікаў

І гэтая табліца базы дадзеных будзе абрэзана перад кожным тэстам.

Табліца пасля скарачэння

Просты, але эфектыўны дэвіз Pafera... у любым выпадку за межамі самай цёмнай шкалы часу...

Фактычныя вынікі выпрабаванняў

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);

І ўсталюйце кантролер для адлюстравання падліку. Laravel па змаўчанні захоўвае сеансы ў базе дадзеных. Гэта таксама забяспечвае 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 запыту ў секунду, калі робіць "Count is 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 радкоў кода, каб зрабіць тое, што зрабілі чатыры радкі кода (і цэлая куча канфігурацыйных работ) у Laravel. (Вядома, калі б мы рабілі належную апрацоўку памылак і паведамленні для карыстальнікаў, гэта было б прыкладна ўдвая больш, чым колькасць радкоў.) Магчыма, мы можам дасягнуць 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 нічога дрэннага, магчыма, мы няправільна наладзілі Laravel?

Перагляд Laravel

Пасля пошуку ў Інтэрнэце праблем з канфігурацыяй і парад па прадукцыйнасці двума найбольш папулярнымі метадамі было кэшаванне даных канфігурацыі і маршрутызацыі, каб пазбегнуць іх апрацоўкі для кожнага запыту. Таму мы скарыстаемся іх парадамі і апрабуем гэтыя парады.

╰─➤  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. У яго выніках тэстаў, ён мае Люсінда перамагае Laravel у 36 разоў для запытаў HTML і ў 90 разоў для запытаў JSON.

Пасля тэставання на маёй уласнай машыне з Apache і Nginx у мяне няма прычын сумнявацца ў ім. Laravel сапраўды проста што павольна! PHP сам па сабе не такі ўжо і дрэнны, але як толькі вы дадаеце ўсю дадатковую апрацоўку, якую Laravel дадае да кожнага запыту, мне вельмі цяжка рэкамендаваць Laravel у якасці выбару ў 2023 годзе.

Джанга

Уліковы запіс PHP/Wordpress каля 40% усіх вэб-сайтаў у Інтэрнэце , што робіць яго найбольш дамінуючай структурай. Тым не менш, асабіста я лічу, што папулярнасць не абавязкова пераходзіць у якасць, як і ў мяне ўзнікла раптоўнае некантралюемае жаданне атрымаць незвычайную вытанчаную ежу з самы папулярны рэстаран у свеце ... McDonald&#x27;s. Паколькі мы ўжо тэставалі чысты 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}")

Чатыры радкі кода такія ж, як і ў версіі Laravel. Давайце паглядзім, як гэта працуе.

╰─➤  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. Django супраць 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 паўторна выконвае скрыпт кожны раз, калі паступае новы запыт. Гэта&#x27 ;эквівалентна таму, што 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 запыты ў секунду, што азначае 6-кратнае паляпшэнне ў параўнанні з чыстым PHP і 4-кратнае паляпшэнне ў параўнанні з Flask.

Калі вы яшчэ не змянілі свой код WSGI Python на ASGI, цяпер можа быць добры час, каб пачаць.

Node.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, яго аднаструменны характар ​​пераможаны чатырма асінхроннымі працэсамі, якімі валодае Starlette Knight, які кажа &quot;Ni!&quot;.

Атрымаем яшчэ дапамогу!

╰─➤  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 для пэўных працоўных нагрузак.

Іржа/Actix

Такім чынам, цяпер мы набліжаемся да вяршыні гары, а пад гарой я маю на ўвазе самыя высокія эталонныя вынікі, зафіксаваныя як мышамі, так і людзьмі.

Калі вы паглядзіце на большасць тэстаў фрэймворка, даступных у Інтэрнэце, вы заўважыце, што ёсць дзве мовы, якія, як правіла, дамінуюць у топе: C++ і Rust. Я працаваў з C++ з 90-х, і ў мяне нават была ўласная фрэймворк Win32 C++ яшчэ да з'яўлення MFC/ATL, так што ў мяне вялікі досвед працы з гэтай мовай. Працаваць з чымсьці, калі вы гэта ўжо ведаеце, не так весела, таму замест гэтага мы збіраемся зрабіць версію Rust. ;)

Што тычыцца моў праграмавання, Rust з'яўляецца адносна новым, але ён стаў прадметам цікаўнасці для мяне, калі Лінус Торвальдс абвясціў, што ён прыме Rust у якасці мовы праграмавання ядра Linux. Для нас, старэйшых праграмістаў, гэта прыкладна тое ж самае, што сказаць, што гэтая навамодная штучка хіпі новага часу будзе новай папраўкай да Канстытуцыі ЗША.

Цяпер, калі вы дасьведчаны праграміст, вы, як правіла, не кідаецеся на падножку так хутка, як гэта робяць маладыя людзі, інакш вы можаце апячыся ад хуткіх зьменаў у мове ці бібліятэках. (Кожны, хто карыстаўся першай версіяй AngularJS, зразумее, пра што я кажу.) Rust усё яшчэ знаходзіцца ў некаторай ступені на стадыі эксперыментальнай распрацоўкі, і мне здаецца смешным, што столькі прыкладаў кода ў Інтэрнэце нават не&#x27; кампіляваць больш з бягучымі версіямі пакетаў.

Аднак нельга адмаўляць прадукцыйнасць, паказаную праграмамі Rust. Калі вы ніколі не спрабавалі ripgrep або 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%. (Я пакіну дэльту прадукцыйнасці з версіяй Laravel як практыкаванне для чытача.)

Я выявіў, што вывучэнне самой мовы Rust было складаней, чым іншых моў, паколькі яна выклікае значна больш праблем, чым што-небудзь, што я бачыў па-за межамі зборкі 6502, але калі ваш сервер Rust можа прыняць у 14 разоў большую колькасць карыстальнікаў у якасці вашага PHP-сервера, то, магчыма, у рэшце рэшт, можна нешта атрымаць з пераключэннем тэхналогій. Вось чаму наступная версія Pafera Framework будзе заснавана на Rust. Крывая навучання нашмат вышэй, чым мовы сцэнарыяў, але прадукцыйнасць таго вартая. Калі вы не можаце выдаткаваць час на вывучэнне Rust, то заснаваць свой тэхнічны стэк на Starlette або Node.js таксама не будзе дрэнным рашэннем.

Тэхнічная запазычанасць

За апошнія дваццаць гадоў мы перайшлі ад танных сайтаў статычнага хостынгу да агульнага хостынгу са стэкамі LAMP і да арэнды VPS для AWS, Azure і іншых воблачных сэрвісаў. У наш час многія кампаніі задавальняюцца прыняццем дызайнерскіх рашэнняў у залежнасці ад таго, каго яны знойдуць даступным або самым танным, паколькі з'яўленне зручных воблачных сэрвісаў дазволіла лёгка выкарыстоўваць больш апаратнага забеспячэння для павольных сервераў і прыкладанняў. Гэта дало ім вялікую кароткатэрміновую выгаду за кошт доўгатэрміновай тэхнічнай запазычанасці.

Папярэджанне галоўнага хірурга Каліфорніі: гэта не сапраўдны касмічны сабака.

70 гадоў таму адбылася вялікая касмічная гонка паміж Савецкім Саюзам і ЗША. Саветы выйгралі большасць першых этапаў. У іх быў першы спадарожнік у Sputnik, першы сабака ў космасе ў Лайка, першы касмічны карабель на месяц у Луна-2, першыя мужчына і жанчына ў космасе Юрый Гагарын і Валянціна Церашкова, і гэтак далей ...

Але яны паціху назапашвалі тэхнічную запазычанасць.

Нягледзячы на ​​тое, што Саветы былі першымі ў кожным з гэтых дасягненняў, іх інжынерныя працэсы і мэты прымушалі іх засяроджвацца на кароткатэрміновых задачах, а не на доўгатэрміновых мэтазгоднасцях. Яны перамагалі кожны раз, калі скакалі, але яны станавіліся ўсё больш стомленымі і павольнымі, у той час як іх супернікі працягвалі рабіць паслядоўныя крокі да фінішу.

Пасля таго, як Ніл Армстранг зрабіў свае гістарычныя крокі па Месяцы ў прамым эфіры, амерыканцы захапілі ініцыятыву, а потым засталіся там, бо савецкая праграма хісталася. Гэта нічым не адрозніваецца ад сучасных кампаній, якія засяродзіліся на наступнай вялікай справе, наступнай буйной выгадзе або наступнай буйной тэхніцы, але не змаглі выпрацаваць правільныя звычкі і стратэгіі на доўгатэрміновую перспектыву.

Быць першым на рынку не азначае, што вы станеце дамінуючым гульцом на гэтым рынку. У якасці альтэрнатывы, знаходзячы час, каб зрабіць усё правільна, не гарантуе поспеху, але, безумоўна, павялічвае вашы шанцы на доўгатэрміновыя дасягненні. Калі вы тэхнічны кіраўнік сваёй кампаніі, абярыце правільны кірунак і інструменты для сваёй працоўнай нагрузкі. Не дазваляйце папулярнасці замяніць прадукцыйнасць і эфектыўнасць.

Рэсурсы

Хочаце загрузіць файл 7z, які змяшчае скрыпты Rust, ExpressJS, Flask, Starlette і Pure PHP?

Пра аўтара

Джым займаецца праграмаваннем з таго часу, як атрымаў IBM PS/2 яшчэ ў 90-я гады. Па гэты дзень ён па-ранейшаму аддае перавагу напісанню HTML і SQL ад рукі, а ў сваёй працы робіць стаўку на эфектыўнасць і карэктнасць.