PHPでMemcachedを利用する際に使うPECL::Memcached。
そんなPECL::MemcachedでESTABLISHEDが大量に発生しているという問題に遭遇しました。
ちなみにPHP7な環境で、php-memcached-devを使った場合でも同じ問題が発生していました。
何が起きたのか?
持続的接続しないと、アクセス数増えたら辛いわけで持続的接続をしようと思い、気軽にこんな感じの実装をしていました。
$memcached = new \Memcached(1);
$memcached->addServer('memcached', 11211);
$recommendTours = $memcached->get('recommendTours');
単純にMemcachedに接続してデータを取ってくる形ですね。
持続的接続を行う場合に軽くググってみたら
@shin1x1さんのPHP Memcached拡張で持続的接続を行うとか、PHPマニュアルを見て、「コンスタントにID渡せばいいのかぁー」なんて思って上記のようなコードを書いてしまっていました。
気づいたときにはMemcachedへの持続的なコネクション大量に貼られていたわけですね・・・。
原因
原因は$memcached->addServer('memcached', 11211);
の部分にありました。
持続的接続の場合は、一度接続処理を行ったらPHPスクリプトが終了されても接続が持続されます。
おそらくfpmやapacheなりを再起動するまでは接続され続けます。
つまり上で書いたコードでは、実行される度に持続的接続が毎回行われてしまいます・・・。
持続的接続を行っている状態だと、$memcached->addServer('memcached', 11211);
の記述がなくても、サーバ情報は取得出来ます。
$memcached = new \Memcached(1);
var_dump($memcached->getServerList());
もちろん初回の接続の場合は、サーバ情報を保持していません。
そのためMemcached::isPristine()で判定してあげる必要があります。
対処法
ということで、持続的接続の場合は初回以外は最初からサーバ情報を持っています。
以下のようにコードを実装しましょう。(当たり前といえば当たり前ですね。)
<?php
$memcached = new \Memcached(1);
if ($memcached->isPristine()) {
$memcached->addServer('memcached', 11211, 1);
}
検証環境にDockerを使った
最初は全然検討がつかずに、単純にMemcached::quit()
メソッドを実行すればいいやーなんて思っていたんですが、普通にコネクションがなくなってしまっていて、これじゃないと気づいたわけです。(マニュアルを読めばわかる話ですが。)
そこで、ちゃんと原因を追求するためにDockerを使い検証環境を用意しました。(単純にまだDockerをちゃんと触れていなかったので、触りたかっただけですが)
3つの検証環境
特定のバージョンで起きるのかなぁーとか思ってしまったので、今回は念のため3つの異なる環境を用意しました。
- php5.6+php-fpm
- php7.0+php-fpm
- php7+apache
それぞれの環境にHTTPリクエストを送る際には以下のルールでアクセス出来ます。
php5.6+php-fpm
http://localhost/56/
でアクセスすればPHP5.6環境でappディレクトリのスクリプトが実行出来ます。
php7.0+php-fpm
http://localhost/
でアクセスすればPHP7.0環境でappディレクトリのスクリプトが実行出来ます。
php7+apache
http://localhost:8080/
でアクセスすればPHP7.0環境でappディレクトリのスクリプトが実行出来ます。
実際にESTABLISHEDが溜まっていくものを再現してみる
ダメなスクリプトを用意しました。
こいつをHTTPでアクセスする度にESTABLISHEDが溜まっていきます。
適当にcloneしてきてdocker-compouse使って動かします。
そして、http://localhost/bad.phpにアクセスします。
何回かリロードしたあとにDocker exec
でコンテナの中に接続してnetstatで確認してみます。
$ docker-compose ps
Name Command State Ports
-------------------------------------------------------------------------------------------------
memcachedtest_memcached_1 docker-entrypoint.sh memcached Up 11211/tcp
memcachedtest_nginx_1 nginx -g daemon off; Up 443/tcp, 0.0.0.0:80->80/tcp
memcachedtest_php-apache_1 docker-php-entrypoint apac ... Up 0.0.0.0:8080->80/tcp
memcachedtest_php56-fpm_1 docker-php-entrypoint php-fpm Up 9000/tcp
memcachedtest_php7-fpm_1 docker-php-entrypoint php-fpm Up 9000/tcp
$ docker exec -it memcachedtest_php7-fpm_1 /bin/bash
root@7975336be0a8:/var/www/html$ netstat -tan | grep ':11211' | awk '{print $6}' | sort | uniq -c
6 ESTABLISHED
こんな感じで大量にアクセスがあると大変なことになりますね・・・。
ちなみに、ESTABLISHEDが溜まっていかないコードはindex.phpの方に記載してあります。
最後に
PHP+Memcachedでこの手の話題があまりなかったのですが、皆さんどう対応しているのでしょうか?
そもそも持続的接続ってあまり使わないんですかね?
Symfony2での対処法もBlogの方に書いておいたのよかったら参考にしてください。
MemcachedSessionHandlerで持続的接続を行うとESTABLISHEDが大量に発生する問題