Apacheリバースプロキシ+ALB構成の落とし穴
Contents
概要
- 今回は、Apacheをリバースプロキシとして使用し、かつバックエンドにALBを配置する構成の落とし穴について、ご紹介します。
- 具体的には、下記構成となります。インターネットからのリクエストをNLBで受け、後続のリバースプロキシがバックエンドのWebサーバーにパススルーします。Webサーバーは、ALBとEC2で構成します。
- クライアント -> NLB(internet facing) -> リバースプロキシ -> ALB(internal) -> Webサーバー
ハマった落とし穴
- 始めに表面化した現象としては、クライアントからのリクエストにHTTP 503のエラーが返ること。トラブルシューティングしたところ、リバースプロキシとして使用しているApacheにConnection timed outのエラーが記録されていることが分かりました。このエラーは何らかの理由でALBに接続できなかったことを表します。
- 実際にクライアントからcurlを投げてみると、反応なし。httpd をrestart することで復旧し、その後使用可能となります。しかし、また数日すると同様の現象が発生する(!?)。継続して調査すると原因が見えてきました。
[Thu Nov 21 10:12:17.170431 2019] [proxy:error] [pid 10000:tid 1234567890] (110)Connection timed out: AH00957: HTTP: attempt to connect to xx.xx.xx.xx:80 (xxxxxxxxxx-111111111.ap-northeast-1.elb.amazonaws.com) failed
[Thu Nov 21 10:12:17.170464 2019] [proxy_http:error] [pid 10000:tid 1234567890] [client xx.xx.xx.xx:xxxxx] AH01114: HTTP: failed to make connection to backend: xxxxxxxxxx-111111111.ap-northeast-1.elb.amazonaws.com
- 一般的なHTTP 503のエラーをトラブルシューティングする方法は、下記記事を参照ください。
ALBに接続できない原因
サマリー
- Apacheのエラーログに記録されている「attempt to connect to xx.xx.xx.xx」のIPアドレスはバックエンドに配置したALBのIPアドレスとなりますが、ApacheがアクセスしていたIPアドレスは既に解放されて使用できないIPアドレスであったことが分かりました(ALBのIPアドレスは可変のため変わる可能性があります)。
- 上記より本現象は、ALB のIPアドレスが変わったタイミングからエラーが起きている可能性があると考えました。事実、ALB のIPアドレスが変わった日時と現象の発生日時が一致することを確認しています。
- つまり、突然ALBに接続できなくなる原因は、リバースプロキシとして使用しているApacheの子プロセスがIPをキャッシュしており、その結果いつまでも古いALB のIPアドレスにアクセスしていることが理由と判断しました。(こちらのテスト環境では、半日経過しても古いALB のIPアドレスへ接続を試みて、エラーとなっておりました) 但し、ALB のIPアドレスが変わると必ず発生するわけではなく、発生しない場合もあります。ApacheのMPM設定やTCPのCLOSEされていないセッションがあるなどの状況によって発生可否が変わると思われます。
- なお、ALBのIPアドレスを調べる方法は、下記記事を参照ください。
裏付け(ALBのIPアドレスを変えて再現試験)
- ALBのIPアドレスが変わる条件が不明であったため、疑似的な環境を準備しました。先ず、リバースプロキシのリダイレクト先にRoute 53で設定した別名を指定します。次にバックエンドを2種類準備します。1つ目はALB1 + 200 OK を返すターゲットグループ、2つ目はALB2 + ターゲットなし(503を返す)のターゲットグループです。
- 再現試験では、Route 53の別名に設定した値をALB1 のDNS名からALB2 のDNS名に切り替え後も、リバースプロキシが継続的にALB1 にリダイレクトし続けることが確認できました。
niikawa@niikawa1:~$ date;nslookup backend.example.com
Mon Jan 6 22:33:28 JST 2020
Server: 10.0.2.4
Address: 10.0.2.4#53
Non-authoritative answer:
Name: backend.example.com
Address: 10.10.2.55
Name: backend.example.com
Address: 10.10.3.111
→ Route 53で設定する別名の値をALB2 側に変更する。
→ 約1分経過後、nslookupによって、ALB2 のIPアドレスが引けていることを確認する。
niikawa@niikawa1:~$ date;nslookup backend.example.com
Mon Jan 6 22:34:07 JST 2020
Server: 10.0.2.4
Address: 10.0.2.4#53
Non-authoritative answer:
Name: backend.example.com
Address: 10.10.3.67
Name: backend.example.com
Address: 10.10.2.192
niikawa@niikawa1:~$
- クライアントからcurlを実行すると、別名の値をALB2 のDNS名に切り替えたにも関わらず、古いALB(ALB1 のDNS名)へアクセスが行われました。
niikawa@niikawa1:~$ curl -vv http://www.example.com
* Rebuilt URL to: http://www.example.com/
* Trying XX.XX.XX.XX...
* TCP_NODELAY set
* Connected to www.example.com (XX.XX.XX.XX) port 80 (#0)
> GET / HTTP/1.1
> Host: www.example.com
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Mon, 06 Jan 2020 13:34:45 GMT
< Server: Apache/2.4.41 ()
< Content-Type: text/html; charset=UTF-8
< Content-Length: 13
< Last-Modified: Mon, 06 Jan 2020 13:19:48 GMT
< ETag: "d-59b788383c192"
< Accept-Ranges: bytes
<
backend web1
* Connection #0 to host www.example.com left intact
niikawa@niikawa1:~$
リバースプロキシの回避策
回避策となるオプション
パターン1 (2021/4/27更新)
- パターン1はALBのIPアドレスが変わった時にバックエンド(ALB)に接続できない事象への対応です。
- proxyに"disablereuse=On"のオプションを設定します。("disablereuse=On"によってコネクションプーリングは無効となり、バックエンドへの接続は再利用されません。DNSから取得したALB IPアドレスは、キャッシュされません。デフォルトは"disablereuse=Off"であり、子プロセスがリサイクルされるまでALB IPアドレスをキャッシュします。)
- 任意でproxyに"retry=xx"を設定します(バックエンドへの接続を再試行する間隔をタイムアウト(秒)で指定、デフォルトは60)。
- retry=0 disablereuse=On
- その他、proxy設定のオプションは、こちらのドキュメントを参照。
パターン2 (2020/3/27追加、2020/5/12更新)
- パターン1はコネクションプーリングが無効となり、かつバックエンドへの接続も頻繁に試行されるため、リバプロの負荷が高くなります。ALBのIPアドレスが変わった場合への対処としては最適ですが、アクセスが多いシステムでは、適用できないケースが考えられます。
- 以下、パターン2です。パターン1より、多少の接続の再利用を行い(5秒後に使っていないコネクションを切断)、負荷を緩くしています。
- proxyに"disablereuse"は設定せず(つまり、デフォルトの"disablereuse=Off")。
- proxyの"retry"は少し緩める(10秒程度でttlより多め)。
- proxyに"smax=0″を設定します。
- 接続数の Soft Maximum (ソフトリミット)まで、 コネクションは必要に応じて生成されます。 smax を超えた数のコネクションは生存時間 ttl で切断されます。デフォルトは、maxで設定するHard Maximum (ハードリミット)と同一。
- proxyに"ttl=5″を設定します。
- smax数を超えた非活動状態のコネクションの生存時間を"秒"で指定します。この期間内に使用されなかったコネクションは、 全て切断されます。
- retry=10 smax=0 ttl=5
- さらにworker MPM のプロセス・スレッド制御の設定を変更します。mpm.confを編集し、MaxRequestsPerChildに適切な値を設定します("0″は子プロセスが処理できるリクエストの数が無制限となるため変更します)。子プロセスが起動している期間はIPアドレスを保持します。今回の事象を対処するため子プロセスは定期的に起動し直す必要があります(再起動のサイクルは少なくとも1日以内が良いと思いますが、ALBが代替後削除されるまでの猶予期間が不明なため保証できません)。また、「MaxRequestsPerChild」の値はリクエストの件数やサーバーの処理能力にも関わるため、システム毎に検討してください。
- その他、proxy設定のオプションは、こちらのドキュメントを参照。
Apacheのproxy設定に回避策を追加する
- proxyのリダイレクト先に回避策として、パターン1で紹介した"disablereuse=On"と"retry=0″オプションを追加します。状況に応じて、パターン2 に読み替えて下さい。
$ cat www.example.com.conf
<VirtualHost *:80>
ServerName www.example.com
ProxyPass / http://backend.example.com/ retry=0 disablereuse=On
ProxyPassReverse / http://backend.example.com/
ProxyRequests Off
ProxyPreserveHost Off
</VirtualHost>
- 上記回避策を追加した後、別名の値をALB2 のDNS名に切り替え、クライアントからcurlを実行すると、新しいALB(ALB2 のDNS名)へアクセスが行われました。ALB2はターゲットが1つもないターゲットグループを割り当てているため、期待通り503のエラーを返しています。
niikawa@niikawa1:~$ curl -vv http://www.example.com
* Rebuilt URL to: http://www.example.com/
* Trying XX.XX.XX.XX...
* TCP_NODELAY set
* Connected to www.example.com (XX.XX.XX.XX) port 80 (#0)
> GET / HTTP/1.1
> Host: www.example.com
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 503 Service Temporarily Unavailable
< Date: Tue, 07 Jan 2020 02:42:40 GMT
< Server: awselb/2.0
< Content-Type: text/html; charset=UTF-8
< Content-Length: 178
< Connection: close
<
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body bgcolor="white">
<center><h1>503 Service Temporarily Unavailable</h1></center>
</body>
</html>
* Closing connection 0