redisセッションドライバーのロックの不具合

1,500 views
Skip to first unread message

兵頭慎児

unread,
Sep 4, 2018, 1:27:53 AM9/4/18
to codeigniter_jp
はじめまして。兵頭と申します。

codeigniterを使用した案件で、ajax通信を多用していたのですが、セッションドライバをredisにすると、「同時に複数のajax通信をすると、時々、セッションエラーになる」という現象が発生していました。
原因がわかりましたので、対策を含めて共有したいと思います。
バージョン: 3.2.0-dev
system/libraries/Session/drivers/Session_redis_driver.php

※redisはKVSでsetnx( http://redis.shibu.jp/commandreference/strings.html )を使うと、キーが存在しないときにのみキーが作成されます。

日本語の注釈を付けたロックをする部分のソースです。


protected function _get_lock($session_id)
{
   
// PHP 7 reuses the SessionHandler object on regeneration,
   // so we need to check here if the lock key is for the
   // correct session ID.
   //すでにロックを取得していればロックキーのttlを伸ばす
   if ($this->_lock_key === $this->_key_prefix.$session_id.':lock')
   
{
     
return $this->_redis->setTimeout($this->_lock_key, 300);
   
}

   
// 30 attempts to obtain a lock, in case another request already has it
   $lock_key = $this->_key_prefix.$session_id.':lock';
   $attempt
= 0;
   
do
   {
     
//$ttl=-1: 無期限キー $ttl=-2 存在しないキー $ttl > 0 キーのttl
      if (($ttl = $this->_redis->ttl($lock_key)) > 0)
     
{
         
//他プロセスでキーが作成されていたらループ
         sleep
(1);
         
continue;
     
}
     
// $ttlを取得してからキーをsetするまでに他のプロセスでキーを作成されてしまうことがまれにある
      if ($ttl === -2) {
         //nxを指定すると、ロックキーが存在すると失敗する(=他のプロセスでロックがかかっている)
         $result
= $this->_redis->set($lock_key, time(), array('nx', 'ex' => 300));
         if ( ! $result)
         
{
           
//※のときセッションエラーになってしまう。
            log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
           
return FALSE;
         
}
     
} else {
         $result
= $this->_redis->setex($lock_key, 300, time());
         
if ( ! $result)
         
{
            log_message
('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
           
return FALSE;
         
}
     
}

      $this
->_lock_key = $lock_key;
     
break;
   
}
   
while (++$attempt < 30);

   
if ($attempt === 30)
   
{
      log_message
('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 30 attempts, aborting.');
     
return FALSE;
   
}
   
elseif ($ttl === -1)
   
{
      log_message
('debug', 'Session: Lock for '.$this->_key_prefix.$session_id.' had no TTL, overriding.');
   
}

   $this
->_lock = TRUE;log_message('error', 'Session: lock '.$this->_lock_key);
   
return TRUE;
}

if (($ttl = $this->_redis->ttl($lock_key)) > 0) の部分で他のプロセスでロックキーが作成されていたらリトライするのですが「※」に書いたようにttlの取得とキーのセットの僅かな時間に他のプロセスでロックキーがセットされる場合があることがわかりました。このため、セッションエラーが出るときと出ない時があるという状況に陥っていました。
以下が、改良したコードです。

protected function _get_lock($session_id)
{
   
//log_message('error', 'lock' . getmypid() . date(' i:s'));
   // PHP 7 reuses the SessionHandler object on regeneration,
   // so we need to check here if the lock key is for the
   // correct session ID.
   if ($this->_lock_key === $this->_key_prefix.$session_id.':lock')
   
{
     
return $this->_redis->setTimeout($this->_lock_key, 300);
   
}

   
// 300 attempts(about 30 second) to obtain a lock, in case another request already has it
   $lock_key = $this->_key_prefix.$session_id.':lock';
   $attempt
= 0;
   
do
   {
     
//setnxを使い、他のプロセスとロックキーを使って排他処理をする。ロックキーがセット出来なかったらループ
      $result = $this->_redis->set($lock_key, time(), array('nx', 'ex' => 300));
     
if ( ! $result)
     
{
 
       usleep(100000);
         
continue;
     
}
      $result
= $this->_redis->setex($lock_key, 300, time());
 

      $this->_lock_key = $lock_key;
     
break;
   
}
   
while (++$attempt < 300);
   
if ($attempt === 300)
   
{
      log_message
('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 300 attempts, aborting.');
     
return FALSE;
   
}

   $this
->_lock = TRUE;
   
return TRUE;
}

元のロックではループ間隔が1秒でしたが、あまりに長いので0.1秒にしています。このあたりはconfig.phpで設定できるとベターだと思います。

codeigniter4でも同様のことが起こりそうでした。

Kenji Suzuki

unread,
Mar 5, 2021, 7:29:50 PM3/5/21
to codeigniter_jp
Kenji です。

情報共有ありがとうございます。

今更ですが、バグ報告しておきました。

// Kenji

2018年9月4日火曜日 14:27:53 UTC+9 shinj...@gmail.com:

Kenji Suzuki

unread,
Mar 18, 2021, 1:03:16 AM3/18/21
to codeigniter_jp
Kenji です。

CI3の方もバグ報告しておきました。

// Kenji


2021年3月6日土曜日 9:29:50 UTC+9 Kenji Suzuki:
Reply all
Reply to author
Forward
0 new messages