2011-12-12

protobuf-net на практике

Не так давно я участвовал в оптимизации работы одного ASP.NET MVC веб-приложения. В частности, занимался улучшением производительности подсистемы кеширования. Сам проект использует Entity Framework 4.1, а означенная подсистема позволяет кешировать результаты отдельных LINQ to Entities запросов в memcached. Результат запроса ложится в кеш как массив сущностей (если точнее - массив экземпляров EF proxies) в сериализованном виде, при взятии из кеша его надо, соответственно, десериализовать.

Для (де)сериализации в приложении использовался старый добрый BinaryFormatter, производительность которого оставляла желать лучшего. Вообще говоря, приложение агрессивно использует кеш, поэтому медлительность BinaryFormatter-а была весьма заметна. Захотелось использовать более производительную библиотеку сериализации, желательно еще и генерирующую footprint поменьше. Да еще и такую, чтобы прикрутить было попроще. Поиск дал результаты, советующие взглянуть на protobuf-net, и в итоге мы стали использовать эту библиотеку в нашем проекте.

Краткое описание

protobuf-net это (наиболее живая) .NET реализация Protocol Buffers — кросс-платформенного протокола для обмена двоичными сообщениями от Google. Одна из особенностей этого протокола - уменьшение размера результирующего двоичного потока за счёт уменьшения избыточности кодирования (сжатия там нет, но вот трюки вроде Base 128 Varints есть) и уменьшения количества сериализуемой информации о типах (в контракте сериализуемого типа жёстко задаётся порядок полей, так что хранить имя поля и его тип нет необходимости).

В теории получается, что protobuf-net генерирует сериализованный поток меньшего размера и, значит, может (де)сериализовать быстрее, чем BinaryFormatter. Но для сериализации типа через protobuf-net контракт этого типа должен быть предварительно зарегистрирован — сериализовать любой [Serializable] класс «из коробки» protobuf-net не умеет.

Практика использования

protobuf-net можно или скачать с официального сайта, или поставить через NuGet. Классический пример использования protobuf-net выглядит примерно так:

У этого кода есть две особенности, которые могут сделать переход на protobuf-net неприятным:

  1. Для сериализуемого типа явно (с помощью атрибутов) указан контракт — какие поля/свойства необходимо сериализовать и в каком порядке. К сожалению, в существующем коде может быть много сериализуемых типов, и прописывать новые атрибуты для всех из них может быть тяжело/неудобно/невозможно.
  2. Необходимо явно указывать десериализуемый тип. Проблема тут в том, что на практике в момент десериализации он не всегда известен.

Добавления [Proto*] атрибутов к существующим классам можно избежать, задав контракт во внешнем .proto файле, используя атрибуты [DataContract]/[DataMember] или задавая контракт в рантайме через классы TypeModel/RuntimeTypeModel. Для существующих классов последний способ может оказаться наиболее полезным. Вот пример кода, который создаёт контракты для всех сериализуемых типов из текущей сборки:

Конечно, это только пример, и этот код придётся адаптировать под свои нужды — например, сериализовать все public properties или выбирать типы по пространству имён. А вот для проблемы десериализации неизвестного типа есть «официальное» решение — сначала сериализовать имя типа, а потом уже сам объект; соответствующий код можно подсмотреть в файле ProtoTranscoder.cs из SVN-репозитория protobuf-net.

Производительность

Для нашего проекта использование protobuf-net улучшило производительность (де)сериализации примерно в 2.5-3 раза (уменьшение размера сериализованного потока было для нас не так важно) — очень хороший результат, учитывая малые затраты на изменение кода. Вообще в интернетах есть несколько синтетических тестов с участием protobuf-net.

Интереса ради я написал и свой тест для сравнения protobuf-net с BinaryFormatter, код которого можно забрать на GitHub. В моём тесте:

  • Сериализуется граф из 90 объектов (сущности 3-х типов).
  • Граф содержит циклические ссылки.
  • Для protobuf-net сериализуется информация о корневом типе.

Результаты теста, скомпилированного в Release, на моей машине:

Особенности и подводные камни

Используя protobuf-net, надо помнить о некоторых особенностях Protocol Buffers:

  • Protocol Buffers разработан для сериализации деревьев объектов т.е. не поддерживает циклические ссылки и при сериализации сохраняет объект, на который есть ссылки из нескольких полей, как несколько независимых объектов. protobuf-net позволяет обходить эти ограничения и хранить объекты по ссылке, для этого нужно указать AsReference = true в метаданных сериализуемого поля.
  • Protocol Buffers должен заранее знать о типах всех сериализуемых объектов и наследованиях между ними — эта информация должна явно указываться в контракте. Опять же, protobuf-net позволяет включить сериализацию информации о типе для поля, если в метаданных поля указать DynamicType = true.
  • protobuf-net специальным образом обрабатывает прокси-типы, генерируемые NHibernate и Entity Framework-ом — если контракт прокси-типа не зарегистрирован, используется контракт его базового типа.
  • Можно контролировать информацию о типе, сериализуемую для DynamicType полей, обрабатывая событие TypeModel.DynamicTypeFormatting. Например, в проекте, над которым я работал, этот обработчик использовался для регистрации контрактов неизвестных типов и сериализации EF прокси-объектов как объектов базового типа.

Это то, с чем я лично столкнулся, наверняка есть и другие детали работы библиотеки. Кроме того, в protobuf-net есть и баги — например, пустой массив может десериализоваться как null (я специальным образом обрабатываю этот случай в коде).

С документацией по protobuf-net дела обстоят не супер — хороших статей я не нашёл, пользовался такими источниками информации:

И, конечно, очень помог код, потому что open source.

Комментариев нет:

Отправить комментарий