Оптимизация производительности сериализации 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.