Quantumleap
2522 words
13 minutes
NestJSをNodeJSの代わりにBunで実行する

NestJS#

Bunって個人的には勝手にnpmやyarnの代替となるものだというイメージだったのですが、NodeJSの代わりにもなるそうです。

で、NodeJSフレームワークのNestJSをBunで動かしてみたという記事を見たのでそれを参考に本当に早くなるのか実験してみたいと思います。

記事の内容ではNodeJSだと4246回しか処理できなかったけれど、Bunだと開発ビルドで6661回処理、本番ビルドで16130回処理できて高速っていう結論でしたが、果たして本当にそんなに速いんでしょうか?

テスト環境#

  • macOS Sonoma 14.1.1
  • Apple M1 Ultra
  • yarn 4.1.0
  • bun 1.0.26
  • NodeJS 20.11.0

パソコンはそこそこいいものを利用しましたが、別の環境でもチェックしてみたいと思います。

コマンドは以下のものが利用されていたので、全く同じものを使ってみます。

# スレッド数12、コネクション数400で30秒間でいくつリクエストを処理できるか
wrk -t12 -c400 -d30s http://localhost:3000

プロジェクト自体は初期設定のものを利用します。またビルドは開発のものと本番のものを両方使います。

Express#

開発環境yarn start:devで実行した環境についてのテスト結果。

Running 30s test @ http://localhost:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    37.55ms  115.30ms   1.99s    98.12%
    Req/Sec     1.33k   139.03     1.87k    88.61%
  477476 requests in 30.04s, 108.83MB read
  Socket errors: connect 0, read 1310, write 5, timeout 78
Requests/sec:  15894.94
Transfer/sec:      3.62MB

開発環境yarn start:prodで実行した環境についてのテスト結果。

Running 30s test @ http://localhost:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    37.91ms  112.02ms   1.99s    98.33%
    Req/Sec     1.24k   133.03     2.07k    89.14%
  445840 requests in 30.05s, 101.62MB read
  Socket errors: connect 0, read 1174, write 5, timeout 95
Requests/sec:  14835.61
Transfer/sec:      3.38MB

Fastify#

FastifyはExpressより速いぞっていうことなので実験してみました。

導入方法についてはこちらをどうぞ。

yarn start:dev#

こちらはyarn start:devの実行結果です。

ただFastifyAdapterを使っただけなのに3倍以上性能が上がっています。タイムアウト数も0なので特別な理由がない限りはExpressからFastifyに乗り換えたほうが良いでしょう。導入自体も簡単ですし。

Running 30s test @ http://localhost:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    10.91ms   34.28ms 841.58ms   99.01%
    Req/Sec     4.10k   356.19     6.90k    90.22%
  1468671 requests in 30.02s, 247.91MB read
  Socket errors: connect 0, read 1147, write 0, timeout 0
Requests/sec:  48921.42
Transfer/sec:      8.26MB

yarn start:prod#

Running 30s test @ http://localhost:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    10.70ms   35.38ms 861.32ms   99.07%
    Req/Sec     4.21k   579.21    27.23k    98.50%
  1509004 requests in 30.10s, 254.72MB read
  Socket errors: connect 0, read 1050, write 1, timeout 0
Requests/sec:  50125.73
Transfer/sec:      8.46MB

こちらはほんの僅かですがリクエスト処理数は一部改善しました。

Fastifyの公式ドキュメントにはi7 4GHzのマシンで77,193回リクエストを処理できたと書いているのでそちらも実際に試してみました。

autocannon -c 100 -d 40 -p 10 localhost:3000
Running 40s test @ http://localhost:3000
100 connections with 10 pipelining factor

┌─────────┬──────┬──────┬───────┬───────┬──────────┬─────────┬────────┐
 Stat 2.5% 50% 97.5% 99% Avg Stdev Max
├─────────┼──────┼──────┼───────┼───────┼──────────┼─────────┼────────┤
 Latency 7 ms 8 ms 17 ms 18 ms 10.88 ms 4.71 ms 340 ms
└─────────┴──────┴──────┴───────┴───────┴──────────┴─────────┴────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬──────────┬─────────┐
 Stat 1% 2.5% 50% 97.5% Avg Stdev Min
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┤
 Req/Sec 80,063 80,063 88,575 91,007 87,856 2,853.89 80,057
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┤
 Bytes/Sec 14.2 MB 14.2 MB 15.7 MB 16.1 MB 15.6 MB 505 kB 14.2 MB
└───────────┴─────────┴─────────┴─────────┴─────────┴───────────┴──────────┴────────┘

すると結果は87,856回となり、パソコンのスペック差を考えると同じような感じになりました。

awkの場合と同じようにコネクション数400、パイプライン12、計測時間30で実行すると以下のような感じになりました。

$ autocannon -c 400 -d 30 -p 12 localhost:3000
Running 30s test @ http://localhost:3000
400 connections with 12 pipelining factor

┌─────────┬───────┬───────┬────────┬────────┬──────────┬───────────┬─────────┐
 Stat 2.5% 50% 97.5% 99% Avg Stdev Max
├─────────┼───────┼───────┼────────┼────────┼──────────┼───────────┼─────────┤
 Latency 29 ms 47 ms 166 ms 205 ms 67.29 ms 129.89 ms 6033 ms
└─────────┴───────┴───────┴────────┴────────┴──────────┴───────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬───────────┬──────────┬────────┐
 Stat 1% 2.5% 50% 97.5% Avg Stdev Min
├───────────┼─────────┼─────────┼─────────┼─────────┼───────────┼──────────┼────────┤
 Req/Sec 68,095 68,095 80,767 84,415 79,833.61 3,409.32 68,052
├───────────┼─────────┼─────────┼─────────┼─────────┼───────────┼──────────┼────────┤
 Bytes/Sec 12.1 MB 12.1 MB 14.3 MB 14.9 MB 14.1 MB 604 kB 12 MB
└───────────┴─────────┴─────────┴─────────┴─────────┴───────────┴──────────┴────────┘

負荷が増えた結果、目に見えて遅延(Latency)が大きくなっていることがわかります。

Bun#

超高速らしいので使ってみます。

あまり本旨とは関係ないのですがbun installが速すぎてビビりました。

これだけで使う価値はあるかもしれません。

Express#

まずは開発ビルドの結果がこちら。コマンドはbun start:devではなくbun startを利用しましょう。

bun start#

Running 30s test @ http://localhost:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    37.54ms  111.75ms   1.99s    98.28%
    Req/Sec     1.28k   143.51     2.38k    88.58%
  457783 requests in 30.04s, 104.34MB read
  Socket errors: connect 0, read 1229, write 0, timeout 83
Requests/sec:  15237.20
Transfer/sec:      3.47MB

NodeJSが15894回だったのでほぼ変わらず。

bun run dist/main.js#

bun run buildでビルドを実行してから立ち上げます。

Running 30s test @ http://localhost:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    11.04ms    2.43ms  79.36ms   98.03%
    Req/Sec     3.02k   219.66     4.19k    94.75%
  1082071 requests in 30.03s, 198.13MB read
  Socket errors: connect 0, read 405, write 0, timeout 0
Requests/sec:  36038.99
Transfer/sec:      6.60MB

急に倍くらい速くなりました。

bun run start:prodを実行するとbun run dist/main.jsではなくnode dist/mainが実行されて結局NodeJSで動いて遅くなるので注意してください。

Fastify#

となるとFastifyで実行したときの結果が気になるというものです。

bun start#

Running 30s test @ http://localhost:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     8.95ms   27.81ms 765.05ms   99.14%
    Req/Sec     4.85k   766.18    32.11k    96.70%
  1739951 requests in 30.10s, 293.70MB read
  Socket errors: connect 0, read 1128, write 1, timeout 0
Requests/sec:  57796.36
Transfer/sec:      9.76MB

先程までの劇的な変化はありませんが単純にNodeJS+Fastifyを利用したものよりも10%ほど高速化できています。

これで本番ビルドでやるともっと速くなるのでしょうか?

bun run dist/main.js#

Running 30s test @ http://localhost:3000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.82ms    1.48ms  43.20ms   97.11%
    Req/Sec     4.89k   340.22     6.42k    95.25%
  1753199 requests in 30.03s, 215.69MB read
  Socket errors: connect 0, read 394, write 0, timeout 0
Requests/sec:  58387.30
Transfer/sec:      7.18MB

更に速く!!とはならず、ほぼ横ばいとなりました。

このマシンのスペックだとTypeScriptでAPIを立てるとこのあたりが限界なのかもしれません。

最後にautocannonの実行結果を載せます。

Running 40s test @ http://localhost:3000
100 connections with 10 pipelining factor

┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬───────┐
 Stat 2.5% 50% 97.5% 99% Avg Stdev Max
├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼───────┤
 Latency 4 ms 9 ms 12 ms 13 ms 8.94 ms 2.98 ms 99 ms
└─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴───────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬───────────┬──────────┬─────────┐
 Stat 1% 2.5% 50% 97.5% Avg Stdev Min
├───────────┼─────────┼─────────┼─────────┼─────────┼───────────┼──────────┼─────────┤
 Req/Sec 93,759 93,759 106,687 110,399 106,009.6 3,690.75 93,734
├───────────┼─────────┼─────────┼─────────┼─────────┼───────────┼──────────┼─────────┤
 Bytes/Sec 12.1 MB 12.1 MB 13.8 MB 14.2 MB 13.7 MB 476 kB 12.1 MB
└───────────┴─────────┴─────────┴─────────┴─────────┴───────────┴──────────┴─────────┘

結果としては大台の一秒での十万リクエスト処理を超えることができました。

うーん、たしかにこれは速いかもしれない…

おまけ#

C/C++と並んで最速と名高いRustでAPIを立てて実行してみました。

brew install rust
git clone https://github.com/rwf2/Rocket
cd Rocket
git checkout v0.5
cd examples/hello
cargo build -r # リリースビルド
cargo run -r # リリースビルド実行

とりあえず環境からなかったのでRustをインストールするところから始めました。

Rustは初心者なので全く同じコードは書けなかったのでとりあえず適当に一番軽そうな単にHiとだけ返すAPIを立ててベンチマークを実行しました。

Running 30s test @ http://localhost:8000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.59ms  500.63us  39.39ms   99.33%
    Req/Sec     9.19k   429.89    10.33k    97.25%
  3291593 requests in 30.02s, 743.97MB read
  Socket errors: connect 0, read 226, write 31, timeout 0
Requests/sec: 109644.02
Transfer/sec:     24.78MB

ソケットエラーこそ発生しているものの、驚くべき速さを見せてくれました。

やはり事前にコンパイルしておける言語は処理速度では圧倒的だと言えますね。

Running 40s test @ http://localhost:8000
100 connections with 10 pipelining factor

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬────────┐
 Stat 2.5% 50% 97.5% 99% Avg Stdev Max
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼────────┤
 Latency 5 ms 6 ms 7 ms 9 ms 6.09 ms 1.36 ms 107 ms
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬────────────┬─────────┬─────────┐
 Stat 1% 2.5% 50% 97.5% Avg Stdev Min
├───────────┼─────────┼─────────┼─────────┼─────────┼────────────┼─────────┼─────────┤
 Req/Sec 138,879 138,879 155,903 157,951 154,771.21 4,017.2 138,810
├───────────┼─────────┼─────────┼─────────┼─────────┼────────────┼─────────┼─────────┤
 Bytes/Sec 32.9 MB 32.9 MB 36.9 MB 37.4 MB 36.7 MB 955 kB 32.9 MB
└───────────┴─────────┴─────────┴─────────┴─────────┴───────────┴──────────┴────────┘

負荷を軽くしたバージョンのautocannonでもこのような結果となりました。平均処理数は15万となり、圧倒的な数値です。

まとめ#

ざっくりと本番用のビルドでのスレッド数12、コネクション数400での一秒間の処理数を比較すると以下のようになります。

FrameworkExpressFastifyRocket
NodeJS1483550125-
Bun3603858387-
Rust--109644

こう見るとFastifyを使っているなら速度の面だけで言えばNodeJSからBunへ移行するメリットはそこまでないように思います。

ただ、実際にデプロイするとなったときにNodeJSであればdistrolessなどでビルドしようとするとマルチステージングビルドを意識してDockerfileを編集しなければいけないですが、Bunであれば何も考えずにoven/bunが使えるのがメリットですね。

移行コストにはよるのですが、ワンチャン切り替えても良さそうです。ビルドが楽ならそっちのほうが良いですし。

しかし、速度面ではRustがぶっちぎりなのでめちゃくちゃ速度が要求される場面では選択しても良さそうです。

今回は結構スペックがあるマシンでチェックしたのですが個人用のAPIサーバーはN100で動いているのでそちらでベンチマークを取ってみても面白いかもしれません。

記事は以上。

NestJSをNodeJSの代わりにBunで実行する
https://fuwari.vercel.app/posts/2024/02/nestjs_bun_node/
Author
tkgling
Published at
2024-02-15