Простая балансировка нагрузки для MySQL и PHP с помощью библиотеки mysqlnd

MySQL всегда занимал первое место в качестве СУБД для работы с PHP. Так было практически с создания языка. Конечно сейчас, применяются и PostgreSQL, SQL Server или Oracle, но для работы в web зачастую используется MySQL.

Выбор пал на MySQL потому, что на запуск решения не приходилось тратить много времени. libmysqlclientраспространялся вместе с PHP до тех пор пока не был лицензирован под GPL, после чего эта библиотека была исключена из состава PHP.

Это создало небольшие трудности для компиляции PHP. Libmysqlclient должен быть установлен до начала процесса компиляции.

Если взять во внимание тот факт, что PHP был очень сильно распространен и почти всегда был в связке с MySQL, Oracle (позднее Sun) пришлось принять некоторые меры. А именно, был создан нативный драйвер MySQL — дополнение в PHP, обеспечивающее доступ к MySQL без libmysqlclient.

Этот драйвер (mysqlnd) входит в состав PHP начиная с версии 5.3, а с 5.4 он стал использоваться по-умолчанию (хотя вы всё равно можете использовать libmysqlclient). В новом драйвере значительно повышена работоспособность и улучшена работа с памятью.

Из руководства по MySQL:

Библиотека mysqlnd использует инфраструктуру языка С, на котором написан сам PHP, тем самым упрощая процесс интеграции библиотеки в систему. В дополнение ко всему она использует PHP Streams (абстрагирование операций ввода/вывода), управление памятью и обработку строковых данных средствами PHP. К примеру, использование управление памятью позволяет mysqlnd экономить память путем применения переменных доступных только для чтения, а также накладывать ограничения памяти PHP.

Также библиотека позволяет подключать к себе различные плагины.

Установка

Для того, чтобы установить mysqlnd достаточно скомпилировать одно из трех расширений MySQL. В любом случае не указывайте путь к библиотеке libmysqlclient.

Три доступные библиотеки:

  • ext/pdo_mysql (начиная с PHP 5.1)
  • ext/mysqli (начиная с PHP 5.0)
  • ext/mysql (начиная с PHP 2.0, поддержка прекращена с PHP 5.6)

Обратите внимания на то, что если вы устанавливаете ext/mysql или ext/mysqli, библиотека exp/pdo_mysql подключается автоматически.

Чтобы активировать одно из расширений, укажите следующие флаги:

  • —with-mysql
  • —with-mysqli
  • —with-pdo-mysql

Если вы работаете на Debian или Ubuntu, то установить php5-mysqlnd можно, выполнив следующую команду:

sudo apt-get install php5-mysqlnd

В итоге все связанные с libmysqlclient пакеты будут удалены, а озвученные выше три расширения подключены.

Нативные плагины Mysql

Помимо повышения производительности вы получаете большой плюс в виде возможности подключения плагинов к mysqlnd. Они доступны средствами PECL и устанавливаются следующим путем:

sudo pecl install mysqlnd_<name>

Доступны следующие стабильные версии плагинов:

  • mysqlnd_memcache: перевод SQL на использование memcache протокола MySQL 5.6 совместимого с NoSQL службой.
  • mysqlnd_ms: выполнение разделения операций чтения/записи между основным и дочерним (ms) серверами с поддержкой простой балансировки нагрузки.
  • mysqlnd_qc: добавляет к PHP простой кеш запросов
  • mysqlnd_uh: позволяет создавать плагины для mysqlnd на PHP.

Так как эти плагины созданы для mysqlnd, они работают со всеми тремя расширениями.

Разделение операций записи/чтения

Наиболее полезным плагином является mysqlnd_ms или master/slave. Он позволяет, незаметно для вас, разделить операции чтения и записи между разными серверами.

Настройка

После установки плагина вам потребуется отредактировать php.ini и файл настроек для mysqlnd_ms.

В php.ini (или в mysqlnd_ms на Debian систмах):

extension=mysqlnd_ms.so
mysqlnd_ms.enable=1
mysqlnd_ms.config_file=/path/to/mysqlnd_ms.json

Затем создайте файл mysqlnd_ms.json. В нем вы указываете основные и дочерние сервера, само разделение операций чтения и записи и параметры балансировки нагрузки.

Сама настройка сильно зависит от вашего окружения.

Самая простая настройка должна содержать в себе, как минимум, один основной и один дочерний сервер:

{
   "appname": {
       "master": {
           "master_0": {
               "host": "master.mysql.host",
               "port": "3306",
               "user": "dbuser",
               "password": "dbpassword",
               "db": "dbname"
           }
       },
       "slave": {
           "slave_0": {
               "host": "slave.mysql.host",
               "port": "3306"
               "user": "dbuser",
               "password": "dbpassword",
               "db": "dbname"
           },
       }
   }
}

Среди всех параметров только host является обязательным.

Балансировка нагрузки

Так же mysqlnd_ms способна осуществлять минимальную балансировку нагрузки, применяя один из нескольких подходов:

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

Важно помнить, что при использовании любой стратегии, за исключением последней, при каждом новом запросе выбор происходит полностью изолировано от других запросов. Таким образом, циклический алгоритм применяется к каждому запросу в пределах одного обращения к БД. В таком случае сервера не чередуются при разных обращениях, как обычно работают другие балансировщики.

Именно поэтому, я бы не рекомендовал использовать встроенный балансировщик, а воспользоваться другими решениями, как, например, haproxy. А в настройке указать, что вы используете только один дочерний сервер.

Маршрутизация запросов

По-умолчанию, mysqlnd_ms незаметно для вас направляет все запросы, начинающиеся с SELECT только на дочерние сервера.

В таком подходе есть как свои плюсы, так и минусы. Вам не приходится учитывать это поведение в своем коде. Но при таком упрощенном варианте запрос не анализируется на то, является ли он действительно только запросом чтения.

Более того, он не только не отправит запрос начинающийся с (SELECT на главный сервер, он отправит запрос вида SELECT … INTO на дочерний сервер, что может закончится проблемой.

Отмечу, что плагин имеет возможность указать на какой сервер следует отправлять запрос.

Вы можете указать одну из трех констант в запрос:

  • MYSQLND_MS_MASTER_SWITCH — выполнение на главном сервере
  • MYSQLND_MS_SLAVE_SWITCH — выполнение на дочернем сервере
  • MYSQLND_MS_LAST_USED_SWITCH — выполнение на последнем использованом сервере

Эти константы имеют значение ms=master, ms=slave, andms=last_used соответственно. Но так как строки могут быть изменены, рекомендуется использование констант.

Для применения этих констант, просто укажите их в качестве комментария перед запросом. Самый простой метод — использование sprintf(), что заменит %s на нужное строковое значение.

Например, чтобы отправить SELECT запрос на главный сервер:

<?php
$sql = sprintf("/*%s*/ SELECT * FROM table_name;", MYSQLND_MS_MASTER_SWITCH);

Отправить не SELECT запрос на дочерний сервер:

<?php
$sql = sprintf("/*%s*/ CREATE TEMPORARY TABLE `temp_table_name` SELECT * FROM table_name;", MYSQLND_MS_SLAVE_SWITCH);

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

<?php
if ($request->isPost() && $form->isValid()) {
    $user>setValues($form->getValues());
    $user->save();
}
$sql = sprintf("/*%s*/ SELECT * FROM user_session WHERE user_id = :user_id", MYSQLND_LAST_USED_SWITCH);

В этом случае главный сервер будет использован только если в предыдущем запросе обращение происходило тоже к нему. В данном примере главный сервер используется, если были обновлены пользовательские данные ($user->save()).

Заключение

Плагин mysqlnd_ms особенно полезен в случае, если вы переводите старое приложение на работу с современными системами распределения операций чтения/записи. Может быть это и не лучший подход, но это больше, чем половина пути, причем без необходимости изменения существующего кода.