はじめに
Apacheを使ったリバースプロキシ構成で、バランシングをしない単純なproxyでバックエンドとhttpとwebsocketの両方に対応するにはmod_proxy_httpとmod_proxy_wstunnelを使えば実現できます。
しかし、mod_proxy_balancerと組み合わせると、そのままではhttpとwebsocketの両方に対応できず、苦労したので、その実現方法を記事にします。
実行環境
- OS : CentOS 7.3
- Apache:v2.4.28
mod_proxy_balancerの一般的な設定の場合
バランシング対象のproxy先のメンバが2つある場合を想定して設定ファイルを書くと次にようになります。
ProxyRequests off
ProxyPass /sample/ balancer://samplecluster/sample/ stickysession=sampleID
ProxyPassReverse /sample/ balancer://samplecluster/sample/
<Proxy balancer://samplecluster/sample/>
BalancerMember http://localhost:6091 loadfactor=1 route=r1
BalancerMember http://localhost:6092 loadfactor=1 route=r2
</Proxy>
mod_proxy_balancerの仕様に従い、BalancerMemberに定義されるバックエンド側でstickysessionのcookieが設定される前提です。
実行結果
-
404(NotFound)が返ってくる。
-
http通信は問題なくバランシングされるのですが、Upgradeされてwsスキーマ(ws://)になるとBalancerMemberに合致せず、また、自身にそのようなリソースもないためです。
設定変更して試してみる
BalancerMemberのスキーマをいじってみます。
wsスキーマの場合
ProxyRequests off
ProxyPass /sample/ balancer://samplecluster/sample/ stickysession=sampleID
ProxyPassReverse /sample/ balancer://samplecluster/sample/
<Proxy balancer://samplecluster/sample/>
- BalancerMember http://localhost:6091 loadfactor=1 route=r1
- BalancerMember http://localhost:6092 loadfactor=1 route=r2
+ BalancerMember ws://localhost:6091 loadfactor=1 route=r1
+ BalancerMember ws://localhost:6092 loadfactor=1 route=r2
</Proxy>
この設定の場合、httpスキーマの時にバランシングされません。
httpとwsの両方のスキーマのメンバを定義した場合
ProxyRequests off
ProxyPass /sample/ balancer://samplecluster/sample/ stickysession=sampleID
ProxyPassReverse /sample/ balancer://samplecluster/sample/
<Proxy balancer://samplecluster/sample/>
BalancerMember http://localhost:6091 loadfactor=1 route=r1
BalancerMember http://localhost:6092 loadfactor=1 route=r2
+ BalancerMember ws://localhost:6091 loadfactor=1 route=r1
+ BalancerMember ws://localhost:6092 loadfactor=1 route=r2
</Proxy>
- 各メンバが独立して認識されるため、同じrouteでも別の接続と認識されてしまう
- 最大接続数を1のように制限したい場合に、2つまで許容されてしまう
現状把握(実装調査)
mod_proxyのフック実行順をモジュールの実装で確認しました。
mod_proxyのproxy_handlerの主要なところの実装確認
mod_proxy_balancerとmod_proxy_wstunnelはmod_proxyのproxy_handler関数内から次のように呼び出されます。
- ap_proxy_pre_request呼び出し(mod_proxy_balancerが実装)
- 適切なworkerを取得する
- proxy_hook_scheme_handler呼び出し(mod_proxy_wstunnelが実装)
- scheme(今回の場合はwebsocket)に対応するのに必要な処理を実施
mod_proxy_balancerでは単純にconfファイルに書いてある内容のみ実施していて、http→wsのアップグレードで特別な処理はしていないように理解をしました。
スキーマ特有の操作はmod_proxy_wstunnelなどのスキーマに応じた別のモジュールのscheme_handler内で処理しているはずです。
実装を参考にすると、2.が呼び出されるまでの間にhttpスキーマをwsスキーマに変更すればmod_proxy_wstunnelでwebsocketの処理ができそうです。
対応策
スキーマの変更ということで、mod_rewriteを組み合わせます。
websocketの特徴であるhttpヘッダのupgradeフィールドとconnectionフィールドを手掛かりに、websocketと判断出来た場合にsticky session cookieの内容に応じたメンバのURLへ書き換えます。
次のすべての条件を満たしたときにRewriteRuleでURLを書き換えます。
- httpヘッダのupgradeフィールドが"WebSocket"である
- httpヘッダのconnectionフィールドが"Upgrade"である
- cookieの値(今回の例ではsmapleID)がrouteに定義している値と同じである
今回は次のようにrewrite_ws.confというrewrite用のconfファイルを作成し、balancer.confからIncludeOptionalで読み込むようにしました。
ProxyRequests off
ProxyPass /sample/ balancer://samplecluster/sample/ stickysession=sampleID
ProxyPassReverse /sample/ balancer://samplecluster/sample/
<Proxy balancer://samplecluster/sample/>
BalancerMember http://localhost:6091 loadfactor=1 route=r1
BalancerMember http://localhost:6092 loadfactor=1 route=r2
</Proxy>
+ IncludeOptional rewrite_ws.conf
RewriteEngine On
RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC]
RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC]
RewriteCond %{HTTP_COOKIE} ^.*(sampleID)=([^=]*)\.r1 [NC]
RewriteRule .* ws://localhost:6091%{REQUEST_URI} [P,L]
RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC]
RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC]
RewriteCond %{HTTP_COOKIE} ^.*(sampleID)=([^=]*)\.r2 [NC]
RewriteRule .* ws://localhost:6092%{REQUEST_URI} [P,L]
このrewriteにより、cookieを手掛かりにmod_rewriteでURLを書き換えることで、バランシングが可能となります。
参考情報
httpsをwssへ書き換える事例ですが、今回のhttpをwsに書き換える対応として参考になりました。
おわりに
今回のようなmod_proxy_balancerでhttpとwsの両方に対応するというものは、それほど珍しくないユースケースだと個人的には考えていますが、web上にはそのものズバリな対応がなかったので記事にしてみました。
同じような対応で困っている人の助けになれば幸いです。