Punch[ICYMI] FULL LIST: Names of 68 new SANs sworn in by CJNThe Jerusalem PostPalestinian West Bank resident arrested for filming Bnei Brak light rail, posing as IsraeliInquirer EntertainmentSelma Blair arrested in New York after alleged assault on flightRTP DesportoPortugal gere vantagem confortável com segundo play-off para Mundial feminino na miraBollywood HungamaRahul Vaidya passes beeped remark about Disha Parmar on Pati Patni Aur Panga 2; leaves contestants shocked as fans call him out on RedditInquirerWATCH: Sison calls withdrawal of 3 impeachment articles ‘ridiculous’ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioХабрЗапланированное устаревание в электронике: наушники Sony WH-1000XM5 и попытка их ремонтаCapital FMKalonzo to outline opposition’s policy agenda at Chatham HouseCNN TürkRaporlu ilaçlarda yeni dönem: Reçete kuyrukları tarihe mi karışıyor?South China Morning PostChinese billionaire featured in ‘American Factory’ shares lesson for firms going globalRapplerFACT CHECK: Trillanes not cited in contempt by impeachment court
The Daily Newsstand · Free, Always
Tuesday, October 13, 2026

Как я делаю защищенное файловое хранилище — 3: PostgreSQL против MongoDB

Translate

Часть 1: Рождение идеи
Часть 2: Ограничения и возможности
Часть 3: PostgreSQL против MongoDB (вы здесь)

1. Норк-Мараш

Первое лето я провел на склоне горы. Мой крошечный домик был втиснут между сотен таких же — с каменными фасадами и выгоревшими на солнце крышами. Район назывался Норк-Мараш.

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

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

Ближайшие магазины находились примерно в километре — либо выше, либо ниже по серпантину. Поэтому за продуктами приходилось либо спускаться вниз, а потом нести покупки обратно вверх, либо сначала подниматься вверх, чтобы затем спуститься с покупками вниз. Обычно я выбирал второй вариант.

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

Как потом выяснилось, это было совершенно обычным делом. В плохую погоду водители нередко останавливались возле автобусных остановок и сообщали, в какую сторону едут. Кому было по пути, просто подвозили.

От моей двери открывался отличный вид на Ереван. По вечерам город внизу постепенно загорался огнями. Можно было до самого утра сидеть с ноутбуком и чашкой кофе на ступеньках возле дома.

2. А нужна ли вообще база данных?

Какое-то время я прикидывал, можно ли обойтись вообще без базы данных. Сервис-то предстояло делать файловый. Чем меньше компонентов, тем проще архитектура. Чем проще архитектура, тем надежнее система.

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

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

К сожалению, права файловой системы здесь не помогали: процесс приложения имел бы доступ ко всем конфигурациям, а разграничение ролей между пользователями все равно оставалось бы частью логики приложения. Вполне решаемо. Тем более, что ожидаемое количество пользователей одного экземпляра приложения должно было измеряться единицами, в крайнем случае — десятками.

Гораздо хуже обстояли дела с метаданными файлов. Предполагалась возможность быстро искать файлы не только по названию, но и по различным атрибутам. Я хотел добавить быстрый поиск, метки, комментарии, сортировку и пагинацию. Это тоже было решаемо. Оставалось понять, какой ценой.

Способов хранить метаданные средствами файловой системы я видел два.

3. xattrs

Первый способ — расширенные атрибуты файлов — xattrs. Файловая система позволяет прикреплять к файлу небольшие пары «ключ — значение», которые не входят в его содержимое и не видны в обычном списке каталога. Условно:

report.pdf
 ├── user_id = 7
 └── starred = true

В Linux это можно представить так:

setfattr -n user_id -v 7 report.pdf
setfattr -n starred -v true report.pdf
getfattr -d report.pdf

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

Во-первых, xattrs плохо подходят для объемных данных. Небольшой идентификатор или флаг хранить удобно, а более сложные структуры — например, список меток — уже неудобно. Можно положить туда JSON, но тогда атрибут начинает играть роль миниатюрного файла, который приходится полностью читать и перезаписывать.

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

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

В-четвертых, связи между объектами остаются на совести приложения. Например, атрибут user_id=7 никак не гарантирует, что пользователь с идентификатором 7 существует.

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

Кроме того, такой подход выглядел довольно хрупко и явно плохо масштабировался. Мне стало интересно, во что может превратиться поиск по файловой системе при большом количестве файлов.

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

Для каждого файла потребуется по меньшей мере две операции: получение информации (stat) и чтение расширенных атрибутов (getxattr). Для упрощения предположим, что их суммарное выполнение вместе с накладными расходами занимает около 1 мс на HDD и около 0,1 мс на SSD/NVMe (ориентировочные значения для оценки порядка величин). В результате получаем:

Файлов

HDD

SSD/NVMe

1

1 мс

0,1 мс

1 000

1 с

100 мс

10 000

10 с

1 с

100 000

100 с

10 с

1 000 000

~17 мин

~1,7 мин

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

Можно грубо прикинуть и этот ресурс. Допустим, информация о каждом файле будет представлена в памяти Python 3.10+ такой структурой:

Атрибут

Размер

Память

path

строка, ~40 символов

~80 байт

size

целое число

28 байт

mtime

число с плавающей точкой

24 байт

type

строка, ~15–20 символов

60–70 байт

xattrs

словарь

200-300 байт

dict

словарь (внешний контейнер)

~270 байт

итого

~700 байт

Тогда на список из миллиона файлов потребуется около 700 Мб оперативной памяти. Это без учета самого списка и временных структур, создаваемых во время сортировки. Кроме того, фактический размер будет зависеть от длины путей и содержимого xattrs, поэтому реальное потребление может оказаться заметно выше.

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

Очевидно, что такой поиск непригоден для предполагаемого контекста. Я перешел к следующему варианту.

4. Sidecar-файлы

Второй способ — рядом с каждым настоящим файлом хранить дополнительный файл с метаданными:

report.pdf
report.pdf.json

Примерно с таким содержимым:

{
  "user_id": 17,
  "is_starred": true
}

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

Проблема в том, что этот подход удваивает количество файловых операций по сравнению с предыдущим.

Но и это еще не все. Средствами приложения придется реализовывать атомарность операций, блокировки, транзакции, уникальные идентификаторы, связи между объектами, проверку целостности, восстановление после сбоев и очистку мусора.

Стало понятно, что обойтись без базы данных если и получится, то придется самостоятельно реализовать довольно большой объем дополнительной логики. Что ж, я перешел к выбору базы данных.

5. Слон против дельфина

Экспериментировать с экзотическими решениями мне не хотелось. Нужна была обычная реляционная база данных. Надежная и предсказуемая. Традиционно я выбирал между PostgreSQL и MySQL.

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

PostgreSQL распространялся под разрешительной PostgreSQL License. Она почти не накладывала ограничений на использование, изменение и распространение.

С MySQL всё выглядело менее однозначно. После перехода проекта под контроль Oracle я не был уверен, как именно лицензирование могло повлиять на мое приложение. Разбираться в юридических нюансах мне совершенно не хотелось.

Поэтому я выбрал PostgreSQL.

6. Немного алхимии

Дальше всё было довольно скучно и стандартно. Работать с сырыми SQL-запросами я не собирался, поэтому сразу добавил в стек SQLAlchemy и asyncpg, а позже к ним присоединился и Alembic.

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

Сначала нужно было решить, как мне больше нравится работать с данными. Выбор между Active Record и Data Mapper оказался простым: я хотел, чтобы модели описывали только данные, а вся работа с базой была сосредоточена в одном месте. Значит, Data Mapper.

Следующий вопрос: кто именно будет этим заниматься? Можно было обращаться к SQLAlchemy Session напрямую из слоя бизнес-логики, а можно было спрятать её за репозиториями. Второй вариант был заметно аккуратнее.

Более того, вместо отдельных репозиториев для каждой модели имело смысл реализовать один универсальный Generic Repository. Пусть он инкапсулирует стандартные CRUD-операции. Тогда бизнес-логика останется отделенной от слоя доступа к данным, а дублирование кода будет сведено к минимуму.

Получалась аккуратная структура с понятными границами ответственности внутри прикладной части приложения (без учета ASGI, маршрутизации и валидации):

│               │               │               │               │
│ Слой          │ Слой доступа  │ Слой          │ Слой хранения │
│ бизнес-логики │ к данным      │ моделей       │ данных        │
│               │               │               │               │
│ Сервисы       │ Generic       │ SQLAlchemy    │ PostgreSQL    │
│ FastAPI       │ Repository    │ Models        │ Database      │
│               │               │               │               │

Каждый слой отвечал только за свою задачу. Бизнес-логика ничего не знала об SQL. Репозиторий ничего не знал о предметной области. ORM-модели ничего не знали о правилах приложения. Слой хранения не знал вообще ничего, он просто хранил данные.

Я набросал простую связку Dockerfile + Makefile и собрал минимальное приложение на FastAPI. Убедившись, что всё выглядит рабочим, взялся за Generic Repository. И спустя некоторое время он был готов.

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

Тогда мне казалось, что вопрос доступа к базе данных закрыт навсегда. Спустя неделю этот вопрос открылся снова.

7. MongoDB входит в чат

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

Любая документоориентированная база данных выглядела гораздо привлекательнее. Всего две коллекции — пользователи и файлы. Никаких связей. Красота.

Мысль настолько мне понравилась, что я решил реализовать два репозитория. Один — с PostgreSQL, второй — с MongoDB. Потом можно будет сравнить, что подходит больше. Спешить мне было некуда.

Кое-какой опыт работы с MongoDB у меня был, так что больших проблем я не предвидел. Но маленькие все равно меня нашли.

Во-первых, я постоянно ловил себя на том, что мыслю категориями реляционной базы данных. Вместо того, чтобы использовать сильные стороны документоориентированной модели, я снова и снова пытался разложить всё по привычным таблицам и связям. Конечно, это не недостаток MongoDB, просто я привык к SQL. Пришлось потратить некоторое время, чтобы перестроиться.

Во-вторых, MongoDB по умолчанию не навязывает строгую схему документов. Я знал об этом заранее и рассчитывал решить вопрос по ходу работы. На практике это быстро превратилось в дополнительную ответственность, которую пришлось брать на себя приложению. Появились дополнительные механизмы валидации, которые увеличивали объём кода. Мне это не нравилось.

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

Когда новый репозиторий был полностью готов, я реализовал два сервиса с одинаковой логикой, которые могли работать с PostgreSQL и MongoDB. Оба сервиса выполняли одни и те же операции над одними и теми же данными. Теперь их можно было сравнить.

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

Так что, несмотря на первоначальный энтузиазм, я всё же вернулся к PostgreSQL. Код получался проще, модель данных — понятнее, большинство задач решалось без дополнительных компромиссов.

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

8. Конец третьей части

Дальше можно было переходить к вопросу хранения самих пользовательских файлов.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.