再谈rails缓存机制的问题

同样,我们还是从一个典型的缓存操作开始。很多时候,当我们创建或者修改了一个active record对象后,需要清除一些对应的缓存,无论是借助Sweeper,还是自己创建Observer,甚至直接写在filter里,最终基本上都归结到一个类似下面的callback,通过上述几种方式在save后被调用: # If we save an article, the cached version of that article is stale def after_save(article) expire_article_page(article.id) end 一旦保存修改了该对象,就清除缓存。无论你看ActionController::Caching::Sweeping的文档,还是Agile Web Development with Rails中Caching, Part One的Expiring Pages Implicitly一节,都会这么告诉你。 看似没什么问题,但在高并发的环境中(又是高并发),经常出现缓存被清除之后,重建缓存仍然使用了过期的数据。 我们首先怀疑是出现了类似“memcached使用中的竞争条件”中提到的问题,于是在缓存清除和创建的地方分别添加日志。结果很让我们吃惊,在after_save里用find取出的数据的确是新的,创建缓存的时间明显晚于after_save被调用的时间,但用同样的方法取出的数据却是旧的。 经过调查,我们终于抓出了今天的陷阱主角,ActiveRecord 的 Built-in Transactions 。 为了保证数据更新操作的原子性,ActiveRecord缺省将destroy和save操作都包裹在一个transaction中(详见activerecord/lib/transaction.rb),而整个 filter chain 也被包含了进来,也就是说,before_save和after_save都是在transaction提交前发生的。 什么是transaction?就是在transaction commit成功返回之前,所有的数据库操作并没有真正在服务器端完成。但在after_save中调用expire_article_page(article.id),这个函数一返回,缓存就已经被清除了,不会等到transaction commit。 一个non-transactional的操作被放在了一个transaction中。 结果是什么? 结果是在缓存被清除和最后transaction commit完成之间,缓存重建有可能被另一个rails实例发起,使用的是没有被更新的数据。 这是一个相当隐蔽的陷阱,一个马上可以想到的解决方法是在transactional save/destroy之外再包一层,在这一层加上自定义的filter,在save之外调用自定义的filter,保证清除缓存是在transactional save之后。 但这样又陷入一个新的陷阱。ActiveRecord的built-in transaction是可以嵌套的。也就是说,在一个after_save中可以调用另一个ActiveRecord对象的save,而这个内层save结束并不会触发commit,只会decrement_open_transactions,减少一层嵌套而已。 通过在transactional save/destroy外再包一层的做法虽然能保证自定义的filter在save后执行,却不能保证在transaction commit之后。 Read more about 再谈rails缓存机制的问题[…]

财帮子性能优化简报

财帮子从三月底正式上线到现在,已经快半年了,网站的流量差不多每月翻一翻。 最近这个星期,每天晚上8点到11点的高峰期,每个小时页面浏览一般在5万到7万,最高可以接近10万。除去由web server处理的静态文件等请求,每个小时由rails处理的请求从10万到30万,峰值可以达到200req/s。 与此同时,根据基金理财网站的特点,浏览高峰期,计算量高峰期和缓存失效高峰期刚好重合,我们接受的是比一般ruby on rails应用更严峻的挑战。 有人说财帮子已经是国内规模最大的ruby on rails应用之一了,这点我们无法考证,但随着量的增长,我们的确对ruby on rails的性能有了越来越多的认识。 别指望一劳永逸 从建站初期,我们就采取了各种性能优化的措施,SQLSession, Fragmeng Cache, Memcached…所有你能从教科书里看到的。但是,很多问题是慢慢暴露出来的,你需要根据实际情况来调整缓存策略,修改程序结构,优化算法,甚至优化rails本身。随着量的不断增长,碰到的问题会越来越特殊,书本、社区包括搜索引擎都很难再给你提供现成的解决方案,完全变成对你经验和能力的挑战。财帮子两个星期以前,遇到严重的性能问题,最终我们采用了相当非主流的部署方案和打了自己补丁的web server,成功度过了难关。 你不在美国 没有Google Adsense来养活你,人工费用和带宽、机位费也不成比例,你不可能有点性能问题就靠加服务器来解决。绝不是否定scale out的策略,但在此之前,一定要尽可能挖掘现有程序的潜力,发现已有和潜在的性能问题并解决。否则,盲目的scale out可能只是掩盖了真正的问题,并让你花冤枉钱。 rails不是你的SQL专家 activerecord成功封装了数据库接口,并提供了很多灵活便捷的功能。但是,完全依赖activerecord和ruby,你可能碰到严重的性能问题。find中的include和select选项能带来一些好处,但也非常有限。有时候你不得不写复杂难看的SQL,来代替优美的rails语句。rails纯粹主义在这时候不能为你赚到CPU。即便使用了缓存机制,而且缓存命中率很高,在访问量基数变大时,缓存释放和重建也有可能增大到相当的数量。数据库性能优化仍然是rails应用不可回避的问题。 log是你最好的朋友 log是所有性能调试的起点,从development log到production log,从rails log到web server log,不同的log有不同的侧重点,学会分析log,每个log都可能为你提供解决问题的蛛丝马迹。另外,要熟练使用benchmark结合log做profiling,当real time远大于db time + rendering time的时候,这点尤其总要。

rails缓存机制的几个问题

ruby on rails提供了一些内建的cache机制,我们比较多的用到了其中的fragment cache。在实际使用过程中,发现了一些问题,如果你不注意,performance和正确性可能都会受到影响。 cache的重复读取问题 这是从Agile Web Development with Rails中抄下来的一段代码: def list @dynamic_content = Time.now.to_s unless read_fragment(:action => ‘list’) logger.info(“Creating fragment”) @articles = Article.find_recent end end 相信大家也都是用判断read_fragment返回值的方法来看cache是否存在。但你有没有想过,read_fragment这个函数的真正含义是读取cache。在view里,helper cache又会把cache的内容读一遍。假设你的cache用的是FileStore,下面是cache.rb中read的实现: def read(name, options = nil) #:nodoc: File.open(real_file_path(name), ‘rb’) { |f| f.read } rescue nil end 两次调用read_fragment就是两次独立的文件IO,而且每次都全文读入。第一次读完全是浪费。 解决方案: 一个是在controller里将read_fragment返回的内容保存在一个instance variable里,再到view里显示。这个做法的缺点是无法在view里使用现成的helper cache了,需要重写一个类似的helper,判断相关的instance variable而不是再去read。 另一个方法是自己写一个check_fragment,用File.exist?去判断,效率要高得多。 cache判断的原子性问题 还是上面那段代码: def list @dynamic_content Read more about rails缓存机制的几个问题[…]