httperfでnginxにおける非同期処理の限界を知る
nginxに負荷をかけて遊んでみました。webによくあがっているnginxのベンチマークは適当な静的ファイルを1つ用意してそこに対してアクセスを行うといったものです。
ただそれだとメインメモリのファイルキャッシュにのっているものを返すだけで、オンメモリで完結する処理のはずなので非同期処理が売りのnginxのベンチマークとしては微妙じゃないかなと思うわけです。
そこでメインメモリのキャッシュではまかなえない量のデータを用意してかつ複数ファイルに対してアクセスを行うようにして負荷をかけてみました。負荷をかけるツールはhttperfを使用しました。
ネットワークの帯域がボトルネックになっていないことを確認するために、事前にiperfで帯域を調査しておきます。自宅のLANと使用するシステムはすべて1000BASE-Tで接続されているので理論値では1Gbits/sの速度がでるはずです。
------------------------------------------------------------ Client connecting to 192.168.3.3, TCP port 5001 TCP window size: 23.5 KByte (default) ------------------------------------------------------------ [ 3] local 192.168.3.8 port 42975 connected with 192.168.3.3 port 5001 [ ID] Interval Transfer Bandwidth [ 3] 0.0-10.0 sec 1.09 GBytes 936 Mbits/sec
まあ実効値はこんなもんです。ネットワーク帯域はフルに使用可能なことが分かったのでOSの設定をしてopen(2)できるファイルディスクリプタの上限値を引き上げます。
まずはユーザプログラムが開くことができるファイルの最大数を引き上げるためlimit.confを変更します。
#ftp hard nproc 0 #ftp - chroot /ftp #@student - maxlogins 4 * soft nofile 1048560 * hard nofile 1048560
再起動してulimit -nで確認します。
shimada-k@backend:~$ ulimit -n 1048560
これでOS側の設定値は変更しました。今度はhttperf側で開くことができるファイル数を変更します。httperfがコードから参照しているFD_SETSIZEの値を変えてビルドしなおさないといけません。
これは/usr/include以下のヘッダファイルを書き換えることで対応します。OS側に設定した値と同一のものを設定しました。
/usr/include/linux/posix_types.h
#define __FD_SETSIZE 1048560
/usr/include/x86_64-linux-gnu/bits/typesizes.h
#define __FD_SETSIZE 1048560
これと同様の手順をnginxを動かすサーバ上でも行います。httperfに対してはポートの再利用の設定をソケットに対して行うコードを追加してビルドします。
core.cの941行目付近です。
int on = 1; setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on)); SYSCALL (BIND, result = bind (sd, (struct sockaddr *)&myaddr, sizeof (myaddr))); if (result == 0) break;
これでhttperfのソースをDLしてビルドします。nginxもビルドします。ここまでで限界突破されたhttperfとnginxができますので、open(2)できるファイルディスクリプタの上限値がボトルネックになる可能性はなくなりました。
さらにhttperf側でTCPポートの再利用の設定とTCPのTIME_WAIT対策としてWAIT時間の設定をします。LAN内なので短めに設定してます。これをしないとhttperf側で使用できるポート番号の数がボトルネックとなり、bind(2)がエラーになります。 /proc/sys/net/ipv4/以下のファイルに数値を書き込むことで対応します。
echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse echo 10 > /proc/sys/net/ipv4/tcp_fin_timeout
加えてnginxの設定でworker_connectionsを引き上げておきます。デフォルトでは1024ですが4096にあげておきます。
events { worker_connections 4096; }
実験環境を書いておきます。
クライアント側
サーバ側
なお今回nginxのworker_processesは1を設定してあります。nginxを動かすマシンは4Coreマシンですが1プロセスで処理を受け付けます。
アクセス先のデータとしては10KByte分のテキストが書き込まれているファイルを3355440個用意しました。合計で32GBになります。ファイルパスは後述するhttpertの引数に列挙してあります。nginx側のサーバには8GBのメインメモリをつんでありますが、用意した32GByte分のデータをすべてキャッシングすることは不可能です。
httperfを実行する際に引数でリクエスト先のパスが書かれたファイルを指定します。ただ毎回同じ内容のファイルを指定するとメインメモリのキャッシュにのってしまうので、ランダムに並べ替えてhttperfで読み込むようにします。毎回ランダムアクセスが発生させることで、ディスクアクセスを一定数発生させることを目的としています。
#!/bin/bash #----------------------------------------------- # パスが書かれたファイルをランダムに並べ替えて # httperfで負荷をかけるプログラム #----------------------------------------------- HTTPERF='/home/shimada-k/httperf-0.9.0/src/httperf' SERVER='192.168.3.3' if [ ! ${#} -eq 1 ]; then echo 'usage: httperf_text [num request rate]' exit 0; fi RATE=${1} NUM_CONNS=`expr ${1} \* 50` # echo ${RATE} # echo ${NUM_CONNS} # ランダムに並べ替える cat list_text | sort -R > list_text_random echo 'sort file is ready' # httperfが読み込めるようにnull区切りのファイルにする cat list_text_random | tr "\n" "\0" > list_text_uri echo 'httperf uri file is ready' echo "${HTTPERF} --hog --server=${SERVER} --port=80 --uri=/ --wlog=Y,/home/shimada-k/list_text_uri --rate=${RATE} --num-conns=${NUM_CONNS}" ${HTTPERF} --hog --server=${SERVER} --port=80 --uri=/ --wlog=Y,/home/shimada-k/list_text_uri --rate=${RATE} --num-conns=${NUM_CONNS}
上記シェルスクリプトは第1引数をrateに設定して、rateに50を掛けたものをnum-connsに指定しています。50を掛けている理由は、httperfのReply rateは5秒間隔でスコアを蓄積している*1ため、ある程度長い時間負荷をかけないとReply rateの数値を均一な精度で取得できないからです。今回は上記スクリプトに500から2500まで100刻みで引数を与えてhttperfのスコアを記録しました。
rate | second(s) | req/s | Repy rate(min) | Reply rate(avg) | Reply rate(max) | Net I/O(*106) |
---|---|---|---|---|---|---|
500 | 50.002 | 500 | 499.8 | 500 | 500 | 42.3 |
600 | 50.003 | 600 | 599.8 | 600 | 600 | 50.7 |
700 | 50.001 | 700 | 699.8 | 700 | 700.1 | 59.2 |
800 | 50.001 | 800 | 799.9 | 800 | 800.1 | 67.6 |
900 | 49.997 | 900.1 | 899.9 | 900.1 | 900.1 | 76.1 |
1000 | 49.997 | 1000.1 | 999.9 | 1000.1 | 1000.1 | 84.5 |
1100 | 50.001 | 1100 | 1099.9 | 1100 | 1100.1 | 93 |
1200 | 49.997 | 1200.1 | 1198.9 | 1199.9 | 1200.3 | 101.4 |
1300 | 49.995 | 1300.1 | 1300.1 | 1300.1 | 1300.3 | 109.9 |
1400 | 50.002 | 1400 | 1399.7 | 1399.9 | 1400.1 | 118.3 |
1500 | 49.997 | 1500.1 | 1499.9 | 1500.1 | 1500.1 | 126.8 |
1600 | 49.992 | 1600.3 | 1599.7 | 1600.2 | 1600.7 | 135.3 |
1700 | 49.996 | 1700.1 | 1625.9 | 1700.1 | 1774.3 | 143.7 |
1800 | 49.997 | 1800.1 | 1790.3 | 1800.1 | 1809.9 | 152.2 |
1900 | 50.36 | 1886.4 | 1899.1 | 1899.6 | 1899.7 | 159.5 |
2000 | 49.997 | 2000.1 | 1999.8 | 2000.1 | 2000.2 | 169.1 |
2100 | 49.994 | 2100.3 | 2099.9 | 2100.2 | 2100.8 | 177.5 |
2200 | 50.18 | 2192.1 | 1904.3 | 2197.7 | 2477.6 | 185.1 |
2300 | 50.012 | 2299.5 | 1965.9 | 2292.9 | 2568.4 | 193.8 |
2400 | 50.882 | 2358.4 | 1924.1 | 2392.9 | 2791.8 | 199.3 |
2500 | 50.582 | 2471.3 | 2061.5 | 2499.4 | 2917.1 | 208.9 |
上記表が取得した生データです。request/sとReply (min)が一致していればnginx側においてキャパ内の処理ができていると考えていいと思います。すべてのrateにおいてNet I/Oがiperfで取得した帯域以下なので通信がボトルネックにはなっていません。さらにnginxのerrorログにも特にエラーは記載されていないことを確認しているのでファイルopen数の上限もボトルネックにはなっていません。
このままだと見にくいので、Reply rate(min)をrequest数で割った数値をグラフにしてみました。
1700付近から投げたリクエスト数に対してReply rateが安定しなくなっていて、rateが2200以降は一気にグラフが下がっています。実験した環境においては2200並列程度がnginxにおける非同期処理の性能限界なのかなと思います。
目的の場所に的確に負荷をかけるためにはボトルネックを1つずつ取り除く必要があり、ハマりポイントが多かったので苦労しました。