Edited at

phpのMemcached拡張で持続的接続を行うとESTABLISHEDが大量に発生する問題の原因と対処法

More than 1 year has passed since last update.

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を使った

https://github.com/polidog/memcached-test

最初は全然検討がつかずに、単純に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が大量に発生する問題