Именно таким вопросом задалась команда Почты 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.
Исходный код
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");
}
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])
}
}))
}
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);
}
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)
Lite4
17.12.2015 13:40+9Интересно, почему python не попал на тестирование? По остальным параметрам более чем подходит:
community size: 509,465
packages count: 71 290
developers: 3 563
p.s. наткнулся на интересный бенчмарк в тему, вот ссылкаgobwas
17.12.2015 14:02Ага, спасибо, тоже изучали бенчмарк по ссылке. В наших тестах, к слову, так же была темная лошадка C++, которая оказалась
самойочень быстрой. На python почему-то никто из команды не взялся разрабатывать первый вариант бенчмарка, поэтому его не стали рассматривать и потом. =)
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 ""
}gobwas
17.12.2015 13:53+1Спасибо! Я тоже думал было переписать на split во всех примерах, но потом показалось, что с regexp будет более жизненно. При оказии попробую прогнать wrk со split.
ImLiar
17.12.2015 14:03+4Интересно, что бы показал finagle от твиттера. Всё же там минимум абстракций над netty
gobwas
17.12.2015 14:09+3Если есть желание – можете напилить проектик на github, мы его соберем и прогоним на той же машине. Ну или можете сами собрать наши сервисы и прогнать их на своей =)
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))
Дал такие результаты:
FinchServer 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)ImLiar
18.12.2015 01:44Найс. Если мне не изменяет память, то Финч где-то на 5-15% добавляет оверхеда, то на то и выходит
spiff
18.12.2015 04:33Я бы сказал 5-7% по последним данным:
— Очень много опитизаций было сделано Тревисом в Circe
— В Finch как минимум две вещи помогли сократить разрыв: TooFastString и быстрые ридеры
Но, на удивление, этот тест не использует ни того ни другого. В любом случае оч крутой результат!
divan0
17.12.2015 14:25+2Ещё интересно было бы посмотреть результаты с Go с использованием fasthttp — ускоренной альтернативы стандартному net/http.
Dimchansky
17.12.2015 16:16+2Кстати, она теперь поддерживает keep-alive в HTTP 1.0, так что можно тестировать и с ab -k.
fuCtor
17.12.2015 15:25+1А если сделать замеры, экззотики для и фана ради, на связке Nginx+LUA?
Еще есть вот такая экзотика: H2O+MRuby (неделю назад наткнулся вот на эту статью 25,000+ Req/s for Rack JSON API with MRuby
PS еще конечно не пятница, но вдруг :)gobwas
17.12.2015 16:53Теоретически – интересно ) Но практически, на lua приложухи сложновато будет писать )
m52
17.12.2015 15:44+5> и, так или иначе, субъективных взглядов мы выбрали Go
Вы ведь его выбрали еще до тестов, к чему тогда был этот карнавал?
vayho
17.12.2015 16:01Вы на localhost тестировали? Почему такая высокая задержка? Или это VirtualBox тормозит?
vayho
17.12.2015 16:19Я имею ввиду Time per request в ab. У меня на локальных тестах с вашим кодом выдает цифры в районе ~10ms.
gobwas
17.12.2015 16:51На локальных тестах – где у кого ~10ms? ) Тесты и серверы запускались на одной виртуалке, да. Естественно, по очереди. Да, может, лучше было бы запустить где-то в облаке, но в среднем, результаты из прогона в прогон – сохраняются. Тем более, что нам важнее было определить не производительность в общем, а производительность относительную.
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
gobwas
17.12.2015 16:52+1Спасибо! Keep-Alive в ab нам был не нужен – иначе мы бы получили тот же wrk =)
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()); }
Dimchansky
17.12.2015 17:02+8C++ надо еще выучить.
И желательно не медленнее, чем за 21 день :)gobwas
17.12.2015 17:09+1wrk, 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, например, тоже может очень быстро.
И данный обзор никак не пытается определить лучший в мире язык программирования. =)vladon
17.12.2015 17:13А покажите код C++ и результаты ab для req/sec.
wrk больше для измерения latency подходит.gobwas
17.12.2015 17:15В ab его не тестировали. Код показать, к сожалению, нельзя.
Почему wrk подходит больше для latency?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.htmlgobwas
17.12.2015 17:54+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 — довольно странный инструмент. Или я совсем ничего не понимаю и пора уже переквалифицироваться в дворники. Кто понимает — помогите понять, плз.gobwas
17.12.2015 18:23А со второй попытки – работает? )
vladon
17.12.2015 19:37Не знаю, со второй или нет, но сейчас запустил — работает.
Всё слишком странно.
Почему ab работает, а wrk нет?
vladon
17.12.2015 19:38Ещё попутно вот что нашёл: github.com/giltene/wrk2
Автор утверждает, что wrk считает latency неправильно.
vladon
17.12.2015 18:02+2> Код показать, к сожалению, нельзя.
Странно, код на других языках показали, а этот нельзя :-)
robert_ayrapetyan
17.12.2015 23:01+1Причем еще бустовая реализация — самая медленная из всех C\CPP.
vladon
17.12.2015 23:03а какая быстрее?
robert_ayrapetyan
18.12.2015 00:27Когда-то давно — нативный модуль к nginx всех уделывал, надо бы перетестить.
Всетроенный libevent-овский и mongoose были не плохи.vladon
18.12.2015 09:22Однопоточные библиотеки (libevent/mongoose) в принципе не могут «уделать» Boost.Asio (многопоточность, под линухом использует epoll, под bsd — kqueue, под виндой — iocp).
cpp-netlib не в счёт, это очень плохая реализация и не зря заброшена на текущий момент.robert_ayrapetyan
18.12.2015 10:52libevent — то же самое (кастомные асинхр. механизмы, он изначально для этого и создан), с помощью fork-ов поднимается любое кол-во процессов на одном порте без оверхеда на переключение потоков.
Pion тоже на boost.asio, результаты у него очень скромные были.
Moxa
17.12.2015 18:50go: 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
vladon
17.12.2015 20:55А можно на `htop` посмотреть во время теста явы? :-)
Moxa
17.12.2015 21:52только завтра, код на рабочем ноутбуке… не знаю, правда на что смотреть, все четыре ядра заняты на 100%
vladon
17.12.2015 21:55ой-ой, тогда можно не показывать htop :-)
вот тот код на с++ использует 1% cpu
вообще, я, конечно, хотел спросить про потребление памяти, но теперь уже не имеет смыслаMoxa
17.12.2015 22:06м… отличный бенчмарк, что он меряет, если фактически не напрягает программу?
vladon
17.12.2015 22:11измеряет производительность
программе на c++, чтобы обработать 1000 параллельных запросов, нужно менее 1% cpu, и, на x86_64 — 3 МБ памяти.
а программе на яве… ну увы.
кстати, мерить на локалхосте неспортивно :-)
Lol4t0
17.12.2015 19:48+9Такие бенчмарки — полная туфта.
Делать их весело, но как себя поведет приложение, когда в него добавится логика, когда начнется сборка мусора, проявятся прочие нюансы — эти тесты не показывают.gobwas
18.12.2015 12:02Зато эти тесты показывают, как ведут себя голые серверы, без «прочих нюансов» которые зависят от рук программистов. И оверхед каждого из претендентов никуда не денется, если «в него добавится логика и начнется сборка мусора».
Lol4t0
18.12.2015 15:13Вот только оверхед может потеряться на фоне основной задачи. Потому что при пустом коде вы меряете не производительность языка, а производительность тест-драйвера, ядра, и http-библиотеки, которую специально оптимизировали в языках, заточенных на создание web-сервисов
dbelka
17.12.2015 19:55+6Написать сервер с «hello world» и тестировать производительность языка? Окай…
Могу сказать про Rust, его производительность ± такая же, что и C++. Писал небольшой сервис для неточного сравнения текстов и сравнивал производительность.
Очевидно, что Rust будет быстрее работать Go, поскольку в Rust нет сборщика мусора.vladon
17.12.2015 20:53Тоже целиком за Раст, единственный минус его — то, что сегодня написанная программа через 2 недели может устареть: «Too old compiler version» :-)
IncorrecTSW
17.12.2015 21:35Вроде как это выражение уже не особо актуально.
Еще смущает выбор hyper для теста.
withkittens
17.12.2015 21:56Так пишите не на nightly, а на stable — до выхода 2.х никаких проблем.
vladon
17.12.2015 21:58ну а минусовать зачем?
только недавно осенью был 1.3, теперь уже 1.5
а вообще у раста есть проблема, похожая на проблему рельсов: сегодня такой-то гем популярен, а завтра буквально он может выйти из модыwithkittens
17.12.2015 22:11Это просто выражение согласия/несогласия, не относитесь к этому серьёзно.
Они каждые 6 недель будут выпускать новую версию.
При этом гарантируют, что в пределах мажорной версии (1.хх) обратная совместимость не сломается.vladon
17.12.2015 22:13говоря про гемы, я не имел в виду сам язык, а говорил о моде на те или иные библиотеки
billyevans
18.12.2015 05:57А какой смысле стрелять по серверу с того же хоста, более того в виртуалке. Сама программа бенчмарка будет неслабо афектить http-сервер. Да и куча всего остального может афектить процесс виртуалки на макбуке. Мне кажется лучше исключать внешние факторы по максимуму.
hyper на сколько я смотрел в его код некоторое время назад, выглядел довольно тормозной либой. Выглядело тогда, что он использует штатный TcpStream с блокирующими сокетами. Тут скорее измерение скорости тормозной либы, а не языка. Но в го, же да есть горутины, которые по логике сильно лучше должны быть, чем треды с синхронными read/write.
Вроде MIO должно быть лучше, но там нет http.
+ я совсем не знаю про дебагеры для го и раста. Как то пункт про дебаг у вас мимо.gobwas
18.12.2015 12:01И go и rust можно дебажить с gdb. По поводу mio – да, его и пытаются прикрутить создатели hyper (есть ссылка на issue в статье). Меня еще расстраивает тот факт, что в Rust выпилили аналогичные «горутины» или «green threads» после, вроде 0.9 версии со смыслом, типа, если вам нужно – сами напилите планировщик для этих дел.
Про тесты с локального хоста – где-то выше я уже говорил, что расклады от прогона к прогону остаются прежними, соответственно, все в равных условиях.
Про «измерение скорости тормозный либы» – какую не тормозную http либу для Rust знаете вы? Или вы предлагает писать для такого бенчмарка свой http сервер? )billyevans
18.12.2015 19:28Я не искал быстрого http-сервера на rust. Возможно его вообще нет. Из-за молодости и не очень большой популярности языка.
Про выпиливание корутит из раста я тоже был не рад. Он стал более общим языком, но из-за этого проиграл в легкости написания высокопроизводительных многопоточных приложений.
Ну http сервер на либе с корутинами на расте я думаю по силам написал в mail.ru ) Tarantool, например, сильно более сложный проект.
Собственно, все что нужно это откопать зарытую либу корутин из версии 0.9, взять MIO, либу парсинга http протокола и слепить все вместе с хорошим API!
beduin01
18.12.2015 14:17-2Лучше бы Swift и D сравнили. Куда более простые языки, чем предложенная выборка.
cy-ernado
18.12.2015 18:21+1Community size по предложенной в статье методике у D будет 1 887. На hh разработчиков — 1.
Судя по githut.info и stackoverflow, D уже менее популярен, чем Rust, а значит пакетов у него скорее всего меньше. И что-то мне подсказывает, что разрыв будет все больше. С производительностью там тоже не все однозначно.
Swift только-только вышел в opensource.
Зачем сравнивать заведомо проигрышные варианты? Особенно если есть уже положительный опыт с другими.
jrip
18.12.2015 14:45>, то первый претендент — это, конечно, Node.js
Лол, месяц сравнения nodejs со всем подряд.
Ладно я понимаю еще с PHP тут пытались сравнить, но cо Scala…
knagaev
18.12.2015 16:44Кто бы вот объяснил — какой скрытый смысл в КДПВ?
jaiprakash
18.12.2015 16:48Скрытого не нашёл, открытый — гонки.
Ваш кэпknagaev
18.12.2015 16:50Кэп, на инвалидных креслах?
jaiprakash
18.12.2015 16:55Вовсе нет, на самоходных тележках для супермаркетов.
Немного несерьёзно, но не Formula 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.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 maingobwas
18.12.2015 19:40Я пробовал gccgo, у меня он с флагом fast оказался медленнее (на той же виртуалке, что и тесты) ~ на 20-25% =(
gobwas
18.12.2015 19:39+2Класс! И еще забавно, что мы тоже использовали rust iron, который по итогу просто исключили за ненадобностью =)
divan0 тоже предложил fasthttp (и ребята из golang-russian slack) и наивный split – в понедельник обновлю пост, думаю, что fasthttp подтвердит ваш результат.wolf13h
18.12.2015 22:44+1Только сейчас заметил что таблица не включает nim и h2o. nim стандартная либа http дала 6к и h2o был в районе 10к.
mOlind
19.12.2015 00:19+1Чтобы nginx не так сильно затормаживал обработку запросов его надо правильно настроить: gist.github.com/hgfischer/7965620
Сам недавно сталкивался с вопросом оптимизации nginx перед сервисом на Go. Без оптимизации Go напрямую показывал 39k rps, через nginx пролазило только 13k rps. После настройки как в статье по ссылке — nginx увеличил скорость до 32k rps.wolf13h
19.12.2015 11:59мы используем практически идентичные настройки и нет нгинкс у нас медленней чем го фаст хттп а когда возишся с модулями тот там дело вообще не очень становится.
mOlind
19.12.2015 12:03Ваши тесты показывают большую разницу между чистым запуском go fast http и go через nginx. Именно поэтому я и посоветовал посмотреть в сторону настроек nginx, чтобы сократить этот разрыв. Хотя общей картины, конечно, это не изменит.
wolf13h
19.12.2015 22:18Видать тут недопонимание, сравнение идет чистого go, go с библиотекой fast http и nginx. Под nginx понимается голый nginx с модулями который самодостаточен(OpenResty)
wolf13h
19.12.2015 22:21Прошу прощения, я думал это ответ на мой комментарий, а оказывается к статье.
gobwas
19.12.2015 13:02Спасибо! Натыкался на этот бенчмарк во время наших тестов. Безусловно, можно ускорить nginx, но в контексте статьи производительность nginx не столь критична, так как все серверы находились в равных условиях.
EagleMoor
Mail.ru как бы намекает, что скоро начнет хантить Go разрабов =)