最近常常用 PHP / Laravel 寫匯入資料的程式。為了效率起見,通常一開始就會使用 csv 格式,但這格式並不是萬能的,當資料量從 1 筆提升到 30000 筆,需要不斷調整程式才能妥善處理。
不過還是在開發之前就先抓好資料量是最好的啦🥲
假設我讓使用者透過 HTML Form 上傳一個 csv 檔案,而我會讀取其中的資料,並匯入資料庫。常見的手段應該是用 UploadedFile 搭配 fgetcsv 讀取資料,並用 Eloquent ORM 寫入。不過這樣子的標準手段很快就會因為資料量提高有所調整。假設寫入一筆資料要 0.1 秒,標準的 PHP upstream timeout 為 30 秒,意思是最多只能處理不到 300 筆資料…。
底下紀錄一下我在各個資料量曾遇到的問題:
資料量 | 會遇到的問題 | 解決手段 |
---|---|---|
~ 300 | Eloquent可能不夠快 | 換成 DB |
記憶體可能不夠 | 使用 SerializeModel trait、Generator、調整php.ini | |
寫入資料庫的速度不符合預期 | 參考這篇文章,一次 insert 寫多筆,或一次 transaction 寫多筆 | |
再怎麼調整都無法在 timeout 前完成 | 使用 Queue | |
~5000 | Queue 執行時出現不明原因的「Killed」 | 預先把使用者上傳的檔案從 /tmp 搬到 storage, |
並且dispatch 多個 Job,每個任務只處理檔案中的一小段 | ||
~15000 | 可能無法在 timeout 前完成所有 dispatch | 將 dispatch 的邏輯也放進 Queue |
Controller -> DispatchJob -> ImportJob | ||
執行順利,但使用者無從得知目前進度 | 開一張 Table,讓每個 Job 把自己處理的進度 increment 到同一筆資料 | |
使用 Laravel 官方解決方案 | 使用 Job Batching | |
~30000 | updateOrInsert() 速度慢 | 在 table 加 index |
由上面這張表可以知道匯入 CSV 要採的坑真的是挺多的,感謝好同事先踩過這些坑XD
我也反思了一下,以前我主要寫 CRUD 的時候,每次 Request 處理的資料量可能都不超過 10 筆,所以幾乎不會使用到 Queue。當時要處理大量匯入、匯出程式的同事時常說他遇到很多問題,我一向都是說把 Eloquent ORM 換 DB Query Builder 即可(因為當時我只有這種經驗),但他也做了,因此我就無法跟他有進一步的討論,直到現在自己也開始接觸這種需求,才知道坑真的很多。
話說回來,本篇文章的使用場景是匯入 CSV 中的大量資料,但有另一種場景是大量寄信。
大量寄信的場景我會採取下面的措施:
-
使用 dispatch job,其中有一 array 記錄了所有 mail job 的 record id
-
每個 mail job 都有一筆 record 紀錄狀態,這筆 record 會關聯到套資料所需的其他 table
對於 SQS 的用戶來說,需要特別注意 dispatch job 的 payload size 不能超過 256 KB(因為 Laravel 會把 Job 序列化成文字,包含 job 內的資料,並送到 SQS)。