
Nếu bạn từng học ngoại ngữ bằng Duolingo, chắc hẳn bạn đã quen với chú chim xanh Duo.
Đôi khi Duo nhắc bạn học bài.
Đôi khi Duo “đe dọa” bạn học bài.
Và đôi khi, Duo xuất hiện đúng lúc khiến bạn cảm thấy như ứng dụng này biết chính xác mình đang làm gì.
Tuy nhiên, đằng sau những notification tưởng chừng đơn giản đó là một bài toán kỹ thuật cực kỳ thú vị.
Một ngày nọ, đội ngũ Duolingo phải giải quyết một vấn đề:
Làm thế nào để gửi gần 4 triệu notification chỉ trong vài giây mà không làm sập toàn bộ hệ thống?
Nghe có vẻ đơn giản.
Nhưng thực tế lại hoàn toàn khác.
Cách Mà Hầu Hết Chúng Ta Sẽ Làm
Giả sử bạn có 4 triệu người dùng.
Vào lúc 8 giờ tối, bạn muốn gửi notification:
“Đã đến giờ học ngoại ngữ!”
Nhiều lập trình viên sẽ nghĩ tới giải pháp như sau:
const users = await getUsers();
for (const user of users) {
await sendNotification(user);
}
Hoặc:
cron.schedule("0 20 * * *", async () => {
const users = await getUsers();
for (const user of users) {
await sendNotification(user);
}
});
Ban đầu, giải pháp này hoạt động hoàn hảo với:
- 100 người dùng
- 1.000 người dùng
Tuy nhiên, với 4 triệu người dùng, mọi thứ bắt đầu sụp đổ.
Mọi thứ bắt đầu sụp đổ.
Database bị quá tải.
Worker bị nghẽn.
Queue bị tắc.
FCM và APNs bắt đầu rate limit.
Chi phí hạ tầng tăng vọt.
Một bài toán tưởng chừng đơn giản đột nhiên trở thành một hệ thống phân tán phức tạp.
Vấn Đề Thực Sự Không Nằm Ở Notification
Thoạt nhìn, nhiều người cho rằng bài toán nằm ở tốc độ gửi notification.
Tuy nhiên, đội ngũ Duolingo lại nhìn vấn đề theo một hướng hoàn toàn khác.
Đây là điều mình thấy thú vị nhất trong case study của Duolingo.
Nhiều người nghĩ bài toán là:
Làm sao gửi notification nhanh hơn?
Nhưng đội ngũ Duolingo lại nhìn theo hướng khác.
Họ nhận ra rằng:
Điều tốn thời gian nhất không phải gửi notification.
Mà là xác định:
- ai cần nhận
- nhận lúc nào
- nội dung là gì
- mức độ ưu tiên ra sao
Nếu đợi tới 8 giờ tối mới bắt đầu tính toán tất cả những điều đó thì đã quá muộn.
Cách Duolingo Nghĩ Như Một Hệ Thống Quy Mô Lớn
Thay vì xử lý mọi thứ vào thời điểm gửi, họ chuyển phần lớn công việc sang trước đó.
Trong suốt cả ngày, hệ thống liên tục chuẩn bị:
- danh sách người nhận
- nội dung notification
- thời điểm gửi
- các thông tin cá nhân hóa
Khi tới thời điểm cần gửi, hệ thống gần như chỉ còn một nhiệm vụ duy nhất:
Gửi.
Không tính toán.
Không truy vấn phức tạp.
Không xử lý business logic.
Chỉ gửi.
Từ Realtime Thành Batch Processing
Một bài học quan trọng trong kỹ thuật hệ thống là:
Nếu không bắt buộc phải realtime, đừng biến nó thành realtime.
Duolingo áp dụng nguyên tắc này rất triệt để.
Thay vì:
8PM
↓
Find users
↓
Generate notifications
↓
Send notifications
Họ chuyển sang:
All Day
↓
Prepare users
↓
Prepare payload
↓
Prepare schedule
↓
8PM
↓
Send
Chỉ một thay đổi nhỏ trong tư duy đã làm giảm đáng kể áp lực lên hệ thống.
Sức Mạnh Của Queue
Sau khi xác định người nhận, notification không được gửi trực tiếp.
Thay vào đó, chúng được đưa vào queue.
Queue đóng vai trò như một lớp đệm giữa business logic và hạ tầng gửi notification.
Nhờ đó:
- hệ thống chịu tải tốt hơn
- dễ scale ngang
- dễ retry khi lỗi
- tránh làm nghẽn service chính
Đây là lý do tại sao rất nhiều hệ thống lớn sử dụng:
- Kafka
- RabbitMQ
- SQS
- Pub/Sub
để xử lý các tác vụ bất đồng bộ.
Fan-Out: Chia Nhỏ Để Chiến Thắng
Một notification gửi cho 4 triệu người nghe có vẻ đáng sợ.
Nhưng 4 triệu notification được chia cho hàng nghìn worker lại là câu chuyện khác.
Đó là kỹ thuật fan-out.
Thay vì:
1 job
↓
4,000,000 users
Hệ thống sẽ:
1 job
↓
1,000 jobs
↓
1,000 workers
↓
4,000,000 users
Bài toán lớn được chia thành hàng nghìn bài toán nhỏ.
Và đột nhiên nó trở nên dễ giải quyết hơn rất nhiều.
Bài Học Cho Những Dự Án Nhỏ Hơn
Điều thú vị là bạn không cần 4 triệu người dùng mới áp dụng được bài học này.
Ví dụ, giả sử Gamiton trong tương lai muốn gửi thông báo:
“Trận đấu của bạn sẽ bắt đầu sau 1 giờ.”
Cách tiếp cận phổ biến là:
SELECT *
FROM matches
WHERE start_time = NOW() + INTERVAL 1 HOUR
Cron chạy mỗi vài phút và truy vấn database liên tục.
Ban đầu điều này không có vấn đề gì.
Nhưng khi số lượng trận đấu tăng lên hàng chục nghìn?
Database sẽ bắt đầu trở thành nút thắt cổ chai.
Thay vào đó, ngay khi trận đấu được tạo:
Create Match
↓
Create Reminder Job
↓
Store send_at
↓
Queue
↓
Worker
↓
Push Notification
Bạn đang chuyển chi phí tính toán sang thời điểm hệ thống rảnh hơn.
Đó chính là tư duy mà Duolingo đã áp dụng.
Bài Học Lớn Nhất
Cuối cùng, điều đáng học hỏi nhất từ Duolingo không phải là công nghệ cụ thể mà họ sử dụng.
Thay vào đó, đó là cách họ thay đổi tư duy về thời điểm xử lý công việc trong một hệ thống quy mô lớn.
Tóm lại, khi cần xử lý hàng triệu tác vụ trong vài giây, hãy cố gắng thực hiện phần lớn công việc từ trước thay vì chờ đến phút cuối cùng.
Sau khi đọc case study này, điều đọng lại với mình không phải là notification.
Mà là một nguyên tắc thiết kế hệ thống cực kỳ quan trọng:
Khi cần xử lý hàng triệu tác vụ trong vài giây, hãy chuyển càng nhiều công việc càng tốt sang trước thời điểm cần thực hiện.
Đó là khác biệt giữa cách nghĩ của một ứng dụng nhỏ và cách nghĩ của một hệ thống phục vụ hàng trăm triệu người dùng.
Và cũng chính là lý do vì sao chú chim xanh của Duolingo có thể xuất hiện đúng lúc trên điện thoại của hàng triệu người trên khắp thế giới.
