Как хранить миллионы файлов с контролем доступа: обзор решений
Всем привет! Меня зовут Павел. В разработке 25+ лет, начинал с Object Pascal, затем Unix + C, затем по наклонной: Delphi, JS, HTML5, немножко Java, Go, Rust. Работал практически со всеми СУБД, иногда довольно больших размеров (>10 TB). Последние 8 лет — на должности архитектора в компании InBase. Один из продуктов компании — система электронного документооборота Megapolis Doc.Net (разрабатывается с 1998 года). Моей задачей была миграция этой системы на веб-технологии. Собственно, о решении одной из проблем, с которой мы столкнулись, а именно — о поиске оптимального способа хранения неструктурированной информации и доступа к ней с учетом прав пользователей — я и хочу рассказать.
Статья может быть полезна как техническим специалистам компаний-заказчиков, перед которыми стоит проблема выбора продукта с оптимальной архитектурой хранилища документов, так и разработчикам информационных систем.
Решаемые задачи
Например, такие:
- Необходимо хранить на серверах значительное (более 100 000) количество документов (скан-копии, файлы PDF, Word, XML и т. д.) и метаинформации к ним (расширенный набор атрибутов). Например: № документа, идентификатор контрагента, который его прислал, состояние документа и т. д.
- Обеспечить доступ к документам по протоколу HTTP(s) (браузер/WebDAV) с авторизацией.
- Обеспечить динамическое определение прав доступа к документам. Например: в зависимости от должности или роли пользователя в системе, значения атрибутов документа (директор видит все, менеджер — только документы, которые касаются его контрагентов), данных внешних систем (сотрудник отдела финансового мониторинга видит документы контрагентов из черного списка, определяется вызовом к внешнему сервису) и тому подобные правила.
В общем, это — типичные задачи, которые стоят перед разработчиками большинства корпоративных приложений. Проще всего решать их как? Правильно, с помощью базы данных. Но оказалось, что когда документов становится очень много, возникают сложности. Дальше о них подробнее.
Базы данных. У всего есть предел
Это, казалось бы, очевидно. Если есть база данных, то можно спокойно пользоваться всеми ее благами: целостностью, транзакционностью, бэкапами и так далее. То есть архитектура довольно простая: есть приложение, есть база данных, мы в нее пишем файлы в BLOB- поле, и у нас все хорошо. Надо откатить транзакцию — откатили. Надо забэкапиться — забэкапились. И это работает. До поры до времени. Пока есть 100 пользователей и 100К документов — все хорошо. Но когда документов становится больше или количество пользователей увеличивается, начинают возникать проблемы (вне зависимости от выбранной СУБД). Рассмотрим их.
Проблема № 1
Чем больше база данных, тем сложнее ее бэкапить. Понятно, что можно выделить какие-то отдельные табличные пространства для BLOBов, бэкапить частично, но все равно — это сложно. И когда мы оказываемся перед фактом, что у нас есть табличка, и в ней, предположим, 100 млн строк с BLOBами по 200к каждый (при этом она не партиционирована), то бэкапить ее придется всю. Впрочем, даже если есть partition, периодически бэкапить таблицу полностью все равно придется. И когда эта табличка «дорастет» до пары десятков терабайт, ее бэкап будет длиться очень долго.
Проблема № 2
Еще одна проблема баз данных — масштабирование. Зачастую можно легко масштабировать серверы приложений горизонтально: поставить десять или двадцать серверов приложений. Но сервер базы данных горизонтально масштабируется очень плохо. Его можно масштабировать только вертикально, наращивая мощность, добавляя память, установив более мощный процессор. И рано или поздно мы упремся в потолок. После этого либо начинается экспоненциальный рост цены вопроса, либо обнаружится, что достаточно мощного оборудования для решения задачи в природе еще не существует. Добавление шардирования (если архитектура изначально его не предусматривала) — тоже задача не из простых.
Таким образом, если система будет продолжать писать все в базу, то в конце концов это закончится тем, что именно база станет самым слабым звеном. А когда база становится слабым звеном, это — очень плохо. С этим практически ничего нельзя сделать, и останется только одно — бросить все и уехать в Ладакх. Этот вариант решения проблемы мы обсудим позже. Когда-нибудь. А сейчас рассмотрим другие опции.
Не храни в базе — храни в файлах
В итоге возникла идея вынести из базы данных все, что можно из нее вынести. То есть: убираем BLOBы из БД, вместо них оставляем текстовое поле с информацией о том, где сохранен контент файла, сам файл пишем в файловую систему. Транзакция БД подтвердилась — файл будет доступен пользователям, транзакция откатилась — файл останется в файловой системе как «мусор».
Во втором поколении UnityBase (платформа, на которой работает наш документооборот) так и сделали. Но оказалось, что не все так просто.
Не клади все файлы в одну корзину
Пока было 1000 файлов или 10 000 файлов, проблем не возникало. Все прекрасно работало. Файлы лежали в одной папке файловой системы, с ними можно было легко работать, а бэкапить с помощью robocopy/fsync. Но когда файлов стало больше, дала о себе знать проблема файловых систем — система начинает работать очень медленно, когда в одной папке много файлов. Эту проблему надо было как-то решать.
Очевидное решение — создавать подпапки. Раскладываем файлики по подпапкам, путь к файлам храним в поле БД.
Схема хорошая, но свои подводные камни в ней тоже есть. Один из них — организация процесса создания папок и распределения файлов по ним. В первой версии алгоритма мы создавали сразу X папок, дальше по кругу помещали в них файлы: первый файл помещаем в первую папку, второй — во вторую и так далее. Бег по кругу. Говорят, это полезно для здоровья... Почему создавали папки сразу? Потому что этот процесс тоже занимает время. Сначала надо проверить, есть такая папка или нет, и если нет, нам надо ее создать на уровне файловой системы.
Оказалось, что это — плохая идея, так как возникают сложности с бэкапом. Для того чтобы забэкапить такую структуру, нужно определить, какие файлы изменились, а для этого необходимо пробежать по всей структуре каталогов. Причем количество папок может быть довольно большим, так как если файлов у вас миллион, то разложив их в сто папок, мы получим по 10 000 файлов в каждой. А это — очень много. Поэтому внутри папок первого уровня надо создать еще X подпапок, чтобы обойти все эти лимиты (на платформе UnityBase есть построенные решения, в которых хранится порядка 70 млн файлов, а это — около 40 терабайт). Но тогда время бэкапа увеличивается до неприличия.
Папка последнего дня
В зависимости от аппетита заказчика, объема данных и количества пользователей можно применять разные стратегии создания папок. Наиболее эффективной оказалась та, которая получила внутреннее название «стратегия Daily». Ежедневно создается новая папка. В ней, при необходимости, можно создать ряд подпапок, но их будет не очень много. Даже если за день появится 100 000 файлов, достаточно будет создать всего 100 папок, чтобы в каждой из них было по 1000. Таким образом, можно легко бэкапить эту однодневную папку. Даже если возникнет необходимость бэкапить чаще, достаточно создавать резервную копию только одной этой папки, а все остальные уже не меняются, они в архиве.
У такой стратегии есть еще один бонус: можно легко переносить старые папки с быстрых дисков на медленные. Зачем это нужно? Все просто: SSD стоят дорого, а SATA хоть и медленнее, но стоят дешевле, и на них можно переместить старые файлы, которыми пользуются редко.
Профит
- Нам не нужно заранее создавать все папки. Мы просто можем каждую ночь, к примеру, в 12 часов, создать папку на следующий день и в течение дня с ней работать.
- Нам очень легко бэкапить.
- Все счастливы!
Отдача файлов
Отдавать файлы можно по-разному. В Linux есть возможность использовать функцию ядра sendfile. В Windows, на уровне API HTTP.SYS, можно отдать файл по файловому дескриптору. Можно самому читать файл и писать в сокет (кстати, при хранении файлов в БД это, наверное, единственный вариант). Вариантов много, и, казалось бы, можно с этим не париться. Но не все так просто. Клиент может поддерживать докачку файлов, сжатие, частичную загрузку и т. д., и все это имплементировать довольно сложно.
Поскольку в нашем случае отдача файлов идет по HTTP, есть и другой путь: поставить обратный прокси перед сервером приложений и использовать прекрасную фичу: X-Accel-Redirect (nginx)/x-sendfile(Apache). Схема становится простой и кросс-платформенной: клиент просит файл, сервер приложений проверяет права доступа (может быть сложная логика, может быть простой RLS), и если все ОК — отдает путь к файлу в специальном заголовке HTTP-ответа. Как NGINX/Apache отдаст файл — это уже не наша проблема: он его отдаст. Что это дает? То, что сервер приложений вообще не тратит ресурсы на передачу файла.
Данная схема позволяет обслуживать довольно много людей. На обычных внедрениях вряд ли возникнет вопрос производительности этой схемы — скорее, производительности сети. В практике InBase бывали случаи, когда
Облачные хранения. Хорошо, но не всегда
Конечно, можно пользоваться Amazon S3 (Azure, Google etc). Прекрасные сервисы! Но есть, опять-таки, нюансы, при которых хранить данные в облаке невозможно или не рационально:
- Информация может быть строго конфиденциальной, и хранить ее на сторонних серверах не хочется. Кроме того, теоретически (а при благоприятном стечении обстоятельств — и практически), с одной виртуальной машины можно залезть в другую виртуальную машину, которая находится на том же хосте, и вытащить данные.
- Существуют законы, запрещающие хранение данных за пределами страны. И есть организации, которые в принципе не хотят ходить в интернет, а их сеть полностью закрыта.
- Цена вопроса. Ценообразование облачных провайдеров зачастую очень сложное, зависит не только от объема и времени хранения информации, но и от того, сколько раз и в каком объеме ее прочитали/записали. Если железяка будет стоять у вас (или в дата-центре), очень вероятно, что это обойдется значительно дешевле, чем арендовать место для хранения в облаке. В долгосрочной перспективе может быть гораздо дешевле — десятки, сотни тысяч долларов, в зависимости от объема. Поэтому стратегия «сделай все сам и храни под подушкой» иногда имеет смысл.
- Можно развернуть S3 совместимое хранилище на собственных мощностях, но это потребует дополнительного администрирования и на малых/средних объемах не имеет особого смысла.
Сжатие, репликация и дедупликация
Почему это важно? Со сжатием понятно — сжимать средствами файловой системы не так уж накладно, но можно хорошо сэкономить на объеме дисков. С дедупликацией не совсем очевидно. Возьмем, к примеру, PDF. Жать его не очень эффективно. Но каждый PDF-A содержит в себе файл шрифта. Сам файл не сжимается, но когда файловая система поддерживает дедупликацию, все эти кусочки со шрифтом будут лежать на диске в одном экземпляре. И тогда каждый из этих PDF будет занимать на 20 Кб меньше. Так что если файловая система поддерживает дедупликацию, грех этим не воспользоваться. При этом мы практически не теряем в производительности.
Что касается репликации, то очень многие файловые системы ее поддерживают, и в некоторых случаях мы можем вообще не париться с бэкапами, а просто использовать, к примеру, Btrfs/CephFS/NTFS etc — все они поддерживают возможность сразу перебросить только что созданный файл на удаленный хост и сделать там его копию. Профит: у нас получится автоматический фейловер. Бэкапить все равно нужно. Если придет какой-то Petya и зашифрует файлы на мастере, они успешно реплицируются на подчиненный узел, и тогда спасет только бэкап. Но можно бэкапить реже. Ну и да — CephFS. к примеру, рассчитана на петабайтные хранилища.
В сухом остатке
Использование файловой системы для хранения неструктурированной информации вместо BLOBов базы данных дает нам множество преимуществ. Особенно в случае, когда количество файлов постоянно растет. Но если вдруг нам захочется использовать, например, Amazon S3, существуют решения в виде прокси, которые преобразуют вызовы к файловой системе в вызовы API S3. Можно поставить такой прокси и, фактически, работать со своей файловой системой, а с другой стороны, это будет S3 хранилище.
Остается только пожелать всем терабайты добра и безоблачного хранения (хоть это и звучит немного двусмысленно).