📜 ⬆️ ⬇️

How to choose a programming language?



This is exactly the question that the Mail.Ru Mail team asked before writing the next service. The main purpose of this choice is the high efficiency of the development process within the framework of the chosen language / technology. What influences this indicator?

In addition, the developers welcomed the laconic and expressive language. Conciseness, of course, also affects the effectiveness of the development, as the lack of kilogram weights on the probability of success of a marathon runner.

Initial data


Challengers


Since many server microtasks are often born in the client side of the mail, the first applicant is, of course, Node.js with its native JavaScript and V8 from Google.

After discussion and on the basis of preferences within the team, the remaining participants of the competition were identified: Scala , Go and Rust .
')
As a performance test, it was proposed to write a simple HTTP server that receives HTML from the general template service and gives it to the client. Such a task is dictated by the current realities of the work of the mail - all template-based client-side occurs on V8 using the fest template engine.

When testing, it turned out that all applicants work with approximately the same performance in this formulation - it was all about V8 performance. However, the implementation of the task was not superfluous - the development in each of the languages ​​made it possible to make up a significant part of the subjective assessments, which in one way or another could affect the final choice.

So, we have two scenarios. The first is just the root URL greeting:
GET / HTTP/1.1 Host: service.host HTTP/1.1 200 OK Hello World! 

The second is a customer greeting by his name, passed in the URL path:
 GET /greeting/user HTTP/1.1 Host: service.host HTTP/1.1 200 OK Hello, user 

Environment


All tests were conducted on a VirtualBox virtual machine.

Host MacBook Pro:

VM:

Software:

In addition to standard modules, in examples on Rust hyper was used, on Scala - spray . In Go and Node.js, only native packages / modules were used.

Measurement tools


The performance of the services was tested using the following tools:

This article discusses the benchmarks wrk and ab.

results


Performance


wrk

The following is the data of the five-minute test, with 1000 connections and 50 threads:
wrk -d300s -c1000 -t50 --timeout 2s service.host

LabelAverage Latency, msRequest, # / sec
Go104.8336,191.37
Rusty0.0290632,564.13
Scala57.7417,182.40
Node 5.1.169,3714 005.12
Node 0.12.786.6811,125.37

wrk -d300s -c1000 -t50 --timeout 2s service.host/greeting/hello

LabelAverage Latency, msRequest, # / sec
Go105.6233,196.64
Rusty0.0320729 623.02
Scala55,817 531.83
Node 5.1.171.2913 620.48
Node 0.12.790.2910,681.11

So good looking, but, unfortunately, implausible figures in the results of Average Latency for Rust indicate one feature that is present in the hyper module. The thing is that the -c parameter in wrk indicates the number of connections that wrk will open on each thread and will not close, i.e. keep-alive connections. Hyper doesn’t work with keep-alive, one or two .

Moreover, if you display the distribution of requests on threads sent by wrk through a Lua script, we will see that only one thread sends all requests.

For those interested in Rust, it is also worth noting that these features led to this .

Therefore, in order for the test to be reliable, it was decided to conduct a similar test by placing in front of the nginx service, which will keep connections with wrk and proxy them to the required service:
 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

LabelAverage Latency, msRequest, # / sec
Rusty155.369,196.32
Go145.247,333.06
Scala233.692 513.95
Node 5.1.1207.822 422.44
Node 0.12.7209.52,410.54

wrk -d300s -c1000 -t50 --timeout 2s nginx.host/service/greeting/hello

LabelAverage Latency, msRequest, # / sec
Rusty154.959 039,73
Go147.877,427.47
Node 5.1.1199.172,470.53
Node 0.12.7177.342,363.39
Scala262.192 218.22

As can be seen from the results, overhead with nginx is significant, but in our case we are interested in the performance of services that are on equal footing, regardless of the delay in nginx.

ab

The Apache ab utility, unlike wrk, does not keep keep-alive connections, so nginx is not useful to us here. Let's try to fulfill 50,000 requests in 10 seconds, with 256 possible parallel requests.
ab -n50000 -c256 -t10 service.host

LabelCompleted requests, #Time per request, msRequest, # / sec
Go50 000.0022.0411,616.03
Rusty32,730.0078.223 272.98
Node 5.1.130,069.0085.143,006.82
Node 0.12.727 103.0094.462 710.22
Scala16 691.00153.741 665,17

ab -n50000 -c256 -t10 service.host/greeting/hello

LabelCompleted requests, #Time per request, msRequest, # / sec
Go50 000.0021.8811,697.82
Rusty49 878.0051.424,978.66
Node 5.1.130 333.0084.403 033.29
Node 0.12.727 610.0092.722 760,99
Scala27 178.0094.342 713.59

It should be noted that Scala-application is characterized by some “warm-up” due to possible JVM optimizations that occur during the operation of the application.

As you can see, without nginx, hyper in Rust still does not do well even without keep-alive connections. And the only one who managed to process 50,000 requests in 10 seconds was Go.

Source


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\/([az]+)$", "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/([az]+)$") 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]) } })) } 

Rusty
 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/([az]+)$").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!") } } } } 


Generalization


We present the success criteria defined in the beginning of the article in the form of a table. All applicants have debugging and profiling facilities, so there are no corresponding columns in the table.

LabelPerformance Rate 0Community size 1Packages countIDE SupportDevelopers 5
Go100.00%12,759104 383 2+315
Rusty89.23%3 3913,582+ 421
Scala52.81%44,844172 593 3+407
Node 5.1.141.03%102 328215,916+654
Node 0.12.732.18%102 328215,916+654

0 Performance was calculated based on the five-minute wrk tests without nginx, using the RPS parameter.
1 The size of the community was estimated on an indirect basis - the number of questions with the corresponding tag on StackOverflow .
2 The number of packages indexed by godoc.org .
3 Very roughly - search by languages ​​Java, Scala on github.com .
4 Under many favorite Idea plug-in is still not.
5 According to hh.ru.

Obviously, such graphs of the number of questions per tag per day can speak about the size of the community:

Go



Rusty



Scala



Node.js



For comparison, PHP:



findings


Understanding that benchmarks of performance is a rather shaky and ungrateful thing, it is difficult to draw any definite conclusions based only on such tests. Of course, everything is dictated by the type of task that needs to be addressed, the requirements for the program indicators and other environmental nuances.

In our case, on the basis of the criteria defined above and, in one way or another, subjective views, we chose Go.

The content of subjective assessments was deliberately omitted in this article in order not to make another round and not to provoke holivar. Moreover, if such estimates were not taken into account, then according to the criteria mentioned above, the result would remain the same.

Source: https://habr.com/ru/post/273341/


All Articles