بله، ویرجینیا، *آیا* وجود دارد بابا نوئل تفاوت بین چارچوب های وب در سال 2023

سفر یک برنامه نویس سرکش برای یافتن کد سرور وب با عملکرد سریع قبل از اینکه تسلیم فشار بازار و بدهی فنی شود.
2023-03-24 11:52:06
👁️ 791
💬 0

مطالب

  1. مقدمه
  2. آزمون
  3. PHP/Laravel
  4. پی اچ پی خالص
  5. بازدید مجدد از لاراول
  6. جانگو
  7. فلاسک
  8. استارلت
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. بدهی فنی
  12. منابع

مقدمه

پس از یکی از جدیدترین مصاحبه های شغلی ام، با تعجب متوجه شدم که شرکتی که برای آن درخواست دادم هنوز از لاراول استفاده می کند، یک چارچوب PHP که حدود یک دهه پیش آن را امتحان کردم. برای آن زمان مناسب بود، اما اگر ثابتی در فناوری و مد وجود داشته باشد، تغییر مستمر و ظهور مجدد سبک ها و مفاهیم است. اگر شما یک برنامه نویس جاوا اسکریپت هستید، احتمالاً با این جوک قدیمی آشنا هستید.

برنامه نویس 1: "من این چارچوب جدید جاوا اسکریپت را دوست ندارم!"

برنامه نویس 2: "نیازی به نگرانی نیست. فقط شش ماه صبر کنید تا یکی دیگر جایگزین شود!"

از روی کنجکاوی، تصمیم گرفتم ببینم وقتی قدیم و جدید را امتحان می کنیم دقیقاً چه اتفاقی می افتد. البته، وب مملو از معیارها و ادعاهایی است که احتمالاً محبوب ترین آنها است معیارهای چارچوب وب TechEmpower در اینجا . با این حال، ما امروز هیچ کاری به پیچیدگی آنها انجام نمی دهیم. ما همه چیز را زیبا و ساده نگه می داریم تا این مقاله تبدیل به جنگ و صلح , و اینکه تا زمانی که مطالعه را تمام کنید، شانس کمی برای بیدار ماندن خواهید داشت. اخطارهای معمول اعمال می شود: این ممکن است روی دستگاه شما یکسان عمل نکند، نسخه های مختلف نرم افزار می توانند بر عملکرد تأثیر بگذارند، و گربه شرودینگر در واقع به یک گربه زامبی تبدیل شد که نیمه زنده و نیمه مرده دقیقاً در همان زمان بود.

آزمون

محیط تست

همانطور که در اینجا نشان داده شده است، برای این تست، از لپ تاپم مجهز به i5 ضعیفی که لینوکس Manjaro را اجرا می کند، استفاده خواهم کرد.

╰─➤  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. شناسه جلسه کاربر فعلی را از یک کوکی بخوانید
  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 اجازه دهیم آن را به سرعت از پنجره برای کاربر پرتاب کند، در این صورت می‌توانیم به 32000 کاربر در ثانیه خدمات رسانی کنیم و عملکرد را 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

Static index.en.html بخشی است که به همه می رسد و فقط قسمت هایی که بر اساس کاربر متفاوت هستند در sessionvars.js ارسال می شوند. این نه تنها بار پایگاه داده را کاهش می دهد و تجربه بهتری را برای کاربران ما ایجاد می کند، بلکه احتمالات کوانتومی را نیز کاهش می دهد که سرور ما به طور خود به خود در هنگام حمله کلینگون ها در یک شکست هسته تار تبخیر شود.

الزامات کد

کد بازگشتی برای هر فریم ورک یک شرط ساده دارد: به کاربر نشان دهید چند بار صفحه را با گفتن "شمارش x است" بازخوانی کرده است. برای ساده نگه داشتن همه چیز، فعلاً از صف‌های Redis، مؤلفه‌های Kubernetes یا AWS Lambdas خودداری می‌کنیم.

نمایش چند بار از صفحه بازدید کرده اید

داده های جلسه هر کاربر در پایگاه داده PostgreSQL ذخیره می شود.

جدول جلسات کاربران

و این جدول پایگاه داده قبل از هر آزمون کوتاه می شود.

جدول پس از کوتاه شدن

شعار پافرا ساده و در عین حال موثر است... به هر حال خارج از تاریک ترین جدول زمانی...

نتایج آزمون واقعی

PHP/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

// ====================================================================
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 را انجام می دهیم.

اگر فکر می کنید که مشکلی در این تست وجود دارد، باید با نویسنده چارچوب PHP Lucinda صحبت کنید. در نتایج آزمایش خود، او دارد لوسیندا لاراول را شکست داد 36 برابر برای درخواست های HTML و 90 برابر برای درخواست های JSON.

بعد از تست روی دستگاه خودم با آپاچی و نگینکس، دلیلی برای شک ندارم. لاراول واقعا عادلانه است که کند! پی اچ پی به خودی خود آنقدر بد نیست، اما زمانی که تمام پردازش های اضافی را که لاراول به هر درخواست اضافه می کند اضافه کنید، پیشنهاد لاراول به عنوان یک انتخاب در سال 2023 برای من بسیار دشوار است.

جانگو

حساب های PHP/Wordpress برای حدود 40 درصد از تمام وب سایت های موجود در وب ، آن را تا حد زیادی به غالب ترین چارچوب تبدیل می کند. با این حال، شخصاً متوجه می شوم که محبوبیت لزوماً به کیفیت تبدیل نمی شود تا اینکه احساس می کنم میل ناگهانی غیرقابل کنترلی برای آن غذای لذیذ فوق العاده از محبوب ترین رستوران دنیا ... مک دونالد. از آنجایی که قبلاً کد PHP خالص را آزمایش کرده‌ایم، خود وردپرس را آزمایش نمی‌کنیم، زیرا هر چیزی که شامل وردپرس باشد بدون شک کمتر از ۷۰۰ درخواست در ثانیه است که با PHP خالص مشاهده کردیم.

جنگو یکی دیگر از فریمورک های محبوبی است که مدت هاست وجود داشته است. اگر در گذشته از آن استفاده کرده‌اید، احتمالاً رابط مدیریت پایگاه داده تماشایی آن را با علاقه به یاد می‌آورید و همچنین پیکربندی همه چیز را به همان صورتی که می‌خواهید آزاردهنده بود. بیایید ببینیم جنگو در سال 2023 چقدر خوب کار می کند، به خصوص با رابط کاربری جدید ASGI که ​​در نسخه 4.0 اضافه کرده است.

راه‌اندازی جنگو به طرز قابل ملاحظه‌ای شبیه راه‌اندازی لاراول است، زیرا هر دو از دورانی بودند که معماری‌های 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 برابر نسخه لاراول است. جنگو در برابر لاراول به نظر می رسد که اصلا رقابتی نیست.

فلاسک

جدا از چهارچوب‌های بزرگتر همه چیز از جمله سینک آشپزخانه، چارچوب‌های کوچک‌تری نیز وجود دارند که فقط برخی از تنظیمات اولیه را انجام می‌دهند و به شما اجازه می‌دهند بقیه موارد را مدیریت کنید. یکی از بهترین موارد برای استفاده فلاسک و همتای 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 خالص است. من متوجه شدم که از بین تمام زبان‌هایی که استفاده کرده‌ام، پایتون احتمالاً رساترین زبان از نظر فشار دادن کلید تایپ شده است. فقدان پرانتز و پرانتز، درک فهرست و دستور، و مسدود کردن بر اساس تورفتگی به جای نیم ویرگول، پایتون را نسبتاً ساده و در عین حال از نظر قابلیت‌های قدرتمند می‌سازد.

متأسفانه، پایتون نیز با وجود نرم افزارهای زیادی که در آن نوشته شده، کندترین زبان هدف عمومی موجود است. تعداد کتابخانه‌های پایتون در دسترس حدود چهار برابر بیشتر از زبان‌های مشابه است و تعداد زیادی از دامنه‌ها را پوشش می‌دهد، با این حال هیچ‌کس نمی‌گوید پایتون سریع است و در خارج از جایگاه‌هایی مانند 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 ما تمام تنظیمات اولیه و پیکربندی خود را هنگام راه اندازی سرور گانیکورن انجام می دهد، در حالی که PHP هر بار که درخواست جدیدی وارد می شود، اسکریپت را دوباره اجرا می کند. معادل فلاسک است که راننده تاکسی جوان و مشتاقی است که قبلاً ماشین را روشن کرده و در کنار جاده منتظر است، در حالی که PHP راننده قدیمی است که در خانه خود می ماند و منتظر تماس تلفنی می ماند و تنها پس از آن رانندگی می کند. برای بلند کردن شما از آنجایی که یک دانش آموز قدیمی هستید و از روزهایی که PHP یک تغییر فوق العاده برای فایل های ساده HTML و SHTML بود، کمی غم انگیز است که متوجه شوید چقدر زمان گذشته است، اما تفاوت های طراحی واقعاً کار را برای PHP سخت می کند. با سرورهای پایتون، جاوا و Node.js رقابت کنید که فقط در حافظه باقی می مانند و درخواست ها را به سادگی یک شعبده باز انجام می دهند.

استارلت

Flask ممکن است سریعترین فریمورک ما تا کنون باشد، اما در واقع نرم افزار بسیار قدیمی است. چند سال پیش جامعه پایتون به سرورهای ناهمزمان 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",
)

همانطور که می بینید، تقریباً با چند تغییر مسیریابی و 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 را پوشش داده ایم. با این حال، بخش بزرگی از جهان در واقع از جاوا، DotNet، Node.js، Ruby on Rails و سایر فناوری‌های مشابه برای وب‌سایت‌های خود استفاده می‌کنند. این به هیچ وجه یک نمای کلی از تمام اکوسیستم ها و زیست بوم های جهان نیست، بنابراین برای جلوگیری از انجام برنامه نویسی معادل شیمی آلی، ما فقط چارچوب هایی را انتخاب می کنیم که راحت ترین کد را برای آنها تایپ کنید. که جاوا قطعاً از آن نیست.

مگر اینکه زیر کپی K&R C یا Knuth پنهان شده باشید هنر برنامه نویسی کامپیوتر در پانزده سال گذشته، احتمالاً نام Node.js را شنیده اید. کسانی از ما که از ابتدای جاوا اسکریپت در اطراف بوده‌اند، یا به طرز باورنکردنی ترسیده‌اند، شگفت زده شده‌اند، یا هر دو از وضعیت جاوا اسکریپت مدرن می‌ترسند، اما نمی‌توان انکار کرد که جاوا اسکریپت به نیرویی برای سرورها تبدیل شده است. به عنوان مرورگر پس از همه، ما حتی اعداد صحیح 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}`));

نوشتن این کد در واقع آسان‌تر از نسخه‌های پایتون بود، اگرچه جاوا اسکریپت بومی زمانی که برنامه‌ها بزرگتر می‌شوند بسیار سخت می‌شود و تمام تلاش‌ها برای تصحیح آن مانند 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; سرعت، و این داستان ها عمدتاً به لطف کار خیره کننده ای که گوگل با موتور جاوا اسکریپت V8 انجام داده است، واقعی هستند. اگرچه در این مورد، اگرچه برنامه سریع ما بهتر از اسکریپت Flask عمل می‌کند، اما ماهیت تک رشته‌ای آن توسط چهار فرآیند همگام‌سازی که توسط شوالیه Starlette به کار می‌رود که می‌گوید «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 نیست، اما برای یک هک سریع جاوا اسکریپت پنج دقیقه ای بد نیست. از آزمایش خودم، این اسکریپت در واقع کمی در سطح رابط پایگاه داده نگه داشته شده است، زیرا node-postgres به اندازه psycopg برای پایتون کارآمد نیست. تغییر به sqlite به عنوان درایور پایگاه داده بیش از 3000 درخواست در ثانیه برای همان کد ExpressJS ایجاد می کند.

نکته اصلی این است که علیرغم سرعت اجرای پایین پایتون، فریمورک های ASGI در واقع می توانند با راه حل های Node.js برای بارهای کاری خاص رقابت کنند.

Rust/Actix

بنابراین اکنون، ما به بالای کوه نزدیک‌تر می‌شویم، و منظور من از کوه، بالاترین امتیازات معیار ثبت شده توسط موش‌ها و مردان است.

اگر به اکثر معیارهای چارچوب موجود در وب نگاه کنید، متوجه خواهید شد که دو زبان وجود دارند که تمایل دارند در بالا تسلط داشته باشند: C++ و Rust. من از دهه 90 با C++ کار می‌کردم، و حتی قبل از اینکه MFC/ATL یک چیز بود، فریمورک Win32 C++ خودم را داشتم، بنابراین تجربه زیادی با این زبان دارم. کار کردن با چیزی در حالی که قبلاً آن را می‌دانید چندان سرگرم‌کننده نیست، بنابراین ما به جای آن یک نسخه Rust را انجام می‌دهیم. ;)

Rust از نظر زبان‌های برنامه‌نویسی نسبتاً جدید است، اما وقتی لینوس توروالدز اعلام کرد که Rust را به عنوان یک زبان برنامه‌نویسی هسته لینوکس می‌پذیرد، برای من یک موضوع کنجکاوی شد. برای ما برنامه‌نویس‌های قدیمی‌تر، این تقریباً شبیه به این است که بگوییم این هیپی‌های هیپی عصر جدید، اصلاحیه جدیدی برای قانون اساسی ایالات متحده خواهد بود.

اکنون، زمانی که شما یک برنامه نویس با تجربه هستید، تمایل دارید که به سرعت افراد جوان تر از آن استفاده نکنید، در غیر این صورت ممکن است با تغییرات سریع در زبان یا کتابخانه ها دچار سوختگی شوید. (هرکسی که از اولین نسخه AngularJS استفاده کرده باشد می‌داند که من در مورد چه چیزی صحبت می‌کنم.) Rust هنوز تا حدودی در آن مرحله توسعه آزمایشی است، و به نظر من خنده‌دار است که بسیاری از نمونه‌های کد در وب حتی نمی‌شوند. دیگر با نسخه های فعلی بسته ها کامپایل کنید.

با این حال، عملکرد نشان داده شده توسط برنامه های Rust را نمی توان انکار کرد. اگر هرگز امتحان نکرده اید ریپگرپ یا fd-find در درختان کد منبع بزرگ، شما قطعا باید به آنها چرخش دهید. آنها حتی برای اکثر توزیع های لینوکس به سادگی از طریق مدیر بسته در دسترس هستند. شما پرحرفی را با اجرا با 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 دانلود کنید؟

درباره نویسنده

جیم از زمانی که در دهه 90 یک IBM PS/2 دریافت کرد، به برنامه نویسی پرداخت. تا به امروز، او همچنان نوشتن HTML و SQL را با دست ترجیح می دهد و روی کارایی و درستی کار خود تمرکز می کند.