請教一下各位先進:
我需要將server上的檔案整批匯出一個壓縮檔給user操作使用,整批實體檔案大小約1.5G以下,整批匯出時遇到已發生類型 'System.OutOfMemoryException' 的例外狀況,查看server的記憶體狀況,落在12GB以上,server共有16G記憶體可用,將server重開後,佔用記憶體約8.5GB,執行整批匯出功能測試記憶體並沒有往上衝至12G,最高大約落在9.5GB,程式依然報System.OutOfMemoryException,初步判斷不是server 記憶體不夠問題。
想請問這樣整批檔案(例如有100個檔案)匯整成一個壓縮檔匯出的功能,有檔案大小的限制嗎?如果有限制,是否有什麼方法突破檔案大小的限制呢?
先謝謝了~
public ActionResult ExportPapersZip(PaperViewModel PV)
{
var ms = new MemoryStream();
FileStream sFile;
string FileName = "";
byte[] toBytes;
List<ConferencePaperFilesViewModel> cpfVM = new List<ConferencePaperFilesViewModel>();
cpfVM = fileSrv.GetConferencePaperFilesList(PV.ConferenceId, 5);
using (var archive = new System.IO.Compression.ZipArchive(ms, ZipArchiveMode.Create, true))
{
cpfVM.ForEach(x =>
{
try
{
string MemberName = memberSrv.GetMemberName(x.MemberId);
string TrackName = conferenceSrv.GetTrackTitle(x.TrackId);
string SubFileName = Path.GetExtension(x.OriginalFileName).ToLower();
sFile = new FileStream(x.SysPath + x.OriginalFileName, FileMode.Open, FileAccess.Read);
toBytes = fileSrv.BinaryReadToEnd(sFile);
if(x.CleanTitle == null)
{
FileName = x.paperId.ToString();
}
else
{
FileName = x.CleanTitle;
}
var zipEntry = archive.CreateEntry(FileName + "_F" + SubFileName, System.IO.Compression.CompressionLevel.Fastest);
using (var zipStream = zipEntry.Open())
{
zipStream.Write(toBytes, 0, toBytes.Length);
}
}
catch (Exception)
{
throw;
}
});
}//end using (var archive = new System.IO.Compression.ZipArchive(ms, ZipArchiveMode.Create, true))
return File(ms.ToArray(), "application/zip", "Files.zip");
}
雖然我對微軟不熟,不過有些概念應該是共通的。
上面的程式因為是用 MemoryStream
暫存壓縮後的結果,太吃記憶體。
建議可以參考 ZipArchive 說明裡面的第一個範例,先把檔案寫到檔案中,再把檔案送給使用者。
另外從範例中可以看到 StreamWriter 可以分段寫入的,所以也不需要一次讀取整個要壓縮的檔案,改為分段讀取送給 StreamWriter 即可。
這樣應該可以避免佔用大量記憶體。
試了一個早上的修改,目前還是不知道該如何修改~
我的實體檔案是PDF檔,舊方法是將PDF檔案讀取後,全部轉成Binary格式,放在MemoryStream,最後再全部輸出打包成zip檔(檔案少時功能是可以正常輸出的)。
參考範例輸出的壓縮檔案,檔案是損毀無法開啓的,可能是我寫法錯誤,參考的範例我不知道如何改寫成可以正常整批檔案輸出的功能~
using (FileStream zipToOpen = new FileStream(x.SysPath + x.OriginalFileName, FileMode.Open, FileAccess.Read))
{
toBytes = fileSrv.BinaryReadToEnd(zipToOpen);
using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Create, true))
{
ZipArchiveEntry readmeEntry = archive.CreateEntry(FileName + "_F" + SubFileName, System.IO.Compression.CompressionLevel.Fastest);
using (StreamWriter writer = new StreamWriter(readmeEntry.Open()))
{
writer.WriteLine("Information about this package.");
writer.WriteLine("========================");
}
}
}
小碼農米爾 Mir 的回答,直接寫入網路串流更方便
下面這只是我改之前的,應該是用不到了
寫出來主要是說明邏輯的部分
而且我手邊也沒有微軟的環境能測試,所以可能還要修一下
//建立暫存的 zip 檔案,這邊的 tmp.zip 再自己改成不會重複的
using (FileStream zipToWrite = new FileStream("tmp.zip", FileMode.Open))
{
//建立 ZipArchive 物件,並指定寫入到 zipToWrite
using (ZipArchive archive = new ZipArchive(zipToWrite, ZipArchiveMode.Update))
{
//依序將讀取每個檔案,並寫入
cpfVM.ForEach(iFile => {
string fpath = iFile.SysPath + iFile.OriginalFileName;
//讀取檔案為 binary
byte[] readBytes = File.ReadAllBytes(fpath);
//在 zip 檔案內建立同樣名稱的檔案
ZipArchiveEntry zipEntry = archive.CreateEntry(fpath);
//寫入 zip 檔
using (var zipStream = zipEntry.Open())
{
zipStream.Write(readBytes, 0, readBytes.Length);
}
});
}
}
目前改成以下樣子,想請問最後return的部份該怎麼改,之前是寫入MemoryStream再輸出,現在如果改成FileStream,那return輸出的部份該怎麼修正呢?
return File(ms.ToArray(), "application/zip", "Files.zip");
var ms = new MemoryStream();
string FileName = "";
byte[] toBytes;
List<ConferencePaperFilesViewModel> cpfVM = new List<ConferencePaperFilesViewModel>();
cpfVM = fileSrv.GetConferencePaperFilesList(PV.ConferenceId, 5);
try
{
//依序將讀取每個檔案,並寫入
cpfVM.ForEach(x =>
{
//建立暫存的 zip 檔案,這邊的 tmp.zip 再自己改成不會重複的
//FileStream – 用於讀取和寫入檔案。File.Open(filename, FileMode.OpenOrCreate)
using (FileStream zipToOpen = new FileStream(x.SysPath + x.OriginalFileName, FileMode.Open, FileAccess.ReadWrite))
{
string SubFileName = Path.GetExtension(x.OriginalFileName).ToLower();
//sFile = new FileStream(x.SysPath + x.OriginalFileName, FileMode.Open, FileAccess.Read);
//toBytes = fileSrv.BinaryReadToEnd(sFile);
if (x.CleanTitle == null)
{
FileName = x.paperId.ToString();
}
else
{
FileName = x.CleanTitle;
}
//toBytes = fileSrv.BinaryReadToEnd(zipToOpen);
toBytes = System.IO.File.ReadAllBytes(x.SysPath + x.OriginalFileName);
//建立 ZipArchive 物件,並指定寫入到 zipToWrite
//ZipArchive – 用於建立和擷取 zip 封存中的項目。
using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Update, true))
{
//在 zip 檔案內建立同樣名稱的檔案
//ZipArchiveEntry – 用於表示壓縮檔。
ZipArchiveEntry readmeEntry = archive.CreateEntry(FileName + "_F" + SubFileName);
//寫入 zip 檔
using (var zipStream = readmeEntry.Open())
{
zipStream.Write(toBytes, 0, toBytes.Length);
}
}
}
});
}
catch (Exception)
{
throw;
}
return File(ms.ToArray(), "application/zip", "Files.zip");
以上程式目前執行有出現"找不到中央目錄記錄的結尾"的Error,應該是最後return時的錯誤,FileStream的部份最後該怎麼return呢?。
另外請問那一段程式碼是指"直接寫入網路串流"呢?謝謝
抱歉,語法上的問題我目前比較難幫忙,因為手邊真的沒環境測試。
其他的方式我還是提一下,你可以當作參考,要不要採用再自己衡量。
其實對於大檔案傳輸的部分,有種作法是把資料切分成幾段,然後前端依序拿到這些資料後,再拼湊回完整的資料。
這樣做的好處是,當我們把原本一次請求分散成多次請求,如果其中某次請求發生錯誤,只需要重新請求錯誤的那個區塊就可。就不會中間因為網路問題出錯就要全部重來。
至於缺點的部分是,這種做法會吃使用者瀏覽器的記憶體,所以檔案大小無法超過使用者的記憶體大小。(類似 mega.nz)
File 傳入 zip 路徑就可以
return File(x.SysPath + x.OriginalFileName, "application/zip");
FileStream 會在下面這個路徑產生檔案
x.SysPath + x.OriginalFileName
另外 淺水員 有發現 cpfVM.ForEach
跑到外面了
ZipArchive 和 FileStream 要放在 ForEach 外,才不會重複產生 zip 物件。
可以將寫入記憶體的部分,改成寫入網路串流,這樣就不會超出記憶體的使用限制
不過 MVC5 沒有內建的功能可以用,所以需要做一些修改
public class PushStreamResult : FileResult
{
private readonly Action<Stream> stream;
public PushStreamResult(string fileName, string contentType, Action<Stream> stream)
: base(contentType)
{
this.stream = stream;
this.FileDownloadName = fileName;
}
protected override void WriteFile(HttpResponseBase response)
{
response.Buffer = false;
stream(response.OutputStream);
}
}
參考文章:
https://stackoverflow.com/questions/943122/writing-to-output-stream-from-action
public class ZipWrapStream : Stream
{
private readonly Stream stream;
private long position = 0;
public ZipWrapStream(Stream stream)
{
this.stream = stream;
}
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return true; } }
public override long Position
{
get { return position; }
set { throw new NotImplementedException(); }
}
public override bool CanRead { get { throw new NotImplementedException(); } }
public override long Length { get { throw new NotImplementedException(); } }
public override void Flush()
{
stream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
position += count;
stream.Write(buffer, offset, count);
}
}
改成回傳 PushStreamResult
並使用 ZipWrapStream
修補 OutputStream。
public ActionResult Zip()
{
var funcZIP = (Action<Stream>)((stream) =>
{
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create))
{
var zipEntry = archive.CreateEntry(@"aaa.txt", CompressionLevel.Fastest);
using (var zipStream = zipEntry.Open())
{
var bytes = System.IO.File.ReadAllBytes(@"D:\專案\aaa.txt");
zipStream.Write(bytes, 0, bytes.Length);
}
}
});
return new PushStreamResult("aaa.zip", "application/zip",
stream => funcZIP(new ZipWrapStream(stream)));
}
大致上就是寫入 Zip 時,將檔案直接寫到 OutputStream 串流裡,這樣就不會占用記憶體空間,適用於大檔案的下載。
我在新增 ZipWrapStream時遇到未實作繼承的抽象成員'Stream.Position.set'的error。
程式碼裡看似有實作,不過錯報的部份不知道如何修補?
public override long Position
{
get => position; set => throw new NotImplementedException();
}
應該是不支援屬性的 =>
語法,我調整回覆了
測試完了,好像更慘,原版程式早上測試200個檔案系統才爆掉System.OutOfMemoryException。
改成前輩的方法,下載100個檔案可以,但150個檔案時就無法下載,但不會報錯,會如下圖所示,在下載到118MB時就會卡住一直loading旋轉直到顯示失敗—網路錯誤後停止,好像極限只到118MB?
原始程式下載200個檔案,壓縮檔大小約200MB左右。
以上為目前測試狀況~
感謝大大提供方法測試~
int count = 1;
string FileName = "";
List<ConferencePaperFilesViewModel> cpfVM = new List<ConferencePaperFilesViewModel>();
cpfVM = fileSrv.GetConferencePaperFilesList(PV.ConferenceId, 5);
var funcZIP = (Action<Stream>)((stream) =>
{
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create))
{
cpfVM.ForEach(x =>
{
try
{
if(count <= 150)
{
string SubFileName = Path.GetExtension(x.OriginalFileName).ToLower();
if (x.CleanTitle == null)
{
FileName = x.paperId.ToString();
}
else
{
FileName = x.CleanTitle;
}
var zipEntry = archive.CreateEntry(FileName + "_F" + SubFileName, System.IO.Compression.CompressionLevel.Fastest);
using (var zipStream = zipEntry.Open())
{
var bytes = System.IO.File.ReadAllBytes(x.SysPath + x.OriginalFileName);
zipStream.Write(bytes, 0, bytes.Length);
}
}
count++;
}
catch (Exception)
{
throw;
}
});
}
});
return new PushStreamResult("Files.zip", "application/zip", stream => funcZIP(new ZipWrapStream(stream)));
--
會不會是連線逾時?
會不會是出現 Exception 被 Debug 停住了 ((測不出來開始亂猜
我覺得連線逾時和出現 Exception 被 Debug 停住了都不像。
當檔案下載到118MB時,會固定卡在118MB,一直loading轉圈約1~2分鐘以上的時間才會失敗-網路錯誤
Exception我己經全開了,好像沒有理由其它段程式會報錯,而這一段程式不會報錯~