在我最近的一次面试之后,我惊讶地发现我申请的公司仍在使用 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
我们的代码针对每个请求有三个简单的任务:
你可能会问,这是什么愚蠢的测试?好吧,如果你看看这个页面的网络请求,你会注意到一个名为 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 的座右铭……无论如何,在最黑暗的时间线之外……
好的,现在我们终于可以开始动手了。我们将跳过 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
// ====================================================================
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?
在网络上搜索配置问题和性能技巧后,发现两种最流行的技术是缓存配置和路由数据,以避免每次请求时都处理它们。因此,我们将采纳他们的建议并尝试这些技巧。
╰─➤ 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 年的选择。
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,那么现在可能是开始的好时机。
到目前为止,我们只介绍了 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 解决方案相媲美。
现在,我们离山顶越来越近了,我说的山,是指老鼠和人类所记录的最高基准分数。
如果你查看网络上的大多数框架基准测试,你会注意到有两种语言往往占据主导地位: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,并且在工作中注重效率和正确性。 |