NOSQL стал очень актуальной темой для крупномасштабного веб-развертывания, где масштабируемость и полуструктурированные данные привели требование БД к NOSQL. За последние пару лет появилось много продуктов NOSQL. В моих прошлых блогах я освещал основную теорию распределенных систем NOSQL , а также некоторые конкретные продукты, такие как CouchDB и Cassandra / HBase .
В прошлую пятницу мне очень повезло встретиться с Джаредом Розоффом из 10gen на технической конференции и обсудить техническую архитектуру MongoDb. Я нашел эту информацию очень полезной и хочу поделиться с большим количеством людей.
MongoDb очень впечатлил меня тем, что он чрезвычайно прост в использовании, а базовая архитектура также очень проста для понимания.
Вот несколько простых шагов администратора для запуска / остановки сервера MongoDb
# Install MongoDB mkdir /data/lib # Start Mongod server .../bin/mongod # data stored in /data/db # Start the command shell .../bin/mongo > show dbs > show collections # Remove collection > db.person.drop() # Stop the Mongod server from shell > use admin > db.shutdownServer()
Основное отличие от RDBMS
MongoDb отличается от RDBMS следующим образом
- В отличие от записи СУБД, которая является «плоской» (фиксированное число простого типа данных), основной единицей MongoDb является «документ», который является «вложенным» и может содержать многозначные поля (массивы, хэш).
- В отличие от РСУБД, где все записи, хранящиеся в таблице, должны быть ограничены схемой таблицы, документы любой структуры могут храниться в одной коллекции.
- В запросе нет операции соединения. В целом, данные рекомендуется упорядочивать более денормализованным образом, и разработчикам приложений ложится больше бремя обеспечения согласованности данных.
- В MongoDb нет понятия «транзакция». «Атомность» гарантируется только на уровне документа (частичное обновление документа не происходит).
- Не существует понятия «изоляция»: любые данные, считанные одним клиентом, могут иметь значение, измененное другим параллельным клиентом.
Удаляя некоторые из тех функций, которые предоставляет классическая СУБД, MongoDb может быть более легким и более масштабируемым при обработке больших данных.
Обработка запросов
MongoDb относится к типу документно-ориентированной БД. В этой модели данные организованы в виде документа JSON и хранятся в коллекции. Коллекция может считаться эквивалентной Таблице, а Документ эквивалентен записям в мире РСУБД.
Вот несколько основных примеров.
# create a doc and save into a collection > p = {firstname:"Dave", lastname:"Ho"} > db.person.save(p) > db.person.insert({firstname:"Ricky", lastname:"Ho"}) # Show all docs within a collection > db.person.find() # Iterate result using cursor > var c = db.person.find() > p1 = c.next() > p2 = c.next()
Чтобы указать критерии поиска, необходимо предоставить пример документа, содержащий поля, которые должны соответствовать.
> p3 = db.person.findone({lastname:"Ho"})
Обратите внимание, что в запросе часть значения должна быть определена до выполнения запроса (другими словами, она не может быть основана на других атрибутах документа). Например, скажем, если у нас есть коллекция «Person», невозможно выразить запрос, который возвращает человека, чей вес превышает его рост в 10 раз.
# Return a subset of fields (ie: projection) > db.person.find({lastname:"Ho"}, {firstname:true}) # Delete some records > db.person.remove({firstname:"Ricky"})
Для ускорения запроса можно использовать индекс. В MongoDb индекс хранится в виде структуры BTree (поэтому запрос диапазона поддерживается автоматически). Поскольку сам документ является деревом, индекс может быть указан как путь и углублен до уровня глубокой вложенности внутри документа.
# To build an index for a collection > db.person.ensureIndex({firstname:1}) # To show all existing indexes > db.person.getIndexes() # To remove an index > db.person.dropIndex({firstname:1}) # Index can be build on a path of the doc. > db.person.ensureIndex({"address.city":1}) # A composite key can be used to build index > db.person.ensureIndex({lastname:1, firstname:1})
Индекс также может быть основан на многозначном атрибуте, таком как массив. В этом случае каждый элемент в массиве будет иметь отдельный узел в BTree.
Построение индекса может выполняться как в автономном режиме переднего плана, так и в режиме онлайн в фоновом режиме. Режим переднего плана будет работать намного быстрее, но к базе данных не будет доступа в течение периода индексации сборки. Если система работает в наборе реплик (описывается ниже), рекомендуется повернуть каждую членскую БД в автономном режиме и построить индекс на переднем плане.
Когда в запросе несколько критериев выбора, MongoDb пытается использовать один единственный наилучший индекс для выбора набора кандидатов, а затем последовательно перебирает их для оценки других критериев.
Когда для коллекции доступно несколько индексов. При первой обработке запроса MongoDb создаст несколько планов выполнения (по одному для каждого доступного индекса) и позволит им по очереди (в пределах определенного числа тактов) выполняться до тех пор, пока не завершится самый быстрый план. Результат самого быстрого исполнителя будет возвращен, и система запомнит соответствующий индекс, используемый самым быстрым исполнителем. Последующий запрос будет использовать запомненный индекс до тех пор, пока в коллекции не произойдет определенное количество обновлений, а затем система повторяет процесс, чтобы выяснить, какой индекс является лучшим в то время.
Поскольку будет использоваться только один индекс, важно взглянуть на критерии поиска или сортировки запроса и создать дополнительный составной индекс, чтобы лучше соответствовать запросу. Поддержание индекса не обходится без затрат, так как индекс должен обновляться при создании, удалении и обновлении документов, что приводит к накладным расходам на операции обновления. Чтобы поддерживать оптимальный баланс, нам необходимо периодически измерять эффективность наличия индекса (например, отношения чтения / записи) и удалять менее эффективные индексы.
Модель хранения
Написанный на C ++, MongoDB использует файл карты памяти, который напрямую отображает файл данных на диске в байтовый массив в памяти, где логика доступа к данным реализована с использованием арифметики указателей. Каждая коллекция документов хранится в одном файле пространства имен (который содержит информацию метаданных), а также в файлах данных нескольких экстентов (с экспоненциальным / удваивающимся увеличением размера). Структура данных широко использует двусвязный список. Каждая коллекция данных организована в виде связного списка экстентов, каждый из которых представляет непрерывное дисковое пространство. Каждый экстент указывает на голову / хвост другого связанного списка документов. Каждый документ содержит связанный список с другими документами, а также фактические данные, закодированные в формате BSON.
Модификация данных происходит на месте. Если модификация увеличивает размер записи за пределы первоначально выделенного пространства, вся запись будет перемещена в больший регион с некоторыми дополнительными байтами заполнения. Заполняющие байты используются в качестве буфера роста, поэтому дальнейшее расширение не требует повторного перемещения данных. Количество заполнения динамически корректируется для каждой коллекции на основе статистики ее изменений. С другой стороны, пространство, занимаемое оригинальным документом, будет свободно. Это отслеживается списком свободных списков разного размера.
Поскольку мы можем предположить, что с течением времени при создании, удалении или изменении объектов будут создаваться дыры, эта фрагментация снизит производительность, так как на один дисковый ввод-вывод будет приходиться меньше данных для чтения / записи. Поэтому нам необходимо периодически запускать команду «compact», которая копирует данные в непрерывное пространство. Эта «компактная» операция, однако, является эксклюзивной и должна выполняться в автономном режиме. Как правило, это выполняется в настройке реплики путем поворота каждого элемента в автономном режиме по одному для выполнения сжатия.
Индекс реализован как BTree. Каждый узел BTree содержит несколько ключей (внутри этого узла), а также указатели на оставленные дочерние узлы BTree каждого ключа.
Обновление данных и транзакция
Чтобы обновить существующий документ, мы можем сделать следующее
var p1 = db.person.findone({lastname:"Ho"}) p1["address"] = "San Jose" db.person.save(p1) # Do the same in one command db.person.update({lastname:"Ho"}, {$set:{address:"San Jose"}}, false, true)
Запись по умолчанию не ждет. Существуют различные варианты ожидания, при которых клиент может указать, какие условия ждать до возврата вызова (это также может быть достигнуто с помощью последующего вызова «getlasterror»), например, когда изменения сохраняются на диске или изменения распространяются среди достаточного числа участников. в наборе реплик. MongoDb также предоставляет изощренный способ назначения тегов членам набора реплик, отражающих их физическую топологию, так что настраиваемая политика записи для каждой коллекции может быть создана на основе их требований надежности.
В СУБД «Сериализуемость» является очень фундаментальным понятием о суммарном эффекте одновременного выполнения рабочих единиц, что эквивалентно тому, как если бы эти рабочие единицы были расположены в некотором порядке последовательного выполнения (по одному за раз). Таким образом, каждый клиент может обращаться с БД исключительно как с доступной. Базовая реализация сервера БД во многих случаях использует LOCK или Multi-version для обеспечения изоляции. Однако эта концепция недоступна в MongoDb (и многих других NOSQL).
В MongoDb все прочитанные вами данные должны рассматриваться как снимок прошлого, что означает, что к тому времени, когда вы смотрите на них, они могут быть изменены в БД. Поэтому, если вы делаете модификацию на основе ранее прочитанных данных, к моменту отправки запроса на модификацию условие, на котором основана ваша модификация, может измениться. Если это неприемлемо для требования согласованности вашего приложения, вам может потребоваться повторно проверить условие в момент, когда вы запрашиваете изменение (т. Е. Необходимо выполнить «conditional_modify»).
Согласно этой схеме, «условие» прикрепляется вместе с запросом на изменение, чтобы сервер БД мог проверить условие перед применением модификации. (конечно, проверка и модификация условия должны быть атомарными, чтобы между ними не происходило обновление). В MongoDb этого можно добиться с помощью вызова findAndModify.
var account = db.bank.findone({id:1234}) var old_bal = account['balance'] var new_bal = old_bal + fund # Pre-condition is specified in search criteria db.bank.findAndModify({id:1234, balance:old_bal}, {$set: {balance: new_bal}}) # Check if the prev command successfully var success = db.runCommand({getlasterror:1,j:true}) if (!success) { #retry_from_beginning }
Понятие «транзакция» также отсутствует в MongoDb. Хотя MongoDb гарантирует, что каждый документ будет атомарно изменен (поэтому частичное обновление не будет происходить в документе), но если обновление изменяет несколько документов, то нет никакой гарантии атомарности между документами.
Таким образом, разработчики приложений несут ответственность за внедрение многократного обновления в нескольких документах. Мы опишем общий шаблон дизайна для достижения этой цели. Этот метод не является специфичным для MongoDb и применим к другому хранилищу NOSQL, которое может по крайней мере гарантировать атомарность на уровне одной записи.
Основная идея заключается в том, чтобы сначала создать отдельный документ (называемый транзакцией), который связывает воедино все документы, которые вы хотите изменить. А затем создайте обратную ссылку из каждого документа (подлежащего изменению) обратно на транзакцию. Тщательно продумав последовательность обновления документов и транзакции, мы можем добиться атомарности изменения нескольких документов.
На веб-сайте MongoDb также описана похожая методика (основанная на той же концепции, но реализация немного отличается).
Модель репликации
Высокая доступность достигается в MongoDb с помощью набора реплик, который обеспечивает избыточность данных на нескольких физических серверах, включая одну первичную БД, а также несколько вторичных БД. Для обеспечения согласованности данных все запросы на изменение (вставка / обновление / удаление) направляются в первичную БД, где вносятся изменения, и асинхронно реплицируются в другие вторичные БД.
Внутри набора реплик члены соединяются друг с другом для обмена сообщениями пульса. Разрушенный сервер с отсутствующим пульсом будет обнаружен другими участниками и удален из членства в наборе реплик. После восстановления мертвого вторичного сервера в будущем он может присоединиться к кластеру, подключившись к первичному серверу для получения последнего обновления с момента последнего сбоя. Если сбой происходит в течение длительного периода времени, когда журнал изменений с первичного сервера не охватывает весь период сбоя, то восстановленному вторичному устройству необходимо перезагрузить все данные с первичного сервера (как если бы это был совершенно новый сервер).
В случае сбоев первичной БД, среди оставшихся членов будет запущен протокол выбора лидера, чтобы назначить нового первичного участника, основываясь на многих факторах, таких как приоритет узла, время работы узла и т. Д. После получения большинства голосов новый первичный сервер состоится. Обратите внимание, что из-за асинхронной репликации вновь выбранная первичная БД не нуждается в обновлении всех последних данных из сбойной БД.
Клиентская библиотека предоставляет API для доступа приложения к серверу MongoDB. При запуске клиентская библиотека подключится к некоторому члену (на основе начального списка) набора реплики и выполнит команду «isMaster», чтобы собрать текущее изображение набора (кто является основным и дополнительным). После этого клиентская библиотека подключается к одному первичному серверу (куда он будет отправлять все запросы на изменение БД) и к некоторому количеству вторичных серверов (где он будет отправлять запросы только для чтения). Клиентская библиотека будет периодически перезапускать команду «isMaster», чтобы определить, присоединились ли новые члены к набору. При сбое существующего члена в наборе соединения со всеми существующими клиентами будут разорваны, что приведет к повторной синхронизации самой последней картинки.
Существует также специальная вторичная БД, называемая ведомой задержкой, которая гарантирует передачу данных с определенной временной задержкой ведущему устройству. Это используется в основном для восстановления данных после случайного удаления данных.
Для запроса на изменение данных клиент отправит запрос в первичную БД, по умолчанию запрос будет возвращен после записи в первичную базу данных, можно указать необязательный параметр, указывающий, что определенное количество вторичных серверов должно получить модификацию перед возвратом, поэтому клиент может убедиться, что большая часть членов получила запрос. Конечно, существует компромисс между задержкой и надежностью.
Для запроса запроса по умолчанию клиент свяжется с первичным, который имеет последние обновленные данные. При желании клиент может указать свою готовность читать из любых вторичных серверов и допустить, что возвращаемые данные могут быть устаревшими. Это дает возможность сбалансировать загрузку запроса на чтение для всех вторичных серверов. Обратите внимание, что в этом случае последующее чтение после записи может не увидеть обновление.
Для приложений, предназначенных главным образом для чтения, чтение любых вторичных файлов может значительно повысить производительность. Чтобы выбрать самый быстрый вторичный элемент БД для выдачи запроса, клиентский драйвер периодически проверяет связь с участниками и предпочитает отправлять запрос тому, который имеет минимальную задержку. Обратите внимание, что запрос на чтение выдан только одному узлу, в MongoDb нет чтения или чтения кворума с нескольких узлов.
Основная цель набора Replica — обеспечить избыточность данных, а также запрос на чтение баланса нагрузки. Он не обеспечивает балансировку нагрузки для запроса на запись, так как все модификации все равно должны идти к одному мастеру.
Другое преимущество набора реплик состоит в том, что участники могут быть переведены в автономный режим на основе ротации для выполнения дорогостоящей операции, такой как сжатие, индексирование или резервное копирование, без воздействия на онлайн-клиентов, использующих живые элементы.
Модель Sharding
Чтобы загрузить запрос записи баланса, мы можем использовать шарды MongoDb. В настройке сегментирования коллекция может быть разделена (с помощью ключа разделения) на порции (которые являются диапазоном ключей), и их можно распределить по нескольким шардам (каждый шард будет набором реплик). Sharding MongoDb эффективно обеспечивает неограниченный размер для сбора данных, что важно для любого сценария больших данных.
Повторим еще раз: в модели сегментирования для каждой коллекции, хранящейся в кластере сегментирования, будет указан один ключ раздела. Пространство ключей ключа разделения разделено на непрерывный диапазон ключей, называемый чанком, который размещается соответствующими шардами.
# To define the partition key db.runcommand({shardcollection: "testdb.person", key: {firstname:1, lastname:1}})
В настройке сегмента клиентская библиотека подключается к серверу маршрутизации без сохранения состояния «MongoS», который ведет себя как «MongoD». Сервер маршрутизации играет важную роль в пересылке клиентского запроса на соответствующий сервер сегмента в соответствии с характеристиками запроса.
Для запроса вставки / удаления / обновления, содержащего ключ раздела, на основе информации о сопоставлении чанка / сегмента (полученной с сервера конфигурации и кэширования локально) сервер маршрутизации перенаправит запрос на соответствующий первичный сервер, на котором размещается чанк, диапазон ключей которого охватывает ключ раздела модифицированного документа. При наличии конкретного ключа раздела основной сервер, содержащий этот чанк, может быть определен однозначно.
В случае запроса запроса сервер маршрутизации проверит, является ли ключ раздела частью критериев выбора, и если это так, то только «направит» запрос к соответствующему сегменту (первичному или вторичному). Однако, если ключ раздела не является частью критериев выбора, то сервер маршрутизации будет «разбрасывать» запрос каждому сегменту (выбрать по одному элементу каждого сегмента), который будет вычислять его локальный поиск, и результат будет получен в сервер маршрутизации и возврат к клиенту. Когда запрос требует, чтобы результат был отсортирован, и если ключ разделения включен в порядок сортировки, сервер маршрутизации направит запрос последовательно на несколько сегментов, когда клиент выполнит итерацию результата. В случае, если сортировка включает другой ключ, который не является ключом раздела,сервер маршрутизатора рассылает запрос всем осколкам, которые будут выполнять его локальную сортировку, а затем объединяет результат на сервере маршрутизации (в основном распределенная сортировка слиянием).
Поскольку данные вставляются в чанк и приближаются к его полной емкости, нам нужно разделить чанк. Сервер маршрутизации может обнаружить эту ситуацию статистически на основе количества запросов, которые он пересылает, а также количества других существующих серверов маршрутизации. Затем сервер маршрутизации свяжется с основным сервером сегмента, который содержит полный фрагмент, и запросит разделение фрагмента. Сервер сегмента вычислит среднюю точку диапазона ключей, которая может равномерно распределить данные, а затем разделит блок и обновит сервер конфигурации относительно его точки разделения. Обратите внимание, что пока нет перемещения данных, так как данные все еще находятся на том же сервере шарда.
С другой стороны, существует еще один процесс «балансировки» (запущенный на одном из серверов маршрутизации), задача которого состоит в том, чтобы каждый сегмент содержал примерно одинаковое количество фрагментов. При обнаружении дисбаланса балансировщик свяжется с занятым осколком, чтобы запустить процесс миграции порции. Этот процесс миграции происходит в режиме онлайн, когда источник связывается с пунктом назначения, чтобы инициировать передачу данных, и данные начинают копироваться из источника в место назначения. Этот процесс может занять некоторое время (зависит от объема данных), в течение которого обновление может происходить непрерывно в начале. Эти изменения будут отслеживаться в начале и после завершения копирования, дельта затем будет перенесена и применена к месту назначения. После нескольких раундов применения дельт,Миграция входит в последний раунд, и источник остановит и удержит все запросы, поступающие от сервера маршрутизации. После того, как последний раунд изменений был применен к назначению, назначение обновит сервер конфигурации о новой конфигурации сегмента и уведомит исходный фрагмент (который все еще удерживает запрос), чтобы вернуть исключение StaleConfigException на сервер маршрутизации, который затем перечитайте последнюю конфигурацию из configServer и повторно отправьте предыдущие запросы. В какой-то момент в будущем данные в начале будут физически удалены.пункт назначения обновит сервер конфигурации о новой конфигурации сегмента и уведомит исходный сегмент (который все еще удерживает запрос), чтобы вернуть StaleConfigException на сервер маршрутизации, который затем повторно прочитает последнюю конфигурацию из configServer и повторно отправит предыдущие запросы. В какой-то момент в будущем данные в начале будут физически удалены.пункт назначения обновит сервер конфигурации о новой конфигурации сегмента и уведомит исходный сегмент (который все еще удерживает запрос), чтобы вернуть StaleConfigException на сервер маршрутизации, который затем повторно прочитает последнюю конфигурацию из configServer и повторно отправит предыдущие запросы. В какой-то момент в будущем данные в начале будут физически удалены.
Вполне возможно, что в условиях высокой частоты обновления изменения, применяемые к месту назначения, не в состоянии догнать частоту обновления, произошедшую в начале. Когда эта ситуация обнаружена, весь процесс миграции будет прерван. Сервер маршрутизации может выбрать другой чанк для последующей миграции.
Map / Reduce Execution
MongoDb предоставляет инфраструктуру Map / Reduce для параллельной обработки данных. Концепция похожа на Hadoop Map / Reduce , но со следующими небольшими отличиями …
- Он принимает входные данные из результата запроса коллекции, а не каталога HDFS
- Вывод сокращения можно добавить в существующую коллекцию, а не в пустой каталог HDFS.
Map / Reduce работает следующим образом
- Клиент определяет функцию отображения, функцию сокращения, запрос, который создает входные данные, и коллекцию, в которой хранится результат вывода. Затем отправьте запрос на сервер маршрутизации MongoS
- MongoS рассылает запрос всем осколкам (по одному участнику каждого осколка)
- Каждый сервер сегментирования выполняет запрос и передает выходные данные запроса в функцию map, которая выполняет определенный пользователем код и генерирует пары ключ-значение.
- Излученные пары одного и того же ключа поступят на один и тот же основной сервер Shard, который выполняет пользовательскую функцию сокращения. Возвращаемое значение будет записано в выходной набор.
Вот простой пример построения инвертированного индекса из документа в разделы
db.book.insert({title:"NOSQL", about:["software", "db"]}) db.book.insert({title:"Java programming", about:["software", "program"]}) db.book.insert({title:"Mongo", about:["db", "technology"]}) db.book.insert({title:"Oracle", about:["db", "software"]}) db.book.find() m = function() { for (var i in this.about) { emit(this.about[i], this.title) } } r = function(k, vals) { return({topic:k, title:vals}) } db.book.mapReduce(m, r, {query:{}, out:{replace:"mroutput"}}) db.mroutput.find()
В целом, я обнаружил, что MongoDb очень мощный и простой в использовании. Я с нетерпением жду возможности использовать MongoDb с Node.js и поделюсь своим опытом в будущих блогах.
Особенно благодаря Джареду Розоффу, который предоставил мне много деталей о том, как реализован MongoDb.
КОНЕЦ