Пишем обработчик ошибок для phpredis

Началось все с того, что у нас в компании решили сделать прокси/балансировщик нагрузки который бы, в зависимости от ключа, отправлял запрос на тот или иной инстанс Redis'а. Так как идеально сразу ничего не работает, то написанный на php проект, работающий с редисом(с помощью phpredis) через этот самый балансировщик, с завидной регулярности вылетал с критическими ошибками. Увы прокси не всегда правильно собирал сложные ответы сервера…
Работа с Redis'ом в коде через каждых 10 строк, и оборачивать каждый вызов в try, catch не было ни малейшего желания, но и с постоянными вылетами дебажить было сильно не удобно. Тут мне и пришла в голову идея подменить объект Redis'a своим, изнутри которого я бы уже вызывал все методы настоящего объекта…

Естественно дублировать все методы исходного класса сильно накладно, да и не зачем, ибо существует замечательный метод __call, к которому идет обращение, при вызове несуществующего метода объекта. На вход мы получаем имя запрашиваемого метода и массив аргументов, после чего успешно вызываем с помощью call_user_func_array, нужный метод исходного объекта. Таким образом оборачивать в try, catch нам надо лишь один вызов call_user_func_array.
Итого метод __call выглядит следующим образом:
public function __call($name, $arguments)
{
	$i=0;
	while(true)
	{
		try{
			return call_user_func_array(array($this->obj, $name), $arguments);
			break;
		}
		catch (Exception $e) {
			$this->handle_exception($e,$name,$arguments);
			if($i<5)
				$i++;
			else
				die('5 time redis lug');
		}
	}
	
}

Если вылазит ошибка, мы отправляем ее на обработчик, а сами пробуем еще раз вызвать тот же метод. После 5ти неудачных вызовов перестаем мучить проксю и идем курить логи…

Первый вариант класса выглядел так:

class RedisErrHandler
{
	private $obj;
	private $ip;
	private $port;
	private $timeout;
	
	public function __construct($ip,$port,$timeout=0)
	{
		$this->ip=$ip;
		$this->port=$port;
		$this->timeout=$timeout;
		$this->rconnect();
	}
	
	private function rconnect()
	{
		$this->obj=new Redis;
		$this->obj->connect($this->ip,$this->port,$this->timeout) or die('Error connecting redis');
	}
	
	public function __call($name, $arguments)
	{
		$i=0;
		while(true)
		{
			try{
				return call_user_func_array(array($this->obj, $name), $arguments);
				break;
			}
			catch (Exception $e) {
				$this->handle_exception($e,$name,$arguments);
				if($i<5)
					$i++;
				else
					die('5 time redis lug');
			}
		}
		
	}
	
	private function handle_exception($e,$name,$args)
	{
		$err=$e->getMessage();
		$msg="Caught exception: ".$err."\tcall ".$name."\targs ".implode(" ",$args)."\n";
		if($_SERVER['LOG'])
		{
			$handle2=fopen('redis_log.txt','a');
			fwrite($handle2,date('H:i:s')."\t$msg");
			fclose($handle2);
		}
		echo $msg;
		if(substr(trim($err),0,37)=='Caught exception: protocol error, got')
			die('bye');
		$this->rconnect();
	}
	
}

Он реконнектился при каждом вылете и «умирал» при вылете с ошибкой «protocol error», ибо именно на такие ошибки мы и охотились.

Для его интеграции надо было всего то заменить
$r=new Redis();
$r->connect('127.0.0.1',6379,10);
на
$r=new RedisErrHandler('127.0.0.1',6379,10);

Этот вариант прекрасно работал до поры до времени, пока один раз скрипт не вылетел при работе с multi. Так как для транзакций в phpredis выделен отдельный объект, то стало понятно что надо писать обертку еще и для него.
В первую очередь был добавлен метод multi в приведенный выше класс:
public function multi($type)
{
	return new RedisMultiErrHandler($this->obj,$type,$this->ip,$this->port,$this->timeout);
}

Ну и написан класс для обработки ошибок в объекте транзакций, по аналогии к предыдущему:
class RedisMultiErrHandler
{
	private $obj;
	private $ip;
	private $port;
	private $timeout;
	private $m;
	private $type;
	private $commands;
	
	public function __construct(&$redis,$type,$ip,$port,$timeout=0)
	{
		$this->ip=$ip;
		$this->port=$port;
		$this->timeout=$timeout;
		$this->type=$type;
		$this->obj=$redis;
		$this->m=$this->obj->multi($type);
	}
	
	private function rconnect()
	{
		$this->obj=new Redis;
		$this->obj->connect($this->ip,$this->port,$this->timeout) or die('Error connecting redis');
		$this->m=$this->obj->multi($this->type);
	}
	
    public function __call($name, $arguments)
    {
		$this->commands[]=array('name'=>$name, 'arguments'=>$arguments);
		return $this;
    }
	
	private function handle_exception($e)
	{
		$err=$e->getMessage();
		$msg='';
		foreach($this->commands as $command)
		{
			$msg.="Multi sent\tcall ".$command['name']."\targs ".implode(" ",$command['arguments'])."\n";
		}
		$msg.="Caught exception: ".$err."\n";
		if($_SERVER['LOG'])
		{
			$handle2=fopen('redis_multi_log.txt','a');
			fwrite($handle2,date('H:i:s')."\t$msg");
			fclose($handle2);
		}
		echo $msg;
		if(substr(trim($err),0,37)=='Caught exception: protocol error, got')
			die('bye');
		$this->rconnect();
	}
	
	
	public function exec()
	{
		$i=0;
		while(true)
		{
			foreach($this->commands as $command)
			{
				call_user_func_array(array($this->m, $command['name']), $command['arguments']);
			}
			try{
				return $this->m->exec();
				break;
			}
			catch (Exception $e) {
				$this->handle_exception($e);
				if($i<5)
					$i++;
				else
					die('5 time mredis lug');
			}
		}
	}
}


Дабы иметь возможность повторной отправки всех команд транзакции при вылете, все вызовы, кроме exec(), которая непосредственно завершает транзакцию, заносились в массив и отправлялись на сервер при вызове последней. Discard у нас в коде не используется потому в классе его отдельно не выносил.

Учитывая, что иногда, хоть и крайне редко, коннект с редисом зависает даже без использования прокси, то данные обертки успешно используются и по сей день.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 14

    +2
    Магические методы ведь тормозят, нет?
      0
      Возможно, но разницы в скорости работы на нашем проекте напрямую через redis и класс-обертку замечено не было
      +2
      Скажите, а зачем
      1) вызывать метод 5 раз в надежде «вдруг заработает»? Почему нельзя обеспечить стабильную работу редиса?
      2) Функцию handle_exception() должен выполнять глобальный обработчик исключений в приложении, а не класс-обертка интерфейса
      3) Из-за пункта 2 вы польностью лишаетесь возможности обрабатывать ожидаемые исключения при вызове функции
        –1
        1)При непосредственной работе с Redis'ом без прокси, ошибкой чаще всего является подвисание коннекта к редису, в данном случае после реконнекта команда успешно отправляется со второй попытки. 5 раз, возможно слишком для чистого Redisa, но при работе с прокси очень помогло в отладке.
        2)В этом случае будет весьма проблематично сделать реконнект отправить снова те же данные и продолжить работу, занеся в лог инфу о исключении. А сейчас у меня абсолютно все вылеты происходят из-за зависания коннекта.
          +1
          Надо решать вопросы нестабильной работы прокси или вообще отказываться от него. Балансировщик по ключам можно сделать и на уровне приложения, это будет лучше, чем писать такой код под прокси.
            0
            Увы даже прямой коннект к редису изредка подвисает, потому сейчас вопрос не в прокси а Redis или phpredis.
            Балансировщик на уровне приложения рассматривался, но был отброшен, ибо приложение работает в нескольких копиях на нескольких серверах, а редису периодически требуется проводить решардинг. В данном случае намного проще сообщить только прокси о новой «таблице маршрутизации», чем сообщать об этом куче серверов с кучей копий приложения. Особенно проблемно это в свете того, что в php довольно накладно сообщить работающему приложению о том что поменялась таблица маршрутизации.
            Возможно, с выходом redis-cluster от прокси получится отказаться.
        +2
        — Я бы отлавливал исключение RedisException вместо простого Exception
        — Есть смысл повторных запросов в цикле, но там нужно делать принудительную паузу в несколько микросекунд
        — Все магические методы тормозят, правильнее — прописать все функции вручную. Их не так уж и много.
        — Конструкцию «or die()» следует забыть как страшный сон, а от die() избавиться
        — Вероятно, проще отнаследовать свой класс-обёртку от класса Redis
        — Логи современные пацаны пишут через file_put_contents
        — Даже однострочные условия следует выделять фигурными скобками :)
          0
          — RedisException, хорошая мысль
          — не вижу смысла в паузе, в данном случае повторная ошибка вероятнее всего говорит о том что коннект висит уже 10 секунд, к тому же был произведен реконнект к Редису
          — возможно, будет время сравню __call и прямое прописывание пары методов по быстродействию, если выигрыш в скорости действительно будет того что стоить — перепишу.
          — в моей программе невозможность соединиться с редисом означает невозможность работы, потому завершение считаю закономерным.
          — Ну тут мне во-первых было стремно наследовать класс, который поставляется как расширение, во-вторых не вижу смысла ибо все равно вручную переписывать все методы.
          — согласен, но уж так вышло
          — Мне привычней и удобней не ставить скобок там, где можно обойтись без них
            0
            А скажите, почему бы вам не сделать репликацию, и не коннектится к локальной машине? В таком случае пропадут все тормоза с удалёнными коннектами.
              0
              На то есть несколько причин:
              1)Пока redis вмещается в оперативку одной машины, но в ближайшие пол-года выйдет за ее рамки.
              2)Программы клиенты размещаются на нескольких машинах, а репликация это использование объема RAM умноженного на количество клиентских машин и, соответственно, много дополнительных трат.
                0
                Весь вопрос в том что вам важнее: скорость или память.
                  0
                  Даже если переплачивать в несколько раз за репликацию, все равно упираешься в проблему, когда база не влазит в RAM одной машины. Ну а скорость нас пока устраивает и не вызывает желания переплачивать.
        • UFO just landed and posted this here
            0
            Использую ваш класс в одном очень нагруженном проекте > 200 запросов (к веб серверу, к кешу более 1000) в секунду, работает стабильно.

            В остальных случаях Rediska, которая к сожалению перестала развиваться. В редиске правда раздражает баг — не делает реконнект к серверу после таймаута «can't read from socket» и на долгих крон-скриптах часто вываливается с эксепшеном.

          Only users with full accounts can post comments. Log in, please.