Статьи

Важная проблема производительности WCF + обходной путь

Я написал о ФОС производительности вопросов , прежде чем , но это, кажется, является самым крупным. Валерий опубликовал на форуме Wcf интересную тему производительности. Короче говоря, клиент WCF пытается использовать не-WCF-сервис, где контракт выглядит примерно так:

class Foo
{
   byte[] picture;
}

В мыле байтовые массивы кодируются как строки base64, поэтому это может выглядеть так:

<picture>/9j/4AAQSkZJReV6R8MLi7nW6UUUViWf/Z.....</picture>

или с переносами строк после каждых 73 символов, например так:

<picture>/9j/4AAQSkZJReV6R8MLi7nW61+58zBz5Q+7Xpdj
/PK/4AAQSkPOIeV6R8MLi7nW61+58zBz5Q+7Xpdj
/9R/4AAQSkZJReV6R8MLi7nW6VZ788zBz5Q+7Xpdj
4U4wVoqwUUUViWf/Z</picture>

оба варианта действительны в соответствии с 
base64 RFC :


Реализации НЕ ДОЛЖНЫ добавлять перевод строки в данные с базовым кодированием, если только спецификация, ссылающаяся на этот документ, явно не указывает базовым кодировщикам добавлять перевод строки после определенного количества символов.

Хорошо, так что на самом деле это не защищает … Но это факт, что многие мыльные стеки все еще используют этот формат, созданный MIME, а также Wcf поддерживает его.

Так в чем проблема?
Кажется, что когда Wcf получает сообщение, которое содержит base64 с CRLF, обработка замедляется за несколько секунд (!). Детализация показывает, что проблема в сериализаторе DataContract. Взгляните на эту программу:

[DataContract]
public class Foo
{
    [DataMember]
    public byte[] picture;
}

class Program
{
        static void Main(string[] args)
        {
            var t1 = getTime(@"C:\temp\base64_with_line_breaks.txt");
            var t2 = getTime(@"C:\temp\base64_without_line_breaks.txt");            
          
            Console.WriteLine("Time with breaks: " + t1);
            Console.WriteLine("Time with no breaks: " + t2);

            Console.ReadKey();
        }

        static double getTime(string path)
        {
            var ser = new DataContractSerializer(typeof (Foo));
            var stream = new FileStream(path, FileMode.Open);
            var start = DateTime.Now;

            for (int i = 0; i < 40; i++)
            {
                ser.ReadObject(stream);                
                stream.Position = 0;
            }

            var end = DateTime.Now;
            var t = end - start;
            return t.TotalSeconds;
        }
} 

Для тех из вас, кто заинтересован в этом, файлы находятся здесь и здесь .

Вывод:
Время с перерывами: 10,8998196 секунд.
Время без перерывов: 0,0029994 секунды.

Это явно выявляет проблему производительности.

Почему это происходит?

При отладке исходного кода .Net я нашел это в классе XmlBaseReader (комментарии кода были в источнике — они не мои):

int ReadBytes(...)
{
  try
 {
   ...
 }

 catch (FormatException exception)
  {
      // Something was wrong with the format, see if we can strip the spaces

      int i = 0;
      int j = 0;
      while (true)
      {
          while (j < charCount && XmlConverter.IsWhitespace(chars[j]))
              j++;
          if (j == charCount)
               break;
          chars[i++] = chars[j++];
      }
...
}
}

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

Заметки:

  • Это происходит как с .Net Framework 3.5, так и с 4.0.
  • Это специфическая проблема DataContract — она ​​не возникает, когда вы используете другие механизмы .Net, такие как Convert.FromBase64String.

    Я сообщал об этом в Microsoft Connect, вы можете проголосовать за эту проблему .

    Обходные пути

    Там несколько обходных путей. Компромиссами обычно являются удобство API (или «где вы предпочитаете помещать« грязные вещи »)

    1.  Как заметил Валерий , вы можете изменить контракт, чтобы использовать String вместо byte []. Затем Convert.FromBase64String выдаст вам байтовый массив.

    2.Измените ваши контракты, чтобы использовать XmlSerializer вместо сериализатора DataContract. Бывший не испытывает эту проблему. XmlSerializer, как правило, медленнее (когда base64 не кажется, что он), так что это то, что вы потеряете. Здесь вы получите лучший API, поскольку клиентам не нужно манипулировать строкой base64.

    3. Лучше всего, конечно, изменить реализацию службы, чтобы она возвращала base64 без разрывов строки. Также, если в любом случае возвращаются большие двоичные файлы, лучше использовать MTOM.

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