Nginx+Expressパフォーマンス計測

---
この記事は [リンク情報システム](https://www.lis.co.jp) の「[2020新春アドベントカレンダー TechConnect!](https://qiita.com/dgkz/items/5131349d56354b33ee97)」のリレー記事です。
TechConnect! は勝手に始めるアドベントカレンダーとして、engineer.hanzomon という勝手に作ったグループによってリレーされます。
（リンク情報システムの[Facebookはこちら](https://www.facebook.com/lis.co.jp/)から）

---

# はじめに

弊社で多く扱う業務系システムではミドルウェアは知名度と実績のある構成が好まれます。（多分）
WebサーバでいうとApacheの方が扱いが多くなりがちです。（多分）
Nginxを採用したいんですが、調べても説得に使えるような詳細な情報が意外と見つからない:sob:

「Nginxは速いらしいです！C10K問題にも対応可能らしいです！」
これだけで採用OKされると、さすがに別の面で心配になってしまいます。

「NginxはApacheと比較すると実測でスループットが約50%高いです！最大同時接続数もApacheの1k程度に対してNginxなら10Kいけます！」
せめてこれくらいの話はできないといかんですよね。

というわけで実際に測ってみました。

# やったこと

* NginxとApacheの性能を比較した。
* Nginxでのチューニングによる性能の変化を確認した。
* ついでにNginxをリバースプロキシとしてExpressに接続する構成における性能を調査した。

結果だけ知りたい人はまとめに飛んでください。

# 計測条件

## 環境

サーバ機とクライアント機をスイッチを介してギガビットイーサで接続。（実測で110MB/s）
通信帯域がボトルネックにならないように、性能計測は小さめのファイル（特に記載がなければ1KB）をダウンロードするようにしている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286717/7c794e2c-92f7-62eb-668a-32ecbc027b3e.png)


サーバ機にApacheとNginxをインストールし、どちらか片方が起動するようにして計測する。
Arch Linuxにpacmanでインストールを行い、設定ファイルは性能比較に必要な箇所のみ修正している。

Apacheはmpmをpreforkに変更し、起動プロセス数を固定したときの性能を測っている。
Apacheのevent mpmは別件で[この問題](https://serverfault.com/questions/516373/what-is-the-meaning-of-ah00485-scoreboard-is-full-not-at-maxrequestworkers)が起きたので採用していない。

クライアント機（Windowsマシン）はWSLでUbuntuを導入して負荷生成用の端末としている。
計測にはnghttp2に含まれるh2loadを利用した。
ab(Apache Bench)ではHTTP/1.1の通信ができないことと、途中で無通信の時間が発生するなど結果が何故か安定しないため採用をやめた。
同時接続数やサーバの設定を変更しつつパフォーマンスを計測している。

計測コマンドは以下の通りで、100万リクエストの六回計測。
あまりにばらつきが大きいときは再実行し、結果の平均をとっている。

```shell
 for i in 1 2 3 4 5 6; do h2load -n 1000000 --clients=100 --h1 --header="Accept-Encoding: gzip,deflate" -T 60s -N 60s -t 4 http://server/test1k.txt ; done;
```

Expressはサイズが1KBのJSONを非圧縮固定で返すだけの簡単なプログラムを作成し、利用している。


## ソフトウェアバージョン

|名称|バージョン|
|---|---|
|Apache HTTP Server|2.4.41|
|Nginx|1.16.1|

## 計測マシン

|名称|OS|CPU|メモリ　|
|---|---|---|---|
|サーバ機|Arch Linux|Core i5-4590S (4core)|16GB|
|クライアント機|WSL(Ubuntu) on Windows 10 Professional|Core i5-6500 (4core)|16GB|


# 測定結果

## Apache vs Nginx

同時接続数を変えたときのApacheとNginxの性能を比較している。
どちらも性能の向上が期待できるgzipの設定を有効化。
同時接続数を増やしていき、エラーが発生しだした時点で「エラー」の表記として計測を終了する。

### 結果

|対象|c100|c500|c1000|c5000|c10000|
|---|---|---|---|---|---|---|
|Nginx|64,050|63,556|61,421|54,867|44,350|
|Apache(500)|34,659|34,429|エラー|-|-|
|Apache(1000)|34,504|31,149|28,140|エラー|-|
|Apache(3000)|34,570|30,769|28,364|エラー|-|
|Apache(5000)|32,829|30,705|エラー|-|-|

※パフォーマンスは秒間に処理できるリクエスト数(requests per second)で表記する。
※同時接続数100であればc100と表記する。
※Apache(N)はpreforkでプロセス数を常時N個を立ち上げる設定を表す。

### 考察

Apacheのpreforkはプロセスを増やしすぎると性能が落ちる。
同時接続数がプロセス数を上回るとエラーが出始めるので設定のバランスを取るのが難しい。
評判通り、Nginxの方が同時接続に対する耐性があるし、処理性能も高い。
__性能が必要であればNginxを採用すべき。__

## gzip設定

圧縮の有無による性能を比較。

### 結果

|設定|同時接続|性能(rps)|CPU使用率|
|---|---|---|---|
|圧縮無効|100|44,829|10%|
|圧縮有効|100|61,616|70%|

### 考察

圧縮により性能は向上する。
静的ファイルを返すだけの場合はCPUが余剰リソースとなっているため、圧縮することで伝送効率が改善して性能が向上していると思われる。
ちなみに、上記のテストでは圧縮比率が1.25であった。
__CPUリソースに余裕があれば圧縮設定を入れるべき。__

## worker_connection設定

worker_connectionはworker_processとは無関係にNginx全体としての接続数上限の設定になっている。
worker_connectionの値を変更したときの性能を比較。

### 結果

|設定|同時接続|性能(rps)|
|---|---|---|
|worker_connection=64|c1000|エラー|
|worker_connection=256|c1000|エラー|
|worker_connection=512|c1000|エラー|
|worker_connection=1024|c1000|38,552|
|worker_connection=4096|c1000|38,557|
|worker_connection=8192|c1000|38,194|
|worker_connection=65536|c1000|37,791|

### 考察

別の性能測定でworker_connection=8192に対して同時接続10,000で負荷をかけてもエラーが起きなかったので必須というわけではないが、worker_connectionを同時接続数より同程度以上の数値にしないとエラーが起きる。
Apacheのpreforkと違い大きい値を設定しても性能劣化が少ない。
__worker_connectionはシステムで想定する最大同時接続数より大きく設定すべき。__

## worker_process設定

worker_processの値を変更したときの性能を比較。

### 結果

|設定|同時接続|性能(rps)|
|---|---|---|
|圧縮無効, worker_process=1|c1000|42,547|
|圧縮無効, worker_process=4|c1000|44,183|
|圧縮無効, worker_process=16|c1000|51,488|
|圧縮有効, worker_process=1|c1000|エラー|
|圧縮有効, worker_process=4|c1000|64,114|
|圧縮有効, worker_process=16|c1000|64,033|

### 考察

圧縮無効時はプロセス数をコア数よりも多くすると性能が良くなる傾向があるが、圧縮が有効であればそうでもない。
CPUリソースが余っているとそうなるのだろうか。
圧縮を有効化したときにプロセス数が少なすぎると圧縮処理がネックとなりレスポンスを返せなくなりエラーが起きてる。
__worker_processの設定はautoにするのが基本。ただし、大きめの値を設定することで性能が向上するケースがあるので要検討。__

## Express vs Express+PM2

素で立ち上げたExpressとPM2でクラスタ化したExpressの性能を比較。
クラスタのプロセス数は自動設定にしており、今回の測定環境ではコ4多重になっている。

※クラスタ化については https://www.yoheim.net/blog.php?q=20170706 を参照

### 結果

|対象|同時接続|性能(rps)|
|---|---|---|
|Express|c100|14,477|
|Express+PM2|c100|46,179|

### 考察

PM2経由でクラスタすることでExpressの性能向上が見込める。
他のクラスタ化手段もあるが、PM2はソースに手を入れなくても動作する点で扱いやすい。
__応答性能が必要な場合はExpressのクラスタ化は行うべき。__

## Nginx+Express応答性能

実験で気付いた問題点として、Nginxをリバースプロキシとして設定しただけでは、高負荷時にTIME_WAIT過多となりエラーが頻発する。
解消方法として、OSに対するtcp_tw_reuse=1の設定を行ってTIME_WAITを使い回すか、NginxとExpress間にKeepaliveを設定してTIME_WAITを減らすという二つの対策ができる。
また、そもそもネットワークを利用せず、Unix Domain Socket経由で通信を行うこともできる。
それぞれのケースでの速度を比較した。

※TIME_WAITに関しては https://www.slideshare.net/takanorisejima/timewait を参照
※Keepaliveに関しては http://blog.nomadscafe.jp/2012/02/nginx-11x-httpupstreamkeepalive.html を参照
※Unix Domain Socketの利用については https://yukidarake.hateblo.jp/entry/2015/07/29/203538 を参照

### 結果

|設定|同時接続|性能(rps)|
|---|---|---|
|追加設定なし|c100|1,748*|
|tcp_tw_reuse=1|c100|10,180|
|Keepalive|c100|30,373|
|Unix Domain Socket|c100|36,129|
*エラーあり

### 考察

ネットワーク的にはソケットの使い回しを行うtcp_tw_reuseより、そもそも接続自体を再利用するKeepaliveの方が性能的に有利。
しかしそれよりも、Unix Domain Socketの方が速度が早くなる。
__利用可能なときはUnix Domain Socketを採用し、利用しないときはKeepaliveを検討すべき。安全のため少なくともtcp_tw_reuseの設定は入れておく。__

## プロキシキャッシュ

プロキシの結果をNginxにキャッシュさせる。
Keepaliveの場合と5秒キャッシュを比較した。

### 結果

|設定|同時接続|性能(rps)|
|---|---|---|
|Keepalive|c100|30,373|
|キャッシュ|c100|46,979|

### 考察

当たり前ではあるが、転送の頻度が大幅に下がるため高負荷時は速度の向上が見込める。
__システム要件で問題が無ければプロキシの結果をキャッシュすべき。__

## その他速度の向上しそうな設定

Nginxのチューニング情報を調べて設定の効果を試した。
sendfileはデフォルトで有効だったので確認していない。
圧縮設定は有効にした状態での計測。

### 結果

|設定|同時接続|性能(rps)|
|---|---|---|
|設定なし|c1000|60,604|
|multi_accept|c1000|60,156|
|open_file_cache|c1000|59,132|
|tcp_nopush, tcp_nodelay|c1000|59,134|
|アクセスログ無効|c1000|60,307|

### 考察

今回の測定では各設定は効果がないように見える。
もちろん測定条件によって結果が変わってくるため、それらの設定が無意味であるとは言えない。
一応入れて置く方がいいのではないかと思う。

# まとめ

今回の調査で以下の情報が得られました。

* 性能を求めるならApacheではなくNginxを採用する。
* CPUリソースに余裕があればgzipの圧縮設定を行う。
* worker_connectionは想定される最大同時接続数より大きめの値とする。
* worker_processはautoが基本だが、大きい値を設定した方が性能が向上することがある。  
* Expressの性能向上にはクラスタ化が有効。
* Nginxをリバースプロキシとして動かすときは、tcp_tw_reuseを設定する。
* 上記に加えて、keepaliveあるいはUnix Domain Socketの利用を検討する。
* 上記に加えて、可能であればキャッシュの利用を検討する。
* Nginxの一般的なチューニング設定は、ベンチマークでは効果を確認できなかった。

環境によって異なる結果が出ることも当然ありますので、むやみに信じず十分確認の上で採用するようにしてください。