Как решить проблемы при хранении загруженных файлов в файловой системе веб-приложения?
Я создаю веб-приложение, в котором пользователи могут создавать отчеты, а затем загружать изображения для созданных отчетов. Эти изображения будут отображаться в браузере, когда пользователь нажимает кнопку на странице отчета. Изображения являются конфиденциальными, и только авторизованные пользователи смогут получить к ним доступ.
Я знаю плюсы и минусы хранения изображений в базе данных, в файловой системе или в таких сервисах, как amazon S3. Для моего приложения я склонен хранить изображения в файловой системе, а пути к изображениям - в базе данных. Это означает, что мне приходится иметь дело с проблемами, возникающими при управлении распределенными транзакциями. Мне нужен совет, как справиться с этими проблемами.
1- Я считаю, что одним из правильных решений является использование таких технологий, как JTA и XADisk. Я не очень разбираюсь в этих технологиях, но считаю, что двухфазная фиксация - это то, как достигается автоматичность. Я использую MySQL в качестве базы данных, и похоже, что MySQL поддерживает двухфазную фиксацию. Проблема с этим подходом заключается в том, что XADisk не выглядит активным проектом, по нему не так много документации, и есть факт, что я не очень разбираюсь в тонкостях этого подхода. Я не уверен, стоит ли мне инвестировать в этот подход.
2- Я считаю, что могу решить некоторые проблемы, возникающие из-за нарушения свойств ACID для моего приложения. При загрузке изображений я могу сначала записать файлы на диск, если эта операция завершится успешно, я могу обновить пути в базе данных. Если транзакция базы данных не удалась, я могу удалить файлы с диска. Я знаю, что это все еще не пуленепробиваемое; нехватка электроэнергии может возникнуть сразу после транзакции db, или диск может не реагировать на какое-то время и т. д. Я знаю, что есть также проблемы с параллелизмом, например, если один пользователь пытается изменить загруженное изображение, а другой пытается удалить его в В то же время возникнут некоторые проблемы. Тем не менее, шансы на одновременное обновление моего приложения будут относительно низкими.
Я считаю, что могу жить с потерянными файлами на диске или путями к потерянным образам в базе данных, если возникают такие исключительные случаи. Если путь к файлу существует в базе данных, а не в файловой системе, я могу показать пользователю уведомление на странице отчета, и он может попытаться повторно загрузить изображение. Сиротские файлы в файловой системе не будут большой проблемой, я мог бы время от времени запускать процесс для обнаружения таких файлов. Тем не менее, мне не очень нравится такой подход.
3- Последний вариант может заключаться в том, чтобы вообще не сохранять пути к файлам в базе данных. Я могу структурировать файловую систему так, чтобы я мог вывести путь к файлу в коде и загрузить все изображения сразу. Например, я могу создать папку с именем идентификатора отчета для каждого отчета. Когда был сделан запрос на загрузку изображений отчета, я могу загрузить изображения сразу, так как я знаю идентификатор отчета. Это может привести к огромному количеству папок в файловой системе, и я не уверен, приемлем ли такой дизайн. В этой схеме все еще будут существовать проблемы с параллелизмом.
Я был бы признателен за совет, какой подход мне следует использовать.
1 ответ
Я считаю, что вы пытаетесь быть сверхкорректным, и, возможно, этого не так уж много, но я также столкнулся с подобной ситуацией некоторое время назад и изучал также различные возможности. Мне не нравились варианты, соответствующие вашему варианту 1, но в отношении 2 и 3 у меня были разные успешные подходы.
Подведем итог по списку проблем:
- Вы хотите, чтобы файл был сохранен
- Вы хотите, чтобы путь к файлу был связан с соответствующей сущностью (например, с отчетом)
- Вы не хотите, чтобы путь к файлу был связан с файлом, который не существует
- Вы не хотите, чтобы файлы в файловой системе не были связаны ни с одним отчетом
И разные подходы:
1. Использование БД
Вы можете гарантировать транзакции в БД практически с любой реляционной базой данных, и с S3
вы можете обеспечить согласованность чтения после записи как для новых объектов, так и для загрузки новых объектов. если тыPUT
объект, и вы получите 200 OK
, он будет читаемым. Как все это собрать воедино? Вам нужно следить за процессом. Я могу придумать 2 способа:
1.1 С таблицей прогресса
- Запрос на загрузку сохраняется в таблице со всем необходимым, чтобы идентифицировать этот файл, идентификатор отчета, путь к временному загруженному файлу, путь назначения и столбец статуса.
- Вы сохраняете файл
- В случае отказа файлового сейфа вы можете обновить запись в таблице или удалить ее.
- Если сохранение файла прошло успешно, в
transaction
:- обновить таблицу прогресса с успешным статусом
- обновите таблицу, в которой вы фактически сохраняете изображение отношения отчета
- Имейте cron, но не проверяйте файловую систему, а проверяйте таблицу процессов. Если в файловой системе есть какой-либо файл, который является сиротским, определенно он был добавлен в таблицу (это был пункт 1). Здесь вы можете решить, будете ли вы удалять файл, или, если у вас достаточно информации, вы можете продолжить прерванный процесс, запустив точку 4.
Та же таблица отношений отчета и изображения с некоторыми дополнительными столбцами статуса.
1.2 С системой очередей
Как RabbitMQ, SQS, AMQ и т. Д.
Очень похожий подход может быть реализован с любой системой очередей вместо таблицы db. Я не буду вдаваться в подробности, потому что это больше зависит от вашей реальной инфраструктуры, а только от общей идеи.
- Запрос на загрузку попадает в очередь, вы отправляете сообщение со всем, что вам может понадобиться для идентификации этого файла, идентификатора отчета и, если вам нужен предварительный окончательный путь.
- Вы загружаете файл
- Рабочий читает ожидающие сообщения в очереди и выполняет свою работу. Сообщение помечается как использованное только тогда, когда все идет хорошо.
- Если что-то не получается, сообщение, естественно, вернется в очередь.
- В следующий раз, когда сообщение будет прочитано, работник может получить достаточно информации, чтобы увидеть, есть ли работа для возобновления, или даже файл, который нужно удалить, если возобновление невозможно.
В обоих случаях проблемами параллелизма не так просто управлять, но ими можно управлять (полагаясь на блокировки БД в первом случае и очереди FIFO во втором случае), но всегда с некоторой логикой приложения.
2. Без БД
В некоторой степени система без базы данных была бы вполне приемлемой, если бы мы могли защитить ее как правильное соглашение по дизайну конфигурации. Вам предстоит иметь дело с 3 вещами:
- Сохранить файлы
- Читать файлы
- Убедитесь, что структура файловой системы управляема
Начнем с 3:
Структура папки
В общем что-то вроде одной папки для
report id
будет слишком простым и, возможно, сложным в обслуживании, а также в конечном итоге слишком простым. Это вызовет проблемы, потому что если у нас есть папкаimages
с одной папкой на отчет, и завтра у вас будет меньше 200 тысяч отчетов,images
в папке будет 200 тыс. элементов, и дажеls
займет слишком много времени, как и для любого языка программирования, пытающегося получить доступ. Это убьет тебяВы можете подумать о чем-нибудь более сложном. Лично мне нравится способ, которым я научился у
Magento 1
более 10 лет назад, и с тех пор я многое использовал: использовал структуру папок, которая сначала следовала внешним правилам, но была расширена за счет правил, производных от самого имени файла.- Мы хотим сохранить изображение продукта. Имя изображения:
myproduct.jpg
- первое правило: для изображений товаров я использую
/media/catalog/product
- затем, чтобы избежать множества изображений в одном, я создаю одну папку для каждой буквы имени изображения, вплоть до некоторого количества букв. Допустим, 3. Итак, моя последняя папка будет примерно такой:
/media/catalog/product/m/y/p/myproduct.jpg
- вот так понятно, куда сохранять любое новое изображение. Вы можете сделать что-то подобное, используя свой идентификатор отчета, категории или что-то еще, что вам подходит. Конечная цель - избежать слишком плоской структуры и создать дерево, которое имеет смысл для вас и которое можно легко автоматизировать.
- Мы хотим сохранить изображение продукта. Имя изображения:
И это подводит нас к следующей части:
Чтение и запись.
Раньше я довольно успешно реализовал подобную систему. Это позволило мне легко сохранять файлы и легко извлекать их из мест, которые были чисто динамическими. Части здесь были:
- S3 (но можно использовать любую файловую систему)
- Небольшой микросервис, действующий как прокси для чтения и записи.
- Некоторая система пространств имен и присоединенная логика.
Логика довольно проста. Пространство имен позволяет мне узнать, где будет сохранен файл. Например, пространство имен может бытьcompanyname/reports/images
.
Скажем, разработаем микросервис для чтения и записи:
Для сохранения файла он получает:
- пространство имен
- идентификатор объекта (т.е. вы сообщаете)
- файл для загрузки
И подойдет:
- на основе правил, которые у меня есть для этого пространства имен, а идентификатор и имя файла сохранят файл в этой папке
- он не возвращает физическое местоположение. Это остается неизвестным клиенту.
Затем для чтения клиенты будут использовать URL-адрес, который также использует соглашение. Например, у вас может быть что-то вроде
https://myservice.com/{NAMESPACE}/{entity_id}
И, исходя из логики, микросервис будет знать, где его найти в хранилище, и вернет изображение.
Если у вас есть более одного изображения в отчете, вы можете делать разные вещи, например:
- вам может понадобиться добавить в путь третий ярлык, например https://myservice.com/{NAMESPACE}/{entity_id}/1
https://myservice.com/{NAMESPACE}/{entity_id}/2
и т.д...
- если это для вашего внутреннего использования приложения, у вас может быть одна конечная точка, которая возвращает список всех подходящих изображений, скажем, https://myservice.com/{NAMESPACE}/{entity_id}
возвращает массив со всеми URL-адресами изображений
Я реализовал это с помощью довольно простой конфигурации yml для определения логики и очень простого кода, читающего эту конфигурацию. Это позволило мне проявить большую гибкость. Например, сохранять отчеты с разными путями, серверами или сегментами s3, если они принадлежат разным компаниям или относятся к разным типам отчетов.