読者です 読者をやめる 読者になる 読者になる

Apacheのmod_proxy_balancerのbybusynessに振り分けについて調べてみた

Apacheのロードバランシングモジュールである mod_proxy_balancer の振り分けアルゴリズムbybusyness について調べた内容です。

byrequestsとbybusyness

語弊を恐れずに1行で書けばいかのような感じかと思います。

メソッド 1行説明
byrequests ワーカー毎の処理リクエスト数が均等になるように配分
bybusyness ワーカー毎の処理キュー長が均等になるように配分

bybusynessの使いどころ

シンプルなサーバで、常に安定して同じような時間で処理をできる類いのシステムなら byrequests でも問題無いと思われます。 bybusyness が有用だと思われるケースとして以下の二つを挙げてみます。

  • 特定サーバが遅くなっている場合 複数の役割をこなしているサーバなら裏の処理の影響がある時など、単一の役割のサーバでもH/Wの不具合等の影響がある時など、特定のサーバが遅延してしまう時、単純にリクエスト数で配分されてしまうと非効率な場合があります。

  • リクエスト毎の処理時間にバラつきがある場合 リクエストの処理時間を図にしたとき、以下のように一定なら問題無いのですが、

・・・・・・・・・・・・・・・・・・・・・・・・・・・
━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━

以下のようにリクエスト毎に処理時間がバラバラだった場合、

━━・・━・━━━・・━・━・・・・━━━━━・━・・・

単純にリクエスト数で振り分けた場合、特定のWEBサーバにだけ長い処理がかかるが振られてしまい処理効率が悪くなってしまう場合があります。

例えばこれらシナリオの場合に bybusyness が役立つ場合があります。

検証による動きの比較

イメージつけるために実際に動かしてみます。デフォルトの振り分け方式である byrequests との動きを比較しながら、上記のbybusyness が役に立つ可能性があるシナリオを検証してみます。

以下のように簡易な検証できる環境を構築してみます。

LB (localhost:8080)
  ├1号機 (localhost:55081)
  └2号機 (localhost:55082)

シナリオ① 特定のサーバが遅くなる時を再現

まずは片方のサーバが重かった時の動きを簡易再現してみます。複数の役割をこなしているサーバなら裏の処理の影響があったり、単一の役割のサーバでも単純にシステムリソースの障害等で同じ状況は発生する可能性があるかと思います。

シナリオ① 検証環境
  • 1号機の処理 こちらは通常のサーバを再現しています。
<?php
echo "1号機";
?>
  • 2号機の処理 逆にこちらは重いサーバを再現しています。
<?php
sleep(5);
echo "2号機";
?>

abコマンド使ってテストしてみます。

ab -c 10 -n 50 http://localhost:8080/index.php
シナリオ① 結果(byrequests)

まず byrequests の結果から。

50件のリクエストが1号機と2号機に均等に25件ずつ割り振られているのがわかります。 f:id:yomon8:20170518112656p:plain

abコマンドのの実行結果を見ると、早い1号機にも遅い2号機にも同じだけ割り振られてしまったため、全体としての処理時間も大きくなってしまっています。

Time taken for tests: 27.430 seconds
シナリオ① 結果(bybusyness)

次に bybusyness です。今度は早い1号機側に多く割り振られています。

f:id:yomon8:20170518113032p:plain

※この画面はbalancer-managerのものなのですが、Loadの項目が-4040 という数字になっています。これは後述するlbstatus という変数の値なのですが、 bybusyness アルゴリズムの場合はほぼ関係無い(ゼロでは無い)ので、ここでは無視して問題ありません。

abコマンドのの実行結果を見ても byrequests よりも効率良く処理されているのがわかります。

Time taken for tests: 5.018 seconds

シナリオ② リクエスト毎の処理時間にバラつきがある時

シナリオ② 検証環境

今度は1号機と2号機共通で wait パラメータをURLに渡すことで処理時間のバラツキを再現してみます。

<?php
if(isset($_GET['wait'])) {
  $wait = $_GET['wait'];
  sleep($wait);
}
echo "1号機";
?>

WAIT時間として異なるURLパラメータを渡したいので拡張abコマンドでテストします。

abコマンドの拡張方法などはこちらに。

Apache Bench(abコマンド)で変数使ったり複数種類のURLリクエストを送りたい - YOMON8.NET

以下の list.txt ファイルを使って、待ち時間がバラバラにしてリクエストを投げてみます。

0
0
1
0
2
0
5
0
2
1

abコマンドを拡張した -R オプションを使います。

ab -c 10 -n 50 -R list.txt http://localhost:8080/index.php?wait=
シナリオ② 結果(byrequests)

byrequestsの場合は変わらずリクエスト数で振り分けます。

処理毎にレスポンスまでの処理時間が異なるので、片方のサーバに重い処理が偏ってしまうことがあり、全体としてのパフォーマンスを十分に活かせない場合があります。

f:id:yomon8:20170518190019p:plain

3回テストしてみましたが今回のケースでは15秒ほどでした。

Time taken for tests:   15.45550 seconds
Time taken for tests:   15.45509 seconds
Time taken for tests:   15.41334 seconds
シナリオ② 結果(bybusyness)

bybusynessの場合はサーバ毎のタスクキューを見ているので、あるサーバが忙しければ他のサーバに割り振るようなことが可能です。

結果としてLB配下のサーバの能力を上手く活用できることがあります。簡易的なテストですが実際に全体の処理時間が短縮されているのがわかります。

f:id:yomon8:20170518190032p:plain

3回テストしてみましたが、byrequestsが15秒ほどだったのと比べると早いです。

Time taken for tests:   9.50075 seconds
Time taken for tests:   9.40181 seconds
Time taken for tests:   8.41857 seconds

振り分けロジック

byrequestsのロジック

bybusyness のロジックを理解するためにも、まず簡単に byrequests のロジックを見ておきます。

ロードバランサーがどのサーバに振り分けるかは lbfactor という変数で決まります。

ロジックはApache公式サイトに説明があるので引用します。

for each worker in workers
    worker lbstatus += worker lbfactor
    total factor    += worker lbfactor
    if worker lbstatus > candidate lbstatus
        candidate = worker

candidate lbstatus -= total factor

実際にどのように動くのか書いてみます。

4台のWEBサーバをロードバランシングすることとします。上のコードで言えば workersは4つのworkerが入っていることになります。

lbfactor は設定ファイルで設定可能な振り分け重み付け変数です。デフォルトは1で、今回は25を設定したことにします。

このロジックで回していった場合の、 lbstatus 変数の値の遷移と、振り分け先( candidate )として選ばれるworkerの遷移です。

loop\worker a b c d candidate
0 0 0 0 0 a
1 -75 25 25 25 b
2 -50 -50 50 50 c
3 -25 -25 -25 75 d
4 0 0 0 0 a
5 -75 25 25 25 b
6 -50 -50 50 50 c

実際のコードの該当部分も貼っておきます。

httpd/modules/proxy/balancers/mod_lbmethod_byrequests.c

                /* Take into calculation only the workers that are
                 * not in error state or not disabled.
                 */
                if (PROXY_WORKER_IS_USABLE(*worker)) {
                    (*worker)->s->lbstatus += (*worker)->s->lbfactor;
                    total_factor += (*worker)->s->lbfactor;
                    if (!mycandidate || (*worker)->s->lbstatus > mycandidate->s->lbstatus)
                        mycandidate = *worker;
                }

bybusynessのロジック

次にbybusyness見ます。パッと見はbyrequestsと似ています。ただif文を見ると (*worker)->s->busy < mycandidate->s->busy と書いてあってしっかりとbusyかどうかで振り分けられます。

lbfactor lbstatusbusy が同じ場合の次の判断基準として使われています。

busy の項目はworkerに割り当てられたタスクのキューの長さなのですが、その値は上位モジュールである mod_proxy_balancer.c にて管理されています。なので、byrequestsのアルゴリズムを使ってもbalancer-managerにはBusyの項目が計算されていることになります。

httpd/modules/proxy/balancers/mod_lbmethod_bybusyness.c

                /* Take into calculation only the workers that are
                 * not in error state or not disabled.
                 */
                if (PROXY_WORKER_IS_USABLE(*worker)) {

                    (*worker)->s->lbstatus += (*worker)->s->lbfactor;
                    total_factor += (*worker)->s->lbfactor;

                    if (!mycandidate
                        || (*worker)->s->busy < mycandidate->s->busy
                        || ((*worker)->s->busy == mycandidate->s->busy && (*worker)->s->lbstatus > mycandidate->s->lbstatus))
                        mycandidate = *worker;

                }

参考

httpd/mod_lbmethod_bybusyness.c at trunk · apache/httpd · GitHub

mod_lbmethod_bybusyness - Apache HTTP Server Version 2.4