技不如人

Welcome to Rickel's blog.
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

ADO.NET 1.x Dataset 序列化問題探討

Posted on 2005-05-18 10:16  Rickel  阅读(664)  评论(0编辑  收藏  举报

作者:李匡正 (台灣微軟應用架構技術經理)

2004 年 12 月

企業用戶鮮少在一項技術處於萌芽階段便急於導入,近年來隨著 Microsoft .NET 進入成長期,許多於近日才接觸到 .NET 的開發人員,往往驚覺許多過去熟知的技術,已成為過往雲煙,這是軟體行業辛苦之處,也是其迷人之處。一個具體的例子,過去熟悉運用 Remote Data Services (RDS) 開發多層資料庫應用系統的開發人員,在進入 .NET 平台後常見的共通問題便是 - RDS 到哪兒去了? 也許您不樂於見到此訊息,曾幾何時 RDS 已被列在 Microsoft Data Access Components (MDAC) 2.8 內應避免使用 (deprecated component) 之元件 (http://msdn.microsoft.com/library/en-us/mdacsdk/htm/mdac_deprecated_components.asp), 這也意味著未來的 MDAC 版本中 RDS 可能會消失。

微軟 Patterns & Practices 系列中最重要的一本書 - .NET 的應用程式架構:設計應用程式和服務 (http://www.microsoft.com/taiwan/msdn/books/apparch/default.htm) 中勾勒出 [圖一] 所示 .NET 應用程式與服務設計架構全貌與藍圖。由此藍圖中我們可以了解,在 .NET 環境中支援 XML Web Services,.NET Remoting,Enterprises Services 與 Message Queue 等多種方式實作服務介面,因此我們可以依據實際需求,在 .NET 所支援之多樣化分散式技術中擇ㄧ實作服務介面,並搭配 ADO.NET 與序列化 (Serialization) 技術,將序列化後的資料於網路中自由傳遞,以取代過去您所熟悉的 RDS。運用 .NET 平台技術替換 RDS 背後其實隱藏了許多潛在的問題,首先我們需要面對的便是效能問題。

Web Services 相關技術是目前熱門的顯學,取代 RDS 的資料傳輸協定您第一個會想到的便是 SOAP,以開放之 XML 格式表達 AOD.NET Dataset,並運用 .NET 對於 SOAP 完整支援能力,您不需要花費很多工夫,便能完成一個替代 RDS 的解決方案,之後您可能會發現執行效能不如預期,相較於過去 RDS 表現差距頗多,當資料量越大時效能表現越差。問題出在何處呢?

圖 1
圖 1

首先 ADO .NET Dataset 是一個相當複雜的物件,由 [圖二] 中我們可以了解;ㄧ個 AOD .NET Dataset 可能包含多個 DataTable 以及各 DataTable 間關聯性,幾乎可以表達一個離線、完整之資料庫,此外 Dataset 中可能保存了離線時異動資訊,並且 SOAP 訊息中加註大量 XML 標籤  (Tag),一個簡單的 Dataset 轉換為 SOAP 訊息後資料量膨脹十餘倍並非奇事,如此巨量的資料在網路間傳遞,效能不如 RDS 原因便顯而易見了。MSDN Library 於 2003 年 2 月份有篇文章,運用簡單的類別 (Class) 封裝 DataTable 內單筆記錄 (Record) 將複雜 Dataset 改由單純的 Array 取而代之,如此可大幅縮減 SOAP訊息資料量(http://msdn.microsoft.com/library/en-us/dnservice/html/service02112003.asp) ,此法兼顧效能與 Web Services 開放性,但卻苦了開發人員,在微軟物件關聯對映 (OR-Mapping) Framework - Object Space 未推出前,.NET Framework 中並未內建相關功能,開發人員需自行撰寫將記錄 (Record) 與物件 (Object) 間轉換的煩瑣程式碼,並且也不易實作表達 DataTable 間的關連性與限制條件,資料傳遞雖達到最佳化目的,但 ADO.NET 離線處理處理資料能力幾乎喪失殆盡。

圖 2
圖 2

Microsoft .NET Framework 1.x 中內建兩種將物件續列化的 Formatter 類別, 分別是 SoapFormatter 與 BinaryFormatter,聰明的您或許已經想到,可以將 ADO.NET Dataset 以二進制方式 ( Binary) 序列化,以便縮減Dataset 序列化後的大小,輔以搭配 .NET Remoting 傳送,理論上可將效能大幅提升。倘若您實際測試後,可能發現事實上並非如此,我們嘗試運用 [程式碼一] 將名為 ds 的 Dataset 分別透過兩種序列化方式儲存 Dataset 狀態於檔案中

// 將 DataSet - ds 寫入 Binary Stream

BinaryFormatter bf = new BinaryFormatter ();
StreamWriter swDat = new StreamWriter ("output_standard_dataset.dat");
bf.Serialize(swDat.BaseStream, ds);
swDat.Close();

// 將 DataSet - ds 寫入 SOAP XML Stream

SoapFormatter sf = new SoapFormatter ();
swDat = new StreamWriter ("output_standard_dataset.xml");
sf.Serialize (swDat.BaseStream,ds);
swDat.Close();

程式碼一

為了模擬大量資料與包含多 DataTable 之 Dataset,我們透過 Microsoft SQL Server 範例資料庫 Northwind 將全部內容置於 Dataset,當程式執行完畢後我們得到 [表格一] 的結果,

  SoapFormatter  BinaryFormatter 
Dataset 序列化後 Bytes 數  1,953,078 1,448,399 
表格一

這個結果令人失望,藉由 BinaryFormatter 序列化後的 Dataset 仍有 1.4 M Bytes,與 SoapFormatter 序列化的結果 1.9 M Bytes 相差無幾,無助於透過 .NET Remoting 傳遞,MSDN 雜誌專欄作家 Dino Esposito 於 2002 年 12 月份專欄中解釋了此一現象的原因 (http://msdn.microsoft.com/msdnmag/issues/02/12/CuttingEdge/),由於 ADO .NET 1.x Dataset 在實作序列化中取得物件方法 GetObjectData 程式如 [程式碼二] 所示:

void GetObjectData(SerializationInfo info, StreamingContext context)
{
   info.AddValue("XmlSchema", this.GetXmlSchema());
   this.WriteXml(strWriter, XmlWriteMode.DiffGram);
   info.AddValue("XmlDiffGram", strWriter.ToString());
}

程式碼二

因此我們可以知悉,Dataset 在做序列化動作之前已經將其全部內容事先轉換為 XML,這也解釋了為什麼運用 BinaryFormatter 也無法縮減序列化後檔案大小的原因了,因為在序列化之前資料已經是膨脹後的 XML,BinaryFormatter 將膨脹後的物件內容再加以序列化,無助於減少傳輸的資料量。Dino Esposito 於該期專欄中提及多種解決方法,其中一種方式便是撰寫一個繼承自 Dataset 或 DataTable,並重新實作 ISerializable 介面,以便自行控制 GetObjectData 內序列化動作,台灣知名 .NET 作家黃忠成先生於 2003 年 Run PC! 雜誌專欄中 (http://www.dreams.idv.tw/~code6421/Doc/Remoting_2.pdf) 依照 Dino Esposito 想法實作了一個 DataTable 版本,而 DataSet 實作版本由於過於複雜,並未在雜誌中刊登。看到此處,熟悉 RDS 的朋友可能會感到沮喪,世界為什麼變的這麼複雜?微軟研發團隊難道不知道這個問題嗎?微軟到底如何解決此問題?

2004 年 8 月微軟技術支援網站終於公佈(http://support.microsoft.com/?kbid=829740) 解決方案,Knowledge Base article 829740 ”Improving Dataset Serialization and Remoting Performance” 內公佈了一個可以直接使用、包含完整範例的 DataSetSurrogate 類別,這個類別運用常見的 Surrogate Pattern (Proxy Pattern) 簡單將 Dataset 內全部內容轉換為 Array 並封裝於 DataSetSurrogate 類別之內,並且巧妙而簡單透過標註 [Serializable] Attribute 而不完整實作 ISerializable 方式,來達到完全掌控續列化效果。而 Dino Esposito 先生於 2004 年 10 月號 MSDN 雜誌專欄中也更新了自己兩年前類似主題之專欄內容 (http://msdn.microsoft.com/msdnmag/issues/04/10/CuttingEdge/), 並將 Knowledge Base article 829740 內的實作方式介紹給 MSDN 雜誌廣大讀者群。我們利用 DataSetSurrogate 類別重新測試兩種序列化 Formatter 並將結果與標準 Dataset 序列化結果做比較,可得到[表格二]測試結果。

  SoapFormatter BinaryFormatter
Dataset 序列化後Bytes數 1,953,078 1,448,399
DataSetSurrogate 序列化後Bytes數 2,371,942 575,684
表格二

針對 BinaryFormatter 所得到結果已經令人滿意了,裝載整個 Northwind 資料庫的 DataSetSurrogate 類別在序列化後已經降到約 576 K Bytes 的大小,如此結果無論透過 .NET Remoting、Enterprise Services 或是 Message Queue 傳遞皆可得到與過去 RDS 近似之效能表現,當然經過層層包裝的 DataSetSurrogate 類別比起 Dataset 更為複雜,因此以 SoapFormatter 序列化為 XML 型態後,其大小比起 Dataset 更為龐大了。針對效能最佳化的問題,預計於 2005 年推出的 .NET Framework 2.0 中終將獲得解決,在 AOD.NET 2.0 中,Dataset 與 DataTable 皆增添了一個新屬性 RemotingFormat,只需將此屬性設定為 SerializationFormat.Binary,如 [程式碼三] 的作法即可輕鬆以 BinaryFormatter 將 ADO.NET 2.0 Dataset 內容以精簡之二進制格式序列化,毋需使用額外輔助的 DataSetSurrogate 類別了。

ds.RemotingFormat = SerializationFormat.Binary;
BinaryFormatter bf = new BinaryFormatter ();
StreamWriter swDat = new StreamWriter ("output_dataset.dat");
bf.Serialize(swDat.BaseStream, ds);
swDat.Close();

程式碼三

無論 Client/Server 架構、多層式架構或是 Services Oriented Architecture,避免非必要、巨量之 Dataset 於網路間傳遞都是設計重要考量,運用二進制方式序列化將傳送之 Dataset 最佳化,是治標而非治本之解決方法,條件過於寬鬆之 SQL 查詢語法不僅造成伺服器端沉重負荷,也連帶影響網路頻寬運用,慎重評估巨型 Dataset 存在的必要性,解決問題發生之根源方為上策。

範例程式下載:http://download.microsoft.com/download/7/1/3/71374b65-172d-45d7-8056-418551480fda/NETColumn_Demo1.zip