8
2

More than 3 years have passed since last update.

Lambda+node.jsのREST APIをDocker+Rustに置き換えて高速化したい

Last updated at Posted at 2019-12-21

はじめに

  • AWS Lambdaをnode.js(javascript/typescript)でよく使っている。
  • コスト、またはレスポンス改善のためにLambdaをECS+fargateなどDocker環境に移植したい。
  • もちろんRustに移植すれば速くなると思ってやっている。

Lambdaの問題

  • リクエスト課金のため、大規模利用では課金がヤバいことになる。
  • レスポンスタイムの揺らぎが大きい、コールドスタートが遅い。
  • このどっちの問題にも当てはまらないならLambdaはオススメです。

(最近、Provisioned Concurrencyとか追加されたけど、それでもコールドスタートは発生する)

なぜRust?

簡単なREST APIサーバーを書いてみる

数値を2つ含んだJSONをPOSTして、その和を返すREST APIを作る。

普段、fastifyを使っているので、node.jsはfastifyで比較する。

node.js(javascript) + fastify

main.js
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

main.rs
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 + fastify Rust + actix
23721 90643

Rustが速い。

node.js vs デフォルトでコア数だけスレッド立てるactixはフェアじゃないだろ

node側のコードをclusterを使って、マルチプロセス化する。

cluster.js
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 + fastify node + fastify + cluster Rust + actix
23721 62275 90643

Rustが1.5倍ほど速いけど、思ったより差がなくなった。

他のhttp framework crateではどうなのか

nickelでやってみる。

main.rs
#[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 + actix Rust + nickel
62275 67503

nickelがやや速い。が、多同時接続が不安。

まとめ

  • Rustはnode.jsの1.5倍速かった。
  • もうちょっとRustは速いと思ってた。
  • ここから処理を追加するから、差はついてくると思うが、REST APIのガワだけであれば大差なかった。
  • マジか。

補足

  • ローカル実行の雑なベンチマークなので参考程度でお願いします。
8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2