Mở đầu
Tập này đưa Celery vào ứng dụng quản lý chi tiêu: việc nặng như gửi báo cáo tháng chạy nền bằng worker, lặp lại đúng lịch bằng beat, quan sát được bằng Flower. Kết thúc tập, bạn có hàng đợi chạy ổn định bằng systemd, task lỗi tự thử lại có giới hạn rồi vào dead-letter. Nối tiếp tập 9, chúng ta tái dùng Redis làm cache để làm broker.
1. Cài đặt và cấu hình Celery
Cài Celery kèm hỗ trợ Redis, rồi tạo celery.py cạnh settings.py để app tự khám phá task.
# config/celery.pyimport osfrom celery import Celeryos.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.prod")app = Celery("expense")app.config_from_object("django.conf:settings", namespace="CELERY")app.autodiscover_tasks()
# config/settings/prod.pyCELERY_BROKER_URL = env("REDIS_URL")CELERY_RESULT_BACKEND = env("REDIS_URL")CELERY_TASK_TIME_LIMIT = 300CELERY_TASK_SOFT_TIME_LIMIT = 240CELERY_TASK_ACKS_LATE = TrueCELERY_WORKER_PREFETCH_MULTIPLIER = 1
Đáng chú ý: acks_late khiến task chỉ được xác nhận sau khi chạy xong, nên worker chết giữa chừng thì task được giao lại; prefetch_multiplier = 1 ngăn worker ôm nhiều task cùng lúc.
2. Viết task đầu tiên
Task báo cáo tháng gom dữ liệu rồi gửi email, chạy ngoài request, người dùng không phải chờ.
# expenses/tasks.pyfrom celery import shared_taskfrom django.contrib.auth import get_user_modelfrom django.core.mail import send_mailfrom .services import monthly_summary@shared_task( autoretry_for=(Exception,), retry_backoff=True, max_retries=3,)def send_monthly_report(user_id): user = get_user_model().objects.get(pk=user_id) body = monthly_summary(user) send_mail("Bao cao chi tieu thang", body, "no-reply@example.com", [user.email])
Chỉ truyền khóa chính vào task rồi truy vấn lại bên trong. Task phải idempotent: chạy lại hai lần cũng cho cùng kết quả, vì Celery có thể giao lại task.
3. Lên lịch với Celery Beat
Beat là đồng hồ của hệ thống: bắn task đúng lịch, worker thực thi. Khai báo lịch trong settings bằng crontab.
# config/settings/prod.pyfrom celery.schedules import crontabCELERY_BEAT_SCHEDULE = { "send-monthly-reports": { "task": "expenses.tasks.send_monthly_reports", "schedule": crontab(day_of_month=1, hour=7, minute=0), }, "remind-due-bills": { "task": "expenses.tasks.remind_due_bills", "schedule": crontab(hour=8, minute=0), },}
Task send_monthly_reports duyệt người dùng rồi gọi send_monthly_report.delay(user.id) cho từng người. Chia nhỏ giúp một email lỗi không chặn cả loạt, từng báo cáo retry độc lập.
4. Chạy worker và beat bằng systemd
Trên production, worker và beat là service hệ điều hành, tự khởi động lại khi chết. Mỗi tiến trình một unit systemd riêng, beat chỉ chạy một bản.
# /etc/systemd/system/celery-worker.service[Unit]Description=Celery worker - quan ly chi tieuAfter=network.target[Service]User=deployWorkingDirectory=/srv/expenseEnvironmentFile=/srv/expense/.envExecStart=/srv/expense/venv/bin/celery -A config worker -l info --concurrency=2Restart=always[Install]WantedBy=multi-user.target
sudo systemctl enable --now celery-worker celery-beatsudo systemctl status celery-worker
Hai bản beat sẽ bắn trùng task, nên nhiều máy chủ thì chỉ một máy chạy beat, worker nhân bản thoải mái. Log worker xem bằng journalctl -u celery-worker, như cách tập trung log ở tập 7.
5. Giám sát bằng Flower
Flower là giao diện web xem worker, task đang chạy, task thất bại và tốc độ xử lý.
/srv/expense/venv/bin/celery -A config flower --port=5555
Đừng phơi Flower ra Internet: đặt sau Nginx với xác thực cơ bản, như cách khóa endpoint nội bộ ở tập 3. Mỗi sáng nhìn Flower là biết đêm qua bao nhiêu báo cáo thành công, task nào chậm bất thường.
6. Retry có giới hạn và dead-letter
Retry vô hạn làm hàng đợi tắc nghẽn vì task lỗi cứ quay lại, chiếm hết worker. Quy tắc ở đây: thử lại tối đa ba lần, thời gian chờ tăng dần, hết ba lần thì đưa vào dead-letter để xử lý tay.
# expenses/tasks.pyfrom celery import Taskclass DeadLetterTask(Task): def on_failure(self, exc, task_id, args, kwargs, einfo): self.app.send_task( "expenses.tasks.handle_dead_letter", args=[self.name, args, kwargs, str(exc)], queue="dead_letter", )@shared_task(base=DeadLetterTask, bind=True, max_retries=3)def remind_due_bills(self): try: send_due_reminders() except Exception as exc: raise self.retry(exc=exc, countdown=60 * 2 ** self.request.retries)
Task handle_dead_letter chỉ ghi log và gửi cảnh báo qua Sentry ở tập 7. Hàng đợi dead-letter là nơi duy nhất cần xem để biết việc nào đang kẹt, thay vì mò trong log.
Áp dụng vào dự án của bạn
Bắt đầu từ task thật ít rủi ro: gửi email báo cáo tháng bằng beat vào mùng 1. Khi đã tin tưởng, chuyển dần việc nặng khác sang hàng đợi: nhắc hạn thanh toán, tổng hợp chi tiêu, xuất CSV. Quy tắc giữ lại: request chỉ làm việc nhanh, thứ chậm hơn vài trăm mili giây đều xứng đáng thành task.
Bài viết liên quan
- Django vận hành production #9: Cache với Redis — cache framework, pattern invalidation, tránh cache sai dữ liệu
- Django vận hành production #8: Monitoring và health checks — endpoint /health, uptime, metrics với django-prometheus
- Django vận hành production #7: Logging tập trung và bắt lỗi với Sentry
Câu hỏi thường gặp
Dùng chung Redis cho cache và broker có sao không?
Được, nhưng tách database index khác nhau: cache db 1, broker db 0. Lượng task lớn thì tách hẳn một Redis riêng cho broker.
Worker nên để bao nhiêu concurrency?
Bắt đầu bằng số lõi CPU, tăng dần theo Flower. Task nặng I/O như gửi email chịu được concurrency cao, task nặng CPU không vượt số lõi.
Task chạy quá lâu thì sao?
task_time_limit kill task cứng sau 300 giây, soft_time_limit cho cơ hội dọn dẹp trước 60 giây. Task hay chạm giới hạn thì chia nhỏ hoặc tối ưu, đừng chỉ tăng giới hạn.
