Квест → Как хакнуть форму
Прошли: 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);