В первой части мы обсудили горизонтальное масштабирование уровня приложения — масштабирование серверов для одновременной обработки PHP-кода с помощью балансировщиков нагрузки и других средств. Мы рассмотрели обмен локальными данными, затронули вопросы потенциальной оптимизации и объяснили основы балансировщиков нагрузки.
Прикладной уровень, однако, не единственное, что требует масштабирования. С повышением спроса на наше приложение возникает потребность в более высоких операциях чтения / записи в базе данных. В этой части мы рассмотрим масштабирование базы данных, объясним репликацию и рассмотрим некоторые распространенные ошибки, которые вы можете избежать.
оптимизация
Как и в части 1, сначала нужно упомянуть оптимизацию. Правильно индексируйте вашу базу данных, убедитесь, что таблицы состоят из наименьшего количества важных данных, и users_basic
вторичную информацию в других ( users_basic
+ users_additional
+ users_alt_contacts
и т. Д. — известную как users_alt_contacts
базы данных — сложная тема, гарантирующая свою собственную статью, users_alt_contacts
), делая небольшие атомарные запросы в отличие от больших оперативных вычислений — все эти методы могут помочь вам ускорить работу ваших баз данных и избежать узких мест. Есть один аспект, который может помочь еще больше — кеш запросов .
Серверы баз данных обычно кэшируют результаты и скомпилированные запросы SELECT, которые были выполнены в последний раз. Это позволяет клиенту (нашему приложению) получать данные из кэша вместо того, чтобы снова ждать выполнения. Такой подход экономит циклы ЦП, дает результаты быстрее и освобождает серверы от ненужных вычислений. Но размер кэша запросов ограничен, и некоторые наборы данных меняются чаще, чем другие. Использование кэша для всех наших данных было бы смешно, особенно если некоторая информация изменяется быстрее, чем какая-либо другая информация. Несмотря на то, что кэш может быть точно настроен, в зависимости от поставщика базы данных, есть подход, который очень хорошо подходит для решения кеша запросов — контекстная группировка серверов.
Когда вы группируете серверы, у вас есть группы серверов, каждый из которых выделен для определенной части вашего приложения. Скажем, я создаю сайт азартных игр онлайн — у меня может быть одна группа для внутриигровых чатов, одна группа для текущих игр, одна группа для учетных записей пользователей. Группа учетных записей пользователей не требует столько чтения или записи, по крайней мере, не так много, как функции чатов или игр, поэтому мы можем уменьшить ее — там есть 5 серверов. В группе чатов у нас может быть 10 серверов, потому что общение миллионов наших пользователей в режиме реального времени очень важно для высокого качества игры. Но нет такого важного аспекта, как настоящие игры — наш хлеб с маслом — поэтому они получают исключительно большую группу, может быть, 20 серверов.
Это гарантирует, что вы делаете все свои пользовательские чтения и записи в одной группе, не затрагивая другие. Игры должны быть быстрыми, поэтому самая большая группа используется для обеспечения быстрого обслуживания (как для чтения, так и для записи), в то время как страница учетной записи пользователя не так важна для людей, которые уже вошли в систему и готовятся к ней. изменить вещи — мы уже привлекли их внимание, и нам не нужно удивлять их сверхбыстрой отзывчивостью в не критически важных для бизнеса частях нашего приложения.
Все группы имеют одинаковые общие данные (см. MSR ниже), так что вы можете перемещать серверы из одной группы в другую — скажем, есть огромный покерный чемпионат, и у вас есть миллионы пользователей, наблюдающих за игрой — игровая группа может извлечь выгоду из 4- 5 серверов взяты из чат-группы, чтобы убедиться, что она справится с нагрузкой. Тем не менее, существуют встроенные решения для кластерных баз данных, и одним из таких решений является Master Slave Replication.
Мастер-Ведомая Репликация
Master-Slave Replication (MSR) — очень распространенная функция в современных базах данных, часто встроенная. Поищите в документации вашего конкретного поставщика базы данных информацию о том, поддерживается ли он изначально. MSR — это процесс отправки всех операций записи в базу данных с одного сервера (главного) одному или нескольким ведомым. Затем подчиненные устройства воспроизводят запросы и, таким образом, воспроизводят основные данные. Интегрируется с веб-приложением, вот как это работает, шаг за шагом:
- посетитель выполняет операцию записи в базу данных. Например, он меняет некоторую информацию профиля.
- приложение отправляет запрос на основной сервер базы данных, как обычно
- мастер выполняет этот запрос и передает его всем рабам
- подчиненные выполняют запрос, и теперь одни и те же данные находятся как на главном, так и на подчиненном компьютерах
- дальнейшие операции чтения выполняются на рабах
Последний шаг — вот что важно — мы выполняем операции чтения на подчиненных. Само по себе это разделение труда между машинами — когда Мастер может делать только записи (как правило, в веб-приложении гораздо меньше авторов, чем читает, и как такового достаточно одного Мастера), и армия рабов доступна для загрузка, никакой конкретный сервер не перегружен.
MSR обычно активируется по умолчанию в современных установках MariaDB и MySQL.
Разделение чтения и записи
Здесь требуется небольшая модификация вашей архитектуры, сложность которой зависит от качества вашего кода и использования фреймворка или хорошей структуры проекта. Когда вам нужно разделить операции чтения и записи, вам необходимо установить отдельные подключения. Для записи вам необходимо подключиться к «записывающей БД» (ведущей), а для чтения вам необходимо подключиться к одному из подчиненных. Это, конечно, можно сделать вручную. Для записи вы должны подключиться к специально определенной главной конфигурации:
<? php $config = $this -> getService ( 'configProvider' ); $master = new PDO (
'mysql:dbname=' . $config -> db -> master -> name . ';host=' . $config -> db -> master -> host , $config -> db -> master -> user , $config -> db -> master -> password ); $statement = $master -> prepare ( 'SOME QUERY FOR UPDATING' );
//...
Для чтения потребуется новое соединение через другой набор значений конфигурации:
<? php $config = $this -> getService ( 'configProvider' ); $slave = new PDO (
'mysql:dbname=' . $config -> db -> slave -> name . ';host=' . $config -> db -> slave -> host , $config -> db -> slave -> user , $config -> db -> slave -> password ); $results = $slave -> query ( 'SOME QUERY FOR READING' );
//...
Если у нас есть несколько ведомых и мы знаем их адреса хостов, мы можем сделать некоторую рандомизацию:
<? php $config = $this -> getService ( 'configProvider' );
// Pick random slave from list of possible slaves $slaveConfig = $config -> db -> slaves [ array_rand ( $config -> db -> slaves )]; $slave = new PDO (
'mysql:dbname=' . $slaveConfig -> name . ';host=' . $slaveConfig -> host , $slaveConfig -> user , $slaveConfig -> password ); $results = $slave -> query ( 'SOME QUERY FOR READING' );
//...
Осознание того, что это неудобно делать каждый раз, когда вам нужно сделать чтение или запись, не займет много времени. Лучшим решением было бы предварительно инициализировать соединение с главной БД в каком-либо сервисном контейнере, а затем просто получить к нему доступ для записи без необходимости переустанавливать соединение каждый раз, когда вам нужны записи. Что касается ведомых, то может применяться тот же подход — встроить функцию рандомизации в адаптер DB «Slave» и просто вызвать что-то вроде $this->getService('db')->slave
, которая автоматически возвращает случайно выбранную. Таким образом, вам никогда не придется беспокоиться о том, чтобы вручную выбирать их снова, и когда вы добавляете больше подчиненных в ваш кластер, просто включите их адрес хоста в файл конфигурации.
Хотите поднять еще одну ступеньку? Пусть служба выбора подчиненных выбирает только тех подчиненных, которые не облагаются налогом или не работают в данный момент — заставьте подчиненные несколько раз сообщать о своем использовании ЦП / ОЗУ и убедитесь, что класс выборки ведомого всегда выбирает тот, который находится под наименьшим количеством Пожар. Этот абстрактный класс селектора должен также убедиться, что он подключается к другому ведомому, если тот, к которому он первоначально пытался подключиться, не работает — вам нужно, чтобы чтение базы данных оставалось прозрачным для конечного пользователя, но вы все же хотите, чтобы администраторы знали, что есть проблема с один из рабов
<? php // ... some class, some connect() method $config = $this -> getService ( 'configProvider' ); $mailer = $this -> getService ( 'mailer' ); $validSlaves = $config -> db -> slaves ; $bConnectionSuccessful = false ;
while (! empty ( $validSlaves ) && ! $bConnectionSuccessful ) { $randomSlaveKey = array_rand ( $validSlaves ); $randomSlave = $validSlaves [ $randomSlaveKey ];
try { $slave = new PDO (
'mysql:dbname=' . $randomSlave -> name . ';host=' . $randomSlave -> host , $randomSlave -> user , $randomSlave -> password ); $bConnectionSuccessful = true ;
} catch ( \P DOException $e ) { unset ( $validSlaves [ $randomSlaveKey ]); $mailer -> reportError ( ... ); // Some kind of email sent to the admins, or the master log, etc
} catch ( \Exception $e ) { unset ( $validSlaves [ $randomSlaveKey ]); $mailer -> reportError ( ... ); // Some kind of different email sent to the admins, or the master log, etc
}
}
if ( $bConnectionSuccessful ) {
return $slave ;
} $mailer -> reportError ( ... ); // Report that the entire attempt to connect to any slave failed
throw new \Exception ( ... ); // or report via an exception with details in the message
Это только псевдокод, но вы понимаете суть — попробуйте подключиться к случайно выбранному ведомому устройству, если это не удастся, удалите этого ведомого из массива допустимых ведомых устройств, сообщите о проблеме и повторите попытку на другом подчиненном устройстве, пока есть больше попробовать.
Чтение / запись задержки синхронизации
Одной вещью, которая может представлять угрозу согласованности данных, является задержка синхронизации между ведущими и ведомыми устройствами. При меньших записях репликация на подчиненные устройства будет почти мгновенной, и для выполнения больших запросов, естественно, потребуется больше времени — эта задержка также увеличивается с ростом армии подчиненных, поскольку мастер имеет больше копий запросов для рассредоточения — некоторые подчиненные будут готовы раньше, чем другие. Однако, независимо от качества сети вашего кластера, скорости отдельных машин или размера запросов, в мире нет установок, способных точно выполнить следующее:
<? php $db = $this -> getService ( 'db' ); $master = $db -> get ( 'master' ); $slave = $db -> get ( 'slave' );
// value is currently 1 $master -> exec ( 'UPDATE `some_table` SET `value` += 1 WHERE `id` = 1' ); // executes $slave -> query ( 'SELECT `value` FROM `some_table` WHERE `id` = 1' ); // value is still 1
Эти операторы выполняются слишком близко друг к другу, и у ведомого просто нет возможности вовремя получить обновление, чтобы точно отразить его.
Для этого есть различные обходные пути, но ни один из них не является отказоустойчивым. Это несоответствие — это то, что вы просто должны принять. В большинстве случаев записанные данные не требуют немедленного чтения. Если это так, лучше всего взять приближение — если вы ожидаете, что значение увеличится на 1, то просто прочитайте исходное значение из ведомого устройства и добавьте 1 к результату, а затем отобразите его для пользователя. Мастер все еще может распространять фактическое обновление в фоновом режиме.
Мастер провал
Что если мастер потерпит неудачу? Вся система останавливается? Ну, это не должно Существуют решения для отработки отказа главного устройства, и они обычно решают проблему, превращая ведомое устройство в ведущее в случае отказа главного устройства. Однако для этого необходимо внести дополнительные изменения в архитектуру. Процедура выглядит следующим образом:
- Мастер не работает и больше не может принимать записи
- Наш основной скрипт выборки распознает это, сообщает об ошибке и выбирает случайного доступного ведомого
- Этот раб получает сигнал, чтобы стать хозяином
- Все остальные ведомые устройства получают сигнал для переключения на нового мастера
- Возможна небольшая потеря данных, если мастер умер в середине записи и не успел распространить изменение данных на все подчиненные устройства или если ведомое устройство не успело выполнить запрос до того, как его превратили в мастера.
- Когда мастер возвращается в режим онлайн, он должен быть уничтожен и настроен с нуля как раб нового мастера — делать что-либо еще было бы бессмысленно, потому что догонять пропущенные запросы во время простоя было бы глупым поручением. Если вышедшая из строя мастер-машина является самой мощной машиной, и вы действительно хотите, чтобы она продолжала оставаться ведущей, необходимо предпринять дополнительные меры безопасности, которые, вероятно, потребуют короткого времени простоя, пока перезагруженный мастер не уловит все.
В частности, в MySQL и MariaDB вы можете заставить подчиненное устройство переключать свое ведущее устройство с помощью команды CHANGE MASTER TO , в то время как продвижение от подчиненного устройства к ведущему осуществляется через STOP SLAVE и RESET MASTER . Для получения дополнительной информации о главном переключении, пожалуйста, смотрите документацию .
Вывод
Во второй части мы рассмотрели репликацию и кластеризацию баз данных. Теперь, когда обе эти части уже позади, мы надеемся, что вы собрали достаточно начальных знаний для самостоятельной работы и создания превосходной масштабируемой среды. Есть ли у вас какие-либо предложения, которые вы считаете важными в горизонтальном масштабировании? Хотите увидеть более сложную часть 3? Мы пропустили некоторые важные аспекты? Дайте нам знать в комментариях ниже!