Индексирование Sphinx с удаленного сервера средствами PHP

от автора

Доброго времени суток, дорогие читатели!

Хочу рассказать вам об интересной задаче, которая стала передо мной в рамках проекта и, естественно, о ее решении.

Исходные данные:
Стандартный набор LAMP (далее СС),
Yii framework (версия здесь не важна),
удаленный сервер (далее УС), на котором установлен демон Sphinx, searchd.
На УС создан пользователь с правами рута (но не сам рут).
На СС установлен модуль ssh2_mod для PHP.

Сразу оговорюсь, в этой статье я не буду расписывать особенности Sphinx, кому интересно, могут почитать официальный мануал sphinxsearch.com/docs/current.html.
Ограничусь только общей информацией.

Итак, Sphinx — поисковый демон, в моем случае работает с MySQL. Основная особенность — он индексирует базу по определенным запросам (описанным в конфиге сфинкса), и результат выборки сохраняет в свои файлы. Чтобы информация была актуальной (в MySQL возможно и добавление и редактирование записей), нужно запускать индексацию сфинкса. Тогда, он сделает повторную выборку и сохранит ее себе.

Задача:
Запускать индексацию сфинкса на УС.
Причина именно удаленного запуска состоит в том, что необходимо запускать команды по крону с конкретными параметрами, определяемыми в коде. Кроны запускаются с СС.
Т.е. на сервере запускается крон, метод которого выполняет индексацию на УС.

Единственное решение, которое нашел — использование ssh2_mod для apache2 (кому интересно, мануал по установке на CentOS можно глянуть здесь www.stableit.ru/2010/12/ssh2-php-centos-55-pecl.html).

Посмотрел мануал по ssh2 (http://www.php.net/manual/en/book.ssh2.php), нашел замечательную функцию ssh2_exec, которая на вход принимает текущую сессию и команду, но, как оказалось, она имеет ряд ограничений.
Например, при попытке выполнения команды indexer —all —rotate для дельта индекса я получал ошибку

WARNING: failed to open pid_file '/var/run/sphinx/searchd.pid'. WARNING: indices NOT rotated. 

Эта ошибка означает, что моему пользователю не хватает прав для выполнения rotate (а у меня юзер с правами рута, sudo -s), хотя из консоли напрямую я спокойно выполнял эту команду безо всяких ошибок.
Далее я решил поискать еще, и обнаружил, что можно эмулировать ввод команд через терминал (функция ssh2_shell). С помощью стандарного потока и фукнции fwrite можно писать команды в «терминал» и получать на выходе такой же стандарный выходной поток, т.е. результат, выдаваемый терминалом. Происходит путем построчного считывания из выходного потока при помощи fgets.

Все хорошо, проверка выполнения дельта индекса прошла успешно, я обрадовался, но…
«НО» произошло, когда я попытался выполнить индексацию основного индекса (порядка 400к записей, выполняется несколько минут). Оказалось, что выходной поток обрывается при малейшей задержке выполнения команды в терминале. Простым языком, когда вводишь команду, и терминал «задумывается». В итоге у меня оставались «недоиндексированные» файлы.

Решил погуглить, как народ решает проблемы, натолкнулся на кусок кода, прямо в мане по ssh2 на php.net. Автор решения предлагал ставить маркеры начала и окончания команды (echo ‘[start]’; $command; echo ‘[end]’) и установить max_execution_time для скрипта.
Код приведен ниже.

$ip = 'ip_address';   $user = 'username';   $pass = 'password';   $connection = ssh2_connect($ip);  ssh2_auth_password($connection,$user,$pass);  $shell = ssh2_shell($connection,"bash");   //Trick is in the start and end echos which can be executed in both *nix and windows systems.  //Do add 'cmd /C' to the start of $cmd if on a windows system.  $cmd = "echo '[start]';your commands here;echo '[end]'";  $output = user_exec($shell,$cmd);   fclose($shell);   function user_exec($shell,$cmd) {    fwrite($shell,$cmd . "\n");    $output = "";    $start = false;    $start_time = time();    $max_time = 2; //time in seconds    while(((time()-$start_time) < $max_time)) {      $line = fgets($shell);      if(!strstr($line,$cmd)) {        if(preg_match('/\[start\]/',$line)) {          $start = true;        }elseif(preg_match('/\[end\]/',$line)) {          return $output;        }elseif($start){          $output[] = $line;        }      }    }  }  

Как мне показалось, хорошее решение, но…
Здесь НО заключалось в условии preg_match. При выводе информации в $output пишется все, что дает на выход терминал. Вышеописанная проблема с «задумавшимся терминалом» снова стала актуальной, т.к. при паузе на терминал выводилась команда вывода маркера завершения echo ‘[end]’ (именно сама команда, а не результат выполнения). Все решилось путем добавления ограничения начала и конца строки в preg_match
preg_match('/^\[start\]\s*$/',$line)
и проверки на is_string для $line.

Оставалось только подрехтовать напильником, и, вуаля, в проекте на Yii был создан компонент, который является своего рода прослойкой для ssh2 функций.

<?php class SshException extends CException {}  /**  * Class Ssh  * It is a base class for the simplify a ssh connection management  * and related commands execution  *  * @author Ivanenko Vladyslav  */ class Ssh {     const EXEC_TYPE_EXEC = 'exec'; // type for ssh2_exec()     const EXEC_TYPE_SHELL = 'shell'; // type for ssh2_shell()      const START_MARK = '__start__';     const FINISH_MARK = '__finish__';      const MAX_EXECUTION_TIME = 1800; // max script execution time in sec      private $user;     private $password;     private $host;     private $port;      private $shellType = 'bash'; // shell type     private $shell = null; //shell identificator      private $ssh = null; //connection      private $execType;      /**      * Construct      *      * @param null $user      * @param null $password      * @param null $host      */     public function __construct($user = null, $password = null, $host = null, $port = null)     {         $config = Yii::app()->params['ssh'];         $params = array('user', 'password', 'host', 'port');          foreach($params as $param) {             if(isset(${$param}) && !is_null(${$param})) {                 $this->{$param} = ${$param};             } else {                 $this->{$param} = @$config[$param];             }         }          return true;     }      /**      * Connect to Ssh      *      * @return resource      * @throws SshException      */     public function connect()     {         $this->ssh = @ssh2_connect($this->host, $this->port);         if(empty($this->ssh)) {             throw new SshException('Cant connect to ssh');         }          if(empty($this->execType)) {             $this->execType = self::EXEC_TYPE_SHELL;         }          return $this->ssh;     }      /**      * Login to ssh      *      * @throws SshException      * @return bool      */     public function login()     {         if(!@ssh2_auth_password($this->ssh, $this->user, $this->password)) {             throw new SshException('Cant login by ssh');         }          return true;     }      /**      * Exec command by ssh      *      * @param $cmd      * @param $type      *      * @return string      * @throws SshException      */     public function exec($cmd, $type = self::EXEC_TYPE_SHELL)     {         if(is_null($this->ssh)) {             $this->connect();             $this->login();         }         $this->execType = $type;         switch($this->execType) {             case self::EXEC_TYPE_EXEC: $result = $this->execCommand($cmd); break;             case self::EXEC_TYPE_SHELL: $result = $this->execByShell($cmd); break;             default: throw new SshException('Incorrect exec type'); break;         }          return $result;     }      /**      * Executes command by the direct ssh2_exec      *      * @param $command      *      * @return string      * @throws SshException      */     private function execCommand($command)     {         if (!($stream = ssh2_exec($this->ssh, $command))) {             throw new SshException('Ssh command failed');         }         stream_set_blocking($stream, true);         $data = "";         while ($buf = fread($stream, 4096)) {             $data .= $buf;         }         fclose($stream);          return $data;     }      /**      * Executes command within the shell opening      *      * @param $command      *      * @return string      */     private function execByShell($command)     {         $this->openShell();         return $this->writeShell($command);     }      /**      * opens shell      *      * @throws SshException      */     private function openShell()     {         if(is_null($this->shell)) {             // here is hardcoded width and height, you can change them.             $this->shell = @ssh2_shell($this->ssh,  $this->shellType, null, 80, 40, SSH2_TERM_UNIT_CHARS);         }          if( !$this->shell ) {             throw new SshException('SSH shell command failed');         }     }      /**      *      * Write the command to the open shell      *      * @param $cmd      * @param int $maxExecTime in sec      *      * @return string      */     private function writeShell($cmd, $maxExecTime = self::MAX_EXECUTION_TIME)     {         // write start marker         fwrite($this->shell, $this->getMarker(self::START_MARK));         // write command         fwrite($this->shell, $cmd . PHP_EOL);         // write end marker         fwrite($this->shell, $this->getMarker(self::FINISH_MARK));         stream_set_blocking($this->shell, true);         sleep(1);         $output = "";         $start = false;         // define the time until the script can be executed         $timeUntil = time() + $maxExecTime;          while(true) {             if(time() > $timeUntil) {                 break;             }             $line = fgets($this->shell, 4096);             // if any delay is happened while command is processing             if(!is_string($line)) {                 sleep(1);                 continue;             }             // define the start executed command             if(preg_match('/^' . self::START_MARK . '\s*$/', $line)) {                 $start = true;             } elseif(preg_match('/^' . self::FINISH_MARK . '\s*$/', $line)) {  // define the last executed command                 break;             } elseif($start) {                 // add console output to the script output data                 $output .= $line;             }         }          return $output;     }      /**      * Disconnect from ssh      */     public function disconnect() {         $this->exec('exit');         $this->ssh = null;         if(!is_null($this->shell)) {             fclose($this->shell);         }     }      /**      * Disconnect in destruct      */     public function __destruct() {         $this->disconnect();     }      /**      * Returns marker command      *      * @param string $type      *      * @return string      */     private function getMarker($type = self::START_MARK)     {         return 'echo "' . $type . '"' . PHP_EOL;     }  } 

П.С. Этот класс можно расширить, ведь ssh2 не ограничивается только двумя функциями по выполнению команд, есть еще и функции для работы с файлами, и другие типы авторизации и т.д. и т.п.

Спасибо за внимание, надеюсь, статья будет полезной.
Буду рад услышать любые отзывы и конструктивную критику!

Автор: Владислав Иваненко, PHP Developer Zfort Group

ссылка на оригинал статьи http://habrahabr.ru/company/zfort/blog/225979/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *