[⇐ К полному списку статей / Content] ID: 20141008_0515-mysql2php

Неблокирующий запрос к MySQL из PHP (MySQLi)

Казалось бы – что можно сказать нового к такой заезженной и тривиальной теме: доступ к данным в MySQL по запросу через браузер – Apache – PHP?
Оказывается и в такой казалось бы банальной операции можно встретить проблемы, которые мало кто знает как решать.

В задачах, с которыми столкнулся я, запросы к СУБД штатно могут быть столь тяжёлыми, что выполнятся продолжительное время. Заметные задержки при этом оказались неизбежны – даже на самых быстрых серверах с применением рейдов-0 из SSD под данные СУБД. Существенные задержки позволили появиться возможности и проблеме: пользователь может захотеть прервать выполнение запроса, передумать ждать результатов. Иными словами запрос может стать не актуальным, и тогда его надо корректно прервать. То же самое может произойти из-за аварийного обрыва соединения или из-за тайм-аута.

Реализация таких отмен запросов, может потребоваться и в простом web-интерфейсе, и при запросах данных через AJAX, и при взаимодействии с сервером специальных клиентских программами по HTTP. С серверной стороны, в принципе, подобная задача может возникнуть и при получении данных из других СУБД или вообще из других медленных источников. В этой статье я привожу решение, относящееся к извлечению данных именно из MySQL через PHP под Apache2 используя новый программный интерфейс MySQLi, то это относится к множеству вариантов клиентов, но к совершенно конкретному получению данных на сервере.

Знакомство вопросом реализации обработчиков отмены или обрыва сразу выявило принципиальное несовершенство связки PHP-Apache2: PHP-скрипт в принципе не может обнаружить обрыв или даже штатное закрытие соединения до тех пор пока не будет ничего отправлять клиенту в сеть. Причём нигде не регламентировано, сколько данных для этого надо отправить, но во всех указаниях подразумевается, что одного байта вроде бы будет не достаточно, часто отправляют холостые блоки из целого килобайта нулей или пробелов. Возможно, это зависит от конкретного сервера, версии PHP и их настроек. Экспериментально я определил, что на моём сервере можно отправлять 32 перевода строки.

В связке PHP-MySQL тоже оказался существенный изъян: все простые способы чтения данных оказались блокирующими, т.е. они приостанавливают работу PHP скрипта до тех пор, пока MySQL не вернёт их (либо не случится тайм-аут). И соответственно, в это время скрипт ничего послать в сеть не может для того, что бы проверить соединение с HTTP-клиентом. Да, можно применить небуфиризированное чтение, но в моём случае задержки не были связанны с большим количеством записей в выборках; даже извлечение одного единственного значения из СУБД может приостановить ход выполнения PHP. В случае отмены можно, конечно, просто оборвать HTTP-соединение с клиента. Но при этом не только бесполезно перенагружается сервер, который продолжает обрабатывать ненужный “зомби-запрос”, но если вы применяете сессии, а СУБД “уйдёт в себя”, ненароком может произойти глобальная блокировка не только окна браузера с запросом, но и всего сайта, да так, что придётся перегружать всё сбрасывая сессию. Блокировка сессии может случится, если на момент обращения к СУБД, которое затянулось, сессия не была закрыта на запись.

Существует одно универсальное решение – при постановке запроса в MySQL получить номер MySQL-процесса, передать его в клиент. Тот если надо, может его использовать, если понадобится, передав по другому соединению в специальный скрипт на сервере, который “собъёт” ставший не нужным запрос с помощью SQL-зароса-комманды KILL, что в свою очередь приведёт и к разблокировки PHP, обрабатывающего запрос. Этот способ, без сомнения, применим, однако он сложен, некрасив и небезопасен. При его реализации нужно опять-таки иметь в виду опасность блокировок сессии, о которой я упоминал выше.

Мне пришло в голову использовать в PHP параллельный поток (thread) для проверки наличия соединения по HTTP, приблизительно так:

class Ping0 extends Thread {
public function
run() {
//if (ob_get_level()) ob_end_clean();
sleep(1);
echo
str_repeat("\n",32);
flush(); // Error 6 (net::ERR_FILE_NOT_FOUND): The file or directory could not be found.
ob_flush();
}
}

function
db_query_long($qstring,$conn)
{
ignore_user_abort(false);
//if (ob_get_level()) ob_end_clean();

$ping0 = new Ping0();
$ping0->start();

$ret = db_query($qstring,$conn);

// $ping0->stop();

return $ret;
}

Но этот способ не заработал, скрипт падал с неадекватными ошибками на flush(), создающими впечатление багов в PHP. Встроенный класс потоков Thread в PHP произвёл впечатление очень “сырого”, его даже не было в стандартной инсталляции и что бы воспользоваться требовалось пересобирать PHP из исходников с поддержкой ZTS (Zend Thread Safety) (опции --enable-maintainer-zts или --enable-zts в Windows). В общем, я решил оставить этот путь, хотя его преимущество в том, что он мог бы помочь в решении задачи обработчика отмены запросов не только из MySQL но и из других медленных источников данных.
К счастью, в MySQLi есть нетривиальный способ обработки запроса к БД, который позволил мне благополучно совершить неблокирующее чтение непосредственно.
Итак к вашему вниманию вот фрагмент функции, организующий неблокирующей запрос и возвращающей результат в виде объекта mysqli_result :
    $r = $gl_mysqli1->query($sql, MYSQLI_ASYNC ); 

ob_implicit_flush(true);
ignore_user_abort(false); // можно поставить true

for ($i=0; $i<$gl_tout; $i++) // $gl_tout - тайм-аут; максимальное время в секундах
{
$ready = $error = $reject = array($gl_mysqli1);
// $ready[] = $error[] = $reject[] = $gl_mysqli1;

mysqli_poll( $ready,$error,$reject, 1); // ждём 1 cек; можно воспользоваться пятым параметром - микросекунды
if (count($ready)>0)
{
// ready - данные получены
$r = $gl_mysqli1->reap_async_query();
if (
$r)
{
// успех - данные получены
return $r;
}
// some error ??
return $r;
}
if (
count($error)>0 || count($reject)>0 )
{
// какая-то ошибка - error
trigger_error("(" . $gl_mysqli1->connect_errno . ") "
. $gl_mysqli1->connect_error, E_USER_ERROR);

return
null;
}

// проверка соединения с клиентом по HTTP - test connection
echo str_repeat("\n",32); // посылка нулей приводит к ошибкам и глюкам
flush();
ob_flush();

if (
connection_status()!=CONNECTION_NORMAL)
{
// соединение с клиентом оборволось, запрос на СУБД не актуален
return null;
}

// возможно нормальное состояние — данные ещё не готовы, надо подождать
}
// если мы тут - время вышло - таймайт $gl_tout сек.

Метод mysqli::poll до сих пор весьма плохо документирован...
Примечание: лишние переводы строки между управляющими тегами HTML и XML обычно никак не проявляются.

Original: http://lj.rossia.org/users/shestero/138522.html
[⇐ К полному списку статей / Content]


© http://netdat.ru — Bulletin Publishing System, 2011-2016.