开发者社区> 技术小阿哥> 正文

异步陷阱之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 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-Exception

看来前面的分析是对的,文件在特定 情况不可用,但是为什么不可用呢?从上面的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,如需转载请自行联系原作者

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
19460 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
28746 0
阿里云服务器安全组设置内网互通的方法
虽然0.0.0.0/0使用非常方便,但是发现很多同学使用它来做内网互通,这是有安全风险的,实例有可能会在经典网络被内网IP访问到。下面介绍一下四种安全的内网互联设置方法。 购买前请先:领取阿里云幸运券,有很多优惠,可到下文中领取。
22377 0
阿里云服务器ECS登录用户名是什么?系统不同默认账号也不同
阿里云服务器Windows系统默认用户名administrator,Linux镜像服务器用户名root
16136 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
20515 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
23564 0
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
14886 0
13690
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载