В своей прошлой публикации я затронул тему скорости работы такого веб-фреймворка как Vapor, разработчики обещают что он будет работать до 100 раз быстрее других фреймворков в ваших проектах, но одних лишь слов мало. Давайте взглянем на результаты официальных бенчмарков от Qutheory (далее перевод, прошу обратить на это внимание, так как я не имею отношения к самим тестам, а все минусы бьют по моей карме, поэтому не стоит обвинять меня в том что использовался PHP 5 а не PHP 7, вы уж поймите)
Участники теста:
- Vapor (Swift)
- Ruby on Rails (Ruby)
- Laravel (PHP)
- Lumen (PHP)
- Express (JavaScript)
- Django (Python)
- Flask (Python)
- Spring (Java)
- Nancy (C#)
- Go (без фреймворков)
Тесты:
- Простой текст
- JSON
- Случайный SQLite запрос
Следующие команды запускались трижды для каждого фреймворка на раздельном Digital Ocean Droplet.
wrk -d 10 -t 4 -c 128 http://<host>:<port>/plaintext
wrk -d 10 -t 4 -c 128 http://<host>:<port>/json
wrk -d 10 -t 4 -c 128 http://<host>:<port>/sqlite-fetch
Простой текст
Тест на обработку простого текста является самым простым, а его результаты показывают максимальную скорость работы для каждого фреймворка. Удивительно насколько близко Vapor подобрался к Go. Чистый Swift HTTP сервер основан на потоках, в то время как Go использует сопрограммы. В некоторых случаях сопрограммы намного быстрее, но они требуют дополнительных библиотек и установки. Вполне возможно что Vapor примет этот способ параллелизма в будущем. Кроме того, Swift на Linux еще в бете, поэтому компилируется неоптимизированными toolchains. С новым компилятором Swift имеет все шансы свергнуть Go.
JSON
Будучи написанным на JavaScript, Express получает в этом тесте преимущество (JSON означает JavaScript Object Notation, если кто не знал). Vapor занимает почетное третье место из-за несовершенного синтаксического анализа JSON на Linux, но все равно остается как минимум в три раза быстрее большинства фреймворков.
SQLite запрос
С огромным отрывом Express вырвался вперед, а Go на удивление занимает четвертую позицию в данном тесте. Еще более удивительным является то, что Vapor стал вторым, будучи единственным фреймворком, кроме Spring, использующим ORM.
Код и конфигурация
Вы можете посмотреть код тестовых запросов и конфигурацию для каждого из фреймворков
Vapor
Vapor был запущен с на POSIX-thread HTTP, который компилировался используя Swift’s 06–06 toolchain с релизной конфигурацией и оптимизацией
vapor run --release --port=8000
Vapor CLI сделал создание и запуск приложений на этом фреймворке очень простым, это весь код, который мы использовали для теста.
import Vapor
import Fluent
import FluentSQLite
let app = Application()
do {
let driver = try SQLiteDriver(path: "/home/helper/database/test.sqlite")
Database.default = Database(driver: driver)
} catch {
print("Could not open SQLite database: \(error)")
}
app.get("plaintext") { request in
return "Hello, world!"
}
app.get("json") { request in
return JSON([
"array": [1, 2, 3],
"dict": ["one": 1, "two": 2, "three": 3],
"int": 42,
"string": "test",
"double": 3.14,
"null": nil
])
}
app.get("sqlite-fetch") { request in
guard let user = try User.random() else {
throw Abort.notFound
}
return user
}
app.globalMiddleware = []
app.start()
Настройка базы данных оказалась весьма простой с использованием Fluent, а Swift обеспечивает вашему приложению защиту от сбоев, даже если база данных не там, где мы думаем.
Ruby
«Рельсы» были запущены с помощью прилагаемого сервера, а база данных и маршруты сгенерированы в виде отдельных файлов.
bin/rails s — binding=107.170.131.198 -p 8600 -e production
class BenchmarkController < ActionController::Base
def plaintext
render plain: "Hello world"
end
def json
a = [1, 2, 3]
d = {"one" => 1, "two" => 2, "three" => 3}
r = {"array" => a, "dict" => d, "int" => 42, "string" => "test", "double" => 3.14, "null" => nil}
render :json => r
end
def sqlite
r = ActiveRecord::Base.connection.exec_query("SELECT * FROM users ORDER BY random() LIMIT 1")
render :json => r
end
end
production:
<<: *default
database: /home/helper/database/test.sqlite
Rails.application.routes.draw do
get 'plaintext' => 'benchmark#plaintext'
get 'json' => 'benchmark#json'
get 'sqlite-fetch' => 'benchmark#sqlite'
end
Nancy
Nancy является open-source проектом для .NET, в преимуществах у него простое тестирование, легкий вес и расширяемость. Имея более чем 250 соавторов и активное сообщество, Nancy показывает насколько C# может быть хорош в вебе.
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Dapper;
using Microsoft.Data.Sqlite;
using Nancy;
namespace NancyVsVapor
{
public class HomeModule : NancyModule
{
private static string connstring = string.Concat("Data Source=", Path.Combine(
Path.GetDirectoryName(typeof(HomeModule).GetTypeInfo().Assembly.Location),
"test.sqlite"));
private static Random random = new Random();
public HomeModule()
{
Get("/plaintext", _ => "Hello, World!");
Get("/json", _ =>
{
return Response.AsJson(new JsonModel());
});
Get("sqlite-fetch", async (_, __) =>
{
using (var conn = new SqliteConnection(connstring))
{
var users = await conn.QueryAsync<User>("select * from users where id = @id", new { id = random.Next(1, 3) });
return Response.AsJson(users.FirstOrDefault());
}
});
}
}
}
Laravel
Laravel был организован используя Nginx и PHP 5.
server {
listen 8701;
root /home/helper/laravel-tanner/benchmark/public;
index index.php index.html index.htm;
server_name 107.170.131.198;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
<?php
use DB;
Route::get('/plaintext', function() {
return 'Hello, world!';
});
Route::get('/json', function() {
return [
'array' => [1, 2, 3],
'dict' => [
'one' => 1,
'two' => 2,
'three' => 3
],
'int' => 42,
'string' => 'test',
'double' => 3.14,
'null' => null
];
});
Route::get('/sqlite-fetch', function() {
return DB::select('SELECT * FROM users ORDER BY random() LIMIT 1');
});
Lumen
Lumen был организован аналогично Laravel.
<?php
$app->get('/plaintext', function() {
return 'Hello, world!';
});
$app->get('/json', function() {
return [
'array' => [1, 2, 3],
'dict' => [
'one' => 1,
'two' => 2,
'three' => 3
],
'int' => 42,
'string' => 'test',
'double' => 3.14,
'null' => null
];
});
$app->get('/sqlite-fetch', function() {
return \DB::select('SELECT * FROM users ORDER BY random() LIMIT 1');
});
Express
Express был запущен используя NPM и кластер.
npm run cluster
var cluster = require('cluster');
if(cluster.isMaster) {
var cpuCount = require('os').cpus().length;
for(var i = 0; i < cpuCount; i += 1) {
cluster.fork();
}
cluster.on('exit', function(worker) {
console.log('Worker %d died, replacing', worker.id);
cluster.fork();
});
} else {
var app = require('./app.js');
app.app.listen(app.port, function() {
console.log('Benchmarking worker %d listening on %d', cluster.worker.id, app.port)
});
}
const express = require('express');
const app = express();
const sqlite3 = require('sqlite3');
const db = new sqlite3.Database('../database/test.sqlite');
app.get('/plaintext', function(req, res) {
res.setHeader('Content-Type', 'text/plain');
res.send('Hello, World!');
});
app.get('/json', function(req, res) {
res.send({
array: [1, 2, 3],
dict: {
one: 1,
two: 2,
'three': 3
},
int: 42,
string: 'test',
double: 3.14,
'null': null
});
});
app.get('/sqlite-fetch', function(req, res) {
db.get('select * from users where id = ?', Math.floor(Math.random() * 3) + 1, function(err, row) {
if(err) {
res.send(err.message);
} else {
res.send(row);
}
});
});
module.exports = {
app: app,
port: 8400
}
Express получает такую скорость из-за того, что под капотом у него находится высокопроизводительный C, даже если вы пишете на JavaScript запросы обрабатываются используя C библиотеки.
Django
Django был запущен используя wsgi и gunicorn.
from django.conf.urls import url
from django.http import HttpResponse
from django.http import JsonResponse
from django.db import connection
def plaintext(request):
return HttpResponse('Hello, world')
def json(request):
return JsonResponse({
"array": [1, 2, 3],
"dict": {"one": 1, "two": 2, "three": 3},
"int": 42,
"string": "test",
"double": 3.14,
"null": None
})
def sqlite(request):
cursor = connection.cursor()
cursor.execute("SELECT * FROM users ORDER BY random() LIMIT 1")
row = cursor.fetchone()
return JsonResponse(row, safe=False)
urlpatterns = [
url(r'^plaintext', plaintext),
url(r'^json', json),
url(r'^sqlite-fetch', sqlite)
]
Flask
К Flask тот же подход.
import sys
import flask
import random
import sqlite3
import logging
import socket
logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s')
app = flask.Flask(__name__)
application = app
db = sqlite3.connect('./test.sqlite')
conn = db.cursor()
conn.row_factory = sqlite3.Row
@app.route("/plaintext")
def plaintext():
return "Hello, world!"
@app.route("/json")
def json():
return flask.jsonify(**{
"array": [1, 2, 3],
"dict": {"one": 1, "two": 2, "three": 3},
"int": 42,
"string": "test",
"double": 3.14,
"null": None
})
@app.route("/sqlite-fetch")
def sqlite_fetch():
id = random.randint(1, 3)
r = conn.execute("select * from users where id = ?", (id,)).fetchone()
if r is not None:
d = dict(zip(r.keys(), r))
return flask.jsonify(d)
else:
flask.abort(404)
if __name__ == "__main__":
port = 8137
print 'Listening on port %s' % port
while True:
try:
app.run(port=port, host="107.170.131.198")
sys.exit(0)
except socket.error as e:
logging.warn("socket error: %s" % e)
Go
Go использует веб-сервер, маршрутизатор, а все приложение уместилось в одном файле.
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
func Plaintext(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello World!\n")
}
type JSONStruct struct {
Array []int `json:"array"`
Dict map[string]int `json:"dict"`
Int int `json:"int"`
String string `json:"string"`
Double float64 `json:"double"`
Null interface{} `json:"null"`
}
func JSON(w http.ResponseWriter, req *http.Request) {
j := JSONStruct{Array: []int{1, 2, 3},
Dict: map[string]int{"one": 1, "two": 2, "three": 3},
Int: 42,
String: "test",
Double: 3.14,
Null: nil}
b, _ := json.MarshalIndent(j, "", " ")
io.WriteString(w, string(b))
}
type User struct {
ID int `db:"id" json:"id,omitempty"`
Name string `db:"name" json:"name,omitempty"`
Email string `db:"email" json:"email,omitempty"`
}
// typical usage would keep or cache the open DB connection
var db, _ = sqlx.Open("sqlite3", "../database/test.sqlite")
func SQLiteFetch(w http.ResponseWriter, req *http.Request) {
user := User{}
rows, err := db.Queryx("select * from users order by random() limit 1")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err = rows.StructScan(&user)
if err != nil {
log.Fatal(err)
}
b, _ := json.MarshalIndent(user, "", " ")
io.WriteString(w, string(b))
}
}
var portNumber int
func main() {
flag.IntVar(&portNumber, "port", 8300, "port number to listen on")
flag.Parse()
http.HandleFunc("/plaintext", Plaintext)
http.HandleFunc("/json", JSON)
http.HandleFunc("/sqlite-fetch", SQLiteFetch)
log.Println("bench running on", fmt.Sprintf("%d", portNumber))
err := http.ListenAndServe(fmt.Sprintf(":%d", portNumber), nil)
if err != nil {
log.Fatal(err)
}
}
Spring
Java была запущена с помощью Spring Boot на JVM.
package com.hlprmnky.vapor_spring_benchmark;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import com.google.common.collect.ImmutableMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ApplicationController {
private final AtomicLong counter = new AtomicLong();
private final Random random = new Random();
@Autowired
private UserRepository userRepository;
@RequestMapping("/json")
public Json json() {
return new Json(counter.incrementAndGet(), Arrays.asList(1, 2, 3),
ImmutableMap.of("one", 1, "two", 2, "three", 3),
"test", 42, 3.14);
}
@RequestMapping("/plaintext")
public String plaintext() {
return "Hello, World!";
}
@RequestMapping("/sqlite-fetch")
public User sqliteFetch() {
List<User> allUsers = userRepository.findAll();
return allUsers.get(random.nextInt(allUsers.size()));
}
}
Спасибо за внимание, источник по ссылке.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (25)
GennPen
07.12.2016 09:04+2А правильно ли так сравнивать, на простом return 'Hello, world!', а не на чем-то более сложном?
fogone
07.12.2016 10:14+2List<User> allUsers = userRepository.findAll(); return allUsers.get(random.nextInt(allUsers.size()));
Думаю, нужно было сделать по-другому:
List<User> allUsers; while(random.nextInt() != random.nextInt() { allUsers = userRepository.findAll(); } return allUsers.get(random.nextInt(allUsers.size()));
Где можно посмотреть полный текст тестов? Проводился ли прогрев? Почему для json-а был использован какой-то Json? Его даже в импортах нет. Хотя спринг умеет в json сериализовать обычные pojo. Зачем использовать orm, если сравнивается в большинстве с не-orm-ами? Зачем использовать ImmutableMap, если всё равно каждый раз его создавать? Я уж не говорю о том коде, который я привел выше — он вообще смешной. Я посмотрел на java-версию, потому что хорошо её знаю, уверен, что все эти мини-приложения написаны так же хорошо как и java-версия. Так что вряд ли этот тест имеет хоть какую-то практическую ценность.DigitalSmile
07.12.2016 10:38Согласен, тесты выглядят неубедительными. Даже если глянуть на реализацию в Go — там рандом вообще на стороне SQL. Остальные не открывал и не вдавался в детали, но уже как минимум тесты для Go и Java не эквивалентны.
zirix
08.12.2016 00:13Посмотрел все тесты, везде кроме Spring рандом идет в запросе либо select * where id=?
Еще зачем нужен был counter.incrementAndGet() в JSON не очень понятно, в остальных тестах номера запроса нет.
Тест делался явно без прогрева. Такие вещи обычно упоминают.
DexterHD
07.12.2016 10:57+3php 5 — серьезно? Express сравнивать с Laravel, Rails или Django? Шта?
Express с асинхронным I/O, и Go, без использования go-рутин? 0\
zapimir
07.12.2016 11:15+5C SQLite вообще загадка, как можно такие тесты писать? Казалось бы, что может быть проще выполнить одинаковый запрос на разных языках. При этом лидеры делают выборку тупо по id, в то время как тот же go делает рандомную сортировку и потом выборку — ежу понятно, что так медленнее будет (и чем больше база, тем больше будет разрыв).
Source
07.12.2016 13:02+1С запросом вообще неудачный выбор. Даже если сделать везде random на стороне SQL, время выполнения запроса будет рандомно меняться :-)
maxru
07.12.2016 11:18+2Для PHP фреймворков очень низкие значения RPS даже для PHP 5.
Должны быть около django или около того (на php-fpm без изощрений, типа react или php-pm).
raidhon
07.12.2016 12:08+4Думаю большинство радует что появился новый язык для бекенд.
В остальном ваши тесты ужас летящий на крыльях ночи.
http://www.techempower.com/benchmarks/#section=data-r13&hw=cl&test=plaintext
https://github.com/TechEmpower/FrameworkBenchmarks — исходники тестов
Просто запустите их и сравните с Vapor.
И будет вас счастье!
ZOXEXIVO
07.12.2016 13:25Что скажете на счет этого заявления?
ASP.NET Core 1.1 with Kestrel was ranked as the fastest mainstream fullstack web framework in the plaintext test.
fuCtor
07.12.2016 13:54+1Так понимаю это перевод, или ваше? Нет ни исходников тестов, методология тоже не понятна.
По Ruby, как сказали выше, без Gemfile можно даже не рассматривать результат. Не понятно какой сервер запускался, какая версия RoR. Если 5ая, то API проект или полный создан. Сплошные вопросы, ответ на каждый влияет на итоговые значения.greyfruit
07.12.2016 14:03-1Это перевод официальных тестов от разработчика (ссылки на источник присутствуют), к самим тестам и его результатам я отношения не имею, уж поймите :/
Моим делом было перевести, оформить, добавить опрос и опубликовать это здесь, а на деле выхвачиваю минусы и страдаю по карме.
MOTORIST
07.12.2016 17:19Опять что то хипстерское и супер быстрое. Чем мне нравится Go, тем что ему не нужны никакие фреймворки. Нравится эта orm используешь её, понравилась другая, используешь другую и так во всем.
rule
Ну эти все тесты предсказуемы. Механизм goroutine очень легко маштабируется на одном процессоре и на целом кластере. В свифте многопоточность реализована в GCD через специальные функции ядра на Дарвине. В линуксе это пока сделано с помощью userspace pthread библиотеки libdispatch. В будущем планиурется реализовать модуль ядра под линукс для поддержки родного GCD. Мощ golang как раз не в скорости отдачи JSON, а в невероятно простой в использовании многозадачности и её мощной реализации «внутри» на всех поддерживаемых платформах.
Так что swift еще очень молод. Можно делать серьезные проекты на нем, но нужно понимать, что будут сложности на пути и нужно к ним быть готовыми. Более того, быть пионером — это очень интересно.
Но я за компилируемые языки на сервере, вот неоспоримые преимущества:
— на сервере нет исходников
— нулевые зависимости (весь рантайм можно в один файл запихнуть)
— возможности оптимизации на уровне системных вызовов
— проверка ошибок на уровне сборки