Индексирование 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_matchpreg_match('/^\[start\]\s*$/',$line)и проверки на is_string для $line.
Оставалось только подрехтовать напильником, и, вуаля, в проекте на Yii был создан компонент, который является своего рода прослойкой для ssh2 функций.
/** * 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