PHP Profi

Продвинутое разделение чтения и записи с помощью MySQLnd в PHP. Часть 1. Перевод Серия 

php database MySQL MySQLnd read write

От переводчика: нам показалось, что эту статью долго не только переводить, но и читать. Поэтому мы решили разбить её на две части.

В первой части нашего цикла статей мы бегло рассмотрели 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 фильтры: этот тип возвращает единственный сервер

Вы можете указать множество 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-фильтра. Этот фильтр позволяет подойти к процессу распределения более детализировано, когда дело касается задержки репликации в сценарии запись — затем — чтение.

Существуют три типа работы фильтра, которые являются уровнями в конфигурации:

  • Консистентность в конечном счёте (eventual): стоит по умолчанию. Все ведущие/ведомые сервера считаются применимыми для запросов на чтение. Вы также можете автоматически отсеивать ведомых, которые имеют лаг в N секунд относительно ведущего.
  • Сессионная консистентность (session): этот тип схож с настройкой master_on_write=1. К тому же, есть возможность, используя Global Transaction IDs (GTIDs), автоматически подключать сервера, которые уже реплицировали данные.
  • Строгая консистентность (strong): применяется только в случае организации структуры MySQL с использованием множества синхронных ведущих серверов, например, технология MySQL Cluster.

Чтобы настроить 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);

Консистентность в конечном счёте (EVENTUAL CONSISTENCY)

Простейший из типов - это консистентность в конечном счёте. Этот тип явно проверяет значение Время_Отставания_Реплики_От_Мастера (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);

продолжение

2015-05-23 alek13 Поделиться: оригинал