Доброго времени суток, уважаемые читатели. В этом посте я хотел бы описать несколько примеров развертки mongoDB, отличия между ними, принципы их работы. Однако больше всего хотелось бы поделиться с вами практическом опытом шардирования mongoDB. Если бы этот пост имел план, он бы выглядел скорее всего так:
- Вступление. Кратко о масштабировании
- Некоторые примеры развертки mongoDB и их описание
- Шардинг mongoDB
Пункты 1 и 2 — теоретические, а номер 3 претендует на практическое руководство по поднятию кластера mongoDB и больше всего подойдет тем, кто столкнулся с этим в первый раз.
1. Вступление. Немного о масштабировании
Представьте себе типичный случай — имеется база данных, в которую осуществляется запись и чтение данных. В динамично растущих системах, объемы данных, как правило, быстро увеличиваются и рано или поздно можно столкнуться с проблемой, когда текущих ресурсов машины будет не хватать для нормальной работы.
Для решения этой проблемы применяют масштабирование. Масштабирование бывает 2-х видов — горизонтальное и вертикальное. Вертикальное масштабирование — наращивание мощностей одной машины — добавление CPU, RAM, HDD. Горизонтальное масштабирование — добавление новых машин к существующим и распределение данных между ними. Первый случай наиболее простой, т.к. не требует дополнительных настроек приложения и какой-либо дополнительной конфигурации БД, но его недостаток в том, что мощности одной машины теоретически рано или поздно упрутся в тупик. Второй случай более сложен в конфигурации, но имеет ряд преимуществ:
- Теоретически бесконечное масштабирование (машин можно поставить сколько угодно)
- Бо́льшая безопасность данных (только при использовании репликации) — машины могут располагаться в разных дата центрах (при падении одной из них, останутся другие)
2. Некоторые примеры развертки mongoDB
1. Самая простая схема, без шардирования
В данной схеме все просто — есть приложение, которое через драйвер общается с mongod. mongod — это основной процесс mongoDB, задача которого — прием запросов, их обработка и выполнение. Данные в mongod хранятся в так называемых chunks (чанки). Каждый chunk имеет размер “chunksize”, который по умолчанию 64 MB. Физически чанки хранятся в файлах dbName.n, где n — порядковый номер, начиная с 0. При достижении размеров 64 MB (или другого chunksize) chunk делится пополам, получается 2 чанка поменьше — по 32 MB, эти 2 чанка начинают наполняться, пока не достигнут размера chunksize, затем разделение происходит снова и т.д. Размер файла dbName.0 равен 64 MB, dbName.1 — 128 MB, dbName.2 — 256 и т.д. до 2Gb. По мере роста количества и размера чанков эти файлы наполняются и когда есть первый файл — dbName.5, размер которого равен 2 Gb, рост размера прекращается и mongoDB просто создает файлы одного и того же размера. Так же следует отметить, что mongoDB не просто создает эти файлы по мере необходимости, а создает их заранее, чтобы при наступлении необходимости реально записать данные в файл, не тратить время на создание файла. Поэтому при относительно небольшом размере реальных данных, вы сможете обнаружить, что места на жестком диске занято прилично.
Данная схема применима чаще всего для локального тестирования.
2. Шардированная схема без реплика сетов
В данной схеме появляются новые элементы. Схема называется шардированной. Важное отличие шардированной схемы — у нее данные не просто записываются в чанки, которые потом делятся пополам, а попадают в них по определенному диапазону заданного поля — shard key. Сначала создается всего один чанк и диапазон значений, которые он принимает, лежит в пределах (-∞, +∞ ):
Когда размер этого чанка достигает chunksize, mongos оценивает значение всех шардкеев внутри чанка и делит чанк таким образом, чтобы данные были разделены примерно поровну. Для примера предположим, что у нас есть 4 документа с полями name, age и id. id — является shard key:
{“name”: “Max”, “age”: 23, “id”: 23}
{“name”: “John”, “age”: 28, “id”: 15}
{“name”: “Nick”, “age”: 19, “id”:56}
{“name”: “Carl”, “age”: 19, “id”: 78}
Предположим размер chunksize, уже достигнут. В данном случае Mongos разделит диапазон примерно так (-,45]; (45, +). У нас получится 2 чанка:
При появлении новых документов, они будут записываться в чанк, который соответствует диапазону shardKey. По достижению chunksize разделение произойдет снова и диапазон будет еще уже и так далее. Все чанки хранятся на шардах.
Важно заметить, что при достижении каким-либо чанком неделимого диапазона, например (44, 45], деление происходить не будет и чанк будет расти свыше chunksize. Поэтому следует внимательно выбирать shard key, чтобы это была как можно наиболее случайная величина. Например, если нам надо заполнить БД всеми людьми на планете, то удачными выборами shard key были бы номер телефона, идентификационный налоговый номер, почтовый индекс. Неудачными — имя, город проживания.
В схеме мы можем видеть config сервер, его отличие от mongod в том, что он не обрабатывает клиентские запросы, а является хранилищем метаданных — он знает физические адреса всех chunk-ов, знает какой chunk, на каком шарде искать и какой диапазон у того или иного шарда. Все эти данные он хранит в специально отведенном месте — config database.
В данной схеме также присутвует роутер запросов — mongos, на него возлагаются следующие задачи:
- Кэширование данных, хранимых на config сервере
- Роутинг запросов чтения и записи от драйвера — маршрутизация запросов от приложений на нужные шарды, mongos точно знает где физически находится тот или иной чанк
- Запуск фонового процесса “Балансер”
Функция балансера заключается в миграции чанков из одного шарда на другой. Процесс происходит примерно так: балансер отсылает команду moveChunk на шард, из которого будет мигрировать chunk, шард получая эту команду, запускает процесс копирования чанка на другой шард. После того, как все документы скопированы, происходит синхронизация документов между этими 2-мя чанками, т.к. пока происходила миграция, в исходный чанк могли добавиться новые данные. После окончания синхронизации шард, который принял новый чанк, отправляет его адрес config серверу, чтобы тот, в свою очередь, обновил его в кэше монгоса. По окончанию этого процесса, если на исходном чанке нет открытых курсоров, он удаляется.
Данная схема часто имеет место в тестовой среде больших приложений, а при использовании 3-х config серверов, может подойти для небольших продакшен приложений. 3 config сервера обеспечивают избыточность данных и если упадет один, mongos все равно будет получать актуальные адреса чанков от других config серверов.
3. Шардированная схема с реплика сетами
В данной схеме помимо шардирования присутствует репликация шардов. Несколько слов об этом. Все операции записи, удаления, обновления, попадают в мастер (primary), а затем записываются в специальную коллекцию oplog, откуда асинхронно попадают на реплики — repl.1 и repl.2 (secondary). Таким образом происходит дублирование данных. Зачем это нужно?
- Избыточность обеспечивает безопасность данных — при падении мастера, происходит голосование между репликами и одна из них становится мастером
- Мастер и реплики могут располагаються в разных дата центрах — это может оказаться полезным, если сервер повреждается физически (пожар в дата центре)
- Реплики могут использоваться для более эффективного чтения данных. Например, есть приложение, которое имеет клиентскую аудиторию в Европе и в США. Одну из реплик можно поместить на территории США и настроить так, чтобы клиенты из США вычитывали данные именно из нее. Стоит отметить, что документы на реплики попадают с запаздыванием и не всегда сразу удается найти на реплике вновь записанный документ. Поэтому данный пункт является преимуществом, только если чтение из реплик позволяет логика приложения
Схема с реплика сетами чаще всего имеет место в серьезных продакшен приложениях, где важна сохранность данных либо имеет место большое количество чтений и логика приложения позволяет читать из реплик.
Подробнее на данной схеме останавливаться не будем, т.к. ей можно посвятить отдельный пост.
3. Шардирование
Итак, приступим. Разворачивать все это будем локально на linux ubuntu 12.0. Для проведения всего этого нам понадобится установленная mongoDB, у меня версия 2.4.9.
Шардировать будем схему №2, только уберем из нее элементы, имеющие чисто теоретическое значение:
Откроем всем нам привычный shell, желательно сразу несколько вкладок, т.к. mongos, mongod, config server — это все отдельные процессы. Далее по пунктам:
- Создадим 2 пустые директории, в которых будут храниться данные:
> sudo mkdir /data/instance1 > sudo mkdir /data/instance2
Поднимаем 2 инстанса mongod командами:
> sudo mongod --dbpath /data/instance1 --port 27000 //для первого инстанса
А в следующем терминале:
> sudo mongod --dbpath /data/instance2 --port 27001 //для второго
Параметр --dbpath указывает путь, по которому будут храниться файлы .0, .1, .2 и .ns. В файлах .0, .1, .2 и т.д. хранятся сами данные данного инстанса в бинарном виде, а в файле .ns — пространство имен, необходимое для навигации по БД. --port — порт, по которому будет доступен объект БД.
После первого пункта у нас имеется два инстанса: - Создадим пустую директорию, в которой будут храниться данные config сервера:
> sudo mkdir /data/config
Поднимаем конфиг сервер, командой
> sudo mongod --configsvr --dbpath /data/config --port 27002
Параметр --configsvr указывает, что новый инстанс будет именно конфиг сервером, --dbpath — путь, по которому будут храниться данные. После второго пункта картина выглядит так (обращу ваше внимание, что пока эти сущности ничего не знают друг о друге):
- Поднимаем mongos, командой
> sudo mongos --configdb 127.0.0.1:27002 --port 27100
По этой команде поднимается mongos на порту 27100, на вход ему нужно передать перечень конфиг серверов с их хостами, на которые он будет обращаться. Если мы при поднятии монгоса не указали порт, то он использует по умолчанию 27017 (если он не занят). После поднятия монгоса:
- Приконнектимся к монгосу, указав порт, на котором мы его поднимали, командой
> mongo --port 27100
После этого получим:
- Остался финальный шаг — добавляем наши шарды в кластер
> sh.addShard("127.0.0.1:27000") > sh.addShard("127.0.0.1:27001")
Эти 2 команды нужно выполнить на монгосе, коннекшен к которому был открыт в пункте 4. Командой db.printShardingStatus() можно просмотреть статус шардинга. Убедимся, что шарды добавлены, в терминале мы должны увидеть что-то вроде:
--- Sharding Status --- sharding version: { "_id" : 1, "version" : 3, "minCompatibleVersion" : 3, "currentVersion" : 4, "clusterId" : ObjectId("53317aefca1ba9ba232b949e") } shards: { "_id" : "shard0000", "host" : "127.0.0.1:27000" } { "_id" : "shard0001", "host" : "127.0.0.1:27001" } databases: { "_id" : "admin", "partitioned" : false, "primary" : "config" } { "_id" : "test", "partitioned" : false, "primary" : "shard0000" }
Имеем финальную картину:
Теперь давайте убедимся, что все, что мы настроили работает, а именно — данные формируются в чанки, а балансер разбрасывает их по шардам. Чтобы данные начали шардироваться, необходимо разрешить шардирование на нужной базе данных, а затем и на коллекции.
Все нижеуказанные команды необходимо выполнять под монгосом. Пусть наша БД называется bank, выполним команды, которые разрешат ее шардировать:
> use admin
> sh.enableSharding("bank")
Еще раз выполним команду db.printShardingStatus(). Вывод должен быть примерно таким:
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "test", "partitioned" : false, "primary" : "shard0000" }
{ "_id" : "bank", "partitioned" : true, "primary" : "shard0000" }
Как видим, напротив partitioned появилось true, значит мы на верном пути.
Теперь давайте поработаем с нашей БД:
> use bank //переключаемся в базу bank (если не существует, то будет создана)
> db.tickets.insert({name: “Max”, amount: Math.random()*100}) //инсертим первую запись в коллекцию tickets (если не существует, то будет создана при первом инсерте)
> for (var i=0; i<2100000; i++) { db.tickets.insert({name: “Max”, amount: Math.random()*100}) } //генерим остальные записи при помощи javascript
> db.tickets.ensureIndex({amount: 1}) //устанавливаем индекс, который будет нам служить в качестве shard Key
> db.tickets.stats() //проверяем, что записи успешно добавились. Следует отметить, что флаг sharded установлен в false, поэтому пока все записи добавлены в primaryShard
> use admin //переключаемся в админ, чтобы разрешить шардирование коллекции tickets. До этого момента все данные хранятся на одном (primary) шарде
> db.runCommand({shardCollection: "bank.tickets", key: {amount: 1}}) //указываем шардируемую коллекцию, а вторым параметром - shard key для нее.
После последний команды мы должны увидеть примерно такое:
{ "collectionsharded" : "bank.tickets", "ok" : 1 }
После всего этого выполним команду sh.status(true) или db.printShardingStatus(), чтобы убедиться, что все заработало и, если все сделано верно, мы должны увидеть следующую картину:
Как видим, данные распределены неравномерно, но если немного подождать и повторить команду db.printShardingStatus(), то картина меняется в сторону равномерного распределения:
И финальная картина:
Как мы увидели, сначала чанки сохраняются на primary шард, а затем мигрируют на второй шард, пока количество не выровняется, причем их диапазоны при этом тоже могут изменяться.
В будущем хотел бы рассказать про организацию памяти в mongoDB и про репликацию. Спасибо за внимание.
Автор: firefoxy