API для плагинов к встроенному драйверу MySQL API для плагинов к встроенному драйверу MySQL является особенностью встроенного драйвера MySQL (mysqlnd). Плагины mysqlnd работают на уровне между PHP-приложениями и сервером MySQL. Это похоже на работу MySQL Proxy. Плагины mysqlnd могут реализовывать стандартные задачи MySQL Proxy, такие как балансирование нагрузки, мониторинг и оптимизация быстродействия. При этом, вследствие другой архитектуры и месторасположения, плагины mysqlnd не имеют некоторых недочётов MySQL Proxy. Например, нет единой точки отказа, не требуется установка отдельного proxy-сервера и нет необходимости в изучении нового языка программирования Lua. Плагин mysqlnd можно рассматривать как модуль mysqlnd. Плагины могут перехватывать большую часть функций mysqlnd. Эти функции вызываются модулями PHP, такими как ext/mysql, ext/mysqli и PDO_MYSQL. В конечном счёте, плагины mysqlnd могут перехватывать все запросы, сделанные этими модулями из клиентского приложения. Внутренние вызовы функций mysqlnd также могут быть перехвачены или заменены. Ограничения по работе с внутренней таблицей функций mysqlnd отсутствуют. Возможно настроить все таким образом, что при вызове определённых функций mysqlnd из модулей, использующих mysqlnd, этот вызов будет перенаправляться в соответствующую функцию плагина mysqlnd. Возможность манипулировать внутренней таблицей функций mysqlnd даёт плагинам максимум гибкости. Плагины mysqlnd фактически являются модулями PHP, написанными на C и использующими API для плагинов mysqlnd (встроенном в драйвер mysqlnd). Плагины могут быть полностью прозрачными для PHP-приложений. Изменять приложение не потребуется, поскольку плагины работают на другом уровне. Плагины mysqlnd можно рассматривать как работающие на уровень ниже mysqlnd. Нижеуказанный список показывает несколько возможных вариантов плагинов mysqlnd. Балансировка нагрузки Разделение чтения и записи. Примером является модуль PECL/mysqlnd_ms (Master Slave). Модель разделяет запросы на чтение и запись для настройки репликации. Отказоустойчивость Равномерная загрузка, запросы на наименее загруженный сервер Мониторинг Логирование запросов Анализ запросов Аудит запросов. Примером является модуль 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 Существует несколько уже доступных плагинов mysqlnd. Список включает: PECL/mysqlnd_mc - Multi Connect plugin. PECL/mysqlnd_ms - Master Slave plugin. PECL/mysqlnd_qc - Query Cache (кеширование запросов) plugin. PECL/mysqlnd_pscache - Prepared Statement Handle Cache plugin (обработка кеширования подготовленных запросов) PECL/mysqlnd_sip - SQL Injection Protection plugin (защита от SQL-инъекций) PECL/mysqlnd_uh - User Handler plugin (обработка пользователей)
Сравнение плагинов 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. Это C API по сути обычная обёртка вокруг протокола MySQL Client Server. Вы можете перехватывать все вызовы C API. PHP использует C API, фактически вы можете перехватывать вообще все вызовы PHP, без необходимости программировать на уровне протокола обмена. Mysqlnd реализует протокол обмена. Таким образом, плагины могут перехватывать, исследовать, менять и даже целиком заменять протокол связи. Хотя обычно ничего этого не требуется. Плагины позволяют вам использовать два уровня (C API и протокол обмена), в этом они гораздо гибче, чем MySQL Proxy. Если плагин mysqlnd реализован с использованием C API, изменения протокола обмена не потребуют изменения плагина.
Получение API плагинов mysqlnd API плагинов mysqlnd является частью стандартного модуля ext/mysqlnd. API плагинов mysqlnd начали разрабатывать в декабре 2009 года. Он разрабатывался как часть репозитория исходных кодов PHP и, соответственно, доступен через публичный репозиторий Git либо через загрузку снапшота исходных кодов. Разработчики плагинов могут узнать версию mysqlnd путём доступа к MYSQLND_VERSION, являющейся строкой формата mysqlnd 5.0.7-dev - 091210 - $Revision: 300535, или через MYSQLND_VERSION_ID, являющейся числовым представлением версии, например 50007. Рассчитать версию из этого числа можно так: Таблица расчёта MYSQLND_VERSION_ID Версия (часть) Пример Major*10000 5*10000 = 50000 Minor*100 0*100 = 0 Patch 7 = 7 MYSQLND_VERSION_ID 50007
В процессе разработки, разработчики должны отслеживать версию mysqlnd на предмет совместимости и версионного тестирования, так как версии mysqlnd имеют свойство меняться и это может случиться в процессе работы над плагином.
Архитектура плагинов MySQL Native Driver В этой секции рассмотрена архитектура плагинов mysqlnd. Поверхностный обзор MySQL Native Driver Перед началом разработки плагинов mysqlnd, полезно ознакомиться, как сам по себе организован mysqlnd. Mysqlnd состоит из следующих модулей: Организационная схема mysqlnd, помодульно Модули статистики mysqlnd_statistics.c Соединение mysqlnd.c Результирующий набор mysqlnd_result.c Метаданные результирующего набора mysqlnd_result_meta.c Оператор mysqlnd_ps.c Сеть mysqlnd_net.c Протокол обмена mysqlnd_wireprotocol.c
Объектно-ориентированная парадигма 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/mysqlnd mysqlnd.query() mysqlnd.query ext/mysqlnd_cache mysqlnd_cache.query() mysqlnd_cache.query() mysqlnd.query ext/mysqlnd_monitor mysqlnd_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. Zend API поддерживает только два аргумента. Вам может понадобиться больше. К примеру: Для обхода этой проблемы вам необходимо сделать копию zend_call_method() и добавить необходимые параметры. Вы можете сделать это создав набор макросов MY_ZEND_CALL_METHOD_WRAPPER. Обращение к пространству пользователя PHP Этот кусок кода демонстрирует оптимизированный метод вызова функций пространства пользователя из С: Обращение к пространству пользователя: простые аргументы Обращение к пространству пользователя: структуры как аргументы первый аргумент многих методов mysqlnd - это "объекты" С. К примеру, первый аргумент метода connect() является указателем на MYSQLND. Структура MYSQLND представляет собой объект соединения mysqlnd. Указатель на объект соединения mysqlnd можно сравнить со стандартным обработчиком файлового ввода/вывода. Так же как и он, объект соединения mysqlnd должен быть связан с пространством пользователя с использованием PHP типом "resource". Из C в пространство пользователя и обратно Пользователи PHP должны иметь возможность вызывать родительские реализации переопределённых методов. В результате наследования возможно "уточнить" только выбранные методы и вы можете выбрать, когда выполнять ваш код, до или после родительского. Встроенный класс: mysqlnd_plugin_connection::connect()