- PHP 7.1.7
- Apache 2.4.10
- ApacheBench 2.3
- zend-expressive 2.0.3
- zend-expressive-skeleton 2.0.3
- zend-diactoros 1.4.0
- zend-stratigility 2.0.1
Docker で Apache+mod_php を実行して、
docker run --rm -p 8888:80 -v "$PWD:/var/www/html" php:apache
header
関数で HTTP/1.1
を指定するコードを配置して、
<?php header('HTTP/1.1 200 OK');
次のように ab
します。
ab -c 1 -n 1 http://localhost:8888/
すると、なんか異様なスコアになります(1回のリクエストに5秒もかかってる)。
Requests per second: 0.20 [#/sec] (mean) Time per request: 5011.751 [ms] (mean) Time per request: 5011.751 [ms] (mean, across all concurrent requests)
header
関数をコメントアウトしたり、
<?php //header('HTTP/1.1 200 OK');
HTTP/1.0
を指定したり、
<?php header('HTTP/1.0 200 OK');
Connection: close
を指定したり、
<?php header('HTTP/1.1 200 OK'); header('Connection: close');
Aapche Bench で KeepAlive を有効にすれば、
ab -k -c 1 -n 1 http://localhost:8888/
それっぽい結果になります。
Requests per second: 560.85 [#/sec] (mean) Time per request: 1.783 [ms] (mean) Time per request: 1.783 [ms] (mean, across all concurrent requests)
原因
Apache Bench はデフォで HTTP/1.0
なリクエストを送ります。なのでレスポンスの最後はサーバの方からクローズされることを期待します。
一方、php が header
関数で HTTP/1.1
を指定すると、Apache は元のリクエストが 1.0 だったとしても 1.1 を応答し、しかも KeepAlive On
なら KeepAlive も有効になります。そのため KeepAliveTimeout
で指定された秒数まで接続しっぱなしになります。
Apache Bench はサーバからのアクティブクローズを待つので KeepAliveTimeout
の秒数が経過するまでリクエストが終わらなくなります。
Docker Hub の php:apache のイメージは KeepAlive ON
で KeepAliveTimeout 5
です。
curl だと発生しない
curl で HTTP/1.0
を指定してもこの現象は発生しません。
time curl -i -0 http://localhost:8888/
HTTP/1.1 200 OK Date: Sun, 30 Jul 2017 08:54:12 GMT Server: Apache/2.4.10 (Debian) X-Powered-By: PHP/7.1.7 Content-Length: 0 Content-Type: text/html; charset=UTF-8 real 0m0.012s user 0m0.003s sys 0m0.004s
curl はリクエストを 1.0 で送ってもレスポンスが 1.1 なら 1.1 のクライアントとして振る舞うからだと思います。ので、Content-Length
を元にレスポンスの終わりを判断できます。
なお、php のコードを次のようにすると、
<?php header('HTTP/1.1 200 OK'); flush();
curl の応答は次のようになります。
time curl -i -0 http://localhost:8888/
HTTP/1.1 200 OK Date: Sun, 30 Jul 2017 08:57:05 GMT Server: Apache/2.4.10 (Debian) X-Powered-By: PHP/7.1.7 Vary: Accept-Encoding Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8 real 0m0.014s user 0m0.002s sys 0m0.005s
この場合は Transfer-Encoding: chunked
なので、チャンクサイズ 0 でレスポンスの終端が判断できます。
zend-expressive
この現象、zend-expressive を Docker Hub の php:apache で動かして ab
したときのスコアがおかしなことになって気づきました。
zend-expressive で使われている zend-diactoros のレスポンスオブジェクトは普通に作るとリクエストの内容に依らず 1.1 で固定のためです。
パイプラインの最初のほうでリクエストのバージョンを元にレスポンスのバージョンを変更するミドルウェアを登録しておけば解決します。
// pipeline.php use Psr\Http\Message\ServerRequestInterface; use Interop\Http\ServerMiddleware\DelegateInterface; $app->pipe(function (ServerRequestInterface $request, DelegateInterface $delegate) { return $delegate->process($request)->withProtocolVersion($request->getProtocolVersion()); });
nginx+php-fpm では発生しない
ちなみに nginx+php-fpm の構成では発生しませんでした。
nginx の場合は php-fpm が 1.1 を返してきたとしても、リクエストのバージョンが 1.0 なら Connection: close
を付与したうえでサーバからアクティブクローズするようです。
結局どこが悪いの?
現代の PHP は http_response_code
があるのでレスポンスコードを指定するためだけにプロトコルバージョンまで付けて header
関数を使うべきでは無いと思いますけれども、zend-diactoros は header
関数でステータスコードを指定しています。ただ、PSR-7 の ResponseInterface
が getProtocolVersion/withProtocolVersion
を持っているので、レスポンスの送信時にプロトコルバージョンも指定するのは至って自然な気がします。
なので、zend-expressive でレスポンスのプロトコルバージョンがリクエストのプロトコルバージョンに基いていないのがダメなのだろうか・・・?
でも、HTTP/1.0 のリクエストに対して HTTP/1.1 のレスポンスを返すこと自体は合法なようです。
ただ、サーバが「おれ HTTP/1.1 でも大丈夫だよ」とクライアントに伝えるためであって、KeepAlive などの HTTP/1.1 の機能は有効にするべきではない、気がする、ので、Apache が悪いような気もする。
あるいは zend-expressive が(あるいは mod_php が) HTTP/1.0
なリクエストに対しては Connection :close
でレスポンスを返すべき?