Оптимизация производительности сериализации JSON для.NET POCO

Я пытался оптимизировать JSON-сериализацию более 500 тыс. POCO для импорта в MongoDB, и у меня не было ничего, кроме головной боли. Первоначально я пытался использовать функцию Newtonsoft Json.Convert(), но это заняло слишком много времени. Затем, основываясь на рекомендациях нескольких постов здесь на SO, на собственном сайте Newtonsoft и в других местах, я попытался вручную сериализовать объекты. Но не заметил много, если какой-либо прирост производительности.

Это код, который я использую для запуска процесса сериализации... Над каждой строкой в ​​комментариях указывается количество времени, которое потребовалось для выполнения каждой отдельной операции, учитывая набор данных из 1000 объектов.

//
// Get reference to the MongoDB Collection
var collection = _database.GetCollection<BsonDocument>("sessions");
//
// 8ms - Get the number of records already in the MongoDB. We will skip this many when retrieving more records from the RDBMS
Int32 skipCount = collection.AsQueryable().Count();
//
// 74ms - Get the records as POCO's that will be imported into the MongoDB (using Telerik OpenAcces ORM)
List<Session> sessions = uow.DbContext.Sessions.Skip(skipCount).Take(1000).ToList();

//
// The duration times displayed in the foreach loop are the cumulation of the time spent on 
// ALL the items and not just a single one.
foreach (Session item in sessions)
{
    StringWriter sw       = new StringWriter();         
    JsonTextWriter writer = new JsonTextWriter(sw);     
    //
    // 585,934ms (yes - 9.75 MINUTES) - Serialization of 1000 POCOs into a JSON string. Total duration of ALL 1000 objects 
    item.ToJSON(ref writer);
    //
    // 16ms - Parse the StringWriter into a String. Total duration of ALL 1000 objects.
    String json = sw.ToString();
    //
    // 376ms - Deserialize the json into MongoDB BsonDocument instances. Total duration of ALL 1000 objects.
    BsonDocument doc = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>(json); // 376ms

    //
    // 8ms - Insert the BsonDocument into the MongoDB dataStore. Total duration of ALL 1000 objects.
    collection.InsertOne(doc);

}

В настоящее время это занимает около 0,5 - 0,75 секунд для каждого отдельного объекта, который будет сериализован в документ JSON... что составляет около 10 минут для 1000 документов... 100 минут для 10000 документов и т. Д. Я считаю, что длительности довольно согласованно, но в конечном итоге это означает, что для загрузки записей 600 КБ потребуется около 125 часов подряд обработки данных. Это для системы обмена сообщениями, которая может в конечном итоге добавлять 20–100 тыс. Новых документов в день, поэтому производительность для нас - РЕАЛЬНАЯ проблема.

Объект (ы), которые я сериализирую, содержат пару слоев "навигационных" свойств или "вложенных документов" (в зависимости от того, просматриваете ли вы их через линзу ORM или MongoDB), но в остальном они не особенно сложны или заслуживают внимания.

Построенный мной код сериализации передает экземпляр JsonTextWriter, созданный в предыдущем примере кода, в функции ToJSON в POCO, поэтому мы не создаем новые средства записи для каждой модели, которые будут использоваться при сериализации.

Следующий код является усеченным примером нескольких объектов в попытке проиллюстрировать методику реализации (как передается писатель и как JSON создается вручную). Есть еще много свойств и несколько связанных / вложенных объектов, но это пример самого "глубокого" обхода, который я должен сделать.

Он начинается с объекта "Session" и рекурсивно вызывает его зависимые свойства, чтобы также сериализовать себя.

public class Session
{

    #region properties

    public Guid SessionUID { get; set; }

    public String AssetNumber { get; set; }

    public Int64? UTCOffset { get; set; }

    public DateTime? StartUTCTimestamp { get; set; }

    public DateTime? StartTimestamp { get; set; }

    public DateTime? EndTimestamp { get; set; }

    public String Language { get; set; }

    // ... many more properties 

    #endregion properties 

    #region navigation properties

    public virtual IList<SessionItem> Items { get; set; }

    #endregion navigation properties

    #region methods
    public void ToJSON(ref JsonTextWriter writer)
    {
        Session session = this;     
        // {
        writer.WriteStartObject();

        writer.WritePropertyName("SessionUID");
        writer.WriteValue(session.SessionUID);

        writer.WritePropertyName("AssetNumber");
        writer.WriteValue(session.AssetNumber);

        writer.WritePropertyName("UTCOffset");
        writer.WriteValue(session.UTCOffset);

        writer.WritePropertyName("StartUTCTimestamp");
        writer.WriteValue(session.StartUTCTimestamp);

        writer.WritePropertyName("StartTimestamp");
        writer.WriteValue(session.StartTimestamp);

        writer.WritePropertyName("EndTimestamp");
        writer.WriteValue(session.EndTimestamp);

        writer.WritePropertyName("Language");
        writer.WriteValue(session.Language);

        // continues adding remaining instance properties

        #endregion write out the properties

        #region include the navigation properties

        // "Items": [ {}, {}, {} ]
        writer.WritePropertyName("Items");
        writer.WriteStartArray();
        foreach (SessionItem item in this.Items)
        {
            item.ToJSON(ref writer);
        }
        writer.WriteEndArray();

        #endregion include the navigation properties

        // }
        writer.WriteEndObject();
        //return sw.ToString();
    }

    #endregion methods 
}

public class SessionItem
{
    #region properties

    public Int64 ID { get; set; }

    public Int64 SessionID { get; set; }

    public Int32 Quantity { get; set; }

    public Decimal UnitPrice { get; set; }

    #endregion properties

    #region navigation properties

    public virtual Session Session { get; set; }

    public virtual IList<SessionItemAttribute> Attributes { get; set; }

    #endregion navigation properties

    #region public methods
    public void ToJSON(ref JsonTextWriter writer)
    {
        // {
        writer.WriteStartObject();

        #region write out the properties

        writer.WritePropertyName("ID");
        writer.WriteValue(this.ID);

        writer.WritePropertyName("SessionID");
        writer.WriteValue(this.SessionID);

        writer.WritePropertyName("Quantity");
        writer.WriteValue(this.Quantity);

        writer.WritePropertyName("UnitPrice");
        writer.WriteValue(this.UnitPrice);

        #endregion write out the properties

        #region include the navigation properties
        //
        // "Attributes": [ {}, {}, {} ]
        writer.WritePropertyName("Attributes");
        writer.WriteStartArray();
        foreach (SessionItemAttribute item in this.Attributes)
        {
            item.ToJSON(ref writer);
        }
        writer.WriteEndArray();

        #endregion include the navigation properties

        // }
        writer.WriteEndObject();
        //return sw.ToString();
    }
    #endregion public methods
}

public class SessionItemAttribute : BModelBase, ISingleID
{
    public Int64 ID { get; set; }

    public String Name { get; set; }

    public String Datatype { get; set; }

    public String Value { get; set; }

    #region navigation properties

    public Int64 ItemID { get; set; }
    public virtual SessionItem Item { get; set; }

    public Int64 ItemAttributeID { get; set; }
    public virtual ItemAttribute ItemAttribute { get; set; }

    #endregion navigation properties

    #region public methods
    public void ToJSON(ref JsonTextWriter writer)
    {
        // {
        writer.WriteStartObject();

        #region write out the properties

        writer.WritePropertyName("ID");
        writer.WriteValue(this.ID);

        writer.WritePropertyName("Name");
        writer.WriteValue(this.Name);

        writer.WritePropertyName("Datatype");
        writer.WriteValue(this.Datatype);

        writer.WritePropertyName("StringValue");
        writer.WriteValue(this.StringValue);

        writer.WritePropertyName("NumberValue");
        writer.WriteValue(this.NumberValue);

        writer.WritePropertyName("DateValue");
        writer.WriteValue(this.DateValue);

        writer.WritePropertyName("BooleanValue");
        writer.WriteValue(this.BooleanValue);

        writer.WritePropertyName("ItemID");
        writer.WriteValue(this.ItemID);

        writer.WritePropertyName("ItemAttributeID");
        writer.WriteValue(this.ItemAttributeID);

        #endregion write out the properties

        // }
        writer.WriteEndObject();
        //return sw.ToString();
    }
    #endregion public methods
}

Я подозреваю, что я что-то упускаю из виду или проблема заключается в том, как я реализую сериализацию. Один SO-постер утверждал, что сократил время загрузки с 28 секунд до 31 миллисекунды, вручную сериализовав данные, поэтому я ожидал несколько более впечатляющих результатов. Фактически, это почти та же производительность, которую я наблюдал, используя метод Newtonsoft Json.Convert().

Любая помощь в диагностике источника задержки в сериализации была бы наиболее ценной. Спасибо!

ОБНОВИТЬ

Хотя я еще не получил доступ к данным из ORM, я смог подтвердить, что задержка действительно исходит от ORM (спасибо, комментаторы). Когда я добавил FetchStrategy, как предполагалось, задержка все еще была, но время перешло от затрат на сериализацию к расходам на запрос (т. Е. Загрузке свойств навигации).

Таким образом, проблема не в сериализации, а в оптимизации поиска данных.

2 ответа

Решение

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

После дальнейших исследований комментаторы исходного поста сделали это правильно. Это была не проблема сериализации, а проблема доступа к данным. ORM "лениво загружал" навигационные свойства, так как они запрашивались в процессе сериализации. Когда я реализовал FetchStrategy для "жадного" извлечения связанных объектов, источник задержки сместился с счетчиков, которые были у меня на месте в процессе сериализации, на счетчики, которые я поместил вокруг доступа к данным.

Я смог решить эту проблему, добавив индексы для полей внешнего ключа в базе данных. Задержка упала более чем на 90%, и то, что требовалось более 100 минут, теперь завершается за 10.

Так что спасибо людям, которые прокомментировали и помогли убрать мои шоры, напомнив мне о том, что еще происходило.

Вот сравнительная таблица сравнения различных сериализаторов JSON. Попробуйте ProtoBuf-net или NetJson, кандидат наивысшего рейтинга для более быстрой сериализации для простых POCO.

Другие вопросы по тегам