СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Скорость работы Vapor по сравнению с другими веб-фреймворками в черновиках Recovery Mode

Правда ли, что Vapor на самом деле такой быстрый и безопасный, как говорят его авторы? Сегодня мы посмотрим на скорость работы Swift в серверной части вашего приложения в сравнении с Ruby, PHP, JS, Python, Java, C#, Go!




В своей прошлой публикации я затронул тему скорости работы такого веб-фреймворка как 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 и Express оказались быстрейшими в рамках конкурса, конкурируя с чистым Go.


Простой текст

Тест на обработку простого текста является самым простым, а его результаты показывают максимальную скорость работы для каждого фреймворка. Удивительно насколько близко Vapor подобрался к Go. Чистый Swift HTTP сервер основан на потоках, в то время как Go использует сопрограммы. В некоторых случаях сопрограммы намного быстрее, но они требуют дополнительных библиотек и установки. Вполне возможно что Vapor примет этот способ параллелизма в будущем. Кроме того, Swift на Linux еще в бете, поэтому компилируется неоптимизированными toolchains. С новым компилятором Swift имеет все шансы свергнуть Go.

image

JSON

Будучи написанным на JavaScript, Express получает в этом тесте преимущество (JSON означает JavaScript Object Notation, если кто не знал). Vapor занимает почетное третье место из-за несовершенного синтаксического анализа JSON на Linux, но все равно остается как минимум в три раза быстрее большинства фреймворков.

image

SQLite запрос

С огромным отрывом Express вырвался вперед, а Go на удивление занимает четвертую позицию в данном тесте. Еще более удивительным является то, что Vapor стал вторым, будучи единственным фреймворком, кроме Spring, использующим ORM.

image

Код и конфигурация

Вы можете посмотреть код тестовых запросов и конфигурацию для каждого из фреймворков

Vapor

Vapor был запущен с на POSIX-thread HTTP, который компилировался используя Swift’s 06–06 toolchain с релизной конфигурацией и оптимизацией

vapor run --release --port=8000


Vapor CLI сделал создание и запуск приложений на этом фреймворке очень простым, это весь код, который мы использовали для теста.

main.swift
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

benchmark_controller.rb
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


database.yml
production:
  <<: *default
  database: /home/helper/database/test.sqlite


routes.rb
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# может быть хорош в вебе.

HomeModule.cs
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.

laravel.conf
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;
    }
}


routes.php
<?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.

routes.php
<?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

cluster.js
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)
	});
}


app.js
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.

urls.py
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 тот же подход.

run.py
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 использует веб-сервер, маршрутизатор, а все приложение уместилось в одном файле.

bench.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.

ApplicationController.java
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)

+4
rule ,  
Ну эти все тесты предсказуемы. Механизм goroutine очень легко маштабируется на одном процессоре и на целом кластере. В свифте многопоточность реализована в GCD через специальные функции ядра на Дарвине. В линуксе это пока сделано с помощью userspace pthread библиотеки libdispatch. В будущем планиурется реализовать модуль ядра под линукс для поддержки родного GCD. Мощ golang как раз не в скорости отдачи JSON, а в невероятно простой в использовании многозадачности и её мощной реализации «внутри» на всех поддерживаемых платформах.

Так что swift еще очень молод. Можно делать серьезные проекты на нем, но нужно понимать, что будут сложности на пути и нужно к ним быть готовыми. Более того, быть пионером — это очень интересно.
Но я за компилируемые языки на сервере, вот неоспоримые преимущества:
— на сервере нет исходников
— нулевые зависимости (весь рантайм можно в один файл запихнуть)
— возможности оптимизации на уровне системных вызовов
— проверка ошибок на уровне сборки
+4
biziwalker ,  
Неплохо было бы сравнить с Phoenix на Elixir
+2
+3 –1
GennPen ,  
А правильно ли так сравнивать, на простом return 'Hello, world!', а не на чем-то более сложном?
+2
ertaquo ,  
В случае с Ruby неплохо было бы показать Gemfile и сравнить с Sinatra.
+2
fogone ,  
List<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-версия. Так что вряд ли этот тест имеет хоть какую-то практическую ценность.
0
DigitalSmile ,  
Согласен, тесты выглядят неубедительными. Даже если глянуть на реализацию в Go — там рандом вообще на стороне SQL. Остальные не открывал и не вдавался в детали, но уже как минимум тесты для Go и Java не эквивалентны.
0
zirix ,  

Посмотрел все тесты, везде кроме Spring рандом идет в запросе либо select * where id=?


Еще зачем нужен был counter.incrementAndGet() в JSON не очень понятно, в остальных тестах номера запроса нет.


Тест делался явно без прогрева. Такие вещи обычно упоминают.

+2
tzlom ,  
php-fpm.conf не показан. php5 устарел.HHVM, HipHop — нет. хорошее сравнение.
+3
+4 –1
DexterHD ,  
php 5 — серьезно? Express сравнивать с Laravel, Rails или Django? Шта?
Express с асинхронным I/O, и Go, без использования go-рутин? 0\
+5
zapimir ,  
C SQLite вообще загадка, как можно такие тесты писать? Казалось бы, что может быть проще выполнить одинаковый запрос на разных языках. При этом лидеры делают выборку тупо по id, в то время как тот же go делает рандомную сортировку и потом выборку — ежу понятно, что так медленнее будет (и чем больше база, тем больше будет разрыв).
+1
Source ,  

С запросом вообще неудачный выбор. Даже если сделать везде random на стороне SQL, время выполнения запроса будет рандомно меняться :-)

+2
maxru ,  

Для PHP фреймворков очень низкие значения RPS даже для PHP 5.
Должны быть около django или около того (на php-fpm без изощрений, типа react или php-pm).

+1
zapimir ,  
видать еще и opcache не включен
+1
maxru ,  

Да может оно там cgi на apache, я не смотрел. Я просто говорю, что по опыту — числа слишком низкие.

+1
zapimir ,  
Ага у меня API сервис на nginx + php7 выдает порядка 1700 rps (на простейшей VPSке), а тут 200 на hello world — смешно. PHP7 конечно быстрее работает, чем 5-ка, но не настолько.
0
maxru ,  

Ну да, от 1-2к. На HHVM примерно такого же порядка результаты.

+4
raidhon ,  
Думаю большинство радует что появился новый язык для бекенд.
В остальном ваши тесты ужас летящий на крыльях ночи.

http://www.techempower.com/benchmarks/#section=data-r13&hw=cl&test=plaintext
https://github.com/TechEmpower/FrameworkBenchmarks — исходники тестов
Просто запустите их и сравните с Vapor.
И будет вас счастье!
0
ZOXEXIVO ,  
Что скажете на счет этого заявления?

ASP.NET Core 1.1 with Kestrel was ranked as the fastest mainstream fullstack web framework in the plaintext test.
+1
fuCtor ,  
Так понимаю это перевод, или ваше? Нет ни исходников тестов, методология тоже не понятна.
По Ruby, как сказали выше, без Gemfile можно даже не рассматривать результат. Не понятно какой сервер запускался, какая версия RoR. Если 5ая, то API проект или полный создан. Сплошные вопросы, ответ на каждый влияет на итоговые значения.
–1
greyfruit ,  
Это перевод официальных тестов от разработчика (ссылки на источник присутствуют), к самим тестам и его результатам я отношения не имею, уж поймите :/
Моим делом было перевести, оформить, добавить опрос и опубликовать это здесь, а на деле выхвачиваю минусы и страдаю по карме.
+5
fuCtor ,  
Тогда поставьте что это перевод и оформите как перевод.
0
Source ,  

Что ж тут непонятного… Явно Webrick и полный проект.

+5
Interfere ,  
Лого для Vapor — это слеза разработчика?
+1
XimikS ,  
Есть маленькая ложь, есть большая ложь, и есть бенчмарки.
0
MOTORIST ,  
Опять что то хипстерское и супер быстрое. Чем мне нравится Go, тем что ему не нужны никакие фреймворки. Нравится эта orm используешь её, понравилась другая, используешь другую и так во всем.