异步陷阱之IO篇
很多教程和資料都強調流暢的用戶體驗需要異步來輔助,核心思想就是保證用戶前端的交互永遠有最高的優先級,讓一切費時的邏輯通通放到后臺,等到諸事完備,通知一下前端給個提示或者繼續下一步。隨著.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?widgetsfor(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?processfunction?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工程。
本文轉自 powertoolsteam 51CTO博客,原文鏈接:http://blog.51cto.com/powertoolsteam/1550437,如需轉載請自行聯系原作者
總結
- 上一篇: Seliux简介
- 下一篇: springboot发送http请求