作爲最流行的輕量級Web服務器的翹楚,lighttpd提供了良好的sendfile支持,JavaEye網站服務器使用的就是lighttpd。在Linux操作系統上面,只需要在lighttpd.conf配置文件如下配置,lighttpd就會使用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支持,僅僅需要你在配置文件打開就可以了:
JavaEye網站在使用了X-sendfile功能之後,lighttpd的內存佔用有明顯的下降。未使用X-sendfile之前,lighttpd有時候內存佔用會到200MB以上(有用戶多線程下載附件),在使用X-sendfile之後,lighttpd的內存佔用還從未突破20MB。
最後要提醒大家幾個問題:
1、lighttpd-1.4.x不認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了,這個大家有興趣自己測試吧。