Привет, Хабр! Представляю вашему вниманию перевод статьи «5 Reasons You Should Stop Using System.Drawing from ASP.NET».

image

Ну что ж, они таки сделали это. Команда corefx в конце концов согласилась на многочисленные просьбы и включила System.Drawing в .NET Core. (оригинальная статья датируется июлем 2017)

Выходящий пакет System.Drawing.Common будет содержать бо?льшую часть функциональности System.Drawing из полного .NET Framework и предназначен для использования в качестве опции совместимости для тех, кто хочет мигрировать на .NET Core но не может этого сделать из-за зависимостей. С этой точки зрения Microsoft делает правильную вещь. Необходимо снижать трение, поскольку принятие .Net Core это более сто?ящая цель.

С другой стороны, System.Drawing одна из наиболее бедных и обделенных областей .Net Framework и многие из нас надеялись, что внедрение .NET Core будет означать медленную смерть System.Drawing. И вместе с этой смертью должна появиться возможность сделать что-то лучшее.

Например, команда Mono сделала .NET-совместимую обертку для кросс-платформенной графической библиотеки Skia от Google, названную SkiaSharp. Чтобы инсталляция стала простой, Nuget проделал долгий путь в поддержке нативных библиотек для каждой платформы. Skia достаточно полнофункциональна и ее производительность уделывает System.Drawing.

Команда ImageSharp также проделала огромную работу, повторяя многое из функциональности System.Drawing, но с лучшим API и 100% реализацией на C#. Они все еще не готовы к продуктивной эксплуатации, но похоже, что уже достаточно близки к этому. Небольшое предупреждение по поводу этой библиотеки, поскольку мы говорим об использовании в серверных приложениях: сейчас, в конфигурации по умолчанию внутри используется Parallel.For для ускорения некоторых операций, что означает, что будет использоваться большее количество рабочих потоков из пула ASP.NET, в конечном итоге снижая общую пропускную способность приложения. Надеюсь, это поведение будет пересмотрено до релиза, но даже сейчас достаточно изменить одну строчку конфигурации, чтобы сделать его более пригодным для использования на сервере.

В любом случае, если вы рисуете, строите графики или рендерите текст в изображения в приложении на сервере, стоит серьёзно рассмотреть смену System.Drawing на что угодно, независимо от того, переходите вы на .NET Core или нет.

Со своей стороны, я собрал конвейер высокопроизводительной обработки изображений для .NET и .NET Core, который предоставляет качество изображений, которое System.Drawing предоставить не может, и делает это в высокомасштабируемой архитектуре, спроектированной специально для использования на сервере. Пока что он только для Windows, однако кроссплатформенность есть в планах. Если ты используешь System.Drawing (или что-то еще) для изменения размера изображений на сервере, то лучше рассмотреть MagicScaler в качестве замены.

Но воскрешение System.Drawing, при котором для некоторых разработчиков облегчается переход, скорее всего убьёт бо?льшую часть импульса, который получили эти проекты, поскольку разработчики были вынуждены искать альтернативы. К сожалению в экосистеме .NET, библиотеки и пакеты Microsoft всегда будут выигрывать, и не важно насколько превосходящими могут быть альтернативы.

Этот пост — это попытка исправить некоторые просчеты System.Drawing в надежде что разработчики исследуют альтернативы даже если System.Drawing останется как вариант.

Я начну с часто цитируемого отказа от ответственности из документации System.Drawing. Этот отказ поднимался пару раз в дискуссии на Гитхабе при обсуждении System.Drawing.Common.
«Классы с пространством имен System.Drawing не поддерживаются для использования в службах Windows или ASP.NET. Попытка использования этих классов с такими типами приложений может спровоцировать неожиданные проблемы, такие как уменьшение производительности сервера и ошибки времени выполнения».

Как и многие из вас, я читал этот отказ от ответственности очень давно, и тогда я пропустил его и все равно использовал System.Drawing в моем ASP.NET приложении. Почему? Потому что люблю опасность. Либо так, либо не нашлось других жизнеспособных вариантов. И знаете что? Ни чего плохого не случилось. Скорее всего я не должен был этого говорить, но держу пари, что многие из вас испытали то же самое. Так почему бы не продолжить использовать System.Drawing или библиотеки на его основе?

Причина №1: Дескрипторы GDI


Если вы когда-нибудь испытывали проблемы при использовании System.Drawing на сервере, это скорее всего был именно этот случай. Если еще не испытывали, то это одна из наиболее вероятно возможных причин.

System.Drawing в большей части, это тонкая обертка Windows GDI+ API. Многие объекты System.Drawing поддерживаются дескрипторами GDI, а они имеют количественное ограничение на процессор и на пользовательский сеанс. Если этот порог будет достигнут, вы получите исключение «Out of memory» и/или GDI+ 'generic' ошибки.

Проблема в том, что в .NET, сборка мусора и завершение процесса могут откладывать высвобождение этих дескрипторов на время, достаточное чтобы вы достигли ограничения, даже под небольшой нагрузкой. Если вы забыли (или не знали, что нужно) вызвать Dispose() для объекта, который содержит такие дескрипторы, вы очень рискуете столкнуться с такими ошибками в своей среде. И как большинство багов, связанных с ограничением ресурсов или с утечками, скорее всего такая ситуация успешно пройдет тестирование и ужалит вас в продуктивной эксплуатации. Естественно это наступит когда ваше приложение будет под наибольшей нагрузкой, так чтобы максимальное число пользователей узнало о вашем позоре.

Ограничения на процессор и на пользовательский сеанс зависят от версии операционной системы, а ограничение на процессор настраиваемое. Но версия не имеет значения, т.к. дескрипторы GDI внутренне представлены типом данных USHORT, так что имеется жёсткое ограничение в 65536 дескрипторов на пользовательский сеанс, и даже хорошо написанное приложение рискует достичь этого предела под достаточной нагрузкой. Когда вы полагаете, что более мощный сервер позволит обслуживать больше и больше пользователей параллельно на одном экземпляре, этот риск становится более реальным. И действительно кто хочет создавать ПО с известным жёстким пределом масштабируемости?

Причина №2: Параллельность


У GDI+ всегда были проблемы с параллельностью, хотя многие из них были связаны с архитектурными изменениями в Windows7 / Windows Server 2008 R2, вы все еще наблюдаете некоторые из них в новых версиях. Наиболее заметной является блокировка по процессу устраиваемая GDI+ во время операции DrawImage(). Если вы меняете размеры изображений на сервере используя System.Drawing (или библиотеки, которые его оборачивают), метод DrawImage(), вероятно, лежит в основе этого кода.

Более того, при выполнении нескольких одновременных вызовов DrawImage(), все они будут заблокированы, пока все они не будут выполнены. Даже если время отклика не является для вас проблемой (почему нет? вы ненавидите своих пользователей?) учтите, что любые ресурсы памяти, связанные с этими запросами и все дескрипторы GDI, удерживаемые объектами, связанными с этими запросами, завязаны на время выполнения. На самом деле не потребуется слишком большой нагрузки на сервер, чтобы начать вызывать проблемы.

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

Причина №3: Память


Рассмотрим обработчик ASP.NET, который генерирует диаграмму. Он должен делать что-то вроде этого:

  1. Создать растровое изображение как канву
  2. Нарисовать несколько форм на растровом изображении используя ручки и/или кисти
  3. Нарисовать текст, используя один или более шрифтов
  4. Сохранить растровое изображение как PNG в MemoryStream

Скажем, диаграмма имеет размеры 600 на 400 точек. Это всего 240 000 точек, умноженное на 4 байта для точки для формата RGBA по умолчанию, итого 960 000 байт для растрового изображения, плюс немного для объектов рисования и буфера сохранения. Пусть будет 1мб для всего запроса. Скорее всего вы не получите проблем с памятью для такого сценария, а если с чем и столкнетесь, то скорее с ограничением на количество дескрипторов, о котором я упомянул ранее, поскольку изображения, кисти, ручки и шрифты обладают своими дескрипторами.

Реальная проблема наступит когда System.Drawing используется для задач формирования изображений. System.Drawing прежде всего графическая библиотека, а графические библиотеки как правило все строятся вокруг идеи, что всё является растровым изображением в памяти. Это прекрасно пока ты думаешь о мелочах. Но изображения могут быть реально больши?ми, и они становятся больше каждый день, т.к. камеры с большим количеством мегапикселей постоянно дешевеют.

Если вы примете наивный подход System.Drawing к построению изображений, то для обработчика изменения размера вы получите что-то вроде этого:

  1. Создайте растровое изображение в качестве холста для изображения-приемника.
  2. Загрузите исходное изображение в еще одно растровое изображение.
  3. Вызовите DrawImage() с параметром «изображение-источник» для изображения-приемника, с применением изменения размера.
  4. Сохраните целевое растровое изображение в формате JPEG в поток памяти.

Предположим что целевое изображение будет иметь размеры 600х400, как и в предыдущем примере, тогда снова имеем 1Мб для целевого изображения и потока памяти. Но давайте предположим, что кто-то загрузил 24-мегапиксельное изображение от их причудливых новых зеркалок, тогда нам необходимо 6000x4000 точек с 3 байтами для каждой (72мб) для декодированного исходного растрового изображения в формате RGB. И будем использовать ресемплинг HighQualityBicubic из System.Drawing, потому как он единственный выглядит хорошо. Тогда нам нужно учесть другие 6000x4000 точек с 4 байтами на каждую, для PRGBA-конверсии которая происходит внутри вызываемого метода, добавляя дополнительные 96мб используемой памяти. Итого получается 169мб (!) для запроса на преобразование одного изображения.

Теперь представим, что у вас не один пользователь делает такие штуки. Теперь вспомним, что запросы заблокируются пока все они полностью не выполнятся. Сколько нужно времени, чтобы у вас кончилась память? И даже если вы не беспокоитесь, что полностью исчерпаете всю доступную, помните, что есть много способов лучше использовать память вашего сервера, чем удерживать кучу пикселей. Рассмотрим влияние давления памяти на другие части приложения/системы:

  1. Кэш ASP.NET может начать сбрасывать элементы, которые дорого воссоздать
  2. Сборщик мусора будет запускаться чаще, замедляя работу приложения
  3. Кэш ядра IIS или кэш файловой системы Windows может удалить полезные элементы
  4. Пул приложений может превысить установленный лимит памяти и может быть перезапущен
  5. Windows может начать подкачку памяти на диск, замедляя работу всей системы

Вы же действительно не хотите ничего из этого?

Библиотеки разработанные специально для задач обработки изображений подходят к этой проблеме совсем по другому. У них нет необходимости загружать исходное или целевое изображение целиком в память. Если вы не собираетесь рисовать на нем, вам не нужна канва/растровое изображение. Это делается скорее так:

  1. Создаете поток для JPEG-кодировщика целевого изображения
  2. Загружаете одну линию из исходного изображения и сжимаете ее по горизонтали
  3. Повторяете столько раз сколько нужно для формирования одной линии для целевого файла
  4. Сжимаете получившиеся линии вертикально
  5. Повторяете с шага 2 пока все линии исходного файла не будут обработаны

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

Я знаю только одну .NET библиотеку, которая оптимизирована по такому принципу, и я дам вам подсказку: это не System.Drawing.

Причина №4: CPU


Другим побочным эффектом того, что System.Drawing более графически-ориентирована, чем ориентирована на обработку изображений, является то, что DrawImage() довольно не эффективна с точки зрения использования процессора. Я довольно подробно осветил это в предыдущем посте, но это обсуждение можно резюмировать следующими фактами:

  • В System.Drawing преобразование масштаба HighQualityBicubic работает только с форматом PRGBA. Почти во всех сценариях это означает дополнительную копию изображения. Мало того, что это использует (значительно) больше дополнительной памяти, также такое поведение сжигает циклы процессора на преобразование и обработку дополнительного альфа-канала.
  • Даже после того, как изображение находится в своем родном формате, преобразование масштаба HighQualityBicubic выполняет примерно в 4 раза больше вычислений, чем необходимо для получения правильных результатов пересчета.

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

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

Причина №5: Обработка изображений обманчиво сложна


Производительность в сторону, System.Drawing во многих отношениях не дает правильно обработать изображение. Использовать System.Drawing значит либо жить с некорректным выводом, либо выучить все про ICC-профиль, квантование цвета, exif ориентацию, коррекцию и многие другие специфичные вещи. Это кроличья нора, которую большинство разработчиков не имеют ни времени, ни желания исследовать.

Такие библиотеки как ImageResizer и ImageProcessor приобрели много поклонников, заботясь о некоторых из этих деталей, но будьте бдительны, у них внутри System.Drawing, и они приходят вместе со всем багажом который я подробно описал в этой статье.

Бонусная причина: вы можете лучше


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

System.Drawing во многом такой же. Он делает правильно, если вы правильно заполнили настройки, но вы удивитесь насколько лучше ваши изображения могут выглядеть если вы используете лучшие утилиты.

Я просто оставлю это здесь в качестве примера. Это самое лучшее, что может сделать System.Drawing по сравнению с настройками MagicScaler по умолчанию. Может быть, ваше приложение выиграет от получения очков…

GDI:

image

MagicScaler:

image
Photo by Jakob Owens

Оглянитесь вокруг, исследуйте альтернативы, и пожалуйста, во имя любви к котятам, прекратите использовать System.Drawing в ASP.NET

Комментарии (23)


  1. WendyH
    28.12.2019 21:12

    Скажите пожалуйста, что заставляет людей использовать System.Drawing на сервере?


    1. vasyapivo
      28.12.2019 21:37

      Используем System.Drawing на сервере потому что конвертируем TIFF в PNG на лету. System.Drawing делает это быстрее и лучше (по поддержке форматов), чем ImageMagick.NET. ImageMagick.NET тупо выжирал весь CPU на тех же объёмах, где System.Drawing потребляет не больше 20%.
      ImageSharp не умеет в TIFF и к тому же он очень баговый и ещё не вышел из беты. Да и спанчбоб довольно одиозная и непредсказуемаая личность: меняет API без всякой обратной совместимости.
      Сейчас делаем попытку перейти на ImageMagick.NET но уже на Linux серверах, но пока результаты противоречивы, видимо надо больше тюнинга.
      В общем, обработка картинок под .net core — это боль.


      1. AikoKirino
        28.12.2019 01:18

        netvips не пробовали?


        1. vasyapivo
          28.12.2019 03:24

          Нет. Спасибо за наводку


        1. vasyapivo
          31.12.2019 12:23

          netvips раза в 3 быстрее ImageMagick.NET на конвертации TIFF в PNG. Но самое приятное, что он не выедает CPU в 100% и сервер не останавливается.
          В общем, вы очень помогли.


          1. AikoKirino
            31.12.2019 12:35

            Скоро подвезут и тру стриминг https://github.com/kleisauke/net-vips/issues/33
            В общем это пока мой фаворит среди библиотек для работы с графикой.
            ImageSharp в перспективе может стать этаким image из Go, но пока он сырой, и как уже отметили — набор кодеков в нём невелик.


      1. some_x
        28.12.2019 18:22

        Попробуйте GraphicsMill


    1. slepmog
      27.12.2019 22:27
      +1

      Используем System.Drawing на сервере, потому что рисуем штрихкоды там, которые надо динамически отдавать картинкой, а библиотека их создания (ZXing.Net) имеет зависимость от System.Drawing.


      По той же причине System.Drawing пришлось протащить даже в SQL Server (create assembly). Из-за интересных багов одной популярной клиентской софтины, картинки в отчётах правильно отрисовываются только если были получены напрямую из подключения к базе данных, а не по какому-то иному каналу, так что для этой софтины приходится генерировать штрихкоды в SQL Server.


    1. AlexOnBeta Автор
      28.12.2019 08:12

      Несколько лет назад принимал участие в проекте по документообороту. Приходилось рендерить страницы предпросмотра для pdf-документа в браузере.
      Сейчас натолкнулся на эту статью в связи с необходимостью рисовать свои тайлы на сервере. Конечно, это другого типа задача, как раз таки связанная с рисованием, а не обработкой уже готового изображения, но подводных камней с параллельностью и блокировкой объектов GDI это не отменяет.


  1. MonkAlex
    28.12.2019 21:18

    В NetCore оно требует libgdiplus на линуксах и маках, лишняя зависимость для вроде бы кроссплатформенного приложения.
    Короче говоря — не нужно, при наличии альтернатив.


    1. kekekeks
      27.12.2019 23:28

      NetCore требует libicu, libgssapi-krb5-2, liblttng-ust0, libssl1.0.2 и zlib1g на линуксах, лишние зависимости для вроде бы кроссплатформенного приложения, короче говоря — не нужно, при наличии альтернатив в виде Go, который собирает бинарник вообще без зависимостей, даже без libc.


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


      1. MonkAlex
        28.12.2019 08:54

        Таки внезапно, но приложение с неткором и рандомным наобором пакетов у меня отлично работало на чистых win7, win10, Ubuntu 16.04 и Linux Mint 18.3, MacOS 10.13.
        А стоило только добавить System.Drawing, как на всех невиндовых машинах пришлось ставить libgdiplus.
        Возможно, какие то пакеты действительно требуют libicu, libgssapi-krb5-2, liblttng-ust0, libssl1.0.2 и zlib1g и те не установлены в системе — но я с таким не сталкивался.


        1. kekekeks
          28.12.2019 11:04
          +1

          чистых

          Ubuntu 16.04

          Нет такого понятия. Есть "предустановленный набор софта для конкретной десктопной либо серверной редакции". В тот же базовый образ докера с убунтой вышеперечисленные пакеты не входят.


          1. MonkAlex
            28.12.2019 11:18

            Ок, конкретно это была десктопная редакция, т.к. тестил я там AvaloniaUI =)

            С образами докера не связывался, поверю на слово.


        1. falconandy
          30.12.2019 07:54
          +1

          Тут перечислены необходимые зависимости/требования для разных операционных систем. Даже для Windows может потребоваться установка дополнительных пакетов.


  1. Viceroyalty
    28.12.2019 21:23

    То чувство, когда зашел почитать что такое System.Drawing, но увы и ах.
    P.S. мне одному кажется, что картинки одинаковые?


    1. Varim
      27.12.2019 22:47

      Думаю одинаковые.


      1. hornT
        28.12.2019 08:13

        Скажу как дальтоник — картинка снизу кажется мне светлее. Но это не точно


    1. vicsoftware
      28.12.2019 05:05

      MagicScaler накидывает дополнительной резкости, из-за чего светлые объекты на темном фоне (ручейки, капли, брызги) становятся ещё светлее.


      1. a-tk
        28.12.2019 10:20

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


    1. AlexOnBeta Автор
      28.12.2019 08:17
      +1

      мне одному кажется, что картинки одинаковые?

      картинки разные: одна резче и светлее, другая более темная и размытая
      в оригинальном посте был контрол, с одной картинкой, по которому можно было поводить шторку влево-вправо, чтобы сразу видеть разницу. Я пока новичок, и такого элемента на хабре не нашел


      1. Varim
        28.12.2019 08:32
        +1

        Да, действительно, вторая картинка чётче.


    1. some_x
      28.12.2019 18:25

      Откройте их в отдельных вкладках