Как стать автором
Обновить
VK
Технологии, которые объединяют

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

Время на прочтение7 мин
Количество просмотров52K


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

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

Теги:
Хабы:
Всего голосов 57: ↑39 и ↓18+21
Комментарии98

Публикации

Информация

Сайт
team.vk.company
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия