是的,弗吉尼亚,有一个 圣诞老人 2023 年 Web 框架之间的差异

一位不屈不挠的程序员在屈服于市场压力和技术债务之前寻找快速执行的 Web 服务器代码的旅程
2023-03-24 11:52:06
👁️ 762
💬 0

内容

  1. 介绍
  2. 测试
  3. PHP/Laravel
  4. 纯 PHP
  5. 重温 Laravel
  6. Django
  7. 烧瓶
  8. 星光熠熠
  9. Node.js/ExpressJS
  10. Rust/Actix
  11. 技术债务
  12. 资源

介绍

在我最近的一次面试之后,我惊讶地发现我申请的公司仍在使用 Laravel,这是我大约十年前尝试过的 PHP 框架。在当时,它还不错,但如果说技术和时尚界有一个不变的东西,那就是风格和概念的不断变化和重现。如果你是一名 JavaScript 程序员,你可能对这句老笑话很熟悉

程序员 1:“我不喜欢这个新的 JavaScript 框架!”

程序员2:“不用担心。只要等六个月就会有另一个来代替它!”

出于好奇,我决定看看当我们测试新旧产品时究竟会发生什么。当然,网络上充斥着各种基准和声明,其中最受欢迎的可能是 TechEmpower Web 框架基准测试 . 不过,我们今天不会做任何像它们一样复杂的事情。我们会尽量保持简洁,这样这篇文章就不会变成 战争与和平 , 并且您读完后仍有一丝清醒的机会。通常需要注意的是:这在您的机器上可能效果不同,不同的软件版本会影响性能,而且薛定谔的猫实际上变成了一只半死半活的僵尸猫。

测试

测试环境

为了进行这次测试,我将使用配备有微弱的 i5 处理器并运行 Manjaro Linux 的笔记本电脑,如下所示。

╰─➤  uname -a
Linux jimsredmi 5.10.174-1-MANJARO #1 SMP PREEMPT Tuesday Mar 21 11:15:28 UTC 2023 x86_64 GNU/Linux

╰─➤  cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family  : 6
model   : 126
model name  : Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
stepping  : 5
microcode : 0xb6
cpu MHz   : 990.210
cache size  : 6144 KB

当前的任务

我们的代码针对每个请求有三个简单的任务:

  1. 从 cookie 中读取当前用户的会话 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 中。这不仅可以减少数据库负载并为我们的用户创造更好的体验,还可以降低当克林贡人进攻时我们的服务器在曲速核心破裂时自发蒸发的量子概率。

代码要求

每个框架返回的代码都有一个简单的要求:通过说“计数为 x”向用户显示他们刷新页面的次数。为了简单起见,我们暂时不会使用 Redis 队列、Kubernetes 组件或 AWS Lambdas。

显示您访问过该页面的次数

每个用户的会话数据将保存在 PostgreSQL 数据库中。

用户会话表

并且这个数据库表将在每次测试之前被截断。

截断后的表格

简单而有效是 Pafera 的座右铭……无论如何,在最黑暗的时间线之外……

实际测试结果

PHP/Laravel

好的,现在我们终于可以开始动手了。我们将跳过 Laravel 的设置,因为它只是一堆 composer 和 artisan 命令。

首先,我们将在 .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 中 4 行代码(以及一大堆配置工作)所做的事情。(当然,如果我们做了适当的错误处理和面向用户的消息,行数将是原来的两倍。)也许我们可以达到每秒 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 框架的作者谈谈。在他的测试结果中,他 Lucinda 击败 Laravel 对于 HTML 请求,速度提高了 36 倍;对于 JSON 请求,速度提高了 90 倍。

在我自己的机器上用 Apache 和 Nginx 测试后,我没有理由怀疑他。Laravel 真的只是 慢!PHP 本身并不是那么糟糕,但是一旦添加 Laravel 添加到每个请求中的所有额外处理,那么我发现很难推荐 Laravel 作为 2023 年的选择。

Django

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 库的数量大约是同类语言的四倍,涵盖了大量领域,但除了 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 版本更快!

如果您对此感到惊讶,您应该意识到,当我们启动 gunicorn 服务器时,我们的 Flask 应用程序会进行所有初始化和配置,而每次收到新请求时,PHP 都会重新执行脚本。这相当于 Flask 是年轻、热切的出租车司机,他已经启动了汽车,正在路边等候,而 PHP 是老司机,他待在家里等待电话,然后才开车过来接你。作为一个老派人,来自 PHP 是纯 HTML 和 SHTML 文件的绝佳替代品的时代,意识到时间已经过去了这么多,有点难过,但设计差异确实使 PHP 很难与 Python、Java 和 Node.js 服务器竞争,这些服务器只停留在内存中,像杂耍演员一样灵活地处理请求。

星光熠熠

Flask 可能是我们目前最快的框架,但它实际上是一款相当老旧的软件。几年前,Python 社区转向了较新的异步 ASGI 服务器,当然,我自己也随之转向了。

Pafera 框架的最新版本, PaferaPyAsync ,基于 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 的状态感到无比恐惧,要么感到惊讶,或者两者兼而有之,但不可否认的是,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 的水平,但对于 5 分钟的 JavaScript 快速破解来说,这已经很不错了。根据我自己的测试,这个脚本在数据库接口级别实际上有点落后,因为 node-postgres 的效率远不及 psycopg 对 Python 的效率。切换到 sqlite 作为数据库驱动程序,相同的 ExpressJS 代码每秒会产生超过 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 Framework 将基于 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,并且在工作中注重效率和正确性。