Blog

25 May
Tìm hiểu về Queue trong Laravel
May 25, 2023

Tìm hiểu về Queue trong Laravel

   

Laravel nhận vào một request, xử lý request đó, sau đó trả về một response cho người dùng, đây là một luồng xử lý đồng bộ thông thường của một webserver, tuy nhiên thỉnh thoảng chúng ta cần thực hiện một số công việc behind the scenes để nó không gây ra gián đoạn hoặc làm chậm luồng xử lý. Ví dụ, khi chúng ta muốn gửi email hóa đơn cho người dùng sau khi họ đã đặt hàng. Nhưng chúng ta không muốn người dùng phải đợi cho đến khi máy chủ email nhận được yêu cầu, xây dựng nội dung email, và sau đó gửi đi. Thay vào đó, chúng ta muốn hiển thị cho người dùng một màn hình “Cảm ơn!” để họ có thể tiếp tục hoạt động của mình trong khi email đang được chuẩn bị và gửi đi phía sau mà không làm ảnh hưởng đến trải nghiệm của họ. Laravel cung cấp một queue system, giúp chúng ta có thể xử lý được những tác vụ ngầm ở bên dưới và cung cấp một trải nghiệm tốt hơn cho người dùng.

Concept cơ bản của Laravel Queue gồm 3 phần Job, Queue và Worker:

    •  Tạo Job
    •  Đưa Job vào Queue để đợi xử lý
    •  Gọi Worker để xử lý những Job trong Queue

Job

    Prerequisites

    Laravel cung cấp cho chúng ta rất nhiều loại connection để sử dụng như Beanstalk, Amazon SQS, Redis hoặc Database.

    Tất cả các loại connection có thể được tìm thấy trong “config/queue.php“, tại đây cung cấp cho chúng ta những thông tin:

    • Những thiết lập cụ thể của từng loại Queue driver
    • Mặc định Queue driver được sử dụng để thực thi khi có Job được thêm vào

    Create the “jobs“ Table

    Trong phạm vi bài viết này, chúng ta sử dụng Queue driver “Database“ và để sử dụng được nó ta cần phải có một table để lưu trữ tất cả các jobs. Nhưng trước tiên, cần phải thay đổi giá trị default của queue từ “sync“ sang “database“ trong “.env“ file:

    QUEUE_CONNECTION=database

    Tiếp theo, để tạo một migration tạo “jobs“ table, run “queue:table“ command:

    php artisan queue:table
    php artisan migrate

    Create first Laravel Job

    Để tạo một “Job“ class, ta sử dụng command “make:job“ và job sẽ được lưu trữ tại “app/Jobs“:

    php artisan make:job SendWelcomeEmail

    Class Structure

    Việc implement “Illuminate\Contracts\Queue\ShouldQueue“ sẽ nói cho Laravel biết job này có được push vào Queue để xử lý hay là không. Có thể thấy, “handle“ method sẽ đảm nhận việc xử lý những logic chính của Job và trong trường hợp này, “handle“ method sẽ gửi một `welcome email` khi user tiến hành đăng ký tài khoản thành công.

    <?php
    namespace App\Jobs;
    use Illuminate\Bus\Queueable;
    use Illuminate\Queue\SerializesModels;
    use Illuminate\Support\Facades\Artisan;
    use Illuminate\Queue\InteractsWithQueue;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Foundation\Bus\Dispatchable;
    use Illuminate\Contracts\Queue\ShouldBeUnique;
    class SendWelcomeEmail implements ShouldQueue
    {
        use Dispatchable;
        use InteractsWithQueue;
        use Queueable;
        use SerializesModels;
        public function __construct()
        {}
        /**
        * Execute the job.
        */
        public function handle()
        {
            // Send welcome email to user
        }
    }

    Queue

    Pushing Jobs To Queue

    Sau khi tạo thành công “Job“ class, ta cần “dispatch“ nó vào trong queue system để xử lý, có nhiều cách để thực hiện việc này:

    • Queue::push(new SendWelcomeEmail());
    • Bus::dispatch(new SendWelcomeEmail());
    • dispatch(new SendWelcomeEmail());
    • (new SendWelcomeEmail())->dispatch();
    <?php
    namespace App\Http\Controllers;
    use App\Jobs\SendWelcomeEmail;
    use Illuminate\Http\Request;
    class RegisteredUserController extends Controller
    {
        /**
        * Handle the incoming request.
        */
        public function __invoke(Request $request)
        {
            ...
        SendWelcomeEmail::dispatch();
            ...
        }
    }

    Ngoài ra, còn có một vài thuộc tính hay ho liên quan đến dispatching jobs:

    Conditional Dispatching

    Thay vì

    $active = true;
    if ($active) {
        SendWelcomeEmail::dispatch();
    }

    Ta có

    $active = true;
    SendWelcomeEmail::dispatchIf($active);

    Ngoài ra còn có “dispatchUnless“ method cũng được sử dụng để dispatch có điều kiện một Job.

    Delay Dispatching

    Nếu như chúng ta muốn một Job không được đưa vào Queue ngay lập tức để Worker có thể xử lý, “delay“ method cho phép ta làm điều đó. Ví dụ, thay vì xử lý ngay, ta sẽ delay gửi email đến user trong vòng 5 phút:

    SendWelcomeEmail::dispatch()->delay(now()->addMinutes(5));
    // or
    SendWelcomeEmail::dispatch()->delay(300); // 300s = 5m * 60s

    Lưu ý:

    • Amazon SQS queue service có giá trị delay tối đa là 15 phút
    • Việc delay “n“ phút không có nghĩa là sau “n“ phút, job sẽ được xử lý, nó sẽ phụ thuộc vào nhiều yếu tố, trong số đó là tại thời điểm job được dispatch, nếu tất cả các worker đang bận xử lý tác vụ khác, thì job vẫn sẽ ở đó và chưa được xử lý.

    Dispatching After The Response Is Sent To Browser

    SendWelcomeEmail::dispatchAfterResponse();

    Theo Laravel docs:
    > Alternatively, the dispatchAfterResponse method delays dispatching a job until after the HTTP response is sent to the user’s browser if your web server is using FastCGI. This will still allow the user to begin using the application even though a queued job is still executing. This should typically only be used for jobs that take about a second, such as sending an email.
    Since they are processed within the current HTTP request, jobs dispatched in this fashion do not require a queue worker to be running in order for them to be processed.

    Theo người viết hiểu, “dispatchAfterResponse“ method cho phép xử lý ngầm tác vụ như khi dispatch theo cách thông thường, tuy nhiên Job sẽ không được đưa vào Queue và không cần Worker để handle Job. (Dùng Queue nhưng không dùng Queue ^^)

    Synchronous Dispatching

    Ngoài ra nếu như muốn một Job được xử lý đồng bộ (synchronously) không đưa vào Queue system, ta có “dispatchSync“ method:

    // instead of
    // .env
    QUEUE_CONNECTION=sync
    // Controller
    SendWelcomeEmail::dispatch();
    // then
    // .env
    QUEUE_CONNECTION=database
    // Controller
    SendWelcomeEmail::dispatchSync();

    Customizing The Queue & Connection

    Mặc định, Jobs sẽ được đưa vào queue với queue name là `default`, ta có thể linh hoạt thay đổi nó bằng cách:

    SendWelcomeEmail::dispatch()->onQueue('email');

    Thông thường, ta sắp xếp Queue theo độ ưu tiên xử lý như: low, default, high. Việc này giúp Worker sẽ xử lý Job theo một độ ưu tiên nhất định.

    Worker

    Đầu tiên, Worker là gì? Worker là một tác vụ PHP chạy ngầm mới mục đích lấy Job từ một storage (MySQL database, Redis store, Amazon SQS) và chạy nó với những cấu hình nhất định.

    php artisan queue:work

    Command này sẽ tạo ra một instance của ứng dụng và bắt đầu thực thi jobs, instance này sẽ sống vô hạn (stay alive indefinitely) có nghĩa là, nếu như codebase bị thay đổi sau khi chạy command này, thì những thay đổi đó sẽ không được áp dụng trong lần chạy này.
    Điều này dẫn tới việc:

    • Tránh việc booting app mỗi lần thực hiện một Job
    • Phải khởi động lại Worker mỗi khi có sự thay đổi ở codebase, để sử dụng code mới nhất thực thi Job
      Ngoài ra còn có:
    php artisan queue:work --once

    Command này sẽ hoạt động theo follow: khởi động worker, xử lý 1 Job duy nhất, sau đó sẽ hủy process đó (hiểu đơn giản là tắt đi terminal dùng để chạy command).

    php artisan queue:listen

    `queue:listen` command sẽ chạy `queue:work –once` trong một vòng loop vô hạn, điều này đảm bảo được mỗi khi Job được thực thi, nó sẽ luôn sử dụng codebase mới nhất, và chúng ta không cần phải khởi động lại Queue mỗi khi có code mới nhất, tuy nhiên điều này có nghĩa là sẽ tiêu tốn tài nguyên server nhiều hơn.
    Một số option khác của Worker command:

    // thực thi Job với độ ưu tiên theo thứ tự high > default > low
    php artisan queue:work --queue=high,default,low

     

    // tự động khởi động lại worker khi đã xử lý xong 1000 job hoặc 3600s
    php artisan queue:work --max-job=1000 --max-time=3600

     

    // thực thi job tối đa 3 lần nếu job bị failed, nếu --tries=0, job sẽ được
    retry vô hạn lần
    php artisan queue:work --tries=3

     

    // nếu job xử lý vượt quá giới hạn là 30s, job sẽ bị đánh dấu là failed
    php artisan queue:work --timeout=30

    Supervisor

    Trong môi trường Production, ta cần phải giữ cho `queue:work` hoạt động một cách liên tục. Worker có thể ngừng bằng một cách nào đó, với bất kỳ lý do gì, vì vậy cần phải `queue:restart` để ứng dụng hoạt động một cách trơn tru. Vì lí do này, ta cần một `process monitor` để có thể phát hiện được khi nào `queue:work` bị exit và cần thực hiện `queue:restart` tự động.
    `Supervisor` là một tool thường được sử dụng trên môi trường Linux.

     Installing Supervisor (Ubuntu)

    sudo apt install supervisor

    Configuring Supervisor

    Config file của supervisor thường được đặt ở `/etc/supervisor/conf.d`. Ví dụ, ta có `laravel-worker.conf‘ file như sau:

    [program:laravel-worker]
    process_name=%(program_name)s_%(process_num)02d
    command=php /home/tupa/seminar/artisan queue:work --sleep=3 --tries=3 --
    max-time=3600 --queue=high,default,low
    autostart=true
    autorestart=true
    stopasgroup=true
    killasgroup=true
    user=forge
    numprocs=8
    redirect_stderr=true
    stdout_logfile=/home/tupa/seminar/storage/logs/worker.log
    stopwaitsecs=3600

    Một vài thuộc tính cần lưu ý:

    • `command`: command mà ta muốn supervisor thực thi, ở đây là `php artisan queue:work`
    • `autostart` & `autorestart`: đảm bảo supervisor sẽ tự động restart khi bị exit
    • `numprocs`: số lượng process `queue:work` được chạy cùng lúc
    • `redirect_stderr`: logging `std_out` và `std_err` cùng một file tại `stdout_logfile`
    • `stopwaitsecs`: khi thực hiện stop supervisor, sẽ cho phép process `queue:work` thực hiện Job đang được xử lý trong vòng 3600s trước khi bị exit.

    Starting Supervisor

    Khi file config được tạo xong, ta cần thực hiện các câu lệnh để có thể chạy được supervisor:

    sudo supervisorctl reread
    sudo supervisorctl update
    sudo supervisorctl start 'laravel-worker:*'

    Ngoài ra còn có:

    sudo supervisorctl stop 'laravel-worker:*' // stop specific supervisor
    sudo supervisorctl stop all // stop all supervisor
    sudo supervisorctl restart 'laravel-worker:*' // restart specific
    supervisor
    sudo supervisorctl restart // restart all supervisor

    Conclusion

    Trong bài viết này, chúng ta đã đi tìm hiểu cơ bản về Queue System của Laravel, cách nó hoạt động và cách set up một Laravel Queue cơ bản từ đầu. Ngoài ra chúng ta còn biết thêm về Supervisor, một công cụ hữu ích và cần thiết cho việc sử dụng queue trên môi trường Prodution.
    _TuPA_