はい、バージニア、 サンタクロース 2023 年の Web フレームワークの違い

市場の圧力と技術的負債に屈する前に、高速に動作するウェブサーバーコードを見つけるための、ある反抗的なプログラマーの旅
2023-03-24 11:52:06
👁️ 803
💬 0

内容

  1. はじめに
  2. テスト
  3. PHP/Laravel
  4. 純粋なPHP
  5. Laravel再訪
  6. ジャンゴ
  7. フラスコ
  8. スターレット
  9. Node.js/ExpressJS
  10. ラスト/アクティクス
  11. 技術的負債
  12. リソース

はじめに

最近の就職面接の後、私が応募した会社がまだLaravelを使っていることに気づいて驚いた。Laravelは私が10年ほど前に試した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

目の前の課題

このコードでは、各リクエストに対して3つの単純なタスクがある:

  1. クッキーから現在のユーザーのセッションIDを読み取る。
  2. データベースから追加情報を読み込む
  3. その情報をユーザーに返す

どんなバカげたテストなんだ、とあなたは尋ねるかもしれない。さて、このページのネットワークリクエストを見てみると、全く同じことをするsessionvars.jsというものがあることに気づくだろう。

sessionvars.jsの内容

現代のウェブページは複雑な生き物であり、最も一般的なタスクのひとつは、データベース・サーバーへの過剰な負荷を避けるために複雑なページをキャッシュすることだ。

ユーザーがリクエストするたびに複雑なページを再レンダリングすると、1秒間に約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に素早くユーザーに投げ出させれば、1秒間に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で送られます。これにより、データベースの負荷が軽減され、ユーザーにとってより良いエクスペリエンスが生まれるだけでなく、クリンゴンが攻撃してきたときにサーバーがワープコアの裂け目で自然蒸発してしまう量子的な確率も減少する。

コード要件

各フレームワークの返されるコードには、1つのシンプルな要件がある:ユーザーが何回ページをリフレッシュしたかを、"Count is x"と表示することだ。物事をシンプルに保つために、今のところRedisキュー、Kubernetesコンポーネント、AWS Lambdasは使わないことにする。

ページへの訪問回数の表示

各ユーザのセッションデータはPostgreSQLデータベースに保存されます。

ユーザーセッション表

そして、このデータベース・テーブルは各テストの前に切り捨てられる。

切り捨て後のテーブル

シンプルでありながら効果的というのがパフェラのモットーだ。

実際のテスト結果

PHP/Laravel

さて、いよいよ手を動かしてみましょう。Laravelのセットアップは省略します。 コマンドを実行するだけなので、Laravelのセットアップは省略する。

まず、.envファイルでデータベースの設定を行います。

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

次に、すべてのリクエストをコントローラに送るフォールバックルートを1つ設定します。

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リクエストから、"Count is 1"をレンダリングする毎秒21リクエストになった。

何がいけなかったのでしょうか?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'];

Laravelで4行のコード(と設定作業)で行っていたことを、98行のコードで行っています。(もちろん、適切なエラー処理とユーザー向けメッセージを行えば、これは約2倍の行数になるでしょう)。もしかしたら、毎秒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再訪

コンフィギュレーションに関する問題やパフォーマンスに関するヒントを求めてウェブを探し回った結果、最も人気のある2つのテクニックは、コンフィギュレーションとルートデータをキャッシュして、リクエストごとに処理するのを避けるというものでした。そこで、彼らのアドバイスに従って、これらのヒントを試してみることにする。

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

1秒あたりのリクエスト数は21.04から28.80になり、約37%も劇的に向上しました!これは、どんなソフトウェア・パッケージにとっても非常に印象的なことでしょう......ただし、まだ純粋なPHPバージョンの1/24のリクエスト数しかしていないという事実を除けば。

もしあなたが、このテストは何か間違っているに違いないと考えているなら、Lucinda PHPフレームワークの作者と話すべきです。彼のテスト結果では ララベルを叩くルシンダ HTMLリクエストでは36倍、JSONリクエストでは90倍である。

自分のマシンでApacheとNginxの両方でテストした結果、彼を疑う理由はない。Laravelは本当に その 遅い!PHP自体はそれほど悪くはないのですが、Laravelが各リクエストに追加するすべての余分な処理を加えると、2023年の選択肢としてLaravelを勧めるのは非常に難しくなります。

ジャンゴ

PHP/Wordpressアカウント ウェブ上の全ウェブサイトの約40%。 という枠組みが圧倒的に多い。個人的には、人気が必ずしも品質に結びつくとは限らない。 世界で最も人気のあるレストラン ...McDonald&#x27;s.純粋なPHPコードはすでにテストしたので、Wordpressそのものをテストするつもりはない。

Django も長い間人気のあるフレームワークです。過去に Django を使ったことがあるなら、その壮大なデータベース管理インタフェー スと、すべてを思い通りに設定するのがいかに面倒だったかを懐かしく思い出すことでしょう。2023 年、特にバージョン 4.0 で追加された新しい ASGI インタフェースで、 Django がどれだけうまく動くか見てみましょう。

Django のセットアップは Laravel のセットアップと驚くほど似ています。退屈な設定は飛ばして、ビューの設定に入りましょう。

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

4行のコードは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 vs. 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ライブラリの数は類似言語の約4倍で、膨大な領域をカバーしているが、NumPyのようなニッチな分野以外では、Pythonが高速で高性能だとは誰も言わないだろう。

Flaskのバージョンと以前のフレームワークとの比較を見てみよう。

Python/Flask

╰─➤  gunicorn --access-logfile - -w 4 flasksite:app
[2023-03-21 15:32:49 +0800] [2856296] [INFO] Starting gunicorn 20.1.0

╰─➤  wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    91.84ms   11.97ms 149.63ms   86.18%
    Req/Sec   272.04     39.05   380.00     74.50%
  10842 requests in 10.04s, 3.27MB read
Requests/sec:   1080.28
Transfer/sec:    333.37KB

私たちのFlaskスクリプトは、純粋なPHPバージョンよりも実際に高速です!

もしこれに驚かれたのなら、Flaskアプリはgunicornサーバーを立ち上げるときにすべての初期化と設定を行うのに対して、PHPは新しいリクエストが来るたびにスクリプトを再実行することを理解してください。これは、Flaskが若くて熱心なタクシー運転手で、すでに車を発進させて道路のそばで待っているのに対して、PHPは家にいて電話がかかってくるのを待ち、それから車で迎えに行く年寄りの運転手であることと同じです。PHPがプレーンなHTMLファイルやSHTMLファイルを見事に変えていた時代からの古い人間であるため、時の流れを実感するのは少し悲しいが、設計の違いによって、PHPがPython、Java、Node.jsサーバーに対抗するのは本当に難しくなっている。

スターレット

Flaskはこれまでで最も速いフレームワークかもしれないが、実はかなり古いソフトウェアだ。Pythonコミュニティは数年前に新しいasychronous ASGIサーバーに切り替えた。

パフェーラ・フレームワークの最新バージョン、 PaferaPyAsync はStarletteをベースにしている。QuartというASGIバージョンのFlaskもあるが、QuartとStarletteのパフォーマンスの違いは、私のコードをStarletteに置き換えるのに十分だった。

非同期プログラミングは多くの人にとって恐ろしいものだが、Node.jsが10年以上前にこの概念を広めたおかげで、実は難しい概念ではない。

かつては、マルチスレッド、マルチプロセシング、分散コンピューティング、プロミス・チェイニングなど、多くのベテラン・プログラマーを早老化させ、乾燥させるような楽しい時間を使って、並行処理と戦っていた。今は async 私たちの機能の前で await を実行に時間がかかりそうなコードの前に置く。確かに通常のコードよりは冗長だが、同期プリミティブやメッセージ・パッシング、約束の解決に対処するよりは、はるかに煩わしくない。

スターレットのファイルはこんな感じ:

#!/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などの技術をウェブサイトに使っている。そのため、プログラミングが有機化学に相当することを避けるために、コードをタイプするのが最も簡単なフレームワークだけを選ぶことにする。

K&#x27;R CやKnuth&#x27;sのコピーの下に隠れていたのでなければ。 コンピューター・プログラミングの極意 この15年間、Node.jsのことを耳にしたことがあるだろう。しかし、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}`));

アプリケーションの規模が大きくなると、ネイティブのJavaScriptはかなり扱いにくくなり、TypeScriptのようなこれを修正しようとする試みは、すぐにPythonよりも冗長になる。

さて、どうなるか!

Node.js/ExpressJS

╰─➤  node --version                                                                                                                                                                     v19.6.0

╰─➤  NODE_ENV=production node nodejsapp.js                                                                                                                                             130 ↵
Server Running at port 8000

╰─➤  wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    90.41ms    7.20ms 188.29ms   85.16%
    Req/Sec   277.15     37.21   393.00     81.66%
  11018 requests in 10.02s, 3.82MB read
Requests/sec:   1100.12
Transfer/sec:    390.68KB

Node.js&#x27;のスピードについての昔話を聞いたことがあるかもしれない。しかしこの場合、我々のクイック・アプリはFlaskスクリプトを凌駕するものの、そのシングル・スレッドの性質は、&quot;Ni!&quot;と言うStarlette Knightが振り回す4つの非同期プロセスに負けてしまう。

もっと助けてもらおう!

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

オーケー!4対4の互角の戦いだ!ベンチマークをしよう!

╰─➤  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のレベルには達していないが、5分でできるJavaScriptハックとしては悪くない。私自身のテストによると、このスクリプトはデータベースのインターフェイスレベルで少し抑制されている。データベースドライバをsqliteに切り替えると、同じExpressJSコードで毎秒3000以上のリクエストが発生する。

注意すべき点は、Pythonの実行速度は遅いものの、ASGIフレームワークは、特定のワークロードではNode.jsソリューションと実際に競合できるということだ。

ラスト/アクティクス

山というのは、ネズミと人間によって記録されたベンチマークの最高得点のことだ。

ウェブ上で公開されているフレームワークのベンチマークのほとんどを見ると、上位を占める傾向がある2つの言語があることに気づくだろう:C++とRustだ。私は90年代からC++を扱ってきたし、MFC/ATLが登場する前には自分のWin32 C++フレームワークも持っていた。だから代わりにRustバージョンを作ろうと思っているんだ。)

Rustはプログラミング言語としては比較的新しいものだが、リーナス・トーバルズがRustをLinuxカーネルのプログラミング言語として認めると発表したときから、私にとって興味津々の対象となった。私たち年配のプログラマーにとっては、この新しい時代のヒッピーが合衆国憲法の修正条項となることを発表したのと同じようなものだ。

経験豊富なプログラマーになると、若い人たちほど流行に飛びつかない傾向がある。(AngularJSの最初のバージョンを使った人なら、私が何を言っているかわかるだろう。)Rustはまだ実験的な開発段階であり、ウェブ上の多くのコード例が、現在のバージョンのパッケージではコンパイルすらできなくなっているのはおかしなことだ。

しかし、ラスト・アプリケーションが示すパフォーマンスは否定できない。もし一度も リップグレップ または 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言語そのものを習得するのは、他の言語よりも難しいことに気づきました。Rust言語には、6502アセンブリ以外で目にしたどの言語よりも多くのゴッチがあるからです。だからこそ、Paferaフレームワークの次のバージョンは、Rustをベースにするのです。学習曲線はスクリプト言語よりもはるかに高いが、パフォーマンスはそれに見合うものになるだろう。Rustを学ぶ時間が取れないのであれば、技術スタックをStarletteやNode.jsをベースにするのも悪くない決断だ。

技術的負債

この20年間で、私たちは安価な静的ホスティングサイトから、LAMPスタックによる共有ホスティング、VPSのレンタル、そしてAWSやAzureなどのクラウドサービスへと移行してきた。便利なクラウドサービスの登場により、低速なサーバーやアプリケーションに多くのハードウェアを投入することが容易になったためだ。これは、長期的な技術的負債を代償に、短期的には大きな利益をもたらしている。

カリフォルニア州外科医の警告:これは本物の宇宙犬ではありません。

70年前、ソビエト連邦とアメリカの間で大きな宇宙開発競争があった。ソ連は初期のマイルストーンのほとんどを制した。スプートニクによる初の人工衛星、ライカによる初の宇宙犬、ルナ2号による初の月宇宙船、ユーリ・ガガーリンとバレンティーナ・テレシコワによる初の男女宇宙飛行士などなど...。

しかし、徐々に技術的負債が蓄積されていった。

ソビエトはこれらの成果をいずれも最初に達成したが、彼らのエンジニアリング・プロセスと目標は、長期的な実現可能性よりも短期的な課題に集中することを引き起こしていた。彼らは跳躍するたびに勝利を収めたが、対戦相手がゴールに向かって一貫した前進を続ける一方で、彼らはより疲れ、より遅くなっていった。

ニール・アームストロングがテレビの生中継で月面の歴史的な一歩を踏み出すと、アメリカは主導権を握り、ソ連の計画が頓挫する中、そこにとどまった。これは、次の大きなもの、次の大きな報酬、あるいは次の大きな技術に焦点を当てながら、長期的な視野に立った適切な習慣や戦略を身につけることに失敗している今日の企業と何ら変わりはない。

最初に市場に参入したからといって、その市場で支配的なプレーヤーになれるわけではない。また、時間をかけて物事を正しく行うことは、成功を保証するものではないが、長期的な業績の可能性を高めることは間違いない。もしあなたが会社の技術責任者なら、自分の仕事量に適した方向性とツールを選びなさい。人気取りがパフォーマンスや効率に取って代わることのないように。

リソース

Rust、ExpressJS、Flask、Starlette、Pure PHPスクリプトを含む7zファイルをダウンロードしたいですか?

著者について

Jim は 90 年代に IBM PS/2 を入手して以来、プログラミングを続けています。現在でも、彼は HTML と SQL を手作業で記述することを好み、仕事の効率性と正確性を重視しています。