优化 .NET POCO 的 JSON 序列化性能

Optimizing JSON Serialization Performance of .NET POCOs

我一直在尝试优化 JSON 将要导入到 MongoDB 中的超过 500K 个 POCO 的序列化,结果 运行 只是令人头疼。我最初尝试了 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);

}

目前,将每个单独的对象序列化为 JSON 文档大约需要 0.5 - .75 秒……相当于 1000 个文档大约需要 10 分钟……10,000 个文档需要 100 分钟,等。我发现持续时间相当一致,但最终这意味着为了加载 600K 记录,将需要大约 125 小时的处理时间来执行数据加载。这是针对最终可能每天添加 20K - 100K 新文档的消息传递系统,因此性能对我们来说是一个真正的问题。

我正在序列化的对象包含几层 "navigation" 属性或 "nested documents"(取决于您是通过 ORM 还是 MongoDB 镜头查看它们)但是在其他方面不是特别复杂或值得注意。

我构造的序列化代码将在前面的代码示例中创建的 JsonTextWriter 实例传递到 POCO 的 ToJSON 函数中,因此我们不会为每个模型创建新的编写器以在序列化自身时使用。

以下代码是一些对象的截断示例,试图说明实现技术(如何传递 writer 以及如何手动构造 JSON)。还有更多属性和更多 related/nested 对象,但这是我必须进行的 "deepest" 遍历的示例。

它从 "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 时,延迟仍然存在,但时间从花在序列化上转移到花在查询上(即导航属性的加载)。

所以问题不在于序列化,而在于优化数据检索。

这是一个 benchmark comparison chart of different JSON serializer. Try ProtoBuf-net or NetJson 排名最高的候选人,它可以更快地序列化简单的 POCO。

为了结束这个问题,我想 post 我的解决方案。

经过进一步研究,原post的评论者认为是正确的。这不是序列化问题,而是数据访问问题。 ORM 是 "lazily loading" 导航属性,因为它们在序列化过程中被请求。当我实施 FetchStrategy 以 "greedily" 获取关联对象时,延迟源从我在序列化过程中设置的计数器转移到了我在数据访问中设置的计数器。

我能够通过在数据库中的外键字段上添加索引来解决这个问题。延迟下降了 90% 以上,运行 需要 100 分钟以上的时间现在可以在 10 分钟内完成。

非常感谢那些发表评论并通过提醒我还发生了什么来帮助我消除盲点的人。