Квест → Как хакнуть форму
Прошли: 77
От переводчика: нам показалось, что эту статью долго не только переводить, но и читать. Поэтому мы решили разбить её на две части.
В первой части нашего цикла статей мы бегло рассмотрели mysqlnd_ms, плагин к MySQLnd для разделения чтения и записи. В этой статье мы рассмотрим его более продвинутое использование.
Те, кто внимательно читал первую часть могли заметить, что в файле конфигурации mysqlnd_ms имеется ключ первого уровня (в дальнейшем примере "appname"), который содержит все наши настройки. Это позволяет задать несколько вариантов конфигураций в одном файле.
Итак, у вас есть:
{ "appname": { "master": { ... }, "slave": { ... } } }
Воспользовавшись ключом в качестве хоста, мы подключим нужную конфигурацию.
Используя mysqli:
$mysqli = new mysqli("appname", "user", "password", "dbname");
Или используя PDO:
$pdo = new PDO("mysql:host=appname;dbname=dbname", "user", "password");
На заметку: это нужно делать всегда, даже когда в файле всего одна конфигурация. В противном случае настройки mysqlnd_ms не будут использоваться в принципе.
Плагин mysqlnd_ms позволяет обрабатывать ситуацию отказа, если попытка соединения с сервером провалилась (что обычно происходит в момент первого запроса). Эта возможность по умолчанию отключена, т.к. могут быть конфликты состояний (например, с пользовательскими переменными SQL). Но это легко исправить, достаточно настроить опцию failover.strategy в конфигурации.
Эта опция имеет три допустимых значения:
disabled
: никогда не обрабатывать отказ. Это значение по умолчанию.master
: ведущий сервер всегда перехватывает управление при отказе.loop_before_master
: если была попытка обращения к ведомому, то сначала по-очереди будут опрошены все ведомые сервера, и только потом в дело вступит ведущий.{ "appname": { "master": { ... }, "slave": { ... } "failover": { "strategy": "loop_before_master" } } }
Дополнительно, вы можете добавить ещё две опции:
remember_failed
: указываем плагину, что нужно запоминать "проштрафившиеся" сервера до конца сессии. По умолчанию, отключена, но всячески рекомендуется включить.max_retries
: количество попыток обращений к серверу прежде, чем повесить на него клеймо “штрафника”.
За одну итерацию к каждому серверу идёт только одно обращение и после N попыток сервер-неудачник будет исключён из очереди. По умолчанию стоит 0, то есть безлимитное количество попыток. Это означает, что сервер никогда не исключат и это, к слову, конфликтует с предыдущей опцией (remember_failed
).
{ "appname": { "master": { ... }, "slave": { ... } "failover": { "strategy": "loop_before_master", "remember_failed": true, "max_retries": 1 } } }
На заметку: при транзакциях (см. ниже), любые настройки отказоустойчивости безоговорочно отключаются.
Может случится так, что в рамках транзакция раскидает свои запросы на разные сервера. Это один из неприятных подводных камней при разделение запросов и балансировки нагрузки между несколькими read-серверами.
Неприятности начинаются, когда вы выполняете INSERT
/UPDATE
/DELETE
и потом хотите сделать SELECT
-запрос на уже изменённую информацию до того, как закоммитить транзакцию.
Чтобы решить эту проблему, вы можете использовать SQL-указатели MYSQLND_MS_MASTER_SWITCH
либо MYSQLND_MS_LAST_USED_SWITCH
. Если вы используете mysqli
или PDO
, то можно позволить плагину автоматически отслеживать транзакции.
Чтобы сделать последнее, вы должны либо выключить авто-коммит при старте транзакции, а при завершении включить назад, либо привязаться к методам транзакции.
Для начала вам надо настроить конфигурацию плагина, чтобы можно было привязаться к транзакциям:
{ "appname": { "master": { ... }, "slave": { ... }, "trx_stickiness": "master" } }
Далее, чтобы использовать авто-коммит, делаем:
// для расширения MySQLi: $mysqli->autocommit(false); // отключаем авто-коммит $mysqli->query('BEGIN'); // стартуем транзакцию // здесь список ваших запросов $mysqli->query('COMMIT'); // или ROLLBACK $mysqli->autocommit(true); // включаем авто-коммит обратно // для расширения PDO: $pdo->setAttribute('PDO::ATTR_AUTOCOMMIT', false); // отключаем авто-коммит $pdo->exec('BEGIN'); // стартуем транзакцию // здесь список ваших запросов $pdo->exec('COMMIT'); // или ROLLBACK $pdo->setAttribute('PDO::ATTR_AUTOCOMMIT', true); // включаем авто-коммит обратно
Плагин отслеживает начало транзакции, её коммит и откат. Он отправит ведущему серверу все обращения, когда вы вызовите PDO->beginTransaction()
или mysqli->beginTransaction()
, в то время как PDO->commit()
, PDO->rollback()
, mysqli->commit()
или mysqli->rollback()
завершат транзакцию.
mysqlnd_ms
поддерживает цепочку фильтров для определения какой из серверов в итоге будет использован для исполнения запроса.
Существует два типа фильтров:
Вы можете указать множество multi фильтров и множество single фильтров, каждый из которых работает с результатами предыдущего, по цепочке.
Фильтры применяются в порядке их объявления в конфигурационном файле. Цепочка обязана заканчиваться single фильтром, который, обычно, является пользовательским (user) или одним из фильтров балансировщика нагрузки: random
, random_once
, roundrobin
.
Первый фильтр, который мы рассмотрим будет multi фильтр qos (аббревиатура от quality of service, качество обслуживания).
Одна из основных причин использовать указатель MYSQLND_MS_LAST_USED_SWITCH
— это ситуация, когда мы отправляем ведущему серверу запрос на чтение сразу же после запроса на запись. Тем самым мы не столкнемся с задержкой репликации. Во всех остальных случаях используется ведомый сервер, чтобы распределить нагрузку.
Этого также можно добиться, используя настройку master_on_write в конфигурации:
{ "appname": { "master": { ... }, "slave": { ... } "master_on_write": 1 } }
Однако, это означает, что ВСЕ оставшиеся запросы в request-е уйдут ведущему серверу. Для запросов, на которые не влияет запись, спровоцировавшая переключение, это не нужно.
В последних версиях mysqlnd_ms
попытались исправить это с помощью qos-фильтра. Этот фильтр позволяет подойти к процессу распределения более детализировано, когда дело касается задержки репликации в сценарии запись — затем — чтение.
Существуют три типа работы фильтра, которые являются уровнями в конфигурации:
master_on_write=1
. К тому же, есть возможность, используя Global Transaction IDs (GTIDs), автоматически подключать сервера, которые уже реплицировали данные.
Чтобы настроить mysqlnd_ms
вы можете либо добавить фильтр в файл конфигурации:
{ "appname": { "master": { ... }, "slave": { ... } "filters": { "quality_of_service": { "TYPEOF_consistency": { "OPTION": VALUE } }, "random": 1 }, } }
Либо вы можете установить это программным образом, используя:
mysqlnd_ms_set_qos($connection, MYSQLND_MS_QOS_CONSISTENCY_[ТИП], MYSQLND_MS_QOS_OPTION_[КЛЮЧ], $value);
Простейший из типов - это консистентность в конечном счёте. Этот тип явно проверяет значение Время_Отставания_Реплики_От_Мастера (Seconds_Behind_Master) каждого ведомого. Все, у кого это время меньше или равно заданному значению, считаются потенциальными кандидатами на чтение. Однако, это значение задаётся в целых секундах, и это означает, что проблема задержки реплики остаётся актуальной. Плюс, зависимость от быстродействия сети.
Используя эту консистентность, мы говорим mysqlnd_ms
использовать только те ведомые сервера, которые отстают от ведущего сервера не более, чем на N секунд. Чтобы настроить плагин мы либо пишем в конфиге:
{ "appname": { "master": { ... }, "slave": { ... } "filters": { "quality_of_service": { "eventual_consistency": { "age": 1 } } } } }
Либо:
mysqlnd_ms_set_qos($connection, MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL, MYSQLND_MS_QOS_OPTION_AGE, 1);
Тем самам плагин начинает использовать только те узлы, которые отстают от ведущего на 1 или менее секунд.
Уверенность в том, что мы используем только те сервера, которые синхронизированы с ведущим, даёт нам Сессионная консистентность. Это решение может быть медленнее, зато более надёжное, чем Консистентность в конечном счёте.
Сессионная консистентность добивается этого, используя Global Transaction IDs (сокращённо GTIDs). В MySQL 5.6.5m8 и выше на стороне сервера поддерживается GTIDs, который плагин цепляет автоматически и это предпочтительное решение. Если же вы используете более древние версии MySQL или GTIDs не включён, вам нужно подключить GTIDs на стороне клиента.
Информацию об этом вы можете найти в руководстве.
Из-за того, что GTIDs может быть недоступен в MySQL, плагин перейдет в режим “только мастер”.
Настраиваем плагин:
{ "appname": { "master": { ... }, "slave": { ... } "filters": { "quality_of_service": { "session_consistency": 1 } } "global_transaction_id_injection": { "select" => "SELECT @@GLOBAL.GTID_EXECUTED", "fetch_last_gtid" => "SELECT @@GLOBAL.GTID_EXECUTED AS trx_id FROM DUAL", "check_for_gtid" => "SELECT GTID_SUBSET('#GTID', @@GLOBAL.GTID_EXECUTED) AS trx_id FROM DUAL", "report_errors" => true, }, } }
На заметку: запросы такого вида работают только начиная с MySQL 5.6.17.
В итоге, плагин будет проверять и учитывать все сервера (для SELECT
'ов), которые синхронизированы с ведущим. Для всех остальных типов запросов все ещё будет использоваться только ведущий сервер.
Если вы не добавите эту часть в конфигурационный файл, то не будет никакой разницы с вариантом master_on_write=1
, который мы рассматривали ранее.
Эту дополнительную настройку невозможно проставить программным образом. Однако, вы можете задать поведение, как при включённом флаге master_on_write
, используя:
mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_SESSION);