0

[Microservices Series] Bài 17: Background Jobs & Task Queues – Nghệ thuật "nhận việc rồi để đó làm sau"

Chào anh em, mình đây! Ở bài 16, chúng ta đã đẩy thành công các file tĩnh nặng nề lên Object Storage (S3/MinIO) thông qua Pre-signed URL. Client đã nhàn, Backend đã nhẹ. Nhưng một bài toán cực "khoai" mới lại xuất hiện.

Giả sử nhân viên vận hành vừa upload thành công một file Excel chứa 100.000 dòng dữ liệu cấu hình cước phí mới cho tuyến Metro. Tiếp theo, họ gọi API: POST /api/fares/import kèm theo cái link S3 của file Excel đó để hệ thống bắt đầu đọc và ghi vào Database.

Nếu bạn viết logic import trực tiếp vào controller của API:

  1. Backend tải file Excel 50MB từ S3 về.

  2. Vòng lặp chạy qua 100.000 dòng, mỗi dòng insert 1 record vào PostgreSQL.

  3. Quá trình này mất 15 phút.

Hậu quả? Trình duyệt của user sẽ xoay mòng mòng, Nginx chém đứt kết nối sau 30 giây (Timeout 504 Gateway Time-out), còn tiến trình import thì bị giết chết giữa chừng, dữ liệu trong DB nửa vời.

Để giải quyết bài toán tác vụ nặng, chúng ta phải dùng chiến thuật Bất đồng bộ (Asynchronous) với Background Jobs, Workers & Task Queues.

1. Nguyên lý "Nhận việc rồi để đó làm sau"

Thay vì bắt người dùng (và HTTP Request) phải đứng chờ đến khi việc hoàn thành, chúng ta tách hệ thống làm 3 thành phần:

  • Producer (API Service): Chỉ làm nhiệm vụ tiếp nhận yêu cầu. Khi có người gọi API import Excel, nó lập tức tạo ra một tờ giấy ghi chú (Job/Message) với nội dung: "Ê, có file Excel URL này, mày import vào DB giùm tao". Sau đó, nó vứt tờ giấy này vào Hàng đợi và trả về ngay cho user kết quả HTTP 202 Accepted: "Đã nhận yêu cầu, hệ thống đang xử lý ngầm, vui lòng quay lại sau". API phản hồi trong 0.1 giây.

  • Task Queue (Hàng đợi): Nơi chứa các tờ giấy ghi chú. Thường dùng RabbitMQ, Redis Queue, hoặc AWS SQS. Các Job sẽ xếp hàng chờ ở đây một cách an toàn.

  • Consumer / Worker (Kẻ làm thuê thầm lặng): Một hoặc nhiều Service chạy ngầm (chạy ở chế độ CLI, không nhận HTTP Request). Chúng liên tục túc trực bên Task Queue. Có Job nào rơi xuống là chúng chộp lấy, âm thầm tải file từ S3, cặm cụi insert 100.000 dòng vào DB.

2. Thực chiến xử lý ngầm tại hệ thống AFC

Trong hệ thống vận hành Metro, mình ứng dụng Worker cực kỳ nhiều cho các tác vụ: Xuất báo cáo cuối tháng, xử lý hoàn tiền (Refund) hàng loạt, hay đối soát giao dịch với đối tác.

Với bài toán import Excel, mình tổ chức như sau:

  • API Service (Laravel): Đẩy Job vào RabbitMQ kèm theo file_url và một cái import_session_id. Đồng thời lưu trạng thái vào DB là status = PENDING.

  • Worker Service (Go): Mình viết Worker bằng Go vì nó xử lý I/O và tính toán cực nhanh, tốn ít RAM. Go Worker kéo Job ra, stream file Excel từ S3, chia nhỏ (chunk) thành từng cục 1.000 dòng rồi batch insert thẳng vào PostgreSQL.

  • Cập nhật tiến độ: Cứ mỗi lần xong 1.000 dòng, Go Worker lại update số % hoàn thành vào Redis. Nhờ vậy, Frontend có thể liên tục gọi API lấy trạng thái từ Redis (hoặc dùng WebSocket) để hiển thị thanh Progress Bar cực kỳ mượt mà cho nhân viên vận hành xem.

3. Những "bãi mìn" khi làm việc với Background Jobs

Làm Worker rất sướng vì không sợ timeout, nhưng nếu code không khéo, anh em sẽ tự hủy hệ thống lúc nào không hay.

  • Tính lũy đẳng (Idempotency): Một ngày đẹp trời, mạng chập chờn, RabbitMQ gửi lại cùng một Job 2 lần cho Worker. Nếu Worker ngây thơ chạy lại từ đầu, bạn sẽ bị lặp dữ liệu (Duplicate). Code Worker luôn phải tự hỏi: "Cái file này mình đã import chưa?" trước khi làm.

  • Lỗi giữa chừng & Dead Letter Queue (DLQ): File Excel bị lỗi format ở dòng thứ 50.000 khiến Worker bị văng Exception. Theo mặc định, Job đó sẽ bị trả lại Queue và Worker khác lại nhảy vào làm, rồi lại văng lỗi. Vòng lặp vô hạn này sẽ kéo sập CPU. Luôn cấu hình Max Retries (ví dụ: thử lại tối đa 3 lần). Nếu vẫn lỗi, vứt cái Job đó sang một góc gọi là Dead Letter Queue (Hàng đợi chết) để anh em Dev vào check log bằng tay, giải phóng hàng đợi chính cho các Job khác.

Tách được tác vụ nặng ra Background Job, hệ thống của bạn đã thực sự đạt đến độ đàn hồi đáng mơ ước. Các API nhẹ bẫng, còn Worker có thể tự động scale lên 10-20 containers khi có quá nhiều Job cần xử lý.

Nhưng chờ đã, đó là những tác vụ do User chủ động bấm nút. Vậy với những tác vụ hệ thống tự động phải làm định kỳ như: "Đúng 23h59 mỗi ngày tổng hợp doanh thu" hay "Xóa dữ liệu rác vào 3h sáng Chủ Nhật" thì sao? Trong Monolith, anh em cấu hình Crontab của Linux. Còn trong Microservices với hàng chục containers cứ sinh ra rồi chết đi, cài Crontab ở đâu bây giờ?

Anh em đã từng "chữa cháy" hệ thống vì một tác vụ quá nặng chạy thẳng trên API khiến cả server treo cứng chưa? Lần đó anh em dùng Queue nào để cứu vãn tình hình?


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí