Хранение, обработка и отдача статики

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

Задачи решаемые системой:
— удобная загрузка файлов;
— возможность асинхронной обработки картинок;
— легкая работа с превью;
— отделение конфигурирования от выполнения.

Поехали



Для начала создадим в базе таблицу File со следующими полями: id, name, size, width, height, is_deleted, is_ready, updated, created.

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

$number = sprintf('%08d', $id);
$uri = '/' .
substr($number, 0, 2) . '/' .
substr($number, 2, 2) . '/' .
substr($number, 4, 2) . '/' .
$number;


На выходе получим примерно такое: /00/47/31/00473161

upd1 Arkadiy_Kulev указал на то что глубину каталогов лучше ограничить двумя уровнями.

upd2 Действительно, правильнее составлять путь в обратном порядке для лучшего распределения картинок по папкам. Тоесть урл получим такой /161/473/00473161. Спасибо atd

Для чего это делается?
  • file_id будет полем в таблицах сущностей которым нужны картинки ($user->avatar_file_id). Такой подход позволяет на основе одного file_id определить полный путь до картинки без дополнительных запросов к базе.
  • Мы получаем унифицированный способ работы со всеми картинками, как пользовательскими, так и загружаемыми операторами через админку.
  • На диске создается структура папок таким образом что в одной папке не будет больше 100 подпапок или файлов. Хотя это число можно увеличить до 1000 папок, но при большем количестве могут возникнуть трудности в работе с ФС.
  • Таблица file помогает при асинхронной работе с файлами. Если нам вдруг понадобится провести конвертацию всех картинок в другой формат или провести любые другие манипуляции, то мы можем воспользоваться полем is_ready (или добавить свое поле) для определения картинок которые мы уже обработали и которые осталось обработать.
  • Благодаря полю is_deleted удалением картинок можно заниматься асинхронно. Код должен будет просто поставить true (удобнее реализовать на триггерах), а удалять будет специальный сборщик (его конечно нужно написать). Это можно реализовать на триггерах.


Как видно из примера «00/47/31/00473161», количество возможных загрузок картинок, при таком подходе, ограничено миллиардом. Естественно это несложно изменить на этапе внедрения.

Для реализации описанного выше функционала нужно создать класс Image_Manager с методом receive. Этот класс как раз и будет заниматься созданием записи в базе и перемещением файла в соответствующую директорию в ФС, а так же создавать отсутствующие директории.

...
if ($form->isValid()) {
// Название поля в форме не важно, Image_Manager просто берет из массива
// $_FILE один элемент считая что это он и есть.
...
$im = new Image_Manager($options); // Лучше оформит в виде ресурса
// 'avatar' это название секции конфига, о котором чуть ниже
$file_id = $im->receive('avatar'); // Может выкидывать исключения
$user->avatar_file_id = $file_id;
$user->save()
}


Мы сохранили исходную картинку и пользователю передали ее id. Но ведь ее еще нужно обработать, а возможно создать несколько размеров превью для показа на сайте. Дальше можно пойти несколькими путями, создавать картинки сразу или использовать lazy load. Второй способ тоже имеет несколько вариантов развития и выходит за рамки данной статьи (В конце статьи указан список ссылок, где приводится пример возможной реализации данного способа). Мы пойдем первым путем. И именно для этого в конструктор image_manager передается массив $options, а методу recive строка «avatar».

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

...
[avatar] ;Названия для превью big, medium и small выбраны произвольно
resize.big.OutputFileFormat = jpg
resize.big.keepFrame = true
resize.big.backgroundColor = 240.240.240
resize.big.width = 236
resize.big.height = 177
resize.medium.keepFrame = true
resize.medium.backgroundColor = 240.240.240
resize.medium.width = 144
resize.medium.height = 108
resize.small.keepFrame = true
resize.small.width = 72
resize.small.height = 54
resize.small.roundCorners = false

[user_album_photo]
...


Именно этот конфигурационный файл передается в конструктор Image_Manager. Вызывая метод recive, мы указываем секцию конфигурационного файла и фактически определяем как будет обрабатываться эта картинка и сколько будет создано превью. Для обработки картинок желательно иметь отдельную библиотеку с которой взаимодействует Image_Manager внутри себя.

Для сохранения превью используется тот же самый формат хранения исходного файла «00/47/31/00473161», но в конце добавляется специальный хеш, вычисляемый на основе параметров данного превью, в итоге путь будет примерно таким "/00/47/31/00473161X2280688952.jpg" (опять же это всего лишь пример, можно сделать по другому). Этот хеш помогает определить существование превью для картинок у которых изменились параметры и в случае отсутствия сгенерировать картинку по запросу.

Осталось разобраться с выводом. Самым простым вариантом будет написать специальный хелпер image, который будет работать подобно url хелперу:

// Возвращает url картинки
$this->image($user->avatar_file_id, 'small', 'avatar')


Хелпер image обращается к объекту (возможно Image_Manager), который знает как построить путь до текущей картинки с учетом ее параметров, секции и названия превью. Если картинки находятся на отдельном хосте, то подставляет и его. В случае отсутствия необходимой превью, хелпер может его генерировать (спорный момент, при большом количестве отсутствующих картинок может закончится память или таймаут) либо отдавать путь на специальную заглушку для этого типа файлов.

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

Ссылки по теме:
http://habrahabr.ru/blogs/nginx/77873/ — здесь очень ценные комментарии
http://habrahabr.ru/blogs/nginx/94435/
http://ru.wikipedia.org/wiki/WebDAV — при выносе статики на отдельный сервер(а)
+29
13 августа 2010, 12:11
141
toxicmt 32,9

комментарии (48)

+2
toxicmt #
Если топик понравится, перенесу в коллективный блог.
НЛО прилетело и опубликовало эту надпись здесь
0
toxicmt #
Спасибо за совет, учту.
0
brain2008 #
Соглашусь, 1-2 уровня самый раз
+1
atd #
а ещё желательно отрезать цифры не сначала, а с конца, типа /161/473/00473161
прирост будет небольшим, но будет.
0
saterenko #
Просто так распределение будет лучше, а не все картинки в папке /00/00/, потому как картинок всего несколько тысяч.
0
atd #
и ещё будет меньше время открытия файла, т.к. разница в имени файла наступит быстрее.
0
toxicmt #
Спасибо, добавлю в топик.
0
kaiten #
спасибо за статью, сейчас тем же самым занимаемся на работе, единый сервер хранения статических файлов, статья оказалась полезной.
0
toxicmt #
Именно для этого я ее и писал). Скажу только то что прямо сейчас у нас встала задача вынесения статики на отдельные сервера и пока я не знаю насколько этот подход масштабируем.
0
kaiten #
у нас план следующий, есть порядка 15 серверов, и 2 сервера для хранения статики реплицированных, будет единое api для загрузки файлов под множество проектов, файлы складываются на файлове хранилище по принципу имя проекта и год месяц день… так же заносятся данные в бд для отслеживания где что…

То что вы описали по моему мнению правильный подход, и очень расширяемый, вплане добавить дополнительный возможности по ресайзу объектов, распаковок, ретуши и прочих манипуляций безгранично…
+2
brain2008 #
Я храню картинки на отдельном сервере и домене.
Получаю хеш картинки и создаю папку с первыми двумя символами
Пример:
450x337
8f
8f30de72de3a710dc11c1b22caa5ede4.jpeg

/450x337/8f/8f30de72de3a710dc11c1b22caa5ede4.jpeg
0
LuciferOverLondon #
Хэш картинки это же куча времени на подсчет, при этом пользы от него никакой, не?
0
banzalik #
польза в отсутсвии дублированных картинок
0
AmirL #
А коллизии? При этом id файла как раз таки 100% уникальный.
0
toxicmt #
Сомневаюсь что это можно назвать пользой. Если картинки грузят разные пользователи то у них и должны быть разные картинки (даже если они идентичны). Потому что один может свою удалить, а другой нет.
0
zooh #
Можно добавить счетчик ссылок, хотя в целом согласен, что в большинстве случаев это экономия на спичках.
0
kovyrlo #
Куча времени тратится только при загрузке файлов, потом хэш уже не генерируется, а берется из базы.
+1
LuciferOverLondon #
Это понятно, но смысла тратить процессорное время на такую операцию особого не вижу. Дублированные картинки — наверное, актуальны только для очень популярных хостингов, а для них время ещё дороже.

Хотя при масштабах в десятки серверов, наверное, уже и не особо важно, всё равно окупается.
0
VDG #
md5($now_time. $rnd. $file_size)
0
IDMan #
А зачем создавать папку? Почему нельзя складывать всё в одну? Это ограничение ФС?
0
Stalker_RED #
да.
не то чтобы ограничение, но будет тормозить.
0
akalend #
вместо хеша использую id
а для разных размеров расширенное имя:
012345.jpg — оригинал — возможно хранится в другой папке
012345_320x240.jpg
012345_640x480.jpg
+1
aleXoid #
Как раз недавно стояла задача со статикой. Все картинки хранятся в директориях со структурой:
images/Y/m/d/картинка — в оригинальном размере. При запросе прямо в запросе указываю какой метод резайза применить и несколько субдоменов (по первым символам хеша картинки), например p1.example.com/resize/250x250/картинка. Ресайзер хранит кеш в отдельно смонированном разделе на tmpfs в оперативной памяти (у нас кеш порядка 150Мб) также в структуре по первым символам кеша e/8/кеш имени картинки.

С JS и CSS там отдельная история.

Чисто клиентской оптимизацией удалось достигнуть ускорение при холодной загрузке в 2 раза, при горячей почти в 5.
0
toxicmt #
Мы хотим попробовать похожую схему.
Стораджи на которых хранится статика. Перед ними фронтенд с ресайзером, который еще и кеширует превью.
Код по webdav будет закачивать файлы на стораджи, и генерировать урлы которые указывают на фронтенд. При запросе картинки с фронтендов nginx в зависимости от того нашел ли файл по указанному пути будет его либо отдавать сразу либо передавать скрипту ресайзеру (ресайзер nginx не может делать то что нам нужно: прозрачность и углы, поэтому его использовать не будем).
0
Butylski #
0
toxicmt #
Выше я про это написал.

Кстати, существует ли нормальная библиотека для работы с webdav? То что я нашел давно не обновлялось и прямо сейчас стоит задача допилить одну из существующих либ до рабочего состояния, чтобы вынести статику на отдельные сервера.
0
Butylski #
да уж пару лет ничего не менялось
0
liaren #
А ещё для хранения/ресайза/расшаривания картинок есть вот такая библиотечка:
Primage — PHP library that works like a proxy for realtime images resizing and watermarking (+ cache support)
0
unconscience #
Всё уже написано до нас ) А вообще интересно. что правильнее — сохранять сначала в БД, а потом в писать файл или наоборот? В случае, когда запись файла в конце, можно получить лишнюю запись в бд, если запись файла выдаст ошибку. Потом ещё надо будет регулярно чистить базу на тему мертвых записей в БД.
0
toxicmt #
Правильно сначала писать в базу, потому что если потом обломается загрузка картинки то выполнится rolback транзакции и в базе мусора не окажется. Базу можно и не чистить, вряд ли когда-нибудь это станет узким местом. Если уж так хочется почистить то нужно пользоваться полем is_deleted, так как написано в статье.
0
unconscience #
ролбэк — это время. При больших потоках, может быть и большое время. Надо считать.
0
toxicmt #
А как вы представляете вставку данных в базу без транзакций? Надеяться на лучшее?
0
kirilloid #
mysql_last_insert_id
по нему делать «откат» вручную на похапе. Я так сделал.
0
akalend #
правильнее, если это хайлоад:
картинку класть сразу на тот сервер (или два) откуда будет произведена отдача, далее в очередь поместить информацию о новом контенте.
постоянно мониторить очередь, например раз в сек и при наличие в ней элементов — запустить скрипт обработки ( ресайз + запись в БД) и делать это на отдельном сервере (как правило на том — который отдает статику)

В качестве сервера очередей у нас используется memcacheq

сылка по теме www.grid.net.ru/nginx/upload.ru.html
php-webdav.pureftpd.org/project/php-webdav
НЛО прилетело и опубликовало эту надпись здесь
+1
toxicmt #
Конечно, надрать задницу интелу святое дело. :D
0
andorro #
Интел в понедельник, завтра на своих тренируемся.
+1
toxicmt #
Ага. Но это не отменяет написанного).
0
Morro #
Решил задачу почти аналогичным методом, только для храненния имя файла генерил хеш на основе его названия и времени загрузки.
+1
Tbird #
Вы хотя бы ставьте тег PHP
0
toxicmt #
Поставил.
–5
muhanov #
О плюсах и минусах данного подхода лишь упоминается, что они есть. А где описание?

Минусы:
— любая ошибка ведет к появлению мусора как на файловой системе так и в базе данных.
— файл на файловой системе не защищен от удаления «руками» или другим приложением
— сложная функциональность для осуществления rollback-a
— нужна переделка при переносе решения с win на lin и обратно (работа с файлами отличается, темповый каталог тоже, права доступа тоже)

Плюсы:
— можно указать прямую ссылку на файл с файловой системы
+6
toxicmt #
Иногда лучше жевать
0
IDMan #
Не совсем понял с флагом «is_delete». Если мы хотим удалить картинку, мы ставим его в true. Сборщик посмотрит базу, удалить нашу картинку — а далее что? Снова поставив флаг в фальш, чтобы потом еще раз не пытаться удалять несуществующую уже картинку?
0
toxicmt #
Сборщик удаляет картинку из файловой системы и из таблицы. Больше этой записи существовать не будет.
0
toxicmt #
Насколько я знаю, в крупных проектах картинки никогда не удаляются. Есть мнение что это вызывает ненужную фрагментацию, да и просто не имеет смысла. Экономия копеечная, потому что количество удаляемых фотографий пренебрежимо мало по сравнению с добавляемыми фотографиями.
+1
vitalyk #
с выходом амазон S3 я перестал парится со статикой. все картинки и видео скидываются туда. под рельсы есть пара очень удобных плагинов которые решают вопрос с ресайзом под нужный размер е т.д. дополнительный плюс: можно очень просто и быстро порейти на cloudfront cdn.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.