Bluemix Platform (PaaS)の負荷分散構成についてのメモ。誰でもふと実装を考えるとは思うのですが、意外とググっても出てこないので、せっかくなのでまとめました。
対象はBluemix Platformの標準基盤であるCloud Foundry上のアプリケーション、リクエストの負荷分散はPHPビルドパック使って実装してみます。
Bluemix Platformでの負荷分散は何が難しい?
Bluemix Platformでの負荷分散といってもざっくりと2つのことを考える必要があります:
(1) リージョン内の負荷分散
(2) リージョン間の負荷分散
(1) リージョン内の負荷分散の仕様
(1)はCloud Foundryが提供するルーター・コンポーネント(GoRouter)の責務ではあるので、アプリケーションを動かすランタイム・インスタンスを複数稼働させたとしても、アプリケーションに対するリクエストは勝手に各インスタンスに分散されます。
ただし、F5社のBIG-IP LTMのようなロードバランサーでよく設定するHTTPセッションのアフィニティー、すなわち常に初回にアクセスしたインスタンスにリクエストを割り振り続ける設定を意図的に行うことは残念ながらできません。基本はラウンドロビン(というかほぼランダムに近いかも)でリクエストが分散される仕様になっています。なので、複数のインスタンスでHTTPセッション共有を行うためにセッション共有の仕組みを用意する必要が出てきます。
しかしながら、HTTPセッションのアフィニティーに対して全く手が打てない、というわけでもありません。
例外その1として、「JSESSIONID」というCookie名に対してのみ、付与/変更された場合に「__VCAP_ID__」が付与されます。この「__VCAP_ID__」が付与されると、実はルーター・コンポーネントがHTTPセッションのアフィニティーを実施するように動作する仕様になっています。「__VCAP_ID__」はユーザーが簡単に偽装することができないので、このCookieを自前で指定しての特定のインスタンスへの意図的なピンポイント・アクセスはほぼ無理です。
例外その2として、「X-CF-APP-INSTANCE」というヘッダーを指定することで、特定のインデックスを持つインスタンスにピンポイントにアクセスできるようにできます。
Routing Requests to a Specific App Instance
https://docs.cloudfoundry.org/devguide/deploy-apps/routes-domains.html#surgical-routing
しかしながら、何回か検証してみていますが、Bluemix Platformではこのヘッダー指定でのアクセスが残念ながらできないようです。まあ、PaaSにおいて特定のインスタンスに最初からピンポイントにアクセスするようなことは基本考えるべきではないので、見なかったことにしましょう^o^
→ 2017/09/11に試した時に使えるようになった模様です、やりましたね!!
したがって、リージョン内の負荷分散である程度HTTPセッションのアフィニティーが必要であれば、「JSESSIONID」の名前のCookieをアプリケーション内で発行してしまえば良いです。Javaはもちろん、Node.jsなどでもCookie名に違和感ありありですが、有効です。
でも、ここでロードバランサーあるあるが1つ。Bluemix上のアプリケーションから同じくBluemixのアプリケーションを呼ぶ場合は超注意です!! せっかく付与された「__VCAP_ID__」が上書きされたりでエライ目に遭います。後段で呼ぶアプリケーションの「__VCAP_ID__」を維持する方法はアフィニティーが必要なら別途考える必要があります。
(2) リージョン間の負荷分散の考慮事項
(2)に関して、2017年5月時点で自由に使えるリージョンは米国南部、英国、シドニー、ドイツの4つです。可用性/災害対策などを考慮し、アクティブ-アクティブなのかアクティブ-スタンバイな構成とするのかは要件次第ではありますが、とにかく少なくとも2つ以上のサイトでアプリケーションを動かしたくなります。
・・・なのですが、現時点ではBluemixの標準機能として実装することができないです。Akamai社やDYN社のGSLB(Global Site Load Balancing)サービスを利用することが現時点でのBluemixのおすすめではありますが、やりたいことに対してちょっと高すぎる傾向があります。安価にできるサービスがきっと来るはずだと信じていますが。。
シンプルな実装方法は?
というわけで、Bluemix上のアプリケーションのリージョン内/間の両方に対応できる、イケてる負荷分散コンポーネントをこれから作ってみます。大前提として、Cloud Foundryの1アプリケーションなんだけど、コーディングしない、誰がなんと言おうとメンテしたくないので一切しないこと。また、あまり標準じゃないものは使いたくない、わがままな要件あるとして前提を置きます。
そうした条件を満たす、実装方法はいろいろ探してみましたが、以下の2通りかなと思っています:
(1) Staticfileビルドパック(Nginx)での簡単ロードバランサー
(2) PHPビルドパック(Apache HTTP Server)での簡単ロードバランサー
(1)に関しては、Staticfileビルドパックと呼ばれるNginxがベースのビルドパックを利用して構成する実装方法です。こちらも簡単にリージョン間の負荷分散、正確にはアクティブ-スタンバイを実装できるのですが、Apache HTTP Serverと異なり、有償のNginx Plusからでないと利用できない機能が多々あるにもかかわらず、StaticfileビルドパックのNginxはコミュニティー版なので、、な状況です。ビルドパックのレベルから作っても良いのだけど、メンテナンスもな〜、な状況ですのでせっかくですが断念しました。。
(2)に関して、PHPビルドパックでできあがるランタイムはApache HTTP Serverベースです。構成ファイルをユーザーでカスタマイズできる素敵仕様なので、モジュールさえあれば如何様にでもカスタマイズできてしまいます。mod_proxy_balancerと呼ばれる、Apache HTTP Serverでプロキシー構成をする際に負荷分散も合わせて実装できるモジュールが実は利用可能なので、これをベースに実装する手順を以下に記載していきます。
このほかにもDockerコンテナーで負荷分散を行うなど、もっと細かくいろいろ実装できるやり方もあります。すでに検証済みですが、アプリケーション・ファイアウォール的なものもDockerを活用すれば簡単に実装できてしまいます。Dockerを利用するケースはまた今度まとめます。
実装してみる
前置きは長かったですが、ここからが実装の話。結構シンプルにできますよ。
イメージ図としては以下のような感じ。
以下を想定して実装していきます:
a. 複数のBluemix Platformのリージョンを利用するが、アプリケーションのユーザーからは1つのURLでアクセスする
b. 動かしたいアプリケーション(Javaを想定)はステートフルで、なるべくHTTPセッションのアフィニティーが維持したい
c. 米国南部とシドニーはアクティブで利用、英国は米国南部/シドニーが両方ダウンした際のフェールオーバー環境として設定
準備1. 割り振り先のアプリケーションの実装
これは割り振り対象のアプリケーションに対する仕込みです。上述したように、複数のインスタンスを動かす前提の場合は、ステートフルなアプリケーションでアフィニティーを有効化したいなら、「JSESSIONID」なるCookieを発行するようにコーディングしておく必要があります。Javaの場合は良いのですが、Node.jsなど別のプログラミング言語の場合は意図的にこのCookieを発行するようにコーディングしてください。
また、リージョン間のロード・バランシングを実現しないとなので、どのリージョンに振られたかを示すCookieをセットするようにアプリケーションに仕込んでおきます。今回は「BMX-REGION」という名前のCookieを発行することとし、例えば米国南部なら「.US」、英国なら「.UK」、シドニーなら「.AU」という値を返すようにします。「.(ドット)」を先頭につけるのはApache HTTP ServerでCookieベースでリクエストのアフィニティーを実装する際のお約束(仕様)なので忘れずにつけます。
各アプリは以下のURLでアクセスできるものとして、以下進めます:
米国南部: https://backend-app-01.mybluemix.net
英国: https://backend-app-01.eu-gb.mybluemix.net
シドニー: https://backend-app-01.au-syd.mybluemix.net
準備2. DNSレジストラでのアドレス登録
アプリケーションのユーザーから見て、リージョンが異なるごとにアクセスするURL/FQDNが異なるのは望ましくありません(適切に転送などされるなら別)。Bluemixでは米国南部なら「.mybluemix.net」、英国ならば「.eu-gb.bluemix.net」のように提供されるドメイン(システム・ドメインと言います)が異なるため、何も気にしなければ提供されるドメインが割り当てられ、結果リージョン毎に別々のURLとなってしまいます。
もし、リージョン共通のURLでアプリケーションにアクセスできるようにしたいのであれば、持ち込みのドメイン、すなわちカスタム・ドメインを各リージョンにセットアップし、DNSレジストラに名前解決ができるように事前に設定する必要があります。
実装1. ディレクトリ/構成ファイルの用意
まずは、任意のからのディレクトリを作成し、そのディレクトリをカレントとして以下のコマンドを実行します:
$ mkdir -p .bp-config/httpd
次に、以下のリンクからPHPビルドパックの最新のコードをgit cloneで落としてきます。
cloudfoundry/php-buildpack
https://github.com/cloudfoundry/php-buildpack
git cloneしたらディレクトリ「defaults/config/httpd」内のファイルを全て作成したディレクトリにコピーします。
実装2. 利用するモジュールの指定
次に、「.bp-config/httpd/extra/httpd-module.conf」を開きます。開いたら、mod_proxy_balancerや関連するモジュールをアンコメントし、使用可能な状態にします。
自分の環境では以下のモジュールを有効化しています。
「LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so」は「.bp-config/httpd/extra/httpd-module.conf」には未記載のモジュールですが、v2.4.21から標準で利用可能なヘルスチェック用のモジュールなので、今回は利用する想定で設定します。
$ cat .bp-config/httpd/extra/httpd-modules.conf |grep -v "^#"
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule dir_module modules/mod_dir.so
LoadModule mime_module modules/mod_mime.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule reqtimeout_module modules/mod_reqtimeout.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule remoteip_module modules/mod_remoteip.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule filter_module modules/mod_filter.so
LoadModule deflate_module modules/mod_deflate.so
LoadModule headers_module modules/mod_headers.so
LoadModule watchdog_module modules/mod_watchdog.so
LoadModule ratelimit_module modules/mod_ratelimit.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule proxy_express_module modules/mod_proxy_express.so
LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so
LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so
LoadModule sed_module modules/mod_sed.so
実装3. .htpasswd(Basic認証のための認証ファイルの作成)
オプションですが、負荷分散の状況を確認したい場合、Balancer Manager画面を表示して確認することが可能です。デフォルトだと色んな人に見せてしまうことになるため、Basic認証をかけておきます。
「.bp-config/httpd/extra/」直下に.htpasswdを生成してください。.htpasswdファイルの生成方法は割愛しますが、「ユーザー名: bluemix、パスワード: bluemix」でログインできるファイルを用意したとします:
\$ cat .bp-config/httpd/extra/.htpasswd
bluemix:\$apr1\$pk7o0btP\$wgT1VkUxUKxu73Fj5TK9/1
実装4. プロキシー構成ファイル(httpd-proxy.conf)の設定
ここが一番肝心な部分です。ポイントは「__VCAP_ID__」を退避するためのCookieを別途設けて付け替えを行うこと、これにつきます。また、「BMX-REGION」で、初回に割り振ったリージョンのインスタンス群に対してリクエストを固定化します。以下、サンプル構成です:
\$ cat .bp-config/httpd/extra/ httpd-proxy.conf
ProxyRequests Off
SSLProxyEngine onProxyHCExpr ok23 {%{REQUEST_STATUS} =~ /^[23]/}
# 負荷分散構成
ProxyPass /balancer-manager !
ProxyPass / balancer://test_cluster/ stickysession=BMX-REGION
ProxyPassReverse / balancer://test_cluster# リクエスト時: BMX-AFFINITYが付与されていたら__VCAP_ID__に変更する
SetEnvIf ^BMX-AFFINITY\$ "([^;]+)" bmx-affinity=\$1
RequestHeader set __VCAP_ID__ %{bmx-affinity}e env=bmx-affinity# レスポンス時: __VCAP_ID__の値をBMX-AFFINITYに格納して返す
Header edit Set-Cookie "__VCAP_ID__=(.*)\$" "BMX-AFFINITY=\$1"# バランサー・マネージャー設定
<Location /balancer-manager>
SetHandler balancer-manager
OutPutSed "s/http:\/\//https:\/\//g"
AddOutputFilterByType Sed text/html
#Order Deny,Allow
#Deny from all
#Allow from 127.0.0.1
AuthUserFile /home/vcap/app/httpd/conf/extra/.htpasswd
AuthGroupFile /dev/null
AuthName "Balancer Manager"
AuthType Basic
require valid-user
</Location># 負荷分散先のメンバー設定
<Proxy "balancer://test_cluster">
BalancerMember https://backend-app-01.mybluemix.net loadfactor=10 route=US hcmethod=GET hcexpr=ok23 hcinterval=10 hcuri=/sample.jsp
BalancerMember https://backend-app-01.au-syd.mybluemix.net loadfactor=10 route=AU hcmethod=GET hcexpr=ok23 hcuri=/sample.jsp
BalancerMember https://backend-app-01.eu-gb.mybluemix.net loadfactor=10 status=+H
</Proxy>
実装5. マニフェスト・ファイル(manifest.yml)の作成
最後に、cf pushするための構成ファイルであるマニフェスト・ファイルを.bp-configディレクトリが配置されているディレクトリ上に作成します:
applications:
- path: .
memory: 256M
instances: 3
domain: <カスタム・ドメインを指定(.ra1nmaker.netなど)>
name: <LBアプリ名を指定>
host: <LBアプリのホスト名を指定>
buildpack: https://github.com/cloudfoundry/php-buildpack.git
ビルドパックはBluemix Platformが管理しているPHPビルドパックではなく、Cloud Foundryが提供しているPHPビルドパックを使用しましょう。バージョンの関係で、proxy_hcheck_moduleを利用したヘルスチェックの設定がCloud Foundryが提供しているPHPビルドパックでないとバージョン足らずで実装できないからです。
あとは各リージョンに「cf login -a 」でログインし、cf pushするだけです。
まとめ
PHPビルドパックという名のApache HTTP Serverで負荷分散を行うための構成例を整理しました。まだ、足りない部分が多々あるとは理解していますが、順次実装していけばよいかなと思っています。
全く同じではないですが、一応3ヶ月間とあるシステムでPHPビルドパックを利用した負荷分散構成を運用してみたこともあります(ダーク・カナリア・リリースをBluemix Platform上でできることを実証するために、半ば強引に採用して利用していました)。その時は大きなトラブルはなかったのですが、応答時間に対する要件も同時接続数もゆるい感じのシステムだったので、問題が顕在化なかっただけでは?とも思っています。多段プロキシー環境なのでやはりパフォーマンスが気になるところですので、同じようなことを実際に実装してみる場合には十分にテストを実施してみてください。