附檔管理模組裡採用 JSON 格式保存暫存物件,將附檔物件序列化暫存成檔案,稍後寫入資料庫時再還原取出資料,直覺又方便。不料因附檔物件內含檔案內容(byte[])體積龐大,在處理極端案例時踢到記憶體不足的鐵板。
批次作業程式為 32 位元模式,依經驗記憶體上限約 1.8 GB,一開始很直覺地將資料用 JsonConvert.SerialObject() 轉成 JSON 字串再用 File.WriteAllText() 寫成檔案,之後用 File.ReadAllText() 讀取 JSON 字串,再以 JsonConvert.DeserializeObject<T>() 還原回物件:
- 寫入
File.WriteAllText(tempFileName, JsonConvert.SerializeObject(attFile)); - 讀取
var attFile = JsonConvert.DeserializeObject<AttachmentFile>(File.ReadAllText(filePath, Encoding.UTF8));
這個寫法處理 50 MB 大小的檔案不成問題,但在處理一個 86MB 檔案時(轉為 JSON 約 116MB) 冒出 OutOfMemoryException 錯誤。實測單獨讀檔並 JsonConvert.DeserializeObject() 可過關,加上其他執行邏輯消耗更多記憶體就爆了。
最簡單的解法是將程式改為 64bit 模式,在我的 16GB 機器上可用記憶空間可放大二、三倍以上。但靠加大記憶體空間鋸箭,在正式環境多人使用時效能堪慮,設法減少非必要的記憶體用量才是治本之道。
依 JSON.NET 官方建議,改用 Stream 方式可節省記憶體: (參考: Performance Tips)
To minimize memory usage and the number of objects allocated, Json.NET supports serializing and deserializing directly to a stream. Reading or writing JSON a piece at a time, instead of having the entire JSON string loaded into memory, is especially important when working with JSON documents greater than 85kb in size to avoid the JSON string ending up in the large object heap.
將讀取程式修改如下,順利通過 116MB JSON 測試:
using (var streamReader = new StreamReader(filePath))
{
using (var reader = new JsonTextReader(streamReader))
{
var serializer = new JsonSerializer();
return serializer.Deserialize<AttachmentFile>(reader);
}
}
好景不常,不久更上層樓,遇上 212MB 檔案,換成序列化為字串再寫檔的做法爆了,也得修改:
using (StreamWriter sw = new StreamWriter(tempFileName))
{
using (JsonTextWriter writer = new JsonTextWriter(sw))
{
var serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Serialize(writer, attFile);
}
}
心得: 處理大型資料物件 JSON 轉換,改用 Stream 方式讀寫檔案較節省記憶體,提升效能。
【後記】雖然改走 Stream 通過 200MB 大檔的序列化反序列化考驗,究竟我還是回頭檢討了超大附檔的必要性,最後透過 PDF 解析度調整將檔案縮小到 40MB 以下,別逼系統超越顛峰,以免翻車墜崖。