가장 최근의 구직 면접 중 하나를 본 후, 제가 지원한 회사가 여전히 Laravel을 사용하고 있다는 사실에 놀랐습니다. Laravel은 제가 약 10년 전에 시도했던 PHP 프레임워크입니다. 당시로서는 괜찮았지만, 기술과 패션에서 한 가지 불변의 것이 있다면, 그것은 스타일과 개념의 지속적인 변화와 재등장입니다. JavaScript 프로그래머라면 아마도 이 오래된 농담을 알고 있을 것입니다.
프로그래머 1: "이 새로운 자바스크립트 프레임워크가 마음에 들지 않아요!"
프로그래머 2: "걱정할 필요 없어요. 6개월만 기다리면 대체할 다른 것이 나올 거예요!"
호기심에 나는 오래된 것과 새로운 것을 테스트하면 정확히 무슨 일이 일어나는지 보기로 했습니다. 물론 웹에는 벤치마크와 주장이 가득 차 있는데, 그 중 가장 인기 있는 것은 아마도 다음과 같습니다. TechEmpower 웹 프레임워크 벤치마크 여기 . 하지만 오늘은 그들과 같은 복잡한 일은 하지 않을 것입니다. 이 글이 다음과 같이 되지 않도록 모든 것을 깔끔하고 간단하게 유지하겠습니다. 전쟁과 평화 , 그리고 당신이 독서를 마칠 때까지 깨어 있을 약간의 기회가 있을 것입니다. 일반적인 경고가 적용됩니다. 이것은 당신의 기계에서 동일하게 작동하지 않을 수 있고, 다른 소프트웨어 버전은 성능에 영향을 미칠 수 있으며, 슈뢰딩거의 고양이는 실제로 정확히 같은 시간에 반쯤 살아 있고 반쯤 죽은 좀비 고양이가 되었습니다.
이 테스트를 위해, 여기에서 보이는 것처럼 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
우리 코드는 각 요청에 대해 세 가지 간단한 작업을 수행합니다.
그게 무슨 멍청한 테스트냐고 물으실지도 모릅니다. 글쎄요, 이 페이지의 네트워크 요청을 보면 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 Lambdas를 사용하지 않겠습니다.
각 사용자의 세션 데이터는 PostgreSQL 데이터베이스에 저장됩니다.
그리고 이 데이터베이스 테이블은 각 테스트 전에 잘립니다.
간단하지만 효과적입니다. 이것이 파페라의 모토입니다. 어쨌든 가장 어두운 시간대를 벗어나서 말입니다.
좋아요, 이제 드디어 손을 더럽히기 시작할 수 있습니다. 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개 요청에서 "Count is 1"을 렌더링하는 초당 21개 요청으로 바뀌었습니다.
그럼 뭐가 잘못된 걸까요? 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'];
우리는 Laravel에서 4줄의 코드(그리고 많은 구성 작업)로 했던 일을 98줄의 코드로 했습니다. (물론, 적절한 오류 처리와 사용자 중심 메시지를 했다면 줄 수가 약 두 배가 될 것입니다.) 초당 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 프레임워크 작성자와 상의해야 합니다. 그의 테스트 결과에서 그는 루신다가 라라벨을 이긴다 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이 아마도 타이핑된 키스트로크 측면에서 가장 표현력이 풍부한 언어라는 것을 알게 되었습니다. 중괄호와 괄호가 없고, list와 dict comprehension이 있으며, 세미콜론이 아닌 들여쓰기를 기반으로 하는 블로킹이 있어 Python은 간단하지만 기능이 강력합니다.
불행히도, 파이썬은 소프트웨어가 얼마나 많이 작성되었는가에 관계없이 가장 느린 범용 언어이기도 합니다. 사용 가능한 파이썬 라이브러리의 수는 비슷한 언어보다 약 4배 더 많고 방대한 도메인을 포괄하지만, 파이썬이 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는 새 요청이 들어올 때마다 스크립트를 다시 실행한다는 것을 알아야 합니다.Flask는 이미 차를 시동하고 길가에서 기다리는 젊고 열정적인 택시 운전사인 반면 PHP는 전화가 올 때까지 집에서 기다리다가 차를 몰고 와서 당신을 태우는 늙은 운전사인 것과 같습니다.옛날 사람이고 PHP가 일반 HTML과 SHTML 파일에 대한 훌륭한 변화였던 시절에 왔기 때문에 얼마나 많은 시간이 흘렀는지 깨닫는 것은 약간 슬프지만 디자인 차이로 인해 PHP는 메모리에 머무르고 저글러의 민첩함으로 요청을 처리하는 Python, Java 및 Node.js 서버와 경쟁하기 어렵습니다.
Flask는 지금까지 가장 빠른 프레임워크일 수 있지만, 사실 꽤 오래된 소프트웨어입니다. Python 커뮤니티는 몇 년 전에 새로운 비동기 ASGI 서버로 전환했고, 물론 저도 그들과 함께 전환했습니다.
Pafera Framework의 최신 버전 파페라파이 비동기 , Starlette에 기반을 두고 있습니다. Quart라는 Flask의 ASGI 버전이 있지만 Quart와 Starlette의 성능 차이가 충분해서 Starlette에 코드를 리베이스했습니다.
비동기 프로그래밍은 많은 사람들에게 두려운 개념일 수 있지만, 10년 전 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
새로운 챔피언이 생겼습니다, 신사 숙녀 여러분! 이전 최고 기록은 초당 704개의 요청을 기록한 순수 PHP 버전이었고, 그 다음에는 초당 1080개의 요청을 기록한 Flask 버전이 뒤를 이었습니다. Starlette 스크립트는 초당 4562개의 요청을 기록하며 이전 경쟁자들을 모두 압도했습니다. 순수 PHP보다 6배, Flask보다 4배 향상되었습니다.
아직 WSGI Python 코드를 ASGI로 변경하지 않았다면 지금이 시작하기에 좋은 시기일 수 있습니다.
지금까지 우리는 PHP와 Python 프레임워크만 다루었습니다. 그러나 실제로 세계의 많은 부분이 Java, DotNet, Node.js, Ruby on Rails 및 기타 이러한 기술을 웹사이트에 사용합니다. 이것은 결코 세계의 모든 생태계와 생물군에 대한 포괄적인 개요가 아니므로 유기 화학과 동등한 프로그래밍을 하지 않기 위해 코드를 입력하기 가장 쉬운 프레임워크만 선택하겠습니다... Java는 분명히 그렇지 않습니다.
K&R C 또는 Knuth의 사본 아래에 숨어 있지 않는 한 컴퓨터 프로그래밍의 예술 지난 15년 동안 여러분은 아마도 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가 휘두르는 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 해킹으로는 나쁘지 않습니다. 제가 직접 테스트한 결과, 이 스크립트는 실제로 데이터베이스 인터페이싱 수준에서 약간 제한을 받고 있는데, 그 이유는 node-postgres가 Python의 psycopg만큼 효율적이지 않기 때문입니다. 데이터베이스 드라이버로 sqlite로 전환하면 동일한 ExpressJS 코드에 대해 초당 3000개 이상의 요청이 생성됩니다.
가장 중요한 점은 Python의 실행 속도가 느리더라도 ASGI 프레임워크는 실제로 특정 작업 부하에 대해서는 Node.js 솔루션과 경쟁할 수 있다는 것입니다.
그러니 지금 우리는 산 꼭대기에 점점 가까워지고 있습니다. 여기서 산이란 쥐와 인간이 모두 기록한 가장 높은 벤치마크 점수를 뜻합니다.
웹에서 제공되는 대부분의 프레임워크 벤치마크를 살펴보면, 상위를 차지하는 경향이 있는 두 가지 언어가 있다는 것을 알 수 있습니다. C++와 Rust입니다. 저는 90년대부터 C++로 작업했고, MFC/ATL이 나오기 전에도 Win32 C++ 프레임워크를 직접 만들어서 이 언어에 대한 경험이 많습니다. 이미 알고 있는 것을 가지고 작업하는 건 별로 재미없으니까, 대신 Rust 버전을 만들어보겠습니다. ;)
Rust는 프로그래밍 언어에 있어서 비교적 새로운 언어지만, Linus Torvalds가 Rust를 Linux 커널 프로그래밍 언어로 수용하겠다고 발표했을 때 저는 Rust가 호기심의 대상이 되었습니다. 우리 같은 오래된 프로그래머들에게는 이 새로운 유행의 뉴에이지 히피족이 미국 헌법에 새로운 수정안을 추가하겠다고 말하는 것과 마찬가지입니다.
이제, 여러분이 숙련된 프로그래머라면, 젊은 사람들처럼 빨리 유행에 뛰어들지 않으려고 합니다. 그렇지 않으면 언어나 라이브러리의 급격한 변화에 속아넘어갈 수 있습니다. (AngularJS의 첫 번째 버전을 사용한 사람이라면 제가 무슨 말을 하는지 알 것입니다.) Rust는 여전히 실험적 개발 단계에 있으며, 웹에 있는 많은 코드 예제가 현재 버전의 패키지로는 더 이상 컴파일되지 않는다는 사실이 우스꽝스럽습니다.
그러나 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 언어 자체를 배우는 것이 다른 언어보다 더 어렵다는 것을 알게 되었습니다.6502 어셈블리를 제외하고 제가 본 어떤 것보다 많은 함정이 있기 때문입니다.하지만 Rust 서버가 PHP 서버보다 14배 많은 사용자를 처리할 수 있다면 결국 기술을 전환하는 데 이득이 있을 것입니다.그래서 Pafera Framework의 다음 버전은 Rust를 기반으로 할 것입니다.학습 곡선은 스크립팅 언어보다 훨씬 높지만 성능은 그만한 가치가 있습니다.Rust를 배우는 데 시간을 투자할 수 없다면 Starlette 또는 Node.js를 기반으로 기술 스택을 구축하는 것도 나쁜 결정이 아닙니다.
지난 20년 동안, 우리는 저렴한 정적 호스팅 사이트에서 LAMP 스택을 사용한 공유 호스팅으로, 그리고 AWS, Azure 및 기타 클라우드 서비스에 VPS를 임대하는 방식으로 전환했습니다. 오늘날 많은 회사는 편리한 클라우드 서비스의 등장으로 인해 느린 서버와 애플리케이션에 더 많은 하드웨어를 쉽게 투입할 수 있게 되면서 사용 가능하거나 가장 저렴한 사람을 찾을 수 있는 사람을 기반으로 설계 결정을 내리는 데 만족하고 있습니다. 이로 인해 장기적인 기술 부채를 희생하고 단기적으로 큰 이익을 얻었습니다.
70년 전 소련과 미국 사이에 큰 우주 경쟁이 있었습니다. 소련은 대부분의 초기 이정표에서 승리했습니다. 그들은 스푸트니크에서 최초의 위성을, 라이카에서 최초의 우주견을, 루나 2에서 최초의 달 우주선을, 유리 가가린과 발렌티나 테레슈코바에서 최초의 우주 남녀를, 등등...
하지만 그들은 천천히 기술 부채를 쌓기 시작했습니다.
소련은 이러한 업적을 모두 가장 먼저 달성했지만, 그들의 엔지니어링 프로세스와 목표 때문에 장기적 실현 가능성보다는 단기적 도전에 집중하게 되었습니다. 그들은 매번 도약할 때마다 이겼지만, 상대방이 결승선을 향해 꾸준히 걸어가는 동안 점점 지치고 느려졌습니다.
닐 암스트롱이 생방송으로 달에 역사적인 발걸음을 내딛은 후, 미국이 선두를 차지했고, 소련 프로그램이 흔들리는 동안 그 자리에 머물렀습니다. 이는 오늘날의 기업들이 다음 큰 일, 다음 큰 보상, 다음 큰 기술에 집중하면서도 장기적으로 적절한 습관과 전략을 개발하지 못하는 것과 다를 바 없습니다.
시장에 처음 진출한다고 해서 해당 시장에서 지배적인 플레이어가 된다는 것은 아닙니다. 반대로, 일을 제대로 하기 위해 시간을 들이는 것이 성공을 보장하지는 않지만, 장기적으로 성과를 거둘 가능성은 확실히 높아집니다. 회사의 기술 책임자라면 업무에 맞는 올바른 방향과 도구를 선택하세요. 인기가 성과와 효율성을 대체하지 않도록 하세요.
Rust, ExpressJS, Flask, Starlette, Pure PHP 스크립트가 포함된 7z 파일을 다운로드하고 싶으신가요?
저자 소개 |
|
![]() |
짐은 90년대에 IBM PS/2를 얻은 이후로 프로그래밍을 해왔습니다. 오늘날까지도 그는 여전히 HTML과 SQL을 손으로 쓰는 것을 선호하며, 작업의 효율성과 정확성에 집중합니다. |