Статьи

Реализация CouchDB

CouchDB — это проект Apache OpenSource. Это
мозг ребенка
Дэмиена Каца, обладающий рядом привлекательных особенностей, основанных на очень классных технологиях. Такие как …

  • RESTful API
  • Хранилище документов без схемы (документ в формате JSON)
  • Модель Multi-Version-Concurrency-Control
  • Определяемый пользователем запрос структурирован как карта / уменьшить
  • Механизм обновления инкрементного индекса
  • Модель Multi-Master Replication
  • Написано на эрланге ( эрланг это хорошо )

Существует широкий спектр сценариев приложений, в которых CouchDB может быть подходящим решением: от периодически подключаемых приложений для ноутбуков, высокопроизводительных кластеров данных и вплоть до виртуальных хранилищ данных в облаке.

Чтобы глубже понять дизайн CouchDB, мне очень повезло поговорить с Дэмиеном, который так любезно поделился со мной многими деталями. Здесь я хочу показать, что я узнал из этого разговора.

Базовая структура хранения

CouchDB — это документно-ориентированная база данных, в которой документ представляет собой строку JSON (с необязательным двоичным вложением). Базовая структура состоит из «хранилища», а также нескольких «индексов представления». «Хранение» используется для хранения документов,
а «просмотр индексов» — для обработки запросов.

Внутри файла хранения есть «смежные» области,
которые используются для хранения документов. Существует 2 индекса дерева B + для ускорения определенной оценки документов.

  • by_id_index (который использует идентификатор документа в качестве ключа). Он в основном используется для поиска документа по его идентификатору документа, он указывает на список ревизий (или на дерево ревизий в случае конфликтов в сценарии репликации) с момента последнего сжатия. Он также хранит историю изменений (которая не будет затронута уплотнением).
  • by_seqnum_index (который использует монотонно увеличивающееся число в качестве ключа). Seqnum генерируется всякий раз, когда документ обновляется. (Обратите внимание, что все обновления происходят последовательно, поэтому последовательность отражает последовательность не одновременных обновлений). Он в основном используется для отслеживания последней точки синхронизации репликации, последнего обновления индекса точки зрения .

Операция «Только добавление»

Все обновления (создание документов, изменение документов и удаление документов) выполняются в механизме только добавления. Вместо изменения существующих документов создается новая копия, которая добавляется в текущий регион. После этого узлы дерева b + также модифицируются, чтобы указывать на новое местоположение документа. Модификация узлов дерева b + также выполняется только для добавления, что означает, что новый узел дерева b + копируется и добавляет хвост в конец файла. Это, в свою очередь, инициирует модификацию родительского узла узла b + дерева, которая вызывает новую копию родительского узла… до тех пор, пока полностью не вернется к корневому узлу b + дерева. И, наконец, измените заголовок файла, чтобы он указывал на новый корневой узел.

Это означает, что все обновления будут инициировать 1 запись в документ (кроме удаления) и запись logN на каждую страницу узла B + Tree. Так что это O (logN) сложность.

Операция только для добавления предоставляет интересную модель MVCC (Multi-Version Concurrency Control), поскольку файл хранит историю всех версий предыдущего состояния документа. Пока клиент удерживает предыдущий корневой узел индекса B + Tree, он может получать представление моментального снимка. Хотя обновление может происходить постоянно, клиент не увидит никаких последних изменений. Такой снимок согласованности очень полезен при онлайн-резервном копировании, а также при онлайн-сжатии.

Обратите внимание, что при чтении операция выполняется одновременно с другими операциями чтения и записи. Операция записи выполняется в последовательном порядке по всем документам. Другими словами, в любое время может выполняться только одно обновление документа (однако запись вложений в документе может происходить параллельно).

GET document

Когда клиент отправляет HTTP-запрос REST GET в CouchDB, DBServer…

  • Посмотрите на заголовок файла, чтобы найти корневой узел индекса B + Tree by_id
  • Пройдите по дереву B +, чтобы выяснить местоположение документа
  • Прочитайте документ и верните его обратно клиенту

Документ PUT (модификация)

Когда клиент выполняет вызов HTTP REST POST для CouchDB, DBServer…

  • Посмотрите на заголовок файла, чтобы найти корневой узел индекса B + Tree by_id
  • Пройдите по дереву B +, чтобы выяснить конечный узел и местоположение документа.
  • Прочитайте документ. Сравните ревизию, сгенерируйте ошибку, если они не совпадают.
  • Если они совпадают, выясните старую последовательность текущей ревизии.
  • Создайте новый (монотонно увеличивающийся) seqnum, а также новую ревизию
  • Найдите последний регион, чтобы посмотреть, подходит ли этот документ. Если нет, выделите другой смежный регион.
  • Запишите документ (с новой редакцией) в новый регион
  • Измените дерево by_id b +, чтобы указать новое местоположение документа
  • Измените дерево by_seqnum b +, чтобы добавить новую запись (нового seqnum) и удалить старую запись (старого seqnum).

Обратите внимание, что by_seqnum B + Tree index всегда указывает на последнюю ревизию, предыдущая ревизия автоматически забывается.

Документ PUT / POST (создание)

Когда клиент выполняет вызов HTTP REST PUT для CouchDB, DBServer…

  • Создайте новый (монотонно увеличивающийся) seqnum, а также новый идентификатор документа и ревизию
  • Найдите последний регион, чтобы посмотреть, подходит ли этот документ. Если нет, выделите другой смежный регион.
  • Запишите документ (с новой редакцией) в новый регион
  • Измените дерево by_id b +, чтобы указать новое местоположение документа
  • Измените дерево by_seqnum b +, чтобы добавить новую запись (нового seqnum)

УДАЛИТЬ документ (изменить)

Когда клиент отправляет HTTP-запрос REST DELETE в CouchDB, DBServer…

  • Посмотрите на заголовок файла, чтобы найти корневой узел индекса B + Tree by_id
  • Пройдите по дереву B +, чтобы выяснить конечный узел и местоположение документа.
  • Прочитайте документ. Сравните ревизию, сгенерируйте ошибку, если они не совпадают.
  • Если они совпадают, выясните старую последовательность текущей ревизии.
  • Создайте новый (монотонно увеличивающийся) seqnum, а также новую ревизию
  • Измените историю изменений by_id b + tree, чтобы показать, что этот путь исправления удален
  • Измените дерево by_seqnum b +, чтобы добавить новую запись (нового seqnum) и удалить старую запись (старого seqnum).

Оперативное сжатие

В качестве операции только для добавления файл хранилища со временем будет увеличиваться. Поэтому нам нужно регулярно сжимать файл.

  • Откройте новый файл хранилища
  • Пройдите по дереву by_seqnum b + index (который указывает только на последнюю ревизию), найдите документ
  • Скопируйте документ в новый файл хранилища (который автоматически обновляет соответствующие индексы дерева b + в новом файле хранилища).

Обратите внимание, что из-за характеристики MVCC, сжатие получит моментальный снимок согласованности и может происходить одновременно без постоянного обновления после начала сжатия. Однако, если скорость обновления слишком высока, процесс уплотнения никогда не сможет догнать обновление, которое продолжает добавляться в файл. Разрабатывается механизм регулирования, который замедляет скорость обновления клиента.

Просмотр индексов

CouchDB поддерживает концепцию «просмотра» базы данных. Представление фактически является результатом пользовательской обработки в базовом хранилище документов. Определяемая пользователем обработка должна быть организована как двухэтапная обработка: «отображение» и «уменьшение». (обратите внимание, что семантика сокращения сильно отличается от модели Google Map / Reduce). Map () — это определенная пользователем функция, которая преобразует каждый документ в ноль, один или несколько промежуточных объектов, а lower () — это другая определенная пользователем функция для объединения промежуточных объектов в конечный результат.

Промежуточные объекты map () и lower () хранятся в индексах представления. Поскольку хранилище обновляется, предыдущие результаты, сохраненные в индексах представления, больше не действительны и должны быть обновлены. CouchDB использует механизм инкрементного обновления, поэтому обновление индексов представлений является высокоэффективным.

Определения видов сгруппированы в проектный документ.

Каждый вид определяется одной функцией «map» и дополнительной функцией «Reduce».

map = function(doc) {

emit(k1, v1)

emit(k2, v2)

}

reduce = function(keys, values) {

return result
}

Функция redu () должна быть коммутативной и ассоциативной, чтобы порядок редукции был произвольным.

Представления, определенные в каждом проектном документе, материализуются в файле представления.
Первоначально, файл представления пуст (индекс еще не был построен). Представление строится лениво, когда первый запрос сделан.

  1. CouchDB будет обходить индекс B + Tree by_seqnum для файла хранилища.
  2. Исходя из этого, CouchDB получает последние версии всех существующих документов
  3. CouchDB запоминает последнюю последовательность и затем подает каждый документ на View Server с помощью «map_doc».
  4. View Server вызывает функцию map (doc), для каждого вызова emit (ключ, значение) создается запись.
  5. Наконец, набор записей вычисляется и возвращается обратно в CouchDB.
  6. CouchDb добавит эти записи в индекс B + Tree, key = emit_key + doc_id. Для каждого из B + Tree оставить узел.
  7. CouchDB отправит все содержащиеся в ней записи карты обратно на сервер просмотра, используя «уменьшить».
  8. View Server вызывает функцию Reduce (ключи, значения).
  9. Результат уменьшения вычисляется и возвращается к CouchDB
  10. CouchDb обновит выходной узел B + Tree, чтобы он указывал на уменьшенное значение результатов, содержащих его карту.
  11. После этого CouchDb переместится на один уровень выше к родительскому узлу уходящего узла B + Tree. Для каждого из родительских узлов B + Tree CouchDB отправит соответствующий результат уменьшения своих дочерних узлов на сервер View, используя «rereduce».
  12. View Server снова вызывает функцию уменьшения (ключи, значения).
  13. Наконец, результат восстановления вычисляется и возвращается обратно в CouchDB.
  14. CouchDB обновит родительский узел B + Tree, чтобы он указывал на значение восстановления.

CouchDB продолжает подниматься на один уровень и повторять расчет результата восстановления. Наконец, результат восстановления корневого узла также обновляется.
Когда это будет сделано, индекс представления будет выглядеть примерно так …
Инкрементное обновление представления CouchDB обновляет индексы представления лениво и постепенно. Это означает, что при обновлении документов CouchDB не будет обновлять индекс представления, пока следующий запрос не достигнет CouchDB.
Затем CouchDB обновляет индекс следующим образом.

  • Затем CouchDB будет обходить индекс B + Tree by_seqnum файла хранения, начиная с последнего seqnum.
  • CouchDB извлекает все документы изменений со времени последнего запроса вида и передает их в функцию карты сервера представления и возвращает набор результатов карты.
  • CouchDb обновит результат карты в индексе B + Tree, некоторые из оставленных узлов B + Tree будут обновлены.
  • Для этих обновленных оставьте узел B + Tree, CouchDB повторно отправляет все содержащиеся в нем записи карты обратно на сервер просмотра, чтобы пересчитать уменьшенное значение. Затем сохраните уменьшенное значение в узле B + Tree.
  • Все родители обновленного покидающего узла B + Tree, CouchDB должны пересчитать значение переопределения и сохранить его внутри узла B + Tree. Пока все, вплоть до корневого узла.

Из-за непротиворечивой характеристики моментального снимка длительный запрос представления может выполняться одновременно (без помех) с текущим обновлением БД. Тем не менее, запрос должен ждать завершения обновления индекса представления, прежде чем увидеть согласованный результат. Существует также возможность (в стадии разработки) немедленно вернуть устаревшую копию представления, если клиент может это допустить.

Обработка запроса

Когда клиент получает результат представления, существуют следующие сценарии

Запрос в представлении только карты

В этом случае фаза обновления индексов представления отсутствует. Для выполнения обработки запроса, CouchDB просто поиск B + дереву , чтобы найти соответствующую начальную точку ключа (обратите внимание , что ключ предваряются emit_key) , а затем вернуть все результаты на карте этого ключа

запрос на карте с уменьшить

Есть 2 случая. Если запрос находится на окончательном уменьшающем значении по всему представлению, то CouchDB просто вернет восстановленное значение, указанное корнем B + Tree индекса представления.

Если запрос относится к уменьшенному значению каждого ключа (group_by_key = true), тогда CouchDB попытается найти границу каждого ключа. Поскольку этот диапазон, вероятно, не совсем точно соответствует узлу B + Tree, CouchDB необходимо выяснить край обоих концов, чтобы найти частично совпадающий оставленный узел B + Tree, и повторно отправить результат своей карты (с этим ключом) на сервер просмотра. Этот результат уменьшения затем объединится с существующим результатом восстановления, чтобы вычислить окончательный результат уменьшения этого ключа.
Например, если интервал между оставленными узлами от A до F, то ключ частично попадает в узел A, и узел F необходимо отправить снова, чтобы уменьшить (). Результат будет восстановлен с использованием существующего уменьшающего значения узла E и существующего восстановительного значения узла P.
Репликация БД

CouchDB поддерживает несколько реплик БД, работающих на разных компьютерах, и предоставляет механизм для синхронизации их данных. Это полезно в 2 распространенных сценариях

  • Иногда подключенные приложения (например, КПК). В этом случае пользователь может работать в автономном режиме в течение определенного периода времени и сохранять свои данные локально. Позже, когда он подключается обратно к своей корпоративной сети, он может синхронизировать свои изменения обратно в свою корпоративную базу данных.
  • Приложение для критически важных задач (например, кластеры). В этом случае БД будет реплицироваться на нескольких машинах, так что надежность может быть достигнута за счет избыточности, а высокая производительность — за счет балансировки нагрузки.

Под ним находится процесс репликации, который принимает команды репликации. Команда указывает исходную и целевую БД. Затем репликатор запросит в исходной БД все обновленные документы после определенного seq_num. Другими словами, репликатор должен отслеживать последний seq_num. Затем он отправляет запрос в целевую БД, чтобы извлечь текущую историю изменений всех этих документов и проверить, является ли история изменений целевой системы более старой, чем исходная. Если это так, он будет выдвигать документы изменений к цели, в противном случае он пропустит отправку документа.

В targetDB конфликты могут быть обнаружены, когда документ был обновлен в целевой DB. Конфликт затем будет отмечен в дереве ревизий, указанном индексом by_id.

Перед разрешением этого конфликта CouchDB рассмотрит версию с самым длинным путем как победителя и покажет это в представлениях. Тем не менее, CouchDB ожидает, что есть отдельный процесс (возможно, вручную), чтобы исправить конфликт.

Теперь построить модель с несколькими основными репликами на основе двунаправленной синхронизации данных поверх репликатора довольно просто.

Например, у нас может быть парный процесс «сплетен», который периодически запускается (или запускается определенными событиями). Процесс будет делать следующее …

  1. Скопируйте изменения из источника = реплика А в цель = реплика Б
  2. В обратном направлении скопируйте изменения из источника = реплика B в цель = реплика A
  3. Выберите случайным образом между replicaA или replicaB, назовите его победителем.
  4. Вызвать предоставленную пользователем функцию слияния (doc_revA), чтобы исправить дерево ревизий. В основном работает специфичная для приложения логика, чтобы вернуть дерево в список.
  5. Скопируйте изменения обратно от победителя к проигравшему. Это будет повторять исправления.

Соображения согласованности данных

CouchDB не имеет концепции транзакций и не отслеживает взаимозависимость между документами. Важно убедиться, что целостность данных не распространяется на более чем один документ.

Например, целостность данных может стать проблемой, если ваше приложение прочитало document-X и основываясь на том, что оно прочитало для обновления document-Y. Возможно, что после прочтения document-X, какое-то другое приложение может изменить document-X на что-то еще, о чем вы не знаете. И вы обновляете document-Y на основе устаревшего значения. CouchDB не может обнаружить такой конфликт, потому что это происходит в двух разных документах.

Дополнительные проблемы согласованности данных возникают в настройке репликации данных. Поскольку синхронизация данных происходит в фоновом режиме, будет задержка, чтобы увидеть последние изменения, если это произойдет в других репликах. Если клиент подключится к реплике недетерминированным способом, то может произойти следующий сценарий:

  • Клиент прочитал документ, а затем снова прочитал тот же документ, но второе чтение вернет более раннюю версию, чем первое чтение.
  • Клиент обновляет документ, а затем снова читает документ, но он не видит свое собственное обновление.