Скорость работы Vapor по сравнению с другими веб-фреймворками
В своей прошлой публикации я затронул тему скорости работы такого веб-фреймворка как Vapor, разработчики обещают что он будет работать до 100 раз быстрее других фреймворков в ваших проектах, но одних лишь слов мало. Давайте взглянем на результаты официальных бенчмарков от Qutheory (далее перевод)
Участники теста:
- 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://:/plaintext
wrk -d 10 -t 4 -c 128 http://:/json
wrk -d 10 -t 4 -c 128 http://:/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("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;
}
}
[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.
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 allUsers = userRepository.findAll();
return allUsers.get(random.nextInt(allUsers.size()));
}
}
Спасибо за внимание, источник по ссылке.
Комментарии (8)
7 декабря 2016 в 06:53
+3↑
↓
Ну эти все тесты предсказуемы. Механизм goroutine очень легко маштабируется на одном процессоре и на целом кластере. В свифте многопоточность реализована в GCD через специальные функции ядра на Дарвине. В линуксе это пока сделано с помощью userspace pthread библиотеки libdispatch. В будущем планиурется реализовать модуль ядра под линукс для поддержки родного GCD. Мощ golang как раз не в скорости отдачи JSON, а в невероятно простой в использовании многозадачности и её мощной реализации «внутри» на всех поддерживаемых платформах.Так что swift еще очень молод. Можно делать серьезные проекты на нем, но нужно понимать, что будут сложности на пути и нужно к ним быть готовыми. Более того, быть пионером — это очень интересно.
Но я за компилируемые языки на сервере, вот неоспоримые преимущества:
— на сервере нет исходников
— нулевые зависимости (весь рантайм можно в один файл запихнуть)
— возможности оптимизации на уровне системных вызовов
— проверка ошибок на уровне сборки7 декабря 2016 в 08:54
0↑
↓
Неплохо было бы сравнить с Phoenix на Elixir7 декабря 2016 в 09:04
0↑
↓
А правильно ли так сравнивать, на простом return 'Hello, world!', а не на чем-то более сложном?7 декабря 2016 в 09:44
0↑
↓
В случае с Ruby неплохо было бы показать Gemfile и сравнить с Sinatra.7 декабря 2016 в 10:14
+2↑
↓
List
allUsers = userRepository.findAll(); return allUsers.get(random.nextInt(allUsers.size())); Думаю, нужно было сделать по-другому:
List
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-версия. Так что вряд ли этот тест имеет хоть какую-то практическую ценность.
7 декабря 2016 в 10:38
0↑
↓
Согласен, тесты выглядят неубедительными. Даже если глянуть на реализацию в Go — там рандом вообще на стороне SQL. Остальные не открывал и не вдавался в детали, но уже как минимум тесты для Go и Java не эквивалентны.
7 декабря 2016 в 10:45
0↑
↓
php-fpm.conf не показан. php5 устарел.HHVM, HipHop — нет. хорошее сравнение.7 декабря 2016 в 10:57
0↑
↓
php 5 — серьезно? Express сравнивать с Laravel, Rails или Django? Шта?
Express с асинхронным I/O, и Go, без использования go-рутин? 0\