Skip to main content

Миграция на 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
}