07/09/2018, 18:03

Import csv với dữ liệu lớn trong Rails

Với các ứng dụng việc cho phép người dùng create, edit và export/import số lượng lớn các record tới databases thông qua file CSV thường xuyên xảy ra và đòi hỏi nhiều thời gian cho việc thực thi. Vì vậy việc tìm ra phương pháp tốt xử lý sẽ giúp giảm thời gian chờ cũng như tài nguyên server là cần ...

Với các ứng dụng việc cho phép người dùng create, edit và export/import số lượng lớn các record tới databases thông qua file CSV thường xuyên xảy ra và đòi hỏi nhiều thời gian cho việc thực thi. Vì vậy việc tìm ra phương pháp tốt xử lý sẽ giúp giảm thời gian chờ cũng như tài nguyên server là cần thiết. Chúng tôi đã tốn nhiều thời gian để tìm ra phương pháp 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à chi tiết user story:

  • User upload một file CSV kích thước lớn (20 columns cho dữ liệu 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 dữ liệu vào databases.
  • Khi tất cả rows đã được xử lý User sẽ nhận được email thông báo quá trình xử lý hoàn thành
  • 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 số ràng buộc về kỹ thuật:

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

Dưới đây là một vài cách tiếp cận mà chúng tôi đã thử và những nhược điểm:

  1. Xử lý CSV file trong controller
    Giống như nhiều developer chúng tôi đã bắt đầu cùng giải pháp Railscast. Về cơ bản chỉ đọc và xử lý CSV file trong controller. Với cách này chúng ta có thể xử lý file khoảng 1000 rows nhưng nếu nhiều hơn ứng dụng sẽ busy tại thời điểm 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)

  2. Sử 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 đơn giản nhất mà chúng tôi có thể nghĩ đến. Trong controller chúng tôi vẫn sử dụng CSV.read để đọc CSV file nhưng dữ liệu 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)

  3. Upload raw text của CSV tới Postgres, xử lý trong Reque.
    Đến bây giờ chúng 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. Cuối cùng giải pháp tốt nhất chúng tôi đưa ra là thêm một trường text vào Postgres và lưu toàn bộ dữ liệu raw CSV file vào database. Với giải pháp này giúp chúng ta tránh timeout của xử lý trong controller và Postgres có thể lưu toàn bộ file một cách nhanh chóng.
    Sau đó một Resque worker sẽ lấy dữ liệu raw từ database, tiến hành phân tích cùng Ruby CSV và xử lý. Mặc dù giải pháp này đã giải quyết được vấn đề timeout nhưng nó lại có 2 nhược điểm:

    • Bởi vì chúng ta chỉ chấp nhận dữ liệu raw của file và không đưa ra bất kỳ phản hồi nào về file cho người dùng tại thời điểm tải lên. Do đó người 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 hiệu quả. Phân tích và sử dụng row sẽ load toàn bộ CSV vào bộ nhớ. Trên vài nghìn row sẽ làm cho worker sử 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)

  4. PapaParse: xử lý CSV tại client-side
    PapaParse là một thư viện javascript phân tích CSV file. PapaParse có thể dễ dàng phân tích 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. Chúng ta có thể xử lý các file và xác nhận format ngay lập tức. CSV file sẽ được phân tích phía client side do đó sẽ tránh được vấn đề memory khi sử dụng Ruby CSV. Trong trường hợp 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ó giải pháp tốt nào được đưa ra. Sau đây là một số cách xử lý CSV file khi sử 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ể xảy 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 dữ liệu CSV cần phân tích tới database. Nhưng vấn đề timeout vẫn sẽ xảy ra.
    4c. Stream dữ liệu row by row
    PapaParse có option để stream dữ liệu row by row. Chúng tôi đã thử vì nó chắc chắn 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.

Giải pháp:
Chúng tôi có một giải pháp khá tốt kết hợp một số cách tiếp cận mà đã nêu ở phía trên. Các bước bao gồm như sau

  1. Sử dụng carrierwave_direct cho việc upload trực tiếp CSV file tới Amazon S3
  2. Sử 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.
  3. Trước khi form được submit sử dụng PapaParse để phân tích CSV file tại client side và kiểm tra tính hợp lệ của CSV file. Nếu không chúng ta sẽ dừng quá trình phân tích và cung cấp thông báo lỗi tới user ngay lập tức.
  4. File CSV chưa được phân tích sẽ được upload lên S3.
  5. Một Resque worker sẽ fetch CSV file và xử lý chúng bằng SmarterCSV
  6. Một email thông báo được gửi đến tới user cùng thông báo thành công. Trong trường hợp 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.

0