shimada-kの日記

ソフトウェア・エンジニアのブログです

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;
}

実験環境を書いておきます。

クライアント側

  • OS:Debian Wheezy (3.2.0-4-amd64)
  • Core(TM) i7-2600 CPU @ 3.40GHz
  • Memory 8GB
  • 自家ビルドhttperf(0.9.0ベース)

サーバ側

  • OS:Debian Wheezy (3.2.0-4-amd64)
  • Core(TM) i5-3470T CPU @ 2.90GHz
  • Memory 8GB
  • 自家ビルドnginx(1.7.2ベース)

なお今回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数で割った数値をグラフにしてみました。

f:id:shimada-k:20140708020007p:plain

1700付近から投げたリクエスト数に対してReply rateが安定しなくなっていて、rateが2200以降は一気にグラフが下がっています。実験した環境においては2200並列程度がnginxにおける非同期処理の性能限界なのかなと思います。

目的の場所に的確に負荷をかけるためにはボトルネックを1つずつ取り除く必要があり、ハマりポイントが多かったので苦労しました。