Привет! Представляю вашему вниманию перевод статьи «5 Reasons You Should Stop Using System.Drawing from ASP.NET».
Ну что ж, они таки сделали это. Команда 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, который генерирует диаграмму. Он должен делать что-то вроде этого:
- Создать растровое изображение как канву
- Нарисовать несколько форм на растровом изображении используя ручки и/или кисти
- Нарисовать текст, используя один или более шрифтов
- Сохранить растровое изображение как PNG в MemoryStream
Скажем, диаграмма имеет размеры 600 на 400 точек. Это всего 240 000 точек, умноженное на 4 байта для точки для формата RGBA по умолчанию, итого 960 000 байт для растрового изображения, плюс немного для объектов рисования и буфера сохранения. Пусть будет 1мб для всего запроса. Скорее всего вы не получите проблем с памятью для такого сценария, а если с чем и столкнетесь, то скорее с ограничением на количество дескрипторов, о котором я упомянул ранее, поскольку изображения, кисти, ручки и шрифты обладают своими дескрипторами.
Реальная проблема наступит когда System.Drawing используется для задач формирования изображений. System.Drawing прежде всего графическая библиотека, а графические библиотеки как правило все строятся вокруг идеи, что всё является растровым изображением в памяти. Это прекрасно пока ты думаешь о мелочах. Но изображения могут быть реально больши́ми, и они становятся больше каждый день, т.к. камеры с большим количеством мегапикселей постоянно дешевеют.
Если вы примете наивный подход System.Drawing к построению изображений, то для обработчика изменения размера вы получите что-то вроде этого:
- Создайте растровое изображение в качестве холста для изображения-приемника.
- Загрузите исходное изображение в еще одно растровое изображение.
- Вызовите DrawImage() с параметром «изображение-источник» для изображения-приемника, с применением изменения размера.
- Сохраните целевое растровое изображение в формате JPEG в поток памяти.
Предположим что целевое изображение будет иметь размеры 600х400, как и в предыдущем примере, тогда снова имеем 1Мб для целевого изображения и потока памяти. Но давайте предположим, что кто-то загрузил 24-мегапиксельное изображение от их причудливых новых зеркалок, тогда нам необходимо 6000x4000 точек с 3 байтами для каждой (72мб) для декодированного исходного растрового изображения в формате RGB. И будем использовать ресемплинг HighQualityBicubic из System.Drawing, потому как он единственный выглядит хорошо. Тогда нам нужно учесть другие 6000x4000 точек с 4 байтами на каждую, для PRGBA-конверсии которая происходит внутри вызываемого метода, добавляя дополнительные 96мб используемой памяти. Итого получается 169мб (!) для запроса на преобразование одного изображения.
Теперь представим, что у вас не один пользователь делает такие штуки. Теперь вспомним, что запросы заблокируются пока все они полностью не выполнятся. Сколько нужно времени, чтобы у вас кончилась память? И даже если вы не беспокоитесь, что полностью исчерпаете всю доступную, помните, что есть много способов лучше использовать память вашего сервера, чем удерживать кучу пикселей. Рассмотрим влияние давления памяти на другие части приложения/системы:
- Кэш ASP.NET может начать сбрасывать элементы, которые дорого воссоздать
- Сборщик мусора будет запускаться чаще, замедляя работу приложения
- Кэш ядра IIS или кэш файловой системы Windows может удалить полезные элементы
- Пул приложений может превысить установленный лимит памяти и может быть перезапущен
- Windows может начать подкачку памяти на диск, замедляя работу всей системы
Вы же действительно не хотите ни чего из этого?
Библиотеки разработанные специально для задач обработки изображений подходят к этой проблеме совсем по другому. У них нет необходимости загружать исходное или целевое изображение целиком в память. Если вы не собираетесь рисовать на нем, вам не нужна канва/растровое изображение. Это делается скорее так:
- Создаете поток для JPEG-кодировщика целевого изображения
- Загружаете одну линию из исходного изображения и сжимаете ее по горизонтали
- Повторяете столько раз сколько нужно для формирования одной линии для целевого файла
- Сжимаете получившиеся линии вертикально
- Повторяете с шага 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:
MagicScaler:
Оглянитесь вокруг, исследуйте альтернативы, и пожалуйста, во имя любви к котятам, прекратите использовать System.Drawing в ASP.NET
Автор: AlexOnBeta