石锅拌饭

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...

Search

Archives

Browse by tags

agile apple blog book design ecto extension firefox git google hack ichm iphone keyword life mac madfox movie nonsense opensource plugin pm ruby rubyonrails sns software startup wordpress work 财帮子