Управление картинками и другим бинарным содержимым вашего веб-проекта

    image
    Мы, в компании XIAG, в разных проектах постоянно решаем одну и ту же задачу: как хранить и показывать бинарные пользовательские данные. Это могут быть логотипы компаний, PDF файлы с описанием вакансий или приветственное видео. Причем это содержание нужно показывать на страницах сайта самым разнообразным способом: лого должно быть нужного размера, PDF — в виде маленькой превью-картинки, а из видео нужно показать пару стоп-кадров.

    Уверен, такая задача знакома всем веб-разработчикам. В этой статье мы хотим поделиться нашим решением проблемы, опубликованным под открытой лицензией на ГитХабе.

    Познакомьтесь с веб-сервисом Barberry (https://github.com/Magomogo/Barberry), который мы успешно используем уже около года. Суть сервиса в том, что он хранит оригиналы загруженных документов и способен по-разному отдавать их, конвертируя на лету.

    Веб-сервис с REST интерфейсом


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

    Barberry размещается на сервере отдельно и взаимодействует с миром по HTTP. Чтобы попробовать сервис в работе, необходимо инсталлировать https://github.com/Magomogo/barberry-service на ваш сервер. Мы уже сделали это у себя, демонстрацию работы сервиса можно увидеть по адресу http://barberry.xiag.ch

    Загрузка

    Для того, чтобы загрузить данные в Barberry, необходимо сделать POST запрос. Сервис определит Content-Type и сообщит уникальный идентификатор сохраненного документа.

    Пример загрузки фотографии DSC01823.JPG, например в консоли командой curl:

    $ curl -X POST -F file=@/tmp/DSC01823.JPG barberry.xiag.ch
    


    Запрос:
    POST / HTTP/1.1
    -- данные --
    
    Ответ:
    HTTP/1.1 201 Created
    
    {"id":"yPkjPk","contentType":"image/jpeg","ext":"jpg","length":218038,"filename":"DSC01823.JPG"}
    


    JSON ответ сервера описывает полученный документ. Дальнейшее обращение к нему будет происходить по уникальному “id”:”yPkjPk”.

    Выгрузка

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

    Запрос:
    GET /yPkjPk.jpg HTTP/1.1
    Ответ:
    HTTP/1.1 200 OK
    Content-Type: image/jpeg


    В окне браузера появится изображение. Ничего особенного. Вся мощь Barberry раскрывается далее.

    Конвертация на лету и кеширование


    Что будет, если попросить документ в другом формате? Для этого к идентификатору документа необходимо дописать другое расширение.

    Запрос:
    GET /yPkjPk.gif HTTP/1.1
    Ответ:
    HTTP/1.1 200 OK
    Content-Type: image/gif


    Изображение изменится, сервис сконвертировал JPG -> GIF, и передал нам. Для конвертации был использован barberry-plugin-imagemagick, который берет на себя конвертацию изображений.
    Запрос может быть еще более сложным:

    Запрос:
    GET /yPkjPk_200x200.gif HTTP/1.1


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

    Если попросить странного, сервис ответит правильно:

    Запрос:
    GET /yPkjPk.pdf HTTP/1.1
    Ответ:
    HTTP/1.1 404 Not Found


    Это значит, что сервис не нашел возможности конвертировать изображение в PDF документ. Конечно, никто не запрещает написать плагин для этого, и подключить к вашей установке Barberry.

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

    Гибкость


    Несмотря на то, что задача стандартна, для разных проектов есть свои ньюансы. Архитектура Barberry позволяет создавать специфичные плагины, расширяя возможности по конвертации данных. Например, вместо плагина для работы с картинками github.com/Magomogo/barberry-plugin-imagemagick можно реализовать свой, добавляющий на изображение водяной знак вашего сайта.

    Так у Barberry появились довольно экзотические способности:



    Управление зависимостями отдано менеджеру пакетов composer. Чтобы создать barberry сервис для своего приложения, мы рекомендуем сделать форк https://github.com/Magomogo/barberry-service и добавлять свою специфику.
    Например, заглянуть в composer.json и подключить необходимые вам плагины. Изначально подключен только плагин для работы с картинками:
    "require": {
        "barberry/barberry": "1.1.*",
        "barberry/plugin-imagemagick": "1.0.*"
    }
    

    Чтобы расширить функциональность, можно подключить плагины, которые будут работать с документами других Content-Type.

    Мониторинг


    Как и любое веб-приложение, Barberry в продуктивном режиме должен быть подключен к системе мониторинга. По адресу barberry.xiag.ch/monitoring.php находится скрипт, который опрашивает все подключенные плагины и рапортует, всё ли в порядке.

    Заключение


    Описание деталей реализации находится за рамками этого поста. Заинтересованных приглашаем к сотрудничеству на страницах ГитХаба.
    Поделиться публикацией

    Комментарии 33

      +3
      А не боитесь, что кто-то устроит перебор GET /yPkjPk_200x200.gif, GET /yPkjPk_200x201.gif, GET /yPkjPk_200x202.gif,… и или забъет кэш, что место на диске закончится или процессор устанет конверитирования выпонять?
        0
        Боюсь :)
        Считаю, это не ответственность сервиса, нужно делать защиту на уровне веб-сервера, разрешая или запрещая определенные URI.
          0
          Так сервер можно положить только допустимыми URI — заставить его генерить все допустимые размеры для всех элементов. Если элементов немного, то это не проблема, но тогда и функционал такой не нужен. Если же элементов много, то над сервером можно круто поизмываться.
            0
            Не понял. Если разрешить, например, с десяток размеров изображений, то остальные генерироваться не будут. Проблема решена. Или нет?
              0
              А все разрешенные размеры для всех хранимых документов правда нужны, это штатная работа сервиса, после генерации они будут отдаваться как статический контент.
                +4
                Не совсем штатная.
                Как я понимаю цели сервиса:
                допустим, есть у нас фотохостинг. Он предусматривает для каждого изображения, скажем, 10 разных размеров. Из них 2-3 являются востребованными, а остальные 7-8 являются экзотикой, которая далеко не для каждого изображения будет востребована (а значит и создана). Почему я считаю что это будет именно так? Потому что в противном случае имело бы смысл генерировать все размеры еще при загрузке оригинала. Так же мы экономим ресурсы и создаем нужные размеры только когда (если) в них возникнет необходимость. По сути, технология нам позволяет сократить занимаемое место до X*0.3, где X — полный объем всех возможных изображений.

                Что делает злоумышленник? А делает он очень просто — запрашивает все возможные размеры для каждого изображения. В результате сервис изображений должен в стрессовом режиме создать и разместить 0,7*Х гигабайт изображений. Более того, даже если предположить что он выдержит этот стресс-тест, потом он должен будет хранить эти никем невостребованные Х гигабайт, вместо того чтобы хранить всего 0,3*Х.

                Собственно, я не утверждаю что сама концепция изображений по запросу плохая, но хотелось бы какого-то решения как раз на вышеописанный случай…
                  –1
                  Теперь понял.
                  У нас другая специфика. Если 100500 клиентов с документами. Мы их показываем на страницах сайта. И вдруг однажды у проекта меняется дизайн. Например лого компании на странице деталей теперь другого размера. Дизайнер меняет URI картинки — у вуаля! Проблемы нет.
                  А дискового места нам не жалко :)
                    0
                    Собственно, вы ушли от ответа.
                      0
                      Так нет решения, предлагайте pull request :) Я просто рассказал как у нас дела.

                      Если придумывать на ходу, я бы сделал какой-нибудь сборщик мусора, если правда памяти жалко.
                        +4
                        >Так нет решения, предлагайте pull request :)
                        Если бы у меня был правильный ответ — я бы не спрашивал, а с умным видом поучал как надо делать:)
                    0
                    К сожалению, ресайз картинки — это медленная процедура, а уж если добавить sharp, чтобы она выглядела получше… Мы пробовали на, кажется, 4-процессорном E5-2660 ресайзить картинки размера 1200х1200 вниз, с помощью imagemagick. Просто ресайз занимал порядка 100мс, ресайз с шарпом — уже больше 300мс.

                    С учётом того, сколько стоит процессор, а сколько диски — выгоднее сразу поресайзить на все размеры и их хранить. С коэффициентом 0.3 вы не совсем правы — если это фотохостинг, то надо оригиналы хранить, или хотя бы хайрез картинки. Оригинал современных мыльниц от 5мб, средний размер приближается к 10мб, ресайзы 1200х1200 и ниже в сумме (штук 7-10 размеров) меньше 2 мегабайт займут, а то и всего 1 мегабайт.
                      0
                      Если не ошибаюсь, можно решить подобное следующим способом:
                      1. Названия/урлы изображений делать рандомными, хранить в базе.
                      2. При запросе изображения, веб-сервер проверяет наличие изображения в директории. Находит — отдает, нет — передает управление скрипту для подготовки изображения.
                      3. Раз в день/неделю/месяц удалять редко запрашиваемые изображения.

                      Три шага вроде решают описанную проблему. Возникает только одна проблема — ручками изображение вставить в страницу будет сложно. Но, по мне, это скорее плюс.
              0
              А есть защита от DDoS? В своё время хотел сделать тоже самое, но решил, что будет слишком легко положить сервер отправкой кучей запросов на изменение размеров изображений, например.
                0
                Как уже сказал выше, защита от DDoS должна быть на уровне веб-сервера.
                  0
                  А что считать DDoS в случае такого сервиса? Вариант если только новые картинки вылетают по таймауту, а старые выдаются как статика уже ддос?
                    0
                    Как уже написали выше, злоумышленник может найти картинки побольше и начать их активно запрашивать в разных размерах, что вызовет нагрузку на процессор нехилую. Или тоже самое с переконвертированием видео, через имеющийся плагин. Единственный видимый вариант — ограничить список размеров до статического (например, только 50х50 и 100х100, все остальные запросы считать невалидными) — но тогда смысл проекта, имхо, немного теряется.
                      0
                      Следует не забывать, что приложение — штука меняющаяся во времени. Это очень ценно, когда мы имеем оригиналы загруженных пользователями данных, чтобы легко менять то, как мы их показываем в новых версиях.
                        0
                        Ну так это можно делать средствами самого приложения — для rails, например, что paperclip, что carrierwave умеют прозрачно для пользователя генерировать из загруженного изображения несколько с разными размерами, без выставления уязвимых веб-сервисов наружу.
                          +1
                          Да, это другой подход для решения той же проблемы. Либо выполняете эту работу для всего контента разом при обновлении проекта, либо в процессе работы по мене необходимости. Разве вы не видите, что в обоих случаях вы потратите одинаковое количество ресурсов?
                        0
                        Просто нагрузка на ддос не тянет. Если, грубо говоря, nginx будет справляться с отдачей полноразмерных и уже обработанных картинок, то можно ли считать такую атаку удачной?

                        В общем, когда я думал над подобной системой, то пришел примерно к такой архитектуре:
                        — веб сервер проверяет есть ли точно запрошенный формат в кэше, если есть отдает (тут никакой новой опасности)
                        — запрос идет в приложение, оно отправляет задание на ресайз в независимую от контекста запроса очередь и делает временный редирект на оригинал, что-то более подходящее по формату или временную заглушку (тут нагрузка не сильно большая чем на «хелло ворлд» средствами php).
                        — демон очереди тупо шарит по заданиям на ресайз c минимальным приоритетом, объединяя одинаковые. Или это вообще скрипт по хрону. Количество процессов ресайза ограничено сверху.

                        По задумке сереверу конечно поплохеет, но ляжет он много позже чем при синхронном ресайзе, а пока не ляжет будет быстро отдавать то, что нужно на старых картинках, и чуть медленнее не совсем то, что нужно. А на обработку очереди можно вообще повесить целый кластер, в том числе и по получению админом аларма от мониторинга. Веб-часть от ресайза не зависит почти.
                          0
                          — запрос идет в приложение, оно отправляет задание на ресайз в независимую от контекста запроса очередь и делает временный редирект на оригинал, что-то более подходящее по формату или временную заглушку (тут нагрузка не сильно большая чем на «хелло ворлд» средствами php).

                          Это очень важный момент, который, как я понял, в представленном проекте не учитывается.
                            0
                            Да, такого протокола нет.
                              0
                              Это просто размышления на тему защиты от DoS подобных сервисов без особой потери функциональности. В принципе ещё можно совместить синхронную и асинхронную модель: если ответа долго нет (или очередь большая или еще как по косвенным признакам оцениваем, что ответа долго не будет)), то выдаем редирект на оригинал или заглушку. Но тут надо экспериментировать, а в предложенном изначально варианте (полностью асинхронном) прототип довольно быстро сделать должно быть, используя хотя бы встроенные в php очереди.
                      0
                      а интеграции с tinymce нету? удобно было бы, заливать медиа информацию на отдельный сервис
                        0
                        Нет, но хорошая идея. Думаю, было бы удобно тем, кто tinymce использует. Я еще помню боль от подключения к нему плагинов для аплоада картинок. Конечно, это должно быть сделано со стороны tinymce а не barberry.
                      0
                      Видео тоже на ходу можно конвертировать? Не сильно ли это накладно?
                      0
                      Wub такое умеет, причём уже давно.
                        0
                        Ух ты, правда давно, первый коммит 2007 года. А где можно найти документацию?
                        0
                        По поводу разрешенных форматов, имхо, дельное замечание. Я бы сделал что-то типа того:
                        * Ввести группы, каждый файл цепляется на группу (например «documents»)
                        * Для группы задаются доступные форматы (в будущем можно и другие настройки цеплять)
                        * При запросе проверять доступен ли формат
                        * Урлы сделать с группой (/documents/yPkjPk.jpg), дабы на вебсервере можно было просто сделать такое же ограничение по формату
                        * Настройки для вебсервера можно генерить прям апликейшеном.

                        Что касается кэширования, то как раз это лучше перенести на вебсервер. На nginx можно сделать простейшую схему с try_files и X-Accel-Redirect, полностью исключив отдачу файлом непосредственно апликейшеном — только генерация.
                          0
                          Идея про кеширование мне нравится. Нужно только понять, получится ли метод DELETE в этом случае (нужно удалить кеш и передать управление сервису, чтобы он удалил документ).
                          0
                          А я уже во втором проекте использую для этих целей UploadCare, опыт оч. положительный.

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

                          Самое читаемое