Apacheリバースプロキシ+ALB構成の落とし穴

2020-04-03

概要

  • 今回は、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がいつまでも古いALB のIPアドレスにアクセスしていることが理由と判断しました。(こちらのテスト環境では、半日経過しても古いALB のIPアドレスへ接続を試みて、エラーとなっておりました) 但し、ALB のIPアドレスが変わると必ず発生するわけではなく、発生しない場合もあり、TCPのCLOSE_WAITセッションが残っているなどの状況によって発生可否が変わるかと推測していますが、これ以上のroot cause 調査はできておりません。
  • なお、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

  • パターン1はALBのIPアドレスが変わった時にバックエンド(ALB)に接続できない事象への対応です。
    • proxyに”disablereuse=On”を設定します。このオプションでは、ALBのIPアドレスを毎回引き直します(disablereuseはコネクションプーリングによるバックエンドへの接続を再利用するか否かの設定、デフォルトは”disablereuse=Off”)。
    • proxyに”retry=0″を設定します(バックエンドへの接続を再試行する間隔をタイムアウト(秒)で指定、デフォルトは60)。
    • retry=0 disablereuse=On
    • その他、proxy設定のオプションは、こちらのドキュメントを参照。

パターン2 (2020/3/27追加)

  • パターン1はコネクションプーリングによる再利用が無効となり、かつバックエンドへの接続も頻繁に試行されるため、リバプロの負荷が高くなります。ALBのIPアドレスが変わった場合への対処としては最適ですが、アクセスが多いシステムでは、適用できないケースが考えられます。
  • 以下、パターン2です。パターン1より、多少の接続の再利用を残して負荷を緩くしています。
    • proxyに”disablereuse”は設定せず(つまり、デフォルトの”disablereuse=Off”)。
    • proxyの”retry”は少し緩める(10秒程度でttlより多め)。
    • proxyに”ttl”を設定します(5秒程度)。接続できていないコネクションの生存期間です。5秒間は古いALBのIPアドレスを利用します。
    • retry=10 ttl=5
    • その他、proxy設定のオプションは、こちらのドキュメントを参照。
 

Apacheのproxy設定に回避策を追加する

  • proxyのリダイレクト先に回避策として、パターン1で紹介した”disablereuse=On”と”retry=0″オプションを追加します。

$ 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
 

参考資料