RoR网站如何利用lighttpd的X-sendfile功能提升文件下载性能(525次阅读) 我要收藏 已有1人收藏
来自:JavaEye热点话题
作者:JavaEye网站 发表于:2008-01-12 17:45:20.0
pesome 最先收藏于 2008-01-13 19:57:42.0
但是在某些情况下,我们却无法直接让lighttpd处理文件的下载,比方说JavaEye网站需要统计帖子附件的下载次数,博客相册的点击次数,比方说需要对下载的文件进行权限的控制,特别是对于一些多用户系统,你不能让用户上传的私密文件被其他用户随便下载到,例如JavaEye圈子的共享文件不能够对圈子外的用户开放下载。因此,文件下载目录千万不能放到public目录下,不能让用户直接通过浏览器的URL地址访问到。在这种情况下,文件下载必须由服务器端应用程序来处理。
在RoR应用当中,我们可以在controller中使用send_file方法来控制文件的下载。send_file方法将下载的文件以4KB为单位写到一个输出流去。如果我们使用mongrel应用服务器的话,mongrel会在内存当中创建一个StringIO对象,把整个下载文件完整的读入内存,然后再向客户端或者前端的Web服务器写出。如果我们使用fcgi来运行RoR的话,fcgi会直接把输出流的内容向前端的Web服务器写出。
毫无疑问,我们可以看到这种下载处理方式有很大的性能缺陷:
1、当使用mongrel的时候,如果下载文件很大,会导致mongrel内存暴涨!
mongrel创建一个StringIO对象缓存整个输出内容,我们假设用户下载的是一个100MB的文件,该用户又很喜欢用多线程下载工具,他开了10个线程并发下载,那么mongrel的内存占用会暴涨1GB以上。而且最可怕的是,即使当用户下载结束以后,mongrel的内存都不会迅速回落,而是一直保持如此高的内存占用。这个缺陷非常容易被别有用心的黑客利用,攻击网站。这也是JavaEye网站为什么始终不用mongrel的原因之一。
2、当使用fcgi的时候,如果前端Web服务器没有足够大buffer,会导致fcgi进程被挂住
fcgi自己不开output buffer,而是实时写出输出内容,如果前端Web服务器用的是lighttpd,那么你很幸运,lighttpd会照单全收,一个字节都不拉下;如果前端Web服务器用的是nginx/apache,那么你很不幸,nginx/apache默认只开8K的buffer,收不下的那就对不起了,您慢点嘞,fcgi进程就被挂住了,只要客户端浏览器下载不结束,fcgi进程就被一直占用。
3、即使使用lighttpd+fcgi,也会对服务器造成不小的性能开销
lighttpd+fcgi是最理想的Rails部署环境,JavaEye网站使用的就是lighttpd+fcgi。当ruby程序执行send_file开始下载的时候,fcgi会以4KB为单位读入文件内容,然后立刻写出到lighttpd去,而lighttpd照单全收。因此当下载文件被完整的通过fcgi被flush到lighttpd的内存里面去以后,即使你杀掉fcgi进程,都丝毫不会影响文件下载。
也许你会问,lighttpd都吃下来文件内容,内存会不会暴涨?会的,我们假设同样的用户场景,某用户启动10个线程下载100MB的文件,fcgi进程内存不会发生变化,但是lighttpd会暴涨1GB。但所幸的是lighttpd的内存管理的不错,一旦用户取消下载,或者下载完毕,lighttpd立刻释放掉1GB的内存。
但是无论怎么说,ruby还是需要完整的读取下载文件,而lighttpd也需要开辟足够大的内存,处理整个文件的下载过程,对服务器开销还是很大的。我们的问题是,能不能让带权限控制的文件下载像lighttpd下载静态资源文件那样快,开销那样小呢?答案就是X-sendfile!
使用X-sendfile方式,服务器端应用程序不需要读取下载文件了,只需要设置response的header信息就足够了,此外还要附加一个信息“X-LIGHTTPD-send-file”信息给lighttpd,告诉lighttpd,文件下载我就不管了,你自己看着办吧:
X-LIGHTTPD-send-file告诉lighttpd,去硬盘的哪个路径找要下载的文件,最后一行啥都不输出了,下载不用ruby来管了。
而lighttpd收到X-LIGHTTPD-send-file信息以后,就会找到硬盘该文件,以静态资源文件的下载方式处理,丝毫不消耗lighttpd的内存。还是以某用户启动10个线程下载100MB文件为例,10个fcgi进程发送了response信息就处理完毕了,而lighttpd知道下载的是硬盘的静态文件,会以sendfile方式下载,文件内容就会被操作系统内核直接送到网卡的buffer里面,既不消耗ruby进程,也不消耗lighttpd,皆大欢喜。
在lighttpd-1.4.18版本里面,fastcgi方式已经内置X-sendfile支持,仅仅需要你在配置文件打开就可以了:
这样我们就可以在RoR应用中使用X-sendfile飞速的下载文件了。但是慢着,免费的午餐有这么容易享用吗? 如果你已经按照我的介绍进行了相关的配置,那么你会失望的发现,下载文件会出问题,无论多大的文件,下载到本地都只有1KB,怎么回事? 这是Rails一个可耻的bug!经过我们努力的摸索和钻研,终于搞清楚的原因以及fix bug的方法,目前好像在互联网上面还搜索不到任何解决相关问题的办法,那么我们提出的解决方案看来是首开先河了。
Rails的render机制在输出页面之前,会根据输出内容的长度来设置response信息的Content-Length,如果输出的内容是页面,这一步是必须的操作。但如果是X-sendfile的话,我们在程序当中 render :nothing => true了,于是Rails就会错误的设置Content-Length为1KB (为什么不是0KB呢?这是因为render :nothing实际上输出一个空白或者回车之类的字符,又是一个可耻的问题!), 覆盖掉我们在程序当中设置的Content-Length。而lighttpd在处理X-sendfile的时候也明显大脑进水,它不会根据硬盘文件的实际长度来确定Content-Length,而是顽固的相信Rails告诉他的Content-Length就是1KB,最后我们会悲哀的发现下载文件只有1KB,而且是下载文件的第一个字符。
为什么说这是Rails一个可耻的bug呢?因为这个bug已经被多次报告到Rails的Trac上面,但是直至最新的Rails2.0的SVN,仍然拒绝被修复。所以这个世界没有无缘无故的爱,也没有无缘无故的恨,Mongrel作者Zed Shaw造反Rails社区是有原因滴。没有办法啦,我们只好自己动手修复:
在我们的Rails项目的lib目录下面创建子目录action_controller,然后在这个子目录下面创建文件cgi_response.rb,文件内容如下:
打开了ActionController::CgiResponse类,覆盖掉set_content_length!方法,当reponse的信息当中包含X-LIGHTTPD-send-file的时候,避免Rails覆盖我们的Content-Length设置。这样X-sendfile功能就可以正常使用了。然后在app/controllers/application.rb里面引入这个文件:
JavaEye网站在使用了X-sendfile功能之后,lighttpd的内存占用有明显的下降。未使用X-sendfile之前,lighttpd有时候内存占用会到200MB以上(有用户多线程下载附件),在使用X-sendfile之后,lighttpd的内存占用还从未突破20MB。
最后要提醒大家几个问题:
1、lighttpd-1.4.18不认X-sendfile这个header,只认X-LIGHTTPD-send-file
按照lighttpd网站自己的文档,以及各种各样流行的X-sendfile文档,设置的header都是X-sendfile,但是经过我们n次失败的摸索,才发现原来必须使用X-LIGHTTPD-send-file,这一点请不要被文档迷惑,目前好像也只有我们提出这个解决办法,互联网上面尚未看到其他人提出过,看来我们又首开先河了。用RoR就是这点好,你动不动就得自己先去当尝螃蟹的那个人。
2、lighttpd-1.5.0版本的X-sendfile设置有所改变
lighttpd-1.5.0版本还未发布正式版本,据说1.5.0已经认识X-sendfile这个header了,这个大家有兴趣自己测试吧。
3、Rails2.0版本仍然存在这个X-sendfile的bug
我们的fix方法目前是在Rails1.2.6上面编写的,针对Rails2.0版本应该也是有效的,但是我们还没有测试过,有兴趣的同学自己测试一下吧。
本文的讨论也很精彩,浏览讨论>>
JavaEye推荐
该文章的原文地址为:http://www.javaeye.com/topic/154538
作者:JavaEye网站 发表于:2008-01-12 17:45:20.0
pesome 最先收藏于 2008-01-13 19:57:42.0
网站: JavaEye
作者: robbin
链接:http://www.javaeye.com/topic/154538
发表时间: 2008年01月12日
声明:本文系JavaEye网站发布的原创文章,未经作者书面许可,严禁任何网站转载本文,否则必将追究法律责任!
传统的Web服务器在处理文件下载的时候,总是先读入文件内容到应用程序内存,然后再把内存当中的内容发送给客户端浏览器。这种方式在应付当今大负载网站,音频视频网站力不从心。sendfile是现代操作系统支持的一种高性能网络IO方式,操作系统内核的sendfile调用可以将文件内容直接推送到网卡的buffer当中,从而避免了Web服务器读写文件的开销,实现了“零拷贝”模式。
作为最流行的轻量级Web服务器的翘楚,lighttpd提供了良好的sendfile支持,JavaEye网站服务器使用的就是lighttpd。在Linux操作系统上面,只需要在lighttpd.conf配置文件如下配置,lighttpd就会使用sendfile方式处理静态资源的下载,效率非常高:
引用
server.network-backend = "linux-sendfile"
但是在某些情况下,我们却无法直接让lighttpd处理文件的下载,比方说JavaEye网站需要统计帖子附件的下载次数,博客相册的点击次数,比方说需要对下载的文件进行权限的控制,特别是对于一些多用户系统,你不能让用户上传的私密文件被其他用户随便下载到,例如JavaEye圈子的共享文件不能够对圈子外的用户开放下载。因此,文件下载目录千万不能放到public目录下,不能让用户直接通过浏览器的URL地址访问到。在这种情况下,文件下载必须由服务器端应用程序来处理。
在RoR应用当中,我们可以在controller中使用send_file方法来控制文件的下载。send_file方法将下载的文件以4KB为单位写到一个输出流去。如果我们使用mongrel应用服务器的话,mongrel会在内存当中创建一个StringIO对象,把整个下载文件完整的读入内存,然后再向客户端或者前端的Web服务器写出。如果我们使用fcgi来运行RoR的话,fcgi会直接把输出流的内容向前端的Web服务器写出。
毫无疑问,我们可以看到这种下载处理方式有很大的性能缺陷:
1、当使用mongrel的时候,如果下载文件很大,会导致mongrel内存暴涨!
mongrel创建一个StringIO对象缓存整个输出内容,我们假设用户下载的是一个100MB的文件,该用户又很喜欢用多线程下载工具,他开了10个线程并发下载,那么mongrel的内存占用会暴涨1GB以上。而且最可怕的是,即使当用户下载结束以后,mongrel的内存都不会迅速回落,而是一直保持如此高的内存占用。这个缺陷非常容易被别有用心的黑客利用,攻击网站。这也是JavaEye网站为什么始终不用mongrel的原因之一。
2、当使用fcgi的时候,如果前端Web服务器没有足够大buffer,会导致fcgi进程被挂住
fcgi自己不开output buffer,而是实时写出输出内容,如果前端Web服务器用的是lighttpd,那么你很幸运,lighttpd会照单全收,一个字节都不拉下;如果前端Web服务器用的是nginx/apache,那么你很不幸,nginx/apache默认只开8K的buffer,收不下的那就对不起了,您慢点嘞,fcgi进程就被挂住了,只要客户端浏览器下载不结束,fcgi进程就被一直占用。
3、即使使用lighttpd+fcgi,也会对服务器造成不小的性能开销
lighttpd+fcgi是最理想的Rails部署环境,JavaEye网站使用的就是lighttpd+fcgi。当ruby程序执行send_file开始下载的时候,fcgi会以4KB为单位读入文件内容,然后立刻写出到lighttpd去,而lighttpd照单全收。因此当下载文件被完整的通过fcgi被flush到lighttpd的内存里面去以后,即使你杀掉fcgi进程,都丝毫不会影响文件下载。
也许你会问,lighttpd都吃下来文件内容,内存会不会暴涨?会的,我们假设同样的用户场景,某用户启动10个线程下载100MB的文件,fcgi进程内存不会发生变化,但是lighttpd会暴涨1GB。但所幸的是lighttpd的内存管理的不错,一旦用户取消下载,或者下载完毕,lighttpd立刻释放掉1GB的内存。
但是无论怎么说,ruby还是需要完整的读取下载文件,而lighttpd也需要开辟足够大的内存,处理整个文件的下载过程,对服务器开销还是很大的。我们的问题是,能不能让带权限控制的文件下载像lighttpd下载静态资源文件那样快,开销那样小呢?答案就是X-sendfile!
使用X-sendfile方式,服务器端应用程序不需要读取下载文件了,只需要设置response的header信息就足够了,此外还要附加一个信息“X-LIGHTTPD-send-file”信息给lighttpd,告诉lighttpd,文件下载我就不管了,你自己看着办吧:
response.headers['Content-Type'] = @attachment.content_type
response.headers['Content-Disposition'] = "attachment; filename=\"#{URI.encode(@attachment.filename)}\""
response.headers['Content-Length'] = @attachment.size
response.headers["X-LIGHTTPD-send-file"] = @attachment.public_filename
render :nothing => true
X-LIGHTTPD-send-file告诉lighttpd,去硬盘的哪个路径找要下载的文件,最后一行啥都不输出了,下载不用ruby来管了。
而lighttpd收到X-LIGHTTPD-send-file信息以后,就会找到硬盘该文件,以静态资源文件的下载方式处理,丝毫不消耗lighttpd的内存。还是以某用户启动10个线程下载100MB文件为例,10个fcgi进程发送了response信息就处理完毕了,而lighttpd知道下载的是硬盘的静态文件,会以sendfile方式下载,文件内容就会被操作系统内核直接送到网卡的buffer里面,既不消耗ruby进程,也不消耗lighttpd,皆大欢喜。
在lighttpd-1.4.18版本里面,fastcgi方式已经内置X-sendfile支持,仅仅需要你在配置文件打开就可以了:
引用
"allow-x-send-file"="enable"
这样我们就可以在RoR应用中使用X-sendfile飞速的下载文件了。但是慢着,免费的午餐有这么容易享用吗? 如果你已经按照我的介绍进行了相关的配置,那么你会失望的发现,下载文件会出问题,无论多大的文件,下载到本地都只有1KB,怎么回事? 这是Rails一个可耻的bug!经过我们努力的摸索和钻研,终于搞清楚的原因以及fix bug的方法,目前好像在互联网上面还搜索不到任何解决相关问题的办法,那么我们提出的解决方案看来是首开先河了。
Rails的render机制在输出页面之前,会根据输出内容的长度来设置response信息的Content-Length,如果输出的内容是页面,这一步是必须的操作。但如果是X-sendfile的话,我们在程序当中 render :nothing => true了,于是Rails就会错误的设置Content-Length为1KB (为什么不是0KB呢?这是因为render :nothing实际上输出一个空白或者回车之类的字符,又是一个可耻的问题!), 覆盖掉我们在程序当中设置的Content-Length。而lighttpd在处理X-sendfile的时候也明显大脑进水,它不会根据硬盘文件的实际长度来确定Content-Length,而是顽固的相信Rails告诉他的Content-Length就是1KB,最后我们会悲哀的发现下载文件只有1KB,而且是下载文件的第一个字符。
为什么说这是Rails一个可耻的bug呢?因为这个bug已经被多次报告到Rails的Trac上面,但是直至最新的Rails2.0的SVN,仍然拒绝被修复。所以这个世界没有无缘无故的爱,也没有无缘无故的恨,Mongrel作者Zed Shaw造反Rails社区是有原因滴。没有办法啦,我们只好自己动手修复:
在我们的Rails项目的lib目录下面创建子目录action_controller,然后在这个子目录下面创建文件cgi_response.rb,文件内容如下:
module ActionController # fix rails X-sendfile bug!
class CgiResponse
def set_content_length!
@headers["Content-Length"] = @body.size unless @body.respond_to?(:call) || @headers["X-LIGHTTPD-send-file"]
end
end
end
打开了ActionController::CgiResponse类,覆盖掉set_content_length!方法,当reponse的信息当中包含X-LIGHTTPD-send-file的时候,避免Rails覆盖我们的Content-Length设置。这样X-sendfile功能就可以正常使用了。然后在app/controllers/application.rb里面引入这个文件:
require 'action_controller/cgi_response'
JavaEye网站在使用了X-sendfile功能之后,lighttpd的内存占用有明显的下降。未使用X-sendfile之前,lighttpd有时候内存占用会到200MB以上(有用户多线程下载附件),在使用X-sendfile之后,lighttpd的内存占用还从未突破20MB。
最后要提醒大家几个问题:
1、lighttpd-1.4.18不认X-sendfile这个header,只认X-LIGHTTPD-send-file
按照lighttpd网站自己的文档,以及各种各样流行的X-sendfile文档,设置的header都是X-sendfile,但是经过我们n次失败的摸索,才发现原来必须使用X-LIGHTTPD-send-file,这一点请不要被文档迷惑,目前好像也只有我们提出这个解决办法,互联网上面尚未看到其他人提出过,看来我们又首开先河了。用RoR就是这点好,你动不动就得自己先去当尝螃蟹的那个人。
2、lighttpd-1.5.0版本的X-sendfile设置有所改变
lighttpd-1.5.0版本还未发布正式版本,据说1.5.0已经认识X-sendfile这个header了,这个大家有兴趣自己测试吧。
3、Rails2.0版本仍然存在这个X-sendfile的bug
我们的fix方法目前是在Rails1.2.6上面编写的,针对Rails2.0版本应该也是有效的,但是我们还没有测试过,有兴趣的同学自己测试一下吧。
本文的讨论也很精彩,浏览讨论>>
JavaEye推荐
该文章的原文地址为:http://www.javaeye.com/topic/154538
已有评论
添加你的评论:
相关标签
相关文章
•第十口口水 - JavaEye on rails
•关于RoR学习一点胡言乱语
•利用Radrails开发Ruby on Rails程序入门指南
•Fast-track your Web apps with Ruby on Rails
•说说Rails吧,启动开始。
•说说Rails吧,config的幕后工作
•文章:Ruby on Rails案例学习:ChangingThePresent.org
•Lucene Hack之通过缩小搜索结果集来提升性能 (1)
•详解spring事务属性
•给Ajax技术初学者的一些建议
•JSON 简介
•漫谈应用缓存的命中率问题
•让Hsqldb随WebAPP一起启动
•Lucene Hack之通过缩小搜索结果集来提升性能 (2)
•阿里软件SAAS分析笔记
•IBM WebSphere Application Server 诊断和调优(二)
•JVM-level clustering
•SAAS(软件即服务)的时代即将来临吗?
•深入了解Java ClassLoader、Bytecode 、ASM、cglib
•REST+RIA方案
•亲爱的朋友们, 我需要你们的帮助
•Ext的组件结构分析,附Ext组件结构图
•用 GlassFish v2 替换 Tomcat 5.x
•【Android】说做就做:都市列表+卫星地图
•rails2.02快速体验
•Ext 的实例演示。
•j2ee笔试题目 servlet笔试题目 jsp 笔试题目 java笔试题目
•Acegi学习总结
•Grails1.0就绪,只欠Groovy东风
•关于RoR学习一点胡言乱语
•利用Radrails开发Ruby on Rails程序入门指南
•Fast-track your Web apps with Ruby on Rails
•说说Rails吧,启动开始。
•说说Rails吧,config的幕后工作
•文章:Ruby on Rails案例学习:ChangingThePresent.org
•Lucene Hack之通过缩小搜索结果集来提升性能 (1)
•详解spring事务属性
•给Ajax技术初学者的一些建议
•JSON 简介
•漫谈应用缓存的命中率问题
•让Hsqldb随WebAPP一起启动
•Lucene Hack之通过缩小搜索结果集来提升性能 (2)
•阿里软件SAAS分析笔记
•IBM WebSphere Application Server 诊断和调优(二)
•JVM-level clustering
•SAAS(软件即服务)的时代即将来临吗?
•深入了解Java ClassLoader、Bytecode 、ASM、cglib
•REST+RIA方案
•亲爱的朋友们, 我需要你们的帮助
•Ext的组件结构分析,附Ext组件结构图
•用 GlassFish v2 替换 Tomcat 5.x
•【Android】说做就做:都市列表+卫星地图
•rails2.02快速体验
•Ext 的实例演示。
•j2ee笔试题目 servlet笔试题目 jsp 笔试题目 java笔试题目
•Acegi学习总结
•Grails1.0就绪,只欠Groovy东风
