В интернете достаточно статей и проектов для ресайза изображений. Почему же нужна еще одна? В этой статье я расскажу почему нас не удовлетворили текущие решения и пришлось пилить собственное.
Проблема
Давайте сначала разберемся зачем мы делали ресайз картинок. Мы, как веб сервис, заинтересованы в максимально быстрой загрузке страниц у пользователя. Это нравится пользователям и повышает конверсию. Если у пользователя медленный или мобильный интернет, то крайне важно, чтобы страницы были легкими, не тратили понапрасну трафик пользователя и ресурсы его процессора. Один из пунктов, который в этом помогает это изменение размеров изображений.
Мы решаем две проблемы. Первая проблема в том, что изображения часто не пережаты под нужное разрешение, то есть клиенту приходится не только качать ненужные ему данные, но и тратить ресурсы CPU на ресайз картинки силами браузера. Решение: отдавать пользователю картинки в том разрешении, в котором они будут показаны в браузере.
Вторая проблема в том, что изображения обычно недостаточно хорошо сжаты, то есть можно закодировать их оптимальнее, что увеличит скорость загрузки страницы без субъективной потери качества изображения. Решение: оптимизировать картинки перед отдачей клиенту.
В качестве примера, как делать не нужно можно посмотреть на главную страницу такого известного сайта, как github.com. При весе страницы 2 Мб, 1.2 из них занимают бесполезные картинки, которые можно оптимизировать и не загружать.
Второй пример — наш Хабр. Скриншот приводить не буду, что бы не растягивать статью, результаты по ссылке. На хабре картинкам изменяют разрешение на нужное, но не оптимизируют их. Это позволило бы сократить их размер на 650 Кб (50%).
Распространенные решения
Все, что будет сказано далее относится к JPEG и PNG изображениям, т.к. это наиболее популярные форматы в интернете.
Вбив в google что-то вроде «image resize backend» вы увидите, что в половине случаев предлагается использовать Nginx, другая часть— это различные самописные сервисы, чаще всего Node.js.
Из nginx, а точнее из libgd, которая используется в модуле nginx’а мы смогли выжать на тестовой картинке 63 RPS, что неплохо, но хотелось бы быстрее и больше гибкости. Graphicsmagick тоже не подходит, т.к. его скорость работы слишком низкая. К тому же оба эти решения выдают не оптимизированные изображения. Большинство других решений, например на Node предлагают использовать Sharp для ресайза, MozJPEG для оптимизации JPEG изображений и pngquant для оптимизации PNG.
Мы и сами достаточно долгое время пользовались самописной связкой из Nod’ы, Libvips и MozJPEG c pngquant, но в один из дней задались вопросом— «А можно ли сделать ресайз быстрее и менее требовательным к ресурсам?».
Спойлер: можно. ;)
Теперь хорошо бы выяснить, как можно ускорить наше приложение. Изучив код приложения мы выяснили, что imagemin, который использовался для оптимизации, а в частности его плагины MozJPEG и pngquant при работе дергают одноименные утилиты через os.Exec. Будем это дело однозначно выпиливать и использовать только биндинги к Cи'шным либам. Для ресайза использовался модуль Sharp, который представляет собой биндинг к С библиотеке Libvips.
Наша реализация
Гуглеж показал, что Libvips по прежнему лидер по скорости и конкурировать с ним может только OpenCV. Значит будем использовать Libvips и в нашей реализации, это уже проверенное решение и он имеет готовый биндинг для Go. Пора попробовать написать прототип и посмотреть что из этого выйдет.
Пару слов о том, почему для попытки решения данной проблемы был выбран Golang. Во первых он достаточно быстрый, вы же еще помните, что мы хотим сделать быстрый ресайз. Код на нем легко читать и поддерживать. Последним требованием была возможность работы с C библиотека, нам это пригодится.
Быстро написали прототип, протестировали и поняли, что несмотря на большее, чем в Sharp, количество внутренних крутилок, Libvips по-прежнему выдает на выход не оптимизированные изображения. С этим надо что-то делать. Опять обращаемся ко всемогущему гуглу и узнаем, что лучший вариант это по-прежнему MozJPEG. Тут начинают закрадываться сомнения, что мы сейчас напишем то же самое, что было на Node, только на Go. Но внимательно почитав описание MoZJPEG узнаем, что она является форком libjpeg-turbo и совместима с ней.
Выглядит очень многообещающе. Дело за малым — собрать свою версию Libvips, в которой jpeg-turbo заменен на версию от Mozila. Для сборки мы выбрали Alpine Linux, т.к. приложение все равно планировалось публиковать с помощью Докера и Alpine имеет очень приятный формат конфига пакета, очень похожий на используемый в Arch Linux.
Оригинальный JPEG 351x527 79 Кб |
Оптимизированный 351x527 17 Кб |
---|---|
Собрали, протестировали. Теперь Libvips сразу при ресайзе выдает оптимизированную версию. То есть в Node версии версии мы сначала делали ресайз, а потом еще раз пропускали картинку через decoder-encoder. Теперь мы только делаем ресайз.
С JPEG разобрались, а что делать с png. Для решения этой задачи была найдена библиотека libpngquant. Она не очень популярная, несмотря на то, что консольная утилита pngquant, которая базируется на ней, используется во многих решениях. Так же к ней был найден биндинг на Go, немного заброшенный и с утечкой памяти, пришлось его форкнуть починить, дополнить документацией и всем остальным, что подобает приличному проекту. Libpngquant мы тоже собрали в виде Alpine пакета для простой установки.
Благодаря тому, что теперь изображение не требуется сохранять в файл для обработки c помощью pngquant мы можем немного оптимизировать процесс. Например не сжимать картинку при ресайзе в Libvips, а только после обработки в pngquant. Это позволит сохранить немного драгоценного процессорного времени. Надо ли говорить, что мы так же очень экономим благодаря тому, что вызов C библиотеки гораздо быстрее запуска консольной утилиты.
Оригинальный PNG 450x300 200 Кб |
Оптимизированный 450x300 61 Кб |
---|---|
Оригинальный PNG 351x527 270 Кб |
Оптимизированный 351x527 40 Кб |
---|---|
После того, как прототип был написан, протестирован на моем пк и выдавал приличные 25 RPS на мобильном двух ядерном проце, сжирая весь CPU, захотелось увидеть сколько можно выжать из него на нормальном железе. Запускаем код на шести ядерной машине, натравливаем Jmeter и WTF??? Получаем 30 RPS. Пробуем разобраться что за фигня.
Libvips сам реализует многопоточность, то есть нам нужно только инициализировать библиотеку и в дальнейшем мы можем безопасно обращаться к ней из любого потока. Но у нас почему-то Libvips работает в 1 поток, что ограничивает нас одним ядром. Еще 1 ядро занимает pngquant. Итого получается, что наша супер быстрая ресайзилка отлично работает только на ноутбуке разработчика, а на остальных машинах не может утилизировать все ресурсы. ;)
Смотрим исходники биндинга к Libvips и видим, что там CONCURRENCY по умолчанию выставляется в 1 из-за возникавших в Libvips гонок данных. Но судя по баг трекеру эти проблемы давно исправлены. Выставили CONCURRENCY обратно, тестируем. Ничего не поменялось, Libvips по-прежнему отказывался ресайзить изображения многопоточно. Все попытки побороть эту проблему потерпели неудачу и сказать по правде, я запарился ее решать и решил обойти проблему на другом уровне.
Все более или менее современные ядра Linux (3.9+ и 2.6.32-417+ в CentOS 6) поддерживают опцию SO_REUSE, которая позволяет использовать один порт нескольким экземплярам приложения. Данный подход удобнее, чем балансировка средствами стороннего ПО, такого как HAProxy, т.к. не требует конфигурации и позволяет быстро добавлять и убирать инстансы.
Поэтому мы использовали SO_REUSE и опцию "--scale" в Docker compose, которая позволяет указать количество запускаемых экземпляров.
Время мерить
Пришло время оценить результат наших трудов.
Конфигурация:
- CPU: Intel Xeon E5-1650 v3 @ 3.50GHz 6 cores (12 vCPU)
- RAM: 64 Gb (используется около 1-2 Gb)
- Кол-во воркеров: 12
Результаты:
Больше бенчмарков (правда без сравнения с Node версией) на wiki странице.
Как видно переделывали ресайз мы не напрасно, увеличение скорости составило от 30 до 400% (в некоторых случаях). Если требуется ресайзить еще быстрее, то можно покрутить ручки «speed» и «quality» в libimagequant. Они позволят дополнительно сократить размер или увеличить скорость кодирования ценой потери качества изображения.
Код проекта на GitHub.
Биндинг Go к libimagequant так же на GitHub.
Комментариев нет:
Отправить комментарий