Bạn luôn cố gắng mang đến cho người dùng trải nghiệm tốt hơn khi sử dụng trang web hoặc ứng dụng của bạn? Một trong những cách quan trọng nhất để đạt được điều này là bằng cách giảm thời gian response của server. Trong bài này, chúng ta sẽ khám phá về Active Job – cho phép thực hiện việc đó bằng hệ thống hàng đợi (queueing backends). Bạn cũng có thể sử dụng hàng đợi để giúp giảm lưu lượng truy cập hoặc tải lên server, cho phép công việc được thực hiện khi server “rảnh” hơn.
Active Job là gì?
Active Job trong Rails là một framework giúp tạo ra các tác vụ (job) và cho phép chúng chạy trên một số hệ thống hàng đợi (queueing backends) khác nhau. Các tác vụ có thể là dọn dẹp thường xuyên theo định kì, upload ảnh lên server, gửi mail… Các hệ thống hàng đợi phổ biến nhất được sử dụng trong ứng dụng Rails là Sidekiq, Resque và Delayed Job.
Sử dụng Active Job
Active Job có giao diện và bộ cài đặt cấu hình khá đơn giản. Dưới đây, cách sử dụng các tính năng của nó:
Tạo Job
Active Job khi được tạo qua command sẽ bao gồm job và các stub cần thiết.
1 2 3 4 5 | rails g job TweetNotifier invoke test_unit create test/jobs/tweet_notifier_job_test.rb create app/jobs/tweet_notifier_job.rb |
Trong Job Class được tạo ra, method #perform được gọi khi Job được thực thi, ta có thể truyền tùy ý tham số vào method này
1 2 3 4 5 6 7 8 | <span class="token keyword">class</span> <span class="token class-name">TweetNotifierJob</span> <span class="token operator"><</span> <span class="token constant">ActiveJob</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> queue_as <span class="token symbol">:default</span> <span class="token keyword">def</span> <span class="token function">perform</span><span class="token punctuation">(</span>user<span class="token punctuation">)</span> user<span class="token punctuation">.</span>update_stats <span class="token keyword">end</span> <span class="token keyword">end</span> |
Thêm Job vào hàng đợi
Ta có các lựa thêm job vào hàng đợi như dưới đây:
1 2 3 | <span class="token comment"># Thêm job vào hàng đợi để chạy sớm nhất có thể khi server rảnh</span> <span class="token constant">GuestsCleanupJob</span><span class="token punctuation">.</span>perform_later guest |
1 2 3 | <span class="token comment"># Thêm job vào hàng đợi để chạy vào thời gian định sẵn</span> <span class="token constant">GuestsCleanupJob</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>wait_until<span class="token punctuation">:</span> <span class="token constant">Date</span><span class="token punctuation">.</span>tomorrow<span class="token punctuation">.</span>noon<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">perform_later</span><span class="token punctuation">(</span>guest<span class="token punctuation">)</span> |
1 2 3 | <span class="token comment"># Thêm job vào hàng đợi để chạy sau khoảng thời gian chờ kể từ hiện tại</span> <span class="token constant">GuestsCleanupJob</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>wait<span class="token punctuation">:</span> <span class="token number">1.</span>week<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">perform_later</span><span class="token punctuation">(</span>guest<span class="token punctuation">)</span> |
1 2 3 | <span class="token comment"># `perform_now` and `perform_later` sẽ gọi method `perform` vì thể có thể truyền tùy ý các tham số</span> <span class="token constant">GuestsCleanupJob</span><span class="token punctuation">.</span><span class="token function">perform_later</span><span class="token punctuation">(</span>guest1<span class="token punctuation">,</span> guest2<span class="token punctuation">,</span> filter<span class="token punctuation">:</span> <span class="token string">'some_filter'</span><span class="token punctuation">)</span> |
Thực thi Job
Nếu không được set adapter, job sẽ được thực thi ngay lập tức.
Backends
Active Job cung cấp các adapters có sẵn cho một số hệ thống hàng đợi như: Sidekiq, Resque, Delayed Job,… Bạn có thể xem chi tiết tại document của ActiveJob::QueueAdapters
Cài đặt Backend
Bạn có thể cài đặt cho hệ thống hàng đợi cho ứng dụng của mình.
1 2 3 4 5 6 7 8 9 | <span class="token comment"># config/application.rb</span> <span class="token keyword">module</span> <span class="token constant">YourApp</span> <span class="token keyword">class</span> <span class="token class-name">Application</span> <span class="token operator"><</span> <span class="token constant">Rails</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Application</span> <span class="token comment"># Be sure to have the adapter's gem in your Gemfile and follow</span> <span class="token comment"># the adapter's specific installation and deployment instructions.</span> config<span class="token punctuation">.</span>active_job<span class="token punctuation">.</span>queue_adapter <span class="token operator">=</span> <span class="token symbol">:sidekiq</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Hàng đợi
Hầu hết các adapter hỗ trợ nhiều hàng đợi. Bạn có thể lập lịch cho job chạy trên một hàng đợi cụ thể
1 2 3 4 5 | <span class="token keyword">class</span> <span class="token class-name">GuestsCleanupJob</span> <span class="token operator"><</span> <span class="token constant">ActiveJob</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> queue_as <span class="token symbol">:low_priority</span> <span class="token comment">#....</span> <span class="token keyword">end</span> |
Nếu muốn quản lý rõ ràng hơn hàng đợi mà job sẽ chạy, bạn có thể dùng method #set như sau:
1 2 | <span class="token constant">MyJob</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>queue<span class="token punctuation">:</span> <span class="token symbol">:another_queue</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">perform_later</span><span class="token punctuation">(</span>record<span class="token punctuation">)</span> |
Hoặc muốn quản lý từ Job level, bạn có thể truyền 1 block vào method #queue_as. Block sẽ được thực thi trong job context (có thể gọi self.arguments) và phải trả về tên hàng đợi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token keyword">class</span> <span class="token class-name">ProcessVideoJob</span> <span class="token operator"><</span> <span class="token constant">ActiveJob</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> queue_as <span class="token keyword">do</span> video <span class="token operator">=</span> <span class="token keyword">self</span><span class="token punctuation">.</span>arguments<span class="token punctuation">.</span>first <span class="token keyword">if</span> video<span class="token punctuation">.</span>owner<span class="token punctuation">.</span>premium<span class="token operator">?</span> <span class="token symbol">:premium_videojobs</span> <span class="token keyword">else</span> <span class="token symbol">:videojobs</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token function">perform</span><span class="token punctuation">(</span>video<span class="token punctuation">)</span> <span class="token comment"># do process video</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token constant">ProcessVideoJob</span><span class="token punctuation">.</span><span class="token function">perform_later</span><span class="token punctuation">(</span><span class="token constant">Video</span><span class="token punctuation">.</span>last<span class="token punctuation">)</span> |
Callbacks
Active Job cung cấp các hooks trong vòng đời của 1 job. Callback cho phép gắn xử lý logic trong vòng đời của job.
Callbacks có sẵn
- before_enqueue
- around_enqueue
- after_enqueue
- before_perform
- around_perform
- after_perform
Cách dùng callbacks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token keyword">class</span> <span class="token class-name">GuestsCleanupJob</span> <span class="token operator"><</span> <span class="token constant">ActiveJob</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> queue_as <span class="token symbol">:default</span> before_enqueue <span class="token keyword">do</span> <span class="token operator">|</span>job<span class="token operator">|</span> <span class="token comment"># do something with the job instance</span> <span class="token keyword">end</span> around_perform <span class="token keyword">do</span> <span class="token operator">|</span>job<span class="token punctuation">,</span> block<span class="token operator">|</span> <span class="token comment"># do something before perform</span> block<span class="token punctuation">.</span>call <span class="token comment"># do something after perform</span> <span class="token keyword">end</span> <span class="token keyword">def</span> perform <span class="token comment"># Do something later</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Một số tác vụ nên dùng Active Job
Gửi email
Gửi email là nhiệm vụ phổ biến nhất có thể và nên được thực hiện trong background job. Không có lý do để gửi email ngay lập tức (trước khi response được hiển thị), tất cả các email nên được chuyển đến hàng đợi. Ngay cả khi máy chủ email phản hồi trong 100ms, thì vẫn còn 100ms mà bạn đang làm cho người dùng chờ đợi không cần thiết.
Gửi email thông qua 1 background job là cực kỳ đơn giản với Active Job, do nó được tích hợp sẵn trong ActionMailer.
Bằng cách đổi method deliver_now sang deliver_later, Active Job sẽ tự động gửi email trong hàng đợi một cách bất đồng bộ.
1 2 | <span class="token constant">UserMailer</span><span class="token punctuation">.</span><span class="token function">welcome</span><span class="token punctuation">(</span><span class="token variable">@user</span><span class="token punctuation">)</span><span class="token punctuation">.</span>deliver_later |
Xử lý ảnh
Hình ảnh có thể mất thời gian để được xử lý. Càng mất thời gian hơn nếu bạn có một vài (hoặc nhiều) kiểu và kích cỡ ảnh khác nhau cần tạo. May thay, cả Paperclip và CarrierWave đều có gem bổ sung có thể giúp xử lý những hình ảnh này trong hàng đợi thay vì tại thời điểm tải lên.
Paperclip sử dụng một gem Delayed Paperclip, hỗ trợ Active Job và CarrierWave sử dụng gem CarrierWave Backgrounder.
Với Delayed Paperclip, bạn chỉ cần gọi một method bổ sung để cho nó biết những gì bạn muốn xử lý trong background và gem sẽ xử lý phần còn lại. Bạn có thể yêu cầu nó xử lý một số kiểu ngay lập tức, trong khi các kiểu khác được xử lý trong hàng đợi.
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> has_attached_file <span class="token symbol">:avatar</span><span class="token punctuation">,</span> styles<span class="token punctuation">:</span> <span class="token punctuation">{</span> small<span class="token punctuation">:</span> <span class="token string">"25x25#"</span><span class="token punctuation">,</span> medium<span class="token punctuation">:</span> <span class="token string">"50x50#"</span><span class="token punctuation">,</span> large<span class="token punctuation">:</span> <span class="token string">"200x200#"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> only_process<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token symbol">:small</span><span class="token punctuation">]</span> process_in_background <span class="token symbol">:avatar</span><span class="token punctuation">,</span> only_process<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token symbol">:medium</span><span class="token punctuation">,</span> <span class="token symbol">:large</span><span class="token punctuation">]</span> <span class="token keyword">end</span> |
Ví dụ trên cho phép xử lý ảnh :small ngay lập tức, còn :medium và :large thì được thực hiện trong background.
Upload nội dung
Thông thường khi bạn có user tải lên nội dung, nó cần được xử lý. Đây có thể là tệp CSV cần được nhập vào hệ thống, hình ảnh cần tạo hình thu nhỏ hoặc video cần xử lý.
Một tệp CSV lớn có thể mất vài phút để xử lý, trong thời gian đó, kết nối có thể bị timeout. Bạn nên xử lý hầu hết các dữ liệu tải lên không đồng bộ trong hàng đợi.
Quá trình sử dụng như sau:
- Chấp nhận tệp và tải nó lên S3 (hoặc bất cứ nơi nào bạn đang lưu trữ nội dung do người dùng tạo).
- Thêm một job vào hàng đợi để xử lý tệp này.
- Người dùng sẽ thấy ngay một trang thành công cho họ biết rằng tệp của họ đã được gửi để xử lý.
- Hệ thống sẽ tải tập tin, xử lý nó và đánh dấu nó đã được xử lý.
Một lưu ý khác là bạn sẽ muốn lưu trữ một báo cáo về việc nhập trong cơ sở dữ liệu. Nó có thể bao gồm bất kỳ bản ghi nào không được xử lý do dữ liệu không hợp lệ. Điều cần làm là tạo tệp tin thông báo lỗi cho mỗi lần nhập để user có thể download.
Sử dụng API bên ngoài
Các API bên ngoài có thể không ổn định, chậm và trải nghiệm của người dùng không nên phụ thuộc vào chúng bất cứ khi nào có thể. Ví dụ, bên dưới nơi ta sử dụng địa chỉ IP để tìm hiểu một số thông tin địa lý bằng cách sử dụng API Telize. Nó thường phản hồi trong 200ms đến 500ms, được thêm vào thời gian phản hồi hiện tại của bạn, có thể tạo ra sự khác biệt lớn. Tất cả các API bên ngoài nên được xử lý theo cùng một cách: Dùng background job nếu có thể.
Đầu tiên, ta lên lịch thực thi 1 job, truyền vào địa chỉ IP của request hiện tại.
1 2 | <span class="token constant">LogIpAddressJob</span><span class="token punctuation">.</span><span class="token function">perform_later</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>remote_ip<span class="token punctuation">)</span> |
1 2 3 4 5 6 7 8 9 | <span class="token keyword">class</span> <span class="token class-name">LogIpAddressJob</span> <span class="token operator"><</span> <span class="token constant">ActiveJob</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> queue_as <span class="token symbol">:default</span> <span class="token keyword">def</span> <span class="token function">perform</span><span class="token punctuation">(</span>ip<span class="token punctuation">)</span> ip <span class="token operator">=</span> <span class="token string">"66.207.202.15"</span> <span class="token keyword">if</span> ip <span class="token operator">==</span> <span class="token string">"::1"</span> <span class="token constant">LogIpAddress</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>ip<span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Ở đây ta thực hiện các công việc thực tế sẽ được thực hiện. Ta sẽ thực hiện một request từ xa thực sự đến API để hiển thị thời gian yêu cầu như thế này có thể mất bao lâu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token keyword">class</span> <span class="token class-name">LogIpAddress</span> <span class="token keyword">def</span> <span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>ip<span class="token punctuation">)</span> <span class="token keyword">self</span><span class="token punctuation">.</span><span class="token keyword">new</span><span class="token punctuation">(</span>ip<span class="token punctuation">)</span><span class="token punctuation">.</span>log <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token function">initialize</span><span class="token punctuation">(</span>ip<span class="token punctuation">)</span> <span class="token variable">@ip</span> <span class="token operator">=</span> ip <span class="token keyword">end</span> <span class="token keyword">def</span> get_geo_info <span class="token constant">HTTParty</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"http://www.telize.com/geoip/<span class="token interpolation"><span class="token delimiter tag">#{</span>@ip<span class="token delimiter tag">}</span></span>"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>parsed_response <span class="token keyword">end</span> <span class="token keyword">def</span> log geo_info <span class="token operator">=</span> get_geo_info <span class="token constant">Rails</span><span class="token punctuation">.</span>logger<span class="token punctuation">.</span><span class="token function">debug</span><span class="token punctuation">(</span>geo_info<span class="token punctuation">)</span> <span class="token comment"># log response to database </span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Bạn có thể thấy những gì đã diễn ra trong Rails logs:
1 2 3 4 5 | <span class="token punctuation">[</span><span class="token constant">ActiveJob</span><span class="token punctuation">]</span> <span class="token constant">Enqueued</span> <span class="token constant">LogIpAddressJob</span> <span class="token punctuation">(</span><span class="token constant">Job</span> <span class="token constant">ID</span><span class="token punctuation">:</span> <span class="token number">839</span>db962<span class="token operator">-</span><span class="token number">28</span>a0<span class="token operator">-</span><span class="token number">4e9</span>d<span class="token operator">-</span><span class="token number">9168</span><span class="token operator">-</span>b08674ba192f<span class="token punctuation">)</span> to <span class="token function">Inline</span><span class="token punctuation">(</span>default<span class="token punctuation">)</span> with arguments<span class="token punctuation">:</span> <span class="token string">"::1"</span> <span class="token punctuation">[</span><span class="token constant">ActiveJob</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">LogIpAddressJob</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token number">839</span>db962<span class="token operator">-</span><span class="token number">28</span>a0<span class="token operator">-</span><span class="token number">4e9</span>d<span class="token operator">-</span><span class="token number">9168</span><span class="token operator">-</span>b08674ba192f<span class="token punctuation">]</span> <span class="token constant">Performing</span> <span class="token constant">LogIpAddressJob</span> from <span class="token function">Inline</span><span class="token punctuation">(</span>default<span class="token punctuation">)</span> with arguments<span class="token punctuation">:</span> <span class="token string">"::1"</span> <span class="token punctuation">[</span><span class="token constant">ActiveJob</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">LogIpAddressJob</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token number">839</span>db962<span class="token operator">-</span><span class="token number">28</span>a0<span class="token operator">-</span><span class="token number">4e9</span>d<span class="token operator">-</span><span class="token number">9168</span><span class="token operator">-</span>b08674ba192f<span class="token punctuation">]</span> <span class="token punctuation">{</span><span class="token string">"longitude"</span><span class="token operator">=</span><span class="token operator">></span><span class="token operator">-</span><span class="token number">79.4167</span><span class="token punctuation">,</span> <span class="token string">"latitude"</span><span class="token operator">=</span><span class="token operator">></span><span class="token number">43.6667</span><span class="token punctuation">,</span> <span class="token string">"asn"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"AS21949"</span><span class="token punctuation">,</span> <span class="token string">"offset"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"-4"</span><span class="token punctuation">,</span> <span class="token string">"ip"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"66.207.202.15"</span><span class="token punctuation">,</span> <span class="token string">"area_code"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"0"</span><span class="token punctuation">,</span> <span class="token string">"continent_code"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"NA"</span><span class="token punctuation">,</span> <span class="token string">"dma_code"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"0"</span><span class="token punctuation">,</span> <span class="token string">"city"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"Toronto"</span><span class="token punctuation">,</span> <span class="token string">"timezone"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"America/Toronto"</span><span class="token punctuation">,</span> <span class="token string">"region"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"Ontario"</span><span class="token punctuation">,</span> <span class="token string">"country_code"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"CA"</span><span class="token punctuation">,</span> <span class="token string">"isp"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"Beanfield Technologies Inc."</span><span class="token punctuation">,</span> <span class="token string">"postal_code"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"M6G"</span><span class="token punctuation">,</span> <span class="token string">"country"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"Canada"</span><span class="token punctuation">,</span> <span class="token string">"country_code3"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"CAN"</span><span class="token punctuation">,</span> <span class="token string">"region_code"</span><span class="token operator">=</span><span class="token operator">></span><span class="token string">"ON"</span><span class="token punctuation">}</span> <span class="token punctuation">[</span><span class="token constant">ActiveJob</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token constant">LogIpAddressJob</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token number">839</span>db962<span class="token operator">-</span><span class="token number">28</span>a0<span class="token operator">-</span><span class="token number">4e9</span>d<span class="token operator">-</span><span class="token number">9168</span><span class="token operator">-</span>b08674ba192f<span class="token punctuation">]</span> <span class="token constant">Performed</span> <span class="token constant">LogIpAddressJob</span> from <span class="token function">Inline</span><span class="token punctuation">(</span>default<span class="token punctuation">)</span> <span class="token keyword">in</span> <span class="token number">572.39</span>ms |
Kết luận
Active Job là một bổ sung tuyệt vời của Rails. Nó cung cấp một interface rõ ràng và duy nhất để thêm công việc và xử lý các job. Nếu bạn đang bắt đầu một dự án Rails mới hoặc thêm một hệ thống xếp hàng vào một dự án hiện có, chắc chắn hãy nghĩ đến việc sử dụng Active Job thay vì làm trực tiếp với hàng đợi.
Sử dụng hàng đợi có thể tăng tính khả dụng trang web (bằng cách giảm thời gian phản hồi), cung cấp thời gian phản hồi và tải server phù hợp hơn (bằng cách truyền tải nhiều công việc và server khác nhau).