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

  • 6
    いいね
  • 0
    コメント

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が大量に発生する問題