很多教程和資料都強調流暢的用戶體驗需要異步來輔助,核心思想就是保證用戶前端的交互永遠有最高的優先級,讓一切費時的邏輯通通放到后臺,等到諸事完備,通知一下前端給個提示或者繼續下一步。隨著.NET發展,async和await關鍵字的推廣,Task Parallel Library (TPL)的穩步發展, 異步編程也越來越多的被重視和采用,很多時候非常便利的解決各種性能問題,但同時也帶來了很多的陷阱。
這里我拋出一個實際項目中遇到的陷阱,先簡單交代一下故事背景:SpreadJS產品有一個Excel IO部件,是一個ASP.NET MVC Web API(MVC4)應用,用來導入Excel文件到SpreadJS中;其工作過程是客戶端先上傳Excel文件,服務器端接收文件后讀出內容,以SpreadJS特有的JSON格式回傳給客戶端。很長一段時間工作正常,直到某一天有一個“大神”級的客戶反饋他在使用Excel IO過程中會一定幾率隨機出現導入失敗,具體的表現是在返回的JSON數據中提示有IO錯誤,好吧,附上用戶場景的代碼片段(略去了腳本引用,DOM以及其他機密代碼):
$(document).ready(function() { // initialize 10 spreadjs widgets for(var i = 0; i < 10; i++) { $("#ss_" + i).wijspread({ sheetCount: 2 }); } // import handler $("#importButton").click(function() { for(var i = 0; i < 10; i++) { importToSpread("ss" + i); } }); // import process function importToSpread(target) { var formData = new FormData(); formData.append("file", $("#importingExcelFile").get(0).files[0]); formData.append("ExcelOpenFlags", "NoFlagsSet"); formData.append("TextFileOpenFlags", "None"); formData.append("Password", ""); $.ajax( { url: "http://your.excelio.path/xsapi/import", type: "POST", success: function(data, textStatus, jqXHR) { $("#" + target).wijspread("spread").fromJSON(JSON.parse(jqXHR.responseText).spread); }, data: formData, contentType: false, processData: false, headers: { "Accept": "application/json" } }); } });
也許各位看官可能有話說了:這明顯的窮折騰么,有這么把一個文件重復導入10次的實際場景嗎?嗯,這是一個社會工程學問題,略過,呵呵。
根據用戶的代碼,可以分析得到一些關鍵信息:
1、用戶在很短時間內快速提交了多個請求并上傳文件;
2、返回結果會隨機出現IO錯誤;
由此可以得出結論:應該是服務器處理上傳的Excel文件時,某個文件在特定情況下不可用,從而導致處理程序拋出IO異常。什么情況會導致IO不可用呢?似乎一下子還真無從下手,作為開發人員,最容易想到的方法就是祭出IDE,直接掛上調試器,只要捕獲到這個IO異常就好了。經過幾次嘗試,終于看到了IO異常了,如下圖:
看來前面的分析是對的,文件在特定 情況不可用,但是為什么不可用呢?從上面的IO異常信息可以看出,這個文件是ASP.NET臨時保存的上傳文件。在ASP.NET WEB API中,處理上傳文件的思路和方法如下:
var root = HttpContext.Current.Server.MapPath("~/App_Data"); var provider = new MultipartFormDataStreamProvider(root); try { await Request.Content.ReadAsMultipartAsync(provider); } catch (Exception ex) { return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex); } var file = provider.FileData.FirstOrDefault(); // File.OpenRead(file.LocalFileName) // may get exception here
從這個片段很容易分析出一下兩種可能導致文件IO的情況:
1、文件的LocalFileName不唯一
2、讀取上傳內容的異步操作結束但是文件還沒有釋放
顯然,第一條可以排除,因為異常信息里可以看到文件的名字有一個GUID,基本可以保證絕對唯一,所以,問題肯定發生在這里的異步處理。
為了深入的搞清楚發生了什么,我查看了ReadAsMultipartAsync的源代碼,這里面會調用MultipartFormDataStreamProvider上的GetStream方法來處理上傳的文件:
// ... 略去參數處理 string localFileName = this.GetLocalFileName(headers); str = Path.Combine(this._rootPath, Path.GetFileName(localFileName)); // ... 略去部分無關邏輯 MultipartFileData item = new MultipartFileData(headers, str); this._fileData.Add(item); return File.Create(str, this._bufferSize, FileOptions.Asynchronous);
這里調用GetLocalFileName來獲取臨時文件名,很清楚的使用了Guid.NewGuid()來保證文件名永遠不會重復;焦點轉到最后一句返回一個可寫的FileStream,注意這里的第三個參數是FileOptions.Asynchronous,就是說,這個FileStream實際是異步IO,但是內部處理邏輯沒有等待這個結果就直接走后續的邏輯了,這樣導致在服務器運行在高IO并發的情況就很容易發生IO異常。
以上分析了問題,但如何解決呢(某PM話外音:那誰誰,快點啊,客戶催著呢),很簡單,去除調這個異步IO就可以了,好吧,代碼一點也不簡單,重寫這個GetStream方法,保證獲取的FileStream使用同步,雖然一定程度降低了性能,但好歹能解決問題。
參考示例工程代碼:下載地址
更新補充:在ASP.NET MVC 5中重寫了ReadAsMultipartAsync所在的整個類,已經修復了這個問題(至少我試過同時1000次毫無壓力),參考示例中AsyncIoTrap_v5工程。
備注:昨天在OSChina上推出了Wijmo 5 《jQuery UI 組件集 Wijmo 五年最大更新,Mobile First!》。但是本次發布的Wijmo 5 Beta版本未包含SpreadJs。
文章列表