這是我今天在 Ruby Tuesday #21 所寫的投影片。以下文章是寫投影片前已經擬好的草稿。可以配著服用
Rails Developer 在使用內建工具開發網站時,若是由小做起,通常不會踩到這些雷。但是若一開始開站資料就預計會有 10 萬筆、甚至 100 萬筆資料。執行 db 的 rake task 通常往往會令人痛苦不堪。
在這篇文章內,我整理了一些處理巨量資料必須注意的細節,應該可以有效解決大家常遇到的問題:
1. 盡量避免使用 ActiveRecord Object
ActiveRecord 當初的設計目的是為了框架內「商業使用」。它的工作是將純資料化為具有商業邏輯的 Ruby Object,並且配合框架,設計了多層 callbacks。
簡單來說,它並不是為了「處理 raw data」而設計。如果開發者只是要作一些簡單的資料操作,建議的方式請直接下 SQL,不要沾到任何 ActiveRecord。
(但大多數開發者直覺都是會開 ActiveRecord 下了條件就直接跑迴圈,忘記 MySQL 是可以直接拿來下指令的)
當然,沒有 ActiveRecord 這麼抽象化的工具,下純指令也是蠻痛苦的一件事,我推薦可以換用 sequel 這套工具試看看。
再者,在實務操作上我也建議避免使用 ActiveRecord + 內建的 rake task 操作巨量資料。原因是,開發者會順帶會把整套的 Rails 環境都載進來跑,其慢無比是正常的…
2. 有 update_all 可以用,少用 for / each。
通常會出問題的 code 是長這樣的:
這段 code 非常直觀,但會造成許多的問題。如果符合的條件有 10 萬筆,大概放著跑一天都跑不完....
先提供快速解法。Rails 提供了 update_all 可以下。可以改成這樣
基本上就是等於直接幫你下 update 的 SQL 啦。同樣資料量跑下去大概只要 10 秒秒以下左右吧。
3. 不要傻傻的直接 Post.all.each,可以用 find_in_batches
直接叫出所有符合的資料(Array) 是一件危險的事。如果符合條件的資料是 10 萬筆,全拉出來有高達 10G 的大小,嗯…我想機器沒個 10 G 以上的記憶體,指令下去機器直接跑到死掉有極大的可能性…
Rails 提供了 find_in_batches
如果沒下 batch_size 預設一次是拉 2000 筆。可以一次指定小一點的數目,如一次 500 筆去跑。
4. 使用 transaction 跳過每次都要 BEGIN COMMIT 的過程,一次做完 1000 筆,然後再 COMMIT。
打開 Rails 的 development.log,這樣的 LOG 應該對你不陌生。
Rails 開發時,為了確保每比資料正確性,儲存的時候都會過一次 transaction,於是即使已經照 3
這樣的解法,還是要過 10 萬次 COMMIT BEGIN。很浪費時間。
如果你只是要 update 少量欄位,而且資料處於離線狀態,可以使用 Transactions 搭配 find_in_batches,做完兩千筆再一次 commit,而不是每次做完就 commit 一次,在資料量很大的狀況下也可以節省不少時間。
5. 使用 update_column / sneacky-save 而非原生 save
用原生 save
會有什麼問題呢?原生的 save
在資料儲存時,會經過一堆 validator
和 callbacks
,即使你只是要簡單 update 一個欄位,會觸發到一狗票東西(假設 10 道 hook),那 10 萬筆就是過 100 萬道 hook 了啊。天啊 /_\,機器死掉不意外。
如果你想要閃掉 hook 的話,可以使用 update_column,
但 update_column 的缺點是一次只能 update 一個欄位,如果你有 update 多個欄位的需求,可以用sneacky-save 這套 gem。
如其名,sneacky-save 偷偷儲存不會勾動任何天雷地火。
6. 可以的話使用 Post.select("column 1, colum2").where
很多人會忽略一件事,Post.where("id < 10")
,其實是把這 10 個 object 拉出 database。Post 裡面有什麼呢?會有幾千字的 content
啊。所以當你下了這道 comment 後,拉出來的是這些內容
拉出 10 萬筆會發生什麼事呢?(炸)
所以這也是我建議如果你沒有複雜操作(相依高度 model 邏輯)需要的話,千萬別碰 ActiveRecord,因為你不會知道會按下哪一顆核彈按鈕。
7. 使用 delegate 把大資料搬出去
ActiveRecord 裡面有 delegate 這個 API。如果你嫌要 Post.select("column 1, colum2").where
這樣東閃西閃很麻煩,還是希望使用 SELECT post.*
。那麼不妨可以換一個思路,把肥的 column 丟到另外一個 table,再用 delegate 接起來。
8. 操作資料前,別忘記打 INDEX
舉凡操作資料,多半是至少會先下個 condition。再看是直接用 SQL 處理掉還是跑迴圈。不過一般開發者最會中地雷的部分就是
- 忘記打 index
忘記打 index 下 condition ,就會引發 table scan,這當然會很慢啊 /_\
- 對 varchar(255) 直接打 index
使用 Rails 產生的 varchar,多半是 varchar(255),很少有人會直接去改長度的。而且使用 Rails 直接打的 index,也就是全長的 index 打下去了。效率爛到炸掉。
可以用這招 index 可以指定只取前面 n chars 的方式增進效率
Percona 前幾天也有一個 talk 是 MySQL Indexing Best Practices,值得參考。
9. delete / destroy,刪除很昂貴。確保你知道自己在幹什麼。
首先第一件事要分清楚 delete 和 destroy 有什麼不同。
- destroy 刪除資料並 go through callbacks
- delete 刪除資料,不過任何 callbacks
所以要刪除資料前,請確認你用的是何種「刪除」。
destroy_all 和 delete_all 也是類似的原則。
找到符合特徵的紀錄,然後呼叫 destroy method。在這個動作中會引發 callbacks
….orz
找到符合特徵的紀錄,刪掉,但不觸發 callbacks
。
不過如果你真的要「清空 DB」。不要用 delete_all,MySQL 提供了:TRUNCATE 給你用。請用這個...
- TRUNCATE TABLE
雖然 delete 不觸發 callbacks,但是「刪除」DELETE 真的很慢,因為
DELETE 涉及到會 update index,所以會…很慢。http://stackoverflow.com/questions/4020240/in-mysql-is-it-faster-to-delete-and-then-insert-or-is-it-faster-to-update-exist
如果你的資料要作大量的刪除動作,有兩種思路可以繞。
一個是使用軟性刪除 soft_delete,也就是加上標記標示已刪除,但實質上不從資料庫刪除資料,只 update 會比 delete 快一點。有 acts_as_archive 可以用。
另外一個想法是:與其用刪的 (DELETE) 不如用 塞的 (INSERT)
開一個新的 Table,用倒的,把 match 的 record 塞到新的 DB 去。INSERT 比 DELETE 快太多了。
10. 無法避免的昂貴操作丟到 background job 去操作。
使用 posts.each
是一個昂貴的線性操作。這個 process 會無限的膨脹及 block 資源。
所以可以改用一個作法,使用 background job 如
- delayed_job (不推薦)
- resque
- sidekiq
把昂貴的操作包成獨立事件。塞進 queue 裡面,丟到背景跑,然後開 10 支 worker,十箭其發,速度可以快不少。
之所以把 delayed_job 列出來又不推薦的原因是因為 delayed_job 清 queue 的方式是用 DELETE,在第九點我們談過了,在有大量資料的情況下,「刪除」這件事會非常昂貴。使用 delayed_job 無異是拿汽油澆火。
結論
十點列下來。我的建議是,如果你手上的資料量大到一個程度,能儘量回歸基本(SQL command)就回歸基本。因為使用 ActiveRecord ,開發者永遠不知道自己什麼時候會按下核爆彈的按鈕啊…
其他
目前我們固定在禮拜二,都會在 松江路的田中園 上舉辦 Taipei Rails Meetup。我自己本身也會固定在這裡免費幫大家解答 Rails 與 Web Operation 相關的問題。而坦白說,最近一些比較經典的 Post 也是從聚會裡的問答集裡面萃取出來的。
如果你對 Rails 有濃厚的興趣又住在台北,歡迎每週加入我們,謝謝!