API интеграции плагинов в драйвер MySQL Native Driver
Драйвер MySQL Native Driver, или mysqlnd, предоставляет API, через который плагины встраиваются
в драйвер.
Плагины драйвера mysqlnd
работают на уровне между PHP-приложениями и MySQL-сервером, аналогично
приложению MySQL Proxy, которое также работает между клиентскими приложениями и сервером MySQL.
Плагины драйвера mysqlnd выполняют типовые
задачи MySQL Proxy наподобие балансировки нагрузки, мониторинга и оптимизации
быстродействия. Из-за другой архитектуры и места обработки запросов
плагины драйвера mysqlnd не содержат недочётов MySQL Proxy наподобие
единой точки отказа, не требуют установки отдельного прокси-сервера
и изучения нового языка программирования Lua.
Плагин mysqlnd расширяет функции драйвера
mysqlnd. Плагин умеет перехватывать вызовы функций
драйвера mysqlnd из MySQL-модулей PHP наподобие
ext/mysql, ext/mysqli и PDO_MYSQL.
Поэтому плагинам драйвера mysqlnd доступен перехват каждого запроса,
который модули выполнили из клиентского приложения.
Плагины умеют перехватывать или подменять вызовы внутренних функций драйвера mysqlnd.
API не ограничивает модификацию таблиц внутренних функций драйвера mysqlnd
и разрешает перенаправлять вызовы конкретных функций драйвера mysqlnd
из mysqlnd-модулей на функции mysqlnd-плагина.
Управление таблицей внутренних функций
драйвера mysqlnd даёт плагинам максимум гибкости.
Плагины драйвера mysqlnd — фактически PHP-модули на языке C,
которые работают через API интеграции mysqlnd-плагинов
в PHP-драйвер MySQL Native Driver, или mysqlnd.
Плагины на 100 % прозрачны
для PHP-приложений. Приложение не требуется изменять, поскольку плагины работают
на другом уровне. С точки зрения поведения, mysqlnd-плагин
работает на уровень ниже mysqlnd-драйвера, поскольку переопределяет поведение драйвера.
Следующий список приводит примеры практических задач,
которые решают посредством плагинов драйвера mysqlnd.
Балансировка нагрузки
Разделение чтения и записи. Пример —
модуль PECL/mysqlnd_ms (Master Slave). Модуль разделяет
запросы на чтение и запись для настройки репликации.
Отказоустойчивость
Циклическое распределение запросов по принципу Round-Robin для равномерной нагрузки, распределение на наименее загруженный сервер
Мониторинг
Логирование запросов
Анализ запросов
Аудит запросов. Пример —
модуль PECL/mysqlnd_sip (SQL Injection Protection, защита
от SQL-инъекций). Модуль инспектирует запросы
и выполняет только те, которые соответствуют набору правил.
Производительность.
Кеширование. Пример —
модуль PECL/mysqlnd_qc (Query Cache, кеширование запросов).
Ограничение числа запросов
Шардирование. Пример — модуль PECL/mysqlnd_mc
(Multi Connect). Модуль пытается разбить запрос
SELECT на n частей через SELECT ... LIMIT part_1,
SELECT LIMIT part_n. Модуль отправляет запросы
на отдельные MySQL-серверы и объединяет результаты на клиенте.
Доступные плагины драйвера MySQL Native Driver
Репозитории PECL уже содержит ряд mysqlnd-плагинов, включая следующие:
mysqlnd_mc — Multi
Connect, разбивка запросов по шардам.
mysqlnd_ms — Master
Slave, разделение запросов на чтение и запросов на запись.
mysqlnd_qc — Query
Cache, кеширование результатов запросов.
mysqlnd_pscache —
Prepared Statement Handle Cache, обработка
кеширования подготовленных запросов.
mysqlnd_sip — SQL
Injection Protection, защита от SQL-инъекций.
mysqlnd_uh — User
Handler, обработка пользователей.
Сравнение плагинов mysqlnd с MySQL Proxy
Плагины mysqlnd и приложение MySQL Proxy — разные
технологии с разными подходами. Оба инструмента решают
стандартные задачи наподобие балансировки
нагрузки, мониторинга и улучшения производительности.
Важное отличие состоит в том, что приложение MySQL Proxy работает
со всеми MySQL-клиентами, тогда как плагины
mysqlnd работают только с PHP-приложениями.
Как PHP-модуль, плагин mysqlnd устанавливается
на сервер PHP-приложений вместе с остальными компонентами
PHP-дистрибутива. MySQL Proxy запускают на сервере
приложений PHP или устанавливают на отдельной машине
для поддержки множественных серверов PHP-приложений.
Установка MySQL Proxy на сервере приложений даёт два преимущества:
Отсутствие единой точки отказа
Лёгкость в горизонтальном масштабировании и масштабировании за счёт клиента
Приложение MySQL Proxy и плагины mysqlnd
легко решают проблемы, которые иначе
потребовали бы изменений в текущих приложениях.
Тем не менее, MySQL Proxy содержит и ряд недостатков:
MySQL Proxy — новый элемент и технология, которую потребуется изучить и установить.
MySQL Proxy требует знания скриптового языка Lua.
Приложения MySQL Proxy настраиваются под конкретные задачи средствами языков C и Lua.
Lua — предпочтительный скриптовый язык для MySQL
Proxy. Для большей части PHP-экспертов Lua новый
язык, который потребуется изучать. Плагин
драйвера mysqlnd пишут на C
или на PHP через модуль PECL/mysqlnd_uh.
MySQL Proxy работает как демон — фоновый процесс. MySQL Proxy может вспомнить ранее
принятые решения, так как все состояние может быть сохранено. Однако плагины
mysqlnd привязаны к жизненному циклу PHP, базирующемуся на
запросах. Также MySQL Proxy может разделять единожды полученный результат между
разными серверами приложений. Плагины mysqlnd для решения этой
задачи должны использовать какое-нибудь постоянное хранилище, для сохранения
результатов между запросами. Например, для этого может быть использован другой демон,
такой как Memcache. Так что в этом случае MySQL Proxy работает явно лучше.
MySQL Proxy работает поверх сетевых протоколов. С помощью
MySQL Proxy вы можете разобрать и подвергнуть инженерному
анализу протокол MySQL Client Server. Что-либо поменять
можно только манипулируя протоколом обмена. Если протокол
вдруг поменяется (что бывает крайне редко), скрипты
MySQL Proxy потребуется переписывать.
Плагины Mysqlnd работают поверх C API,
который дублирует клиент libmysqlclient. Этот API-интерфейс языка C по сути обычная обёртка вокруг
протокола MySQL Client Server. Вы можете перехватывать каждый
вызов API-интерфейса C. PHP использует API-интерфейс C, фактически можно
перехватывать вообще каждый вызов PHP, без программирования на уровне протокола обмена.
Mysqlnd реализует протокол обмена.
Таким образом, плагины могут перехватывать, исследовать, менять
и даже целиком заменять протокол связи. Хотя
обычно ничего этого не требуется.
Плагины позволяют вам использовать два уровня (C API и
протокол обмена), в этом они гораздо гибче, чем MySQL
Proxy. Если плагин mysqlnd реализован с
использованием C API, изменения протокола обмена не
потребуют изменения плагина.
Получение API плагинов mysqlnd
API плагинов mysqlnd является частью
стандартного модуля ext/mysqlnd.
API плагинов mysqlnd начали разрабатывать
в декабре 2009 года. Он разрабатывался как часть
репозитория исходных кодов PHP и, соответственно,
доступен через публичный репозиторий Git либо через
загрузку снапшота исходных кодов.
Разработчики плагинов узнают версию mysqlnd путём доступа
к MYSQLND_VERSION — строка в формате mysqlnd 8.3.17,
или через MYSQLND_VERSION_ID — числовое представление версии
наподобие 50007. Разработчики вычисляют номер версии из этого числа следующим способом:
Разработчики отслеживают версию драйвера
mysqlnd на предмет совместимости и версионного тестирования,
поскольку версии mysqlnd иногда изменяется
в процессе работы над плагином.
Архитектура плагинов MySQL Native Driver
В секции рассматривается архитектура плагинов
драйвера mysqlnd.
Обзор MySQL Native Driver
Перед началом разработки плагинов для драйвера
mysqlnd полезно узнать,
как организован сам драйвер mysqlnd.
Mysqlnd состоит из следующих модулей:
Объектно-ориентированная парадигма C
На уровне кода, mysqlnd использует
паттерн C для реализации объектно-ориентированного подхода.
В C объекты описывают используя struct.
Члены структуры являются свойствами объекта.
Члены структуры, указывающие на функции, являются
методами.
В отличие от таких языков как C++ или Java, в C нет фиксированных правил наследования.
Однако существуют некоторые договорённости, которым необходимо следовать,
но это мы обсудим позже.
Жизненный цикл PHP
При рассмотрении жизненного цикла PHP существует два основных цикла:
Цикл старта и остановки движка PHP
Цикл обработки запроса
При старте движка PHP, первым делом вызывается функция
инициализации модулей (MINIT) для каждого зарегистрированного модуля. Это
позволяет каждому модулю установить переменные и выделить ресурсы,
которые будут задействованы все время жизни процесса
движка PHP. Когда движок PHP выключается, он вызывает
функцию остановки модулей (MSHUTDOWN) для каждого модуля.
На протяжении жизненного цикла движка PHP, он принимает некоторое количество
запросов. Каждый запрос порождает новый жизненный цикл. На каждый запрос, движок PHP
вызывает функцию инициализации для каждого модуля. Модуль может предпринять
выставление переменных и выделение ресурсов, требуемых для обслуживания запроса.
По окончании жизни запроса, движок вызывает функцию остановки запроса (RSHUTDOWN)
для каждого модуля, что позволяет им произвести необходимые чистки.
Как работает плагин
Плагин mysqlnd работает перехватывая
вызовы модулей, использующих mysqlnd,
к mysqlnd. Это достигается подменой
таблицы функций mysqlnd на созданную
плагином.
Следующий код демонстрирует замену таблицы функций
mysqlnd:
query = MYSQLND_METHOD(my_conn_class, query);
}
]]>
Манипуляцией с таблицей функций соединения необходимо
заниматься на этапе инициализации модуля (MINIT). Таблица
функций — глобальный разделяемый ресурс.
В многопоточном окружении, со сборкой TSRM, манипуляция
глобальным разделяемым ресурсом на этапе обработки
запроса вызовет конфликты.
Не используйте логику, которая связана
с фиксированным размером при манипуляции с таблицей
функций mysqlnd. Всегда добавляйте
новые методы в конец таблицы, так как сама таблица может
в будущем в любой момент измениться.
Вызов родительских методов
Если записи оригинальной таблицы функций сохранились,
остаётся возможность вызвать оригинальный метод — родительский.
В отдельных случаях, например, для Connection::stmt_init(), жизненно
важно сначала вызвать родительский метод, и только потом делать
что-либо в новом методе.
Расширение свойств
Объекты mysqlnd представлены как C struct.
Невозможно добавить члена в C struct во время исполнения.
Пользователи объектов mysqlnd не могут
просто добавить свойства объекту.
Произвольные данные (свойства) могут быть добавлены к
объекту mysqlnd с использованием
соответствующей функции из семейства
mysqlnd_plugin_get_plugin_<object>_data().
При размещении объекта mysqlnd
резервируется место в конце объекта для удержания
void * указателя на произвольные данные.
mysqlnd резервирует место для одного
void * указателя на плагин.
В следующей таблице показано, как вычислить положение указателя для конкретного плагина:
Расчёт указателя для mysqlndАдрес памятиСодержимое0Начало объекта mysqlnd (C struct)nКонец объекта mysqlnd (C struct)n + (m x sizeof(void*))void* для данных объекта плагина номер m
Если вы планируете делать подкласс от одного из конструкторов объекта
mysqlnd, которые разрешены, имейте это в виду!
Следующий код демонстрирует расширение свойств:
persistent);
(*props)->query_counter = 0;
}
return props;
}
]]>
Разработчик плагина отвечает за управление памятью данных плагина.
Рекомендуется использовать управление памятью mysqlnd для данных
плагина. Эти функции именуются используя такие соглашения:
mnd_*loc(). Управление памятью mysqlnd имеет
ряд полезных свойств, таких как использование отладочного
модуля управления памятью в неотладочных сборках.
Когда и как создавать подклассКогда создавать подкласс?Каждый экземпляр имеет свою собственную таблицу функций?Как создавать подкласс?Соединение (MYSQLND)MINITНетmysqlnd_conn_get_methods()Результирующий набор (MYSQLND_RES)MINIT or laterДаmysqlnd_result_get_methods() или методом объекта,
манипулирующим таблицей функцийРезультирующий набор (MYSQLND_RES_METADATA)MINITНетmysqlnd_result_metadata_get_methods()Оператор (MYSQLND_STMT)MINITНетmysqlnd_stmt_get_methods()Сеть (MYSQLND_NET)MINIT или позжеДаmysqlnd_net_get_methods() или методом объекта,
манипулирующим таблицей функцийПротокол обмена (MYSQLND_PROTOCOL)MINIT или позжеДаmysqlnd_protocol_get_methods() или методом
объекта, манипулирующим таблицей функций
Вы не должны манипулировать таблицей функций после
MINIT, если это прямо не разрешено в таблице выше.
Некоторые классы содержат указатель на таблицу функций методов. Все экземпляры
подобных классов должны делить одну и ту же таблицу функций. Для того, чтобы избежать
хаоса, особенно в многопоточном окружении, управлять такими таблицами функций стоит
только во время MINIT.
Прочие классы используют копии глобально разделённых таблиц функций. Таблица функций
создаётся одновременно с объектом. Каждый объект использует свою таблицу. Это даёт вам
две возможности: вы можете управлять таблицей функций по умолчанию для объекта во
время MINIT, а также вы можете изменять методы объекта не затрагивая другие
экземпляры этого же класса.
Преимущество разделяемой таблицы функций в производительности, так как нет нужды
копировать таблицу функций отдельно для каждого объекта.
Статус конструктораТипРазмещение, создание, сбросМожет быть изменено?ВызывающийConnection (MYSQLND)mysqlnd_init()Нетmysqlnd_connect()Результирующий набор (MYSQLND_RES)
Размещение:
Connection::result_init()
Сброс и повторная инициализация во время:
Result::use_result()
Result::store_result
Да, но вызовите родителя!
Connection::list_fields()
Statement::get_result()
Statement::prepare() (Только метаданные)
Statement::resultMetaData()
Метаданные результирующего набора (MYSQLND_RES_METADATA)Connection::result_meta_init()Да, но вызовите родителя!Result::read_result_metadata()Оператор (MYSQLND_STMT)Connection::stmt_init()Да, но вызовите родителя!Connection::stmt_init()Сеть (MYSQLND_NET)mysqlnd_net_init()НетConnection::init()Протокол обмена (MYSQLND_PROTOCOL)mysqlnd_protocol_init()НетConnection::init()
Настоятельно рекомендуется не заменять конструктор
целиком. Конструкторы производят выделение памяти.
Выделение памяти жизненно необходимо для API плагинов
mysqlnd и для логики объекта
mysqlnd. Если вам не страшны
предупреждения и хотите сильно поменять конструктор, то
хотя бы вызовите родительский конструктор прежде, чем что-либо делать.
Несмотря на предупреждения, это бывает полезным для конструктора подкласса.
Конструкторы — отличное место для изменения таблицы функций для объектов,
которые не работают с разделённой таблицу, например,
результирующий набор, сеть, протокол обмена.
Статус уничтоженияПроизводный метод должен вызвать родительский?ДеструкторСоединениеда, после выполнения методаfree_contents(), end_psession()Результирующий наборда, после выполнения методаfree_result()Метаданные результирующего наборада, после выполнения методаfree()Операторда, после выполнения методаdtor(), free_stmt_content()Сетьда, после выполнения методаfree()Протокол обменада, после выполнения методаfree()
Деструкторы являются подходящим местом, чтобы освободить ресурсы, занимаемые
свойствами
mysqlnd_plugin_get_plugin_<object>_data().
Перечисленные деструкторы могут не совпадать с актуальными методами
mysqlnd для очистки самого объекта. Однако они являются самым лучшим
местом, куда вы можете вклиниться для очистки данных своего плагина. Так же как и
с конструкторами, вы можете полностью переопределить эти методы, но делать это не
рекомендуется. Если вам необходимо вставить в каждый из перечисленных методов
очистку данных своего плагина, то необходимо обеспечить запуск родительских
методов mysqlnd.
Для плагинов рекомендуют метод — выполнить код очистки данных плагина
и сразу после этого вызывать родительский метод.
API плагинов mysqlnd
API-интерфейс плагинов mysqlnd поддерживает следующие функции:
mysqlnd_plugin_register()
mysqlnd_plugin_count()
mysqlnd_plugin_get_plugin_connection_data()
mysqlnd_plugin_get_plugin_result_data()
mysqlnd_plugin_get_plugin_stmt_data()
mysqlnd_plugin_get_plugin_net_data()
mysqlnd_plugin_get_plugin_protocol_data()
mysqlnd_conn_get_methods()
mysqlnd_result_get_methods()
mysqlnd_result_meta_get_methods()
mysqlnd_stmt_get_methods()
mysqlnd_net_get_methods()
mysqlnd_protocol_get_methods()
Нет стандартных определений того, что такое плагин и как он работает.
Часто встречающиеся в плагинах компоненты:
Менеджер плагина
API плагина
Сервисы приложения (или модули)
API сервисов приложения (или API модулей)
Концепция плагина mysqlnd эксплуатирует эту функциональность и,
кроме того, радует нас открытой архитектурой.
Нет запретов
Плагин имеет полный доступ ко всем внутренностям mysqlnd.
Нет ограничений или запретов, связанных с безопасностью. Все что угодно можно
переписать для реализации дружественных или враждебных алгоритмов, так что
рекомендуется ставить плагины только из доверенных источников.
Как обсуждалось выше, плагины могут свободно использовать указатели. Эти указатели
ничем не ограничены и могут указывать на данные другого плагина. Простейшая
арифметическая операция позволит получить доступ к данным другого плагина.
Рекомендуется писать сотрудничающие плагины, которые могут работать сообща с другими
плагинами и всегда вызывать родительские методы. Плагины никогда не должны вести себя
враждебно к самому mysqlnd.
Проблемы: пример сотрудничества и построения цепочкиМодульУказатель mysqlnd.query()Стек вызова, если вызывается родительext/mysqlndmysqlnd.query()mysqlnd.queryext/mysqlnd_cachemysqlnd_cache.query()
mysqlnd_cache.query()
mysqlnd.query
ext/mysqlnd_monitormysqlnd_monitor.query()
mysqlnd_monitor.query()
mysqlnd_cache.query()
mysqlnd.query
В этом сценарии загружены плагины кеша (ext/mysqlnd_cache) и
мониторинга (ext/mysqlnd_monitor).
Оба наследуют класс Connection::query(). регистрация плагинов
происходит на этапе MINIT в соответствии с описанной
выше логикой. PHP, по умолчанию, вызывает модули в алфавитном порядке.
Плагины не знают друг о друге и не накладывают каких-либо зависимостей.
По умолчанию, плагины вызывают родительский метод query из своей, переопределённой,
версии этого метода.
Резюме по модулю PHP
Повторение пройденного материала на примере поведения плагина
ext/mysqlnd_plugin, использующего API плагинов
mysqlnd для PHP:
Любое приложение PHP, использующее MySQL пытается установить соединение по
адресу 192.168.2.29
Приложение использует одно из следующих модулей ext/mysql,
ext/mysqli или PDO_MYSQL. Все
три модуля используют mysqlnd для соединения с 192.168.2.29.
Mysqlnd вызывает метод соединения, который наследуется
плагином ext/mysqlnd_plugin.
ext/mysqlnd_plugin вызывает зарегистрированный
пользователем метод proxy::connect().
Этот метод подменяет IP адрес соединения с 192.168.2.29
на 127.0.0.1 и возвращает установленное parent::connect() соединение.
ext/mysqlnd_plugin делает то же самое, что и
parent::connect(127.0.0.1), вызывая оригинальный метод
mysqlnd для соединения.
ext/mysqlnd устанавливает соединение и возвращает его
ext/mysqlnd_plugin.
ext/mysqlnd_plugin, в свою очередь, передаёт его дальше.
Без разницы, какой модуль был использован, он всё равно получит
соединение к 127.0.0.1. После этого, модуль возвращает это соединение
приложению. Круг замкнулся.
Начинаем разработку плагина mysqlnd
Важно помнить, что плагин mysqlnd сам по себе
является модулем PHP.
Следующий пример показывает базовую структуру функции MINIT,
использующуюся в типичном плагине mysqlnd:
query = MYSQLND_METHOD(mysqlnd_plugin_conn, query);
conn_m->connect = MYSQLND_METHOD(mysqlnd_plugin_conn, connect);
}
]]>
Анализ задачи: от C до пользовательского пространства
Процесс:
PHP: пользователь регистрирует callback-функцию плагина
PHP: пользователь вызывает PHP MySQL API для соединения с MySQL
C: ext/*mysql* вызывает метод mysqlnd
C: mysqlnd обрывается в ext/mysqlnd_plugin
C: ext/mysqlnd_plugin
Вызывает пользовательскую callback-функцию
Или оригинальный метод mysqlnd, если она не задана
Вам необходимо выполнить следующие действия:
Создайте в С класс "mysqlnd_plugin_connection"
Примите и зарегистрируйте прокси объект с помощью
"mysqlnd_plugin_set_conn_proxy()"
Вызовите прокси методы пространства пользователя из C (оптимизация -
zend_interfaces.h)
Методы объекта пространства пользователя должны быть вызваны с помощью
call_user_function() или на уровень ниже, с помощью
zend_call_method().
Оптимизация: вызывайте методы из С с помощью
zend_call_method
Следующий кусок кода демонстрирует прототип функции
zend_call_method, взятый из
zend_interfaces.h.
API движка Zend поддерживает только два аргумента. Иногда требуется больше. Например:
Для обхода этой проблемы вам необходимо сделать копию
zend_call_method() и добавить необходимые параметры.
Вы можете сделать это создав набор макросов
MY_ZEND_CALL_METHOD_WRAPPER.
Обращение к пространству пользователя PHP
Этот кусок кода демонстрирует оптимизированный метод вызова функций пространства
пользователя из С:
Обращение к пространству пользователя: простые аргументы
Обращение к пространству пользователя: структуры как аргументы
первый аргумент многих методов mysqlnd — «объекты» С.
Например, первый аргумент метода connect() — указатель
на MYSQLND. Структура MYSQLND —
объект соединения mysqlnd.
Указатель на объект соединения mysqlnd сравнивают со стандартным
обработчиком файлового ввода-вывода. Так же как и он, объект соединения
mysqlnd связывают с пространством пользователя
через PHP-тип resource.
Из C в пространство пользователя и обратно
Пользователи PHP должны иметь возможность вызывать родительские
реализации переопределённых методов.
В результате наследования возможно «уточнить» только выбранные
методы и вы можете выбрать, когда выполнять ваш код, до или после родительского.
Встроенный класс:
mysqlnd_plugin_connection::connect()