polidog lab++

Blog
symfony2のMemcachedSessionHandlerの不具合について

symfony2のMemcachedSessionHandlerの不具合について

Dec 5, 2013 | tech | memcached php symfony2

この記事は間違った記事です。

MemcachedSessionHandlerで持続的接続を行うとESTABLISHEDが大量に発生する問題を参照してください。




相変わらずsymfony2で開発している僕です。

Symfony2のMemcachedSessionHandlerを利用してセッションの管理をしていたんですが、ブラウザから実行するとmemcacheのコネクションをずっと持ち続けてしまうという不具合が発生していました。
ブラウザからアクセスすればするほどmemcacheとのコネクションが貼りっぱなしになり、、、やがて接続できなくなるというかmemcacheからレスポンスが返ってこなくなるという現象でした。
ちなみに環境は「PHP 5.4.19」と「Apache/2.2.15」の組み合わせで発生していました。

最初は原因が分からずに、apache再起動してなんとかしていましたが、、、本気でヤバそうなので、原因を調査しようと思いnetstatしてみると大変な事に・・・

$ netstat --tcp | grep memcache
tcp        0      0 192.168.1.5:38497          192.168.1.6:memcache       ESTABLISHED
tcp        0      0 192.168.1.5:38605          192.168.1.6:memcache       ESTABLISHED
tcp        0      0 192.168.1.5:38419          192.168.1.6:memcache       ESTABLISHED
tcp        0      0 192.168.1.5:38554          192.168.1.6:memcache       ESTABLISHED
....

こんなのがものすごい数表示されてたよ・・・

最初はいみがわからなかったけど、まあどうみてもコネクションが貼りっぱなしになっていたので、アレゲな感じだったわけです。
で、頑張って調べてみたところ、Memcachedの処理に問題があるのではないかという結論に達したわけでs。

<?php

$memcached = new Memcached('hoge');
$memcached->set('a','b');

echo 'exit;';

上記のようなコードを実行したあとにnetstatすると

$ netstat --tcp | grep memcache
tcp        0      0 192.168.1.5:38497          192.168.1.6:memcache       ESTABLISHED

このように「ESTABLISHED」状態になってしまいます。apacheからレスポンスが返ってきた時点で、本来であればTIME_WAITになっていれば正解かなぁーと・・・

まあこの時点でおかしいのはコネクション切る処理を行っていない事かなぁーと思い、以下のようにコードを修正しました。

<?php

$memcached = new Memcached('hoge');
$memcached->set('a','b');
$memcached->quit();
echo 'exit;';

このようにコネクションを切る処理を入れてみました。

$ netstat --tcp | grep memcache
tcp        0      0 192.168.1.5:38497          192.168.1.6:memcache       TIME_WAIT

ちゃんとTIME_WAITになる!!!
ちなみにunsetでもちゃんとTIME_WAITになる事確認してます。

<?php

$memcached = new Memcached('hoge');
$memcached->set('a','b');
unset($memcached);
echo 'exit;';

ということで、Symfony2がわのMemcachedハンドラー側で最後にquit処理していないようでした・・・
MemcachedSessionHandler

イベントで処理すればなんとかなるかなぁーと思っていたんですが、結局__destructで対応するという微妙なハンドラ書いて観ました・・・

<?php
namespace Polidog\TestBundle\Session\Storage\Handler;

/**
 * MemcachedSessionHandlerの継承
 * 明示的にmemcahced->quit() or unset(memcached object)しないと解放されないためあえて継承させる
 * Class MemcachedSessionHandler
 */
class MemcachedSessionHandler implements \SessionHandlerInterface
{
  /**
   * @var \Memcached Memcached driver.
   */
  private $memcached;

  /**
   * @var integer Time to live in seconds
   */
  private $ttl;

  /**
   * @var string Key prefix for shared environments.
   */
  private $prefix;

  /**
   * Constructor.
   *
   * List of available options:
   *  * prefix: The prefix to use for the memcached keys in order to avoid collision
   *  * expiretime: The time to live in seconds
   *
   * @param \Memcached $memcached A \Memcached instance
   * @param array      $options   An associative array of Memcached options
   *
   * @throws \InvalidArgumentException When unsupported options are passed
   */
  public function __construct(\Memcached $memcached, array $options = array())
  {
    $this->memcached = $memcached;

    if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) {
      throw new \InvalidArgumentException(sprintf(
        'The following options are not supported "%s"', implode(', ', $diff)
      ));
    }

    $this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400;
    $this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s';
  }

  /**
   * {@inheritDoc}
   */
  public function open($savePath, $sessionName)
  {
    return true;
  }

  /**
   * {@inheritDoc}
   */
  public function close()
  {
    $this->memcached->quit();
    return true;
  }

  /**
   * {@inheritDoc}
   */
  public function read($sessionId)
  {
    return $this->memcached->get($this->prefix.$sessionId) ?: '';
  }

  /**
   * {@inheritDoc}
   */
  public function write($sessionId, $data)
  {
    return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl);
  }

  /**
   * {@inheritDoc}
   */
  public function destroy($sessionId)
  {
    return $this->memcached->delete($this->prefix.$sessionId);
  }

  /**
   * {@inheritDoc}
   */
  public function gc($lifetime)
  {
    // not required here because memcached will auto expire the records anyhow.
    return true;
  }


  public function set($key, $value)
  {
    $this->memcached->set($key, $value);
  }

  public function get($key)
  {
    return $this->memcached->get($key);
  }

  public function __destruct()
  {
    unset($this->memcached);
  }
}

ちょっと微妙な実装ですが、まあこれで一旦様子を見ようと思います。
本当はもう少し調査に時間かけられたらいいんですが・・・

という事で、もっと良い方法を知っている方はぜひ僕に教えてください。

追記 2013-12-06

どうやら僕が勘違いしていたようです。
決してこれは不具合ではないということです。ただ見方によっては不具合かもしれない。。。

# app/config/config.yml
services:
  memcached:
      class: Memcached
      arguments:
          persistent_id: %session_memcached_prefix%
      calls:
          - [ addServer, [ %session_memcached_host%, %session_memcached_port% ]]
  session.handler.memcached:
      class:      Vpf\MainBundle\Session\Storage\Handler\MemcachedSessionHandler
      arguments: ["@memcached",{ prefix: %session_memcached_prefix%, expiretime: %session_memcached_expire% }]

Memcachedインスタンスの設定をする際に、コンストラクタの引数を設定していたので同じpersistent_idな文字列であればコネクションを使い回してくれるということですね。
ただし、挙動からみてる感じだと、コネクションを使い回す際は明示的にMemcacheとのコネクションを切る(Memcached::quit())か、インスタンスを破棄しないとコネクションは解放されないっぽいです。
同じプロセスの中でしか使い回せないぽい気がするのですが・・・そういう認識であってるのかな。。。。検証してみたい。。。

まあSymfony2の場合はDIあるわけだし、persistent_id指定しなくても良いから、MemcachedSessionHandlerにquitやunsetによる解放の実装が無いのかな・・・。

comments powered by Disqus

関連記事

© 2017 polidog lab++