再谈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缓存机制的问题[…]

最近看的几本书

数十位来自互联网和软件开发等企业的创始人,谈他们创业初期的经历,对想创业或者在创业的人,都是值得一读的一本书。你可以从中获得三种东西,经验、鼓励和乐趣。 李安的故事告诉了我们,三十六岁以后再开始也是有戏的:P。虽然讲的是拍电影,比如“平常在班上滔滔不绝、分析电影头头是道的人,一拍片,你不敢相信是同一个人,那么简单的事情,他都反应不过来。我这才知道,读理论和拍片根本是两码事,是两种不同的才分”,但是这种事,好像搁哪都挺对。 看《2001太空冒险》居然已经是二十年前的事情了,还好赶在2010年前把这本看了。引进的台湾译本,翻译质量可以保证,封面设计也很酷。

对一篇关于RoR部署方案文章的疑问

这两天从Google Reader的Friends’ Shared Items中,好几个朋友都分享了同一篇文章,题目是”RoR部署方案深度剖析“,因为自己对这个内容也很感兴趣,仔细读了一下.应该说是很有意思的一篇文章,我也从中学到了不少东西,但是也发现了一些问题: Apache/Nginx的接收缓冲区都只开了8KB,如果页面比较大,Mongrel就没有办法一次性把数据全部推给Web服务器,必须等到Web服务器把接收缓冲区的8K数据推到客户浏览器端以后,清空缓冲区,才能接收下一个8KB的数据。这种情况下,Mongrel必须和Web服务器之间进行多次数据传输,才能完成整个Web响应的过程,显然没有一次性把页面数据全部推给Web服务器快。 只有几个问题,application layer的读缓存和读的次数是否影响到socket另一端写socket的次数的? 写socket的次数是否和另一段socket read的次数对应? Web服务器向到客户端socket的写操作在什么情况下会被阻塞? 文中的一些观点和说法好像多少混淆了应用层和传输层的概念. 我们假设使用服务器端程序控制带权限的文件下载,某用户下载的是一个100MB的文件,该用户使用了多线程下载工具,他开了10个线程并发下载,那么每个线程Mongrel在响应之后,都会把整个文件读入到内存的StringIO对象当中,所以总共会创建出来10个StringIO对象保存10份文件内容,所以Mongrel的内存会一下暴涨到1GB以上。而且最可怕的是,即使当用户下载结束以后,Mongrel的内存都不会迅速回落,而是一直保持如此高的内存占用,这是因为Ruby的GC机制不好,不能够及时进行垃圾回收。 Mongrel处理rails request和静态文件使用的是不同的handler.处理静态文件用的是DirHandler,DirHandler没有通过调用response的start方法使用到StringIO,而是直接调用了response的send_file方法,直接write socket.验证过程非常简单,自己起一个mongrel,在public下放一个100M的文件,通过浏览器下载一下.在我的mac上,本来是76M的mongrel,在下载结束后是74M,下载过程中也一直小于80M,最低到过68M.无论是过程中还是结束后,都没有出现占用内存暴涨的现象. 另外,mongrel本不应该用来处理静态文件,该文章开始的图也画得很清楚. 我们知道Mongrel在使用Lighttpd的时候,可以达到最快的RoR执行速度,但是Lighttpd当前的1.4.18版本的HTTP Proxy的负载均衡和故障切换功能有一些bug,因此一般很少有人会使用这种方式。 Lighttpd 1.5重写了mod_proxy_core,sqf方式下的balancer已经没有这个问题. 而Mongrel就简单了,gem install mongrel安装完毕,mongrel_rails start启动,哪个人不会?毕竟绝大多数开发人员和部署人员不是高手,他们熟悉哪种方式,自然就会推崇哪种方式。 首先,我不太赞同复杂的就好,简单的就不专业这样的观点,Unix philosophy也不是这样的.其次,mongrel也不是mongrel_rails start就能部署到产品环境的.

2007年总结

有人说2007年的年度汉字是“涨”,股市在涨,基金在涨,猪肉价格、房价在涨,连街边的牛肉面在涨。就在这年,我选择离开稳定的职位,和朋友一起开始创业。回顾这一年,感觉就象在做过山车。 一月,我的女儿出生,手忙脚乱加睡眠严重不足的一个月。 二月,女儿满月。提出辞职,离职日期定在三月底。开始准备网站基础设施,将攒的8000元的服务器送到机房。 三月,通过路考,拿到驾照。和各level的同事和老板谈话、吃饭、聊天,正式离职。引用宫老板的话,“外企是毒品,去外企是毒瘾,出来是戒毒”。用一篇blog,宣布了财帮子的开始。 四月,上线十天迎来第一千个注册。一些投资机构开始找我们了解情况。参加清华2字班毕业十周年活动,正在创业的老同学和我说下一个十年是“黄金十年,只争朝夕”。女儿百天。 五月,机房遭攻击,服务器被粗暴重启,不得不去机房修复硬盘。pv突破六万,注册过六千。 六月,征集网站Logo,攒了新服务器,注册过万。 七月,多帐本和分析平台上线,股市调整,pv先陷入低谷,又随大盘上升再创新高。女儿半岁。 八月,带宽出现瓶颈,升级带宽。为了弥补支出,开始投放Google Adsense。注册快速增长。 九月,带宽再次出现瓶颈,再次升级,瓶颈转移。调整算法,优化结构,pv突破六十万。女儿第一次发烧。 十月,pv继续增长,出现新的瓶颈,优化分布式算法,再次度过难关。女儿烫伤左手。 十一月,参加金博会,第一次在公共场所发宣传品。财帮子被邀请参加China Foocamp。受股市影响,流量下降。静心开发,部署新功能最多的一个月。和我们同期辞职去美国参加startup的同事,回来告知我们已经烧了六百万美元,产品还未发布。女儿烫伤恢复,没有留下疤痕。 十二月,女儿学会走路。 07年,就是这样了。 08年,希望能多一点时间陪陪女儿,她在抓周的时候抓了鼠标。