0

[Microservices Series] Bài 18: Distributed Cron Jobs – Hồi sinh "Crontab" trong thế giới Container đầy biến động

Chào anh em! Ở bài 17, chúng ta đã giải quyết mượt mà các tác vụ nặng do User kích hoạt (như upload file, xuất báo cáo) bằng Background Jobs và Task Queues. Nhưng hệ thống đâu chỉ chạy khi có người đụng vào.

Một hệ thống thực tế luôn có những tác vụ ngầm chạy theo giờ:

  • "Đúng 2h sáng mỗi ngày, tổng hợp doanh thu và đối soát giao dịch."

  • "Cứ 15 phút một lần, quét các đơn hàng chưa thanh toán để hủy."

  • "Sáng mùng 1 hàng tháng, gửi email chúc mừng sinh nhật khách hàng."

Nếu làm Monolith trên một máy chủ vật lý, anh em chỉ việc gõ crontab -e của Linux, hoặc xài php artisan schedule:run của Laravel là xong. Cứ đến giờ là nó chạy. Nhưng khi mang tư duy đó lên Microservices, anh em sẽ tạo ra một mớ hỗn độn thảm họa.

1. Tại sao Crontab truyền thống "phế võ công"?

Khi đóng gói ứng dụng thành Docker container và chạy trên môi trường phân tán (như Kubernetes hay Docker Swarm), kiến trúc của bạn sẽ va phải 2 hố bom chí mạng:

  • Hiệu ứng "Nhân bản đa đình": Để chịu tải, bạn scale Service Gửi Email lên 5 containers. Nếu cả 5 containers này đều chạy Crontab nội bộ cục bộ, thì đúng 8h sáng, khách hàng sẽ nhận được... 5 cái email chúc mừng sinh nhật giống hệt nhau. Bạn vừa tốn tiền dịch vụ mail, vừa bị khách hàng chửi là spam.

  • Sự mong manh của Container: Container sinh ra để "chết". Nó có thể bị Kubernetes kill đi bất cứ lúc nào để dọn RAM hoặc chuyển sang Node khác. Nếu container chứa tác vụ Cron bị chết đúng vào lúc 1h59 sáng, thì cái lịch 2h sáng coi như bị bỏ lỡ hoàn toàn. Không ai làm thay nó cả.

Vậy làm sao để lập lịch trong một môi trường mà máy chủ cứ thoắt ẩn thoắt hiện? Chúng ta có 3 tuyệt chiêu.

2. Các chiến thuật "Distributed Cron" kinh điển

Chiến thuật 1: Dùng "Kẻ bấm giờ" của hạ tầng (Kubernetes CronJob) Nếu anh em đang dùng Kubernetes, đừng tự viết logic canh giờ trong code nữa. Hãy giao việc đó cho hạ tầng.

  • Cách hoạt động: K8s có một resource gọi là CronJob. Bạn cấu hình lịch 0 2 * * * (2h sáng mỗi ngày). Đúng giờ đó, K8s sẽ tự động sinh ra một container hoàn toàn mới (chứa code Worker của bạn), chạy xong tác vụ đối soát, rồi tự động kill container đó đi.

  • Điểm cộng: Sạch sẽ, không ngốn RAM lúc rảnh rỗi. Đảm bảo tác vụ chỉ chạy đúng 1 lần (At-least-once).

Chiến thuật 2: Bộ lập lịch tập trung (Centralized Scheduler) Bạn tạo ra một Service chuyên biệt chỉ làm đúng một việc: Bấm giờ.

  • Cách hoạt động: Service Bấm Giờ này có thể viết bằng Go (dùng thư viện như Asynq hoặc gocron). Đúng 2h sáng, nó không tự xử lý data, mà nó chỉ việc ném một thông điệp (Message) vào RabbitMQ/Kafka với nội dung: "Đến giờ đối soát rồi kìa!".

  • Ở đầu kia, dù bạn có 5 hay 50 containers của Service Đối Soát đang nghe hàng đợi, thì cơ chế của RabbitMQ cũng chỉ giao cái Message đó cho duy nhất 1 container xử lý. Hoàn toàn chấm dứt tình trạng chạy trùng lặp.

Chiến thuật 3: Ổ khóa phân tán (Distributed Lock - "Trâu chậm uống nước đục") Nếu anh em vẫn muốn dùng code (như Laravel Console Kernel) để lập lịch cho nhanh mà không muốn can thiệp hạ tầng, hãy dùng Redis Lock.

  • Cách hoạt động: Cả 5 containers đều được cài lịch chạy lúc 2h sáng. Khi đồng hồ điểm 2h, cả 5 thằng cùng lao vào... giành giật một chiếc chìa khóa duy nhất lưu trên Redis (Redis Mutex Lock).

  • Thằng Container A nhanh tay lấy được khóa (Lock Acquired), nó bắt đầu chạy lệnh đối soát. Thằng B, C, D, E chậm chân hơn khoảng vài mili-giây, thấy Redis báo "khóa đã bị lấy mất", chúng sẽ tự động quay xe và bỏ qua tác vụ đó.

  • (Anh em code Laravel chắc quen thuộc với hàm ->onOneServer() – bên dưới mui xe, nó chính là dùng Redis Lock kiểu này đấy).

3. Thực chiến đối soát vé tại hệ thống AFC

Trong hệ thống điều hành tuyến Metro, tác vụ quan trọng nhất trong ngày là đối soát giao dịch (Reconciliation) lúc 2h sáng. Hàng trăm ngàn lượt quẹt thẻ (Tap in/Tap out) phải được đối soát với dữ liệu trừ tiền thực tế từ đối tác (VNPay, ví điện tử) để tìm ra các giao dịch lệch, thiếu tiền, hoặc lỗi mạng chưa hoàn tiền cho khách.

Đây là một tác vụ nặng kinh khủng, có thể kéo dài 30 phút, và tuyệt đối không được phép chạy 2 lần cùng lúc.

Luồng thực thi của mình:

  1. Mình không dùng Cron nội bộ. Mình dùng Kubernetes CronJob.

  2. Đúng 2h sáng, K8s boot lên một container đặc biệt gọi là reconciliation-worker (viết bằng Go).

  3. Thằng Worker này tự động kéo (pull) file sao kê định dạng CSV từ SFTP của VNPay về, phân tích và so khớp với dữ liệu quẹt thẻ lưu trong TimescaleDB/PostgreSQL.

  4. Những giao dịch lệch sẽ được Worker đóng gói và gửi sang Queue của Service Hoàn Tiền (Refund Service).

  5. 2h30 sáng, chạy xong, K8s tự động "bóp cổ" cái container này để trả lại tài nguyên Server cho các dịch vụ khác. Ngủ ngon đến sáng hôm sau!

Kinh nghiệm "xương máu"

  • Idempotency (Tính lũy đẳng) là vua: Dù bạn xài K8s hay Redis Lock, thỉnh thoảng lỗi mạng vẫn có thể khiến một CronJob bị kích hoạt 2 lần. Code xử lý của bạn LUÔN phải tự kiểm tra: "Giao dịch của ngày 04/10 này mình đã đối soát chưa?" Nếu database báo status = DONE rồi thì return luôn. Đừng mù quáng chạy lại.

  • Lịch chạy rải rác (Jittering): Đừng gom 10 cái tác vụ Cron cùng chạy vào đúng 0h00 hoặc 2h00 chẵn. Lúc đó DB sẽ nhận cú sốc (Spike) khủng khiếp. Hãy rải đều ra: Cái thì chạy 2h05, cái 2h17, cái 2h33 để làm phẳng biểu đồ tải (Flatten the curve).

Với Distributed Cron, hệ thống của anh em đã có thể tự động hóa mọi quy trình ngầm mà không sợ bị dẫm chân lên nhau.

Nhưng hãy nhìn lại một chút. Chúng ta đã chia nhỏ thành 20 cái Services. Khi Mobile App (Frontend) cần hiển thị màn hình Trang Chủ, nó đang phải gọi API tới 5 cái Services khác nhau (Lấy thông tin User, Lấy ví tiền, Lấy lịch sử vé, Lấy thông báo...). Frontend đang phải làm quá nhiều việc gộp dữ liệu, code phình to, và pin điện thoại người dùng thì sụt nhanh hơn tụt quần.

Làm sao để bảo vệ Frontend khỏi sự phức tạp của Microservices bên dưới? Ở bài tiếp theo (Bài 19), chúng ta sẽ khám phá BFF Pattern (Backend for Frontend) – Khi Frontend cần một gã "quản gia" đắc lực.

Anh em đang handle các tác vụ Cron trong môi trường Docker như thế nào? Có bao giờ bị dính chưởng gửi 10 cái email sinh nhật cho sếp chưa? Cùng bình luận 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í