石锅拌饭

Tag: rubyonrails

财帮子是如何建成的

by Robin Lu on Oct.26, 2008, under Uncategorized

这上周在QClub上做了一个关于Rails网站建设的专题,主要从技术和构架的角度讲了讲财帮子发展的过程,现在把幻灯片分享出来:

7 Comments :, more...

新部署时代

by Robin Lu on May.15, 2008, under Uncategorized

从上一次写通过capistrano来部署Ruby On Rails应用,居然已经是一年九个月以前了,在这段时间里,Ruby On Rails,Capistrano都有着不小的进步和变化,git慢慢开始取代svn成为大家首选的SCM.与时俱进,来看看如何用Capistrano 2.3配合git和ssh来部署Ruby On Rails + Mongrel的应用.

(continue reading…)

1 Comment :, , , , more...

再谈rails缓存机制的问题

by Robin Lu on Jan.31, 2008, under Uncategorized

同样,我们还是从一个典型的缓存操作开始。很多时候,当我们创建或者修改了一个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 RailsCaching, Part OneExpiring 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之后。

由此再引申出一个解决方案,重载Caching的API,在Sweeper中使用新定义的expire,所有的expire并不真正清除缓存,而是将要清除的缓存记录下来,延后执行。再重载commit_db_transaction,在真正的commit之后执记录下来的操作。这种延迟执行的方法应该适用于所有需要在filter chain中调用的non transactional方法。

这已经是第三篇谈到rails缓存相关的问题了,也许你对另外两篇也感兴趣:
“rails缓存机制的几个问题”
“memcached使用中的竞争条件”

快成系列了。

7 Comments :, , more...

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

by Robin Lu on Jan.15, 2008, under Uncategorized

这两天从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就能部署到产品环境的.

11 Comments :, , , more...

memcached使用中的竞争条件

by Robin Lu on Oct.25, 2007, under Uncategorized

在通过ruby的memcached-client使用memcached的过程中,遇到一些问题,数据更新时清除了缓存,缓存重建的时候却仍然是老数据,在并发密集的情况下更容易出现。研究了一下,类似这样典型的memcache使用方法:

Controller里:

...
def foo_action
  ...
  unless d = Cache.get("key")
    d = Data.find(...)
    Cache.put("key", d)
  end
  ...
end
...

Model里

...
def after_save
   ...
   Cache.delete("key")
   ...
end
...

存在下面的竞争条件(race condition):

存在两个rails应用实例(比如两个并发的mongrel)A和B,

  1. memcached清除过期缓存c。
  2. 实例A运行foo_action,发现缓存c不存在(Cache.get失败),读取数据d。
  3. 实例B更新数据d’,清除过期缓存c(Cache.delete)。
  4. 实例A保存缓存c(Cache.put),其中的数据是老数据d。

这时,再有数据访问缓存c的时候,c已经存在,到下一次缓存c被清除前,这个缓存都是存在问题的过期数据。不难看出,即使将第三步中的清除过期缓存c改成更新缓存c,c仍然会被实例A在第四步覆盖。

其实,memcached为了避免这种竞争条件,提供了一些便利的原子操作(参看memcached protocol):

“add” means “store this data, but only if the server *doesn’t* already
hold data for this key”.

“cas” is a check and set operation which means “store this data but
only if no one else has updated since I last fetched it.”

将第三步中的“清除过期缓存c”变成“用memcached set方法(也就是memcached-client中的Cache.put)来更新缓存c”,然后在第四步中始终用add方法来更新缓存c,就可以解决问题。也就是说,在能够确认数据是最新的地方,比如after save中,不采用Cache.delete,而直接用Cache.put来更新缓存,在不能确认是否是最新数据的其它地方,只使用Cache.add,就能保证过期数据不会在race condition下覆盖新数据。(更新无论是Cache.delete还是Cache.put,放在after_save中仍然有问题,会因为activerecord的built-in transaction而破坏了数据的完整性,具体参见再谈rails缓存机制的问题)

memcached-client从1.4.0起才开始支持add方法,目前还不支持cas方法。不过add方法已经能够解决不少竞争条件了。如果你也有类似的问题,升级memcached-client,修改缓存更新策略吧。

8 Comments :, , more...

Archives

Browse by tags