はじめに
- 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
main.js
constfastify=require('fastify');constserver=fastify({});server.post('/',(request,reply)=>{reply.send({answer:request.body.a+request.body.b});});server.listen(3000,(err,address)=>{if(err)throwerr;console.log(`server listening on ${address}`);});
Rust + actix_web
main.rs
useactix_web::{web,App,HttpServer,Responder,post,HttpResponse};useserde::{Deserialize,Serialize};#[derive(Serialize)]structAddResult{answer:i32,}#[derive(Deserialize)]structAddQuery{a:i32,b:i32,}#[post("/")]fnpost(query:web::Json<AddQuery>)->implResponder{HttpResponse::Ok().json(AddResult{answer:query.a+query.b})}fnmain(){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 + fastify | Rust + actix |
---|---|
23721 | 90643 |
Rustが速い。
node.js vs デフォルトでコア数だけスレッド立てるactixはフェアじゃないだろ
node側のコードをclusterを使って、マルチプロセス化する。
cluster.js
constcluster=require('cluster');constos=require('os');constfastify=require('fastify');if(cluster.isMaster){for(leti=0;i<os.cpus().length;i++){cluster.fork();}}else{constserver=fastify({});server.post('/',(request,reply)=>{reply.send({answer:request.body.a+request.body.b});});server.listen(3000,(err,address)=>{if(err)throwerr;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 + fastify | node + fastify + cluster | Rust + actix |
---|---|---|
23721 | 62275 | 90643 |
Rustが1.5倍ほど速いけど、思ったより差がなくなった。
他のhttp framework crateではどうなのか
nickelでやってみる。
main.rs
#[macro_use]externcratenickel;usenickel::{Nickel,HttpRouter,JsonBody};useserde::{Deserialize,Serialize};useserde_json;#[derive(Serialize)]structAddResult{answer:i32,}#[derive(Deserialize)]structAddQuery{a:i32,b:i32,}fnmain(){letmutserver=Nickel::new();server.post("/",middleware!{|request,response|letquery=request.json_as::<AddQuery>().unwrap();letresponse=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 + actix | Rust + nickel |
---|---|
62275 | 67503 |
nickelがやや速い。が、多同時接続が不安。
まとめ
- Rustはnode.jsの1.5倍速かった。
- もうちょっとRustは速いと思ってた。
- ここから処理を追加するから、差はついてくると思うが、REST APIのガワだけであれば大差なかった。
- マジか。
補足
- ローカル実行の雑なベンチマークなので参考程度でお願いします。