System.IO.Packagingクラスが 大量にメモリを消費し、IISがエラーを返してしまうことについて
ASP.NET2.0+VB2005でOfficeOpenXML(OOXML)仕様のExcel(SpreadsheetML)を作成しています。
OOXML対応については、.NetFramework3.0のDLL(WindowsBase.dll)を参照し、System.IO.Packagingクラスを利用しています。
発生した問題は、ある程度の大きさ(sheet.xmlファイルで170MB(50カラム、6万行))のXMLファイルを処理しようとするとサーバ(Windows2003Server、メモリ4GB、IIS6.0)がエラーを返し、処理を停止してしまう、というものです。
(小さなsheet.xmlファイルであれば、問題なく、動作が終了します。)
SpreadsheetMLの作成手順は以下の通りです。
1.データベース(oracle)からデータを読み込む。
2.XMLWriterを使用して、style.xmlファイル、sheet.xmlファイルを作成する。
3.System.IO.Packagingクラスを使用して、「xlsx」ファイルを作成する。
この3番目の処理を行っている段階で、サーバーが大量のメモリ(タスクマネージャ上で約1GB)を消費、以下のエラーを返します。
「System.ApplicationException: アクセスが拒否されました。 (HRESULT からの例外: 0x80070005 (E_ACCESSDENIED))」
処理はここで停止します。
Try~Catchを使って無理矢理継続させ xlsxファイルを作成し その中を確認すると、上記「2.」の段階で作成したsheet.xmlファイルが途中まで収納されている状態でした。
ソースの一部を以下に記述します。
01 'ファイル作成開始(メモリ)
02 Dim xmlMain As XmlDocument = New XmlDocument()
03 'workbook(メイン)
04 Dim tWorkbook As XmlElement = xmlMain.CreateElement("workbook", spreadsheetML)
05 '
06 Dim nsAttribute As XmlAttribute = xmlMain.CreateAttribute("xmlns", "r", "http:www.w3.org/2000/xmlns/")
07 nsAttribute.Value = relationSchema
08 tWorkbook.Attributes.Append(nsAttribute)
09 xmlMain.AppendChild(tWorkbook)
10 '
11 Dim tSheets As XmlElement = xmlMain.CreateElement("sheets", spreadsheetML)
12 tWorkbook.AppendChild(tSheets)
13 '
14 Dim tSheet As XmlElement = xmlMain.CreateElement("sheet", spreadsheetML)
15 '
16 tSheet.SetAttribute("name", sheetName)
17 tSheet.SetAttribute("sheetId", "1")
18 '
19 tSheet.SetAttribute("id", relationSchema, "rId1")
20 tSheets.AppendChild(tSheet)
21
22 'worksheet(外部ファイルの読み込み)
23 Dim xmlWorksheet As New XmlDocument
24 xmlWorksheet.Load(fileXmlSheetPath)
25
26 'stylesheet(外部ファイルの読み込み)
27 Dim xmlStylesheet As New XmlDocument
28 xmlStylesheet.Load(fileXmlStylePath)
29
30 ' 物理ファイル作成
31 Dim pkgOutputDoc As Package = Nothing
32 pkgOutputDoc = Package.Open(filePath, FileMode.Create, FileAccess.ReadWrite
33
34 ' 一時ファイル作成
35 Dim uriDefaultContentType As Uri = New Uri("/default.xml", UriKind.Relative)
36 Dim partTemp As PackagePart = pkgOutputDoc.CreatePart(uriDefaultContentType, "application/xml")
37
38 ' Workbook.xml
39 Dim uriStartPart As Uri = New Uri("/xl/workbook.xml", UriKind.Relative)
40 Dim partWorkbookXML As PackagePart = pkgOutputDoc.CreatePart(uriStartPart, workbookContentType)
41 Dim streamStrartPart As StreamWriter = New StreamWriter(partWorkbookXML.GetStream(FileMode.Create, FileAccess.Write))
42 xmlMain.Save(streamStrartPart)
43 streamStrartPart.Close()
44 pkgOutputDoc.Flush()
45
46 ' sheet1.xml
47 Dim uriWorksheet As Uri = New Uri("/xl/worksheets/sheet1.xml", UriKind.Relative)
48 Dim partWorksheetXML As PackagePart = pkgOutputDoc.CreatePart(uriWorksheet, worksheetContentType, CompressionOption.SuperFast)
49 Dim streamWorksheet As StreamWriter = New StreamWriter(partWorksheetXML.GetStream(FileMode.Create, FileAccess.Write))
50 xmlWorksheet.Save(streamWorksheet)
51 streamWorksheet.Close()
52 pkgOutputDoc.Flush()
サーバログで「w3wp.exe」プロセスを確認したところ、「24行目」を実行した段階で急激にメモリを消費し、どうにか処理を続けているものの、「50行目」の処理で力尽きます。
相談に乗って頂きたいのは、以下の点です。
1.この記述方法に気になる点はありますか?
(メモリ消費を起こすような記述方法でしょうか?XMLDocumentがメモリ消費をしてしまうのは認識しています。
ここまで浪費するとは思っていませんでしたが。。。)
2.そもそも DOMを使って170MBのXMLファイルを扱う、という この処理形式に間違いがあるのでしょうか?
(最終的には「~.xlsx」という名のZIP形式ファイルを作成するので、SAXでは無理だと思い込んでいます。)
3.170MBしかないXMLファイルをXMLDocumentクラスのLoadで読み込むと、その5倍近いサイズのメモリを消費するのは、仕様でしょうか?
4.もし、System.IO.Packagingの仕様である(このサイズをサーバで作成することは不可)、という場合、他にどのような技術があるのでしょうか?
以上、よろしくお願い致します。
回答
- 回答としてマークElderBoxer 2009年11月15日 12:19
- テストしてみましたが、どうもうまいことExcel2007で読めるファイルを作れなかったので案だけですが。
案1
sheet.xmlに書き出さずに、partWorksheetXML.GetStream()で得られるStreamをコンストラクタに指定したXmlTextWriterでデータ作成するのはどうでしょうか。
案2
sheet.xmlに書き出したほうが都合がいいのでしたら、XmlDocumentにわざわざ読み込んでxmlWorksheet.Save(streamWorksheet)をしなくても、sheet.xmlをFileStreamで分割読込してpartWorksheetXML.GetStreamのStreamに書き出していくのはどうでしょう。- 回答としてマークElderBoxer 2009年11月15日 12:19
「8MBあたりの壁」について とりあえずの解決ができましたので、報告致します。
ZipPackageクラスでファイル用データをメモリに作成していると、8MBを超えたあたりで、一時ファイルをHDDに書き込みに行くようです。
書き込みに行く場所が、なぜか「C:\Documents and Settings\Default User\Local Settings\Temporary Internet Files」フォルダなのです。。
Windows2003サーバは「Network Service」ユーザがIIS6.0を動かす訳ですが、、、このユーザにはデフォルトで上記ファルダへの書き込み権限がありません。そこで、権限を与えることで、ZipPackageクラスによる大きなファイルの圧縮が可能となりました。
皆様、お騒がせしました。。。
が、しかし!!!!
また疑問が出てきてしまったので、すいません、新しいスレッドを立てますので、よろしくお願い致します。
追記
一緒に調べていた方から下記のURLを頂きました。
権限関係です。
参考までに。
http://msdn.microsoft.com:80/ja-jp/library/kwzs111e(VS.80).aspx
http://support.microsoft.com/kb/812614/ja- 回答としてマークElderBoxer 2009年11月15日 12:19
- 回答としてマークElderBoxer 2009年11月15日 12:17
- 回答としてマークされていないElderBoxer 2009年11月15日 12:17
すべての返信
- 回答としてマークElderBoxer 2009年11月15日 12:19
- テストしてみましたが、どうもうまいことExcel2007で読めるファイルを作れなかったので案だけですが。
案1
sheet.xmlに書き出さずに、partWorksheetXML.GetStream()で得られるStreamをコンストラクタに指定したXmlTextWriterでデータ作成するのはどうでしょうか。
案2
sheet.xmlに書き出したほうが都合がいいのでしたら、XmlDocumentにわざわざ読み込んでxmlWorksheet.Save(streamWorksheet)をしなくても、sheet.xmlをFileStreamで分割読込してpartWorksheetXML.GetStreamのStreamに書き出していくのはどうでしょう。- 回答としてマークElderBoxer 2009年11月15日 12:19
ひらぽんさん
素早いレスポンス、ありがとうございました。
ご紹介頂いたリンク先は、私の検索能力では引き当てることができず、とても参考になりました。
DOMとSAXの違い、は、認識しておりましたが、
>一般的に、大きな XML ファイルの処理に DOM を使うと、ディスクにおける XML ドキュメント サイズの 3、4 倍に相当するメモリを消費することになります。
まさかここまで大きな差が出るとは思いませんでした。これはすなわち、私がDOMの仕様は理解できていなかった、ということです。。
メモリの消費に関しては仕様なんですね。私自身で確認した現象と一致していたので、とても納得しました。読んでいると、「DOMで読み込むとXMLの内部を検証し、ツリー構造を作成するのでメモリを消費する」とのことであり、また、どうやら これらの工程を回避できそうな雰囲気があったので、調査をしていたら、gekkaさんからのお返事を頂きましたので、、、そちらに続きます。
gekkaさん
レスポンス、ありがとうございました。
ひらぽんさんからのレスポンスから方式を検討しているときにアドバイスを頂けた形になりました。
コードの都合で、案2にて試してみたところ、メモリの増加もなく、動作させることができました。
(System.IO.ZipPackageのサンプルソースと混ぜ合わせてみました。)コードは以下の通りです。
27 Dim xmlStylesheet As New XmlDocument
28 'xmlStylesheet.Load(fileXmlStylePath) ※削除
~~
46 ' sheet1.xml
47 Dim uriWorksheet As Uri = New Uri("/xl/worksheets/sheet1.xml", UriKind.Relative)
48 Dim partWorksheetXML As PackagePart = pkgOutputDoc.CreatePart(uriWorksheet, worksheetContentType, CompressionOption.SuperFast)
49 'Dim streamWorksheet As StreamWriter = New StreamWriter(partWorksheetXML.GetStream(FileMode.Create, FileAccess.Write)) ※削除
50 'xmlWorksheet.Save(streamWorksheet) ※削除
51 'streamWorksheet.Close() ※削除
'以下追加
Using _sheetStream As FileStream = New FileStream(fileXmlSheetPath, FileMode.Open, FileAccess.Read)
CopyStream(_sheetStream, partWorksheetXML.GetStream())
End Using
xmlWorksheet.Save(partWorksheetXML.GetStream)
'以上追加
52 pkgOutputDoc.Flush()'新規プロシージャ
Private Sub CopyStream(ByRef aSource As Stream, ByRef aTarget As Stream)
Dim bufSize As Integer = &H1000
Dim buf As Byte() = New Byte(bufSize - 1) {}
Dim bytesRead As Integer = 0
Do
bytesRead = aSource.Read(buf, 0, bufSize)
If bytesRead > 0 Then
aTarget.Write(buf, 0, bytesRead)
Else
Exit Do
End If
Loop
End Sub(バイトストリームの取得バッファについては、まだ調整中です。)
しかし、自身をマシン(WinXP)では問題なく動作するのに、サーバ(Win2003Server)で動作させると、以前と同じエラーで落ちてしまう。。。
System.IO.PackagingのZipPackagingクラスを使い、XMLDocumentを使わない(=DOMを使わない)方式でテストソースを書いても同じ現象が発生するので、これはIISかASPの問題のようです。調査中です。何となく8MBあたりに壁がありそうなのですが。。
引き続き、ご存知な方のアドバイスを頂けると、とてもうれしく思います。
ひらぽんさん、gekkaさん、ありがとうございました。
「8MBあたりの壁」について とりあえずの解決ができましたので、報告致します。
ZipPackageクラスでファイル用データをメモリに作成していると、8MBを超えたあたりで、一時ファイルをHDDに書き込みに行くようです。
書き込みに行く場所が、なぜか「C:\Documents and Settings\Default User\Local Settings\Temporary Internet Files」フォルダなのです。。
Windows2003サーバは「Network Service」ユーザがIIS6.0を動かす訳ですが、、、このユーザにはデフォルトで上記ファルダへの書き込み権限がありません。そこで、権限を与えることで、ZipPackageクラスによる大きなファイルの圧縮が可能となりました。
皆様、お騒がせしました。。。
が、しかし!!!!
また疑問が出てきてしまったので、すいません、新しいスレッドを立てますので、よろしくお願い致します。
追記
一緒に調べていた方から下記のURLを頂きました。
権限関係です。
参考までに。
http://msdn.microsoft.com:80/ja-jp/library/kwzs111e(VS.80).aspx
http://support.microsoft.com/kb/812614/ja- 回答としてマークElderBoxer 2009年11月15日 12:19
- 回答としてマークElderBoxer 2009年11月15日 12:17
- 回答としてマークされていないElderBoxer 2009年11月15日 12:17


