Компания
521,61
рейтинг
17 декабря 2015 в 12:57

Разработка → Как выбрать язык программирования?



Именно таким вопросом задалась команда Почты 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 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 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 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 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 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 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.

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

Автор: @gobwas

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

  • +10
    Mail.ru как бы намекает, что скоро начнет хантить Go разрабов =)
  • +9
    Интересно, почему python не попал на тестирование? По остальным параметрам более чем подходит:
    community size: 509,465
    packages count: 71 290
    developers: 3 563

    p.s. наткнулся на интересный бенчмарк в тему, вот ссылка
    • 0
      Ага, спасибо, тоже изучали бенчмарк по ссылке. В наших тестах, к слову, так же была темная лошадка C++, которая оказалась самой очень быстрой. На python почему-то никто из команды не взялся разрабатывать первый вариант бенчмарка, поэтому его не стали рассматривать и потом. =)
      • +3
        Было бы круто увидеть бенчмарки темной лошадки
        • 0
          Внизу чуть-чуть в комментах приоткрыли завесу по wrk ))
  • +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 ""
    }

    Код бенчмарка
    package regex

    import «testing»

    var str = "/greeting/user"

    func TestRegexps(t *testing.T) {
    if RegexOrig(str) != «user» {
    t.Fatal(«RegexpOrig failed on», str, ":", RegexOrig(str))
    }
    if RegexPCRE(str) != «user» {
    t.Fatal(«RegexpPCRE failed on», str, ":", RegexPCRE(str))
    }
    if RegexStrings(str) != «user» {
    t.Fatal(«RegexpStrings failed on», str, ":", RegexStrings(str))
    }
    if RegexNaive(str) != «user» {
    t.Fatal(«RegexpNaive failed on», str, ":", RegexNaive(str))
    }
    }

    func BenchmarkOrig(b *testing.B) {
    for i := 0; i < b.N; i++ {
    RegexOrig(str)
    }
    }

    func BenchmarkPCRE(b *testing.B) {
    for i := 0; i < b.N; i++ {
    RegexPCRE(str)
    }
    }

    func BenchmarkStrings(b *testing.B) {
    for i := 0; i < b.N; i++ {
    RegexStrings(str)
    }
    }

    func BenchmarkNaive(b *testing.B) {
    for i := 0; i < b.N; i++ {
    RegexNaive(str)
    }
    }


    Результат примерно такой выходит:
    $ go test -v -bench . -benchmem .
    === RUN   TestRegexps
    --- PASS: TestRegexps (0.00s)
    PASS
    BenchmarkOrig-4   	 2000000	       845 ns/op	      64 B/op	       2 allocs/op
    BenchmarkPCRE-4   	 1000000	      1481 ns/op	     160 B/op	       3 allocs/op
    BenchmarkStrings-4	 5000000	       284 ns/op	      48 B/op	       1 allocs/op
    BenchmarkNaive-4  	100000000	        11.4 ns/op	       0 B/op	       0 allocs/op
    ok  	test/regex	6.879s
    
    • +1
      Спасибо! Я тоже думал было переписать на split во всех примерах, но потом показалось, что с regexp будет более жизненно. При оказии попробую прогнать wrk со split.
  • –7
    И на чем бы им свой очередной вирус написать? Кто бы написал сервис по вычищению ихних сервисов с компа.
    • +38
      image
  • +4
    Интересно, что бы показал finagle от твиттера. Всё же там минимум абстракций над netty
    • +3
      Если есть желание – можете напилить проектик на github, мы его соберем и прогоним на той же машине. Ну или можете сами собрать наши сервисы и прогнать их на своей =)
    • +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)

      Для сравнения:
      Go
      Server Software:
      Server Hostname: localhost
      Server Port: 8080

      Document Path: /
      Document Length: 12 bytes

      Concurrency Level: 256
      Time taken for tests: 1.241 seconds
      Complete requests: 50000
      Failed requests: 0
      Total transferred: 6450000 bytes
      HTML transferred: 600000 bytes
      Requests per second: 40297.59 [#/sec] (mean)
      Time per request: 6.353 [ms] (mean)
      Time per request: 0.025 [ms] (mean, across all concurrent requests)
      Transfer rate: 5076.55 [Kbytes/sec] received

      Connection Times (ms)
      min mean[±sd] median max
      Connect: 0 3 49.5 0 1000
      Processing: 1 3 4.5 3 204
      Waiting: 0 3 4.5 3 203
      Total: 1 6 51.2 3 1199

      Percentage of the requests served within a certain time (ms)
      50% 3
      66% 3
      75% 3
      80% 3
      90% 4
      95% 6
      98% 8
      99% 15
      100% 1199 (longest request)


      Но если брать несколько прогонов то там результаты и Go и Finch примерно одинаковы ~6ms.
      • 0
        Круто ) Спасибо!
      • 0
        Найс. Если мне не изменяет память, то Финч где-то на 5-15% добавляет оверхеда, то на то и выходит
        • 0
          Я бы сказал 5-7% по последним данным:

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

          Но, на удивление, этот тест не использует ни того ни другого. В любом случае оч крутой результат!
  • +2
    Ещё интересно было бы посмотреть результаты с Go с использованием fasthttp — ускоренной альтернативы стандартному net/http.
  • +1
    А если сделать замеры, экззотики для и фана ради, на связке Nginx+LUA?
    Еще есть вот такая экзотика: H2O+MRuby (неделю назад наткнулся вот на эту статью 25,000+ Req/s for Rack JSON API with MRuby

    PS еще конечно не пятница, но вдруг :)
    • 0
      Теоретически – интересно ) Но практически, на lua приложухи сложновато будет писать )
      • 0
        Это уже ближе к вопросу о микросервисах и соответствующей архитектуре.

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

        PS: если сложно на Lua, там и второй вариант рядом, еще большая экзотика )
  • +5
    > и, так или иначе, субъективных взглядов мы выбрали Go

    Вы ведь его выбрали еще до тестов, к чему тогда был этот карнавал?
    • +4
      Это неправда, Node для нас был изначально ближе, а ещё gobwas возлагал большие надежды на Rust, так что всё по чесноку было сделано. Тест перепроверялись, велись обсуждения и по совокупности факторов, в том числе и субъективных, был выбран Go.
  • 0
    Вы на localhost тестировали? Почему такая высокая задержка? Или это VirtualBox тормозит?
    • 0
      Я имею ввиду Time per request в ab. У меня на локальных тестах с вашим кодом выдает цифры в районе ~10ms.
      • 0
        На локальных тестах – где у кого ~10ms? ) Тесты и серверы запускались на одной виртуалке, да. Естественно, по очереди. Да, может, лучше было бы запустить где-то в облаке, но в среднем, результаты из прогона в прогон – сохраняются. Тем более, что нам важнее было определить не производительность в общем, а производительность относительную.
        • 0
          У go — 6.2ms в среднем, у Scala/Spray — 8.3ms в среднем. Scala после прогрева, потому что до прогрева тестировать смысла нет.
          • 0
            А что по rps и completed requests?
            • +2
              Я выше выложил результаты Scala/Finch и Go. Можете глянуть.
  • +1
    Вы выбирали основной язык для mail.ru или для одного определенного сервиса?
    • +1
      Выбирали язык для новых сервисов внутри команды почты =) В MailRu команд много )
  • +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
      Спасибо! Keep-Alive в ab нам был не нужен – иначе мы бы получили тот же wrk =)
  • +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());
    }
    



    И тестируете. Ну рвёт ведь всех просто как тузик грелку.

    Спойлер с измерениями
    комп
    (192.168.1.16 — моя виртуалка)

    wrk:

    $ wrk -d300s -c1000 -t50 --timeout 2s http://192.168.1.16/
    Running 5m test @ http://192.168.1.16/
      50 threads and 1000 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    38.56ms  139.88ms   2.00s    95.47%
        Req/Sec   210.75    156.88     4.49k    67.59%
      2963235 requests in 5.00m, 146.95MB read
      Socket errors: connect 0, read 1108, write 31, timeout 3153
    Requests/sec:   9874.16
    Transfer/sec:    501.42KB
    


    ab:

    $ ab -n50000 -c256 -t10 http://192.168.1.16/ 
    This is ApacheBench, Version 2.3 <$Revision: 1604373 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    
    Benchmarking 192.168.1.16 (be patient)
    Completed 5000 requests
    Completed 10000 requests
    Completed 15000 requests
    Completed 20000 requests
    Completed 25000 requests
    Completed 30000 requests
    Completed 35000 requests
    Completed 40000 requests
    Completed 45000 requests
    Completed 50000 requests
    Finished 50000 requests
    
    
    Server Software:        
    Server Hostname:        192.168.1.16
    Server Port:            80
    
    Document Path:          /
    Document Length:        13 bytes
    
    Concurrency Level:      256
    Time taken for tests:   2.915 seconds
    Complete requests:      50000
    Failed requests:        0
    Total transferred:      1600000 bytes
    HTML transferred:       650000 bytes
    Requests per second:    17155.15 [#/sec] (mean)
    Time per request:       14.923 [ms] (mean)
    Time per request:       0.058 [ms] (mean, across all concurrent requests)
    Transfer rate:          536.10 [Kbytes/sec] received
    
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        2    7  30.9      6    1005
    Processing:     2    8   2.0      7      25
    Waiting:        1    6   1.9      5      17
    Total:          9   15  31.0     14    1011
    
    Percentage of the requests served within a certain time (ms)
      50%     14
      66%     15
      75%     15
      80%     15
      90%     17
      95%     20
      98%     22
      99%     24
     100%   1011 (longest request)
    


    $ ab -n50000 -c256 -t10 http://192.168.1.16/greeting/hello 
    This is ApacheBench, Version 2.3 <$Revision: 1604373 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    
    Benchmarking 192.168.1.16 (be patient)
    Completed 5000 requests
    Completed 10000 requests
    Completed 15000 requests
    Completed 20000 requests
    Completed 25000 requests
    Completed 30000 requests
    Completed 35000 requests
    Completed 40000 requests
    Completed 45000 requests
    Completed 50000 requests
    Finished 50000 requests
    
    
    Server Software:        
    Server Hostname:        192.168.1.16
    Server Port:            80
    
    Document Path:          /greeting/hello
    Document Length:        12 bytes
    
    Concurrency Level:      256
    Time taken for tests:   2.844 seconds
    Complete requests:      50000
    Failed requests:        0
    Total transferred:      1550000 bytes
    HTML transferred:       600000 bytes
    Requests per second:    17580.17 [#/sec] (mean)
    Time per request:       14.562 [ms] (mean)
    Time per request:       0.057 [ms] (mean, across all concurrent requests)
    Transfer rate:          532.21 [Kbytes/sec] received
    
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        3    6   1.4      6      15
    Processing:     2    8   1.8      8      20
    Waiting:        1    6   1.9      6      15
    Total:          8   15   1.9     14      28
    
    Percentage of the requests served within a certain time (ms)
      50%     14
      66%     15
      75%     15
      80%     15
      90%     17
      95%     19
      98%     20
      99%     22
     100%     28 (longest request)
    



    Вынесу из-под спойлера:

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

    (лень заморачиваться с nginx, результат понятен).

    ab, 1-й тест:
    Completed requests: 50000
    Time per request, ms: 14.923
    Request, #/sec: 17155.15

    ab, 2-й тест:
    Completed requests: 50000
    Time per request, ms: 14.562
    Request, #/sec: 17580.17

    Причём не просто успевает 50000 запросов за 10 секунд, а даже укладывается менее чем в 3 секунды.

    О чём это говорит? Надо выбирать C++ (C++14).

    И это ещё я не говорю об использовании памяти и процессора (т.е., в конечном счёте, электроэнергии). Ведь мы хотим не только быстро обслуживать клиентов, но ещё и делать это дешевле на долгом промежутке времени.

    Причём даже тут есть куда пооптимизировать.
    • +8
      C++ надо еще выучить.

      И желательно не медленнее, чем за 21 день :)
      image
      • 0
        Да что-то не верится, что в Mail.ru нет C++-программистов
    • +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, например, тоже может очень быстро.
      И данный обзор никак не пытается определить лучший в мире язык программирования. =)
      • 0
        А покажите код C++ и результаты ab для req/sec.

        wrk больше для измерения latency подходит.
        • 0
          В ab его не тестировали. Код показать, к сожалению, нельзя.

          Почему wrk подходит больше для latency?
          • +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
              Понял, большое спасибо!
              • 0
                Или вот прямо сейчас пытаюсь разобраться:

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

                $ ./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 — довольно странный инструмент. Или я совсем ничего не понимаю и пора уже переквалифицироваться в дворники. Кто понимает — помогите понять, плз.
                • 0
                  А со второй попытки – работает? )
                  • 0
                    Не знаю, со второй или нет, но сейчас запустил — работает.
                    Всё слишком странно.
                    Почему ab работает, а wrk нет?
                  • 0
                    Ещё попутно вот что нашёл: github.com/giltene/wrk2

                    Автор утверждает, что wrk считает latency неправильно.
          • +2
            > Код показать, к сожалению, нельзя.

            Странно, код на других языках показали, а этот нельзя :-)
    • +1
      Причем еще бустовая реализация — самая медленная из всех C\CPP.
      • 0
        а какая быстрее?
        • 0
          Когда-то давно — нативный модуль к nginx всех уделывал, надо бы перетестить.
          Всетроенный libevent-овский и mongoose были не плохи.
          • 0
            Однопоточные библиотеки (libevent/mongoose) в принципе не могут «уделать» Boost.Asio (многопоточность, под линухом использует epoll, под bsd — kqueue, под виндой — iocp).

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

              Pion тоже на boost.asio, результаты у него очень скромные были.
              • 0
                Не зря Pion тоже заброшен :-)
  • 0
    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
    
    • 0
      А можно на `htop` посмотреть во время теста явы? :-)
      • 0
        только завтра, код на рабочем ноутбуке… не знаю, правда на что смотреть, все четыре ядра заняты на 100%
        • 0
          ой-ой, тогда можно не показывать htop :-)

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

          вообще, я, конечно, хотел спросить про потребление памяти, но теперь уже не имеет смысла
          • 0
            м… отличный бенчмарк, что он меряет, если фактически не напрягает программу?
            • 0
              измеряет производительность

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

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

              кстати, мерить на локалхосте неспортивно :-)
  • +9
    Такие бенчмарки — полная туфта.

    Делать их весело, но как себя поведет приложение, когда в него добавится логика, когда начнется сборка мусора, проявятся прочие нюансы — эти тесты не показывают.
    • 0
      Зато эти тесты показывают, как ведут себя голые серверы, без «прочих нюансов» которые зависят от рук программистов. И оверхед каждого из претендентов никуда не денется, если «в него добавится логика и начнется сборка мусора».
      • 0
        Вот только оверхед может потеряться на фоне основной задачи. Потому что при пустом коде вы меряете не производительность языка, а производительность тест-драйвера, ядра, и http-библиотеки, которую специально оптимизировали в языках, заточенных на создание web-сервисов
        • –1
          Тест-драйвер и ядро исключаем, ибо они равны. В итоге измеряем http-библиотеки в выбранных языках. Ой, разве не этого мы и хотели? =)
          • 0
            Я думал, что мы хотим померить скорость нашего будущего приложения.
  • 0
    Странно, что вы не добавили maintainability в список показателей.
  • +6
    Написать сервер с «hello world» и тестировать производительность языка? Окай…
    Могу сказать про Rust, его производительность ± такая же, что и C++. Писал небольшой сервис для неточного сравнения текстов и сравнивал производительность.
    Очевидно, что Rust будет быстрее работать Go, поскольку в Rust нет сборщика мусора.
    • 0
      Тоже целиком за Раст, единственный минус его — то, что сегодня написанная программа через 2 недели может устареть: «Too old compiler version» :-)
      • 0
        Вроде как это выражение уже не особо актуально.
        Еще смущает выбор hyper для теста.
        • 0
          меня смущает отсутствие в расте stackless coroutines
        • 0
          А что бы выбрали вы?
      • 0
        Так пишите не на nightly, а на stable — до выхода 2.х никаких проблем.
        • 0
          ну а минусовать зачем?

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

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

            Они каждые 6 недель будут выпускать новую версию.
            При этом гарантируют, что в пределах мажорной версии (1.хх) обратная совместимость не сломается.
            • 0
              говоря про гемы, я не имел в виду сам язык, а говорил о моде на те или иные библиотеки
  • 0
    А какой смысле стрелять по серверу с того же хоста, более того в виртуалке. Сама программа бенчмарка будет неслабо афектить http-сервер. Да и куча всего остального может афектить процесс виртуалки на макбуке. Мне кажется лучше исключать внешние факторы по максимуму.
    hyper на сколько я смотрел в его код некоторое время назад, выглядел довольно тормозной либой. Выглядело тогда, что он использует штатный TcpStream с блокирующими сокетами. Тут скорее измерение скорости тормозной либы, а не языка. Но в го, же да есть горутины, которые по логике сильно лучше должны быть, чем треды с синхронными read/write.
    Вроде MIO должно быть лучше, но там нет http.
    + я совсем не знаю про дебагеры для го и раста. Как то пункт про дебаг у вас мимо.
    • 0
      И go и rust можно дебажить с gdb. По поводу mio – да, его и пытаются прикрутить создатели hyper (есть ссылка на issue в статье). Меня еще расстраивает тот факт, что в Rust выпилили аналогичные «горутины» или «green threads» после, вроде 0.9 версии со смыслом, типа, если вам нужно – сами напилите планировщик для этих дел.

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

      Про «измерение скорости тормозный либы» – какую не тормозную http либу для Rust знаете вы? Или вы предлагает писать для такого бенчмарка свой http сервер? )
      • 0
        Я не искал быстрого http-сервера на rust. Возможно его вообще нет. Из-за молодости и не очень большой популярности языка.
        Про выпиливание корутит из раста я тоже был не рад. Он стал более общим языком, но из-за этого проиграл в легкости написания высокопроизводительных многопоточных приложений.
        Ну http сервер на либе с корутинами на расте я думаю по силам написал в mail.ru ) Tarantool, например, сильно более сложный проект.
        Собственно, все что нужно это откопать зарытую либу корутин из версии 0.9, взять MIO, либу парсинга http протокола и слепить все вместе с хорошим API!
  • –2
    Лучше бы Swift и D сравнили. Куда более простые языки, чем предложенная выборка.
    • +1
      Community size по предложенной в статье методике у D будет 1 887. На hh разработчиков — 1.
      Судя по githut.info и stackoverflow, D уже менее популярен, чем Rust, а значит пакетов у него скорее всего меньше. И что-то мне подсказывает, что разрыв будет все больше. С производительностью там тоже не все однозначно.
      Swift только-только вышел в opensource.

      Зачем сравнивать заведомо проигрышные варианты? Особенно если есть уже положительный опыт с другими.
  • 0
    >, то первый претендент — это, конечно, Node.js

    Лол, месяц сравнения nodejs со всем подряд.
    Ладно я понимаю еще с PHP тут пытались сравнить, но cо Scala…
  • 0
    Кто бы вот объяснил — какой скрытый смысл в КДПВ?
    • 0
      Скрытого не нашёл, открытый — гонки.
      Ваш кэп
      • 0
        Кэп, на инвалидных креслах?
        • 0
          Вовсе нет, на самоходных тележках для супермаркетов.
          Немного несерьёзно, но не Formula 1 же было ставить.
  • +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.
    • 0
      Также интересно что я так и не решил проблему с 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
      • 0
        Я пробовал gccgo, у меня он с флагом fast оказался медленнее (на той же виртуалке, что и тесты) ~ на 20-25% =(
      • +1
        Rust версия была скомпилирована не как релиз версия

        А какой смысл мерить код без оптимизаций?
        • 0
          Все было скомпилировано с оптимизациями за исключением Rust. Это обнаружилось уже после проведения тестов. Вторичный прогон уже был невозможен.
    • +2
      Класс! И еще забавно, что мы тоже использовали rust iron, который по итогу просто исключили за ненадобностью =)
      divan0 тоже предложил fasthttp (и ребята из golang-russian slack) и наивный split – в понедельник обновлю пост, думаю, что fasthttp подтвердит ваш результат.
      • +1
         Только сейчас заметил что таблица не включает nim и h2o. nim стандартная либа http дала 6к и h2o был в районе 10к.
  • +1
    Чтобы nginx не так сильно затормаживал обработку запросов его надо правильно настроить: gist.github.com/hgfischer/7965620

    Сам недавно сталкивался с вопросом оптимизации nginx перед сервисом на Go. Без оптимизации Go напрямую показывал 39k rps, через nginx пролазило только 13k rps. После настройки как в статье по ссылке — nginx увеличил скорость до 32k rps.
    • 0
      мы используем практически идентичные настройки и нет нгинкс у нас медленней чем го фаст хттп а когда возишся с модулями тот там дело вообще не очень становится.
      • 0
        Ваши тесты показывают большую разницу между чистым запуском go fast http и go через nginx. Именно поэтому я и посоветовал посмотреть в сторону настроек nginx, чтобы сократить этот разрыв. Хотя общей картины, конечно, это не изменит.
        • 0
          Видать тут недопонимание, сравнение идет чистого go, go с библиотекой fast http и nginx. Под nginx понимается голый nginx с модулями который самодостаточен(OpenResty)
        • 0
          Прошу прощения, я думал это ответ на мой комментарий, а оказывается к статье.
    • 0
      Спасибо! Натыкался на этот бенчмарк во время наших тестов. Безусловно, можно ускорить nginx, но в контексте статьи производительность nginx не столь критична, так как все серверы находились в равных условиях.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка