mod_bwで接続数制限

多数同時起動するとバックエンドのシステムに大きな負荷を与えるようなCGIプログラムがあって、Apacheで特定のURLに対する接続数に制限を設けるということを行う必要が出てきたので、帯域制限系のモジュールについて調べてみました。Apache全体に対する接続数はMaxClientsで制限できますが、これをURL単位で行うというイメージです。いくつかのモジュールを検討したところ、mod_bwが最も要件に近いようなので試してみることにしました。

mod_bw開発元

ダウンロードしてインストールします。

$ tar zxvf mod_bw-0.8.tgz
$ cd mod_bw
# apxs -i -a -c mod_bw.c
# apachectl restart

まずは帯域制限の設定を試してみます。/bwtest/largefile.htmlというファイルに対するアクセスを全体で10240bytes/secに制限する設定です。

<Location /bwtest/largefile.html>
  BandwidthModule On
  ForceBandWidthModule On
  Bandwidth all 10240
</Location>

ブラウザやApache Bench(ab)でアクセスしてみるとたしかに帯域が絞られていることがわかります。

次に、帯域は無制限で接続数を10に制限してみます。

<Location /bwtest/largefile.html>
  BandwidthModule On
  ForceBandWidthModule On
  Bandwidth all 0
  MaxConnection all 10
</Location>

ドキュメントにはこの設定ができるように書いてあるのですがどうも効かないようです。
ソースを見てみると、bw_filter関数の中で、Bandwidthが0のときは接続数カウントを行わずアクセスを通しているように見えます。

    /* Check if we've got an ilimited client */
    if ((bw_rate == 0 && bw_f_rate == 0) || bw_f_rate < 0) {
        ap_pass_brigade(f->next, bb);
        return APR_SUCCESS;
    }

しょうがないので、BandWidthを1000000000とかの十分に大きな値にしてみたところ、接続数制限が行えるようになり、接続数を超えた場合は503 Service Temporary Unavailableが返るようになりました。接続数の様子は、ApacheのLogLevelをdebugにすると、error_logに記録されます。

ログの例(3クライアント接続)
[Fri Nov 30 18:02:00 2007] [debug] mod_bw.c(883): clients : 3/10  rate/min : 1000000000,256

しかし、abで何回か負荷をかけると、接続数オーバー後ずっと503のまま全く接続できなくなるという現象が発生しました。abの実行・停止(Ctrl+C)を何回か繰り返すと現象が確実に再現しました。現象が発生する前にはerror_logに以下のようなログが多数出ていました。

[Fri Nov 30 13:27:52 2007] [info] [client 10.100.102.134] (32)Broken pipe: core_output_filter: writing data to the network
[Fri Nov 30 13:27:52 2007] [debug] mod_bw.c(961): Connection to hell
[Fri Nov 30 13:27:53 2007] [info] [client 10.100.102.134] (32)Broken pipe: core_output_filter: writing data to the network
[Fri Nov 30 13:27:53 2007] [debug] mod_bw.c(961): Connection to hell

mod_bwでは、接続数をインクリメント→コンテンツを返す→接続数をデクリメント、という処理を行っています。中途半端なタイミング(Broken Pipeが出るような状況)でコネクションを切断すると接続数のカウンタがデクリメントされずに残ることがあると推測しました。

そこで、接続制限実施時にカウンタがいくつになっているか出力する処理を入れてみました。

[Fri Nov 30 13:24:12 2007] [warn] Connections reached to MaxConnection. Clients : -1/10 Directory : /opt/apache/htdocs/bwtest/

接続できなくなる現象が発生したときに上のようなログが出ました。接続数が-1になっています。デクリメントされてないのではなくデクリメントしすぎでしょうか。
-1はunsigned intだと4294967295になります。handle_bw関数の中で、現在のコネクション数がapr_uint32_t型で、MaxConnectionがint型になっていて、apr_uint32_t型とint型をキャストせずに比較しているところがあります。

        /* If we are too busy, deny connection */
        confid = get_maxconn(r, conf->maxconnection);
        if ((bwstat->connection_count >= confid) && (confid > 0))
            return conf->error;

現象発生時、条件式が 4294967295 >= 10 みたいな感じになっていると思います。危険な香りがします。
そこで、bwstat->connection_countをint型にキャストするように変更したところ、現象は発生しなくなりました。しかし、そもそもインクリメント数とデクリメント数が合ってないということなのであまりよくありません。debugログを見ると、接続数が0になったり-1になったり、1コネクションしかないのに2とか3になったりしています。

[Fri Nov 30 18:02:00 2007] [debug] mod_bw.c(883): clients : 0/10  rate/min : 1000000000,256
[Fri Nov 30 18:02:01 2007] [notice] child pid 16647 exit signal Arithmetic exception (8)

接続数が0とカウントされたときは、Arithmetic exceptionで子プロセスがお亡くなりになっています。帯域を接続数で割る処理があるので、おそらくそこで0による除算が発生しているのでしょう。このときブラウザの画面は真っ白になります。

なかなかにバギーな代物のようなので商用環境で恒久的に使うのはちょっとためらわれます。とりあえず暫定対処ということで別の方法を探した方がいいかもしれませんね。

パッチを置いておきます。内容は以下のとおりです。

  • bwstat->connection_countをint型にキャストする
  • 接続制限実施時にerror_logにログを出力する
  • APRのバージョンチェックを外す(これはついで)

何の責任も負えませんのでご利用は自己責任でよろしくお願いします。

--- mod_bw.c.orig 
+++ mod_bw.c  
@@ -69,13 +69,13 @@
 #endif

 /* Compatibility for APR < 1 */
-#if ( defined(APR_MAJOR_VERSION) && (APR_MAJOR_VERSION < 1) )
+//#if ( defined(APR_MAJOR_VERSION) && (APR_MAJOR_VERSION < 1) )
     #define apr_atomic_inc32 apr_atomic_inc
     #define apr_atomic_dec32 apr_atomic_dec
     #define apr_atomic_add32 apr_atomic_add
     #define apr_atomic_cas32 apr_atomic_cas
     #define apr_atomic_set32 apr_atomic_set
-#endif
+//#endif

 /* Enum types of "from address" */
 enum from_type {
@@ -774,8 +774,12 @@

         /* If we are too busy, deny connection */
         confid = get_maxconn(r, conf->maxconnection);
-        if ((bwstat->connection_count >= confid) && (confid > 0))
+        if (((int)(bwstat->connection_count) >= confid) && (confid > 0)) {
+               ap_log_error(APLOG_MARK, APLOG_NOERRNO|APLOG_WARNING, 0, r->server,
+                 "Connections reached to MaxConnection. Clients : %d/%d Directory : %s",
+                 (int)(bwstat->connection_count), confid, conf->directory);
             return conf->error;
+               }
     }

     /* Add the Filter, if in forced mode */