Rails Rails with massive data

xdite · 2012年08月22日 · 最后由 fsword 回复于 2012年08月27日 · 6235 次阅读
本帖已被管理员设置为精华贴

http://blog.xdite.net/posts/2012/08/22/rails-with-massive-data/

這是我昨天在台灣的 Ruby Tuesday #21 做的分享。

關於用 Rails 處理大量資料,需要注意的一些點...

最近就遇上要在 rake task 写把缓存批量更新到数据库的东西,感谢 xdite 大姐的文章让我免踩地雷,总是为我们带来这么多干货!

另问一个问题,有条件对一个表做多个 update 有什么好方案?例如不同的 article_id 对应 hits,目前自己的项目是使用 sql 的 update case when then,总感到很别扭

干货!感谢

很实用的经验

感谢分享

谢谢分享 :)

我的观点是无论 功能开发 还是 性能优化,自底向上的都不是什么好方法。

我会首先考虑代码简洁。一个问题是:简洁的代码和高性能二者真的只能取其一吗?如果答案是肯定的,那么你真的曾经把代码写简洁过吗?

这篇博客的问题在于,业务代码当中考虑了本该由底层的支持(Rails)该做的事情。里面提到的问题也许难免遇到,问题是解决的方法:是该在业务代码里考虑这些,还是应该去改进 Rails?这是对象职责分配的问题。

程序员圈某“名人”的做法是,“link_to 有性能问题,那我们就不用 link_to,全都给我改用 html 的 a 标签。”封装哪里去了?散弹式修改这么强烈的 bad smell,居然没嗅到。

个人观点,任何人都可以不同意。我也相信多数人不支持我的观点,无所谓,只希望不会影响到我周围的环境。好的环境很难找,也许就不存在,至少别让它变得更坏。

这个好像在哪里早就看过,不过还是要赞下。

#6 楼 @yuan

首先,开篇就写这么一段前提:

Rails Developer 在使用內建工具開發網站時,若是由小做起,通常不會踩到這些雷。但是若一開始開站資料就預計會有 10 萬筆、甚至 100 萬筆資料。執行 db 的 rake task 通常往往會令人痛苦不堪。

这是个前提,也是个需求,大数据量处理不能太慢的需求。文中简述的几个点,告诉大家如何通过 Rails 一些内建的机制去简洁优雅地满足这个需求。

其次,文中用的代码也不会不简洁,比如文中第二点的例子,你觉得一个each的写法会比update_all简洁吗?

最后,想说一点的是,可以交付可以工作的软件是合格的软件开发者首先应该考虑的事情。而把代码写简洁,追求高可读性,注重抽象,这是在应对软件复杂性的手段,而不是一切的。如果你写出来的软件慢得不得了,根本不可用,还谈何代码简洁,这就是本末倒置了。

不是说别人不支持你的观点,而是你的观点不具有说服力。

@xdite 可以再加一条,关于 pluck 的使用。

有时需要对查找某个 column 中的所有值,但大部分人都会先把所有的对象查询出来后再使用map去把每个对象上的某个 column 值取回来。比如下面的情况:

Post.all.map(&:id) # vs Post.pluck(:id)

pluck会执行一个select指定 column 的 SQL,不生成 ActiveRecord 的 Object,直接取走所有 column 的值。

从这个帖子里发现了一个 Bug,提交了 Pull Request,请 @huacnlee 查收。

#8 楼 @_kaichen 首先回复你

“用起来很慢”跟“根本不可用”不是一回事吧?实际上软件不可用的问题倒更有可能是因为代码不清晰引起的,常见的一个场景就是用户的一个很合理的需求,程序员由于各种原因推迟甚至不给支持,根本上的原因是代码改动困难,一个小需求在代码上需要动大刀,成本太大。这才叫做软件“不可用”。

老温那句话怎么说来着,“如果不是为了使程序能用,我可以写出每张卡片的处理时间只需要 1 毫秒钟的程序来”。

编程的本质是抽象,从 0 和 1 层层抽象到用户需求。烂代码对程序员不可用,最终迟早会积累(抽象)成软件对用户不可用,这是一个熵增的过程。所以,到底是注重代码质量的程序员本末倒置,还是急于完成功能的程序员短视呢?有的时候快即是慢。

以上,与主题无关。接着再回复关于主帖的部分

我并不是逐个的反对 lz 的建议,有一些甚至是赞同的。但是从文章整体当中嗅到一些味道,首先我做个猜测,赞同 lz 所有建议的,是不是习惯在 rake task 里写大量代码呢?我在 rake task 里的代码往往只有几句,例如Model.backup,多数逻辑分散在 Model 的各个方法里。所以这时候,第 1 个建议我是反对的,放弃重用已有逻辑直接写 SQL 或者用另一套 ORM 违反了 DRY 原则,多出许多维护的工作。

2 和 3 我没有反对。

第 4 个建议,我用 Rails 几乎没有关心过 transaction。事务本是用来保障数据完整性的,但是文章中使用它居然是为了提高性能,是不是过分追求性能了点?有时候,高层的一些方案能够解决性能问题,就尽量使用高层的解决方法,实际上在越高层解决性能问题,效果越明显。实在顶不住了,才再往底层挖掘,这样可以尽量的减少脏代码,同时又不损失性能。而且就算挖到底层,我也不会用这样的歪路子,为了性能到处声明事务,让人莫名其妙。

第 5 个建议,该 save 时还是要 save,一些时候的保存操作就是应该触发 callback 和 validator。不过如果习惯在 rake task 里写许多代码,而不是把多数逻辑分散到模型当中去,有这样的建议好像也不奇怪。

建议 6 中 lz 提到的问题,在批量操作数据的时候,实际上是用建议 3 解决,而非建议 7。在另一些情况下,可以用缓存搞定,也不是用建议 7。而建议 7 的做法实际上不是用来解决性能相关问题,而是代码质量的问题,详见设计模式的“代理模式”,或者重构名录“隐藏委托”。

建议 8 无争议。

建议 9,同建议 5,我反而是多用 destroy 少用 delete,生怕忘了清除什么,留下脏数据,或者是需要手动来维护一些本来可以自动维护的关联关系。

建议 10 无争议。

#11 楼 @yuan 第四个建议那里,不是使用事务来提高性能,而是减少事务 begin commit 的次数。本身就有 2000 次事务,合为一次,是个很大的改进。如果高层抽象可以满足需要,谁愿意往底层挖?但 leaky abstraction 无处不在(如果不了解这个概念参看:http://www.joelonsoftware.com/articles/LeakyAbstractions.html),有时不得不绕过残废的高层方法

這是我從演講中抽出的文章。我想 #6 樓 很大程度斷章取義了我的文章。我甚至認為你沒有用過我裡面提到的 API 設計過 application,只是按照「字面意義」逐條批駁設計(如 transaction, delegate)。也沒有處理過巨量資料的經驗。

這篇文章是有使用情境的。我寫 Rails 超過五年了,在怎麼樣的情境下要設計出什麼樣的 code 我還是曉得的。

我的原意是在處理大量資料時,「ActiveRecord」是「完全不適合」被使用的。原因是 ActiveRecord 本身原先就是設計來處理「事務上」的需求,所以 ActiveRecord 內建了大量的關係,Validation,Callbacks,這在開發網站上面使用非常的便利。

但是如果 task 的主要目的是屬於「處理數據」的話,那麼我建議應該直接回歸到原點「寫 SQL statement」。因為使用 Ruby Object 再這麼包一層,是嚴重浪費 CPU 以及 MySQL 效能。

當然,事情永遠不是這麼單純美好。有時候我們還是想要「偷用」ActiveRecord 便利的設計,來讓我們的工作省事點。

那麼在這個前提下,開發者必須要知道,ActiveRecord 的哪一些設計,會使你原先「直觀」的寫法,直接變成破壞效能的殺手。在設計 DB task 必須要用一些「設計技巧」能繞即繞,否則 task 一跑下去,機器直接死機是必然的。

這整篇文章和 Talk 都是繞著這個出發點所構築的。

所以 #6 樓 整串爭論下來我只有股被紮草人打的莫名其妙感...

但是我想講了這麼多,建議還是直接開個有一百萬筆資料(其實也不用到 100 萬,30 萬就夠了)的 db,直接跑個 for each,大家就不用浪費這麼多唇舌在這裡戰了....

@xdite 感谢回答,具体一下问题 例如有文章的 Article 类,有一个叫 hits 的属性代表访问量,每访问一次articles#show就加1, 这东西放在缓存服务器中 (redis) 做 counter, 用 whenever 跑定时任务 rake 写到 mysql

这时每 update 一个 hits 就要和 article_id 对应,不能直接 update_all

本来 (性能有问题):

Article.where("id in (?)", redis_find_all_id).each do |a|
  a.update_attribute(:hits, redis_value)
end

现在 (直接用 sql):

sql = " update articles hits=(case id "

redis_find_all_id.each do |redis|
  sql << " when #{redis_key} then #{redis_value} "
end

sql  <<  " end) where id in #{redis_find_all_id} "

ActiveRecord::Base.connection.execute(sql)

下面的实现虽然一条 sql 语句搞定,但是很别扭丑陋,而且如果 redis 的 key 或者 article 的属性名更换的话又得改...上线后有大量这类任务更是灾难,请问大家有何更好的方法做到 性能和维护 两者兼顾?

update_attribute 會引發 callbacks, 你可以改用 update_column, 那麼這樣就不會觸發 callbacks 我想這是你原先會遇到的效能問題。

plus, Article.where("id in (?)", redis_find_all_id) 會勾出 SELECT * 。所以 memory 會 bloat ...

#15 楼 @reducm 你的文章每秒有多少的访问量?搞得这么麻烦。

我不負責任的給一個解法,爆炸不要來找我

https://github.com/agibralter/ar-resque-counter-cache 然後塞

ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue(klass, id, column, direction) 作 async update XD

#16 楼 @xdite select 的问题知道,但即使第一种情况下使用 update_column,也会生成多条 update sql 语句吧?

一个 transaction 里包多个 update 语句 能比 一条 update 语句 快吗?

#17 楼 @hooopo 因为届时是多用户,这个访问量做缓存的需求比较正常吧?我是求 ruby 可维护性里优雅的解法...因为直接写 sql 就是以前 php 而来的解法,维护一堆 sql 很痛苦...

一个transaction里包多个update语句 能比 一条update语句 快吗? 看你是要把壓力放在 web 還是 db。這就跟有時候我們會去用多個 SELECT 換 JOIN 的狀況一樣。

比如說一次執行一次執行這句 SQL 要 60 秒,甚至會造成 down time。那麼我就會寧願比如說拆成 200 條慢慢跑,但是每一條幾乎都不會造成 DB lock。

當然這種招數只能用在「不在乎正確性」的數字上,比如說 hit, count ...要求精度的數字就沒辦法了...DB 的 ACID 是沒有辦法一次全滿足的。

干货!期待能有个 mongodb(mongoid) 版的

#15 楼 @reducm 这种计数的需求,没必要用 redis 这么麻烦,可以用 activerecord 的 increment_counter 搞定

Article.increment_counter(:hits, params[:id])

不用担心数据库的这种简单操作性能,等你的访问量到了数据库瓶颈的时候,你的网站 alexa 已经可以排到全世界排名前 1000 了 :) 那个时候可以选择 db sharding,加台服务器就是了

这些处理的方法不光在 ruby 上有用,对于其他语言来说都有参考意义;所以我们招人是也是必然要考察对高性能系统的经验,这个比语言本身要重要很多

刚刚在看@xdite 大姐的 blog,很有帮助

文章很不错,不过 @yuan 的发言也不是完全没有用,我也担心会有人盲目照搬不看前提,有争论可以帮助他们。 提个小问题,下面这句话

如果符合條件的資料是 10 萬筆,全拉出來有高達 10G 的大小

难道一条数据是 100K 么?这个数据库的表设计也太可怕了吧?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号