ALB のトラブルシューティング&チューニング


# ALB のトラブルシューティング&チューニング

AWSのApplication Load Balancer(ALB)を使用している際のトラブルシューティングに関する事項をまとめる。
HTTPの仕様上、4XX系や5XXは基本的にエラーのため、各種エラーコードがどういった場合に再現するか、原因は何かに関して検証してみる。
以下マニュアルに記載のあるNGケースをベースに実際に想定のエラーになるか検証してみる。
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html

# 検証構成例

以下の構成で Clinet PC から curl コマンドで検証してみる。ELBは、ALBをいずれも利用する。

```
Client PC ===> ALB ===> Nginx
```


# 1.各HTTPステータスコード基準で検証

あくまで各HTTPステータスコードが発生する事象を知っているベースで各LBについて検証してみる。
ちなみにレスポンスヘッダーにserver: nginxが入っている場合は、LBは少なくとも通過できているのでLBのエラーではなく後段のNginxが返すエラー。

## HTTP 400: BAD_REQUEST

`考えられる原因:`

- クライアントが HTTP 仕様を満たさない誤った形式のリクエストを送信した。
- リクエストヘッダーが、リクエスト行あたり 16K、1 つのヘッダーあたり 16K、またはヘッダー全体に対して 64K を超えている。

(1) 例として、Host ヘッダー無し状態でリクエスト
* <strong>Hostヘッダーは、HTTP/1.1 で唯一の必須ヘッダ</strong>

```ruby:想定通りエラー発生
[root@centos8 ~]# curl -I -H 'Host: ' http://alb01/index.php
HTTP/1.1 400 Bad Request
Server: awselb/2.0
Date: Sun, 22 Mar 2020 11:49:06 GMT
Content-Type: text/html
Content-Length: 138
Connection: close
```

(2) 例として、1ヘッダーあたりのサイズ上限超えてリクエスト

```ruby:想定通りエラー発生
[root@centos8 ~]# test=`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20000 | head -1`
[root@centos8 ~]# curl -I -H "TEST: $test" http://alb01/index.php 
HTTP/1.1 400 Bad Request
Server: awselb/2.0
Date: Sat, 21 Mar 2020 13:27:58 GMT
Content-Type: text/html
Content-Length: 214
Connection: close
```

##HTTP 403: Forbidden

`Application Load Balancer へのリクエストをモニタリングするよう AWS WAF ウェブアクセスコントロールリスト (ウェブ ACL) を設定し、リクエストがブロックされました。`

* WAF でアクセスを BLOCK した上で実施する。
* ちなみに WAF は、ELB の中では現時点では、ALB にしか紐付けられない。

```ruby:想定通りエラー発生
[root@centos8 ~]# curl -I http://alb01/index.php 
]HTTP/1.1 403 Forbidden
Server: awselb/2.0
Date: Sat, 21 Mar 2020 13:47:22 GMT
Content-Type: text/html
Content-Length: 134
Connection: keep-alive
```

##HTTP 405: METHOD_NOT_ALLOWED

`クライアントが、Application Load Balancer でサポートされていない TRACE メソッドを使用しました。`


```ruby:想定通りエラー発生
[root@centos8 ~]# curl -I -X TRACE http://alb01/index.php 
HTTP/1.1 405 Not Allowed
Server: awselb/2.0
Date: Sat, 21 Mar 2020 13:33:41 GMT
Content-Type: text/html
Content-Length: 138
Connection: close
```

##HTTP 408: Request Timeout

`アイドルタイムアウト期間の期限が切れる前に、クライアントからデータが送信されませんでした。TCP キープアライブを送信しても、このタイムアウトを防ぐことはできません。各アイドルタイムアウト期間が経過する前に、1 バイト以上のデータを送信します。必要に応じて、アイドルタイムアウト期間を長くします。`

ELBのアイドルタイムアウトが過ぎても何もデータが送信されなかった場合にALBがエラーを返す。


どうやって発生させるのか。。。検証未定



##HTTP 413: Payload Too Large

`ターゲットは Lambda 関数で、リクエストボディが 1 MB を超えています。`

Content-Lengthを適当にかなり大きなサイズにしてELBではなく、後段のNginx側で起こしてみる。
その他だとNginx側で許容されているリクエストボディサイズを超えたファイルをアップロードする時に起こりうる

```ruby:想定通りエラー発生
[root@centos8 ~]# curl -H "Content-Length: 100000000000" -I http://alb01/ 
HTTP/1.1 413 Request Entity Too Large
Date: Sun, 22 Mar 2020 06:14:24 GMT
Content-Type: text/html
Content-Length: 176
Connection: keep-alive
Server: nginx
```

##HTTP 414: URI Too Long

`リクエストの URL またはクエリ文字列パラメータが大きすぎます。`

一定のURL文字列数以上でELB自体からエラーとして返される。（文字数がいくつかまでは未検証）

```ruby:想定通りエラー発生
[root@centos8 ~]#  test=`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 100000 | head -1`
[root@centos8 ~]# curl -I http://alb01/?test=${test}
HTTP/1.1 414 Request-URI Too Large
Server: awselb/2.0
Date: Sun, 22 Mar 2020 06:33:12 GMT
Content-Type: text/html
Content-Length: 158
Connection: close
```

##HTTP 500: 内部サーバーエラー

`考えられる原因:`

* AWS WAF のウェブアクセスコントロールリスト (ウェブ ACL) を設定し、ウェブ ACL ルールの実行でエラーが発生しました。
* ユーザーを認証するようリスナールールを設定しましたが、以下のいずれかが該当しません。
    * ロードバランサーは、IdP トークンのエンドポイントまたは IdP ユーザー情報エンドポイントと通信できません。ロードバランサーのセキュリティグループおよび VPC のネットワーク ACL がこれらのエンドポイントに対するアウトバウンドアクセスを許可していることを検証します。VPC がインターネット接続されていることを確認します。内部向けロードバランサーがある場合は、NAT ゲートウェイを使用してインターネットアクセスを有効にします。
    * IdP によって返されるクレームのサイズが、ロードバランサーによってサポートされる最大サイズを超えています。
    * クライアントがホストヘッダーなしで HTTP/1.0 リクエストを送信し、ロードバランサーはリダイレクト URL を生成できませんでした。
    * クライアントが HTTP プロトコルなしでリクエストを送信し、ロードバランサーはリダイレクト URL を生成できませんでした。
    * リクエストされたスコープで ID トークンが返さません。

ELBで発生させるのはなかなか手間がかかりそうなため後段のNginx側で発生させる。
NginxのupstreamにあるPHPプログラムで意図的な構文エラーを仕掛けて実施。

```ruby:想定通りエラー発生
# @Webサーバ
## ホスト名表示関数の ) を削除して不正な構文にする。
<?php
    echo "[Hostname: ";
    echo gethostname(;
    echo "]";
    echo "<br />";
?>

# @Client PC
[root@centos8 ~]# curl -I http://alb01/error.php
HTTP/1.1 500 Internal Server Error
Date: Sun, 22 Mar 2020 11:56:57 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Server: nginx
X-Powered-By: PHP/7.4.4
```

##HTTP 502: Bad Gateway

`考えられる原因:`

- 接続の確立を試みているときに、ロードバランサーがターゲットから TCP RST を受信した。
- 接続の確立を試みているときに、ロードバランサーがターゲットから予期しないレスポンスを受信した (例: 「ICMP Destination unreachable (Host unreachable) (ICMP 送信先に到達できません (ホストに到達できません))」など)。ターゲットポートでロードバランサーサブネットからターゲットへのトラフィックが許可されているかどうかを確認します。
- ロードバランサーにターゲットへの未処理のリクエストがあるときに、ターゲットが TCP RST または TCP FIN との接続を閉じた。ターゲットのキープアライブ期間がロードバランサーのアイドルタイムアウト値よりも短いことを確認します。
- ターゲットのレスポンス形式が正しくないか、有効でない HTTP ヘッダーが含まれている。
- ロードバランサーがターゲットに接続するときに、SSL ハンドシェイクエラーまたは SSL ハンドシェイクタイムアウト (10 秒) が発生しました。
- 登録解除されたターゲットによって処理されていたリクエストで登録解除の遅延期間が経過しました。時間のかかるオペレーションが完了できるように、遅延期間を増やします。
- ターゲットは Lambda 関数で、レスポンスボディが 1 MB を超えています。
- ターゲットは、設定されたタイムアウトに達する前に応答しなかった Lambda 関数です。


(1) 例として、WebサーバをOSダウンさせて試す。

```ruby:想定通りエラー発生
[root@centos8 ~]# curl -I  http://alb01/index.php
HTTP/1.1 502 Bad Gateway
Server: awselb/2.0
Date: Sat, 21 Mar 2020 14:57:05 GMT
Content-Type: text/html
Content-Length: 138
Connection: keep-alive

```

(2) 例として、Webサーバ(Nginx)をプロセスダウンさせて試す。

```ruby:想定通りエラー発生
[root@centos8 ~]# curl -I  http://alb01/index.php
HTTP/1.1 502 Bad Gateway
Server: awselb/2.0
Date: Sat, 21 Mar 2020 15:02:06 GMT
Content-Type: text/html
Content-Length: 138
Connection: keep-alive
```

##HTTP 503: Service Unavailable

`ロードバランサーのターゲットグループに登録済みターゲットがありません。`

ターゲットグループから全てのホストを登録解除して実施

```ruby:想定通りエラー発生
[root@centos8 ~]# curl -I  http://alb01/index.php
HTTP/1.1 503 Service Temporarily Unavailable
Server: awselb/2.0
Date: Sat, 21 Mar 2020 15:06:27 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive

```

##HTTP 504: Gateway Timeout

もっともよくありがちなサーバ側エラー。

`考えられる原因:`

- ロードバランサーは、接続タイムアウトが期限切れになる (10 秒) 前にターゲットへの接続の確立に失敗した。
- ロードバランサーはターゲットへの接続を確立したが、アイドルタイムアウト期間が経過する前にターゲットが応答しなかった。
- サブネットのネットワーク ACL で、ターゲットから一時ポート (1024-65535) のロードバランサーノードへのトラフィックが許可されなかった。
- ターゲットがエンティティ本文より大きな Content-Length ヘッダーを返した。ロードバランサーが欠落しているバイトを待機してタイムアウトした。
- ターゲットは Lambda 関数であり、接続タイムアウトが期限切れになる前に Lambda サービスが応答しませんでした。

もっともよくありがちなサーバ側エラー。
https://sigopt.com/blog/the-case-of-the-mysterious-aws-elb-504-errors/

(1) 例として、Webサーバ(Nginx)をHangさせて実施。アイドルタイムアウト経過後以下エラー。

```ruby:想定通りエラー発生

#@webサーバ
pkill -19 nginx

#@Client PC
[root@centos8 ~]# curl -I http://alb01/index.php
HTTP/1.1 504 Gateway Time-out
Server: awselb/2.0
Date: Sat, 21 Mar 2020 15:20:24 GMT
Content-Type: text/html
Content-Length: 148
Connection: keep-alive

```

(2) 例として、Webサーバ(Nginx)のリクエスト転送先が長時間レスポンスを返さない場合で実施。アイドルタイムアウト経過後以下エラー

```ruby:想定通りエラー発生
#@webサーバ
00:22:33[root@web1 ~]### cat /appl/html1/sleep.php 
<html>
<head><title>hello</title/></head>
<body>
<?php
    echo "[Hostname: ";
    echo gethostname();
    echo "]";
    echo "<br />";
    sleep(100)
?>

#@Client PC
HTTP/1.1 504 Gateway Time-out
Server: awselb/2.0
Date: Sat, 21 Mar 2020 15:23:05 GMT
Content-Type: text/html
Content-Length: 148
Connection: keep-alive
```


###補足：NginxのHTTPステータスコード499

Nginx は特にHTTP標準にも書かれていない独自のエラーコード499を持つ。

- nginxがリクエストを処理している間にクライアントがコネクションを切ったときのためのもの
- nginxがHTTPヘッダをクライアントに送信する前に、クライアントがコネクションを切断済みであれば、コード499をログに記録する

上記、504 エラーの上記 (2) のようにNginxがupstreamへリクエストを転送している最中に、クライアント側(ここではELB)がコネクションを切った場合はこの状態に当てはまる。少なくともクライアント側とのコネクションが切れているため、Nginx はレスポンスをクライアント側には返すことができず挙動としてはアクセスログにその旨記録するという形になる。

```ruby:アクセスログ
{"remote_addr": "xx.xx.xx.xxx", "loadbalancer_addr": "10.10.1.10", "time": "22/Mar/2020:16:50:17 +0900", "request":  "GET /sleep.php HTTP/1.1", "status": "499", "request_time": 61.000, "upstream_response_time": 61.000, "body_bytes_sent": 0, "http_referer": "-", "user_agent": "curl/7.61.1", "authorization": "-"}
```

`参考`
https://github.com/yuuki/yuuki/blob/master/misc/nginx-status-499.md
https://blog.tonbiworks.com/tech/2017/01/22/http-status-code/



#2.ELBに対して設定すべき適切なタイムアウト値

以下によるとELB配下でNginxまたはApacheを使う場合に設定すべき適切なタイムアウト値が定義されている。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/apache-backend-elb/

## リクエストヘッダータイムアウト

```
クライアントヘッダータイムアウト (Apache の Timeout、NGINX の client_header_timeout)
ロードバランサーでアイドル接続を適切に切断できるように、アプリケーションのアイドルタイムアウトは、ロードバランサーに設定されたアイドルタイムアウトよりも大きな値に設定します。ロードバランサーに適切に通知せずに、バックエンドサーバーで接続を終了すると、504 エラーが表示されることがあります。
```
===> ELB のアイドルタイムアウトよりも値を大きくすることが必要。
===> 各MWのパラメータの意味詳細

* <strong>Nginx</strong>
    * client_header_timeout
        * Default=60s。Defines a timeout for reading client request header. If a client does not transmit the entire header within this time, the request is terminated with the 408 (Request Time-out) error.
 

## キープアライブ有効化
```
キープアライブ (Apache の KeepAlive、NGINX の keepalive_disable)
CPU 使用率を削減し、応答時間を改善するには、キープアライブをオンにします。キープアライブをオンにすると、ロードバランサーで、HTTP リクエストの度に新しい TCP 接続を確立する必要がありません。
```
=====> keepaliveはOnにすることを推奨。
===> 各MWのパラメータの意味詳細

* <strong>Nginx</strong>
    * keepalive_disable
        * Default=msie。Disables keep-alive connections with misbehaving browsers. The browser parameters specify which browsers will be affected. The value msie6 disables keep-alive connections with old versions of MSIE, once a POST request is received. The value safari disables keep-alive connections with Safari and Safari-like browsers on macOS and macOS-like operating systems. The value none enables keep-alive connections with all browsers.
        
## キープアライブタイムアウト
```
キープアライブタイムアウト (Apache の KeepAliveTimeout、NGINX の keepalive_timeout)
キープアライブオプションが有効になっている場合は、アプリケーションタイムアウトよりも長いキープアライブタイムアウトを選択します。
```
===> ELB のアイドルタイムアウトよりも値を大きくすることが推奨。
===> 各MWのパラメータの意味詳細

* <strong>Nginx</strong>
    * keepalive_timeout
        * Default=75s。The first parameter sets a timeout during which a keep-alive client connection will stay open on the server side. The zero value disables keep-alive client connections. The optional second parameter sets a value in the “Keep-Alive: timeout=time” response header field. Two parameters may differ. The “Keep-Alive: timeout=time” header field is recognized by Mozilla and Konqueror. MSIE closes keep-alive connections by itself in about 60 seconds.
        

## キープアライブリクエスト数
```
キープアライブリクエストの最大数 (Apache の MaxKeepAliveRequests、NGINX の keepalive_requests)
このオプションでは、キープアライブがオンになった時に単一の TCP 接続で処理するリクエストの数を設定します。リソースの使用を最適化するために、キープアライブリクエストの最大数は 100 以上に設定します。
```
===> 100以上が推奨
===> 各MWのパラメータの意味詳細

* <strong>Nginx</strong>
    * keepalive_requests
        * Default=100。Sets the maximum number of requests that can be served through one keep-alive connection. After the maximum number of requests are made, the connection is closed.
Closing connections periodically is necessary to free per-connection memory allocations. Therefore, using too high maximum number of requests could result in excessive memory usage and not recommended.


##読み取りタイムアウト
```
読み取りタイムアウト (Apache の RequestReadTimeout、NGINX の client_header_timeout と client_body_timeout)
ロードバランサーがリクエストのヘッダーとボディの両方を受信するために接続を十分な時間開いたままにできるように、アプリケーションの応答時間に合わせた読み取りタイムアウトを設定します。

警告: ロードバランサーのアイドルタイムアウト値がバックエンドタイムアウトよりも小さいことを確認してください。
```
===> ELB のアイドルタイムアウトよりも値を大きくすることが推奨。
===> 各MWのパラメータの意味詳細

* <strong>Nginx</strong>
    * client_header_timeout
        * Default=60s。Defines a timeout for reading client request header. If a client does not transmit the entire header within this time, the request is terminated with the 408 (Request Time-out) error.
    * client_body_timeout
        * Default=60s。Defines a timeout for reading client request body. The timeout is set only for a period between two successive read operations, not for the transmission of the whole request body. If a client does not transmit anything within this time, the request is terminated with the 408 (Request Time-out) error.


`Nginxのマニュアル`
https://nginx.org/en/docs/http/ngx_http_core_module.html