Categories
程式開發

我們是怎麼做到每小時推送百萬級通知的


推送通知是讓用戶立即接收到事件的一個非常有效的工具。在Gojek,我們每天需要處理300多萬個訂單,跨20多款產品。

可以想像的是,我們每天推送的通知數量有多大——大概每小時1百萬個。這篇文章將介紹我們在處理如此體量的推送通知時所面臨的挑戰,以及我們的解決方案。

體量大還只是其中的一個方面,在Gojek,我們還需要面對一些獨有的問題。

多個應用程序

Gojek不只有一個App,除了用戶App,我們還有GoLife、司機App、商家App,還有服務商App。

我們是怎麼做到每小時推送百萬級通知的 1

當我們的系統要推送通知,要么是推給某個用戶的App(例如,推給GoLife的通知不會被推到Gojek上),要么是推給所有的App(例如促銷通知)。

我們的系統需要足夠靈活,能夠在廣播和單獨推送之間自由地選擇。

多個通知服務提供商

因為我們的用戶App需要支持iOS和Android兩個平台,所以也需要支持多個通知系統。

Android平台我們使用了FCM(Firebase Cloud Messaging)和GCM(Google Cloud Messaging),iOS平台我們使用了APNS(Apple Push Notification Service)。

我們是怎麼做到每小時推送百萬級通知的 2

每一個通知服務提供商都為不同的App提供了不同的API秘鑰和令牌。例如,GoLife和Gojek使用的FCM API秘鑰就不一樣。

一個用戶多個設備

我們允許一個用戶同時登錄多個設備,所以通知需要被推送給用戶已登錄的所有設備上,這就存在之前的兩個問題:

  1. 用戶可以在單個設備上登錄多個App(Gojek和GoLife);

  2. 用戶可能會登錄多個設備,每個設備需要使用不同的通知服務提供商。例如,用戶可以在Android設備和iOS設備上登錄Gojek。

多個需要推送通知的服務

Gojek採用了微服務架構,我們想要讓每個服務都能推送通知,不需要操心多設備和多服務提供商問題。

我們是怎麼做到每小時推送百萬級通知的 3

為了解決上述問題,並儘可能保持API簡單,我們的通知系統被分為三個組件:

  1. 通知服務器——提供通知推送API,並將通知推送到作業隊列中;

  2. 令牌存儲——保存已登錄用戶的設備和設備令牌數據;

  3. 通知處理器——處理作業隊列中的消息,並將消息發送給通知服務提供商。

我們是怎麼做到每小時推送百萬級通知的 4

每個組件都解決了上述的一部分問題,接下來,我們來深入介紹這些組件。

令牌存儲

用戶在登錄App後,App會使用設備令牌和App ID調用令牌存儲API。

我們是怎麼做到每小時推送百萬級通知的 5

在用戶退出時,這些記錄會被刪除。

令牌存儲用於決定向用戶的哪些設備推送通知。

通知服務器

這是HTTP服務器,提供用於推送通知的API。

為了簡單起見,API要求把用戶ID和App ID放在HTTP頭部,把通知信息放在請求體裡:

POST http:///notification
user_id: 
application_id: 
{
"payload": {},
"title": "You driver is here",
"message": "Please meet your driver at the pickup point"
}

服務器從令牌存儲獲取所有的用戶設備信息,然後為每個用戶設備安排一個調度作業。

通知服務器為系統提供了外部接口,需要推送通知的服務只要通過用戶ID來調用它的API,剩下的事情由通知服務負責處理。

作業隊列

我們使用RabbitMQ作為作業隊列,並為每一種App ID和通知類型創建了單獨的隊列。

我們是怎麼做到每小時推送百萬級通知的 6

分配單獨的隊列是很重要的,因為我們要為每一種App和通知類型做好故障隔離。例如,如果com.gojek.app的FCM令牌過期,就不會影響到com.gojek.life或者com.gojek.driver.bike的作業。

通知處理器

處理器進程從作業隊列里拉取消息,把它們發送給對應的通知服務提供商。

為了保持代碼簡單,並能夠支持不同的服務提供商,我們定義了統一接口:

type PushService interface {
Push(ctx context.Context, m PushRequest) (PushResponse, error)
}

Push方法接收一個請求對象,並返回一個響應對象。

請求對象包含了與接收方和通知(比如過期時間、標題和文本)有關的信息。

type PushRequest struct {
DeviceID   string 
Title      string
Message    string
Payload    map(string)interface{}
//其他参数
}

響應消息裡包含了通知是否發送成功的信息:

type PushResponse struct {
Success         bool
ErrorMsg        string
}

然後為不同的服務提供商實現接口。例如,FCM和APNS對應的實現看起來像下面這樣:

type FCMProvider struct {
// 配置信息,比如API令牌和URL端点
}
func (p *FCMProvider) Push(ctx context.Context, m queue.Message) (notification.PushResponse, error) {
// 发送通知给FCM服务器
}
type APNSProvider struct {
// 配置信息,比如API令牌和URL端点
}
func (p *APNSProvider) Push(ctx context.Context, m queue.Message) (notification.PushResponse, error) {
// 发送通知给APNS服务器
}

通知處理器負責選擇對應的通知服務提供商,並將消息發送給它們。

結論

在面對這些挑戰時,我們找出其中的一些常用模式,把它們抽離成不同的服務,將一個相對複雜的問題變成了一系列簡單且易於管理的服務。

每當一個核心邏輯需要不同的實現時,我們就把它抽離成單獨的服務:

  • 多個設備問題通過令牌服務來解決;

  • 多個App問題通過統一的通知服務器接口來解決;

  • 多個通知服務提供商通過單獨的作業隊列和通知處理器來解決。

最終,我們構建了一個每小時能夠處理1百多萬個推送通知的系統。

原文鏈接

How We Manage a Million Push Notifications an Hour