Apache ProxyPassディレクティブのttlパラメータ設定時の挙動について

https://httpd.apache.org/docs/2.4/en/mod/mod_proxy.html#proxypass のttlの説明では

>Time to live for inactive connections and associated connection pool entries, in seconds. Once reaching this limit, a connection will not be used again; it will be closed at some later time.

と、ttlを超えてアイドルしている接続は少し時間が経てば自動的にクローズされるみたいに読めますが、実際のところいつクローズされるのか検証してみました。

## 検証した環境

|OS|CentOS 7.2|
|---|---|
|Apache|2.4.6|
|バックエンド|Tomcat 8.5.14|

## ProxyPass の設定

```apache
ProxyPass / http://localhost:8080/ smax=0 ttl=20

# ttlの値(20秒)はTomcatのconnectionTimeoutの値(20000ミリ秒)と同じ
```

## 検証方法

Tomcat起動、Apache起動後、Apacheにリクエストを送ります。

```shell-session
netstat -anp | grep 8080

# 8080はTomcatがLISTENしているポート。
```

で、Apache-Tomcat間にESTABLISHEDな接続ができたことを確認し、しばらく放置した後、netstatで接続状態を確認します。

## 検証結果

### event MPMの場合

TomcatのconnectionTimeout設定により、Tomcat -> Apacheの接続はクローズされましたが、Apache -> Tomcatの接続はttl経過後もCLOSE_WAIT状態で残り続けました。
ただし、この状態で再度リクエストを送ると、Apacheの子プロセスが新規に生成されてリクエストを処理する場合もあれば、CLOSE_WAIT状態の接続を保持する既存の子プロセスがリクエストを処理する場合もあり、後者の場合、CLOSE_WAITの接続はクローズされて再接続され、正常にレスポンスが返されました。
また、その際にリクエストを処理する子プロセスが複数のCLOSE_WAIT状態の接続を保持していた場合、その子プロセスのもつCLOSE_WAIT状態の接続がすべてクローズされました。

>#### netstatによる確認結果
>しばらくアイドルさせた後、netstatで確認すると、apache -> Tomcatの接続がCLOSE_WAITで残っています。
>
>```shell-session
>tcp 1 0 127.0.0.1:34585 127.0.0.1:8080 CLOSE_WAIT 18098/httpd
>tcp 1 0 127.0.0.1:34575 127.0.0.1:8080 CLOSE_WAIT 17966/httpd
>tcp 1 0 127.0.0.1:34587 127.0.0.1:8080 CLOSE_WAIT 18098/httpd
>tcp 1 0 127.0.0.1:34579 127.0.0.1:8080 CLOSE_WAIT 17967/httpd
>```
>
>この状態でしばらく待っていてもCLOSE_WAITは消えません。ただし、ここにリクエストを送ると
>
>```shell-session
>tcp 1 0 127.0.0.1:34593 127.0.0.1:8080 ESTABLISHED 18098/httpd
>tcp 1 0 127.0.0.1:34575 127.0.0.1:8080 CLOSE_WAIT 17966/httpd
>tcp 1 0 127.0.0.1:34579 127.0.0.1:8080 CLOSE_WAIT 17967/httpd
>```
>
>のようになりました。PID=17966、17967のプロセスはそのままですが、18098は2接続あったのが両方ともなくなり、新たに1本接続されています(エフェメラルポートが34593に変わっています)。

一方、ttl設定なし(ProxyPass / http://localhost:8080/ のみ)で検証してみると、CLOSE_WAIT状態の接続が残る点や、その後のリクエスト時にクローズ後再接続されて正常にレスポンスが返される点は同じですが、複数のCLOSE_WAIT状態の接続を持つ子プロセスがリクエストを処理した場合でもクローズされたのはそのうちの1接続のみでした。

### profork MPMの場合

基本的にevent MPMの場合と同じ挙動となりました。
ただし、preforkの場合、1つのプロセスは1つの接続しか持ちませんので、1つのリクエストでクローズされるCLOSE_WAIT状態の接続も1本のみとなりました。
なお、この挙動はttl設定なし(ProxyPass / http://localhost:8080/ のみ)の場合でも同じでした。

## 考察

挙動を見る限り、ttlはタイマーのように自動的に接続をクローズするものではなく、Apacheの子プロセスがリクエストを受け付けた時にttlのチェックが行われ、そのタイミングでクローズされるような実装になっているように見えます。

以下、ttlのチェックと接続クローズが行われるタイミングについてApacheのソースコードを調査した結果です。

ttlを見ているのはここ。
https://github.com/apache/httpd/blob/64086b6ea9ebb33d61e9f5429b6bb215cf15aed9/modules/proxy/proxy_util.c#L1906

```c
        if (worker->s->hmax) {
            rv = apr_reslist_create(&(worker->cp->res),
                                    worker->s->min, worker->s->smax,
                                    worker->s->hmax, worker->s->ttl,
                                    connection_constructor, connection_destructor,
                                    worker, worker->cp->pool);

            apr_pool_cleanup_register(worker->cp->pool, (void *)worker,
                                      conn_pool_cleanup,
                                      apr_pool_cleanup_null);

            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00930)
                "initialized pool in child %" APR_PID_T_FMT " for (%s) min=%d max=%d smax=%d",
                 getpid(), worker->s->hostname, worker->s->min,
worker->s->hmax, worker->s->smax);
```

apr_reslist_createはapr-utilの関数であり、
https://apr.apache.org/docs/apr-util/1.5/group___a_p_r___util___r_l.html#ga608ebcddce542603a7f1d3cf51ae8d3c
では、引数ttlには以下の説明が記載されています。

>If non-zero, sets the maximum amount of time in microseconds an unused resource is valid. Any resource which has exceeded this time will be destroyed, either when encountered by apr_reslist_acquire() or during reslist maintenance.

apr_reslist_acquire関数に入った時、またはreslistのメンテナンスの間に、ttlを超過したリソースが破棄されるとなっています。

apr_reslist_acquire関数に入った時というのは、
apr_reslist_acquire関数のドキュメント
https://apr.apache.org/docs/apr-util/1.5/group___a_p_r___util___r_l.html#gadfbb592b31ff9215f45d3280636e41d7
に以下の説明が記載されています。

>Retrieve a resource from the list, creating a new one if necessary. If we have met our maximum number of resources, we will block until one becomes available. 

要するに、接続プールからの取得タイミングですね。

reslistのメンテナンスの間とは、
reslistは
https://apr.apache.org/docs/apr-util/1.5/group___a_p_r___util___r_l.html#ga608ebcddce542603a7f1d3cf51ae8d3c
に、引数reslistの説明として以下が記載されています。

>An address where the pointer to the new resource list will be stored. 

直訳すると、新しい接続が格納されるポインタアドレスとなります。

apr_reslist_createのコードを見ると
https://github.com/apache/apr/blob/c72aba54218d32c76464d92aebbf65818c66c806/util-misc/apr_reslist.c#L253
ttlが参照されている関数はapr_reslist_acquireとapr_reslist_maintain。つまり、ttlを超過した接続が破棄されるのは、この2つの関数どちらかが実行された時ということになります。

apr_reslist_acquireは上記の通り、mod_proxyのproxy_util.cから実行されます。
apr_reslist_maintainは、GitHubのapache/httpdで検索してもヒットしないので、Apacheからは実行されていなさそうです。
これは検証結果とも一致します。

## 結論

Apache(mod_proxy)でttlを超過した接続がクローズされるタイミングは、リクエストを処理する際に接続プールからの取得(apr_reslist_acquire関数呼び出し)を行う時のみ。
また、クローズされるのはリクエストを受け付けたApache子プロセスが保持しているすべての接続が対象となります(他のプロセスが保持している接続は対象外)。