Thứ Năm, tháng 4 11, 2024

GoJek::How We Manage a Million Push Notifications an Hour

 

GoJek - Một đơn vị đang hoạt động trên lĩnh vực digital logistic & transport mà các anh chị em hẳn không còn thấy lạ lẫm, hoặc thậm chí cũng đã sử dụng tới dịch vụ của họ. Hôm nay nhân dịp mình đọc được bài viết của Go Jek mô tả về hệ thống push notification của họ ở đây: How We Manage a Million Push Notifications an Hour, mình cũng thấy khá là thú vị. Và trên phương diện người làm liên quan tới công nghệ, cộng với mò mẫm tìm hiểu trên internet, thêm 1 chút lý thuyết (lý thuyết nhé) mà mình có trong quá trình học cloud, aws/google services... mình muốn chia sẻ thêm một chút về mặt giải pháp công nghệ, phương án giải quyết vấn đề trên quan điểm thu lượm và góp nhặt được.
Về lý thuyết mình đã có, thì thông thường 1 hệ thống push notification cũ (dễ sẽ được viết bằng python + mysql) có thể gặp nhiều vấn đề về performance như:
  • Latency lớn gây chậm các service yêu cầu.
  • Service quá tải vào các thời gian cao điểm dẫn tới mất push.
  • Failed rate liên quan tới network với firebase lớn do tần suất call dày đặc.
  • Quản lý token không hiệu quả, xuất hiện duplicate token và nhiều token inactive gây chậm quá trình push.
  • Thiết kế DB dạng normalized không phù hợp dẫn tới việc tất cả các request push đều cần join 3 table.
  • Sử dụng nhiều tài nguyên CPU và RAM
Ngoài ra hệ thống cũ này chưa track được tỷ lệ thành công của mỗi lần push, cũng như truy vết hành động push từ các service nội bộ khác. Đơn giản với mỗi request nhận được, Push API sẽ gọi list token của user từ Token store và gửi yêu cầu push lên Firebase API
Trên lý thuyết, mình cần giải quyết các nút thắt bởi DB (database) và API.
Về cơ bản, database sẽ lưu 2 loại dữ liệu chính:
  • Push token (FCM token) của user: Update ít, đọc theo list nhiều, nhiều data bên lề như thông tin thiết bị, thông tin hệ điều hành cần lưu cùng.
  • Push log chứa log và kết quả push: Insert + update rất nhiều, đọc ít.
Phần Push API sẽ chủ yếu nghe traffic request push từ các service nội bộ. Để quá trình này diễn ra nhanh chóng và không bắt các service khác phải chờ thì flow như sau:
  • Nhận yêu cầu push từ service nội bộ
  • Tạo job với unique job-id
  • Push job vào Job Queue
  • Trả lại job-id cho service để trace về sau
Bằng việc đơn giản hóa phần việc của Push API và thiết kế lại DB mình sẽ giải quyết được 3 vấn đề: latency của API, các vấn đề liên quan tới quản lý tokenperformance của việc tìm token cho từng request.

Thiết kế job worker

Các bạn đọc bài viết của Go Jek mình đề cập ở trên thì sẽ thấy phần thiết kế job worker của họ khá sơ sài khi chỉ nhắc tới việc xử lý mỗi yêu cầu push bằng 1 job. Với cách implement truyền thống 1 job dạng:
  • Insert Push log vào DB
  • Filter token của user từ DB
  • Build push request dựa vào list token
  • Call Firebase API
  • Update kết quả push vào DB
thì 1 job push sẽ mất trung bình 300~400ms để xử lý. Hệ thống sẽ cần scale số lượng worker lên tương đối lớn để đạt hiệu suất sử lý song song lớn.
Tuy nhiên trong hệ thống này các bạn sẽ thấy 2 điểm bottleneck ảnh hưởng tới performance của hệ thống như sau:
  • Insert log và update kết quả làm tăng load database
  • Call API tới bên thứ 3 (ở đây là Firebase API) có giới hạn và độ trễ lớn do network
Để giải quyết được 2 vấn đề này, kỹ thuật batch processing nội bộ trong từng instance Job worker sẽ là tối ưu. Tức là gộp chung 1 loạt những request giống nhau vào 1 lần call DB, API thông qua batch.
Cũng đơn giản thôi, những bài toán xử lý dữ liệu lớn, có sự đồng bộ nhất định giữa các bản ghi... thì sử dụng batch luôn sẽ được nghĩ tới đâu tiên. Thế nhưng có mạnh thì sẽ có yếu thôi. Batch processing là một kỹ thuật khó và xử lý được nó đòi hỏi các bạn phải tính toán thật cẩn thận về các vấn đề liên quan tới error, retry, report. Batch là đồng bộ nên thay vì khi phát sinh sự cố trên mỗi push riêng lẻ, thì phương án này sẽ có sự ảnh hưởng hàng loạt tới vài trăm tiến trình khác trong cùng batch... nếu như việc retry không được xử lý cẩn thận.
Firebase API cũng hỗ trợ batch request lên tới 500 message/call thì việc tận dụng batch đã giảm đi rất nhiều thời gian chờ API call (do giảm số lượng request) cũng như tài nguyên của database (do giảm số query đơn lẻ). Điều này có thể đáp ứng trên 1000 push/s, tức là 3,6 triệu push / giờ chỉ với 1 worker và tiêu thụ lượng tài nguyên rất khiêm tốn.
-from: gojekengineering blog

Không có nhận xét nào:

Đăng nhận xét

Ý chí của FAM

 Bất kỳ sự tồn tại nào muốn ổn định và lâu dài đều cần những tiêu chí hữu ích và tốt đẹp để mang lại lợi ích cho mọi người. Thế giới hoàn mỹ...