Pull to refresh

XmlSerializer — Assembly Leak без спроса :)

Reading time 5 min
Views 5.7K

Коротко о главном


В некоторых частях .NET Framework, таких как XmlSerializer, используется внутреннее динамическое создание кода.XmlSerializer создает временные файлы C#, компилирует результирующие файлы во временную сборку и затем загружает эту сборку в процесс. Такое создание кода тоже стоит сравнительно дорого, поэтому XmlSerializer размещает временные сборки в кэш, по одной на каждый тип. Это значит, что в следующий раз при создании кода XmlSerializer для класса Х не будет создаваться новая сборка, а будет использована сборка из кэша. Однако, не все так просто.

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

Локализация проблемы


Для начала расскажу о системе, которая построена нашей командой.

Приложение состоит из трех частей – веб-сайта, хранилища данных и бизнес-центра.
Вся система построена на .net 3.5.
Веб-сайт позволяет запускать проверки данных на бизнес-сервисе, который работает на Windows Workflow Foundation. Каждый workflow должен получать некие данные (для этого он общается к persistence сервису).

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

Например, приложение, которое запускает workflow-ы, которое, в общем, с ними работает (WCF-сервис), начинало при нагрузке съедать до 2.5 гигабайт памяти.

Проблему с утечкой памяти мы решили, о ней немного попозже напишу, так как сейчас под рукой нужных данных нету.

После решения проблемы процесс с приложением забирал до 500 Мб, а иногда — до 800 Мб. Мы прекрасно знали, что это не предел, что раньше работоспособность пропадала при 2 гигабайтах. Однако, приложение даже с таким объемом после некоторого времени начинало работать заметно медленнее. После некоторых наблюдений мы заметили, что иногда запускается C# компилятор csc.exe, который, в принципе, должен у нас в системе запускаться только при первом запросе workflow, а при следующих — брать уже готовую сборку.

Подумав еще немного, мы решили посмотреть на количество сборок в процессе. :)

И тут нас ждало удивление — сразу же после запуска приложения в основной домен было загружено около 100 сборок, однако со временем их количество доходило до 3000, а позже — до 5000. И вот уже при 4-5 тысячах сборок было заметно замедление работы.

Написав утилиту, которая позволяет просмотреть домены и загруженные в них сборки в любом приложении .net на ходу, мы увидели, что те 100 начальных сборок остаются. И постоянно добавляются лишь какие-то анонимные сборки. К сожалению, получить более детальную информацию (какие типы объявлены в сборке) в чужом процессе нам не удалось.

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

В общем, оказалось что «анонимные» сборки — это сборки, создаваемые XmlSerializer-ом для сериализации. И все они одинаковые :)

Преставляете, вы 1000 раз сериализуете один и тот же класс. И у вас ужасно тормозит приложение и, более того, у вас утекает память…

Не, ну что… Это же .net. Там ведь есть GC. Он ведь занимается памятью.

Собственно, проблема


Перейдем теперь к деталям. XmlSerializer в .net способен вызывать assembly leak (а assembly leak перетекает в memory leak). Не всегда, конечно же. У этого класса есть несколько конструкторов.

Если вы пользуетесь обычным конструктором, который принимает Type, то утечки памяти нету:

namespace XmlSerializerMemoryLeak
{
 class Program
 {
  private static XmlSerializer serial = null;

  static void Main(string[] args)
  {
   for (int index = 0; index < 10000; index++)
   {
    TestClass test = new TestClass();
    test.Id = index;
    test.Date = DateTime.Now;
    StringBuilder builder = new StringBuilder();
    StringWriter writer = new StringWriter(builder);
    serial = new XmlSerializer(typeof(TestClass));
    serial.Serialize(writer, test);
    string xml = builder.ToString();
   }
   Console.WriteLine(«Done»);
  }
 }

 public class TestClass
 {
  public DateTime Date { get; set; }
  public int Id { get; set; }
 }
}
* This source code was highlighted with Source Code Highlighter.


Однако, если использовать несколько другой конструктор, то утечка памяти гарантирована:

namespace XmlSerializerMemoryLeak
{
 class Program
 {
  private static XmlSerializer serial = null;
  static void Main(string[] args)
  {
   Console.ReadLine();
   for (int index = 0; index < 100000; index++)
   {
    TestClass test = new TestClass();
    test.Id = index;
    test.Date = DateTime.Now;
    StringBuilder builder = new StringBuilder();
    StringWriter writer = new StringWriter(builder);
    serial = new XmlSerializer(typeof(TestClass), new XmlRootAttribute(«MemoryLeak»));
    serial.Serialize(writer, test);
    string xml = builder.ToString();
   }
   Console.WriteLine(«Done»);
  }
 }

 public class TestClass
 {
  public DateTime Date { get; set; }
  public int Id { get; set; }
 }
}
* This source code was highlighted with Source Code Highlighter.


Вся разница между ними видна в рефлекторе — первый (как, впрочем, и еще один — XmlSerializer(Type,String)) работает отлично. Лезет в кэш сериализаторов и смотрит, нет ли там уже готового. Нету — компилируем и добавляем в кэш.
А вот второй — совсем фигово. Никакой кэш ему не нужен. Вот поэтому он компилирует каждый раз новую сборку и вызывает assembly leak.

Пути решения


Выходов есть несколько:
  1. Использовать «правильные» конструкторы
  2. Реализовать XmlSerializerCache — который будет всегда смотреть в кэш. Можно, впринципе, не реализовывать, а посмотреть тут
  3. Не использовать сериализацию, а, к примеру, если у вас уже есть приложение, которое может заняться сериализацией (или даже именно ей и занимается), то можно отдать объект ему и получить лишь сам хml.

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

Выводы


Это не новость, данная проблема описана в MSDN Magazine, неясно только, почему ее до сих пор не исправили.

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

P.S.:


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

Кросс-пост с персонального блога
Tags:
Hubs:
+27
Comments 5
Comments Comments 5

Articles