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



PHPでMemcachedを利用する際に使う[PECL::Memcached](https://pecl.php.net/package/memcached)。
そんなPECL::MemcachedでESTABLISHEDが大量に発生しているという問題に遭遇しました。
ちなみにPHP7な環境で、[php-memcached-dev](https://github.com/php-memcached-dev/php-memcached)を使った場合でも同じ問題が発生していました。

## 何が起きたのか？
持続的接続しないと、アクセス数増えたら辛いわけで持続的接続をしようと思い、気軽にこんな感じの実装をしていました。


```php
$memcached = new \Memcached(1);
$memcached->addServer('memcached', 11211);

$recommendTours = $memcached->get('recommendTours');
```

単純にMemcachedに接続してデータを取ってくる形ですね。

持続的接続を行う場合に軽くググってみたら
[@shin1x1](https://twitter.com/shin1x1)さんの[PHP Memcached拡張で持続的接続を行う](http://qiita.com/shin1x1/items/092588ac9b4f610b40d6)とか、[PHPマニュアル](http://php.net/manual/ja/memcached.construct.php)を見て、「コンスタントにID渡せばいいのかぁー」なんて思って上記のようなコードを書いてしまっていました。

気づいたときにはMemcachedへの持続的なコネクション大量に貼られていたわけですね・・・。

## 原因
原因は`$memcached->addServer('memcached', 11211);`の部分にありました。

持続的接続の場合は、一度接続処理を行ったらPHPスクリプトが終了されても接続が持続されます。
おそらくfpmやapacheなりを再起動するまでは接続され続けます。

つまり上で書いたコードでは、実行される度に持続的接続が毎回行われてしまいます・・・。

持続的接続を行っている状態だと、`$memcached->addServer('memcached', 11211);`の記述がなくても、サーバ情報は取得出来ます。

```php
$memcached = new \Memcached(1);
var_dump($memcached->getServerList());

```

もちろん初回の接続の場合は、サーバ情報を保持していません。
そのため[Memcached::isPristine()](http://php.net/manual/ja/memcached.ispristine.php)で判定してあげる必要があります。


## 対処法

ということで、持続的接続の場合は初回以外は最初からサーバ情報を持っています。
以下のようにコードを実装しましょう。（当たり前といえば当たり前ですね。）

```php
<?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/](http://localhost/56/)
でアクセスすればPHP5.6環境で[appディレクトリ](https://github.com/polidog/memcached-test/tree/a0c1a2d19c00fa9d302be309147fb9f6d62f3fb3/app)のスクリプトが実行出来ます。

### php7.0+php-fpm
[http://localhost/](http://localhost/)
でアクセスすればPHP7.0環境で[appディレクトリ](https://github.com/polidog/memcached-test/tree/a0c1a2d19c00fa9d302be309147fb9f6d62f3fb3/app)のスクリプトが実行出来ます。

### php7+apache
[http://localhost:8080/](http://localhost:8080/)
でアクセスすればPHP7.0環境で[appディレクトリ](https://github.com/polidog/memcached-test/tree/a0c1a2d19c00fa9d302be309147fb9f6d62f3fb3/app)のスクリプトが実行出来ます。


## 実際にESTABLISHEDが溜まっていくものを再現してみる

[ダメなスクリプト](https://git.io/vMfE2)を用意しました。
こいつをHTTPでアクセスする度にESTABLISHEDが溜まっていきます。

適当にcloneしてきてdocker-compouse使って動かします。
そして、[http://localhost/bad.php](http://localhost/bad.php)にアクセスします。
何回かリロードしたあとに`Docker exec`でコンテナの中に接続してnetstatで確認してみます。

```bash
$ 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](https://github.com/polidog/memcached-test/blob/a0c1a2d19c00fa9d302be309147fb9f6d62f3fb3/app/index.php)の方に記載してあります。


## 最後に

PHP+Memcachedでこの手の話題があまりなかったのですが、皆さんどう対応しているのでしょうか？
そもそも持続的接続ってあまり使わないんですかね？

Symfony2での対処法もBlogの方に書いておいたのよかったら参考にしてください。
[MemcachedSessionHandlerで持続的接続を行うとESTABLISHEDが大量に発生する問題](http://polidog.jp/2016/12/27/symfony_memcached/)