Tag Archive for 'rails'
February 16th, 2010 by 张磊
继续压缩话题。之前写了 Rails生成压缩的静态缓存,以及配置nginx以支持直接发送压缩文件两篇文章。今天谈的是在Rails中,将大段文本内容以压缩格式存在数据库。这也是从公司的一个项目中获得的灵感。 原始需求 手头有一个结构简单的文本库,可以看做是key=>value。约180万条数据,2.8G(确实不算大,见笑了)。 这个库查询频繁,CPU在IOWAIT耗时颇多。 Linode服务器硬盘紧张,而CPU富裕。 若能通过压缩减小体积,备份/回导时也会更省时。 实施过程 整个实施过程做到了无缝转换,不需要停服务。 第一步:将文本字段类型由TEXT修改为BLOB;新增一个is_gzip字段用于标识是否经过压缩,默认为0。 * 需要注意,BLOB和TEXT最长支持65535字节。 第二步,修改Rails相应的Model。代码示例(压缩content字段): class TextData < ActiveRecord::Model def content if self.is_gzip Zlib::Inflate.inflate(super) else super end end def content=(info_content) super Zlib::Deflate.deflate(info_content, 9) self.is_gzip = true end end 再一次感叹super的便捷。修改代码后,需要重新部署一下Rails应用。 第三步,创建一个rake任务,用来批量做转换。关键部位只消这么做: text_data.content = text_data.content text_data.save 第四步,在做过修改的表上做一次optimize table,以释放那些不需要的空间。 效果观察 经如上处理,2.8G数据只剩1.4G。如果用了Gzip而不用Deflate,有可能更小。 IOWAIT未见明显下降。压缩前,单条记录理论上可以通过一次IO完成。 备份的速度确实有所提升,意料之中。 更多信息 这种思路同样适用于PHP、Python以及其他语言下的Web应用,只是在Rails中搞起来更为轻松。 Mysql提供了Compress/Uncompress两个函数,但是不建议直接使用。一方面会增加数据库服务器计算的压力;另一方面,如果用了主从,每个服务器都得算一次。 Zlib::Deflate.deflate 的第二个参数是压缩的level。我用随机数据测试,Level 9压缩后的体积比Level 1 [...]
February 8th, 2010 by 张磊
最近我的Linode服务器硬盘吃紧,一个16G的分区,使用率达到99%,就快无立锥之地了。某个Rails应用,生成的静态文件占用了超过9G的空间,并且还在不断增长。最初没想到好办法,想,难道要被逼升级服务器?后来联想到WP-SuperCache的原理,计上心来:用GZip压缩这些静态文件。 压缩文件做缓存有什么好处? 文本压缩的比例够大,这样做节约 压缩好之后,无需Web服务器再做压缩 那,会有什么问题? 不支持GZip的客户端,只能看到一堆乱码。据我观察,YSlurp疑似不支持GZip。 CPU的开销会增加 第一个问题也可以通过web服务器的一些逻辑判断,给不支持GZip的客户端返回未经压缩的内容(WP-SuperCache就是这么做的)——但个人感觉意义不大,大部分客户端都支持GZip,Google的SPDY(自备翻墙工具)都强制压缩强制SSL了。对第二个问题,VPS上主要是内存不够,CPU资源相对富裕。 在Rails中,做到这点非常轻松。原Controller中代码大致如下,无需改动这些代码,即可把静态页面缓存改为GZip格式。 cache_pages函数原型在action_pack/lib/action_controller/caching/pages.rb 中,大致的调用顺序为: 其中,class.cache_page函数进行了写入文件的操作,写入的位置是由class.page_cache_path决定的。看到这里,难道要撸起袖子hack Rails框架?No,只要在ApplicationController中加入几行代码,覆盖这两个函数即可做到。 def self.cache_page content, path # 若目录不存在,则创建,以免写入时报错 FileUtils.makedirs(File.dirname(page_cache_path(path))) Zlib::GzipWriter.open(page_cache_path(path)) do |gz| gz.write content end end # 使用super可以调用父类的同名方法 def self.page_cache_path path super + “.gz” end 将上面代码加入后,生成的静态缓存,就变成.gz后缀的压缩文件了。此外,Ruby的super函数真是一个很棒的玩意。 但这仅做了一半,我们还需要配置WEB服务器以支持直接发送GZip压缩文件。对Apache来说,可以大部分照搬WP-SuperCache的Rewrite规则;对nginx来说,还需要写一些配置。下一篇文章,将谈谈在nginx中如何做配置以支持直接发送GZip文件。
September 27th, 2009 by 张磊
入职3个月,参与的第一个大项目已经上线。于是开始思考对现有Web开发框架的改良,或者考虑另起炉灶搞一套。思考的过程中形成一些想法,现记录下来。 没有用过Rails的话,大概会想到把一切业务都抽成配置;使用过Rails后,更推崇约定大于配置。 约定大于配置,不仅体现在分发的过程,也体现在类、方法的命名,更重要的是贯穿在框架中的思想。不管是维护一大堆配置文件,还是维护一个庞大的配置文件,都不是个好主意。实际上,大多数配置都可以被约定的。 分发:对业务简单的页面,进行一次分发足够了;对业务复杂的页面,可能需要进行第二次基于业务的分发。应当假设框架不了解具体业务,但要保留二次分发的灵活性。 框架应该做多少事,是个需要拿捏的问题,过度设计非常容易。一个框架的目标不是把代码用配置来代替,也不是把代码用约定来代替。 引入AOP的机制很有必要。过滤器能帮忙做许多事,使真正业务部分的逻辑更清晰。 高来高去一通,其实最大的问题还没说。开发过程中发现,每次请求后台服务和向模板传递变量时,总会遇到数组中字段名称不一致的问题。可能后台给了一个trans_id,但模板需要user_trans_id,于是程序很多时候都在做“改名”。要解决这个,还得靠约定。 自己思考并设计一个框架的过程中,更加觉得Rails妙不可言,推荐没有使用过Rails的Web开发者关注下。
June 18th, 2009 by 张磊
NewRelic是一家提供Rails性能监测服务的网站,我很早就注册了。好多好多上线以后,我本没打算用,但最终被NewRelic孜孜不倦地发送Sample Weekly Report的精神打动,试用了一下。 安装和配置很简单:只需要安装一个NewRelic插件,然后用NewRelic提供的配置文件替换掉默认文件(操作细节可参阅FAQ)。Rails应用启动后,就会自动向NewRelic发送数据。 而NewRelic提供了不同级别的监测功能,免费的是Lite版本,最高有Gold版本,价钱有点小贵。我开始使用Lite后,NewRelic赠送了一个Gold版试用的优惠码,得以体验一周的Gold版本。 在监控页面上方有两个菜单: 第一个用来选择监控的对象,如果不选就是当前Application的所有Instances。我在服务器后台开了5个thin进程,全部被NewRelic识别出来。 针对不同版本,提供了不同的监测功能。免费的Lite版只能有Overview和Controllers去看,Apdex其实会显示在Overview里面。 OverView页面的截图(点击有大图): 5张图表分别展示了响应时间、Apdex Score(应用性能指数)、CPU利用率、内存占用情况以及数据库的活动情况。这里只能得到一些总体的数据。因为好多好多流量不大,而且全站都采用了静态缓存,所以图表也没那么好看 。 由于可以试用Gold版本,所以也尝试了Gold版本的一些功能。比如有一个Controller Report。 从这里可以看出,好多好多最耗时的两个控制器是MiscController#go 和 IndexController#index 。前者是由于需要和淘宝的API进行交互,获得跳转地址;后者是因为要随机获取多个类别的热卖商品,还有优化的余地。可以想象,如果是一个提供更复杂功能的网站,这个Controller Report很容易帮开发者定位性能问题。实际上,这也是监测程序存在的最大价值。 NewRelic还有不少别的功能,可以从多个角度监测Rails应用,绝对值得一试;另一个值得称赞的是便捷的安装配置,几乎是“无痛”的。不过Gold版本的服务很贵,要$200每月,一看就不是给小网站们准备的。
May 4th, 2009 by 张磊
51放假我没能消停,放在Linode(Linode服务介绍)的服务器出状况了。看起来负载太高(load average 长期高于3),观察发现CPU频繁处于IOWAIT状态。而在Linode后台的统计图显示,IORate持续高于3k。要知道,Linode默认IORate高于300时就会发邮件报警。 开始我还没意识到问题的严重性。直到去查询服务器上Rails应用的日志,发现处理一个请求最长居然要70秒(!)——平时只要300ms左右。一下懵了,赶紧查问题。iostat显示对swap的读写非常多,同一时刻等待io阻塞的进程达20个。考虑到服务器不只跑着 nginx/mongrel/mysql还跑着一个sphinx,我改了一下程序,减少了对sphinx的查询。情况有所好转,但依然不稳定:IORate会突然冲高,之后回落。 只得在Linode发一个support ticket,描述情况后,那边的技术支持向我要free -m的输出和vmstat 的输出。提交了输出后,那边邮件回复,咬定我的服务器发生了swap抖动。全文如下: You are quite a bit into swap, and it appears your Linode is swap thrashing. You’re either going to want to tweak the app(s) that are consuming RAM or upgrade to a larger Linode plan. As a general rule, we don’t recommend swap sizes larger than [...]
April 15th, 2009 by 张磊
开始用Mongrel部署rails应用时,我参考这篇文章,设置了一下rails的log rotate。当时传入的是”daily”:每天一个日志文件。没想到每天日志切换后,大部分Mongrel进程都会挂掉,只剩下一个可用的。跟踪日志发现,在每天切换日志文件时,只有第一个进程可以成功切换,其它进程因为新的日志文件已经存在,全部挂死。 仔细思考之后我认为,对日志的rotate其实不该放在应用程序里进行。对需要长时间运行的应用来说,只需要向固定的位置输出日志,然后在外部按照需要做截断处理——可以按天、按周甚至按月,如有必要可以进行压缩。这也符合Unix哲学。 我尝试过rotatelogs、cronolog以及logrotate,觉得logrotate更好用。在ubuntu服务器上可以用apt-get安装: apt-get install logrotate 之后可以打开它的配置文件进行编辑: vim /etc/logrotate.conf 详细的参数说明可以看它的man page,我的配置如下: /path_to_app/log/production.log { daily #按日阶段 missingok rotate 7 #保留7天 compress #压缩 delaycompress #不压缩前一个(previous)截断的文件(需要与compress一起用) dateext #增加日期作为后缀,不然会是一串无意义的数字 copytruncate #清空原有文件,而不是创建一个新文件 } 保存之后运行: /usr/sbin/logrotate /etc/logrotate.conf 新的配置就能生效了。 不光对rails应用的log可以这么做,任何日志拿来给logrotate处理,都一视同仁。
March 24th, 2009 by 张磊
Robbin写过一篇《监视Rails进程内存泄漏的技巧》,谈到了“如何监控rails进程的执行性能”。因为production.log文件已经记录了每一次请求执行的时间,所以通过一些命令的组合,把需要的数据提取、排序、输出到文件就可以了。但windows服务器上没有这些命令(如grep, awk等),怎么办呢?这篇文章能给出个解决办法,虽然稍显复杂。 要在windows上拥有linux的命令,Cygwin是一个办法,但我总觉得安装Cygwin太麻烦,于是我找了UnxUtils。下载后解压,然后在环境变量PATH中增加UnxUtils/usr/wbin目录。常用的linux命令已经被移植到windows了,包括但不限于wget, g(un)zip, tail, grep。 robbin在文章中提到的组合命令是这样的: grep “200 OK” production.log | awk ‘{print ”ALL: ” $3 ” View: ” $8 ” DB: ” $12 ” URL: ” $17 }‘ | sort -r | head -n 500 > timing.log 我们需要进行一些修改: 首先,UnxUtils没有提供awk,但有个替代品gawk。同时,在windows上的gawk不能用形如’{…}’这样的参数格式,而只能用”{…}”(两边只能用双引号),相应地,内部的语句如果使用了双引号,就需要进行转义、 其次,UnxUtils没有提供sort命令,不过Windows提供了。但这里要稍作修改,把sort -r 改为sort /r。 修改之后就可以使用了,命令改为: grep “200 OK” production.log | awk “{print \”ALL: \” $3 \” View: \” $8 \” DB: \” $12 \” URL: \” $17 }” | sort /r | head -n 500 > timing.log 如果想要更方便点,可以做一个批处理文件,在生成timing.log以后用记事本打开: grep “200 OK” production.log | awk “{print \”ALL: \” $3 \” View: \” $8 \” DB: \” $12 \” URL: \” $17 }” | sort /r | head -n 500 > timing.log notepad [...]
February 3rd, 2009 by 张磊
使用wordpress的人应该听过wp-supercache的大名,这是一款wordpress插件,用以为blog上的内容生成静态缓存。这样,即使有再多的访问请求,服务器也只需要处理静态文件。我不知道王小峰有没有用wp-supercache,只看到他在抱怨wp太慢(确实挺慢)。 我在使用wp-supercache过程中发现它非常棒,让人甚至感觉不到缓存的存在。我登录以后,在文章页面上会显示出编辑文章的链接,退出登录打开就是一个普通浏览者。我好奇,它是如何判断访问者身份的呢?昨天终于有机会一探究竟,然后在自己的项目中也实现了一个类似的super_cache。 其实奥妙在于wp-supercache在.htaccess中写入的一条段涉及到rewrite的内容: RewriteCond %{REQUEST_METHOD} !=POST RewriteCond %{QUERY_STRING} !.*s=.* RewriteCond %{QUERY_STRING} !.*p=.* RewriteCond %{QUERY_STRING} !.*attachment_id=.* RewriteCond %{QUERY_STRING} !.*wp-subscription-manager=.* RewriteCond %{HTTP_COOKIE} !^.*(comment_author_|wordpress|wp-postpass_).*$ RewriteCond %{HTTP:Accept-Encoding} gzip RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html.gz -f RewriteRule ^(.*) /wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html.gz [L] 标红的这一行的作用是,如果cookie中包含comment_author/wordpress/wp-postpass_这样的字符,则不进行rewrite。也就是说,当我登录以后,打开任何一个页面都是没有被缓存过的页面;而如果我退出,相应cookie将被清空,此时如果存放缓存的目录有生成好的缓存文件,则进行rewrite,服务器将静态文件发送给我。 我恍然大悟。大部分人其实是“游客”,所以不会带有导致缓存不工作的cookie,这种缓存方式很适合wordpress这样的cms。于是我考虑把这种缓存方式迁移到我的rails项目中。 rails自带了page cache, action cache 以及fragment cache(还可进行对象缓存,rails还有sql cache,暂不讨论)。其中page cache就是生成静态文件在服务器上,速度最快;action cache速度其次,因为毕竟把action输出的内容缓存了,消耗的是进入rails、执行前置后置过滤器的时间;fragment cache就是页面中的片段缓存,速度最慢。 但rails的page cache默认会生成文件到/public目录。比如这个地址:http://www.niupu.com/subject/311898/,默认生成的page cache是 /public/subject/311898.html。如果下次再有人访问,会直接把静态文件返回。在这种情况下,虽然也可以通过校验cookie使该请求不用静态文件去响应,但这样有点“逆天而行”的感觉,而且很难操作。如果能把缓存文件放到个别的地方(就像wp-supercache),那就好了。 好在rails提供了足够的灵活性。首先需要更换一个保存缓存文件的目录,在environment.rb的 Rails::Initializer.run 这一block中加入一句: config.action_controller.page_cache_directory = RAILS_ROOT+”/public/cache/” 这就把存放缓存文件的位置,由/public目录改到了/public/cache目录。 然后在相应Controller中加入page [...]
February 1st, 2009 by 张磊
以前写过三篇关于ultrasphinx和sphinx的文章,都是关于如何更好地利用sphinx来做搜索引擎。最近在我需要在一台windows服务器上部署一个rails系统和相应的sphinx,积累了一些心得写出来与大家分享。如果你的服务器是linux,这篇文章应该对你没什么作用。 首先简单解释下这几个名词,sphinx是一个全文检索软件,类似于lucene;ultrasphinx是一个rails的插件,可以让rails项目轻松使用sphinx;而我使用的sphinx是由coreseek改造过的版本,加入了中文分词的算法(mmseg)。 1、以服务的方式运行sphinx 在开发环境中,只要执行”rake ultrasphinx:daemon:start“,就可以启动一台sphinx服务器。但如果在生产环境还能这么做么?把sphinx安装为服务无疑是个靠谱的办法,这样它可以像mongrel、apache一样随系统启动。sphinx自带了安装为windows服务的命令: searchd –install –config xxxx.conf 不妨把这个加入到rake命令中,于是我hack了一下ultrasphinx插件的任务列表,加入了一个”rake ultrasphinx:daemon:install“命令。名为ultrasphinx.rake的文件我将稍后提供。 既然把sphinx安装为服务,相应的start和stop命令,也需要改改,改为使用”net start/stop searchd”。 2、主索引和增量索引(delta) 对于大量的数据,经常重建主索引很不现实。我的项目中几十万条数据,重建一次索引需要将近10个小时,所以打算以每天多次增量索引+一次合并索引的形式去做。 但javaeye论坛有人发帖说,在windows上要编制索引必须先停止sphinx服务器,因为searchd服务会把索引文件挂住。实际上并非如此:不论在windows还是在linux,sphinx服务器都会锁住索引文件,但只要在编制索引时加上–rotate参数,就可以不停止sphinx服务器来编制索引。sphinx的官方文档解释: –rotate creates a second index, parallel to the first (in the same place, simply including .new in the filenames). Once complete, indexer notifies searchd via sending the SIGHUP signal, and searchd will attempt to rename the indexes (renaming [...]
January 22nd, 2009 by 张磊
我曾经写过两篇关于和web开发中处理图片有关的文章,一篇是在PHP中如何裁剪图片,对比了ECShop/Babel这两套系统的做法,侧重于对图片本身的操作;另一篇是在rails项目中如何上传图片,侧重于上传并保存的策略,即使上传的是pdf文档,也一样适用。 最近我手头的项目里需要提供用户上传图片作为头像的功能,于是需要有一些后端的处理过程,比如裁剪、缩放。项目基于Rails框架,所以得找ruby上的实现。有人会推荐用attachment_fu/file_column这些插件,都显得太麻烦,我要的只是用户上传之后,经过适当裁剪,然后缩放到想要的尺寸。仅此而已。花了半天时间来研究,中文的参考资料有限,我把我的收获写下来,希望能对大家有帮助。 原来想用RMagick,但据说内存泄露的问题比较厉害,作为替代品MiniMagick不存在内存泄露的问题。而二者都是使用ImageMagick的,所以需要下载并安装ImageMagick。之后使用”gem install mini_magick“安装好MiniMagick,基础设施就搭建好了。 在开始之前,先思考一下,用于做用户头像的图片应该如何裁剪?我的考虑是所有用户采用同样的尺寸,比如48×48。但用户上传的图片往往不是正方形的,如果直接缩小到正方形,人像必然会变形。所以需要先切除一部分,把图片切成正方形,然后再缩小到想要的尺寸。 上面是原始图片(尺寸为2048×1536),先把它变成正方形。我能想到的最好办法就是在左右两边各截掉一部分,使处理后宽度和高度相等,这样能留下中间主体部分。如图(阴影部分为裁去部分,示意图不保证精确): 对于高度大于宽度的图片这招一样适用。得到中间的部分再进行缩放,就可以保证图片不失真。下面看看具体实现。 MiniMagick中Image对象有一个shave方法,正好可以满足这个需求。在irb中运行: require ‘mini_magick’ img = MiniMagick::Image.from_file “1.jpg” #取得宽度和高度 w,h = img[:width],img[:height] #=> [2048, 1536] shaved_off = ((w-h)/2).round #=> 256 img.shave “#{shaved_off}x0″ #此处表示宽度上左右各截取256个像素,高度上截取0像素 img.write “2.jpg” 在使用shave方法处理后,图片已经是正方形了(像素数1536×1536)。此时保存得到的2.jpg如图: 之后想得到不同尺寸的缩略图,只消调用resize方法。 img.resize 100 img.write “3.jpg” 得到的3.jpg如下图: 当然,想要其他尺寸也可以多次调用resize。这个示例只是在irb中操作,在实际的项目中,应该先判断一下宽度和高度哪个大,决定从哪个维度上截取,也需要考虑一下存储策略。但总体来说,只要调用一次shave,调用一次resize,就能达到目的。 参考:http://atomgiant.com/2006/07/19/resizing-images-with-minimagick/