Я занимаюсь АТСками. И как-то так сложилась, что с самого первого заказа от меня хотели отказоустойчивости. Одним из ключевых компонентов современной АТС (как и любой информационной системы, наверное) является БД, где хранятся как данные о текущем состоянии системы, так и всякие конфигурационные параметры. Естественно, падение БД приводит к поломке всей системы. Начиналось все с MASTER-MASTER репликации в MySQL (исключительно для оперативности переключения), потом были эксперименты с MySQL over DRBD. Все это жило в pacemaker/corosync инфраструктуре. Там ездили IP-адреса, шлюзы и прочая лабудень. Со временем оно даже стало работать как-то более-менее устойчиво. Но тут мне попалась пара серверов, на которых DRBD сделать было нельзя, в MASTER-MASTER я разочаровался довольно давно (постоянно она у меня ломается, такая репликация), а без отказоустойчивой БД терялся весь смысл решения. На глаза мне попалось название InnoDB cluster и я решил: "была-не-была". Что из этого получилось — смотрите под катом.
Я все делаю на Debian Jessie. В других системах отличия будут, но не очень существенные.
Нам понадобятся:
- MySQL APT репозиторий
- MySQL Shell — для Debain пользователй в самом низу есть ссылка, по которой можно скачать скомпилированные бинарники, по какой причине они не попали в пакет именно для Debain для меня осталось загадкой
- терпение
- терпение
- и еще раз терпение
Скачиваем (это можно сделать только вручную, пройдя регистрацию на сайте Оракла) и устанавливаем файл репозитория, обновляем наш кэш, устанавливаем MySQL Router (интересный зверь, познакомимся ниже) и MySQL Client:
dpkg -i mysql-apt-config_0.8.6-1_all.deb
apt-get update
apt-get install mysql-router
apt-get install mysql-client
Распаковываем и раскладываем по местам MySQL Shell из архива. Двигаемся дальше, ставим сервер:
apt-get install mysql-server
Тут устанавливаем ВСЕ предложенные компоненты (обратите внимание, третий компонент в списке по-умолчанию не выбран, а он понадобится). И останавливаем запущенный сразу ретивым установщиком сервер:
systemctl stop mysql
Далее, идем в
/etc/mysql/mysql.conf.d/mysqld.cnf
и пишем туда что-то типа:
[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
log-error = /var/log/mysql/error.log
bind-address = 0.0.0.0
port = 3300
symbolic-links=0
# Replication part
server_id=3
gtid_mode=ON
enforce_gtid_consistency=ON
master_info_repository=TABLE
relay_log_info_repository=TABLE
binlog_checksum=NONE
log_slave_updates=ON
log_bin=binlog
binlog_format=ROW
# Group replication part
transaction_write_set_extraction=XXHASH64
loose-group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
loose-group_replication_start_on_boot=off
loose-group_replication_local_address= "1.1.1.1:33061"
loose-group_replication_group_seeds= "1.1.1.1:33061,1.1.1.2:33061,1.1.1.3:33061"
loose-group_replication_bootstrap_group= off
Здесь уже надо остановиться подробнее.
Первая опция, заслуживающая нашего внимания — port Рекомендую её значение устанавливать отличным от 3306. Почему — станет понятно ниже (на 3306 мы повесим кое-что другое)
Следующая: server_id. Для каждого сервера mysql в кластере это значение должно быть уникальным.
Опция, которая съела пару часов моего времени — безобидная (казалось бы) group_replication_local_address
. Эта опция говорит о том, на каком адресе/порту слушать обращения за репликами с локальной БД. Алгоритм, по которому MySQL определяет, является ли указанный в этом поле IP-адрес локальным, мне угадать не удалось. На машине с 2-мя активными интерфейсами и тремя IP-адресами, подвешенными на эти интерфейсы только один адрес устроил MySQL.
И последняя: group_replication_group_seeds — в ней перечисляются сокеты, на которые необходимо обращаться (в указанном порядке) с попытками заказать реплики актуальных данных при подключении.
За подробностями можно обратиться на страничку с официальной документацией по GROUP REPLICATION
Итак, развернув на машине MySQL и настроив его таким образом двигаемся дальше. Удаляем все содержимое /var/lib/mysql
Я серьезно. Взяли себя в руки, пошли и удалили. Если там лежит что-то, что дорого Вам как память — сделайте предварительно бэкап. Можно. Запускаем mysql:
systemctl start mysql
В общем-то, все подготовительные манипуляции завершены. Описанную процедуру надо будет повторить на всех серверах, которые планируется объединять в кластер.
Теперь особенности для первого сервера. Для начала объявляем root:
mysql
> create user 'root'@'%' identified by 'ochen-strashniy-parol';
> grant all to 'root'@'%' on *.* with grant option;
> flush privileges;
> \q
Я знаю, знаю, знаю… Несекьюрно. Но, поверьте, указанному в документации набору привилегий верить нельзя (проверял: не работает), и подключиться с локалхоста тоже не выйдет. Более точный вариант с выделением отдельного пользователя в следующий раз, как будет "час та натхнення".
Потом подгружаем модуль X. Честно-честно, именно так его и зовут:
mysqlsh --classic --dba enableXProtocol
И в этот момент модные брюки превращаются… превращаются… превращаются в элегантные шорты. То бишь, MySQL уже не совсем SQL, а вовсе даже Document Store
Но нас это никак не расстраивает, поэтому двигаемся дальше:
mysqlsh
Теперь мы можем общаться с MySQL на JavaScript. Для начала подключимся к нашему серверу:
\c root@1.1.1.1:3300
Введем пароль и проверим, все ли в порядке с нашим сервером, подходит ли его конфигурация для кластеризации:
dba.checkInstanceConfiguration('root@1.1.1.1:3300')
Если все сделано по инструкции, то вопросов быть не должно. Если вопросы есть — исправляем, дока вам в помощь. В принципе, там даже есть возможность сгенерировать конфигурацию самостоятельно:
dba.configureLocalInstance('localhost:3300', {password:'somePwd', mycnfPath:'some path'})
Однако, у меня этот вариант сходу не заработал, потребовались допиливания напильником конфига до состояния, указанного в начале заметки. Естественно, что после каждого перепиливания конфига, сервер надо перезапускать. Например, при помощи systemctl restart mysq
. Ну или как вам больше нравится…
Создаем кластер (предполагаю, что вы все еще в mysqlsh и сессия не обрывалась):
var cl = dba.createCluster('moyCluster')
Ну и добавляем в него только что настроенный сервер:
cl.addInstance('root@1.1.1.1:3300')
Начиная с этого момента рекомендую на какой-нибудь отдельной консоли держать
tail -f /var/log/mysql/error.log
и при появлении любых ошибок туда поглядывать. Вывод у mysqlsh не очень информативный, а в логе есть все необходимое. В доке говорят, что можно увеличить уровень дебага вывода, но я не пробовал.
Если ошибок до данного момента не наблюдалось, то у нас есть кластер из одной машины.
Последняя манипуляция на данном сервере:
mysqldump --all-databases --triggers --routines --events > dump.sql
Теперь займемся остальными. Для этого на всех машинах повторяем манипуляции, описанные от начал статьи и до
systemctl start mysql
Не забываем поправить значения server_id
и group_replication_local_address
. После этого:
mysql
> reset master;
mysql < dump.sql
Да-да, заливаем в машину дамп с работающего в кластере сервера. Теперь с локальной машины (той, на которой крутится инстанс mysql-server`а, который мы будем подключать к кластеру) делаем:
mysql
> set GLOBAL group_replication_allow_local_disjoint_gtids_join=ON;
mysqlsh
> dba.checkInstanceConfiguration('root@1.1.1.2:3300')
> \c root@1.1.1.1:3300
> var cl = getCluster('moyCluster')
> cl.addInstance('root@1.1.1.2:3300',{ipWhitelist: '1.1.1.0/24, 127.0.0.1/8'})
Если мы ничего не перепутали, то в этот момент у нас в кластере уже две машины. Аналогично добавляем третью.
В данный момент у нас есть кластер, который сам за собой присматривает, выбирает мастера и реплицируется на слэйвы. К каждому серверу можно обратиться на его порт 3300 и, если вы обратитесь к мастеру, то вы сможете читать и писать, а если к слейву — то только читать. Выяснить, мастером является сервер или слейвом можно из вывода cluster.status(). Но это еще не очень удобно. Хотелось бы обращаться всегда в одно и то же место, на один и тот же ip/port и не зависеть от внутреннего состояния кластера. Для этого используем MySQL Router Прямо в доке есть пример его начального конфигурирования, который делает почти все, что нам нужно. Изменим его немного:
mysqlrouter --bootstrap 1.1.1.1:3300 --user mysqlrouter
Теперь идем в /etc/mysqlrouter/mysqlrouter.conf
и исправляем там порты как-нибудь так:
[routing:moyCluster_default_rw]
...
bind_port=3306
...
[routing:moyCluster_default_ro]
...
bind_port=3307
...
После этого можно делать
systemctl start mysqlrouter
И теперь у вас на порту 3306 отвечает обычный mysql с поддержкой чтения/записи вне зависимости от того, какая из машин кластера сейчас мастером, а какая слейвом. На 3307 — всегда read-only. Количество экземпляров mysqlrouter никак не ограничено, вы можете на каждой клиентской машине запустить свой экземпляр и повесить его на внутренний интерфейс 127.0.0.1:3306. mysqlrouter сам отслеживает изменения в кластере (как добавления, так и пропадания нод в нем) и актуализирует маршрутизацию. Происходит это раз в пять минут (если верить логам). Если в этом промежутке возникает обращение которое не может быть обработано (нода выпала или была целенаправленно выведена из кластера), то роутер дает отказ в исполнении транзакции и перечитывает состояние кластера во внеочередном порядке.
Кстати, если по какой-то причине нода отвалилась от кластера, её можно вернуть в семью командой
mysqlsh
> \c root@<ip:port-текущего_мастера>
> var cl = dba.getCluster('moyCluster')
> cl.rejoinInstance('root@ip:port-отвалившейся_ноды')
Спасибо, что дочитали. Надеюсь, что это будет полезно. Если заметите в тексте неточности — пишите в комментарии, писал на горячую, но по памяти, так что мог напутать.
Комментарии (23)
easty
06.07.2017 14:30Спасибо за статью. Покупка ораклом пошла мускулу на пользу. Даже в комьюнити эдишн много плюшек из оракла.
Borikinternet
06.07.2017 16:07В опенсорс комьюнити не все с этим утверждением согласны, насколько мне известно.
MuKPo6
06.07.2017 14:30+1Скачиваем (это можно сделать только вручную, пройдя регистрацию на сайте Оракла)
Регистрация не обязательна. Просто на одной из страниц загрузки внизу есть неприметная прямая ссылочка с текстом «No thanks, just start my download».akamensky
07.07.2017 06:07Для скачивания из репозиториев можно еще проще — добавить в закладки http://repo.mysql.com и оттуда на сервере wget пакет который установит репу. Для Centos7, например, это будет (напрямую без wget):
# sudo yum install http://repo.mysql.com/yum/mysql-5.7-community/el/7/x86_64/mysql57-community-release-el7-10.noarch.rpm
После этого все будет доступно напрямую из реп.
akamensky
06.07.2017 14:30+1Спасибо за подробную инструкцию.
Очень интересный вопрос есть по этому делу — как такая конфигурация реагирует на временные проблемы с сетью (например split-brain ситуация)?Borikinternet
06.07.2017 14:34Я тесты не делал, но если верить документации, то кластер поддерживает кворум. При его отсутствии все ноды переходят в super_read_only состояние. При возобновлении связи репликация восстанавливается автоматически (там есть таймауты, но они довольно большие). В общем, если верить разработчикам, то для геораспределенных редантных систем решение должно подойти. У меня намечается подобный опыт, поделюсь, если не будет свежих отзывов раньше.
akamensky
06.07.2017 16:48Да вот у меня прямо сейчас такой проект. Даже два. Один все а одном ДЦ где такое вряд ли случится, а вот второй как раз геораспределенный и ещё все через VPN подключается, так что задержки там могут быть нехилые.
Borikinternet
06.07.2017 21:44Если решитесь использовать — поделитесь, каков он на практике, геораспределенный InnoDB Cluster
knutov
12.07.2017 22:45Чуть менее 10 лет использую мультимастер между разными ДЦ. Никогда не испытывал проблем.
UncleAndy
07.07.2017 07:53Ничего нового, но оставлю как справку. Я такие кластеры лет 10 назад делал. :)
Borikinternet
07.07.2017 18:22А поделитесь технологиями: какой БД пользовались? Как обеспечивали репликацию? Как была обеспечена консистентность кластера? Как организовывали переключение потоков данных при аварии?
Как-то со всем этим 10 лет назад было не очень, насколько мне помнится. По крайней мере, в opensource
kromiko
07.07.2017 18:18Спасибо за статью. А что произойдет, если нода, которая на данный момент является мастером, по каким-то причинам отвалится? Кластер сам выберет нового мастера?
Borikinternet
07.07.2017 18:18Да, конечно. А mysqlrouter переключит запросы на запись на него, тоже автоматически.
zonek
07.07.2017 20:33+1объясните. как понимаю общего IP или имени здесь нет. как понимаю приложение само должно знать что работает с несколькими инстанциями? или что то путаю?
Borikinternet
07.07.2017 20:46Спасибо за вопрос! Значит я плохо/непонятно написал про mysqlrouter. Давайте попробую еще раз:
mysqlrouter — специальное приложение, предназначенное для маршрутизации приходящих на него запросов на нужную ноду кластера MySQL. Когда Вы запускаете mysqlrouter, он открывает на порту (например, 3306 для того, что б не переписывать конфиги сторонних приложений) сокет, на котором принимает запросы от клиентских приложений. Так же при запуске mysqlrouter опрашивает кластер и выясняет, какие машины могут принимать запросы на чтение/запись, а какие — только на чтение.
Далее, в какой-то момент Ваш сайт обращается (как обычно) к порту 3306. mysqlrouter переправляет этот запрос на соответствующую ноду кластера, получает ответ и переправляет его запросившему. Таким образом, mysqlrouter берет на себя задачи распределения трафика между нодами. Он самостоятельно поддерживает актуальность состояния кластера проводя периодические опросы на предмет изменения в составе и смены мастера.
Все несколько усложняется тем, что на самом деле mysqlrouter открывает не один 3306, а целых четыре порта. В моем примере 3306, 3307, 33061 и 33071. На 3306 я цепляю приложения, которым требуется доступ к БД на запись, к 3307 — только на чтение. На эти портах обмен производится по классическому протоколу MySQL. 33061 и 33071 используются, соответственно, для операций на чтение/запись и только чтение по протоколу X (новый протокол обмена MySQL)
Если знакомы с haproxy, то mysqlrouter — это специализированный haproxy для MySQL
Upd: Разработчики рекомендуют располагать mysqlrouter непосредственно на машинах, которые будут выполнять запросы к БД.zonek
07.07.2017 21:07Спасибо за развернутый ответ! Теперь бы понять каким образом для такой схемы ( кластер) можно осуществлять резервное копирование, и самое главное востановлкние как целиком БД так и докатыванием логами до определенного момента.
Borikinternet
07.07.2017 21:41Да точно так же, как обычно. Подключиться к кластеру можно как через mysqlrouter, так и непосредственно к каждому инстансу mysqld, который при этом полноценно отвечает. Другое дело, что на слэйвах он вам не даст сделать запись. Если вы хотите сделать мгновенный слепок системы, то любую машину выводите из кластера, делаете на ней mysqldump и заводите её обратно. Все пропущенное за время отсутствия она подтянет сама. Если не боитесь за консистентность БД, то делайте mysqldump прямо через mysqlrouter.
Про восстановление по логам до определенного момента — не знаю, тут я не спец. Если для MySQL такое решение есть, то и к кластеру оно наверняка применимо, поскольку для обмена данными используются самые обычные бинлоги.zonek
07.07.2017 21:50с востановлением из дампа все так просто? разве при востановление базы на одном из серверов он не выпадет из кластера, а когда его туда обратно подключат не докатит состояние базы до более свежей кластера?
Borikinternet
07.07.2017 22:02С восстановлением всего кластера на определенный момент не знаю. Простейший вариант: создал новый кластер, развернул в него нужный бэкап и заменил старый кластер на новый целиком. Это точно сработает. Другие варианты — надо изучать раздел Group Replication официальной доки. Наверняка есть менее радикальные варианты, просто по роду деятельности не приходилось сталкиваться с такой задачей.
Borikinternet
07.07.2017 20:50Например, у Вас 10 backend-серверов, которые обращаются к БД. На каждом разворачиваете mysqlrouter и с делаете обращение к 127.0.0.1:3306, а mysqlrouter`ы самостоятельно отправляют запросы на нужные ноды кластера.
xXxSPYxXx
Это выглядит сложнее, чем Galera Cluster c MaxScale. Сами пробовали настраивать Galera Cluster?
Borikinternet
Нет, эта конструкция завелась раньше, чем я потерял терпение. При изначальном выборе вектора выбор не упал на Галеру потому, что там встретилось меньше знакомых слов :)