是的,維吉尼亞,有*有*一個 聖誕老公公 2023 年 Web 框架之間的差異

一位挑釁的程式設計師在屈服於市場壓力和技術債務之前尋找快速執行的 Web 伺服器程式碼的旅程
2023-03-24 11:52:06
👁️ 786
💬 0

內容

  1. 介紹
  2. 測試
  3. PHP/Laravel
  4. 純PHP
  5. 重溫 Laravel
  6. 薑戈
  7. 燒瓶
  8. 史塔萊特
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. 技術債
  12. 資源

介紹

在我最近的一次工作面試之後,我驚訝地發現我申請的公司仍在使用 Laravel,這是我大約十年前嘗試過的 PHP 框架。這在當時是不錯的,但如果說科技和時尚之間有一個不變的因素的話,那就是風格和概念的不斷變化和重塑。如果您是 JavaScript 程式設計師,您可能熟悉這個老笑話

程式設計師 1:“我不喜歡這個新的 JavaScript 框架!”

程式設計師2:「不用擔心。只要等六個月,就會有另一個替代它!

出於好奇,我決定看看當我們對新舊產品進行測試時到底會發生什麼。當然,網路上充滿了基準和聲明,其中最受歡迎的可能是 TechEmpower Web 框架基準在這裡 . 不過,我們今天不會做任何像他們那麼複雜的事。我們會讓事情變得美好又簡單,這樣這篇文章就不會變成 戰爭與和平 , 當你讀完書時,你會有輕微的機會保持清醒。通常的警告適用:這在您的電腦上可能不一樣,不同的軟體版本會影響效能,薛丁格的貓實際上變成了一隻半生半死的殭屍貓。

測試

測試環境

對於此測試,我將使用配備運行 Manjaro Linux 的小型 i5 的筆記型電腦,如下所示。

╰─➤  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中讀取目前使用者的會話ID
  2. 從資料庫載入附加資訊
  3. 將該資訊傳回給用戶

您可能會問,這是什麼樣的愚蠢測試?好吧,如果您查看此頁面的網路請求,您會注意到一個名為 sessionvars.js 的請求執行完全相同的操作。

sessionvars.js 的內容

您會看到,現代網頁是複雜的生物,最常見的任務之一是快取複雜的頁面以避免資料庫伺服器上的過多負載。

如果我們每次使用者要求時都重新渲染一個複雜的頁面,那麼我們每秒只能為大約 600 個使用者提供服務。

╰─➤  wrk -d 10s -t 4 -c 100 http://127.0.0.1/system/index.en.html      
Running 10s test @ http://127.0.0.1/system/index.en.html
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   186.83ms  174.22ms   1.06s    81.16%
    Req/Sec   166.11     58.84   414.00     71.89%
  6213 requests in 10.02s, 49.35MB read
Requests/sec:    619.97
Transfer/sec:      4.92MB

但如果我們將此頁面快取為靜態 HTML 文件,並讓 Nginx 快速將其扔出視窗給用戶,那麼我們每秒可以為 32,000 個用戶提供服務,從而將效能提高 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中發送。這不僅減少了資料庫負載並為我們的用戶創造了更好的體驗,而且還降低了當克林貢人攻擊時我們的伺服器在曲速核心突破中自發蒸發的量子機率。

代碼要求

每個框架的回傳代碼都有一個簡單的要求:透過說「Count is x」向使用者顯示他們刷新頁面的次數。為了簡單起見,我們暫時遠離 Redis 佇列、Kubernetes 元件或 AWS Lambda。

顯示您造訪該頁面的次數

每個使用者的會話資料將保存在 PostgreSQL 資料庫中。

使用者會話表

而這個資料庫表在每次測試之前都會被截斷。

截斷後的表

簡單而有效是帕費拉的座右銘……無論如何,在最黑暗的時間線之外……

實際測試結果

PHP/Laravel

好的,現在我們終於可以開始動手了。我們將跳過 Laravel 的設置,因為它只是一群作曲家和工匠 命令。

首先,我們將在 .env 檔案中設置資料庫設置

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

然後我們將設定一個後備路由,將每個請求傳送到我們的控制器。

Route::fallback(SessionController::class);

並設定控制器顯示計數。 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 個請求渲染「計數為 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%!這對於任何軟體包來說都是相當令人印象深刻的......除了我們仍然只執行純 PHP 版本的 1/24 請求數量這一事實。

如果您認為此測試一定有問題,您應該與 Lucinda PHP 框架的作者交談。在他的測試結果中,他有 露辛達擊敗拉拉維爾 HTML 請求提高了 36 倍,JSON 請求提高了 90 倍。

在我自己的機器上用 Apache 和 Nginx 進行測試後,我沒有理由懷疑他。 Laravel 真的只是 慢的! PHP 本身並沒有那麼糟糕,但一旦添加了 Laravel 為每個請求添加的所有額外處理,那麼我發現很難在 2023 年推薦 Laravel 作為選擇。

薑戈

PHP/Wordpress 帳戶 約佔網路上所有網站的 40% ,使其成為迄今為止最主要的框架。但就我個人而言,我發現受歡迎程度並不一定會轉化為質量,就像我發現自己突然無法控制地渴望品嚐來自 世界上最受歡迎的餐廳 ……麥當勞。由於我們已經測試了純 PHP 程式碼,因此我們不會測試 Wordpress 本身,因為涉及 Wordpress 的任何內容無疑都會低於我們在純 PHP 中觀察到的每秒 700 個請求。

Django 是另一個已經存在很長時間的流行框架。如果您過去使用過它,您可能會深深地記得它壯觀的資料庫管理介面以及按照您想要的方式配置所有內容是多麼煩人。讓我們看看 Django 在 2023 年的表現如何,尤其是從 4.0 版本開始添加了新的 ASGI 介面。

設定 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 版本的一半,但也是 Laravel 版本的 12 倍。 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 都會重新執行腳本。熱心的計程車司機,已經發動了車,在路邊等候,而PHP 是老司機,待在家裡等電話進來,然後才開車過來接你。作為一個老同學,來自 PHP 對純 HTML 和 SHTML 檔案進行了美妙改變的時代,意識到時間已經過去了多少有點難過,但設計上的差異確實讓 PHP 很難與Python、Java 和Node.js 伺服器競爭,這些伺服器只停留在記憶體中並像雜耍者一樣靈活輕鬆地處理請求。

史塔萊特

Flask 可能是迄今為止我們最快的框架,但它實際上是相當古老的軟體。幾年前,Python 社群轉向了更新的非同步 ASGI 伺服器,當然,我自己也隨之轉向了。

最新版本的Pafera框架, 帕費拉PyAsync ,基於 Starlette。儘管 Flask 有一個名為 Quart 的 ASGI 版本,但 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 個請求的速度擊敗了所有先前的競爭者,這意味著比純 PHP 提高了 6 倍,比 Flask 提高了 4 倍。

如果您尚未將 WSGI Python 程式碼變更為 ASGI,現在可能是開始的好時機。

Node.js/ExpressJS

到目前為止,我們只介紹了 PHP 和 Python 框架。然而,世界上很大一部分人實際上在他們的網站上使用 Java、DotNet、Node.js、Ruby on Rails 和其他此類技術。這絕不是對世界上所有生態系統和生物群落的全面概述,因此為了避免進行相當於有機化學的編程,我們將只選擇最容易鍵入程式碼的框架。 其中Java絕對不是。

除非你一直藏在 K&R C 或 Knuth 的副本下面 電腦程式設計的藝術 在過去的十五年裡,您可能聽說過 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 的古老(無論如何,按照互聯網標準來說是古老的……)民間故事。速度,而這些故事大多是真實的,這要歸功於 Google 在 V8 JavaScript 引擎上所做的出色工作。但在這種情況下,儘管我們的快速應用程式優於 Flask 腳本,但它的單線程特性被 Starlette Knight 所使用的四個非同步進程所擊敗,他說「Ni!」。

讓我們獲得更多幫助!

╰─➤  pm2 start nodejsapp.js -i 4 

[PM2] Spawning PM2 daemon with pm2_home=/home/jim/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/jim/projects/paferarust/nodejsapp.js in cluster_mode (4 instances)
[PM2] Done.
┌────┬──────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name         │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼──────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ nodejsapp    │ default     │ N/A     │ cluster │ 37141    │ 0s     │ 0    │ online    │ 0%       │ 64.6mb   │ jim      │ disabled │
│ 1  │ nodejsapp    │ default     │ N/A     │ cluster │ 37148    │ 0s     │ 0    │ online    │ 0%       │ 64.5mb   │ jim      │ disabled │
│ 2  │ nodejsapp    │ default     │ N/A     │ cluster │ 37159    │ 0s     │ 0    │ online    │ 0%       │ 56.0mb   │ jim      │ disabled │
│ 3  │ nodejsapp    │ default     │ N/A     │ cluster │ 37171    │ 0s     │ 0    │ online    │ 0%       │ 45.3mb   │ jim      │ disabled │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

好的!現在是四對四的平手!讓我們來進行基準測試!

╰─➤  wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    45.09ms   19.89ms 176.14ms   60.22%
    Req/Sec   558.93     97.50   770.00     66.17%
  22234 requests in 10.02s, 7.71MB read
Requests/sec:   2218.69
Transfer/sec:    787.89KB

雖然還沒有達到 Starlette 的水平,但對於五分鐘的 JavaScript 快速破解來說已經不錯了。根據我自己的測試,這個腳本實際上在資料庫介面層級上受到了阻礙,因為 Node-postgres 的效率遠不如 psycopg 對於 Python 的效率。對於相同的 ExpressJS 程式碼,切換到 sqlite 作為資料庫驅動程式每秒會產生超過 3000 個請求。

主要要注意的是,儘管 Python 的執行速度較慢,但對於某些工作負載,ASGI 框架實際上可以與 Node.js 解決方案競爭。

Rust/Actix

現在,我們離山頂越來越近了,我所說的山,是指小鼠和人類記錄的最高基準分數。

如果您查看網路上的大多數框架基準測試,您會發現有兩種語言往往佔據主導地位:C++ 和 Rust。我從 90 年代就開始使用 C++,甚至在 MFC/ATL 出現之前我就有了自己的 Win32 C++ 框架,所以我對該語言有很多經驗。當你已經知道某件事時,使用它並沒有多大樂趣,所以我們將製作一個 Rust 版本。 ;)

就程式語言而言,Rust 相對較新,但當 Linus Torvalds 宣布他將接受 Rust 作為 Linux 核心程式語言時,它成為了我好奇的對象。對我們這些年長的程式設計師來說,這就像說這個新時代嬉皮士的新事物將成為美國憲法的新修正案。

現在,當您是經驗豐富的程式設計師時,您往往不會像年輕人那樣快地趕上潮流,否則您可能會因語言或函式庫的快速變化而感到困惑。 (任何使用過 AngularJS 第一個版本的人都會知道我在說什麼。)Rust 仍然處於實驗性開發階段,我覺得有趣的是,網路上的許多程式碼範例甚至都沒有不再使用當前版本的軟體包進行編譯。

然而,Rust 應用程式所表現出的性能是不可否認的。如果你從未嘗試過 ripgrep 或者 fd查找 在大型原始碼樹上,您絕對應該嘗試一下。它們甚至可以透過套件管理器用於大多數 Linux 發行版。你正在用 Rust 來交換冗長的效能... 很多 冗長的 很多 的性能。

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

而且性能更加優異!

我們使用 Actix/deadpool_postgres 的 Rust 伺服器輕鬆擊敗了我們之前的冠軍 Starlette 125%,ExpressJS 362%,純 PHP 1366%。 (我將 Laravel 版本的效能增量作為讀者的練習。)

我發現學習 Rust 語言本身比其他語言更困難,因為它的陷阱比我在 6502 Assembly 之外看到的任何語言都多,但如果你的 Rust 伺服器可以承擔 14 倍的數量用戶作為您的PHP 伺服器,那麼也許透過切換技術可以獲得一些好處。這就是為什麼 Pafera 框架的下一個版本將基於 Rust。學習曲線比腳本語言高得多,但效能是值得的。如果您無法花時間學習 Rust,那麼將您的技術堆疊基於 Starlette 或 Node.js 也不是一個糟糕的決定。

技術債

在過去的二十年裡,我們已經從廉價的靜態託管網站轉向使用 LAMP 堆疊的共享託管,再到向 AWS、Azure 和其他雲端服務租用 VPS。如今,許多公司滿足於根據他們能找到的可用或最便宜的人做出設計決策,因為方便的雲端服務的出現使得可以輕鬆地在速度較慢的伺服器和應用程式上投入更多硬體。這使他們以長期技術債為代價獲得了巨大的短期利益。

加州衛生局局長警告:這不是真正的太空狗。

70年前,蘇聯和美國之間發生了一場偉大的太空競賽。蘇聯贏得了大部分早期里程碑。他們擁有第一顆衛星 Sputnik、第一隻進入太空的狗 Laika、第一艘月球飛船 Luna 2、第一位進入太空的男人和女人 Yuri Gagarin 和 Valentina Tereshkova 等等…

但他們正在慢慢累積技術債。

儘管蘇聯率先取得了這些成就,但他們的工程流程和目標導致他們專注於短期挑戰而不是長期可行性。他們每次跳躍都獲勝,但他們變得越來越累,速度越來越慢,而對手則繼續朝著終點線邁進。

當尼爾阿姆斯壯在電視直播中邁出他在月球上的歷史性一步時,美國人一馬當先,然後在蘇聯計劃步履蹣跚時留在那裡。這與今天的公司沒有什麼不同,他們專注於下一個大事、下一個大回報或下一個大技術,但卻未能養成正確的長期習慣和策略。

率先進入市場並不意味著您將成為該市場的主導者。另外,花時間做正確的事情並不能保證成功,但肯定會增加您長期成就的機會。如果您是公司的技術主管,請為您的工作量選擇正確的方向和工具。不要讓流行取代性能和效率。

資源

想要下載包含 Rust、ExpressJS、Flask、Starlette 和 Pure PHP 腳本的 7z 檔案?

關於作者

Jim 自從 90 年代買回 IBM PS/2 以來就一直在編程。直到今天,他仍然更喜歡手工編寫 HTML 和 SQL,並在工作中專注於效率和正確性。