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

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

ce7cdd722c974651a581a2433887e70c.jpg


В своей прошлой публикации я затронул тему скорости работы такого веб-фреймворка как 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 и 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("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
 [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
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 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 на Elixir
  • 7 декабря 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\

© Habrahabr.ru