Именно таким вопросом задалась команда Почты Mail.Ru перед написанием очередного сервиса. Основная цель такого выбора — высокая эффективность процесса разработки в рамках выбранного языка/технологии. Что влияет на этот показатель?
  • Производительность;
  • Наличие средств отладки и профилирования;
  • Большое сообщество, позволяющее быстро найти ответы на вопросы;
  • Наличие стабильных библиотек и модулей, необходимых для разработки веб-приложений;
  • Количество разработчиков на рынке;
  • Возможность разработки в современных IDE;
  • Порог вхождения в язык.

Кроме этого, разработчики приветствовали немногословность и выразительность языка. Лаконичность, безусловно, так же влияет на эффективность разработки, как отсутствие килограммовых гирь на вероятность успеха марафонца.

Исходные данные


Претенденты


Так как многие серверные микротаски нередко рождаются в клиентской части почты, то первый претендент — это, конечно, Node.js с ее родным JavaScript и V8 от Google.

После обсуждения и исходя из предпочтений внутри команды были определены остальные участники конкурса: Scala, Go и Rust.

В качестве теста производительности предлагалось написать простой HTTP-сервер, который получает от общего сервиса шаблонизации HTML и отдает клиенту. Такое задание диктуется текущими реалиями работы почты — вся шаблонизация клиентской части происходит на V8 с помощью шаблонизатора fest.

При тестировании выяснилось, что все претенденты работают примерно с одинаковой производительностью в такой постановке — все упиралось в производительность V8. Однако реализация задания не была лишней — разработка на каждом из языков позволила составить значительную часть субъективных оценок, которые так или иначе могли бы повлиять на окончательный выбор.

Итак, мы имеем два сценария. Первый — это просто приветствие по корневому URL:
GET / HTTP/1.1
Host: service.host

HTTP/1.1 200 OK

Hello World!

Второй — приветствие клиента по его имени, переданному в пути URL:
GET /greeting/user HTTP/1.1
Host: service.host

HTTP/1.1 200 OK

Hello, user

Окружение


Все тесты проводились на виртуальной машине VirtualBox.

Хост, MacBook Pro:
  • 2,6 GHz Intel Core i5 (dual core);
  • CPU Cache L1: 32 KB, L2: 256 KB, L3: 3 MB;
  • 8 GB 1600 MHz DDR3.

VM:
  • 4 GB RAM;
  • VT-x/AMD-v, PAE/NX, KVM.

Программное обеспечение:
  • CentOS 6.7 64bit;
  • Go 1.5.1;
  • Rustc 1.4.0;
  • Scala 2.11.7, sbt 0.13.9;
  • Java 1.8.0_65;
  • Node 5.1.1;
  • Node 0.12.7;
  • nginx 1.8.0;
  • wrk 4.0.0.

Помимо стандартных модулей, в примерах на Rust использовался hyper, на Scala — spray. В Go и Node.js использовались только нативные пакеты/модули.

Инструменты измерения


Производительность сервисов тестировалась при помощи следующих инструментов:

В данной статье рассматриваются бенчмарки wrk и ab.

Результаты


Производительность


wrk

Ниже представлены данные пятиминутного теста, с 1000 соединений и 50 потоками:
wrk -d300s -c1000 -t50 --timeout 2s http://service.host
Label Average Latency, ms Request, #/sec
Go 104,83 36 191,37
Rust 0,02906 32 564,13
Scala 57,74 17 182,40
Node 5.1.1 69,37 14 005,12
Node 0.12.7 86,68 11 125,37

wrk -d300s -c1000 -t50 --timeout 2s http://service.host/greeting/hello
Label Average Latency, ms Request, #/sec
Go 105,62 33 196,64
Rust 0,03207 29 623,02
Scala 55,8 17 531,83
Node 5.1.1 71,29 13 620,48
Node 0.12.7 90,29 10 681,11

Столь хорошо выглядящие, но, к сожалению, неправдоподобные цифры в результатах Average Latency у Rust свидетельствуют об одной особенности, которая присутствует в модуле hyper. Все дело в том, что параметр -c в wrk говорит о количестве подключений, которые wrk откроет на каждом треде и не будет закрывать, т. е. keep-alive подключений. Hyper работает с keep-alive не совсем ожидаемо — раз, два.

Более того, если вывести через Lua-скрипт распределение запросов по тредам, отправленным wrk, мы увидим, что все запросы отправляет только один тред.

Для интересующихся Rust также стоит отметить, что эти особенности привели вот к чему.

Поэтому, чтобы тест был достоверным, было решено провести аналогичный тест, поставив перед сервисом nginx, который будет держать соединения с wrk и проксировать их в нужный сервис:
upstream u_go {
    server 127.0.0.1:4002;
    keepalive 1000;
}

server {
        listen 80;
        server_name go;
        access_log off;

        tcp_nopush on;
        tcp_nodelay on;

        keepalive_timeout 300;
        keepalive_requests 10000;

        gzip off;
        gzip_vary off;

        location / {
                proxy_pass http://u_go;
        }
}

wrk -d300s -c1000 -t50 --timeout 2s http://nginx.host/service
Label Average Latency, ms Request, #/sec
Rust 155,36 9 196,32
Go 145,24 7 333,06
Scala 233,69 2 513,95
Node 5.1.1 207,82 2 422,44
Node 0.12.7 209,5 2 410,54

wrk -d300s -c1000 -t50 --timeout 2s http://nginx.host/service/greeting/hello
Label Average Latency, ms Request, #/sec
Rust 154,95 9 039,73
Go 147,87 7 427,47
Node 5.1.1 199,17 2 470,53
Node 0.12.7 177,34 2 363,39
Scala 262,19 2 218,22

Как видно из результатов, overhead с nginx значителен, но в нашем случае нас интересует производительность сервисов, которые находятся в равных условиях, независимо от задержки nginx.

ab

Утилита от Apache ab, в отличие от wrk, не держит keep-alive соединений, поэтому nginx нам тут не пригодится. Попробуем выполнить 50 000 запросов за 10 секунд, с 256 возможными параллельными запросами.
ab -n50000 -c256 -t10 http://service.host/
Label Completed requests, # Time per request, ms Request, #/sec
Go 50 000,00 22,04 11 616,03
Rust 32 730,00 78,22 3 272,98
Node 5.1.1 30 069,00 85,14 3 006,82
Node 0.12.7 27 103,00 94,46 2 710,22
Scala 16 691,00 153,74 1 665,17

ab -n50000 -c256 -t10 http://service.host/greeting/hello
Label Completed requests, # Time per request, ms Request, #/sec
Go 50 000,00 21,88 11 697,82
Rust 49 878,00 51,42 4 978,66
Node 5.1.1 30 333,00 84,40 3 033,29
Node 0.12.7 27 610,00 92,72 2 760,99
Scala 27 178,00 94,34 2 713,59

Стоит отметить, что для Scala-приложения характерен некоторый «прогрев» из-за возможных оптимизаций JVM, которые происходят во время работы приложения.

Как видно, без nginx hyper в Rust по-прежнему плохо справляется даже без keep-alive соединений. А единственный, кто успел за 10 секунд обработать 50 000 запросов, был Go.

Исходный код


Node.js
var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
var http = require("http");
var debug = require("debug")("lite");
var workers = [];
var server;

cluster.on('fork', function(worker) {
    workers.push(worker);

    worker.on('online', function() {
        debug("worker %d is online!", worker.process.pid);
    });

    worker.on('exit', function(code, signal) {
        debug("worker %d died", worker.process.pid);
    });

    worker.on('error', function(err) {
        debug("worker %d error: %s", worker.process.pid, err);
    });

    worker.on('disconnect', function() {
        workers.splice(workers.indexOf(worker), 1);
        debug("worker %d disconnected", worker.process.pid);
    });
});

if (cluster.isMaster) {
    debug("Starting pure node.js cluster");

    ['SIGINT', 'SIGTERM'].forEach(function(signal) {
        process.on(signal, function() {
            debug("master got signal %s", signal);
            process.exit(1);
        });
    });

    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }
} else {
    server = http.createServer();

    server.on('listening', function() {
        debug("Listening %o", server._connectionKey);
    });

    var greetingRe = new RegExp("^\/greeting\/([a-z]+)$", "i");
    server.on('request', function(req, res) {
        var match;

        switch (req.url) {
            case "/": {
                res.statusCode = 200;
                res.statusMessage = 'OK';
                res.write("Hello World!");
                break;
            }

            default: {
                match = greetingRe.exec(req.url);
                res.statusCode = 200;
                res.statusMessage = 'OK';
                res.write("Hello, " + match[1]);    
            }
        }

        res.end();
    });

    server.listen(8080, "127.0.0.1");
}

Go
package main

import (
    "fmt"
    "net/http"
    "regexp"
)

func main() {
    reg := regexp.MustCompile("^/greeting/([a-z]+)$")
    http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        switch r.URL.Path {
        case "/":
            fmt.Fprint(w, "Hello World!")
        default:
            fmt.Fprintf(w, "Hello, %s", reg.FindStringSubmatch(r.URL.Path)[1])
        }
    }))
}

Rust
extern crate hyper;
extern crate regex;

use std::io::Write;
use regex::{Regex, Captures};

use hyper::Server;
use hyper::server::{Request, Response};
use hyper::net::Fresh;
use hyper::uri::RequestUri::{AbsolutePath};

fn handler(req: Request, res: Response<Fresh>) {
    let greeting_re = Regex::new(r"^/greeting/([a-z]+)$").unwrap();

    match req.uri {
        AbsolutePath(ref path) => match (&req.method, &path[..]) {
            (&hyper::Get, "/") => {
                hello(&req, res);
            },
            _ => {
                greet(&req, res, greeting_re.captures(path).unwrap());
            }
        },
        _ => {
            not_found(&req, res);
        }
    };
}

fn hello(_: &Request, res: Response<Fresh>) {
    let mut r = res.start().unwrap();
    r.write_all(b"Hello World!").unwrap();
    r.end().unwrap();
}

fn greet(_: &Request, res: Response<Fresh>, cap: Captures) {
    let mut r = res.start().unwrap();
    r.write_all(format!("Hello, {}", cap.at(1).unwrap()).as_bytes()).unwrap();
    r.end().unwrap();
}

fn not_found(_: &Request, mut res: Response<Fresh>) {
    *res.status_mut() = hyper::NotFound;
    let mut r = res.start().unwrap();
    r.write_all(b"Not Found\n").unwrap();
}

fn main() {
    let _ = Server::http("127.0.0.1:8080").unwrap().handle(handler);
}

Scala
package lite

import akka.actor.{ActorSystem, Props}
import akka.io.IO
import spray.can.Http
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
import akka.actor.Actor
import spray.routing._
import spray.http._
import MediaTypes._
import org.json4s.JsonAST._

object Boot extends App {
  implicit val system = ActorSystem("on-spray-can")
  val service = system.actorOf(Props[LiteActor], "demo-service")
  implicit val timeout = Timeout(5.seconds)
  IO(Http) ? Http.Bind(service, interface = "localhost", port = 8080)
}

class LiteActor extends Actor with LiteService {
  def actorRefFactory = context
  def receive = runRoute(route)
}

trait LiteService extends HttpService {
  val route =
    path("greeting" / Segment) { user =>
      get {
        respondWithMediaType(`text/html`) {
          complete("Hello, " + user)
        }
      }
    } ~
    path("") {
      get {
        respondWithMediaType(`text/html`) {
          complete("Hello World!")
        }
      }
    }
}


Обобщение


Представим определенные в начале статьи критерии успеха в виде таблицы. Все претенденты имеют средства дебага и профилирования, поэтому соответствующие столбцы в таблице отсутствуют.

Label Performance Rate0 Community size1 Packages count IDE Support Developers5
Go 100,00% 12 759 104 3832 + 315
Rust 89,23% 3 391 3 582 +4 21
Scala 52,81% 44 844 172 5933 + 407
Node 5.1.1 41,03% 102 328 215 916 + 654
Node 0.12.7 32,18% 102 328 215 916 + 654

0 Производительность считалась на основании пятиминутных тестов wrk без nginx, по параметру RPS.
1 Размер сообщества оценивался по косвенному признаку — количеству вопросов с соответствующим тегом на StackOverflow.
2 Количество пакетов, индексированных на godoc.org.
3 Очень приблизительно — поиск по языкам Java, Scala на github.com.
4 Под многими любимую Idea плагина до сих пор нет.
5 По данным hh.ru.

Наглядно о размерах сообщества могут говорить вот такие графики количества вопросов по тегам за день:

Go



Rust



Scala



Node.js



Для сравнения, PHP:



Выводы


Понимая, что бенчмарки производительности — вещь достаточно зыбкая и неблагодарная, сделать какие-то однозначные выводы только на основании таких тестов сложно. Безусловно, все диктуется типом задачи, которую нужно решать, требованиями к показателям программы и другим нюансам окружения.

В нашем случае по совокупности определенных выше критериев и, так или иначе, субъективных взглядов мы выбрали Go.

Содержание субъективных оценок было намеренно опущено в этой статье, дабы не делать очередной наброс и не провоцировать холивар. Тем более что если бы такие оценки не учитывались, то по критериям, указанным выше, результат остался бы прежним.

Комментарии (96)


  1. EagleMoor
    17.12.2015 13:30
    +10

    Mail.ru как бы намекает, что скоро начнет хантить Go разрабов =)


  1. Lite4
    17.12.2015 13:40
    +9

    Интересно, почему python не попал на тестирование? По остальным параметрам более чем подходит:
    community size: 509,465
    packages count: 71 290
    developers: 3 563

    p.s. наткнулся на интересный бенчмарк в тему, вот ссылка


    1. gobwas
      17.12.2015 14:02

      Ага, спасибо, тоже изучали бенчмарк по ссылке. В наших тестах, к слову, так же была темная лошадка C++, которая оказалась самой очень быстрой. На python почему-то никто из команды не взялся разрабатывать первый вариант бенчмарка, поэтому его не стали рассматривать и потом. =)


      1. ilnuribat
        17.12.2015 15:13
        +3

        Было бы круто увидеть бенчмарки темной лошадки


        1. gobwas
          17.12.2015 17:14

          Внизу чуть-чуть в комментах приоткрыли завесу по wrk ))


  1. divan0
    17.12.2015 13:45
    +1

    Хороший обзор, спасибо.

    К слову, regexp библиотека в Go считается медленной, хотя там использован алгоритм, несколько отличный от C-шного, который гарантирует линейную зависимость времени от размера входных данных.

    Для этого случая можно попробовать PCRE (хотя, он оказался медленнее), или наивный split строки:

    Код вариантов
    package regex

    import (
    «github.com/glenn-brown/golang-pkg-pcre/src/pkg/pcre»
    «regexp»
    «strings»
    )

    var (
    reg = regexp.MustCompile("^/greeting/([a-z]+)$")
    pcreReg = pcre.MustCompile("^/greeting/([a-z]+)$", 0)
    )

    func RegexOrig(str string) string {
    return reg.FindStringSubmatch(str)[1]
    }

    func RegexPCRE(str string) string {
    return pcreReg.MatcherString(str, 0).GroupString(1)
    }

    func RegexStrings(str string) string {
    return strings.Split(str, "/")[2]
    }

    func RegexNaive(str string) string {
    if strings.HasPrefix(str, "/greeting/") {
    return str[len("/greeting/"):]
    }
    return ""
    }


    1. gobwas
      17.12.2015 13:53
      +1

      Спасибо! Я тоже думал было переписать на split во всех примерах, но потом показалось, что с regexp будет более жизненно. При оказии попробую прогнать wrk со split.


  1. whitepen
    17.12.2015 14:00
    -7

    И на чем бы им свой очередной вирус написать? Кто бы написал сервис по вычищению ихних сервисов с компа.


    1. gobwas
      17.12.2015 14:03
      +38

      image


  1. ImLiar
    17.12.2015 14:03
    +4

    Интересно, что бы показал finagle от твиттера. Всё же там минимум абстракций над netty


    1. gobwas
      17.12.2015 14:09
      +3

      Если есть желание – можете напилить проектик на github, мы его соберем и прогоним на той же машине. Ну или можете сами собрать наши сервисы и прогнать их на своей =)


    1. vayho
      17.12.2015 17:11
      +3

      Вот такой код:

        val endpoints: Endpoint[String :+: String :+: CNil] =
          get(/) {Ok("Hello, World!")} :+: get("greeting" / string) { name: String => Ok("Hello, " + name + "!") }
      
        Await.ready(Http.serve(":8080", endpoints.toService))
      


      Дал такие результаты:
      Finch
      Server Software:
      Server Hostname: localhost
      Server Port: 8080

      Document Path: /
      Document Length: 13 bytes

      Concurrency Level: 256
      Time taken for tests: 1.154 seconds
      Complete requests: 50000
      Failed requests: 0
      Total transferred: 5800000 bytes
      HTML transferred: 650000 bytes
      Requests per second: 43328.87 [#/sec] (mean)
      Time per request: 5.908 [ms] (mean)
      Time per request: 0.023 [ms] (mean, across all concurrent requests)
      Transfer rate: 4908.35 [Kbytes/sec] received

      Connection Times (ms)
      min mean[±sd] median max
      Connect: 0 2 1.1 1 8
      Processing: 0 2 1.6 2 15
      Waiting: 0 2 1.4 1 15
      Total: 0 4 2.6 3 17

      Percentage of the requests served within a certain time (ms)
      50% 3
      66% 5
      75% 6
      80% 6
      90% 8
      95% 9
      98% 10
      99% 11
      100% 17 (longest request)


      1. gobwas
        17.12.2015 17:18

        Круто ) Спасибо!


      1. ImLiar
        18.12.2015 01:44

        Найс. Если мне не изменяет память, то Финч где-то на 5-15% добавляет оверхеда, то на то и выходит


        1. spiff
          18.12.2015 04:33

          Я бы сказал 5-7% по последним данным:

          — Очень много опитизаций было сделано Тревисом в Circe
          — В Finch как минимум две вещи помогли сократить разрыв: TooFastString и быстрые ридеры

          Но, на удивление, этот тест не использует ни того ни другого. В любом случае оч крутой результат!


  1. divan0
    17.12.2015 14:25
    +2

    Ещё интересно было бы посмотреть результаты с Go с использованием fasthttp — ускоренной альтернативы стандартному net/http.


    1. gobwas
      17.12.2015 14:47
      +1

      Accepted.


    1. Dimchansky
      17.12.2015 16:16
      +2

      Кстати, она теперь поддерживает keep-alive в HTTP 1.0, так что можно тестировать и с ab -k.


  1. fuCtor
    17.12.2015 15:25
    +1

    А если сделать замеры, экззотики для и фана ради, на связке Nginx+LUA?
    Еще есть вот такая экзотика: H2O+MRuby (неделю назад наткнулся вот на эту статью 25,000+ Req/s for Rack JSON API with MRuby

    PS еще конечно не пятница, но вдруг :)


    1. gobwas
      17.12.2015 16:53

      Теоретически – интересно ) Но практически, на lua приложухи сложновато будет писать )


      1. fuCtor
        17.12.2015 17:45

        Это уже ближе к вопросу о микросервисах и соответствующей архитектуре.

        Сделали же вот такое Kong.

        PS: если сложно на Lua, там и второй вариант рядом, еще большая экзотика )


  1. m52
    17.12.2015 15:44
    +5

    > и, так или иначе, субъективных взглядов мы выбрали Go

    Вы ведь его выбрали еще до тестов, к чему тогда был этот карнавал?


    1. RubaXa
      17.12.2015 15:59
      +4

      Это неправда, Node для нас был изначально ближе, а ещё gobwas возлагал большие надежды на Rust, так что всё по чесноку было сделано. Тест перепроверялись, велись обсуждения и по совокупности факторов, в том числе и субъективных, был выбран Go.


  1. vayho
    17.12.2015 16:01

    Вы на localhost тестировали? Почему такая высокая задержка? Или это VirtualBox тормозит?


    1. vayho
      17.12.2015 16:19

      Я имею ввиду Time per request в ab. У меня на локальных тестах с вашим кодом выдает цифры в районе ~10ms.


      1. gobwas
        17.12.2015 16:51

        На локальных тестах – где у кого ~10ms? ) Тесты и серверы запускались на одной виртуалке, да. Естественно, по очереди. Да, может, лучше было бы запустить где-то в облаке, но в среднем, результаты из прогона в прогон – сохраняются. Тем более, что нам важнее было определить не производительность в общем, а производительность относительную.


        1. vayho
          17.12.2015 17:05

          У go — 6.2ms в среднем, у Scala/Spray — 8.3ms в среднем. Scala после прогрева, потому что до прогрева тестировать смысла нет.


          1. gobwas
            17.12.2015 17:12

            А что по rps и completed requests?


            1. vayho
              17.12.2015 17:17
              +2

              Я выше выложил результаты Scala/Finch и Go. Можете глянуть.


  1. shoomyst
    17.12.2015 16:08
    +1

    Вы выбирали основной язык для mail.ru или для одного определенного сервиса?


    1. gobwas
      17.12.2015 16:52
      +1

      Выбирали язык для новых сервисов внутри команды почты =) В MailRu команд много )


  1. Dimchansky
    17.12.2015 16:12
    +1

    Утилита от Apache ab использует HTTP 1.0 и может передавать заголовок «Connection: Keep-Alive». Если сервер достаточно вменяемый, то он, увидев этот заголовок, будет держать keep-alive соединение. Делается это с помощью флага -k, например:

    ab -k -n50000 -c256 -t10 http://service.host/ 
    ab -k -n50000 -c256 -t10 http://service.host/greeting/hello 
    


    1. gobwas
      17.12.2015 16:52
      +1

      Спасибо! Keep-Alive в ab нам был не нужен – иначе мы бы получили тот же wrk =)


  1. vladon
    17.12.2015 16:53
    +5

    Ребят, вот честно, ну что вы в самом деле, вот берёте плюшевый плюсовый http-сервер из примеров буста: www.boost.org/doc/libs/1_59_0/doc/html/boost_asio/examples/cpp11_examples.html

    Меняете там request_handler.cpp под задачу из поста (которая гораздо проще, чем в примере).

    Код метода
    void request_handler::handle_request(const request& req, reply& rep)
    {
        using namespace std::literals::string_literals;
    
        static std::regex greeting_regex("^/greeting/([a-z]+)$");
    
        std::string request_path;
        if (!url_decode(req.uri, request_path))
        {
            rep = reply::stock_reply(reply::bad_request);
            return;
        }
    
        std::smatch greeting_match;
        if (request_path == "/")
        {
            rep.status = reply::ok;
            rep.content = "Hello, World!";
        }
        else if (std::regex_match(request_path, greeting_match, greeting_regex))
        {
            if (greeting_match.size() == 2)
            {
                rep.content = "Hello, "s + greeting_match[1].str() + "\r\n"s;
            }
            else
            {
                rep = reply::stock_reply(reply::not_found);
                return;
            }
        }
        else {
            rep = reply::stock_reply(reply::not_found);
            return;
        }
    
        rep.status = reply::ok;
        rep.headers.resize(1);
        rep.headers[0].name = "Content-Length";
        rep.headers[0].value = std::to_string(rep.content.size());
    }
    


    1. Dimchansky
      17.12.2015 17:02
      +8

      C++ надо еще выучить.

      И желательно не медленнее, чем за 21 день :)
      image


      1. vladon
        17.12.2015 17:06

        Да что-то не верится, что в Mail.ru нет C++-программистов


    1. gobwas
      17.12.2015 17:09
      +1

      wrk, 1-й тест:
      Average Latency, ms: 38.56
      Requests/sec: 9874.16


      Если сравнивать наши реализации серверов на go и c++, то у нас они следующие (c++ не представлен в статье):

      Go:
      Average Latency, ms: 104,83
      Requests/sec: 36191.37

      C++
      Average Latency, ms: 57.88
      Requests/sec: 16792.48

      Скорость ответа не всегда равно высокая производительность.

      Никто не спорит, что C++ может быстрее. Erlang, например, тоже может очень быстро.
      И данный обзор никак не пытается определить лучший в мире язык программирования. =)


      1. vladon
        17.12.2015 17:13

        А покажите код C++ и результаты ab для req/sec.

        wrk больше для измерения latency подходит.


        1. gobwas
          17.12.2015 17:15

          В ab его не тестировали. Код показать, к сожалению, нельзя.

          Почему wrk подходит больше для latency?


          1. vladon
            17.12.2015 17:52
            +3

            Особенности реализации (предназначен для тестирования nginx).

            Например, скорость работы wrk зависит от ресурсов системы (не сервера, а откуда запускаете).

            Если, например, запускать подряд, то он может даже выдать `Socket errors: connect: 1000` или что-то типа: `Socket errors: connect 0, read 1490, write 159932, timeout 0`, полная ерунда, т.е. все коннекты свалились в ошибку, хотя при этом на сервер даже не было соединений (я проверял по tcpdump). Соответственно, все такие «несостоявшиеся» соединения уменьшат значение «Requests/sec». А вот latency рассчитывается только по удачным соединениям.

            При этом прямо во время этих ошибок коннекта можно проверять банально браузером — всё будет работать.

            Вот здесь ещё много «грязных» подробностей: gwan.com/en_apachebench_httperf.html


            1. gobwas
              17.12.2015 17:54
              +1

              Понял, большое спасибо!


              1. vladon
                17.12.2015 17:59

                Или вот прямо сейчас пытаюсь разобраться:

                в одном окне терминала запустил сервер, во втором (на той же машине чтобы уж точно не повлияло ничего):

                $ ./wrk -d300s -c1000 -t50 --timeout 2m http://localhost/greeting/hello
                


                в третьем:

                $ netstat -an | grep tcp 
                tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     
                tcp        0      0 127.0.1.1:53            0.0.0.0:*               LISTEN     
                tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN     
                tcp        0      0 127.0.0.1:6942          0.0.0.0:*               LISTEN     
                tcp        0      0 127.0.0.1:63342         0.0.0.0:*               LISTEN     
                


                т.е. сервер слушает, всё ок, но соединений нет

                как результат:

                
                Running 5m test @ http://localhost/greeting/hello
                  50 threads and 1000 connections
                  Thread Stats   Avg      Stdev     Max   +/- Stdev
                    Latency    81.71ms  174.10ms   1.75s    90.76%
                    Req/Sec    62.24    136.83     1.74k    94.96%
                  7328 requests in 5.00m, 379.28KB read
                  Socket errors: connect 1000, read 0, write 0, timeout 0
                Requests/sec:     24.42
                Transfer/sec:      1.26KB
                
                

                Вот что он делал? Я не знаю. И никаких подробностей.

                Вывод: wrk — довольно странный инструмент. Или я совсем ничего не понимаю и пора уже переквалифицироваться в дворники. Кто понимает — помогите понять, плз.


                1. gobwas
                  17.12.2015 18:23

                  А со второй попытки – работает? )


                  1. vladon
                    17.12.2015 19:37

                    Не знаю, со второй или нет, но сейчас запустил — работает.
                    Всё слишком странно.
                    Почему ab работает, а wrk нет?


                  1. vladon
                    17.12.2015 19:38

                    Ещё попутно вот что нашёл: github.com/giltene/wrk2

                    Автор утверждает, что wrk считает latency неправильно.


          1. vladon
            17.12.2015 18:02
            +2

            > Код показать, к сожалению, нельзя.

            Странно, код на других языках показали, а этот нельзя :-)


    1. robert_ayrapetyan
      17.12.2015 23:01
      +1

      Причем еще бустовая реализация — самая медленная из всех C\CPP.


      1. vladon
        17.12.2015 23:03

        а какая быстрее?


        1. robert_ayrapetyan
          18.12.2015 00:27

          Когда-то давно — нативный модуль к nginx всех уделывал, надо бы перетестить.
          Всетроенный libevent-овский и mongoose были не плохи.


          1. vladon
            18.12.2015 09:22

            Однопоточные библиотеки (libevent/mongoose) в принципе не могут «уделать» Boost.Asio (многопоточность, под линухом использует epoll, под bsd — kqueue, под виндой — iocp).

            cpp-netlib не в счёт, это очень плохая реализация и не зря заброшена на текущий момент.


            1. robert_ayrapetyan
              18.12.2015 10:52

              libevent — то же самое (кастомные асинхр. механизмы, он изначально для этого и создан), с помощью fork-ов поднимается любое кол-во процессов на одном порте без оверхеда на переключение потоков.

              Pion тоже на boost.asio, результаты у него очень скромные были.


              1. vladon
                18.12.2015 10:54

                Не зря Pion тоже заброшен :-)


  1. Moxa
    17.12.2015 18:50

    go: 92838.98 rps
    java: 200127.00 rps

    ./wrk -c 512 -t 2 -d 60 http://localhost:8080/
    Running 1m test @ http://localhost:8080/
      2 threads and 512 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     5.22ms    3.58ms  77.45ms   77.41%
        Req/Sec    46.68k     5.97k   73.15k    69.77%
      5577230 requests in 1.00m, 686.13MB read
    Requests/sec:  92838.98
    Transfer/sec:     11.42MB
    
    ./wrk -c 512 -t 2 -d 60 http://localhost:8084/
    Running 1m test @ http://localhost:8084/
      2 threads and 512 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.56ms    2.73ms  49.36ms   87.17%
        Req/Sec   100.61k    11.36k  130.28k    74.87%
      12009644 requests in 1.00m, 1.29GB read
    Requests/sec: 200127.00
    Transfer/sec:     21.95MB
    


    1. vladon
      17.12.2015 20:55

      А можно на `htop` посмотреть во время теста явы? :-)


      1. Moxa
        17.12.2015 21:52

        только завтра, код на рабочем ноутбуке… не знаю, правда на что смотреть, все четыре ядра заняты на 100%


        1. vladon
          17.12.2015 21:55

          ой-ой, тогда можно не показывать htop :-)

          вот тот код на с++ использует 1% cpu

          вообще, я, конечно, хотел спросить про потребление памяти, но теперь уже не имеет смысла


          1. Moxa
            17.12.2015 22:06

            м… отличный бенчмарк, что он меряет, если фактически не напрягает программу?


            1. vladon
              17.12.2015 22:11

              измеряет производительность

              программе на c++, чтобы обработать 1000 параллельных запросов, нужно менее 1% cpu, и, на x86_64 — 3 МБ памяти.

              а программе на яве… ну увы.

              кстати, мерить на локалхосте неспортивно :-)


  1. Lol4t0
    17.12.2015 19:48
    +9

    Такие бенчмарки — полная туфта.

    Делать их весело, но как себя поведет приложение, когда в него добавится логика, когда начнется сборка мусора, проявятся прочие нюансы — эти тесты не показывают.


    1. gobwas
      18.12.2015 12:02

      Зато эти тесты показывают, как ведут себя голые серверы, без «прочих нюансов» которые зависят от рук программистов. И оверхед каждого из претендентов никуда не денется, если «в него добавится логика и начнется сборка мусора».


      1. Lol4t0
        18.12.2015 15:13

        Вот только оверхед может потеряться на фоне основной задачи. Потому что при пустом коде вы меряете не производительность языка, а производительность тест-драйвера, ядра, и http-библиотеки, которую специально оптимизировали в языках, заточенных на создание web-сервисов


        1. gobwas
          18.12.2015 15:43
          -1

          Тест-драйвер и ядро исключаем, ибо они равны. В итоге измеряем http-библиотеки в выбранных языках. Ой, разве не этого мы и хотели? =)


          1. Lol4t0
            18.12.2015 16:09

            Я думал, что мы хотим померить скорость нашего будущего приложения.


  1. shock_one
    17.12.2015 19:51

    Странно, что вы не добавили maintainability в список показателей.


  1. dbelka
    17.12.2015 19:55
    +6

    Написать сервер с «hello world» и тестировать производительность языка? Окай…
    Могу сказать про Rust, его производительность ± такая же, что и C++. Писал небольшой сервис для неточного сравнения текстов и сравнивал производительность.
    Очевидно, что Rust будет быстрее работать Go, поскольку в Rust нет сборщика мусора.


    1. vladon
      17.12.2015 20:53

      Тоже целиком за Раст, единственный минус его — то, что сегодня написанная программа через 2 недели может устареть: «Too old compiler version» :-)


      1. IncorrecTSW
        17.12.2015 21:35

        Вроде как это выражение уже не особо актуально.
        Еще смущает выбор hyper для теста.


        1. vladon
          17.12.2015 21:59

          меня смущает отсутствие в расте stackless coroutines


        1. gobwas
          17.12.2015 23:22

          А что бы выбрали вы?


      1. withkittens
        17.12.2015 21:56

        Так пишите не на nightly, а на stable — до выхода 2.х никаких проблем.


        1. vladon
          17.12.2015 21:58

          ну а минусовать зачем?

          только недавно осенью был 1.3, теперь уже 1.5

          а вообще у раста есть проблема, похожая на проблему рельсов: сегодня такой-то гем популярен, а завтра буквально он может выйти из моды


          1. withkittens
            17.12.2015 22:11

            Это просто выражение согласия/несогласия, не относитесь к этому серьёзно.

            Они каждые 6 недель будут выпускать новую версию.
            При этом гарантируют, что в пределах мажорной версии (1.хх) обратная совместимость не сломается.


            1. vladon
              17.12.2015 22:13

              говоря про гемы, я не имел в виду сам язык, а говорил о моде на те или иные библиотеки


  1. billyevans
    18.12.2015 05:57

    А какой смысле стрелять по серверу с того же хоста, более того в виртуалке. Сама программа бенчмарка будет неслабо афектить http-сервер. Да и куча всего остального может афектить процесс виртуалки на макбуке. Мне кажется лучше исключать внешние факторы по максимуму.
    hyper на сколько я смотрел в его код некоторое время назад, выглядел довольно тормозной либой. Выглядело тогда, что он использует штатный TcpStream с блокирующими сокетами. Тут скорее измерение скорости тормозной либы, а не языка. Но в го, же да есть горутины, которые по логике сильно лучше должны быть, чем треды с синхронными read/write.
    Вроде MIO должно быть лучше, но там нет http.
    + я совсем не знаю про дебагеры для го и раста. Как то пункт про дебаг у вас мимо.


    1. gobwas
      18.12.2015 12:01

      И go и rust можно дебажить с gdb. По поводу mio – да, его и пытаются прикрутить создатели hyper (есть ссылка на issue в статье). Меня еще расстраивает тот факт, что в Rust выпилили аналогичные «горутины» или «green threads» после, вроде 0.9 версии со смыслом, типа, если вам нужно – сами напилите планировщик для этих дел.

      Про тесты с локального хоста – где-то выше я уже говорил, что расклады от прогона к прогону остаются прежними, соответственно, все в равных условиях.

      Про «измерение скорости тормозный либы» – какую не тормозную http либу для Rust знаете вы? Или вы предлагает писать для такого бенчмарка свой http сервер? )


      1. billyevans
        18.12.2015 19:28

        Я не искал быстрого http-сервера на rust. Возможно его вообще нет. Из-за молодости и не очень большой популярности языка.
        Про выпиливание корутит из раста я тоже был не рад. Он стал более общим языком, но из-за этого проиграл в легкости написания высокопроизводительных многопоточных приложений.
        Ну http сервер на либе с корутинами на расте я думаю по силам написал в mail.ru ) Tarantool, например, сильно более сложный проект.
        Собственно, все что нужно это откопать зарытую либу корутин из версии 0.9, взять MIO, либу парсинга http протокола и слепить все вместе с хорошим API!


  1. beduin01
    18.12.2015 14:17
    -2

    Лучше бы Swift и D сравнили. Куда более простые языки, чем предложенная выборка.


    1. cy-ernado
      18.12.2015 18:21
      +1

      Community size по предложенной в статье методике у D будет 1 887. На hh разработчиков — 1.
      Судя по githut.info и stackoverflow, D уже менее популярен, чем Rust, а значит пакетов у него скорее всего меньше. И что-то мне подсказывает, что разрыв будет все больше. С производительностью там тоже не все однозначно.
      Swift только-только вышел в opensource.

      Зачем сравнивать заведомо проигрышные варианты? Особенно если есть уже положительный опыт с другими.


  1. jrip
    18.12.2015 14:45

    >, то первый претендент — это, конечно, Node.js

    Лол, месяц сравнения nodejs со всем подряд.
    Ладно я понимаю еще с PHP тут пытались сравнить, но cо Scala…


  1. knagaev
    18.12.2015 16:44

    Кто бы вот объяснил — какой скрытый смысл в КДПВ?


    1. jaiprakash
      18.12.2015 16:48

      Скрытого не нашёл, открытый — гонки.
      Ваш кэп


      1. knagaev
        18.12.2015 16:50

        Кэп, на инвалидных креслах?


        1. jaiprakash
          18.12.2015 16:55

          Вовсе нет, на самоходных тележках для супермаркетов.
          Немного несерьёзно, но не Formula 1 же было ставить.


  1. wolf13h
    18.12.2015 19:20
    +4

    Мы буквально на днях проводили похожие тесты для поиска основы для наших веб серверов. Только мы стреляли яндекс танком с одной машины в другую. Ну и среди участников также были haskell и clojure.
    Вот результаты по Responses Per Second

    source Yandex.Tank response per second(ubuntu vm 8 cores)
    target ubuntu vm 8 cores
    golang fast http 30k+
    nginx 20k
    golang http 20k-
    haskell wai warp 15k+
    clojure http-kit 15k-
    node.js 7k
    rust hyper 10k+
    rust iron 10k-
    fsharp suave.io 4k+ (best result ever for .net web servers)
    asp.net 5 kestrel coreclr/mono ??? 400-

    В чистом итоге видно что golang fast http абсолютный лидер. Хотя изначально мы возлагали большие надежды на nginx с lua(openresty). Забавно что мы также уперлись в регексп в голанг и решили его просто через слайс по FindIndex.


    1. wolf13h
      18.12.2015 19:36

      Также интересно что я так и не решил проблему с fsharp там можно было поднять скорость через libuv. Но видать баг в suave и скорость упала. Кестрел еще сыроват но ребята вроде его уже почти допилили по скорости до нетти. бенчмарк.

      По компиляции я не заметил разницы для голанг флагов go build -ldflags "-s -w". gccgo так и не удалось проверить.
      Rust версия была скомпилирована не как релиз версия.
      Haskell был скомпилирован с тредами и запущен +RTS -A4M -N8 -qg0 -qb -g1
      HttpKit был запущен как java -server -Xms3072m -Xmx3072m -cp `lein classpath` clojure.main -m main


      1. gobwas
        18.12.2015 19:40

        Я пробовал gccgo, у меня он с флагом fast оказался медленнее (на той же виртуалке, что и тесты) ~ на 20-25% =(


      1. dbelka
        19.12.2015 18:44
        +1

        Rust версия была скомпилирована не как релиз версия

        А какой смысл мерить код без оптимизаций?


        1. wolf13h
          19.12.2015 22:14

          Все было скомпилировано с оптимизациями за исключением Rust. Это обнаружилось уже после проведения тестов. Вторичный прогон уже был невозможен.


    1. gobwas
      18.12.2015 19:39
      +2

      Класс! И еще забавно, что мы тоже использовали rust iron, который по итогу просто исключили за ненадобностью =)
      divan0 тоже предложил fasthttp (и ребята из golang-russian slack) и наивный split – в понедельник обновлю пост, думаю, что fasthttp подтвердит ваш результат.


      1. wolf13h
        18.12.2015 22:44
        +1

         Только сейчас заметил что таблица не включает nim и h2o. nim стандартная либа http дала 6к и h2o был в районе 10к.


  1. mOlind
    19.12.2015 00:19
    +1

    Чтобы nginx не так сильно затормаживал обработку запросов его надо правильно настроить: gist.github.com/hgfischer/7965620

    Сам недавно сталкивался с вопросом оптимизации nginx перед сервисом на Go. Без оптимизации Go напрямую показывал 39k rps, через nginx пролазило только 13k rps. После настройки как в статье по ссылке — nginx увеличил скорость до 32k rps.


    1. wolf13h
      19.12.2015 11:59

      мы используем практически идентичные настройки и нет нгинкс у нас медленней чем го фаст хттп а когда возишся с модулями тот там дело вообще не очень становится.


      1. mOlind
        19.12.2015 12:03

        Ваши тесты показывают большую разницу между чистым запуском go fast http и go через nginx. Именно поэтому я и посоветовал посмотреть в сторону настроек nginx, чтобы сократить этот разрыв. Хотя общей картины, конечно, это не изменит.


        1. wolf13h
          19.12.2015 22:18

          Видать тут недопонимание, сравнение идет чистого go, go с библиотекой fast http и nginx. Под nginx понимается голый nginx с модулями который самодостаточен(OpenResty)


        1. wolf13h
          19.12.2015 22:21

          Прошу прощения, я думал это ответ на мой комментарий, а оказывается к статье.


    1. gobwas
      19.12.2015 13:02

      Спасибо! Натыкался на этот бенчмарк во время наших тестов. Безусловно, можно ускорить nginx, но в контексте статьи производительность nginx не столь критична, так как все серверы находились в равных условиях.