Evet, Virginia, Bir *Var* Noel Baba 2023'te Web Çerçeveleri Arasındaki Farklar

Piyasa baskısına ve teknik borca yenik düşmeden önce hızlı performans gösteren web sunucusu kodunu bulma yolculuğunda azimli bir programcı
2023-03-24 11:52:06
👁️ 797
💬 0

İçindekiler

  1. giriiş
  2. Test
  3. PHP/Laravel
  4. Saf PHP
  5. Laravel'i Yeniden Ziyaret Etmek
  6. Django
  7. Şişe
  8. Yıldız kız
  9. Düğüm.js/ExpressJS
  10. Pas/Actix
  11. Teknik Borç
  12. Kaynaklar

giriiş

En son iş görüşmelerimden birinin ardından, başvurduğum şirketin yaklaşık on yıl önce denediğim bir PHP çerçevesi olan Laravel'i hâlâ kullandığını fark ettiğimde şaşırdım. O zamanlar için fena değildi, ancak teknoloji ve modada bir sabit varsa, o da stillerin ve kavramların sürekli değişmesi ve yeniden ortaya çıkmasıdır. Eğer bir JavaScript programcısıysanız, muhtemelen bu eski şakayı biliyorsunuzdur

Programcı 1: "Bu yeni JavaScript çerçevesini beğenmedim!"

Programcı 2: "Endişelenmeye gerek yok. Sadece altı ay bekleyin, yerini alacak başkası gelecek!"

Meraktan, eski ve yeniyi test ettiğimizde tam olarak ne olacağını görmeye karar verdim. Elbette, web kıyaslamalar ve iddialarla dolu, bunların en popüleri muhtemelen TechEmpower Web Çerçevesi Karşılaştırmaları burada . Bugün onlar kadar karmaşık bir şey yapmayacağız. Her şeyi güzel ve basit tutacağız, böylece bu makale bir Savaş ve Barış , ve okumayı bitirdiğinizde uyanık kalma şansınızın da az da olsa olacağı. Her zamanki uyarılar geçerli: bu sizin makinenizde aynı şekilde çalışmayabilir, farklı yazılım sürümleri performansı etkileyebilir ve Schrödinger'in kedisi aslında aynı anda yarı canlı yarı ölü olan bir zombi kediye dönüştü.

Test

Test Ortamı

Bu test için, burada gösterildiği gibi Manjaro Linux çalıştıran zayıf bir i5'e sahip dizüstü bilgisayarımı kullanacağım.

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

Eldeki Görev

Kodumuz her istek için üç basit göreve sahip olacak:

  1. Geçerli kullanıcının oturum kimliğini bir çerezden oku
  2. Bir veritabanından ek bilgi yükleyin
  3. Bu bilgiyi kullanıcıya geri döndür

Bu ne tür bir aptalca test diye sorabilirsiniz? Pekala, bu sayfa için ağ isteklerine bakarsanız, sessionvars.js adında, tam olarak aynı şeyi yapan bir tane göreceksiniz.

sessionvars.js'nin içeriği

Görüyorsunuz, modern web sayfaları karmaşık yaratıklardır ve en yaygın görevlerden biri de veritabanı sunucusunda aşırı yükü önlemek için karmaşık sayfaları önbelleğe almaktır.

Karmaşık bir sayfayı her kullanıcı istediğinde yeniden oluşturursak, saniyede yalnızca yaklaşık 600 kullanıcıya hizmet verebiliriz.

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

Ancak bu sayfayı statik bir HTML dosyası olarak önbelleğe alırsak ve Nginx'in bunu hızla kullanıcıya göndermesine izin verirsek, saniyede 32.000 kullanıcıya hizmet verebilir ve performansı 50 kat artırabiliriz.

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

Statik index.en.html herkese giden kısımdır ve sadece kullanıcıya göre farklılık gösteren kısımlar sessionvars.js'de gönderilir. Bu sadece veritabanı yükünü azaltmakla ve kullanıcılarımız için daha iyi bir deneyim yaratmakla kalmaz, aynı zamanda Klingonlar saldırdığında sunucumuzun bir warp çekirdeği ihlalinde kendiliğinden buharlaşmasının kuantum olasılıklarını da azaltır.

Kod Gereksinimleri

Her çerçeve için döndürülen kodun basit bir gereksinimi olacak: kullanıcıya sayfayı kaç kez yenilediğini "Sayı x" diyerek göster. İşleri basit tutmak için şimdilik Redis kuyruklarından, Kubernetes bileşenlerinden veya AWS Lambdalardan uzak duracağız.

Sayfayı kaç kez ziyaret ettiğinizi gösteriyor

Her kullanıcının oturum verileri PostgreSQL veritabanına kaydedilecektir.

Kullanıcılar oturumları tablosu

Ve bu veritabanı tablosu her testten önce kesilecektir.

Kesildikten sonraki tablo

Basit ama etkili olan budur Pafera'nın sloganı... en karanlık zaman diliminin dışında bile...

Gerçek Test Sonuçları

PHP/Laravel

Tamam, şimdi nihayet ellerimizi kirletmeye başlayabiliriz. Laravel için kurulumu atlayacağız çünkü bu sadece bir grup composer ve artisan komutu.

İlk olarak .env dosyasında veritabanı ayarlarımızı yapacağız

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

Daha sonra, her isteği kontrolcümüze gönderen tek bir geri dönüş rotası ayarlayacağız.

Route::fallback(SessionController::class);

Ve denetleyiciyi sayımı görüntüleyecek şekilde ayarlayın. Laravel, varsayılan olarak oturumları veritabanında depolar. Ayrıca şunları da sağlar: session() Oturum verilerimizle arayüz oluşturmak için bir fonksiyon kullandık, bu yüzden sayfamızı oluşturmak için sadece birkaç satır kod yazmamız yeterliydi.

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

    $count  += 1;

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

    return 'Count is ' . $count;
  }
}

Php-fpm ve Nginx'i kurduktan sonra sayfamız oldukça güzel görünüyor...

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

En azından test sonuçlarını görene kadar...

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

Hayır, bu bir yazım hatası değil. Test makinemiz karmaşık bir sayfayı işlemek için saniyede 600 istekten... saniyede 21 istekle "Sayı 1" işlemeye geçti.

Peki ne yanlış gitti? PHP kurulumumuzda bir sorun mu var? Nginx php-fpm ile arayüz oluştururken bir şekilde yavaşlıyor mu?

Saf PHP

Bu sayfayı saf PHP koduyla yeniden yapalım.

<?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'];

Laravel'de dört satır kodun (ve bir sürü yapılandırma çalışmasının) yaptığı şeyi yapmak için artık 98 satır kod kullandık. (Elbette, eğer doğru hata işleme ve kullanıcıya dönük mesajlar yapsaydık, bu satır sayısının yaklaşık iki katı olurdu.) Belki saniyede 30 isteğe ulaşabiliriz?

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

Vay canına! Görünüşe göre PHP kurulumumuzda hiçbir sorun yok. Saf PHP sürümü saniyede 700 istek yapıyor.

PHP'de bir sorun yoksa belki Laravel'i yanlış yapılandırdık?

Laravel'i Yeniden Ziyaret Etmek

Web'de yapılandırma sorunları ve performans ipuçlarını taradıktan sonra, en popüler tekniklerden ikisi yapılandırmayı önbelleğe almak ve her istek için işlenmesini önlemek için verileri yönlendirmekti. Bu nedenle, onların tavsiyesini dinleyip bu ipuçlarını deneyeceğiz.

╰─➤  php artisan config:cache

   INFO  Configuration cached successfully.  

╰─➤  php artisan route:cache

   INFO  Routes cached successfully.  

Komut satırında her şey iyi görünüyor. Karşılaştırmayı tekrar yapalım.

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

Peki, şimdi saniyede 21.04'ten 28.80 isteğe kadar performansı artırdık, neredeyse %37'lik dramatik bir artış! Bu, herhangi bir yazılım paketi için oldukça etkileyici olurdu... saf PHP sürümünün istek sayısının yalnızca 1/24'ünü gerçekleştirdiğimiz gerçeği hariç.

Bu testte bir şeylerin yanlış olduğunu düşünüyorsanız, Lucinda PHP framework'ünün yazarıyla görüşmelisiniz. Test sonuçlarında, Lucinda Laravel'i yendi HTML istekleri için 36x ve JSON istekleri için 90x.

Hem Apache hem de Nginx ile kendi makinemde test ettikten sonra, ondan şüphe etmek için hiçbir nedenim yok. Laravel gerçekten sadece O yavaş! PHP kendi başına o kadar da kötü değil, ancak Laravel'in her isteğe eklediği tüm ekstra işlemleri eklediğinizde, 2023'te Laravel'i bir seçenek olarak önermeyi çok zor buluyorum.

Django

PHP/Wordpress hesapları web'deki tüm web sitelerinin yaklaşık %40'ı , bunu açık ara en baskın çerçeve haline getiriyor. Kişisel olarak, popülerliğin kaliteye dönüşmesinin zorunlu olmadığını düşünüyorum, tıpkı kendimi o olağanüstü gurme yemeğe karşı aniden kontrol edilemez bir istek duyarken bulmam gibi dünyanın en popüler restoranı ... McDonald's. Saf PHP kodunu zaten test ettiğimizden, Wordpress'in kendisini test etmeyeceğiz, çünkü Wordpress'i içeren herhangi bir şey, saf PHP ile gözlemlediğimiz saniyede 700 istekten şüphesiz daha düşük olacaktır.

Django uzun zamandır var olan bir diğer popüler framework'tür. Geçmişte kullandıysanız, muhteşem veritabanı yönetim arayüzünü ve her şeyi istediğiniz gibi yapılandırmanın ne kadar can sıkıcı olduğunu muhtemelen sevgiyle hatırlıyorsunuzdur. Django'nun 2023'te ne kadar iyi çalıştığını, özellikle de 4.0 sürümünden itibaren eklediği yeni ASGI arayüzüyle görelim.

Django'yu kurmak, Laravel'i kurmaya oldukça benzerdir, çünkü ikisi de MVC mimarilerinin şık ve doğru olduğu çağdan kalmadır. Sıkıcı yapılandırmayı atlayıp doğrudan görünümü kurmaya geçeceğiz.

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

Dört satır kod Laravel versiyonuyla aynıdır. Nasıl performans gösterdiğine bakalım.

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

Saniyede 355 istekle hiç de fena değil. Saf PHP sürümünün performansının sadece yarısı, ancak Laravel sürümünün de 12 katı. Django ve Laravel hiç de yarışmıyor gibi görünüyor.

Şişe

Mutfak lavabosu dahil her şeyi kapsayan daha büyük çerçevelerin yanı sıra, yalnızca bazı temel kurulumları yapıp gerisini sizin halletmenize izin veren daha küçük çerçeveler de vardır. Kullanılabilecek en iyilerinden biri Flask ve onun ASGI karşılığı olan Quart'tır. Benimki PaferaPy Çerçevesi Flask üzerine inşa edilmiştir, bu yüzden performansı korurken işlerin ne kadar kolay halledilebileceğini gayet iyi biliyorum.

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

Gördüğünüz gibi, Flask betiği saf PHP betiğinden daha kısadır. Kullandığım tüm diller arasında Python'un muhtemelen tuş vuruşları açısından en ifade edici dil olduğunu düşünüyorum. Parantez ve parantezlerin olmaması, liste ve sözlük kavrayışları ve noktalı virgül yerine girintiye dayalı engelleme Python'u oldukça basit ama yetenekleri açısından güçlü kılıyor.

Ne yazık ki, Python, içinde ne kadar çok yazılım yazılmış olursa olsun, aynı zamanda en yavaş genel amaçlı dildir. Mevcut Python kütüphanelerinin sayısı benzer dillerden yaklaşık dört kat daha fazladır ve çok sayıda alanı kapsar, ancak hiç kimse Python'un NumPy gibi nişler dışında hızlı veya performanslı olduğunu söyleyemez.

Flask versiyonumuzu önceki framework'lerimizle karşılaştıralım.

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 betiğimiz aslında saf PHP versiyonumuzdan daha hızlıdır!

Eğer buna şaşırıyorsanız, Flask uygulamamızın tüm başlatma ve yapılandırmasını gunicorn sunucusunu başlattığımızda yaptığını, PHP'nin ise her yeni istek geldiğinde betiği yeniden çalıştırdığını fark etmelisiniz. Bu, Flask'ın arabayı çalıştırmış ve yol kenarında bekleyen genç ve istekli taksi şoförü olmasına, PHP'nin ise bir çağrı gelmesini bekleyip ancak o zaman sizi almaya gelen yaşlı şoför olmasına eşdeğerdir. Eski kafalı biri olarak ve PHP'nin düz HTML ve SHTML dosyalarına harika bir değişiklik olduğu günlerden geliyorum, ne kadar zaman geçtiğini fark etmek biraz üzücü, ancak tasarım farklılıkları PHP'nin sadece bellekte kalan ve istekleri bir hokkabazın çevik kolaylığıyla işleyen Python, Java ve Node.js sunucularına karşı rekabet etmesini gerçekten zorlaştırıyor.

Yıldız kız

Flask şimdiye kadarki en hızlı framework'ümüz olabilir, ancak aslında oldukça eski bir yazılımdır. Python topluluğu birkaç yıl önce daha yeni asenkron ASGI sunucularına geçti ve tabii ki ben de onlarla birlikte geçtim.

Pafera Framework'ün en yeni sürümü, PaferaPyAsync , Starlette'e dayanmaktadır. Flask'ın Quart adında bir ASGI sürümü olmasına rağmen, Quart ve Starlette arasındaki performans farkları kodumu Starlette'e dayandırmam için yeterliydi.

Asenkron programlama birçok kişi için korkutucu olabilir, ancak Node.js ekibinin on yıldan fazla bir süre önce bu kavramı popülerleştirmesi sayesinde aslında zor bir kavram değildir.

Çoklu iş parçacığı, çoklu işlem, dağıtılmış bilgi işlem, söz zincirleme ve birçok deneyimli programcıyı erken yaşlandıran ve kurutan tüm o eğlenceli zamanlarla eşzamanlılıkla mücadele ediyorduk. Şimdi, sadece yazıyoruz async fonksiyonlarımızın önünde ve await Yürütülmesi uzun sürebilecek herhangi bir kodun önünde. Gerçekten de normal koddan daha ayrıntılıdır, ancak senkronizasyon ilkel öğeleri, mesaj geçişi ve vaatleri çözümlemekle uğraşmaktan çok daha az can sıkıcıdır.

Starlette dosyamız şu şekilde görünüyor:

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

Gördüğünüz gibi, Flask betiğimizden kopyalanıp yapıştırılmış, sadece birkaç yönlendirme değişikliği var ve async/await Anahtar kelimeler.

Kopyalayıp yapıştırılan kod bize gerçekten ne kadar gelişme sağlayabilir?

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

Yeni bir şampiyonumuz var, hanımlar ve beyler! Önceki rekorumuz saniyede 704 istekle saf PHP versiyonumuzdu, ardından saniyede 1080 istekle Flask versiyonumuz tarafından geçildi. Starlette betiğimiz saniyede 4562 istekle önceki tüm rakipleri eziyor, bu da saf PHP'ye göre 6 kat, Flask'a göre 4 kat iyileştirme anlamına geliyor.

Eğer WSGI Python kodunuzu henüz ASGI'ye çevirmediyseniz, şimdi başlamak için iyi bir zaman olabilir.

Düğüm.js/ExpressJS

Şimdiye kadar yalnızca PHP ve Python çerçevelerini ele aldık. Ancak, dünyanın büyük bir kısmı web siteleri için aslında Java, DotNet, Node.js, Ruby on Rails ve diğer benzer teknolojileri kullanıyor. Bu, dünyanın tüm ekosistemleri ve biyomlarının kapsamlı bir genel bakışı değildir, bu nedenle organik kimyanın programlama eşdeğerini yapmaktan kaçınmak için yalnızca kod yazması en kolay çerçeveleri seçeceğiz... ki Java kesinlikle bunlardan biri değildir.

K&R C veya Knuth'un kopyasının altında saklanmadığınız sürece Bilgisayar Programlamanın Sanatı Son on beş yıldır, muhtemelen Node.js'yi duymuşsunuzdur. JavaScript'in başlangıcından beri var olan bizler, modern JavaScript'in durumundan inanılmaz derecede korkuyoruz, hayrete düşüyoruz veya her ikisini birden yaşıyoruz, ancak JavaScript'in sunucularda ve tarayıcılarda hesaba katılması gereken bir güç haline geldiğini inkar edemeyiz. Sonuçta, artık dilde yerel 64 bit tamsayılar bile var! Bu, her şeyin 64 bit kayan noktalı sayılarda depolanmasından çok daha iyi!

ExpressJS muhtemelen kullanımı en kolay Node.js sunucusudur, bu yüzden sayacımızı sunmak için hızlı ve basit bir Node.js/ExpressJS uygulaması yapacağız.

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

Bu kod aslında Python versiyonlarından daha kolay yazılıyordu, ancak uygulamalar büyüdükçe yerel JavaScript oldukça kullanışsız hale geliyor ve TypeScript gibi bu durumu düzeltmeye yönelik tüm girişimler Python'dan daha ayrıntılı hale geliyor.

Bakalım nasıl performans gösterecek!

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'nin hızı hakkında eski (en azından İnternet standartlarına göre eski...) halk hikayeleri duymuş olabilirsiniz ve bu hikayelerin çoğu Google'ın V8 JavaScript motoruyla yaptığı muhteşem çalışma sayesinde gerçektir. Ancak bu durumda, hızlı uygulamamız Flask betiğinden daha iyi performans gösterse de, tek iş parçacıklı yapısı, &quot;Ni!&quot; diyen Starlette Knight tarafından kullanılan dört asenkron işlem tarafından alt edilir.

Hadi biraz daha yardım alalım!

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

Tamam! Şimdi dörtlü dörtlü bir mücadele! Hadi kıyaslama yapalım!

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

Hala Starlette seviyesinde değil, ancak hızlı beş dakikalık bir JavaScript hack'i için fena değil. Kendi testlerime göre, bu betik aslında veritabanı arayüz seviyesinde biraz geride tutuluyor çünkü node-postgres, Python için psycopg kadar verimli değil. Veritabanı sürücüsü olarak sqlite'a geçmek, aynı ExpressJS kodu için saniyede 3000'den fazla istek üretiyor.

Dikkat edilmesi gereken en önemli nokta, Python'un yavaş yürütme hızına rağmen ASGI çerçevelerinin belirli iş yükleri için Node.js çözümleriyle rekabet edebildiğidir.

Pas/Actix

Yani şimdi dağın tepesine yaklaşıyoruz ve dağ derken, hem fareler hem de insanlar tarafından kaydedilen en yüksek kıyaslama puanlarını kastediyorum.

Web üzerinde mevcut olan framework kıyaslamalarının çoğuna bakarsanız, zirveye hakim olma eğiliminde olan iki dil olduğunu fark edeceksiniz: C++ ve Rust. 90'lardan beri C++ ile çalışıyorum ve hatta MFC/ATL henüz bir şey olmadan önce kendi Win32 C++ framework'üm bile vardı, bu yüzden dil konusunda çok deneyimim var. Zaten bildiğiniz bir şeyle çalışmak pek eğlenceli değil, bu yüzden bunun yerine bir Rust sürümü yapacağız. ;)

Rust, programlama dilleri arasında nispeten yeni bir dildir, ancak Linus Torvalds Rust'ı Linux çekirdek programlama dili olarak kabul edeceğini duyurduğunda benim için bir merak konusu haline geldi. Biz eski programcılar için bu, bu yeni moda yeni çağ hippi şeyinin ABD Anayasası'na yeni bir değişiklik olacağını söylemekle hemen hemen aynı şey.

Şimdi, deneyimli bir programcı olduğunuzda, gençlerin yaptığı kadar hızlı bir şekilde trene atlama eğiliminde olmazsınız, aksi takdirde dil veya kütüphanelerde yapılan hızlı değişiklikler yüzünden yanabilirsiniz. (AngularJS'nin ilk sürümünü kullanan herkes ne demek istediğimi anlayacaktır.) Rust hala deneysel geliştirme aşamasında ve web'deki birçok kod örneğinin artık paketlerin güncel sürümleriyle derlenmemesi bana komik geliyor.

Ancak Rust uygulamalarının gösterdiği performans inkar edilemez. Eğer hiç denemediyseniz ripgrep veya fd-bul büyük kaynak kodu ağaçlarında, kesinlikle onlara bir şans vermelisiniz. Hatta çoğu Linux dağıtımı için paket yöneticisinden bile kullanılabilirler. Rust ile performans için ayrıntılı bilgi alışverişinde bulunuyorsunuz... pay ayrıntılı bir açıklama için pay Performansın.

Rust için tam kod biraz büyük, bu yüzden burada ilgili işleyicilere bir göz atacağız:

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

Bu Python/Node.js versiyonlarından çok daha karmaşıktır...

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

Ve çok daha performanslı!

Actix/deadpool_postgres kullanan Rust sunucumuz, önceki şampiyonumuz Starlette'i +125%, ExpressJS'i +362% ve saf PHP'yi +1366% oranında rahatlıkla geçiyor. (Laravel sürümüyle performans farkını okuyucunun egzersizi olarak bırakacağım.)

Rust dilinin kendisini öğrenmenin, 6502 Assembly dışında gördüğüm her şeyden daha fazla tuzak içerdiği için diğer dillerden daha zor olduğunu gördüm, ancak Rust sunucunuz PHP sunucunuzun 14 katı kadar kullanıcıyı kaldırabiliyorsa, o zaman belki de teknolojiler arasında geçiş yaparak kazanılacak bir şeyler vardır. Bu nedenle Pafera Framework'ün bir sonraki sürümü Rust'a dayalı olacak. Öğrenme eğrisi betik dillerinden çok daha yüksektir, ancak performans buna değecektir. Rust'ı öğrenmek için zaman ayıramıyorsanız, teknoloji yığınınızı Starlette veya Node.js'ye dayandırmak da kötü bir karar değildir.

Teknik Borç

Son yirmi yılda, ucuz statik barındırma sitelerinden LAMP yığınlarıyla paylaşımlı barındırmaya, AWS, Azure ve diğer bulut hizmetlerine VPS kiralamaya geçtik. Günümüzde, birçok şirket, uygun bulut hizmetlerinin ortaya çıkmasıyla yavaş sunuculara ve uygulamalara daha fazla donanım atmayı kolaylaştırdığından, bulabildikleri mevcut veya en ucuz olana göre tasarım kararları almaktan memnun. Bu, onlara uzun vadeli teknik borç pahasına büyük kısa vadeli kazançlar sağladı.

Kaliforniya Cerrah Genel Müdürlüğü'nün Uyarısı: Bu gerçek bir uzay köpeği değil.

70 yıl önce, Sovyetler Birliği ile Amerika Birleşik Devletleri arasında büyük bir uzay yarışı vardı. Sovyetler erken dönüm noktalarının çoğunu kazandı. Sputnik'te ilk uyduya, Laika'da uzaya çıkan ilk köpeğe, Luna 2'de ilk ay uzay aracına, Yuri Gagarin ve Valentina Tereshkova'da uzaya çıkan ilk erkeğe ve kadına sahiplerdi, vb...

Ancak yavaş yavaş teknik borç birikmeye başladı.

Sovyetler bu başarıların her birine ilk ulaşanlar olsa da, mühendislik süreçleri ve hedefleri onları uzun vadeli uygulanabilirlikten ziyade kısa vadeli zorluklara odaklanmaya yöneltiyordu. Her sıçrayışta kazandılar, ancak rakipleri bitiş çizgisine doğru istikrarlı adımlar atmaya devam ederken onlar daha yorgun ve yavaş hale geliyorlardı.

Neil Armstrong canlı televizyonda Ay'a tarihi adımlarını attığında, Amerikalılar liderliği ele geçirdi ve ardından Sovyet programı tökezlediğinde orada kaldılar. Bu, uzun vadede uygun alışkanlıklar ve stratejiler geliştirmeyi başaramayan, bir sonraki büyük şeye, bir sonraki büyük getiriye veya bir sonraki büyük teknolojiye odaklanan günümüz şirketlerinden farklı değildir.

Pazara ilk giren olmak, o pazarda baskın oyuncu olacağınız anlamına gelmez. Alternatif olarak, işleri doğru yapmak için zaman ayırmak başarıyı garantilemez, ancak uzun vadeli başarı şansınızı kesinlikle artırır. Şirketinizin teknoloji lideriyseniz, iş yükünüz için doğru yönü ve araçları seçin. Popülerliğin performans ve verimliliğin yerini almasına izin vermeyin.

Kaynaklar

Rust, ExpressJS, Flask, Starlette ve Pure PHP scriptlerini içeren 7z dosyasını indirmek mi istiyorsunuz?

Yazar Hakkında

Jim, 90'larda bir IBM PS/2 aldığından beri programlama yapıyor. Bugüne kadar, hala HTML ve SQL'i elle yazmayı tercih ediyor ve işinde verimliliğe ve doğruluğa odaklanıyor.