Apache-Tomcat連携モジュールmod_jk/mod_proxy_ajp/mod_proxy_httpそれぞれについて接続が切れた時の挙動を比較・調査してみた

Apache-Tomcat間の連携モジュールとしてはmod_jk、mod_proxy_ajp、mod_proxy_httpの3つがありますが、それぞれについてApache-Tomcat間の接続が切れた時の挙動や、設定によりどこまでその挙動を変更できるかの観点より、以下4点について調査、検証してみました。

* 接続プーリング状態(デフォルト)で、Apacheを停止したり、Tomcatを停止したら接続はどうなるか？
* CLOSE_WAIT状態で滞留するApache側の接続を設定によりクローズさせることが可能か？
* プーリングされたApache側の接続がTCPハーフオープン状態になった時の挙動と、設定によりその接続をクローズさせることが可能か？
* 設定により接続をプーリングせず、リクエスト毎に接続・切断させることが可能か？

# 検証した環境

|プロダクト|バージョン|備考|
|---|---|---|
|CentOS|7.2||
|Apache|2.4.6|event MPMを使用<br/>mod_proxy_ajp検証時はmod_proxy_httpはLoadModuleしない設定にして検証|
|Tomcat|8.5.14|HTTPはlocalhost:8080、AJPはlocalhost:8009でLISTEN|
|mod_jk|1.2.42|

# 検証結果

## 接続プーリング状態(デフォルト)で、Apacheを停止したり、Tomcatを停止したら接続はどうなるか？

mod_jkもmod_proxy_ajpもmod_proxy_httpもデフォルトで接続プーリングを行いますので、接続が確立された状態でApacheやTomcatの一方のみを停止した時に、プールされている接続がどうなるか検証しました。
ただし、Tomcatのデフォルト設定が、AJPコネクタはアイドルタイムアウトなしとなっているのに対し、HTTPコネクタは20秒でアイドルタイムアウトする設定となっているため、HTTPコネクタのconnectionTimeout値を-1(タイムアウトなし)に変更して検証しました。

### mod_jkの場合

* Apache停止時
    Apache側からFINパケットが送信され、Apache側もTomcat側も正常にクローズされました。kill -9した場合も同じでした。

* Tomcat停止時
    Tomcat側からFINパケットが送信されますがApache側がFINを送らず切ろうとしません。そのため、Tomcat側は/proc/sys/net/ipv4/tcp_fin_timeout(デフォルト60秒)までFIN_WAIT2状態となり、その後クローズされますが、Apache側はCLOSE_WAIT状態で残ります。kill -9した場合も同じでした。
ただし、この状態でTomcatを起動し、再度リクエストを投げると、新規のApacheのプロセスが起動されてリクエストを処理する場合もあれば、CLOSE_WAIT状態の接続を保持するApacheプロセスがリクエストを処理する場合もあり、後者の場合でもCLOSE_WAIT状態の接続は破棄されて、再接続され、正常にレスポンスが返されました。

### mod_proxy_ajpの場合

mod_jkと同じ挙動となりました。

### mod_proxy_httpの場合

mod_jkと同じ挙動となりました。

## CLOSE_WAIT状態で滞留するApache側の接続を設定によりクローズさせることが可能か？

前項の通り、Tomcat停止時にApache側にCLOSE_WAIT状態の接続が残るため、これをクローズさせる設定があるか調査しました。

### mod_jkの場合

以下の2つの方法があります。

* JkWatchdogIntervalを設定し、かつping_mode=Aを設定することで、ウォッチドッグスレッドによりCPINGプローブパケットを使った接続の有効性チェックが行われ、CLOSE_WAIT状態の接続はクローズされました。複数のApacheプロセスでCLOSE_WAIT状態の接続があった場合てもすべて破棄されました。なお、ドキュメント(https://tomcat.apache.org/connectors-doc/reference/workers.html) によると、ping_mode=Aの代わりに、ping_mode=Iを設定するかまたはconnection_ping_intervalに0より大きい値を設定することでも有効性チェックが行われるようです。
    
    例.
    
    ```apache:httpd.conf
    JkWatchdogInterval 60
    ```
    
    ```properties:workers.properties
    worker.hoge.ping_mode=A
    ```

* socket_keepalive=trueを設定し、OSのカーネルパラメータのTCPキープアライブ周りの設定値を調整することで、OSによりTCPキープアライブプローブパケットを使った接続の有効性チェックが行われ、CLOSE_WAIT状態の接続はクローズされました。複数のApacheプロセスでCLOSE_WAIT状態の接続があった場合てもすべて破棄されました。(カーネルパラメータの変更をしなくても時間が経過すればクローズされると思いますが、デフォルトでは以下の通りの設定となっていますのでCLOSE_WAIT状態の接続の滞留を短時間に留めるためには変更する必要があります)
    
    ```properties:workers.properties
    worker.hoge.socket_keepalive=true
    ```

    ```:/etc/sysctl.conf
    net.ipv4.tcp_keepalive_time = プローブパケット送信間隔秒数(デフォルトは7200秒)
    net.ipv4.tcp_keepalive_intvl = プローブパケット送信で応答がない場合の再送間隔秒数(デフォルトは75秒)
    net.ipv4.tcp_keepalive_probes = プローブパケット送信回数(デフォルトは9回)
    ```

### mod_proxy_ajpの場合

ProxyPassディレクティブのkeepaliveパラメータにonを設定し、上記と同様にカーネルパラメータのTCPキープアライブ周りを調整することで、全てのCLOSE_WAIT状態の接続がクローズされました。

例.

```apache:httpd.conf
ProxyPass / ajp://localhost:8009/ keepalive=on
```

### mod_proxy_httpの場合

mod_proxy_ajpと同じでした。

## プーリングされたApache側の接続がTCPハーフオープン状態になった時の挙動と、設定によりその接続をクローズさせることが可能か？

前項にてハーフクローズ時の挙動とそれをクローズさせる設定について確認しましたが、ハーフオープン時はどうなるのか調査しました。

調査手順

```shell-session
# ループバックインターフェースの停止
ip link set lo down

# Tomcatをkill
kill -9 TomcatのPID #この時点でApacheのみがESTABLISHEDの接続をもっていて、それに対応するTomcat側の接続は存在しない状態になる

# Tomcatを起動
$CATALINA_HOME/bin/startup.sh

# ループバックインターフェースの起動
ip link set lo up
```

この後に、リクエストを投げて挙動を確認しました。

### mod_jkの場合

ハーフオープン状態の接続を持つApacheプロセスがリクエストを処理する場合、その接続はクローズされ、再接続されて、正常にレスポンスが返されました。
mod_jkのログには以下のような出力がありました。

```
[info] ajp_connection_tcp_get_message::jk_ajp_common.c (1350): (tomcat) can't receive the response header message from tomcat, network problems or tomcat (127.0.0.1:8009) is down (errno=104)
[error] ajp_get_reply::jk_ajp_common.c (2259): (tomcat) Tomcat is down or refused connection. No response has been sent to the client (yet)
[info] ajp_service::jk_ajp_common.c (2778): (tomcat) sending request to tomcat failed (recoverable),  (attempt=1)
```

ここで、以下のようにretriesに1(デフォルトは2)を設定したところ、ループバックインターフェース起動後に行ったリクエストがハーフオープン接続をもつApacheプロセスに処理されたときは502エラーが返されるように変わりました。

```properties:workers.properties
worker.hoge.retries=1
```

つまり、mod_jkはデフォルト設定で、ハーフオープン状態の接続でもエラーを検知し、retriesのデフォルトにより1回だけ再接続を行うようになっているように思われます。
また、前項のCLOSE_WAIT状態の接続をクローズする設定をすることで、ハーフオープン状態の接続についてもクローズさせることができました。

### mod_proxy_ajpの場合

mod_jkの場合と同様に、ハーフオープン状態の接続を持つApacheプロセスがリクエストを処理する場合、その接続はクローズされ、再接続されて、正常にレスポンスが返されました。
また、前項のCLOSE_WAIT状態の接続をクローズする設定をすることで、ハーフオープン状態の接続についてもクローズさせることができました。

### mod_proxy_httpの場合

リクエストを、新規のApacheプロセスが処理する場合は正常にレスポンスが返されましたが、ハーフオープン状態の接続をもつプロセスが処理する場合は502エラーが返されました。ハーフオープン状態の接続は残ったままでした。
Apacheエラーログには以下の出力がありました。

```
[Sun Apr 30 15:00:16.087835 2017] [proxy_http:error] [pid 3251:tid 140424593192704] (104)Connection reset by peer: [client 127.0.0.1:34190] AH01102: error reading status line from remote server localhost:8080
[Sun Apr 30 15:00:16.087959 2017] [proxy:error] [pid 3251:tid 140424593192704] [client 127.0.0.1:34190] AH00898: Error reading from remote server returned by /
```

ProxyPassディレクティブのpingパラメータを設定すると同じく502エラーが返されるものの、RSTパケットを受信し、ハーフオープン状態の接続はクローズされました。
さらに、ttlパラメータを設定することで、ハーフオープン状態の接続をもつApacheプロセスがリクエストを処理した場合でも接続がクローズ・再接続され、正常にレスポンスが返されるようになりました。

例.

```apache:httpd.conf
ProxyPass / http://localhost:8080/ ping=3 smax=0 ttl=20
```

ただし、ttlに大きな値を設定し、ttlが切れるまでの間にハーフオープン状態の接続をもつApacheプロセスがリクエストを処理すると、やはり502エラーが返されました。
ttlを短くすることでエラーが発生する可能性を小さくすることは可能ですが、ハーフオープン状態の接続が使われてしまう可能性をゼロにはできないため、502エラーを完全に回避することは不可能と思います。
また、前項のCLOSE_WAIT状態の接続をクローズする設定をすることで、ハーフオープン状態の接続についてもクローズさせることができました。

#### 補足. Tomcat->Apacheの接続がハーフオープン状態になった場合について

逆にループバックインターフェース停止中にApacheを停止させた場合は、Tomcat->Apacheの接続がハーフオープン状態となります。Tomcatの設定がデフォルトの場合、AJP接続(mod_jk/mod_proxy_ajp)ではハーフオープン接続は使用されないまま滞留し続けますが、HTTP接続(mod_proxy_http)ではデフォルトでconnectionTimeoutに20秒が設定されているため20秒経過後クローズされます。
ハーフオープン接続が大量に滞留した場合、Tomcat側の最大スレッド数(maxThreads。デフォルトは200)に達してしまい、パフォーマンス悪化や応答停止に陥る可能性もあるため、AJP接続の場合でもconnectionTimeoutは設定した方がよいと思います。
なお、connectionTimeoutの値はApache側の接続プールアイドルタイムアウト値(mod_jkならconnection_pool_timeout、mod_proxy_ajp/mod_proxy_httpならProxyPassのttl)と合わせるべきです(そうでないと、Apache->Tomcatの接続がCLOSE_WAITで残ったりするため)。
※ただし、mod_jkではウォッチドッグスレッドによるタイマー式の接続クローズが可能なのに対し、ProxyPassのttlでは [Apache ProxyPassディレクティブのttlパラメータ設定時の挙動について](http://qiita.com/i_learnin/items/b1c15bdf9210597f0a71) に書いた通り、リクエストが来てその接続を持つApacheプロセスが受け付けた際にしかクローズしないためttlの値をconnectionTimeoutと合わせていてもCLOSE_WAITが滞留する可能性は高いと思います。

## 設定により接続をプーリングせず、リクエスト毎に接続・切断させることが可能か？

最後に、接続プーリングを行わない設定が可能か調査しました。

### mod_jkの場合

以下の設定でリクエストの度に、接続・切断されるようになりました。curl http://localhost/ http://localhost/ のようにHTTP Keep-Aliveなリクエストの場合でもリクエスト毎に接続、切断されました。

```apache:httpd.conf
JKOptions +DisableReuse
```

### mod_proxy_ajpの場合

以下2つのいずれかの設定でリクエスト毎に、接続・切断されるようになりました。

* ProxyPassディレクティブのdisablereuseパラメータにonを設定する

    例.

    ```apache:httpd.conf
    ProxyPass / ajp://localhost:8009/ disablereuse=on
    ```

* 環境変数proxy-nokeepaliveを有効にする
  (マニュアル上はmod_proxy_httpに記載がある設定 (https://httpd.apache.org/docs/2.4/mod/mod_proxy_http.html#env) ですが効きました)

    ```apache:httpd.conf
    SetEnv proxy-nokeepalive 1
    ```

なお、以下はどちらも効果がありませんでした。

```apache:httpd.conf
SetEnv force-proxy-request-1.0 1
SetEnv proxy-initial-not-pooled 1
```

### mod_proxy_httpの場合

以下3つのいずれかの設定でリクエスト毎に、接続・切断されるようになりました。

* ProxyPassディレクティブのdisablereuseパラメータにonを設定する
  (HTTPリクエストヘッダはConnection: Keep-Aliveとなっていましたが切断されました。curl http://localhost/ http://localhost/ のようにHTTP Keep-Aliveなリクエストの場合でもリクエスト毎に接続、切断されました)

    例.

    ```apache:httpd.conf
    ProxyPass / http://localhost:8080/ disablereuse=on
    ```

* 環境変数proxy-nokeepaliveを有効にする
  (HTTPリクエストヘッダにConnection: closeが設定されました)

    ```apache:httpd.conf
    SetEnv proxy-nokeepalive 1
    ```

* 環境変数force-proxy-request-1.0を有効にする
  (HTTP/1.0が使用されるようになりました。HTTPリクエストヘッダにConnection:は設定されませんでした)

    ```apache:httpd.conf
    SetEnv force-proxy-request-1.0 1
    ```

なお、以下を設定した場合、リクエストを処理したApacheプロセスの保持している接続のうちの1本がクローズ(それがESTABLISHEDであっても)され、新規接続されました。なお、その接続はレスポンスを返した後も切断されませんでした。

```apache:httpd.conf
SetEnv proxy-initial-not-pooled 1
```

ただし、この設定は https://httpd.apache.org/docs/2.4/mod/mod_proxy_http.html#env によると、バックエンドへの接続チェック後に、Apacheからバックエンドへリクエストを送信して到達する前に接続がクローズされて502エラーが発生することを回避するために、クライアントの初期リクエスト時にはプールされた接続を使わないようにするものであり、実際に curl http://localhost/ http://localhost/ で試してみると、マニュアルに記載の通り、HTTP Keep-Aliveなリクエスト(2回目のリクエスト)の場合は1回目と同一ポート、つまりプールした接続が使われました。

# 検証結果まとめ

|#|検証ポイント|mod_jk|mod_proxy_ajp|mod_proxy_http|
|---|---|---|---|---|
|1|接続プーリング状態(デフォルト)で、Apacheを停止したり、Tomcatを停止したら接続はどうなるか？|Apache停止時は両側とも正常にクローズされる。<br/>Tomcat停止時はApache->Tomcatの接続がCLOSE_WAIT状態で滞留する|mod_jkと同じ|mod_jkと同じ|
|2|プーリングされたApache側の接続がCLOSE_WAIT状態になった時の挙動|CLOSE_WAIT状態の接続を持つApacheプロセスがリクエストを処理する時にその接続はクローズ、再接続され、正常にレスポンスが返される。|mod_jkと同じ|mod_jkと同じ|
|3|CLOSE_WAIT状態で滞留するApache側の接続を設定によりクローズさせることが可能か？|可能。mod_jkの設定のみで可能|可能。短時間でクローズさせるにはOSのTCPキープアライブ設定の調整も必要|mod_proxy_ajpと同じ|
|4|プーリングされたApache側の接続がTCPハーフオープン状態になった時の挙動|ハーフオープン状態の接続を持つApacheプロセスがリクエストを処理する時にその接続はクローズ、再接続され、正常にレスポンスが返される。|mod_jkと同じ|ハーフオープン状態の接続を持つApacheプロセスがリクエストを処理した場合、クライアントに502エラーが返される。ProxyPassのttlパラメータの設定もしくはkeepaliveパラメータの設定 + OSのTCPキープアライブ設定の調整により、ハーフオープン状態の接続を短時間でクローズさせることは可能だが、それらの設定値によりプローブパケットが送信されてクローズされるよりも先にその接続が使用されてしまう可能性をなくすことはできないため、502エラーを完全に回避することはできない|
|5|ハーフオープン状態で滞留するApache側の接続を設定によりクローズさせることが可能か？|#3と同じ|#3と同じ|#3と同じ|
|6|設定により接続をプーリングせず、リクエスト毎に接続・切断させることが可能か？|可能|可能|可能|

# 考察

結果を見る限りでは、mod_jkがもっともベターであり、アイドル時切断ありのプーリング＋不正状態となった接続を自動クローズさせる設定として、少なくとも以下の設定は入れておくのがよさそうに思います。

```apache:httpd.conf
#実際にプールされた接続のチェックが行われるのは下のworker.maintainの秒数を経過している場合なので、
#worker.maintainよりもあまりに小さい値を設定しても意味がない。
JkWatchdogInterval ウォッチドッグスレッドによる監視間隔秒数
```

```properties:workers.properties
worker.hoge.ping_mode=AまたはI

#Tomcatのserver.xmlのConnectorタグのconnectionTimeoutの値(mod_jkは秒で、こっちはミリ秒と単位が異なることに注意)と同じ時間になるようにすること。
worker.hoge.connection_pool_timeout=アイドルタイムアウト秒数
```

さらにプールされた接続のチェック間隔を調整するため、必要に応じて以下も検討するといいかと思います。

```properties:workers.properties
worker.maintain=接続プールやロードバランサのメンテナンス間隔秒数(デフォルトは60秒)

worker.hoge.connection_ping_interval=プールされた接続がこの秒数より長くアイドルしていたらCPINGを送信して接続が活きているかチェックするためのしきい値となる秒数(デフォルトは100秒((ping_timeout/1000)*10))
```

ただ、実際にはこの検証した環境のようにApacheとTomcatを同一サーバ上で運用することも多いのではないかと思います。その場合はApache-Tomcat間の接続がハーフオープン状態になる可能性は少なくなるのかもしれません。また、Apache->Tomcat接続のCLOSE_WAITが滞留しても多少のリソースは食うかもしれませんが、CLOSE_WAIT状態の接続をもつApacheプロセスがリクエストを処理する際にクローズ、再接続されるため、大きな実害はないのかもしれません。

これ以外の接続が切れた場合の対応方法として、最後に検証したようにリクエスト毎に接続・切断させるというものもありますが、接続をプーリングするのに比べてパフォーマンス面で不利になると思います。また、頻繁に接続・切断を行うことでTIME_WAIT状態の接続によるエフェメラルポート枯渇にも注意する必要があります。