Я занимаюсь АТСками. И как-то так сложилась, что с самого первого заказа от меня хотели отказоустойчивости. Одним из ключевых компонентов современной АТС (как и любой информационной системы, наверное) является БД, где хранятся как данные о текущем состоянии системы, так и всякие конфигурационные параметры. Естественно, падение БД приводит к поломке всей системы. Начиналось все с 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-отвалившейся_ноды')
Спасибо, что дочитали. Надеюсь, что это будет полезно. Если заметите в тексте неточности — пишите в комментарии, писал на горячую, но по памяти, так что мог напутать.
Автор: Borikinternet