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

## はじめに

- 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サーバーを書いてみる

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

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

node.js(javascript) + fastify

```javascript: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

```rust: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`で実行する。

```shell
% 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を使って、マルチプロセス化する。


```javascript: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でやってみる。

```rust: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`が大量に出て耐えれなかった。
仕方なく、

```shell
% 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のガワだけであれば大差なかった。
- マジか。

## 補足
- ローカル実行の雑なベンチマークなので参考程度でお願いします。