2014年5月26日月曜日

mod_ratelimit: クライアント向けの帯域制限

レート制御モジュールは出力フィルタで実装されている。
環境変数 rate-limitsで接続ごとの送信データレートを指定できる。
単位はKB/秒。

<Location /downloads>
  SetOutputFilter RATE_LIMIT
  SetEnv rate-limit 400
</Location>
参考: http://httpd.apache.org/docs/2.4/mod/mod_ratelimit.html

指定するレートの値が環境変数なので、条件によって変更が効くという自由度があるが、他方、処理内でsleepしてタイミングをとっているのが event MPMとの組み合わせではいけていない気がする。
mod_dialup()との比較はどうだろうか。

この処理ではレート制御は個々のレスポンスデータの送信に適用される。サーバが処理する送信全体の帯域制御などの用途ではない。少数の大きなファイルのダウンロードなどで通信帯域が枯渇するといった問題を緩和するために利用されるのだろうか?(最近のTCPのレート制御事情がどうなっているのかは知らないが、既に帯域が消費されている状態からでは、後続の接続はなかなかスピードアップは計れない気がする。どうだろう)。

出力フィルタ処理を見ていく。
出力フィルタの概要 を前提にする。


(1)出力フィルタの登録

出力フィルタの登録は、以下の起動時の処理で行われる

24 #define RATE_LIMIT_FILTER_NAME "RATE_LIMIT"
  :

298 static void register_hooks(apr_pool_t *p)
299 {
300     /* run after mod_deflate etc etc, but not at connection level, ie, mod_ssl. */
301     ap_register_output_filter(RATE_LIMIT_FILTER_NAME, rate_limit_filter,
302                               NULL, AP_FTYPE_PROTOCOL + 3);
303 }

ap_register_output_filter()。フィルタ名はマクロ RATE_LIMIT_FILTER_NAME で、定義を見ると、"RATE_LIMIT"だ。
このフィルタ名を SetOutputFilterディレクティブに指定している。
出力フィルタの実体が、rate_limit_filter()関数だ。
AP_FTYPE_PROTOCOL + 3 がフィルタタイプ。プロトコルフィルタになっている。
フィルタタイプ順にフィルタの階層ができるので、プロトコルフィルタとしては下流におかれるように +3 している、ということだろうか。

(2)出力フィルタ処理 rate_limit_filter

出力フィルタを見ていく。

58 static apr_status_t
59 rate_limit_filter(ap_filter_t *f, apr_bucket_brigade *input_bb)
60 {

このフィルタ処理はコネクションが異常終了している場合には、処理を継続しない。

68     if (f->c->aborted) {
69         ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r, APLOGNO(01454) "rl: conn aborted");
70         apr_brigade_cleanup(bb);
71         return APR_ECONNABORTED;
72     }

フィルタ情報 ap_filter_t の 変数cは、このリクエストに関係するconn_rec コネクション情報が保持されている。接続が異常終了している場合には処理を中断している。
この処理では エラーログが出力され(69行目)、bucket brigadeもクリアされている(70行目)。

(2.1)フィルタ用コンテキスト情報

出力フィルタ用のコンテキスト情報が ap_filter_t の変数 ctxに登録できる。
ratelimitモジュールのコンテキスト情報はrl_ctx_tだ(62行目)。
まだ未登録の場合に、74行目のチェックが 真となり、101行目でメモリが確保され、
102行目でap_filter_t情報にセットされ、
103行目から110行目まで、コンテキスト情報 rl_ctx_t に情報がセットされている。

62     rl_ctx_t *ctx = f->ctx;
  :

74     if (ctx == NULL) {
  :

101         ctx = apr_palloc(f->r->pool, sizeof(rl_ctx_t));
102         f->ctx = ctx;
103         ctx->state = RATE_LIMIT;
104         ctx->speed = ratelimit;
105
106         /* calculate how many bytes / interval we want to send */
107         /* speed is bytes / second, so, how many  (speed / 1000 % interval) */
108         ctx->chunk_size = (ctx->speed / (1000 / RATE_INTERVAL_MS));
109         ctx->tmpbb = apr_brigade_create(f->r->pool, ba);
110         ctx->holdingbb = apr_brigade_create(f->r->pool, ba);
111     }

出力フィルタは、1つのリクエストの処理中に、何度も実行される。
実行回数は出力フィルタの階層内の位置や、他にどんな出力フィルタ処理が行われるかも関係するし、送信されるデータのサイズによっても異なる。
コンテキスト情報は何度も呼ばれる処理で継続的に情報を持ちまわるために利用する。

ctx値がNULLだというのは、このリクエストでの最初の出力フィルタ処理だったことを意味している。
この最初の処理でコンテキスト情報が作成された。

このフィルタではサブリクエストは処理対象外となっている。
この場合、 ap_pass_brigade()を実行して bucket brigadeを下流のフィルタに引き渡し、returnしている。

79         /* no subrequests. */
80         if (f->r->main != NULL) {
81             ap_remove_output_filter(f);
82             return ap_pass_brigade(f->next, bb);
83         }

サブリクエストというのは、Apache内部で派生されるリクエストで、実際にレスポンスは返さないが、アクセスチェックなど、Apacheで行われる各種チェックを行うための仕掛けである。
f->rがこのフィルタで処理されるリクエスト情報 request_recであり、 request_recの main変数は、そのリクエストがサブリクエストでなければ、NULLとなる。

サブリクエストでは、レスポンスは送られないのだから、このモジュールのような実際にレスポンスを返す処理では処理対象外となる。

また、環境変数"rate-limit"が未設定の場合も対象外だ。
この場合も同じように、ap_pass_brigade()で下流にbucket brigadeが渡される。

85         rl = apr_table_get(f->r->subprocess_env, "rate-limit");
86
87         if (rl == NULL) {
88             ap_remove_output_filter(f);
89             return ap_pass_brigade(f->next, bb);
90         }


環境変数 "rate-limit" にはKBytes/secの値がセットされているので、これをBytes/secに変換している。
変換した値をチェックし、不適格であれば、やはり処理は行われない。

92         /* rl is in kilo bytes / second  */
93         ratelimit = atoi(rl) * 1024;
94         if (ratelimit <= 0) {
95             /* remove ourselves */
96             ap_remove_output_filter(f);
97             return ap_pass_brigade(f->next, bb);
98         }

コンテキスト情報 rl_ctx_t に設定される情報を見る。

25 #define RATE_INTERVAL_MS (200)
   :

103         ctx->state = RATE_LIMIT;
104         ctx->speed = ratelimit;
105
106         /* calculate how many bytes / interval we want to send */
107         /* speed is bytes / second, so, how many  (speed / 1000 % interval) */
108         ctx->chunk_size = (ctx->speed / (1000 / RATE_INTERVAL_MS));
109         ctx->tmpbb = apr_brigade_create(f->r->pool, ba);
110         ctx->holdingbb = apr_brigade_create(f->r->pool, ba);

103行目のstateは、レート制御を適用するかどうかの状態を識別する。
RATE_LIMITはレート制御を適用する状態。
そのほかに、RATE_FULLSPEEDは適用されていない状態。 RATE_ERRORは何らかのエラーである。
104行目のspeedは、環境変数"rate-limit"×1024 Bytes/sec 。
一定量のデータの送信を一定間隔(RATE_INTERVAL_MS)で行うように、chunke_sizeを設定する(108行目)。

RATE_INTERVAL_MSの単位はミリ秒なので、 1000ミリ秒をこれで割ると、1秒間に何回送信が行われるかが決まる。
speed(Bytes/sec)は1秒間に送られるべきデータサイズなので、1秒間の送信回数で割れば、1回の送信データサイズが決まる。これがchunke_sizeに入る。

tmpbbおよび、holdingbbは作業用に使用されるbucket brigadeだ。
最初はbucketを持たない、空bucket brigadeである。
用途を見ると、tmpbbはchunk_size分のデータを下流のフィルタに引き渡す際に使用されている。
一方の、holdingbbは、RL_START/RL_ENDという本フィルタ固有のメタデータbucketの制御に関連して使用されている。

このフィルタ設定はリクエストごとに適用されるので、レート制限はリクエストごとに行われることになる。

(2.2)bucket brigade の処理

引き続き、bucket brigadeの処理フェーズがある。

送信処理では、bucket brigadeからchunke_sizeのデータを取り出し、tmpbbに移し、tmpbbの末尾にflushバケットを追記してap_pass_brigade()でtmpbbを下流のフィルタに引き渡し、送信を実行する。そしてRATE_INTERVAL_MSミリ秒スリープし、次のデータを処理する。
この処理が上流から受け取った bucket brigadeが空になるまで続けられている。

ここで利用されるbucket/bucket brigade処理用のマクロや関数についても簡単に説明しておく。

 64     int do_sleep = 0;
   :

113     while (ctx->state != RATE_ERROR &&
114            (!APR_BRIGADE_EMPTY(bb) || !APR_BRIGADE_EMPTY(ctx->holdingbb))) {
115         apr_bucket *e;

APR_BRIGADE_EMPTY(bb) bucket brigadeがbucketを持たない場合に真となる
: 154 while (ctx->state == RATE_LIMIT && !APR_BRIGADE_EMPTY(bb)) {    : 168 while (!APR_BRIGADE_EMPTY(bb)) {
bucket brigade bb が空になるまで処理を続ける。
  : 178 if (do_sleep) {
do_sleepは最初は0なので、スリープしない。 apr_sleep()はスリープ処理で、引数はマイクロ秒。
179 apr_sleep(RATE_INTERVAL_MS * 1000); 180 } 181 else { 182 do_sleep = 1; 183 } 184 185 apr_brigade_length(bb, 1, &len);
apr_brigade_length(bb,read_all,length) bucket brigade bb 内のbucketの長さ(データ長)の合計を求め、 *lengthに収める。 ただし、このフィルタ関数内ではlenは参照されていない。 read_allが1の場合、bucketの長さ情報が未設定(-1)だと、この関数でデータを読み込んで長さを確定してするようなので、ここでは未読込のデータを読み込む処理が行われていることになる。 ファイルbucket の場合は作成時に長さが設定されていた。
186 187 rv = apr_brigade_partition(bb, ctx->chunk_size, &stop_point);
apr_brigade_partition(b, point, after_point) bucket brigade bを先頭から pointバイトの位置で分割する。 分割された後ろ側の先頭のbucketの位置が after_pointに格納される。 pointバイトの位置が一つのbucketの切れ目に一致していればそのまま次のbucketがafter_pointにセットされるが、 bucketの中間の場合、bucketがその位置で前後に分割されて、後ろのbucketがafter_poolにセットされる。 この時点では、すべてのbucketは元のbucket brigadeに保持されている。
: 195 if (stop_point != APR_BRIGADE_SENTINEL(bb)) {
APR_BRIGADE_SENTINEL(bb)は bucket brigade bbの終端を意味する。 リングにつながったbucketの最後のbucketの「次」を指している。 これ自体はbucketではない。 以下では分割した次のbucket であるstop_pointが終端でない場合、 bucket brigade bbから chunk_size分のデータ(stop_pointの前までのbucket)を切り出し、tmpbbにセットする処理を行っている。
196 apr_bucket *f; 197 apr_bucket *e = APR_BUCKET_PREV(stop_point);
APR_BUCKET_PREV()は名前の通りで、stop_pointの前のbucketを取得している
198 f = APR_RING_FIRST(&bb->list);
APR_RING_FIRST()はbucket brigade bbが持っているbucketのリング(bb->list)の先頭を返している。 APR_BRIGADE_FIRST(bb)でいいんじゃないかと思う。
199 APR_RING_UNSPLICE(f, e, link);
ここまでで、 fにはbucketのリングの先頭が入り、 そこからchunk_size分のデータを持っている最後のbucketが eに入っている。その、リングの一部(f~e)を元のリングから取り除いているのがAPR_RING_UNSPLICE()だ。 この結果、bucket brigade bb から f~eは取り除かれている。
200 APR_RING_SPLICE_HEAD(&ctx->tmpbb->list, f, e, apr_bucket, 201 link);
そして、取り除いた bucket列 f~eを、bucket brigade tmpbbの先頭に挿入する。
202 } 203 else {
もし、bucket brigade bbの持っているデータサイズが chunk_sizeより小さい場合、すべてのデータを送信する。
204 APR_BRIGADE_CONCAT(ctx->tmpbb, bb);
APR_BRIGADE_CONCAT(tmpbb, bb) bucket brigade tmpbbの末尾に bbのbucketをすべて追加し、bbを空にする。
205 } 206 207 fb = apr_bucket_flush_create(ba);
apr_bucket_flush_create(list) FLUSHバケットの作成 FLUSHバケットを受け取った 下流のCOREフィルタは、そこまでのデータをクライアントに送信する
208 209 APR_BRIGADE_INSERT_TAIL(ctx->tmpbb, fb);
APR_BRIGADE_INSERT_TAIL(tmpbb, fb) bucket fb を bucket brigade tmpbb の末尾に追加する
: 216 rv = ap_pass_brigade(f->next, ctx->tmpbb);
tmpbbを下流のフィルタに引き渡す。
217 apr_brigade_cleanup(ctx->tmpbb);
tmpbbをクリアする(空の bucket brigadeになる) tmpbbはap_pass_brigadeされるたびにクリアされることになる。
: 225 }


このほかに、ratelimitモジュール用の制御バケットが2つ定義されている。
レート制御開始(RL_START)とレート制御終了(RL_END)の制御バケットだ。

この制御開始~終了の間のバケットはレート制御の対象外になっている(stateがRATE_FULLSPEEDになっている)ようだ。
ただ、この制御用bucketを生成する関数ap_rl_start_create()とap_rl_end_create()は、利用しているコードがhttpd-2.4.9には見当たらなかった。
今回、このメタデータbucketの関連と思われる処理は無視した。


0 件のコメント:

コメントを投稿