Cách Duolingo gửi 4 triệu thông báo trong 5 giây

5 min read

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.

Tài Liệu Tham Khảo

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *