高負荷、大量アクセスなサイトで Lighttpd を使う場合の注意点

すいませんm(_ _)m 昨日、17:45 〜 20:30 まで Amazon Search 落ちてました・・・

当サイトの web サーバは Lighttpd ってのを使っています。使用しているバージョンは試行錯誤の結果 ver 1.4.7 を採用。ver 1.4.8 は何故か Amazon Search の FastCGI が落ちまくるので使い物にならず。最近、ver 1.4.9 がでたようなので、安定性を検証予定。

さて、「lighttpd + FastCGI は mod_perl + Apache1.3 より1割ほど高速」とかで書いているとおり、Lighttpd は Apache と比較して確かに高速。しかも負荷は約 1/3 程度になるスバラシイ側面があるのですが、安定性という面では Apache の方がかなり上かと感じ始めています。

- スポンサーリンク -

今回、Amazon Search が落ちていた原因は、Lighttpd プロセスが FastCGI プロセスと socket 通信不能になるバグ?が原因です。実は、ちょくちょくこの現象が発生しています。具体的には、

2006-02-01 20:29:03: (server.c.1033) [note] sockets disabled, out-of-fds
2006-02-01 20:29:33: (server.c.998) [note] sockets enabled again
2006-02-01 20:29:33: (server.c.1033) [note] sockets disabled, out-of-fds
2006-02-01 20:30:22: (server.c.946) connection closed - write-request-timeout: 717

みたいなエラーが多発します。この現象が発生すると、

Lighttpd プロセスを終了しても、FastCGI プロセスが同時に終了してくれない。

て、気がついたたびに Lighttpd プロセスを終了し、FastCGI プロセスを手動で全て kill して、/tmp
配下のロックファイルを全て削除して Lighttpd を起動。なんて手順で対応していました。

なんか、この周辺のソースはバージョンが変わるたびに変化があるので、ver 1.4.7 以外では以下と違うソースですが、

server.c / 934 - 952 line

if ((con->state == CON_STATE_WRITE) &&
(con->write_request_ts != 0)) {
#if 0
if (srv->cur_ts - con->write_request_ts > 60) {
log_error_write(srv, __FILE__, __LINE__, "sdd",
"connection closed - pre-write-request-timeout:", con->fd, srv->cur_ts - con->write_request_ts);
}
#endif

if (srv->cur_ts - con->write_request_ts > con->conf.max_write_idle) {
/* time - out */
#if 1
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed - write-request-timeout:", con->fd);
#endif
connection_set_state(srv, con, CON_STATE_ERROR);
changed = 1;
}
}

server.c / 987 - 1038 line

if (srv->sockets_disabled) {
/* our server sockets are disabled, why ? */

if ((srv->cur_fds + srv->want_fds < srv->max_fds * 0.8) && /* we have enough unused fds */
(srv->conns->used < srv->max_conns * 0.9) &&
(0 == graceful_shutdown)) {
for (i = 0; i < srv->srv_sockets.used; i++) {
server_socket *srv_socket = srv->srv_sockets.ptr[i];
fdevent_event_add(srv->ev, &(srv_socket->fde_ndx), srv_socket->fd, FDEVENT_IN);
}

log_error_write(srv, __FILE__, __LINE__, "s", "[note] sockets enabled again");

srv->sockets_disabled = 0;
}
} else {
if ((srv->cur_fds + srv->want_fds > srv->max_fds * 0.9) || /* out of fds */
(srv->conns->used > srv->max_conns) || /* out of connections */
(graceful_shutdown)) { /* graceful_shutdown */

/* disable server-fds */

for (i = 0; i < srv->srv_sockets.used; i++) {
server_socket *srv_socket = srv->srv_sockets.ptr[i];
fdevent_event_del(srv->ev, &(srv_socket->fde_ndx), srv_socket->fd);

if (graceful_shutdown) {
/* we don't want this socket anymore,
*
* closing it right away will make it possible for
* the next lighttpd to take over (graceful restart)
* */

fdevent_unregister(srv->ev, srv_socket->fd);
close(srv_socket->fd);
srv_socket->fd = -1;

/* network_close() will cleanup after us */
}
}

if (graceful_shutdown) {
log_error_write(srv, __FILE__, __LINE__, "s", "[note] graceful shutdown started");
} else if (srv->conns->used > srv->max_conns) {
log_error_write(srv, __FILE__, __LINE__, "s", "[note] sockets disabled, connection limit reached");
} else {
log_error_write(srv, __FILE__, __LINE__, "s", "[note] sockets disabled, out-of-fds");
}

srv->sockets_disabled = 1;
}
}

が、Lighttpd と FastCGI が socket 通信できなくてエラーを吐く部分になります。どうやら、FD_SETSIZE * 0.9 を超えた socket 通信が発生すると、エラーが発生。どんどんリクエストが来るので永久に要求される socket 数が FD_SETSIZE を下回ることなく接続不可能な状態に陥るみたいです。

ちなみに FD_SETSIZE とは、ファイルディスクリプタのデフォルトサイズを設定する値で、

# find /usr/src/ -name posix_types.h
/usr/src/kernels/2.6.9-22.EL-smp-x86_64/include/linux/posix_types.h
....

# grep FD_SETSIZE /usr/src/kernels/2.6.9-22.EL-smp-x86_64/include/linux/*
./kernels/2.6.9-22.EL-smp-x86_64/include/linux/posix_types.h:#define __FD_SETSIZE 1024
....

で現状のデフォルト値を知ることができます。多くの場合、デフォルト値は 1024 になっていると思います。

で、ファイルディスクリプタとはそもそも何か?と調べると

プログラムがアクセスするファイルや標準入出力などをOSが識別するために用いる識別子。0から順番に整数の値が割り当てられる。OSによってはファイルディスクリプタにバッファ管理機能なども含めた「ファイルハンドル」と呼ばれる管理体系が存在する。

ファイルディスクリプタには、識別子とともにファイル名、ファイルサイズ、プログラムが操作中のファイル内の位置、ファイル作成、更新日時などの情報が含まれており、OSは識別子によってどのファイルを操作するかを判断する。

通常、0:標準入力(stdin)、1:標準出力(stdout)、2:標準エラー出力(stderr)の3つはOS(シェル)が最初に用意するため、プログラムがファイルをオープンすると「3」から順番にディスクリプタが割り当てられる。


な訳です。ファイル入出力だけではなく、socket 通信とかもカウントの対象というところがミソです。

ここまで調べて自分でもようやくわかったのですが、大量の socket 通信が必要になるメール配信サーバであったり、web サーバであったりする場合、バッチ処理等で大量のファイルの同時アクセス(例えばDB処理とか)をしていると、標準の FD_SETSIZE で定義された 1024 では足りない場合が出てくるというわけです。

今回は、月次のバッチ処理で大量の File に対する入出力処理を実行中でした。そこに Amazon Search 経由で FastCGI への大量 socket リクエストでファイルディスクリプタが飽和したと言うことなんですね。

対処方法は Lighttpd をコンパイルする際に、

CFLAGS="-DFD_SETSIZE=4096" ./configure
make install

みたく、FD_SETSIZE の値を明示的に増量してあげればOK。

ちなみに、FD_SETSIZE で Lighttpd のソースを検索してみると、

if (srv->event_handler == FDEVENT_HANDLER_SELECT) {
/* select limits itself
*
* as it is a hard limit and will lead to a segfault we add some safety
* */
srv->max_fds = FD_SETSIZE - 200;
} else {
srv->max_fds = 4096;
}
なる部分が見つかる。うーん。FD_SETSIZE = 4096 が良さそうですね。早速リコンパイルして検証中です。
- スポンサーリンク -