Ресайз изображений на лету

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

    Задача



    Обозначим список требований:

    • Формировать дополнительные изображения любых форматов на лету без внесения дополнительного функционала в приложение в любой момент существования приложения;
    • Дополнительные изображения не должны формироваться при каждом запросе;
    • Закрыть возможность формирования дополнительных изображений неустановленных форматов.


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

    Конфигурация установки nginx



    Для решения вышеуказанных требований нам потребуется следующий набор модулей nginx:



    Модули ngx_http_image_filter_module и ngx_http_secure_link_module по-умолчанию не ставятся поэтому их нужно указать на этапе конфигурации установки nginx:

    phoinix@phoinix-work:~/src/nginx-0.8.29
    $ ./configure --with-http_secure_link_module --with-http_image_filter_module


    Конфигурация nginx



    В конфигурацию нашего хоста добаляем новый location и общие параметры кеша:

    ...
        proxy_cache_path /www/myprojects/cache levels=1:2 keys_zone=image-preview:10m;
    ...
        server {
    ...
            location ~ ^/preview/([cir])/(.+) {
            # Тип операции
                set                         $oper $1;
            # Параметры изображения и путь к файлу
                set                         $remn $2;
            # Проксируем на отдельный хост
                proxy_pass                  http://myproject.ru:81/$oper/$remn;
                proxy_intercept_errors      on;
                error_page                  404 = /preview/404;
            # Кеширование
                proxy_cache                 image-preview;
                proxy_cache_key             "$host$document_uri";
            # 200 ответы кешируем на 1 день
                proxy_cache_valid           200 1d;
            # остальные ответы кешируем на 1 минуту
                proxy_cache_valid           any 1m;
            }
            
            # Возвращаем ошибку
            location = /preview/404 {
                internal;
                default_type                image/gif;
                alias                       /www/myprojects/image/noimage.gif;
            }
    ...
        }
    ...


    Так же добаляем новый хост в конфиг:

    server {
        server_name                     myproject.ru;
        listen                          81;

        access_log                      /www/myproject.ru/logs/nginx.preview.access_log;
        error_log                       /www/myproject.ru/logs/nginx.preview.error_log info;

        # Указываем секретное слово для md5
        secure_link_secret              secret;

        # Ошибки отправляем она отдельный location
        error_page                      403 404 415 500 502 503 504 = @404;

        # location Для фильтра size
        location ~ ^/i/[^/]+/(.+) {
            
            # грязный хак от Игоря Сысоева *
            alias                       /www/myproject.ru/images/$1;
            try_files                   "" @404;
        
            # Проверяем правильность ссылки и md5
            if ($secure_link = "") { return 404; }
            
            # Используем соответсвующий фильтр
            image_filter                size;
        }

        # По аналогии остальные location для других фильтров
        location ~ ^/c/[^/]+/(\d+|-)x(\d+|-)/(.+) {
            set                         $width  $1;
            set                         $height $2;
            
            alias                       /www/myproject.ru/images/$3;
            try_files                   "" @404;
        
            if ($secure_link = "") { return 404; }
        
            image_filter                crop  $width  $height;
        }
        
        location ~ ^/r/[^/]+/(\d+|-)x(\d+|-)/(.+) {
            set                         $width  $1;
            set                         $height $2;

            alias                       /www/myproject.ru/images/$3;
            try_files                   "" @404;

            if ($secure_link = "") { return 404; }

            image_filter                resize  $width  $height;
        }

        location @404 { return 404; }
    }


    В итоге дополнительные изображения можно забирать по ссылкам:



    * try_files — чувствителен к пробелам и русским символам, поэтому пришлось сделать костыль с alias.

    Использование в веб-приложении



    На уровне веб-приложения можно сделать следущую процедуру (Perl):

    sub proxy_image {
        use Digest::MD5     qw /md5_hex/;
        my %params = @_;
        my $filter = {
                        size    => 'i',
                        resize  => 'r',
                        crop    => 'c'            
                      }->{$params{filter}} || 'r';
        my $path = ($filter ne 'i' ?
                        ( $params{height} || '_' ) . 'x' . ( $params{width} || '_' ) . '/' :
                        ()
                   ) . $params{source};
        my $md5 = md5_hex( $path . 'secret' );
        $path = '/preview/' . $filter . '/' . $md5 . '/' . $path;
        return $path;
    }

    my $preview_path = &proxy_image(
                        source  => 'image1.jpg',
                        height  => 100,
                        width   => 100,
                        filter  => 'resize'
                    );


    Хотя я бы еще рекомендовал рассчитывать размеры preview.

    Грабли



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

    оригинал
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 65

      0
      Спасибо за статью!

      p.s. «для решения вышеуказанныХ требований»
        +1
        Спасибо, исправил
        +5
        Хочется добавить, что если на ресайзищий хост идёт большая нагрузка, да ещё ко всему прочему картинки не маленькие, то лучше под него запустить отдельный инстанс nginx'а, чтобы не было проблем с блокировкой.
          +1
          В общем-то, именно поэтому ресайз выделен в отдельный хост, что бы можно было вообще разделить сервера: frontend-сервер с кешированием, storage-сервер с ресайзом.
          +4
          почему бы тупо не хранить все необходимые ресайзы? нагрузка — только в момент аплода, апдейта исходного изображения. что скажете?
            –1
            Придется писать больше кода. А это всегда плохо.
              +1
              Я, кстати, не так давно написал подобную статью, но там используется apache: torqueo.net/image-resizing-easy-and-useful-solution/
                +2
                это далеко не всегда плохо. кроме того, кода получится всего-то на несколько строк больше.
                  0
                  Ну в принципе-то да. Просто я уже очень давно использую подход «ресайзинг на лету» и уже привык :) Минусов замечено не было, даже на довольно посещаемых сайтах где много изображений, т.е. нагрузка минимальная и совсем незаметная.
                    +1
                    А место расходуется экономнее, особенно если подчищать кэш от картинок к которым давно не было обращений.
                      +3
                      место? вы о чем? сейчас место стоит дешевле процессорного времени.
                        +1
                        В принципе да, но в случае шаредхостинга часто присутствует ограничение на максимальное количество файлов, да и на своём сервере тоже скорость доступа к диску будет плавно падать если он будет постепенно засераться всеми размерами всех изображений, даже тех, к которым нет обращений.

                        В ресайзе по запросу есть своя логика и для определённого вида проектов он оправдан.
                          0
                          По моему скромному мнению, если есть проблема с хранением изображений или любых других файлов, то имеет смысл задуматься о расширении в сторону использования платных сервисов, аля Amazon S3 и им подобных. Блано цены за гигабайт места и трафик там более чем демократичны.
                            0
                            здесь ключевой момент, определенного вида проектов, просто для онлайн СМИ это вряд ли будет оправдано.
                              0
                              Почему нет? Кстати для онлайн СМИ это более оправдано, так как в них присутствует «горячий» контент, который будет кешироваться. Картинки старых новостей не будут занимать лишнего места. При смене дизайна, не нужно будет задумываться о том, что бы проводить глобальный ресайз по всем картинкам, формировать новые и удалять старые превью.
                                0
                                дизайн меняется не так часто, и из-за этого одна ночь на ресайз ничего не поменяет. старые превью не так много занимают, чистить особого смысла нет, а если и есть, то они в отдельной папке, всю папку просто удаляешь и все
                        +2
                        я не знаком с алгоритмами ресайза изображений, но я абсолютно уверен, что выдача заранее сгенерированной статики будет обходиться гораздо дешевле по ресурсам. хотя бы по тому, что в случае с генерацией на лету выполняется три действия: генерация, сохранение в кеше, высер клиенту.
                          +1
                          Вопрос в том где эту статику хранить, и насколько велико количество запросов к этой статике. Например в моем проекте эти цифры велики. И я очень рад что мы используем для хранения статики S3 и Cloud Front. Затраты на генерацию там на грани ошибки в статистике.
                            +1
                            вери найс гут! после некоторых размышлений мне стало всё понятно.
                            0
                            Да, три действия, только один раз в сутки, на не при каждом запросе.
                            +1
                            «Довольно посещаемые» и «много изображений» — это сколько в цифрах?
                            Сколько изображений в секунду запрашивается?
                            С какого исходного до какого финального размера они при этом ресайзятся?
                              +1
                              И когда приходит робот яндекс-картинок, то тоже нагрузка незаметная?
                                +1
                                Кто не шардит роботов — сам виноват )
                                  0
                                  Это как? без ущерба для индексации?
                                    0
                                    В разумных пределах — не помешает.
                                    От валяющегося сайта больший ущерб, я считаю )

                                    Ну, то есть шейпинг, а не шардинг, конечно — понапридумывали слов, а нам вот мучайся.
                        +1
                        На правах оффтопа:
                        1. Есть модуль под Drupal, который реализует все три указанных пункта, да ещё и кроме ресайза умеет много чего (пакетный фотошоп ;) ), зовётся он ImageCache
                        2. Есть ещё интересная схема кэширования (постепенно ставлю её себе на все сайты) когда картинка загружается на сервак в большом разрешении а в статьи вставляется, уменьшая параметрами html тега img. Скрипт на стороне сервака парсит текст, находит картинки и ресайзит загруженное изображение до указанных в тексте размеров, меняя путь к изображению на закешированное, да ещё и проставляя линку на оригинал. Если ещё учесть что картинки вставляются и ресайзятся в WYSIWYG редакторе мышкой, то работа по набору статей превращается в отдых.
                        Результат выглядит вот так
                          0
                          пути к картинкам страшные получаются, а можно так организовать?

                          оригинал: site.ua/i/ori/culler.jpg

                          уменьшенная: site.ua/i/sm/culler.jpg

                          если можно без переписывания модуля, то респект, и почитаю на него доку, а если нельзя, то как обычно кастомные решения удобнее
                            0
                            В моем примере предоставлена возможность формировать размеры исходя из того что передано в uri.
                            Ничто мешает сделать location ~ /i/sm… в котором жестко указать размеры и фильтр для этого вида изображений. Тем более их можно будет менять без вреда для здоровья, для всех сразу.
                            Правда, придется каждый дополнительный размер описывать отдельно в конфиге, но это гораздо проще, чем кодинг.
                              0
                              Если правильно написать скрипт ресайзинга, все аналогично, поправил конфиги, за одну ночь, сделались все ресайзы и все отлично
                              0
                              ImageCache работает так:
                              1. создаём профиль по обработке изображений который состоит из последовательности действий (масштабирование, уменшение, поворот, всяческие фильтры) и называем его например «thumb»
                              2. загружаем на сервер изображение, которое получит имя site.ua/files/culler.jpg
                              3. обращаемся по модифицированному адресу и получаем обработанную версию site.ua/files/imagecache/thumb/culler.jpg
                              т.е. в разрыв URI между каталогом с загружаемым файлом и путём к файлу в этом каталоге вставляется imagecache/<имя профиля>

                              По второй схеме пути меняются так:
                              если у оригинала путь site.ua/files/culler.jpg
                              то у уменьшенной копии site.ua/files/resize/culler-400x300.jpg
                              по этой схеме ДДоСить смысла нет т.к. УРЛы формируются только на стороне сервера и если я на клиенте запрошу модифицированный УРЛ то система просто ругнётся на его отсутствие.

                              ИМХО очень даже читабельные имена.
                                0
                                Ах да, ещё забыл упомянуть что по второй схеме если я вставляю изображение с внешнего урла, то оно тоже будет кэшироваться локально (если не указано иного) что существенно сокращает время загрузки страницы.
                                  0
                                  они то читабельные, но не такие как мне нужны, а объяснять почему именно такие нужны не охота ;)
                                    0
                                    Ну, у каждого свои задачи)
                                    Мне мой вариант больше подходит т.к. работает без плясок с бубном на любом шаредхостинге.
                              –10
                              мне лень было разбираться в написанном, но если суть в том что файлы картинок ставится 404 обработчик и в случае отсутствии картинки она генерится с параметрами, указанными в запросе — то это уже старо как мир и было на хабре, при том очень подробно, насколько я помню. Даже вроде и не 1 раз…

                              Но если привнесли что то новенькое — молодцы!
                                0
                                Мне лень было разбираться в сути твоего комментария, но если ты говоришь о том, что статья бойан, то чтоб тебя это самое… вообщем ай-ай-ай, но если вдруг ты такого не говорил, то респект тебе.
                                +8
                                Маниакально верю что ресайзить картинки лучше единожды при аплоде, и выдавать их клиенту в готовом виде. Кеш не бесконечен, его объем гораздо меньше дискового объема (а при каждом ображении к картинке вытертой из кеша, она будет снова ресайзиться). Чтобы использовать описанный в топике методо, задача должна быть настолько специфическая, что я даже ее и представить не могу.
                                А давайте прикинем сколько тратиться памяти на ресайз. Предположим картинка размером 800 х 600, т.е. состоит из 480000 пикселей. Если грубо предположить что каждый пиксель это 4 байта (3 — цветность. 1 -альфа канал), то получается что в момент ресайза фотки сервер выделяет 480000 х 4 = 1 920 000 байт, т.е. почти 2 мегабайта оперативки только на хранения в памяти во время обработки изображения. Так что лучще потратить единожды 2 мегабайта и хранить фотку (пусть это будеть превьюха размером 120х80) с «весом» 2-10 кб. Или хранить исходную фотку, согласно примера 800 х 600. Это, я думаю, в среднем 100 — 400кб и каждый раз тратить память на ее повторную обработку в случае если изображение удалилось из кеша?
                                  +3
                                  Маниакально не верю, что если ресайзить картинки при аплоаде и «наресайзить» хотя бы несколько десятков гигабайт картинок, то при следующем редизайне вашего проекта, когда вам прийдет промигрировать все картинки с размеров оригинал, большая, средняя, маленькая, превью до размеров x+10px, то вы не за… тесь это делать(например искать под это место), и сможете выкатить новую версию проекта без даунтайма.

                                  Ну а если картинок в проекте 100 штук, то можно и в пейтнбраше все поресайзить ;)
                                    +1
                                    Легко, ночью, скрипт с делеями по 100-1000 картинок поресайзит за несколько часов
                                      0
                                      Все зависит от архитектуры, например у меня на фотохостинге storage-сервера и backend-сервера стоят раздельно, при этом на storage-серверах процессорное время не использовалось. Проставили на них ресайз на лету — сэкономили 30 % дискового пространства, но при этом 30 % загрузка процессоров в пике, но они и так простаивали до этого.
                                        –1
                                        Честно говоря, непонятно за счет чего вы экономите дисковое пространство делая ресайз на лету. Ведь вы должны хранить оригиналы изображений (которые потом ресайзятся), которые гораздо больше по «весу» чем уже оптимизированные изображения?
                                          0
                                          Экономия достигается за счет того, что для старых фото, превьюшки не лежать в кеше, так как их никто не смотрит, реально в кеше лежат сгенеренные превью для фото, загруженных за последние 2-3 дня + топовые, а это ничтожно мало по сравнению с остальным контентом.
                                          Не понял вопроса про то, что нам приходится хранить оригиналы фото, все таки это основная задача фотохостинга.
                                          0
                                          ну если простаивали, то можно и на лету, но обычно при правильном проектировании систем, они не простаивают ;)
                                      +3
                                      Предположим, что у вас крутой фотохостинг со средним числом аплоадов = 100 в секунду.
                                      После аплоада картинка ресайзится один раз, чтоб показаться залившему ее автору автору и попадает в дисковый кеш фронтенда.
                                      Получается в среднем на бекенде стораджа выделяется 100 x 2Мб = аж 200 Мб ОЗУ.
                                      Ужасная цифра для фотохостинга :)

                                      Далее:
                                      При таких параметрах у нас на диск сваливается 8 640 000 картинок в сутки, 3 153 600 000 картинок в год.
                                      6 307 200 000 Мб в год только на оригиналы.
                                      Даже если все необходимые превьюшки весят 20% от оригинала, все равно получается недешевый у нас мусорник.
                                        0
                                        Когда будет на проекте такой трафик, прибыль с этого проекта позволит купить дата-центр и не переживать по этому вопросу
                                    • UFO just landed and posted this here
                                        0
                                        Да, еще следить, есть ли у изображения превью или нет, и какие, а это уже нагрузка на базу данных.
                                          –1
                                          if(is_file(/tath/to/my_thumbnail))
                                          нет тут никакой нагрузки на базу. Не надо в нее пихать что попало и не будет нагрузки лишней :)
                                            0
                                            Ага, а если storage и backend разделены?
                                            • UFO just landed and posted this here
                                                0
                                                Storage должен отдавать статично файлы, чем быстрее тем лучше.
                                                Вынесение логики работы приложения на Storage это во-первых замедление отдачи файлов, во-вторых дополнительная точка отказа.
                                                Тем более в вашем примере вообще 3 варианта запроса, это значит, что Backend сначала делает запрос Try, в случае отказа — запрос Make, а потом запрос Give. Итого 3 запроса на одно превью — пример действительно тупой.
                                                Даже лишнее обращение к диску на предмет существования файла для высоконагруженных систем — уже плохо.
                                                • UFO just landed and posted this here
                                        +2
                                        эээ, в проекте в котором я сейчас работаю была подобная проблема, решена была с помошью того самого 404ого обработчика, верстальщик в верстке задает необходимые размеры превью картинки и если при запросе данной картинки ее нет, то из запроса берется имя оригинала и генерится нужная превьюшка по взятым из того же запроса парамтерам, кладется рядом с оригиналом и отдается, если картинка перезаливается или удаляется то удаляются и все ее превью, то есть пых поднимается в среднем один раз на превью ну чуть больше) картинки не часто удаляются или перезаливаются
                                        • UFO just landed and posted this here
                                          • UFO just landed and posted this here
                                              +2
                                              Преимущества этого способа:
                                              — Дизайнер при разработке дизайна сайта не ограничен в размерах превьюшек, а также от используемого движка или лени разрботчиков.
                                              — При редизайне не надо перегенерять в новые размеры все существующие картинки, большая часть из которых к тому же больше никогда не будет запрошена.
                                              — В основном сторадже хранятся только оригиналы, это удобно и эффективно (кеш превьюшек лучше выносить на отдельное железо, а если все сурово — даже на кластер)
                                              — Очень простой код приложения, нужно только уметь сохранять файлы при заливке и генерить разные урлы при отдаче.
                                              — В случаях, когда исходные картинки вообще не хранятся на сервере (аггрегаторы, поисковики, партнерские блоки от тормозных источников) это лучший вариант.
                                              — У редкого разработчика самописный ресайзер будет эффективней написанного автором nginx :)

                                              Прегенерация до выдачи клиенту (если сильно хочется, хотя необходимости не вижу) тоже не проблема — достаточно в админке после сабмита картинки рисовать ее же в нужных размерах — они отресайзятся и попадут в кеш.
                                                0
                                                Согласен. По работе сталкивался с задумкой дизайнера, где имели превьюшки маленькие в просмотре товаров категории, превьюшки средние, когда заходишь в товар и превьюшки большие (оригинальные картинки, которые все равно должны обрезаться, иначе юзер нам зальет 20Мб картинку с огромным разрешением) и совсем маленькие превьюшки для админки, чтобы осуществлять их удаление/редактирование. Это решение бы помогло не писать и не поддерживать хорошее количество кода.
                                                +1
                                                Делал когда-то такую штуку, но на mod_rewrite + php. Отдельную статью писать уже нет смысла, так что напишу здесь.
                                                Суть в следующем. Оригинальные картинки лежат например в {DOCUMENT_ROOT}/img. Обращение к отресайзеным идёт через ссылку вида /resampled/100x100/img/something.jpg
                                                В этой ссылке 100x100 это размер, котрый мы в итоге хотим получить, а "/img/something.jpg" — путь к оригиналу.

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

                                                Скрипт, гененрирует уменьшеную картинку и отдаёт её клиену, а также, что важно, сохраняет на диск в {DOCUMENT_ROOT}/resampled/100x100/img/something.jpg

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

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

                                                С таким подходом есть проблемы, правда легко решаемые.
                                                Во первых в папке {DOCUMENT_ROOT}/resampled/ будет собираться кещ, которой надо чистить либо в ручную, либо писать для этого какой-то скрипт.
                                                Также актуальна проблема описаная в статье, когда можно атаковать сервер запросив мого разных ресайзнутых картинок.
                                                Но есть и плюс, который заключается в том, что помимо изменения размеров картинки можно реализовать, наложение, эффектов, водяных, знаков итд. И всё это будет просто задаваться через ссылку.

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

                                                  Но мне этот способ не подходит — в моей cms можно выбрать тему дизайна, и может оказаться, что для нее размеры запрещены, которые ей нужны. А человек, выбирающий тему дизайна, не должен знать ничего ни про какой список допустимых размеров
                                                    0
                                                    Как раз для этого и используется ngx_http_secure_link_module.
                                                    На приложение может формировать любые размеры, но извне сгенерить URL любого превью достаточно сложно не зная secure_link_secret.
                                                    Тут только вопрос насколько можно давать пользователям доступ к функции формирования URL превью приложения.
                                                      0
                                                      Можно ли реализовать это без nginx? Как вообще указанный модуль работает, я что-то не совсем понял?
                                                        0
                                                        >Можно ли реализовать это без nginx?
                                                        А зачем?
                                                        > Как вообще указанный модуль работает, я что-то не совсем понял?
                                                        sysoev.ru/nginx/docs/http/ngx_http_secure_link_module.html
                                                          0
                                                          Как зачем? Чтобы моя cms работала на любом хостинге
                                                            0
                                                            Нет, давайте уж мухи отдельно, котлеты отдельно.
                                                            Это раздел ngnix, следовательно он используется априори, остальное не имеет значение.
                                                            Можно. Можно сделать через обработчик 404 ошибки, можно через mod_rewrite, но это будет скрипт, который, впрочем, тоже не всегда подойдет для любого хостинга.
                                                  +3
                                                  У нас больше года уже как таким способом картинки ресайзятся. =)
                                                  Хорошо что написали, респект.
                                                    +3
                                                    А… Заглянул в профайл. Сразу не узнал)

                                                  Only users with full accounts can post comments. Log in, please.