Skip to content

Instantly share code, notes, and snippets.

@DmitryDorofeev
Last active December 14, 2015 10:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DmitryDorofeev/3452bfd4e5d652db75e1 to your computer and use it in GitHub Desktop.
Save DmitryDorofeev/3452bfd4e5d652db75e1 to your computer and use it in GitHub Desktop.
Article

Как выбрать язык разработки?

Именно таким вопросом задалась команда почты mail.ru перед написанием очередного сервиса.

Почему?

Не так давно, в почтовой команде mail.ru зародилась идея внедрения микросервисной архитектуры. Плюсы и минусы такого подхода, ровно, как сложности и подводные камни минуют данную публикацию, ибо цель ее – повествование истории о терзаниях ответа на главный вопрос. 42.

Как вы могли догадаться, помимо плюсов и минусов микросервисной архитектуры, среди целей так же – высокая эффективность процесса разработки в рамках выбранного языка/технологии. Что влияет на этот показатель?

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

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

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

Претенденты

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

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

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

Однако, в процессе тестирования выяснилось, что все претенденты работают примерно с одинаковой производительностью в такой поставновке – все упиралось в производительность V8. =)

Поэтому тест свелся практически к «Hello World!».

И так, мы имеем два сценария. Первый, это просто приветствие по корневому 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

Окружение

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

Хост, MacBook Pro:

  • 2,6 GHz Intel Core i5 (dual core);
  • CPU Cache L1:32KB, L2:256KB, L3:3MB;
  • 8Gb 1600MHz DDR3.

VM:

  • 4GB 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 – srpay. В 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 36191.37
rust 0.02906 32564.13
scala 57.74 17182.40
node 5.1.1 69.37 14005.12
node 0.12.7 86.68 11125.37
wrk -d300s -c1000 -t50 --timeout 2s http://service.host/greeting/hello
Label Average Latency, ms Request, #/sec
go 105.62 33196.64
rust 0.03207 29623.02
scala 55.8 17531.83
node 5.1.1 71.29 13620.48
node 0.12.7 90.29 10681.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 9196.32
go 145.24 7333.06
scala 233.69 2513.95
node 5.1.1 207.82 2422.44
node 0.12.7 209.5 2410.54
wrk -d300s -c1000 -t50 --timeout 2s http://nginx.host/service/greeting/hello
Label Average Latency, ms Request, #/sec
rust 154.95 9039.73
go 147.87 7427.47
node 5.1.1 199.17 2470.53
node 0.12.7 177.34 2363.39
scala 262.19 2218.22

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

ab

Утилита от Apache ab, в отличие от wrk не держит keep-alive соединений, поэтому nginx нам тут не пригодится. Попробуем выполнить 50000 запросов за 10 секунд, с 256 возможными параллельными запросами.

ab -n50000 -c256 -t10 http://service.host/
Label Completed requests, # Time per request, ms Request, #/sec
go 50000.00 22.04 11616.03
rust 32730.00 78.22 3272.98
node 5.1.1 30069.00 85.14 3006.82
node 0.12.7 27103.00 94.46 2710.22
scala 16691.00 153.74 1665.17
ab -n50000 -c256 -t10 http://service.host/greeting/hello
Label Completed requests, # Time per request, ms Request, #/sec
go 50000.00 21.88 11697.82
rust 49878.00 51.42 4978.66
node 5.1.1 30333.00 84.40 3033.29
node 0.12.7 27610.00 92.72 2760.99
scala 27178.00 94.34 2713.59

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

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

Выводы

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

В нашем случае, по совокупности оценок эффективности процесса разработки, показателей производительности, и, так или иначе, субъективных взглядов, мы выбрали 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!")
        }
      }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment