Chương 24. Worker Là Gì?
Ở chương trước, ta nói:
> Request nên trả nhanh. Việc lâu nên đưa vào job nền.
Nhưng khi đã đưa việc vào job nền, câu hỏi tiếp theo là:
> Ai là người thật sự làm những việc đó?
Câu trả lời là worker.
Nói đơn giản:
> Worker là tiến trình hoặc chương trình chạy nền, lấy việc từ queue ra và xử lý.
Ví dụ:
Queue có job RunGradingJob(job_123)
Worker lấy job này
Worker gọi Gemini
Worker lưu kết quả
Worker đánh dấu job xong
Nếu web server giống quầy nhận yêu cầu của khách, thì worker giống người trong bếp hoặc trong xưởng.
Quầy nhận đơn không nên đứng làm bánh 30 phút.
Quầy nhận đơn ghi phiếu.
Bếp lấy phiếu và làm.
Trong phần mềm cũng vậy:
Web server nhận request.
Worker xử lý việc nền.
---
24.1. Worker khác web server ở đâu?
Web server xử lý request từ người dùng hoặc client.
Worker xử lý job từ queue.
So sánh:
| Thành phần | Nhận việc từ đâu? | Mục tiêu chính | |---|---|---| | Web server | HTTP request | Trả response nhanh | | Worker | Queue/job table | Làm việc nền đến khi xong |
Ví dụ web server:
POST /submissions
-> validate
-> lưu submission
-> tạo grading job
-> enqueue job
-> trả 202 + job_id
Ví dụ worker:
RunGradingJob(job_id)
-> load grading job
-> gọi AI API
-> parse kết quả
-> lưu score/feedback
-> phát GradingCompleted
Web server sống trong nhịp của người dùng.
Worker sống trong nhịp của hàng đợi.
Đây là sự khác biệt rất quan trọng.
---
24.2. Ví dụ quán bánh: quầy và bếp
Khách đến quầy đặt bánh.
Nhân viên quầy:
- Nhận yêu cầu.
- Kiểm tra thông tin.
- Tạo đơn.
- Đưa phiếu cho bếp.
- Báo khách mã đơn.
Bếp:
- Lấy phiếu.
- Chuẩn bị nguyên liệu.
- Làm bánh.
- Đánh dấu xong.
Nếu quầy cũng tự làm bánh, khách phía sau phải chờ.
Nếu bếp cũng phải ra quầy nhận khách, bếp bị gián đoạn.
Vì vậy quầy và bếp có vai trò khác nhau.
Trong hệ thống:
Web server = quầy nhận yêu cầu.
Worker = bếp xử lý việc.
Queue = khay phiếu chờ.
Một hệ thống khỏe thường biết tách ba thứ này.
---
24.3. Worker lấy việc như thế nào?
Mô hình cơ bản:
Producer -> Queue -> Worker
Producer có thể là:
- Web server.
- Scheduler/cron.
- Một worker khác.
- Event handler.
- Admin action.
Queue giữ job.
Worker lấy job ra xử lý.
Ví dụ:
Web server enqueue RunGradingJob(job_123)
Queue:
RunGradingJob(job_123)
RunGradingJob(job_124)
SendEmail(email_555)
Worker:
lấy RunGradingJob(job_123)
xử lý
Worker có thể chạy một hoặc nhiều process.
Mỗi worker có thể xử lý một hoặc nhiều job cùng lúc, tùy concurrency.
Chương sau sẽ nói kỹ về concurrency.
---
24.4. Worker có phải là một service riêng không?
Không nhất thiết.
Worker có thể nằm trong cùng codebase với backend.
Ví dụ Django + Celery:
same codebase
├── web process
└── celery worker process
Web và worker dùng chung code, chung domain model, chung database.
Chúng chỉ là hai cách chạy khác nhau.
Worker cũng có thể nằm trong service riêng.
Ví dụ:
main-app
ai-judge-service
├── API
└── workers
Tách service riêng khi có lý do:
- Workload khác hẳn.
- Cần scale riêng.
- Cần deploy riêng.
- Team riêng sở hữu.
- Cần runtime khác.
- Cần cô lập lỗi/cost.
Nhưng đừng nhầm:
> Có worker không có nghĩa là phải có microservice.
Monolith vẫn có worker rất tốt.
---
24.5. Worker task không nên chứa nghiệp vụ lộn xộn
Một lỗi phổ biến là nhét toàn bộ logic vào task.
Ví dụ không tốt:
celery_task_run_grading(job_id):
query submission
build prompt
call AI
parse score
update submission
update learning progress
send email
update analytics
unlock next lesson
Task này làm quá nhiều.
Cách tốt hơn:
Celery task
-> RunGradingJobUseCase
-> GradingJob Aggregate
-> Repository
-> Outbox/Event
Worker task nên giống adapter:
Task nhận job_id.
Task gọi use case.
Use case xử lý nghiệp vụ.
Như vậy nếu sau này muốn chạy lại job từ admin, CLI, hoặc queue khác, ta vẫn dùng được use case cũ.
Worker là nơi chạy việc.
Không phải nơi vứt mọi logic vào cho khuất mắt.
---
24.6. Một worker xử lý một loại việc hay nhiều loại việc?
Ban đầu, một worker có thể xử lý nhiều loại job.
Ví dụ:
default worker:
- SendEmail
- RunGradingJob
- GenerateReport
- SyncCrm
Hệ thống nhỏ thì ổn.
Nhưng khi job khác nhau quá nhiều, nên tách.
Ví dụ:
worker-email
worker-ai
worker-report
worker-import
worker-analytics
Vì mỗi loại việc có đặc điểm khác nhau:
- Email nhanh nhưng nhiều.
- AI chậm, I/O-bound, tốn tiền.
- Report nặng database.
- Video nặng CPU.
- Analytics có thể low priority.
Nếu để chung, job dài có thể chặn job ngắn.
Ví dụ:
100 job AI mỗi job 90 giây
email reset password phải chờ sau chúng
Đây là thiết kế không tốt.
Email reset password nên có queue/worker ưu tiên cao hơn.
---
24.7. Queue riêng là gì?
Queue riêng nghĩa là chia hàng đợi theo loại việc.
Ví dụ:
queues:
ai_grading
email_transactional
email_bulk
reports
imports
analytics
Worker có thể nghe một hoặc nhiều queue.
Ví dụ:
worker-ai:
listens to ai_grading
worker-email-critical:
listens to email_transactional
worker-email-bulk:
listens to email_bulk
worker-report:
listens to reports
Lợi ích:
- Job quan trọng không bị job phụ chặn.
- Mỗi loại job có concurrency riêng.
- Mỗi loại job có retry/timeout riêng.
- Dễ nhìn bottleneck.
- Dễ scale đúng chỗ.
Queue riêng là một trong những cách đơn giản nhất để hệ thống worker trưởng thành hơn.
---
24.8. Khi nào cần nhiều queue?
Cần nghĩ đến nhiều queue khi:
- Có job rất lâu và job rất ngắn trộn nhau.
- Có job quan trọng và job phụ trộn nhau.
- Có job dùng tài nguyên khác nhau.
- Có job cần retry khác nhau.
- Có job cần concurrency khác nhau.
- Có job không được để chậm.
Ví dụ:
Reset password email:
cần nhanh.
Marketing email:
chậm vài giờ vẫn được.
Không nên để chúng chung một queue nếu marketing email có thể rất nhiều.
Ví dụ:
AI grading:
chờ Gemini 90 giây, I/O-bound, tốn tiền.
Report export:
nặng database.
Nếu chung queue, tăng worker để chấm AI có thể làm report kéo DB quá mạnh.
Tách queue giúp mỗi loại việc có nhịp riêng.
---
24.9. Khi nào cần nhiều nhóm worker?
Nhiều queue thường đi cùng nhiều nhóm worker.
Ví dụ:
worker-ai:
concurrency cao hơn nếu I/O-bound
rate limit theo Gemini quota
worker-report:
concurrency thấp để bảo vệ database
worker-email:
retry/backoff theo email provider
worker-video:
chạy trên máy CPU/GPU mạnh hơn
Mỗi nhóm worker có thể có:
- Số lượng instance riêng.
- Concurrency riêng.
- Timeout riêng.
- Memory/CPU riêng.
- Rate limit riêng.
- Retry policy riêng.
Đây là lý do tách worker rất hữu ích.
Không phải job nào cũng nên chạy trên cùng một loại máy.
---
24.10. Worker pool là gì?
Worker pool là nhóm worker cùng xử lý một loại hoặc một nhóm queue.
Ví dụ:
AI worker pool:
worker-ai-1
worker-ai-2
worker-ai-3
Email worker pool:
worker-email-1
worker-email-2
Nếu queue AI dài, ta scale AI worker pool.
Nếu email chậm, ta scale email worker pool.
Không cần scale toàn bộ web app.
Đây là điểm rất đẹp:
> Worker pool giúp scale theo workload, không scale mù toàn hệ thống.
---
24.11. Worker chạy liên tục hay chạy theo job?
Có hai kiểu phổ biến.
Worker chạy liên tục
Worker luôn sống, chờ job mới.
Ví dụ:
Celery worker
Sidekiq worker
BullMQ worker
SQS consumer
Phù hợp cho queue có job thường xuyên.
Worker chạy theo job
Mỗi job có thể tạo một process/container riêng rồi kết thúc.
Ví dụ:
- Kubernetes Job.
- Cloud Run Job.
- Batch job.
- Serverless function.
Phù hợp cho việc:
- Không chạy liên tục.
- Cần cô lập mạnh.
- Job rất nặng.
- Job theo lịch.
Không có kiểu nào luôn tốt hơn.
Worker chạy liên tục đơn giản và nhanh cho workload thường xuyên.
Job/container riêng tốt cho việc nặng hoặc cần cô lập.
---
24.12. Worker và scheduler khác nhau thế nào?
Scheduler là người quyết định khi nào tạo job.
Worker là người xử lý job.
Ví dụ:
Scheduler:
mỗi đêm 1h tạo GenerateDailyReport
Queue:
giữ GenerateDailyReport
Worker:
lấy GenerateDailyReport và chạy
Đừng để scheduler tự làm hết việc nặng nếu có queue.
Cách tốt:
Scheduler -> enqueue job -> Worker xử lý
Như vậy vẫn có retry, timeout, monitoring, DLQ.
---
24.13. Worker và cron khác nhau thế nào?
Cron là một cách lập lịch.
Worker là người làm việc.
Ví dụ:
Cron chạy mỗi 5 phút:
enqueue CheckExpiredSeatHolds
Worker:
xử lý CheckExpiredSeatHolds
Nếu cron tự chạy logic nặng:
Cron:
query 1 triệu dòng
gửi 100k email
update database lớn
thì khó retry, khó monitoring, khó scale.
Cron nên là cò khởi động.
Worker nên là người xử lý.
---
24.14. Worker cần biết trạng thái job không?
Với việc quan trọng, có.
Queue chỉ biết message đang chờ hoặc đang xử lý ở mức broker.
Ứng dụng cần biết trạng thái nghiệp vụ:
PENDING
RUNNING
SUCCEEDED
FAILED
RETRYING
CANCELLED
Ví dụ:
GradingJob
- id
- status
- attempt_count
- started_at
- completed_at
- failed_reason
Worker cập nhật trạng thái:
PENDING -> RUNNING -> SUCCEEDED
hoặc:
RUNNING -> FAILED -> RETRYING
Không có job status, frontend và admin không biết việc đang ra sao.
Queue không thay thế bảng trạng thái job trong nghiệp vụ.
---
24.15. Worker chết giữa chừng thì sao?
Worker có thể chết vì:
- Deploy.
- Server restart.
- Out of memory.
- Bug.
- Network lỗi.
- Process bị kill.
Nếu worker chết giữa job, hệ thống phải xử lý được.
Cần:
- Job timeout.
- Queue visibility timeout hoặc ack đúng cách.
- Retry.
- Idempotency.
- Phát hiện job kẹt
RUNNING. - Graceful shutdown.
Ví dụ:
Job RUNNING quá 30 phút
không heartbeat
-> đánh dấu STALE
-> retry hoặc fail
Không nên giả định worker đã lấy job thì chắc chắn chạy xong.
Trong hệ thống thật, worker chết là chuyện bình thường.
---
24.16. Graceful shutdown của worker
Khi deploy, ta không nên cắt worker quá thô.
Graceful shutdown nghĩa là:
- Ngừng nhận job mới.
- Cho job đang chạy hoàn thành trong giới hạn.
- Nếu không hoàn thành, job được retry an toàn.
- Ack/nack rõ.
Nếu kill worker ngay:
- Job có thể chạy lại.
- Job có thể kẹt trạng thái.
- External API call có thể dang dở.
Vì vậy job phải idempotent.
Graceful shutdown giúp giảm rủi ro, nhưng không thay thế idempotency.
---
24.17. Worker có nên gọi API bên ngoài không?
Có, rất nhiều job nền là để gọi API bên ngoài.
Ví dụ:
- Gemini API.
- Email provider.
- Payment provider.
- CRM.
- Delivery provider.
Nhưng cần:
- Timeout.
- Retry có kiểm soát.
- Rate limit.
- Circuit breaker nếu cần.
- Idempotency.
- Log request/response vừa đủ.
Ví dụ AI worker gọi Gemini:
RunGradingJob
-> call Gemini
-> timeout 120s
-> retry nếu lỗi tạm thời
-> mark failed nếu quá số lần
Không nên để worker treo vô hạn vì external API không trả lời.
---
24.18. Worker có nên truy cập database không?
Có.
Worker thường cần database để:
- Load job data.
- Cập nhật trạng thái.
- Lưu kết quả.
- Kiểm tra idempotency.
- Ghi event/outbox.
Nhưng worker cũng có thể làm database nghẽn.
Ví dụ:
100 report workers cùng export dữ liệu lớn
Database có thể bị kéo sập.
Vì vậy mỗi worker pool cần concurrency phù hợp.
Đặc biệt với job:
- Export lớn.
- Import lớn.
- Analytics batch.
- Backfill dữ liệu.
phải bảo vệ database.
Không phải cứ tăng worker là tốt.
---
24.19. Worker và memory
Job nền có thể ăn nhiều memory hơn request bình thường.
Ví dụ:
- Đọc file CSV lớn vào memory.
- Render report lớn.
- Xử lý ảnh/video.
- Load nhiều records.
Nếu worker bị out of memory, process chết và job có thể retry mãi.
Cần:
- Xử lý streaming/batch.
- Giới hạn kích thước input.
- Timeout.
- Memory limit.
- Tách worker nặng ra máy riêng.
- Theo dõi memory.
Đừng để job lớn chạy cùng worker với job nhỏ quan trọng.
---
24.20. Worker và CPU
Job CPU-bound cần CPU thật.
Ví dụ:
- Nén ảnh lớn.
- Render video.
- Tính toán nặng.
- Xử lý ML local.
Tăng concurrency quá cao có thể làm CPU tranh nhau và mọi thứ chậm hơn.
Với CPU-bound:
- Dùng process.
- Giới hạn concurrency gần số core.
- Tách worker riêng.
- Có thể dùng máy mạnh hơn hoặc GPU nếu cần.
Khác với I/O-bound như gọi AI API, CPU-bound không được lợi nhiều từ việc tạo hàng trăm greenlet chờ mạng.
---
24.21. Worker và I/O-bound
Job I/O-bound phần lớn thời gian là chờ.
Ví dụ:
- Gọi Gemini API.
- Gửi email.
- Gọi CRM.
- Upload file.
Với I/O-bound, concurrency cao hơn có thể giúp dùng tài nguyên tốt hơn.
Ví dụ:
Mỗi AI job chờ 90 giây.
4 worker slot chỉ xử lý khoảng 2.67 job/phút.
Nếu external API cho phép nhiều hơn và tài nguyên nội bộ chịu được, tăng concurrency có thể cải thiện throughput.
Nhưng vẫn phải kiểm soát:
- Rate limit.
- Cost.
- DB connection.
- Timeout.
- Error rate.
Chương sau sẽ nói kỹ hơn.
---
24.22. Worker và rate limit
Worker có thể vô tình gọi external API quá nhanh.
Ví dụ:
100 AI workers cùng gọi Gemini
Nếu quota không đủ, provider trả 429.
Cần:
- Global rate limiter.
- Concurrency limit.
- Backoff.
- Queue riêng.
- Theo dõi 429/error rate.
Rate limit không nên chỉ nằm trong đầu người vận hành.
Nó nên được thiết kế thành cơ chế.
---
24.23. Worker và priority
Không phải job nào cũng quan trọng như nhau.
Ví dụ:
High priority:
- reset password email
- payment confirmation
- exam grading trong giờ thi
Low priority:
- marketing email
- analytics backfill
- recommendation rebuild
Có thể dùng:
- Queue riêng.
- Priority queue.
- Worker riêng.
- Rate limit riêng.
Cách đơn giản và rõ nhất thường là queue riêng.
Đừng để job phụ làm chậm job quan trọng.
---
24.24. Worker và fairness
Nếu một user hoặc một tenant tạo quá nhiều job, họ có thể chiếm hết worker.
Ví dụ:
Một trường nộp 100.000 bài chấm AI.
Các trường khác phải chờ rất lâu.
Cần nghĩ đến fairness:
- Giới hạn job pending mỗi user/tenant.
- Round-robin theo tenant.
- Tách queue theo khách hàng lớn.
- Ưu tiên theo plan.
- Backpressure khi quá tải.
Không phải hệ thống nào cũng cần ngay.
Nhưng với SaaS nhiều khách hàng, fairness rất quan trọng.
---
24.25. Worker và observability
Worker system cần dashboard riêng.
Cần biết:
- Queue length.
- Oldest job age.
- Job wait time.
- Job processing time.
- Success rate.
- Failure rate.
- Retry count.
- DLQ count.
- Worker online/offline.
- Worker CPU/RAM.
- External API timeout/429.
Nếu chỉ nhìn web request latency, bạn sẽ không biết job nền đang chết.
Ví dụ:
Web app vẫn nhanh.
Nhưng queue AI backlog 20.000 job.
Người dùng vẫn phàn nàn vì điểm mãi chưa có.
Worker cần được quan sát như một phần chính của hệ thống.
---
24.26. Worker trong AI Judge
Một thiết kế thực dụng:
web:
nhận submission
tạo GradingJob
enqueue RunGradingJob
trả job_id
worker-ai:
nghe queue ai_grading
chạy RunGradingJobUseCase
gọi Gemini
lưu score/feedback
phát GradingCompleted
worker-notification:
gửi email/push/in-app notification
worker-analytics:
ghi analytics/cost
Tại sao tách?
- AI job chậm và tốn quota.
- Notification có retry khác.
- Analytics có thể chậm hơn.
- Web request cần nhanh.
Nếu tất cả chung một worker pool, AI job có thể làm notification chậm.
Nếu tất cả nằm trong web request, web app nghẽn.
---
24.27. Worker trong payment
Webhook payment nên nhận nhanh.
Ví dụ:
Payment Gateway -> /webhooks/payment
Webhook handler:
verify signature
lưu event
enqueue ProcessPaymentWebhook
return 200
Worker:
ProcessPaymentWebhook
-> cập nhật Payment
-> phát PaymentSucceeded
Các worker/consumer khác:
Ordering cập nhật order paid
Accounting ghi doanh thu
Notification gửi biên nhận
Không nên để webhook request làm mọi việc nặng.
Provider có thể timeout và retry, tạo rối.
---
24.28. Worker trong report/export
Report/export thường nên có worker riêng.
Ví dụ:
POST /report-jobs
-> tạo ReportJob
-> enqueue GenerateReport
-> trả report_job_id
worker-report
-> query database theo batch
-> tạo file
-> upload storage
-> cập nhật link tải
Worker report nên có concurrency thấp nếu query nặng.
Không nên để 50 report lớn chạy cùng lúc kéo database xuống.
Với report, giới hạn concurrency đôi khi quan trọng hơn tăng tốc.
---
24.29. Cấu trúc deployment đơn giản
Một hệ thống nhỏ có thể deploy như:
web x 2
worker-default x 1
redis/rabbitmq
database
Khi lớn hơn:
web x 4
worker-ai x 10
worker-email x 3
worker-report x 2
worker-analytics x 2
broker
database
Không cần bắt đầu quá phức tạp.
Nhưng cần biết đường tiến hóa:
Một worker chung
-> tách queue
-> tách worker pool
-> scale từng pool
-> tách service nếu có lý do
Đi từng bước là cách thực dụng.
---
24.30. Bảng phân vai nhanh
| Thành phần | Vai trò | |---|---| | Web server | Nhận request, trả response nhanh | | Queue/Broker | Giữ job chờ xử lý | | Worker | Lấy job ra làm | | Scheduler/Cron | Tạo job theo lịch | | Job table | Lưu trạng thái nghiệp vụ của job | | Outbox | Đảm bảo message/event không mất | | DLQ | Giữ job xử lý thất bại nhiều lần | | Dashboard | Cho biết worker system khỏe hay không |
---
24.31. Bảng chọn nhanh
| Tình huống | Nên làm | |---|---| | Một vài job nhỏ, hệ thống mới | Một worker/default queue có thể đủ | | Job dài chặn job ngắn | Tách queue | | Job quan trọng bị job phụ làm chậm | Tách queue/priority | | AI job I/O-bound chờ lâu | Worker pool riêng, concurrency phù hợp | | Report làm DB nặng | Worker riêng, concurrency thấp | | Email reset password chậm vì email marketing | Tách transactional và bulk email | | Worker chết làm job kẹt | Timeout, retry, stale job detection | | Không biết job đang ra sao | Job status + dashboard | | External API bị 429 | Rate limit worker |
---
24.32. Những lỗi phổ biến
Lỗi 1: Dùng chung một worker cho mọi thứ quá lâu
Job dài, job ngắn, job quan trọng, job phụ chen nhau.
Lỗi 2: Task chứa toàn bộ nghiệp vụ
Task trở thành chỗ code rối mới.
Lỗi 3: Không có job status
Queue có message, nhưng nghiệp vụ không biết trạng thái.
Lỗi 4: Scale worker mù
Tăng worker làm database hoặc external API sập.
Lỗi 5: Không có graceful shutdown
Deploy làm job đang chạy bị cắt lộn xộn.
Lỗi 6: Không tách worker theo workload
CPU-bound và I/O-bound dùng cùng cấu hình.
Lỗi 7: Không monitoring worker
Web app xanh, nhưng job nền chết âm thầm.
---
24.33. Checklist thiết kế worker
Khi thêm worker/job system, hãy hỏi:
- Worker này xử lý loại job nào?
- Job đến từ queue nào?
- Job có trạng thái trong database không?
- Worker gọi use case nào?
- Job có timeout không?
- Retry policy là gì?
- Job có idempotent không?
- Có cần queue riêng không?
- Có cần worker pool riêng không?
- Workload là I/O-bound, CPU-bound hay DB-bound?
- Concurrency bao nhiêu là hợp lý?
- Có rate limit external API không?
- Worker chết giữa chừng thì sao?
- Có graceful shutdown không?
- Có dashboard queue/job/worker không?
Nếu trả lời được, worker system sẽ dễ vận hành hơn rất nhiều.
---
24.34. Kết luận của chương
Worker là người xử lý việc nền.
Web server nhận yêu cầu và trả response nhanh.
Queue giữ việc chờ.
Worker lấy việc ra làm.
Một hệ thống nhỏ có thể bắt đầu bằng một queue và một nhóm worker chung.
Nhưng khi workload đa dạng, ta nên tách:
- Queue theo loại việc.
- Worker pool theo tài nguyên.
- Concurrency theo bottleneck.
- Retry/timeout theo mức rủi ro.
- Monitoring theo từng nhóm job.
Thông điệp quan trọng nhất:
> Worker không phải là nơi giấu việc lâu cho khỏi thấy. Worker là một phần chính thức của kiến trúc, cần được thiết kế, scale, theo dõi và bảo vệ như web server.
Ở chương tiếp theo, ta sẽ nói kỹ hơn về concurrency trong worker: một worker có thể xử lý bao nhiêu việc cùng lúc, process/thread/async/greenlet khác nhau ra sao, và vì sao I/O-bound như AI API cần cách nghĩ khác CPU-bound như render video.