Wednesday, January 23, 2008

BestPractice|JavaEye网站的RoR性能优化经验谈 RoR的部署方案选择

RoR的部署方案选择 - robbin的自言自语 - JavaEye技术网站

RoR的部署方式从架构上来说分为前端和后端:

一、前端
前端的作用就是处理静态资源,将动态请求分发到后端,有时候也带有一些额外的功能,例如对特定URL进行rewrite和redirect,对HTTP输出进行gzip压缩等等。

前端目前已知的可以选择apache, lighttpd, litespeed, nginx, haproxy

1、apache2.2
apache是全球市场占有率最高的web server,超过全球互联网网站50%的网站都用apache。apache2.2 + mod_proxy_balancer是一个非常流行,非常稳定的方案。

使用apache2.2唯一的问题就是apache的性能和后面那些轻量级web server相比,差太远了。一方面在处理静态请求方面apache要比lighttpd慢3-5倍,内存消耗和CPU消耗也高出一个数量级,另一方面mod_proxy_balancer的分发性能也不高,比haproxy差很远。

2、lighttpd
lighttpd是一个轻量级高性能web server,一个在MySQL Inc工作的德国人写的。性能很好,内存和CPU资源消耗很低,支持绝大多数apache的功能,是apache的绝好替代者。目前lighttpd已经上升到全球互联网第四大web server,市场占有率仅此于apache,IIS和Sun。

lighttpd唯一的问题是proxy功能不完善,因此不适合搭配mongrel来使用。lighttpd下一个版本1.5.0的proxy模块重写过了,将会解决这个问题。

3、litespeed
和lighttpd差不多,商业产品,收费的。比lighttpd来说,多一个web管理界面,不用写配置文件了。litespeed专门为单机运行的RoR开发了一个lsapi协议,号称性能最好,比httpd和fcgi都要好。他的proxy功能比lighttpd完善。

litespeed的缺点我却认为恰恰是这个lsapi。因为lsapi不是web server启动的时候启动固定数目的ruby进程,而是根据请求繁忙程度,动态创建和销毁ruby进程,貌似节省资源,实则和apache2.2进程模型一样,留下很大的黑客攻击漏洞。只要黑客瞬时发起大量动态请求,就会让服务器忙于创建ruby进程而导致CPU资源耗尽,失去响应。

当然,litespeed也支持httpd和fcgi,这个和lighttpd用法一样的,到没有这种问题。

4、nginx
一个俄国人开发的轻量级高性能web server,特点是做proxy性能很好,因此被推荐取代apache2.2的mod_proxy_balancer,来和mongrel cluster搭配。其他方面和lighttpd到差不多。

要说缺点,可能就是发展的时间比较短,至今没有正式版本,还是beta版。没有经过足够网站的验证。

5、haproxy
就是一个纯粹的高性能proxy,不处理静态资源的,所有请求统统分发到后端。

二、后端
后端就是跑ruby进程,处理RoR动态请求了。运行后端ruby进程有两种方式:

1、fcgi方式
准确的说,不能叫做fcgi方式,其实就是启动一个ruby进程,让这个ruby进程监听一个tcp/unix socket,以fcgi协议和前端通讯。所以fcgi不是指ruby进程的运行方式,而是ruby进程使用的通讯协议。这就好比你tomcat可以用http也可以使用ajp通讯一样,tomcat自己的运行方式都一样的,只是通讯方式不一样。

fcgi方式启动ruby进程,可以使用lighttpd带的一个spawn-fcgi工具来启动(JavaEye目前采用这种方式)。

值得一提的是,apache2.2的mod_fastcgi的方式和上面还不太一样,由apache动态创建fcgi进程和管理fcgi进程,这种方式和litespeed的lsapi面临的问题是一样的,此外apache的mod_fastcgi自己也有很多严重的bug,是一种很糟糕的部署方式。这种糟糕的部署方式也败坏了fcgi的名声。

fastcgi只是一种协议,虽然古老,但并不是不好用,http协议也很古老。没有必要因为apache的mod_fastcgi的运行方式的问题而连带把fastcgi都一同否定了。fastcgi只是一个协议(程序之间的语言),是apache的mod_fastcgi这个模块有问题。打个比方,有个人英语水平很差,和你用英语对话,总是结结巴巴的,那你说是英语(fastcgi)这种语言有问题呢?还是和你对话的这个人(mod_fastcgi)有问题呢?

2、http方式
也就是用mongrel去跑ruby进程,由于mongrel实际上已经是一个简单的http server,所以也可以单独作为web server使用。mongrel现在越来越受欢迎了。

用fcgi方式还是http方式,我个人觉得区别不大,关键还是看应用的场合,一般而言,推荐的搭配是:

lighttpd + fcgi 或者 nginx +mongrel,而apache因为性能差距,而不被推荐。

JavaEye为什么用lighttpd + fcgi呢?原因如下:

1) lighttpd发展了好几年了,市场占有率也相当高,是一个经过实践检验的server,它的文档也很全;而nginx还没有经过足够的市场检验,文档也很缺乏
2) JavaEye的ruby进程和web server在一台机器上面跑,通过unix socket使用fcgi协议通讯可以避免tcp的网络开销,其通讯速度比使用tcp socket使用http协议通讯要快一些。

什么场合使用haproxy?

大规模部署,例如你的RoR应用到十几台服务器上面去,你用haproxy会更好,可以方便的添加删除应用服务器节点,proxy性能更好。


RoR部署方案深度剖析

关键字: deployment
RoR的部署方案可谓五花八门,有Apache/Fastcgi方式的,有Nginx/Mongrel方式的,还有lighttpd/Fastcgi方 式,也有人使用HAProxy/Mongrel,各种部署方式都是众说纷纭,让人搞不清楚哪种方式更好一些。我的这篇文章就是希望结合我们运营 JavaEye网站一年多以来的经验(通过统计Rails的production.log,JavaEye网站目前每天处理超过70万200 OK状态的Ruby动态请求,应该是国内目前负载量最大的RoR应用了),为大家剖析RoR部署方案的优劣,帮助大家选择适合自己生产环境的RoR部署方 式。

在讨论部署方案之前,先让我们看一下RoR网站部署的简单架构:



浏览器的HTTP访问请求首先达到Web服务器,充当Web服务器的一般是Lighttpd/Apache/Nginx,如果访问请求包含静态资 源,那么Web服务器就会直接从本地硬盘读取静态资源文件,例如图片,JavaScript,CSS等等,返回给客户端浏览器;如果访问请求是动态请求, 那么Web服务器把URL请求转发到后端的FastCGI/Mongrel来处理,等到FastCGI/Mongrel处理完请求,将生成的页面数据返回 给Web服务器,最后Web服务器把页面数据发送到客户端的浏览器。

从RoR的部署方式来看,主要由前端的Web服务器和后端的应用服务器构成:前端的Web服务器可以使用Apache,Lighttpd,Nginx和Litespeed,后端的应用服务器可以使用FastCGI和Mongrel,下面我们分门别类的介绍和剖析:

一、介绍Web服务器
Web服务器的主要作用有两点:一是处理静态资源,二是将动态请求分发到后端应用服务器,然后接收后端应用服务器生成的页面数据,将其返回浏览器,充当了一个信息沟通的桥梁作用,在本文当中我们重点分析后者的作用。

1、Apache 2.2

Apache是全球互联网使用最广泛的Web服务器,但在处理静态资源文件上却不是性能最优秀的Web服务器,不过一般情况下,静态资源的访问并不是RoR网站的瓶颈,因此也不必过于在意这一点。

Apache 2.2既支持HTTP Proxy方式连接后端的Mongrel应用服务器,也可以通过mod_fastcgi/mod_fcgid来连接FastCGI应用服务器:当以 HTTP Proxy方式连接Mongrel的时候,Apache接收Mongrel返回的页面数据的buffer size最大只能开到8KB(默认是4KB或者8KB),因此当页面数据超过8KB的时候,可能需要Apache和Mongrel之间发生多次交互;当以 mod_fastcgi方式连接FastCGI应用服务器的时候,接收返回数据的Buffer size仍然只有8KB而已,如果使用mod_fcgid,那么buffer size为64KB,有了很大的改善。

2、Nginx

Nginx是俄国人出品的轻量级Web服务器,在处理静态资源方面,据说性能还略微超过Lighttpd,但是Nginx在性能消耗方面略微比Lighttpd要高一些。

Nginx内置了良好的HTTP Proxy和FastCGI支持,因此即可以连接Mongrel,也可以连接FastCGI服务器,在这两种情况下,Nginx默认的接收应用服务器返回 数据的Buffer Size也只有区区的8KB,但是你可以自行设置更大Buffer Size。

3、Lighttpd

Lighttpd是全球互联网排名第五的Web服务器,也是近两年来上升最快的Web服务器,特别是很受一些著名Web 2.0大网站的欢迎,例如wikipedia的某些服务器,youtube的视频服务器,在国内,豆瓣网站和JavaEye网站都是Lighttpd的绝 对拥护者。在处理静态资源方面,Lighttpd性能远远超过Apache。

Lighttpd既支持HTTP Proxy连接Mongrel,也支持FastCGI方式,但是Lighttpd的FastCGI支持在所有流行的Web服务器当中可能是最优秀的,所以 用FastCGI的网站都很喜欢Lighttpd。Lighttpd在接收后端应用服务器返回数据的方式上和Apache/Nginx有非常大的区别:

Apache/Nginx是针对每个应用服务器连接分配固定Size的Buffer,而且默认只开8KB,这个Size对于现在网页动辄50- 100KB的情况来说,显得过于保守,如果应用服务器的返回数据无法一次填满Web服务器的Buffer,那么就会导致应用服务器和Web服务器之间多次 数据传输,这对于RoR网站的性能会造成一些相关的影响,我们在后面会详细的分析。

Lighttpd并不针对应用服务器的每个连接分配固定的Buffer,而是尽可能的把应用服务器返回的数据一次性接收下来,因此无论应用服务器返回多大的数据量,Lighttpd都是照单全收,胃口非常惊人。

4、Litespeed

Litespeed是一个商业收费的Web服务器,静态资源处理能力据它自己的评测数据比Lighttpd略高。Litespeed也同时支持 HTTP Proxy连接Mongrel和FastCGI连接应用服务器。此外Litespeed专门为单机运行的RoR开发了一个lsapi协议,号称性能最好的 RoR通讯协议,比HTTP Proxy和FastCGI都要好。但是lsapi的运行方式有很大缺陷:因为lsapi不是web server启动的时候启动固定数目的ruby进程,而是根据请求繁忙程度,动态创建和销毁ruby进程,貌似节省资源,实则留下很大的黑客攻击漏洞。只 要黑客瞬时发起大量动态请求,就会让服务器忙于创建ruby进程而导致CPU资源耗尽,失去响应。

由于Litespeed在运行RoR方面并没有表现出比Lighttpd优越之处,而且还是收费软件,企业版本售价在双核CPU上面每年收费 499美元,并且也不开源,因此我们就不再把关注点放在Litespeed上面。当然Litespeed收费也不是白收的,它提供了非常好用的基于Web 的服务器管理界面,以及非常多的安全性方面的设置参数。

5、HAProxy

HAProxy并不是一个Web服务器,他既不能处理静态资源,也不能充当浏览器和应用服务器之间的缓冲桥梁,他只是充当了一个请求分发的软件网 关作用。ThoughtWorks公司的RubyWorks选择使用HAProxy + Mongrel Cluster的方式来部署RoR应用,不能不说是一个愚蠢的方案。这种方案其实相当于把n个Mongrel应用服务器捆绑起来,直接充当Web服务器, 而Mongrel毕竟是一个Ruby写的服务器,无论是网络IO能力,还是静态资源的处理速度,无法和真正的Web服务器相提并论,让Mongrel直接 处理静态资源和调度网络IO,会造成服务器资源毫无必要的极大开销,因此HAProxy也不在我们的考虑之列。


二、分析应用服务器的处理方式

无论是Mongrel还是FastCGI,都能够良好的运行Rails服务器,但是他们在和Web服务器之间的数据传输方式上存在一些差别,而正是这些差别,对部署方式有重大的影响:

1、Mongrel

Mongrel本身可以直接充当Web服务器,但在这种情况下性能并不会好。因为Mongrel只有HTTP协议的解析部分是用C语言编写的,其 余所有代码都是纯Ruby的。在处理静态资源下载上面,Mongrel的实现方式非常低效率,他只是简单的以16KB为单位,依次读入文件内容,再写出到 网络Socket端口,其性能远远比不上传统的Web服务器调用操作系统的read()和write()库实现的静态文件下载速度,如果和现代Web服务器实现的sendfile方式的"零拷贝"下载相比,简直就是望尘莫及。

Mongrel使用了Ruby的用户线程机制来实现多线程并发,并且使用了一个fastthread补丁,改善了Ruby用户线程的同步互斥锁问 题。但是Ruby并不是本地线程,我们也不要对Mongrel的网络IO负载能力抱有什么不切实际的幻想。同时Rails本身也不是线程安全的,因此 Mongrel在执行Rails代码的过程中,完全是加锁的状态,那和单进程其实也没有太大差别。

因此,当我们使用Mongrel的时候,一般会在前端放置Web服务器,通过HTTP Proxy方式把请求转发给后端的Mongrel应用服务器。在这种情况下,Mongrel只处理动态请求,在运行Rails框架生成页面数据之后,把数 据返回给Web服务器就可以了。但是在这种部署方案下,有一个很重要的细节被我们忽视了,Mongrel运行Rails生成的页面数据是怎么返回给Web 服务器的呢?通过仔细钻研源代码我们可以搞清楚Mongrel处理Rails请求的细节:

1) Mongrel接收到请求以后,启动一个ruby线程解析请求信息
2) 加锁,调用Rails Dispatcher启动Rails框架
3) Rails处理完毕,创建一个StringIO对象,把Rails生成的页面数据写入到StringIO中
4) 解锁,把StringIO的数据flush到Web服务器

这个StringIO对象其实很重要!它充当了一个输出缓冲区的作用,我们设想一下,当Mongrel作为独立的Web服务器的时候,如果 Rails生成的页面比较大,而客户端浏览器下载页面的速度又比较慢,假设没有这个StringIO对象,会发生什么问题? Rails线程在执行render方法的时候就会被挂住!同步互斥锁没有解锁,Mongrel再也无法处理下一个动态请求了。

当Mongrel仅仅作为应用服务器的时候,这个StringIO仍然很重要,为什么?我们前面提到过了,Apache/Nginx的接收缓冲区 都只开了8KB,如果页面比较大,Mongrel就没有办法一次性把数据全部推给Web服务器,必须等到Web服务器把接收缓冲区的8K数据推到客户浏览 器端以后,清空缓冲区,才能接收下一个8KB的数据。这种情况下,Mongrel必须和Web服务器之间进行多次数据传输,才能完成整个Web响应的过 程,显然没有一次性把页面数据全部推给Web服务器快。如果Web服务器使用Lighttpd的话,情况会不一样。当Mongrel把StringIO的 数据flush出去的时候,Lighttpd是一次性全部接收下来了,不需要多次交互,因此Lighttpd+Mongrel的RoR网站的实际速度要快 于Apache/Nginx+Mongel。

Mongrel使用StringIO对象缓存输出结果,在某些特殊的情况下会带来很大的安全隐忧。我们假设使用服务器端程序控制带权限的文件下 载,某用户下载的是一个100MB的文件,该用户使用了多线程下载工具,他开了10个线程并发下载,那么每个线程Mongrel在响应之后,都会把整个文 件读入到内存的StringIO对象当中,所以总共会创建出来10个StringIO对象保存10份文件内容,所以Mongrel的内存会一下暴涨到 1GB以上。而且最可怕的是,即使当用户下载结束以后,Mongrel的内存都不会迅速回落,而是一直保持如此高的内存占用,这是因为Ruby的GC机制 不好,不能够及时进行垃圾回收。

也许你会觉得不太可能下载100MB那么大的附件,但是以JavaEye网站为例,圈子的共享文件最大允许10MB,只要用户在多台机器上面,每 台机器开100个线程下载圈子共享文件,每个Mongrel的内存占用都会立刻超过1GB,用不了几分钟,服务器的物理内存就会被耗尽,网站失去响应。这 个缺陷非常容易被别有用心的黑客利用,攻击网站。这也是JavaEye网站为什么始终不用mongrel的原因之一。

通过上面的剖析,我们知道Mongrel在使用Lighttpd的时候,可以达到最快的RoR执行速度,但是Lighttpd当前的1.4.18 版本的HTTP Proxy的负载均衡和故障切换功能有一些bug,因此一般很少有人会使用这种方式。大多数人都会采用Mongrel搭配Apache2.2或者 Nginx,但是正如我们上面做分析的那样,Apache/Nginx的Buffer Size实在是一个很讨厌的限制,特别是Apache只能最大开8KB的Buffer,因此我建议使用Nginx搭配Mongrel,并且把Nginx的 Proxy Buffer Size设置的大一些,比如说设置为64KB,以保证大多数页面输出结果可以一次性flush到Web服务器去。

2、FastCGI

很多人对FastCGI谈虎色变,仿佛FastCGI就是内存泄漏,性能故障的罪魁祸首,又或者嫌弃FastCGI太古老了,已经被淘汰掉的技术 了,其实这是一个很大的误解。FastCGI本质上只是一种进程间通讯的协议,虽然是一个比较古老的协议,但是还是比HTTP协议年轻多了,HTTP协议 不是照样现在很流行吗?

在PHP/ASP/JSP流行之前,FastCGI曾经非常普及,只不过那个时代的FastCGI程序是用C语言编写的,写起来太费劲,而 PHP/ASP/JSP相比之下,写起来就太简单了,所以FastCGI就渐渐被丢到了历史的故纸堆里面。但是最近两年来,由于Ruby和Python的 快速Web开发框架的强势崛起,FastCGI仿佛又咸鱼翻身了。

当我们以FastCGI方式运行Rails应用服务器的时候,每个FastCGI进程都是单线程运行的,考虑到Rails本身不是线程安全的,所 以和Mongrel运行Rails的实际效果是一样的,都是每个进程只能跑一个Rails实例。但是FastCGI在Rails生成页面数据返回给Web 服务器的方式和Mongrel截然不同:

前面我们说到Mongrel自己开了输出缓冲区,而FastCGI则完全不开任何缓冲区,当Rails执行render方法的时候, FastCGI实际执行的是FCGI::Stream.write方法调用,直接把数据写给Web服务器了。此时如果Web服务器是 Apache/Nginx,会发生什么?

如果我们使用mod_fastcgi模块,那么Apache的接收缓冲区就是8KB;
如果我们使用mod_fcgid模块,那么Apache的接收缓冲区就是64KB;(mod_fcgid是中国人开发的取代mod_fastcgi的开源项目,在Apache社区很受欢迎,谁敢说中国人只是开源"消费"国?)
如果我们使用Nginx服务器,那么默认的接收缓冲区就是8KB,但是可以改得更大;

如果页面数据比较大,超过8KB,会怎么样? FastCGI进程被挂在render方法上!必须等到Web服务器的缓冲区清空,把页面数据全部接收下来以后,FastCGI进程才能结束本次 Rails调用,处理下一个请求!所以千万别用Apache/Nginx搭配FastCGI应用服务器,否则你的RoR应用会死的很难看。根据我个人的测 试数据表明,同样的测试负载,Apache搭配70个FastCGI进程挂掉,但是Lighttpd搭配30个FastCGI进程轻松跑完!

当FastCGI搭配Lighttpd的时候,我们知道Lighttpd会一次性照单全收FastCGI送过来的页面数据,所以FastCGI进 程并不会被挂住。如果我们对比一下Lighttpd搭配Mongrel和FastCGI会发现,Lighttpd搭配FastCGI性能最好,为什么呢?

Mongrel首先自己会用StringIO缓冲页面数据,然后推送给Lighttpd以后,Lighttpd也在内存当中缓冲了一份页面数据, 造成了毫无必要的double buffer的开销。这自然不如FastCGI不做任何缓冲,直接推给Lighttpd性能来得高,内存消耗少了。

我们的方案分析到这里,大家应该自己心里有结论了,Lighttpd+FastCGI是性能最佳,服务器资源消耗最少的RoR部署方案,事实上目 前RoR网站部署使用最多最流行的也是Lighttpd+FastCGI方式,而JavaEye网站,自然也是这种方式的部署。因此我们可以对各种方案进 行一个性能优劣的排队:

引用
Lighttpd+FastCGI > Lighttpd+Mongrel > Nginx+Mongrel > Apache+Mongrel > Ngignx+FastCGI > Apache+FastCGI


其中Lighttpd+FastCGI是性能最佳方案,而Apache+FastCGI是性能最差方案。


有些细心的同学可能会产生一个新的疑问?你说到底,之所以Lighttpd跑RoR性能最好,还是在于Lighttpd接收数据不限定缓冲区的大 小,而Apache/Nginx限定了缓冲区大小所至。那为什么Nginx要限制呢?Lighttpd如果不限制的话,会不会导致Lighttpd内存爆 掉?

Nginx限制Proxy Buffer Size其实也有道理,因为Nginx并不是为RoR量身打造的Web服务器,Nginx最广泛的用途还是高负载大访问量的代理服务器,在Nginx主要 的应用场合,如果不做这样的限制,那Nginx端的资源消耗就相当高了,有可能会拖累所代理的服务速度。

Lighttpd主要用途之一就是提供高性能的FastCGI支持的Web服务器,所以必须为FastCGI量身打造。Lighttpd端承担的 负载越高,就越能有效的加快FastCGI执行速度。其实我们稍微心算一下,假设Lighttpd后面挂1000个FastCGI进程,每个 FastCGI进程同时送过来50KB的页面数据,Lighttpd就是全部吃下来,也不过只消耗50MB的内存而已,而事实上1000个FastCGI 进程足以支撑每日上千万的大网站了。

只有当我们使用服务器端程序控制大文件下载的时候,有可能造成Lighttpd内存暴涨,例如某个用户使用100个线程并发下载JavaEye圈 子的共享文件,在没有特殊处理的情况下,Lighttpd将全部吃下100个FastCGI进程送过来的10MB数据,就会立刻暴涨1GB的内存。这种情 况怎么办呢?其实我们也有办法让Lighttpd一点内存都不吃, 请看我写的另外一篇文章:RoR网站如何利用lighttpd的X-sendfile功能提升文件下载性能

可能很多人看了我的文章,对结论觉得很诧异,既然Lighttpd+FastCGI这样好,为什么那么多人都推崇Mongrel,否定FastCGI呢?我想,不外乎几个原因:

一、Lighttpd+FastCGI配置起来比较专业,而Mongrel配置简单

尽管我当初第一次搭建Lighttpd+FastCGI环境没费什么周折,但是我观察到非常多的Ruby程序员很难成功搭建一个Lighttpd +FastCGI的环境出来,很多人连Lighttpd都无法独立的运行起来。这也许是因为很多程序员习惯了Windows开发环境,对于Unix上面通 过源代码编译安装的方式过于陌生造成的。而我从97年开始使用Unix,至今已有10年历史,因此搭建这样简单的系统,对我来说不造成什么障碍。

而Mongrel就简单了,gem install mongrel安装完毕,mongrel_rails start启动,哪个人不会?毕竟绝大多数开发人员和部署人员不是高手,他们熟悉哪种方式,自然就会推崇哪种方式。


二、Mongrel可以独立作为Web服务器运行,开发环境和部署环境统一

一般来说,程序员肯定是尽量保持开发环境和部署环境的一致性,避免部署到生产环境出现不测的后果。既然在开发环境熟悉了Mongrel,当然更加愿意在生产环境使用Mongrel,而不愿意碰没有接触过的Lighttpd。


三、Mongrel支持HTTP协议,因此不论监控还是集成其他服务都比较简单,容易玩出更多的花活。

HTTP协议要比FastCGI协议普及的多,因此通过HTTP方式的监控工具,群集管理工具,集成其他服务的工具都是一抓一大把。而支持 FastCGI的第三方工具就少得可怜了。你要玩很多花活出来,用FastCGI的话,就难免得自己开发相应的工具,那当然不如使用Mongrel方便 啦。

最后,如果你看了这篇文章,想摩拳擦掌的安装一把Lighttpd+FastCGI的话,那么我的这篇文章就是最好的安装指南:

在Linux平台上安装和配置Ruby on Rails详解



JavaEye网站的RoR性能优化经验谈

JavaEye网站从2006年9月11日上线基于RoR的2.0版本开始,到现在已经运行了将近一年半了。在这一年半的时间里,JavaEye网站的每 日PV从最开始的5万,缓慢增长到了现在的60万。随着网站负载的不断增加,我们也在不断尝试和调整网站的性能,积累了不少第一手RoR应用性能优化的实 战经验。虽然我们并不是RoR性能优化的权威专家,我们所积累的经验也许并不是最优实践,但是作为国内最早涉足RoR商业运营的互联网网站之一,我们非常 乐意分享和交流我们的实战经验,以帮助后来者节省必要的摸索时间。

RoR惊人的开发速度恐怕是每个互联网创业者都梦寐以求的,但是随着网站流量的不断增大,可能大多数采用RoR的网站或迟或早会遇到RoR的性能 瓶颈,我的一个朋友capitian说过一句很有意思的话:"RoR应用做到后来,总有自己修改底层的冲动"。就我所了解和掌握的情况来看,很多RoR网 站都过早的遇到了性能瓶颈,一个很普遍的现象就是:RoR应用的CPU负载要远远高于数据库的负载。这是一个有点违背常理的现象,因为我们知道,硬盘IO 速度要比内存慢得多,所以一般Web应用的性能瓶颈往往会出现在数据库IO上,因此优化数据库访问,进行对象缓存是非常有效的性能优化手段。但是一旦应用 服务器负载比数据库还高的话,单纯的对象缓存就无用武之地了。下面我们从几个方面分别谈一谈如何进行RoR的性能优化:


应用的部署

RoR应用的部署包括操作系统,Web服务器,应用服务器和数据库四个方面:

一、操作系统

1、发行版本

RoR适合于部署在Unix类操作系统上面,通常比较多的人使用RHEL/CentOS/Ubuntu,我们比较偏爱SuSE Linux,对于我们服务器使用的AMD Opteron x86_64的CPU来说,SLES要比RHEL有更多的优化。另外应该尽量使用64位版本操作系统,以充分发挥x86_64 CPU的性能,并且x86_64的Linux很多Kernel参数也大很多,代价就是需要更多的物理内存。

2、文件系统

Linux最常用的文件系统是ext3,但我们使用的是Reiserfs文件系统。Reiserfs在读写大量小文件的目录性能非常高,即使处理 目录下面直接存放10万个文件,性能仍然不会下降。我们知道默认情况Rails会对每个浏览器会话在硬盘生成session文件,一个繁忙的网站,临时文 件目录下面有上万乃至几万个session文件是很常见的现象。对于这种目录下面几万个小文件的存取,reiserfs要比ext3性能高一个数量级。如 果希望对session文件有更好的存取性能,可以把临时目录链接到Linux的内存文件系统/dev/shm目录下面,这样实际上session文件的 存取都是直接内存操作了,这种方式唯一的问题在于不能支持群集部署。如果你已经升级到了Rails2.0,可以采取把session保存到Cookie里 面的方式,既可以避免服务器处理session的开销,而且还支持群集部署,是大规模网站部署的首选方式。

3、内核的网络参数调整

对于流量很大的网站来说,默认的Linux内核网络参数偏小,因此如果你的网站流量非常大,或者上传下载大文件比较多,可以针对性的调整内核网络参数,扩大内核的TCP接收数据和发送数据的Buffer缓冲区大小,比方说:
引用
net.core.rmem_default=262144
net.core.wmem_default=262144
net.core.rmem_max=262144
net.core.wmem_max=262144
net.ipv4.tcp_rmem=4096 65536 524288
net.ipv4.tcp_wmem=4096 65536 524288

参数具体调整,可以Google相关的Linux内核参数的文档,这里不展开详谈。


二、Web服务器

Web服务器首选Lighttpd,因为Lighttpd在和后端的应用服务器通讯方式上做了足够的优化:当POST大数据量的时候, Lighttpd在完整的接收客户端浏览器的数据之后,才会一次性发送给应用服务器;同样的,Lighttpd也是一次性把应用服务器处理的页面数据全部 接收,不设置Buffer Size的限制。因此Lighttpd能够尽最大可能的减轻应用服务器的负担,减少应用服务器用于处理数据传输的延迟,更加有效的利用应用服务器资源。这 方面的详细的论述请看:RoR部署方案深度剖析

关于Lighttpd的安装可以参考在Linux平台上安装和配置Ruby on Rails详解,这里仅谈Lighttpd的性能优化的几个要点:

1、网络IO调度方式
Linux Kernel 2.6支持sysepoll方式调度网络IO,能够处理极高的并发连接请求,Lighttpd可以通过配置文件打开sysepoll支持:
引用
server.event-handler = "linux-sysepoll"


2、网络IO传输方式
Linux Kernel 2.6支持sendfile方式传输数据,Lighttpd可以通过配置文件打开sendfile支持:
引用
server.network-backend = "linux-sendfile"

此外Lighttpd还支持应用服务器参与的文件下载控制X-sendfile,详细的论述请看:RoR网站如何利用lighttpd的X-sendfile功能提升文件下载性能

3、文件状态缓存
Lighttpd通过stat()调用获得文件被修改的信息,来决定当请求同一个静态文件资源的时候,是否需要再次读取硬盘文件。但是每次 stat()调用也有一定的开销,Lighttpd支持通过Fam Server来减少stat调用。即每次当文件被修改之后,Kernel会发送一个消息通知Fam Server,而Lighttpd会通过进程间通讯连接Fam Server,可以知道文件是否被修改的信息,不必再每次调用stat()。
引用
server.stat-cache-engine = "fam"


4、限定POST Size
为了避免黑客恶意的攻击服务器,伪造超大Post数据包轰炸Web服务器和应用服务器,可以限制Request请求的大小,例如限制为10MB:
引用
server.max-request-size = 10240


5、日志文件
Lighttpd是单进程单线程的服务器,调度网络IO性能是极高的,但是在某些极端情况下,单进程服务器也有风险,即一旦被某操作系统调用挂 住,整个服务器就没有办法响应请求了。比方说服务器其他进程导致的IO WAIT很高,操作系统的buffer又不够的时候,Lighttpd在大量的写access log就有被挂住的可能性。因此如果Lighttpd日志对你的参考价值不大,可以考虑关闭掉。像JavaEye网站每天Lighttpd产生430万条 log,对硬盘IO也是一个不小的负担,既然已经开着Rais的production.log,那么Lighttpd的access log没什么参考价值了,那就关掉它。

Lighttpd的性能优化请看其作者写的文章:
http://trac.lighttpd.net/trac/wiki/Docs%3APerformance


三、应用服务器

Ruby的应用服务器可以使用FastCGI,或者Mongrel,如果我们使用Lighttpd的话,FastCGI是最好的搭配。

1、FastCGI和Lighttpd的通讯方式

如果FastCGI和Lighttpd是在同一台服务器,那么建议采用Unix Socket通讯,这种通讯方式比TCP要快一些,FastCGI可以通过Lighttpd自带的spawn-fcgi命令行工具启动,创建socket 文件,而Lighttpd监听socket文件。如果两者不在同一台服务器,需要群集部署,那就必须采用TCP Socket通讯,方式是一样的。

2、FastCGI进程应该开多少个合适?

Rails是单进程方式运行的,理论上来说,开几个FastCGI进程,就只能并发响应几个请求。对于繁忙的网站来说,峰值期间每秒有几十个动态 请求是很正常的事情,但实际上FastCGI进程并不需要开那么多。这是因为前端的Web服务器在处理用户浏览器连接,发送Request请求需要相当长 的时间,在FastCGI处理完请求释放该连接以后,Web服务器还需要相当长的时间才能把页面数据完整的发送到客户端浏览器。用户在点击一个链接以后, 等待1-2秒,页面内容就显示出来,这对用户的感觉来说已经是非常快的了,而FastCGI用于处理该请求可能只需要0.1秒,那么一个FastCGI进 程虽然并不能够真正的并发运行,但实际上的效果是他可以在1秒之内处理10个请求,让10个用户在同时访问网站的过程当中感觉不到明显的延迟。

因此FastCGI需要开多少个,取决于你的网站峰值期间每秒有多少个用户请求过来,而你的FastCGI又能够以多快的速度处理请求。比方说你 的网站峰值期间每秒有50个动态请求,FastCGI在峰值期间处理每个请求需要0.2秒,那么实际上你只需要开10个FastCGI进程就足够了,为了 应付突发的峰值请求,你可以在这个计算量上面增加一些余量,比方说15-20个进程,肯定是绰绰有余了。

关于FastCGI的性能优化,可以参考Lighttpd作者的文章,虽然他是针对PHP跑FastCGI写的,但对RoR也有参考价值:
http://trac.lighttpd.net/trac/wiki/Docs%3APerformanceFastCGI



四、数据库

JavaEye网站使用MySQL5.0.XX版本,数据库引擎是InnoDB。关于MySQL数据库的调优,推荐大家看MySQL Performance Blog, 作者是一个MySQL性能调优方面的专家,并且提供MySQL咨询服务。他的博客上面有很丰富的关于MySQL调优的文章和演讲文稿,特别是关于 InnoDB方面,非常深入。JavaEye的数据库调优就是根据他的InnoDB演讲文稿来调整的,一般说来,有几个需要调整的参数:

innodb_buffer_pool_size
这个参数很重要,越大越好,对于专用的数据库服务器一般建议开服务器内存的50%以上。

query_cache_size
查询缓存,对于查询的性能提高有很大帮助,但不宜开得过大,查询缓存的过期可能很频繁,过大查询缓存反而降低性能,增加服务器开销

innodb_flush_method = O_DIRECT
针对InnoDB的数据文件,关闭操作系统的文件缓冲,由于InnoDB自己有巨大的Buffer Pool,操作系统对文件的读写缓冲功能反而会降低MySQL的InnoDB的IO性能。

最后针对数据库的SQL优化来说有两点原则:

1、对数据库表要适当的创建索引
特别是出现在where查询条件当中字段,和关联查询当中的外键,要高度注意。

2、尽量避免大表的全表扫描和数据库的硬盘IO
查询比较慢的SQL要explain一下,看看是否发生了全表扫描,采取各种措施减少或者避免大表的全表扫描问题,例如拆分表等等。

最后针对MySQL数据库运行情况,我们可以用show status; 和 show innodb status\G 来监测。


Rails应用程序的优化

Rails应用程序优化包括ruby解析器的优化,缓存的使用,以及应用代码级别的优化。Stefans Kaes曾经在Railsconf 2006有一个Rails应用程序优化的演讲,他的演讲PPT是极好的Rails性能优化指南,可以在这里下载:http://www.javaeye.com/topic/24508 。他还编写了一个用于Rails性能测试的软件包RailsBench,大家可以参考。由于Stefans Kaes的代码优化文档已经写的非常详细了,因此我就不在一一复述,只提出几点对性能影响比较大的方面:


一、ruby解析器的优化

ruby的解析器性能是很糟糕的,ruby早期的主要用途是取代perl写批量处理的脚本的,并不是为服务器应用编写的,因此在内存分配策略上非 常不适合服务器应用。Stefans Kaes编写了一个ruby GC的补丁文件,在railsbench下载包里面提供了。虽然当前Railsbench提供的GC补丁只有针对ruby 1.8.4和1.8.5版本的,但是在ruby 18,6上面使用1.8.5的GC补丁也完全没有问题。GC补丁的作用主要是针对Rails应用开大了ruby的内存堆,可以有效提高内存堆的利用率,降 低GC的频率。根据Stefans Kaes提供的测试数据,打补丁并且调整参数以后,GC的频率下降到只有原来的1/10还不到。降低GC频率尽管并不能够提高单个请求的执行速度,但是可 以增加整体应用的负载能力。

我们在JavaEye的服务器上也使用了GC补丁,并且根据推荐参数进行了调整。在使用GC补丁之后,Web服务器的CPU负载下降了大概15% 左右,效果非常显著。当然开大内存堆的代价就是ruby进程会多消耗内存,在我们的服务器上,ruby打补丁之后多消耗了50%左右的物理内存。


二、缓存的使用

1、对象缓存
JavaEye上面关于对象缓存的讨论很多,我们也提供了JavaEye这方面很多数据,因此不展开了。RoR可以使用两个对象缓存,一个是 CachedModel,类似Hibernate,比较简单,对Model的CRUD操作自动进行缓存;另外一个是cache_fu,需要自己编码来添加 对象缓存,但提供了更多高级机制,目前我们使用的是cache_fu。在使用对象缓存的情况下,应该把查询方法的:include去掉,避免关联查询无法 利用缓存的现象。

2、查询缓存
对于统计类耗时查询,如果不要求实时性,那么可以使用memcache-client将查询结果缓存到memcached里面,例如博客排行榜之类。

3、页面局部缓存
对象缓存和查询缓存都是降低数据库访问负载的,但如果RoR的负载很高,那么只能依靠页面局部缓存了。传统的互联网web1.0网站很流行采用动 态页面静态化技术来提高网站的负载,但是对于web2.0网站来说,每个页面都带有登陆用户的个人信息,页面的很多部分需要实时更新,例如投票,点击统 计,digg,显示用户在线状态等等,动态页面静态化非常困难。当然如果你非要采用动态页面静态化,技术上也不是实现不了,可以通过AJAX请求来处理静 态页面的动态部分,但是这种解决方案的开发成本过高,而且性能未必会有明显的改善,大家看看新浪和搜狐博客就知道这种技术被应用的有多糟糕了。

web2.0网站比较常用使用页面局部缓存,一种情况是页面不需要实时更新的,那么只需要设置一个合理的过期时间就行了,这种情况我们目前使用的 比较多;另外一种情况是虽然不需要实时更新,但是会在用户执行某些操作后需要缓存过期,比方说博客个人主页的很多页面,这种情况下缓存过期策略会比较复 杂,考虑到合理的开发成本,我们尚未对这样的页面使用局部缓存。

此外,Rails的页面局部缓存有一个缺点,就是和页面查询结果对应的Action当中的查询语句要放在View里面,否则每次action里面的查询还是会被执行,但是这样做会破坏程序代码良好的MVC结构。这种情况下,也可以采用另外一个Cache插件: better rails caching,在缓存页面的同时可以缓存Action当中的查询语句。


三、应用代码的优化

Stefans Kaes的文档里面对应用代码的优化进行了非常详细的介绍,因此我这里只提两个比较重要的注意事项:

1、link_to
Rails的link_to是非常慢的,它的代码实现过于复杂,特别是Rails1.2引入了REST以后,大量的命名路由被使用,这些命名路由 还需要通过一次method_missing,那就更加缓慢了。因此对于被频繁使用的内部URL地址,一定要自己用字符串拼接方式改写,可以很明显提高 View的render性能。此外类似的helper还有很多,例如button_tag,image_tag啥啥的,如非必要,尽量不用他的 helper

2、正则表达式
ruby的正则表达式也是极慢,例如auto_fix这个helper的正则表达式就比较复杂,造成的结果就是一但大量使用auto_fix, View的render就明显变慢,类似依赖正则表达式进行字符串过滤的helper有很多,如果需要频繁大量使用,请先自行做benchmark。



Rails应用程序的内存泄漏问题和解决

内存泄漏是服务器端程序经常遇到的,有时候内存泄漏问题会让人很头疼,总体来说,Rails的内存泄漏问题比Java要少得多,这是因为Java内存泄漏最常见的三种情况在Rails当中不存在:

1、HttpSession导致的内存泄漏
Java程序员喜欢往session里面丢很多东西,最糟糕的是竟然有很多框架软件也肆无忌惮往session里面丢状态数据,但Rails的session是不放在内存里面的,所以无此烦恼。

2、数据库连接释放不彻底
Java的数据库连接池释放不彻底,以及查询游标释放不彻底,都必然导致内存泄漏。Rails没有数据库连接池,而是每个进程持有一个长连接,因此不存在这个问题,而且由于持有长连接,也不存在Java里面的OpenSessionInView的烦恼。

3、用静态变量持有全局共享数据
Java程序员很喜欢通过静态全局变量来持有共享数据,但共享数据忘记清理的话,也很容易导致内存泄漏,Ruby是SNA架构,多进程服务器模式,进程间无法共享数据,反而避免了全局共享数据带来的麻烦。

但是Rails应用有一种情况:在Ruby代码中调用C写的第三方ruby类库的时候,很容易导致内存泄漏,但这种内存泄漏反而在Java中极其 罕见。Ruby本身有GC来管理内存堆,但是代码一旦调用C写的第三方ruby类库,内存堆的分配权就掌握在第三方C库的实现上面了,如果这个C库的代码 质量不够好,内存泄漏就不可避免。由于ruby本身性能很差,因此计算量大的功能往往依赖底层的C库来实现,这下内存泄漏的潘多拉魔盒就打开了!而 Java性能比较好,功能都是纯Java编写,基本上看不到需要依赖第三方C库的情况,因此比较安全。

JavaEye也面临着内存泄漏的困扰,这方面困扰主要来自于Rmagic。Rmagick调用ImageMagick的C库来完成图片的操作, 从我们的监测来看,RMagick大多数情况下会缓慢的泄漏内存,在某些特定的图片操作上会急剧的泄漏内存。解决办法就是用mini_magick替代 Rmagick,mini_magick是直接调用ImageMagick的mogrify命令,另起一个进程来操作图片,操作完进程就结束了,绝无后 患,由于Linux的fork进程开销不大,因此也不必担心性能问题。

此外,调用第三方C库的ruby代码编写都需要高度小心,比方说JavaEye使用ferret实现全文检索,根据应用的需要调用ferret的 API来编写自己的analyzer,其中在实现token_stream方法上面使用了XXXAnalyzer.new和XXXToken.new, XXXFilter.new,结果内存急剧泄漏,经过检查发现是Analyzer对象不能被反复创建,改成创建后缓存该对象就好了,但是Filter和 Token对象却必须每次创建,此外ferret的PerAnalyzerFilter也有内存泄漏问题。由于类库是用C编写的,单纯看API文档或者看 源代码片断一般无法判断出里面的内存泄漏陷阱的。


当遇到了难以解决和定位的内存泄漏问题,Ruby也有类似Java的内存Profiler工具:

1、Memory Profiler
一个纯ruby编写的内存探测器,原理很简单,就是用ruby的对象引用计数器ObjectSpace.each_object去遍历内存堆中的 每个ruby对象,进行统计和分析。用起来很简单,非常适合于开发环境下侦测内存泄漏问题,但不能用在生产环境下,极度影响Rails性能。

2、Bleak_house
Bleak_house给Ruby解析器打了补丁,插入相关的指令,可以从底层探测整个ruby内存堆中对象的情况,然后你可以定期dump出来 完整的内存堆里面的所有对象,再用bleak工具去分析dump文件,他比上面的工具分析的信息要全面,可以在测试环境和预发布环境下使用,但在生产环境 下,也会对应用的性能产生很大的影响,要慎用。

JavaEye网站在RoR性能方面的经验就全部分享给大家了,也希望做RoR的朋友都拿出来自己的经验和大家分享,共同学习和促进RoR的应用和普及。

No comments: