Статьи

Хранение вложений в СУБД «Енисей»

Введение

«Енисей» является документо-ориентированной СУБД и очень хорошо подходит для хранения бинарных данных. Под бинарными данными здесь понимается любой файл произвольного формата, который СУБД не должна парсить и анализировать. Обычно бинарные данные противопоставляются структурированным данным, которые представляют собой непосредственно документы в базе данных, включающие иерархию атрибутов произвольной вложенности. Структурированные данные предназначены для выполнения с ними некоторых операций в соответствии с бизнес-логикой приложения или системы, в то время как бинарные данные сохраняются только для того, чтобы получить их впоследствии при необходимости. Далее по тексту бинарные данные в основном будут называться просто файлами.
Хранение неструктурированных данных в СУБД имеет большое значение в реальных проектах, так как позволяет не разворачивать рядом еще одну систему, например распределенную файловую систему. Файлы просто хранятся в «Енисее», они оказываются доступными для чтения и изменения в любых узлах распределенной системы, доступ к ним обеспечивается с помощью HTTP REST API.
Бинарные данные обычно используются вместе с метаданными, которые выполняют роль структурированного описания всевозможных атрибутов данных. Таким образом, неструктурированные и структурированные данные практически всегда располагаются вместе, дополняя друг друга. В «Енисее» бинарные данные — это вложения к документам, а под метаданными можно понимать непосредственно документы. Каждый документ может содержать одно или несколько вложений.

Особенности работы «Енисея» с файлами баз данных

СУБД «Енисей» работает с файлами баз данных и индексов в режиме append-only. Любые изменения документов приводят к добавлению новой ревизии в конец файла. При этом никогда не выполняется адресная запись в середину файла БД. Такой подход гарантирует высокий уровень отказоустойчивости, поскольку в любой момент времени файл остается полностью консистентным. Даже если произойдет неожиданное выключение или операционная система завершит работу «Енисея» в аварийном порядке, консистентность файла не пострадает. У «Енисея» нет потребности в функции восстановления БД, так как. ее повреждение просто невозможно. Более того, существует подход к резервному копированию БД «Енисея» с помощью простого копирования файлов на уровне файловой системы. В проектах, использующих хранение файлов, очень важна производительность системы для операций записи и чтения. Интересно, что на производительность оказывает непосредственное влияние размер вложений. В различных объектных хранилищах и в СУБД применяются разные способы хранения неструктурированных данных: их можно хранить в основных файлах БД либо в виде отдельных файлов.
Рассмотрим, как функционирует режим append-only при работе с вложениями в основных файлах БД. Такой вариант реализован в прародителе «Енисея» — СУБД CouchDB. Форматом файла БД предусмотрено, что вложение может быть записано не одним блоком, а несколькими. Если присутствует значительная нагрузка на запись в базу данных, то есть одновременно изменяется или добавляется большое количество документов и вложений к ним, то вложения оказываются сильно фрагментированными и в файле БД попеременно лежат фрагменты разных вложений. Вложения записываются в файл БД по мере приема, чтобы обеспечить параллельную работу с базой большого числа клиентов, а в ревизии документа сохраняется список фрагментов данных с указаниями, где начинается каждый фрагмент и какова его длина. Если БД используется для хранения вложений больших размеров, например в качестве системы резервного копирования, то число фрагментов может достигать сотен тысяч и более. То есть даже для хранения списка фрагментов в составе ревизии документа требуются значительные накладные расходы. Это существенно увеличивает итоговый размер файла БД и усложняет процесс получения и сборки вложения при обслуживании операции чтения из БД. Более того, фрагментация происходит даже в том случае, если идет запись единственного вложения, но параллельно с этим записываются новые ревизии других документов. Все принимаемые ревизии немедленно записываются в файл БД с его текущей позиции, прерывая запись вложения одним непрерывным блоком. В общем, в любом случае при наличии высокой нагрузки на БД (на запись) вложения оказываются фрагментированными (рис. 1).

Рисунок 1. Фрагментация вложений и ревизий со списками указателей на фрагменты

Далее, в «Енисее» предусмотрена процедура уплотнения БД (compaction), которая периодически запускается в соответствии с некоторыми критериями. Это необходимо делать во всех append-only-системах по причине увеличения размера файла БД за счет новых ревизий документов при их изменении. При уплотнении создается новый файл БД, в него переносятся только последняя или несколько последних ревизий каждого из документов, после чего исходный файл БД удаляется. Что происходит с вложениями при уплотнении БД? Тело вложений переносится из старого файла БД в новый. Таким образом, при наличии вложений СУБД периодически выполняет перезапись каждого из них вместе со всей базой, то есть копируются достаточно большие объемы данных в фоновом режиме, расходуется ресурс дисков и такты CPU. Да, уплотнение благоприятно сказывается на хранении вложений, так как при этом исключается их фрагментация, но все равно при каждом уплотнении происходит полное копирование всех данных.
При добавлении вложения к документу ссылка на новую ревизию документа в индексном файле появится только после того, как, во-первых, будет сохранено вложение в файл БД , а во-вторых, будет сохранено тело новой ревизии документа. То есть до окончания процесса сохранения вложения индекс ничего «не знает» об обновленной ревизии документа. Если в процессе приема и сохранения вложения запустится уплотнение, то сохраняемая новая ревизия не будет перенесена в новый файл БД, она просто потеряется. Это приведет к тому, что придется повторно принимать и сохранять вложение.
С учетом описанных негативных последствий в «Енисее» дополнительно реализован другой метод хранения вложений — в виде отдельных файлов. В результате удалось существенно увеличить скорость уплотнения и значительно уменьшить нагрузку на диск. Теперь при уплотнении БД вложения вообще не копируются, а просто остаются на том же месте, где лежали изначально, после того как были получены от клиентов. Больше не приходится тратить ресурс диска на периодическое полное копирование, снижаются требования к свободному месту на диске. Больше нет необходимости хранить в основном файле базы длинные списки фрагментов вложений, то есть БД занимает меньше места на диске. Производительность операций чтения данных из базы возросла, так как появилась возможность работать с непрерывными блоками дисковых данных. Это важно, когда база используется именно в качестве хранилища бинарных данных. И наконец, решена проблема с полной потерей сохраненной части вложения, если в процессе передачи был прерван коннект с клиентом. Теперь можно продолжить загрузку вложения с сохраненной позиции.

Эффективность работы с маленькими файлами

Однако не всегда хранение вложений в виде отдельных файлов оптимизирует занимаемое место на диске и производительность записи-чтения данных. Ситуация меняется на 180 градусов, если база используется не в качестве архивного хранилища, а для хранения большого числа мелких бинарных объектов. Эта проблема в свое время получила широкую известность при использовании системы MinIO. В ней, наоборот, сразу стали хранить каждый объект отдельно. И такой подход привел к тому, что для маленьких объектов суммарная скорость выдачи данных оказалась на порядок меньше, чем было заявлено в характеристиках. Кроме того, при хранении маленьких объектов прикладные системы ожидают небольших задержек (latency) при чтении данных, чего в реальности не происходило. Задержки были значительными из-за того, что вместе с выдачей бинарных данных нужно было прочитать с диска и выдать метаданные, а они хранились отдельно. Рассматриваемые задержки выдачи ответа не отличаются для больших и маленьких объектов, но в случае с крупными файлами дополнительные накладные расходы просто незаметны. В результате вроде бы незначительные задержки в выдаче каждого файла приводили к тому, что при работе с большим числом файлов все они суммировались, это определяло низкую результирующую скорость выдачи при последовательной работе с объектами. Данная проблема даже получила свое особенное наименование: «Маленькое файлы — это большой вызов для всех big-data-систем».
Как было сказано выше, формат файлов БД «Енисея» изначально хорошо адаптирован именно к маленьким файлам, так как сохранял их рядом с самими документами, в одном общем файле.

Подход к хранению вложений в «Енисее»

После того как был добавлен новый способ хранения вложений, «Енисей» начал работать в комбинированном режиме. Вложения могут храниться и отдельно, и в составе основного файла БД. Способ хранения автоматически выбирается с учетом размера файла. Крупные вложения хранятся отдельно, а маленькие — в основном файле. В последнем случае оказывается, что бинарные данные и ревизия документа расположены в файле БД рядом, а следовательно, не возникает задержка при их совместной выдаче. Порог переключения способа хранения (пороговый размер файла) задается в конфигурации «Енисея», по умолчанию он равен 32 кБ. Для каждой из баз, хранящихся на одном сервере или кластере, можно принудительно указать тот или иной способ хранения вложений. И указанный способ будет сохранен и после уплотнения базы.
Дополнительно проведено несколько оптимизаций и сделаны некоторые служебные функции. Так, при репликации данных между несколькими базами внутри одного сервера вложения копируются в дисковое пространство новой базы простыми файловыми операциями вместо их прокачки через ядро СУБД. Был решен вопрос с миграцией данных с предыдущих версий «Енисея» и на них, притом что там новый подход к хранению еще не был реализован. Добавлена пара десятков статистических метрик, связанных с обработкой вложений. Работа с вложениями в ядре «Енисея» выполняется с помощью пула процессов фиксированного размера, каждый процесс поддерживает работу фиксированного количество клиентов. «Енисей» реализован на Эрланге и процессы здесь упоминаются в терминологии этого языка, а не как процессы ОС. Это сделано в качестве мер дополнительной защиты от перегрузки сервера и контроля производительности. Вложения хранятся в отдельном подкаталоге для каждой базы данных, поэтому при выполнении дискового копирования баз данных вместе с основным файлом базы и файлами индексов можно сразу скопировать и все имеющиеся вложения.
Для нового способа хранения вложений был проведен эксперимент по измерению скорости работы. Производительность операций записи больших файлов возросла на 15–30% в зависимости от размера файлов. После внесенных изменений «Енисей» стал показывать такую же скорость работы с файлами, как и легковесные веб-сервера (сравнение выполнялось с Python’s uploadserver 5.0.0). Изменения проводились с использованием файлов размером от 0,1 до 100,0 ГБ. Напомним, что ставилась задача улучшить поддержку именно больших файлов, так как изначально в «Енисее» был реализован только способ хранения вложений в общем файле БД, его работа не претерпела изменений.

API работы с вложениями

Интерфейс работы с вложениями «Енисея» предоставляет все варианты получения данных:

  1. Только метаданные. Это запрос самого документа по идентификатору.
GET http://<ADDRESS>:<PORT>/<DB_NAME>/<DOC_ID>
Например, для сервера на локальной машине, со стандартным номером порта, а также для базы с именем db1 и документа с идентификатором doc1 запрос будет:
GET http://127.0.0.1:5984/db1/doc1
Если в документе есть вложения, можно получить следующий ответ:
200 OK
{
   "_id": "ABC_1705693695249",
   "_rev": "2-e6dba1a41c760dda76d9cfaa0b8e7c8c",
   "ts": "1705693695249",
   "inst": "ABC",
   "_attachments": {
       "buffer.txt": {
           "content_type": "text/plain",
           "revpos": 2,
           "digest": "md5-+1QxFVBZi8sUpaf0DqbRmg==",
           "length": 4839,
           "stub": true
       }
   }
}
Видно, что в составе JSON-документа дополнительным служебным атрибутом возвращается список всех вложений с указанием их имени, длины и дополнительных параметров.

2. Только прикрепленные бинарные данные. Делается запрос вложения по его имени.
GET http://<ADDRESS>:<PORT>/<DB_NAME>/<DOC_ID>/<ATTACHMENT_ID>
Для вложения из документа выше запрос будет таким:
GET http://127.0.0.1:5984/db1/doc1/buffer.txt
3.Бинарные данные вместе с метаданными.

GET http://127.0.0.1:5984/db1/doc1?attachments=true
Здесь в параметрах запроса указан ключ, предписывающий возвращать сразу и JSON-документ, и бинарные данные из вложений.

4.Запрос результатов представлений вместе с вложениями.
GET http://<ADDRESS>:<PORT>/<DB_NAME>/_design/<DDOC_ID>/_view/<VIEW>?attachments=true
В формате строки запроса плейсхолдер <DDOC> обозначает имя проектного документа, а <VIEW> — имя одного из представлений, объявленных в данном проектном документе.

Выводы

Таким образом, СУБД «Енисей» обладает достаточно производительной подсистемой работы с вложениями, которая хорошо справляется с файлами различного размера и может быть использована в качестве эффективного хранилища бинарных неструктурированных данных, или, другими словами, в качестве производительного объектного хранилища. Эффективность операций чтения и записи остается высокой независимо от размера хранимых файлов.
Юрий Пипченко
Архитектор СУБД Енисей
ООО “Эквирон”