- PVSM.RU - https://www.pvsm.ru -

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

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

Именно таким вопросом задалась команда Почты Mail.Ru перед написанием очередного сервиса. Основная цель такого выбора — высокая эффективность процесса разработки в рамках выбранного языка/технологии. Что влияет на этот показатель?

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

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

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

Претенденты

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

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

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

При тестировании выяснилось, что все претенденты работают примерно с одинаковой производительностью в такой постановке — все упиралось в производительность 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 [7], на Scala — spray [8]. В 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 не совсем ожидаемо — раз [13], два [14].

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

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

Поэтому, чтобы тест был достоверным, было решено провести аналогичный тест, поставив перед сервисом 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 Foundn").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 [16] 104 3832 + [17] 315 [18]
Rust 89,23% 3 391 [19] 3 582 + [20]4 21 [21]
Scala 52,81% 44 844 [22] 172 5933 + 407 [23]
Node 5.1.1 41,03% 102 328 [24] 215 916 + 654 [25]
Node 0.12.7 32,18% 102 328 [24] 215 916 + 654 [25]

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

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

Go

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

Rust

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

Scala

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

Node.js

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

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

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

Выводы

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

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

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

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

Автор: Mail.Ru Group

Источник [31]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/106475

Ссылки в тексте:

[1] Node.js: http://nodejs.org

[2] V8: https://developers.google.com/v8

[3] Scala: http://www.scala-lang.org/

[4] Go: https://golang.org/

[5] Rust: https://www.rust-lang.org/

[6] fest: https://github.com/mailru/fest

[7] hyper: http://hyper.rs

[8] spray: http://spray.io

[9] wrk: https://github.com/wg/wrk

[10] ab: https://httpd.apache.org/docs/2.2/programs/ab.html

[11] JMeter: http://jmeter.apache.org/

[12] boom: https://github.com/rakyll/boom

[13] раз: https://github.com/hyperium/hyper/issues/368

[14] два: https://github.com/hyperium/hyper/issues/601

[15] чему: https://github.com/hyperium/hyper/issues/395

[16] 12 759: http://stackoverflow.com/search?q=go

[17] +: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

[18] 315: http://hh.ru/search/resume?text=go%2C+golang&logic=any&pos=full_text&exp_period=all_time&order_by=relevance&specialization=1.221&area=1&clusters=true

[19] 3 391: http://stackoverflow.com/search?q=rust

[20] +: https://www.rust-lang.org/ides.html

[21] 21: http://hh.ru/search/resume?text=rust&logic=any&pos=full_text&exp_period=all_time&order_by=relevance&specialization=1.221&area=1&clusters=true

[22] 44 844: http://stackoverflow.com/search?q=scala

[23] 407: http://hh.ru/search/resume?text=scala&logic=any&pos=full_text&exp_period=all_time&order_by=relevance&specialization=1.221&area=1&clusters=true

[24] 102 328: http://stackoverflow.com/search?q=node.js

[25] 654: http://hh.ru/search/resume?text=node.js&logic=any&pos=full_text&exp_period=all_time&order_by=relevance&specialization=1.221&area=1&clusters=true

[26] StackOverflow: http://stackoverflow.com/search?q=php

[27] godoc.org: https://godoc.org/

[28] github.com: https://github.com/

[29] hh.ru: http://hh.ru/

[30] количества вопросов по тегам за день: http://data.stackexchange.com/stackoverflow/query/409175/posts-with-tag-by-day#graph

[31] Источник: http://habrahabr.ru/post/273341/