Import csv với data lớn trong Rails

Với các app việc cấp quyền người sử dụng create, edit và export/import số lượng lớn các record tới databases thông qua file CSV liên tục diễn ra và đòi hỏi nhiều thời gian cho việc thực thi. Vì vậy việc tìm ra cách thức tốt xử lý sẽ giúp đỡ giảm thời gian chờ tương tư tài nguyên server là rất cần. Chúng tôi đã tốn nhiều thời gian để tìm ra cách thức tốt cho việc upload và xử lý bất động bộ cho các file CSV với kích thước lớn (10000+ row).

Sau đây là cụ thể user story:

User upload một file CSV kích thước lớn (20 columns cho data 10k rows ~ 2MB file size). File CSV được validate ngay lập tức: file type, format, column heading…Đưa ra message nếu tồn tại errors. File được xử lý bất đồng bộ và import data vào databases. Khi tất cả rows đã được xử lý User sẽ nhận được email thông tin quá trình xử lý hoàn tất Nếu có bất kỳ rows nào không pass validate, User nhận được mail đính kèm file CSV và message về lỗi.

một vài ràng buộc về kỹ thuật:

Heroku 2x dynos (1GB) Limited 30 second timeout Resque & RedisToGo

sau đây là một vài cách approach mà chúng tôi đã thử và những nhược điểm:

Xử lý CSV file trong controller
Giống như nhiều lập trình viêneloper chúng tôi đã bắt đầu cùng phương án Railscast. Về cơ bản chỉ đọc và xử lý CSV file trong controller. Với cách này ta có thể xử lý file khoảng 1000 rows nhưng nếu nhiều hơn app sẽ busy tại lúc upload file, và bạn sẽ nhận được lỗi timeout khi thời gian controller xử lý lớn hơn 30s.
Nhược điểm: Request timeout nếu controller xử lý file csv quá lớn(700+ row)

dùng Resque
Thay vì xử lý CSV file trong controller chúng tôi biết rằng cần phải chuyển chúng đến background job để xử lý. Đầu tiên chúng tôi đã thử version dễ hiểu nhất mà chúng tôi có thể nghĩ đến. Trong controller chúng tôi vẫn dùng CSV.read để đọc CSV file nhưng data sẽ pass vào Resque cho phần xử lý thực tế. Điều này làm tăng hiệu suất nhưng không nhiều và vẫn dẫn đến nhiều thời gian chờ.
Nhược điểm: Request timeout nếu controller xử lý file csv quá lớn(1000+ row)

Upload raw text của CSV tới Postgres, xử lý trong Reque.
Đến giờ đây ta biết rằng việc xử lý CSV file trong controller không được đối với các CSV file lớn hơn 1000+ row. sau cùng phương án tốt nhất chúng tôi đưa ra là thêm một trường text vào Postgres và lưu tất cả data raw CSV file vào database. Với phương án này giúp đỡ ta tránh timeout của xử lý trong controller và Postgres có thể lưu tất cả file một cách nhanh chóng.
tiếp theo một Resque worker sẽ lấy data raw từ database, thực hiện tìm hiểu cùng Ruby CSV và xử lý. Mặc dù phương án này đã giải quyết được việc timeout nhưng nó lại có 2 nhược điểm:

Bởi vì ta chỉ chấp nhận data raw của file và không đưa ra bất kỳ lời hồi đáp nào về file cho người sử dụng tại lúc tải lên. Do đó người sử dụng có thể tải lên các file .XLS, hoặc CSV file không đúng template, sai format. Với các file CSV rất lớn cũng sẽ không thành công. Ruby CSV không phải là bộ nhớ siêu có tác dụng. tìm hiểu và dùng row sẽ load tất cả CSV vào bộ nhớ. Trên vài nghìn row sẽ làm cho worker dùng quá nhiều memory và Heroku sẽ tự động kill và ném ra lỗi R14/R15

Nhược điểm: Request timeout nếu controller xử lý file csv quá lớn(1000+ row)

PapaParse: xử lý CSV tại client-side
PapaParse là một thư viện javascript tìm hiểu CSV file. PapaParse có thể rất dễ tìm hiểu các file lớn trong vài giây và chuyển chúng sang format JSON có thể gửi thông qua Ajax POST request. ta có thể xử lý các file và xác nhận format ngay lập tức. CSV file sẽ được tìm hiểu phía client side do đó sẽ tránh được việc memory khi dùng Ruby CSV. Trong hoàn cảnh của chúng tôi PapaParse xử lý tốt với các file khoảng 5000-8000 rows, nhưng đối với các file lớn hơn 10000-25000 row thì thực sự chưa có phương án tốt nào được đưa ra. Sau đây là một vài cách xử lý CSV file khi dùng PapaParse:
4a. Xử lý data trong Resque

row = params[:rows] # the row data from PapaParse   Resque.enqueue(CSVProcessor, user_id, rows)   

Cách này không thực sự tốt nhưng vẫn xử lý được đối với file lớn. Nhưng controller timeout vẫn có thể diễn ra.
4b. Lưu trữ data xử lý trong databases

# In the User model serialize: :csv_rows, Hash  # and in the controller user.csv_rows = params[:rows]   

giúp đỡ tăng tốc độ controller bằng cách chuyển data CSV cần tìm hiểu tới database. Nhưng việc timeout vẫn sẽ diễn ra.
4c. Stream data row by row
PapaParse có option để stream data row by row. Chúng tôi đã thử vì nó đảm bảo tránh được timeout như ở trên. Nhưng với 10000 request Ajax liên tiếp quá nhanh sẽ làm server crash.

Nhược điểm: Quá nhiều request Ajax được gọi.

phương án:
Chúng tôi có một phương án khá tốt phối hợp một vài cách approach mà đã nêu ở phía trên. Các bước bao gồm như sau

dùng carrierwave_direct cho việc upload trực tiếp CSV file tới Amazon S3 dùng parameter accept trong file_field để chỉ chấp nhận ‘text/csv’, điều này ngăn chặn user chọn các file không phải là csv file. Trước khi form được submit dùng PapaParse để tìm hiểu CSV file tại client side và kiểm tra tính hợp lệ của CSV file. Nếu không ta sẽ dừng quá trình tìm hiểu và cung ứng thông tin lỗi tới user ngay lập tức. File CSV chưa được tìm hiểu sẽ được upload lên S3. Một Resque worker sẽ fetch CSV file và xử lý chúng bằng SmarterCSV Một email thông tin được gửi đến tới user cùng thông tin thành công. Trong hoàn cảnh row có lỗi, một file CSV mới được sinh ra sau khi xử lý lỗi được gửi tới user.

Tài liệu tham khảo:

https://dalibornasevic.com/posts/68-processing-large-csv-files-with-ruby http://ruby-doc.org/stdlib-2.0.0/libdoc/csv/rdoc/CSV.html http://www.rubydoc.info/gems/smarter_csv/1.0.9/SmarterCSV

Nguồn viblo.asia