Миграция на mongo replica set без потери данных
По сути - это не статья, а, скорее, просто памятка, чтобы самому не забыть. MongoDB - популярный документо-ориентированный движок управления базой. Штатно он имеет две разных технологии кластеризации: репликационные наборы (replica set) и шардирование (shard). Разница проста - в случае реплики на всех узлах данные одинаковы и любой узел может выступать источником данных (внимание, mongodb не является CAP-полной базой, так что точность данных тут под большим вопросом!), что обеспечивает отказоустойчивость. В случае шардирования данные “размазаны” по всему набору шарда, но каждый сервер внутри шарда имеет только свои данные. За счет этого распределяется нагрузка (например, с трех узлов данные можно читать параллельно), но снижается надежность - упавший узел означает потерю части данных шарда. В данной статье будет описание, как переехать с единичной монги на репсет не потеряв при этом данные.
Важной особенностью репсета является тот факт, что в нем всегда только один мастер, и запись можно производить только на мастер. Таким забавным способом mongo борется с потенциальными конфликтами конкурентной записи данных (задача, на самом деле, куда как непростая, Riak это ярко демонстрирует). В случае, если мастер умер - производятся выборы нового мастера. К слову, пока они идут - в репсет нельзя записать ничего, от слова совсем.
Инструкция
Итак, у нас есть одна MongoDB, к которой подключено приложение и мы хотим перевести это приложение на работу с кластером. Авторы mongo рекомендуют иметь нечетное количество узлов в наборе для гарантии достижения кворума, то есть потребуется 3 сервера. В отличае от MySQL - управление выбором узлов для чтения и записи в монго возложено на драйвер (то есть - коннектор языка программирования к базе, а не на саму базу). Перед началом работ очень рекомендую, умеет ли ваш драйвер работать с репсетами. Первое, что нужно поменять - это в файле настроек указать IP адрес, на который mongo привязывается (она должна быть доступна по сети) и задать параметр replicationSet. Один инстанс mongo может одновременно находится только в одном replication set. Параметр - не динамический, mongo надо перезапустить для его применения. Вписываем в /etc/mongo:
#имя replica set. Должно быть уникальным, но единым для всего набора
replSet=replica1
#bind_ip закомментирован - слушать на всех доступных адресах.
#bind_ip=
port=27017
Перезапускаем mongo, подключаемся к консоли (команда mongo) и инициируем набор:
# service mongodb restart
# mongo
> rs.initiate()
{
"info2" : "no configuration explicitly specified -- making one",
"me" : "db1.cluser:27017",
"ok" : 1
}
Чтобы быть уверенными, что мастером останется именно тот сервер, с которого мы начинаем конфигурирование (и на котором храним данные, которые не хотим потерять) - повысим ему приоритет в наборе. Мастер выбирается по принципу - случайный из максимального доступного приоритета, приоритет 0 будет означать, что данный сервер стать мастером не сможет никогда.
replica1:PRIMARY> cfg = rs.conf()
{
"_id" : "replica1",
"version" : 1,
"members" : [
{
"_id" : 0,
"host" : "db1.cluster:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {
},
"slaveDelay" : 0,
"votes" : 1
}
],
"settings" : {
"chainingAllowed" : true,
"heartbeatTimeoutSecs" : 10,
"getLastErrorModes" : {
},
"getLastErrorDefaults" : {
"w" : 1,
"wtimeout" : 0
}
}
}
replica1:PRIMARY> cfg.members[0].priority = 100
100
replica1:PRIMARY> rs.reconfig(cfg)
{ "ok" : 1 }
Проверим состояние нашей реплики:
replica1:PRIMARY> rs.status()
{
"set" : "replica1",
"date" : ISODate("2015-08-20T19:38:18.845Z"),
"myState" : 1,
"members" : [
{
"_id" : 0,
"name" : "db1.cluster:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 96,
"optime" : Timestamp(1440099489, 1),
"optimeDate" : ISODate("2015-08-20T19:38:09Z"),
"electionTime" : Timestamp(1440099465, 2),
"electionDate" : ISODate("2015-08-20T19:37:45Z"),
"configVersion" : 2,
"self" : true
}
],
"ok" : 1
}
Все ок. Ставим новый (пустой) сервер с mongo, прописываем в настройках replica set, запускаем. Теперь надо новый сервер добавить в набор:
replica1:PRIMARY> rs.add("db2.cluster:27017")
{ "ok" : 1 }
Узлы кластера должны быть доступны друг другу по порту, на котором работает mongo (по умолчанию 27017). Если используются имена, а не IP-адреса - важно убедится в том, что они правильно определяются со всех машин. Сервера общаются асинхронно, все со всеми. Проверим состояние нашего набора:
replica1:PRIMARY> rs.status()
{
"set" : "replica1",
"date" : ISODate("2015-08-20T21:16:13.381Z"),
"myState" : 1,
"members" : [
{
"_id" : 0,
"name" : "db1.cluster:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 77,
"optime" : Timestamp(1440105350, 1),
"optimeDate" : ISODate("2015-08-20T21:15:50Z"),
"electionTime" : Timestamp(1440105297, 1),
"electionDate" : ISODate("2015-08-20T21:14:57Z"),
"configVersion" : 3,
"self" : true
},
{
"_id" : 1,
"name" : "db2.cluster:27017",
"health" : 1,
"state" : 5,
"stateStr" : "STARTUP2",
"uptime" : 23,
"optime" : Timestamp(0, 0),
"optimeDate" : ISODate("1970-01-01T00:00:00Z"),
"lastHeartbeat" : ISODate("2015-08-20T21:16:12.351Z"),
"lastHeartbeatRecv" : ISODate("2015-08 20T21:16:12.363Z"),
"pingMs" : 0,
"syncingTo" : "db1.cluster:27017",
"configVersion" : 3
}
],
"ok" : 1
}
Статус STARTUP2 означает, что новый член набора синхронизируется (выгружает данные) с мастером. так, как по умолчанию приоритет у новых серверов - 1, даже после окончания, даже случайно этот сервер не сможет стать мастером, что убережет нас от потери данных.
Дождавшись синхронизации (state у нового сервера должен будет изменится на SECONDARY) - добавляем еще один сервер по точно такой же схеме:
replica1:PRIMARY> rs.add("db3.cluster:27017")
{ "ok" : 1 }
И снова ждем состояния SECONDARY. Теперь можно снизить приоритет основного сервера, чтобы он стал равноправным членом кластера:
replica1:PRIMARY> cfg.members[0].priority = 1
1
replica1:PRIMARY> rs.reconfig(cfg)
{ "ok" : 1 }
И теперь убедимся, что все ок:
replica1:PRIMARY> rs.status()
{
"set" : "replica1",
"date" : ISODate("2015-08-21T10:15:11.851Z"),
"myState" : 1,
"members" : [
{
"_id" : 0,
"name" : "db1.cluster:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 46815,
"optime" : Timestamp(1440152107, 1),
"optimeDate" : ISODate("2015-08-21T10:15:07Z"),
"electionTime" : Timestamp(1440105297, 1),
"electionDate" : ISODate("2015-08-20T21:14:57Z"),
"configVersion" : 4,
"self" : true
},
{
"_id" : 1,
"name" : "db2.cluster:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 46761,
"optime" : Timestamp(1440152107, 1),
"optimeDate" : ISODate("2015-08-21T10:15:07Z"),
"lastHeartbeat" : ISODate("2015-08-21T10:15:09.856Z"),
"lastHeartbeatRecv" : ISODate("2015-08-21T10:15:09.997Z"),
"pingMs" : 0,
"syncingTo" : "db1.cluster:27017",
"configVersion" : 4
},
{
"_id" : 2,
"name" : "db3.cluster:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 46550,
"optime" : Timestamp(1440152107, 1),
"optimeDate" : ISODate("2015-08-21T10:15:07Z"),
"lastHeartbeat" : ISODate("2015-08-21T10:15:09.856Z"),
"lastHeartbeatRecv" : ISODate("2015-08-21T10:15:09.997Z"),
"pingMs" : 0,
"syncingTo" : "db1.cluster:27017",
"configVersion" : 4
}
],
"ok" : 1
}