はじめに
-
- AWS Lambdaをnode.js(javascript/typescript)でよく使っている。
-
- コスト、またはレスポンス改善のためにLambdaをECS+fargateなどDocker環境に移植したい。
- もちろんRustに移植すれば速くなると思ってやっている。
Lambdaの問題
-
- リクエスト課金のため、大規模利用では課金がヤバいことになる。
-
- レスポンスタイムの揺らぎが大きい、コールドスタートが遅い。
- このどっちの問題にも当てはまらないならLambdaはオススメです。
(最近、Provisioned Concurrencyとか追加されたけど、それでもコールドスタートは発生する)
なぜRust?
-
- 速いから。速さがそのままインフラコスト改善になるから。
-
- C++を長くやってきたけど、最近Rustがいい気がしてきたから。
-
- でも、現状のLambdaではnode.jsのほうが速いらしい。(node.js、Go、Pythonは同じぐらい)
- https://medium.com/the-theam-journey/benchmarking-aws-lambda-runtimes-in-2019-part-i-b1ee459a293d
簡単なREST APIサーバーを書いてみる
数値を2つ含んだJSONをPOSTして、その和を返すREST APIを作る。
普段、fastifyを使っているので、node.jsはfastifyで比較する。
node.js(javascript) + fastify
const fastify = require('fastify');
const server = fastify({});
server.post('/', (request, reply) => {
reply.send({answer: request.body.a + request.body.b});
});
server.listen(3000, (err, address) => {
if (err) throw err;
console.log(`server listening on ${address}`);
});
Rust + actix_web
use actix_web::{web, App, HttpServer, Responder, post, HttpResponse};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct AddResult {
answer: i32,
}
#[derive(Deserialize)]
struct AddQuery{
a: i32,
b: i32,
}
#[post("/")]
fn post(query: web::Json<AddQuery>) -> impl Responder {
HttpResponse::Ok().json(AddResult{answer: query.a + query.b})
}
fn main() {
HttpServer::new(|| {
App::new().service(post)
})
.bind("127.0.0.1:3000")
.expect("Can not bind to port 3000")
.run()
.unwrap();
println!("server listening on 3000");
}
測定
ローカルマシン(MacBook Pro 4コア)で、heyを使って測定する。
Rustはcargo run –releaseで実行する。
% hey -n 1000000 -c 100 -m POST -d '{"a":1,"b":2}' -T 'application/json' http://localhost:3000
node+fastify heyの結果
Summary:
Total: 42.1554 secs
Slowest: 0.0426 secs
Fastest: 0.0001 secs
Average: 0.0042 secs
Requests/sec: 23721.7658
Total data: 12000000 bytes
Size/request: 12 bytes
Response time histogram:
0.000 [1] |
0.004 [723692] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.009 [271311] |■■■■■■■■■■■■■■■
0.013 [3823] |
0.017 [789] |
0.021 [286] |
0.026 [20] |
0.030 [3] |
0.034 [22] |
0.038 [28] |
0.043 [25] |
Latency distribution:
10% in 0.0036 secs
25% in 0.0036 secs
50% in 0.0039 secs
75% in 0.0044 secs
90% in 0.0054 secs
95% in 0.0058 secs
99% in 0.0076 secs
Rust+actix heyの結果
Summary:
Total: 11.0322 secs
Slowest: 0.1170 secs
Fastest: 0.0001 secs
Average: 0.0011 secs
Requests/sec: 90643.4012
Total data: 12000000 bytes
Size/request: 12 bytes
Response time histogram:
0.000 [1] |
0.012 [997956] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.023 [1203] |
0.035 [280] |
0.047 [182] |
0.059 [212] |
0.070 [89] |
0.082 [61] |
0.094 [14] |
0.105 [0] |
0.117 [2] |
Latency distribution:
10% in 0.0006 secs
25% in 0.0009 secs
50% in 0.0010 secs
75% in 0.0011 secs
90% in 0.0013 secs
95% in 0.0015 secs
99% in 0.0032 secs
結果
Requests/sec
node + fastifyRust + actix2372190643
Rustが速い。
node.js vs デフォルトでコア数だけスレッド立てるactixはフェアじゃないだろ
node側のコードをclusterを使って、マルチプロセス化する。
const cluster = require('cluster');
const os = require('os');
const fastify = require('fastify');
if(cluster.isMaster) {
for(let i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
}
else {
const server = fastify({});
server.post('/', (request, reply) => {
reply.send({answer: request.body.a + request.body.b});
});
server.listen(3000, (err, address) => {
if (err) throw err;
console.log(`server listening on ${address}`);
});
}
それに対するheyの結果
Summary:
Total: 16.0576 secs
Slowest: 0.1326 secs
Fastest: 0.0001 secs
Average: 0.0016 secs
Requests/sec: 62275.7432
Total data: 12000000 bytes
Size/request: 12 bytes
Response time histogram:
0.000 [1] |
0.013 [977295] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.027 [17411] |■
0.040 [4146] |
0.053 [812] |
0.066 [176] |
0.080 [67] |
0.093 [30] |
0.106 [32] |
0.119 [0] |
0.133 [30] |
Latency distribution:
10% in 0.0003 secs
25% in 0.0005 secs
50% in 0.0007 secs
75% in 0.0010 secs
90% in 0.0020 secs
95% in 0.0063 secs
99% in 0.0208 secs
Requests/sec
node + fastifynode + fastify + clusterRust + actix237216227590643
Rustが1.5倍ほど速いけど、思ったより差がなくなった。
他のhttp framework crateではどうなのか
nickelでやってみる。
#[macro_use] extern crate nickel;
use nickel::{Nickel, HttpRouter, JsonBody};
use serde::{Deserialize, Serialize};
use serde_json;
#[derive(Serialize)]
struct AddResult {
answer: i32,
}
#[derive(Deserialize)]
struct AddQuery {
a: i32,
b: i32,
}
fn main() {
let mut server = Nickel::new();
server.post("/", middleware! { |request, response|
let query = request.json_as::<AddQuery>().unwrap();
let response = AddResult{answer: query.a + query.b};
serde_json::to_string(&response).unwrap()
});
server.listen("127.0.0.1:3000").unwrap();
}
けど、heyの同じ負荷ではsocket: too many open filesが大量に出て耐えれなかった。
仕方なく、
% hey -n 100000 -c 10 -m POST -d '{"a":1,"b":2}' -T 'application/json' http://localhost:3000
同時接続数を減らして比較した
Rust + actixRust + nickel6227567503
nickelがやや速い。が、多同時接続が不安。
まとめ
-
- Rustはnode.jsの1.5倍速かった。
-
- もうちょっとRustは速いと思ってた。
-
- ここから処理を追加するから、差はついてくると思うが、REST APIのガワだけであれば大差なかった。
- マジか。
補足
- ローカル実行の雑なベンチマークなので参考程度でお願いします。