Apacheのmod_proxy_balancerでhttpとwebsocketの両方に対応したバランシングをする方法


# はじめに

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つある場合を想定して設定ファイルを書くと次にようになります。

```apache:balancer.conf
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スキーマの場合

```diff:balancer.conf
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の両方のスキーマのメンバを定義した場合

```diff:balancer.conf
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関数内から次のように呼び出されます。

1. ap_proxy_pre_request呼び出し(mod_proxy_balancerが実装)
  - 適切なworkerを取得する
2. 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で読み込むようにしました。

```diff:balancer.conf
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
```

```apachef: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を書き換えることで、バランシングが可能となります。

## 参考情報

- [Apache load balance tomcat websocket](https://stackoverflow.com/questions/37564692/apache-load-balance-tomcat-websocket)

httpsをwssへ書き換える事例ですが、今回のhttpをwsに書き換える対応として参考になりました。

# おわりに

今回のようなmod_proxy_balancerでhttpとwsの両方に対応するというものは、それほど珍しくないユースケースだと個人的には考えていますが、web上にはそのものズバリな対応がなかったので記事にしてみました。

同じような対応で困っている人の助けになれば幸いです。