PHP Profi

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

php database MySQL MySQLnd read write advanced

Продолжение статьи

Шардинг и секционирование

Запросы на запись очень трудно распределить на множество машин (например, мульти-мастер репликация) традиционными средствами MySQL (т.е. не используя MySQL Cluster). Поэтому зачастую используется секционирование или шардинг. Это позволит вам равномерно распределить запросы среди множества ведущих серверов, записывая разные данные на каждый из них.

Для примера можно привести такую ситуацию: информация о пользователе может лежать на ведущем сервере А, а информация по покупкам на ведущем B.

Не всегда секционирование и шардинг применяются для распределения записи. Иногда топология репликации выбирается с умыслом реплицировать куски данных с ведущей БД на специфические ведомые. Это позволяет, для определённой информации, распределять аппаратную нагрузку более эффективно.

Плагин mysqlnd_ms позволяет вам настроить мульти-мастер реплику, используя ini-настройку mysqlnd_ms.multi_master или секционирование, используя multi-фильтр  node_groups (группы узлов).

Процесс секционирования в mysqlnd_ms не полностью прозрачный, в отличие от множества других возможностей. Поэтому разделы должны быть определены либо во время составления запроса в вашем редакторе, либо программно во время исполнения.

Если вам надо включить поддержку мульти-мастера, то в php.ini добавьте следующее:

mysqlns_ms.multi_master=1

Затем, чтобы настроить группы узлов, добавьте следующий текст в ваш JSON:

{
    "appname": {
        "master": {
            "master_0": {
                "host": "mysql.master.0",
              "db": "dbname"
            },
            "master_1": {
                "host": "mysql.master.1",
              "db": "dbname"
            },
        },
        "slave": {
            "slave_0": {
                "host": "mysql.slave.0",
              "db": "dbname"
            },
            "slave_1": {
                "host": "mysql.slave.1",
              "db": "dbname"
            },
        }
        "filters": {
             "node_groups": {
                  "Group_A" : {
                      "master": ["master_0"],
                      "slave": ["slave_0", "slave_1"]
                  },
                  "Group_B" : {
                      "master": ["master_1"],
                      "slave": ["slave_0", "slave_1"]
                  }
             }
         }
    }
}

Это добавит две группы узлов: Group_A и Group_B. Каждая группа использует все ведомые сервера и при этом только один ведущий.
Теперь вы можете направлять запросы на определённую группу узлов просто добавляя указатель на неё в начале запроса:

/*Group_A*/SELECT * FROM users;

Вы, кстати, можете писать цепочки SQL-указателей, никто вас не ограничивает:

/*Group_B*//*ms=last_used*/SELECT * FROM transactions;

Как добавить SQL-указатель — дело ваше. Как я уже упоминал выше, вы можете сделать это в редакторе, когда пишите код, или программным образом, когда конструируете запрос.

Пользовательская маршрутизация

Последнюю фишку, которую мы рассмотрим – это пользовательская маршрутизация. Она позволяет вам описать логику в распределении запросов по серверам, используя пользовательские (user) или мульти-пользовательские (user_multi) фильтры. В них мы обязаны определить callback-функцию, которая должна вернуть либо один сервер, либо список из двух элементов: ведущего и ведомого.

Эта функция не может быть статической или методом объекта (и анонимной функцией класса Сlosure она тоже быть не может). Она может быть только классическим примером процедурного программирования. Но при этом внутри ее тела может быть любой код. Таким образом, эта функция может быть обёрткой, например, объекта или любого другого кода.

С пользовательской маршрутизацией мы можем программно отлавливать некоторые граничные условия, которые, в противном случае были бы по умолчанию пропущены.

Для начала, настроим конфигурацию:

{
    "appname": {
        "master": {
            ...
        },
        "slave": {
            ...
        }
        "filter": {
             "user": "select_db_server"
        }
    }
}

Затем, мы определим нашу callback-функцию:

function select_db_server($connection, $sql, $masters, $slaves, $last_used, $in_transaction)
{
    // всегда переключаемся на ведущий сервер, если он был использован последним:
    if (in_array($masters, $last_used)) {
        return $last_used;
    }

    // если мы в транзакции, то всегда используем последнее подключение:
    if ($in_transaction === true) {
        return $last_used;
    }

    // проверяем SQL-указатели:
    if (strpos($sql, '/*' .MYSQLND_MS_MASTER_SWITCH. '*/') !== false) {
        // используем ведущие: 
        return array_rand($masters); // Use array_rand just in case we have multiple masters
    } elseif (strpos($sql, '/*' .MYSQLND_MS_SLAVE_SWITCH. '*/') !== false) {
        // используем ведомые:
        return array_rand($slaves);
    } elseif (strpos($sql, '/*' .MYSQLND_MS_LAST_USED_SWITCH. '*/') !== false) {
        // используем последний задействованный:
        return $last_used;
    }

    // Проверяем некоторые пограничные условия:

    // запросы SELECT или (SELECT идут ведомым:
    if (stripos($sql, 'SELECT') === 0 || stripos($sql, '(SELECT') === 0) {
        // но не запросы SELECT... INTO:
        // помните, если у нас транзакция, то надо использовать последний задействованный
        if (pcre_match('/^\(?SELECT (.*) INTO (@|OUTFILE|DUMPFILE)(.*) FROM (.*)$/i', $sql) == 1) {
            return array_rand($masters);
        }

        return array_rand($slaves);
    }

    // отправляем CREATE TEMPORARY TABLE последнему задействованному:
    if (stripos($sql, 'CREATE TEMPORARY TABLE') === 0) {
        return $last_used;
    }

    // по умолчанию отправляем ведущему, если до сих пор не определились:
    return array_rand($masters);
}

Пожалуйста, учтите, что это всего лишь пример, и его, возможно, не следует применять на практике, но он прекрасно демонстрирует всю мощь пользовательских фильтров.

Продолжим. Мы делаем ряд проверок, чтобы в конечном счёте прийти к выбору, какой сервер следует использовать:

  1.  Для всех будущих запросов мы переключаемся на ведущий сервер сразу же, как он был использован. Это очень похоже на флаг master_on_write и на qos-фильтр сессионной консистентности.
  2.  Далее, мы убеждаемся, что используем последнее соединение, если начали транзакцию
  3.  Затем мы проверяем SQL hint-ы и следуем им
  4.  Наконец, мы подобрались к граничным условиям, отдельно обрабатываем: запросы, начинающиеся с (SELECT, запросы вида SELECT... INTO, а затем запросы CREATE TEMPORARY TABLE.
  5.  Если ничего не вернулось, мы просто устанавливаем по умолчанию один из ведущих серверов. Это уменьшит вероятность случайной отправки запросов на запись к ведомым серверам.

Теперь вы можете делать в пределах этой функции все что угодно. Нужно лишь помнить, что возвращаться должен только один сервер.

В качестве альтернативы, вы можете использовать user_multi-фильтр. Это почти то же самое, за исключением того, что возвращаемое значение — массив с двумя значениями, каждое из которых — массив ведущих или ведомых серверов соответственно.

Заключение

Плагин mysqlnd_ms имеет множество продвинутых возможностей, которые с помощью традиционных топологий репликаций MySQL позволяют эффективно масштабировать ваш проект.

Есть еще некоторые возможности, которые не попали в описание этой статьи, например:  поддержка кэширования, распределенные транзакции, поддержка MySQL Fabric.

В следующей части нашего цикла мы рассмотрим MySQL’s InnoDB Memcache Interface, который очень легко использовать вместе с еще одним плагином MySQLnd: mysqlnd_memcache.

P.S. Вы уже используете какие-нибудь из этих продвинутых технологий? Мы были бы рады, если бы вы рассказали о вашем опыте использования плагина mysqlnd_ms.

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