天天看點

如何實作一個通知系統?

在實際工作中,我們經常會用到通知系統,比如,使用者完成線上購買後,需要發送訂單确認郵件、支付處理成功的短信以及包裹發貨的推送通知。那麼,什麼是通知系統?如何設計一個通知系統?

需求收集

在設計之前,我們先來詳細了解下通知系統的需求,本文從功能需求和非功能需求兩個方面來介紹。

功能需求

  • 通知類型:例如消息通知、警告通知、活動通知等。
  • 使用者群體:需要通知的使用者群體是誰,是否有分組。
  • 通知管道:例如郵件、短信、推送通知、應用内通知等。
  • 通知頻率:通知的發送頻率和限流政策。
  • 優先級:不同通知的優先級管理。
  • 使用者偏好:使用者是否可以自定義接收通知的偏好。
  • 重試機制:處理通知發送失敗的情況,必要時重試(如短信或電子郵件發送失敗)。

非功能需求

  • 可擴充性:系統應能夠每分鐘處理數百萬條通知,支援數百萬并發使用者。
  • 高可用性:確定最小的停機時間,即使在故障情況下也能發送通知。
  • 可靠性:保證至少一次的通知傳遞,對于某些使用場景可能需要保證隻有一次傳遞。
  • 低延遲:通知應盡快發送,以確定及時傳遞。

容量預估

在深入設計之前,讓我們先估算下系統規模以更好地做出設計決策。假設系統服務于 1000萬日活使用者,每個使用者平均每天接收 5條通知。

峰值負載

假設在峰值時間内(如秒殺期間)1分鐘内發送 100萬條通知,這意味着系統應能夠處理:

  • 每天的通知數量:10,000,000 x 5 = 50,000,000條通知
  • 峰值每秒通知數量:1,000,000 / 60 = ~17,000條通知/秒

存儲需求

假設每條通知的資料量大小是 1KB,則存儲容量評估為:

  • 使用者資料存儲需求:10,000,000 * 1 KB = 10GB
  • 每日通知存儲需求:10,000,000 * 5 * 1 KB = 50GB

High-level 設計

從 High-level 層面來看,通知系統将包括以下元件:

如何實作一個通知系統?

1. 通知服務(Notification Service)

  • 通知服務是所有通知請求的入口,無論是來自外部應用程式還是内部系統。它暴露的API可以供各種用戶端調用以觸發通知。
  • 這些請求可以是發送事務性通知(如密碼重置郵件)、促銷通知(如折扣優惠)或系統警報(如停機警告)。
  • 每個請求都被驗證以確定其包含所有必要的資訊,如接收者ID、通知類型、消息内容以及應通過哪些管道發送通知(電子郵件、短信等)。
  • 對于需要在未來日期或時間發送的通知,通知服務與排程服務(Scheduler Service)內建。
  • 處理請求後,通知服務将通知推送到通知隊列(如 Kafka或 RabbitMQ)。

2.使用者偏好服務

  • 使用者偏好服務允許使用者控制如何接收通知。
  • 它存儲和檢索使用者接收不同管道通知的個人偏好。
  • 服務跟蹤使用者明确選擇加入或退出的通知類型。
  • 例如:使用者可以選擇退出營銷或促銷内容。
  • 為防止使用者被通知淹沒,使用者偏好服務對某些類型的通知(尤其是促銷消息)實施頻率限制。
  • 例如:使用者每天隻能接收2條促銷通知。

3.排程服務

  • 排程服務負責存儲和跟蹤定時通知——那些需要在特定未來時間發送的通知。
  • 這些可以包括提醒、促銷活動或其他不立即發送但必須基于預定時間觸發的時間敏感通知。
  • 例如:促銷消息可能計劃在下周發送。
  • 一旦到達預定時間,排程服務将從其存儲中提取通知并将其發送到通知隊列。

4. 通知隊列

  • 通知隊列在通知服務和管道處理器之間充當緩沖區。
  • 通過将通知請求送出與通知發送解耦,隊列使系統能夠更有效地擴充,尤其是在高流量期間。
  • 隊列系統提供消息傳遞的保證。
  • 根據使用場景,可以配置為:
    • 至少一次傳遞:確定每條通知至少發送一次,即使這在罕見情況下會導緻重複消息。
    • 隻有一次傳遞:確定每條通知隻發送一次,防止重複,同時保持可靠性。

5. 管道處理器

  • 管道處理器負責從通知隊列中提取通知并通過特定管道(如電子郵件、短信、推送通知和應用内通知)發送給使用者。
  • 通過将通知服務與實際發送解耦,管道處理器實作了獨立擴充和異步處理通知。
  • 這種設定允許每個處理器專注于其指定的管道,確定可靠的發送,并内置重試機制和高效處理故障。

6. 資料庫/存儲

  • 資料庫/存儲層管理大量資料,包括通知内容、使用者偏好、定時通知、發送日志和中繼資料。
  • 系統需要混合存儲解決方案來支援不同需求:
    • 事務性資料:使用關系資料庫(如 PostgreSQL或 MySQL)存儲結構化資料,如通知日志和發送狀态。
    • 使用者偏好:使用NoSQL資料庫(如 MongoDB)存儲大量使用者特定資料,如偏好和限速。
    • Blob存儲:對于包含大附件的通知(如帶圖檔或 PDF的電子郵件),使用 OSS,Amazon S3或類似服務存儲這些附件。

Low-level設計

設計完 High-level,我們将進入更詳細的 Low-level 設計層面,主要包含以下步驟:

步驟1:通知請求建立

首先,通知系統的調用方(如電商平台、或營銷系統等)需要生成通知請求。

請求的消息結構如示例請求:

{
  "requestId": "xxx1",
  "timestamp": "2024-09-18T22:00:00Z",
  "notificationType": "transactional",
  "channels": ["email", "sms", "push"],
  "recipient": {
    "userId": "user1",
    "email": "[email protected]"
  },
  "message": {
    // 消息體
  }
}           

步驟2:通知服務接收

當調用方送出請求後,通知服務(通過API網關/負載均衡器)會接收到通知請求。請求經過身份驗證和驗證,確定其來自授權來源,并包含所有必要資訊(接收者、消息、管道等)。

步驟3:擷取使用者偏好

通知服務會查詢使用者的一些偏好服務,這部分帶有一些定制化的功能,可以根據實際情況決定是否需要此部分:

  • 偏好的通知管道(如某些使用者可能偏好通過電子郵件接收促銷消息,但通過短信接收關鍵警報)。
  • 選擇加入/退出偏好:確定符合使用者偏好,如使用者選擇退出營銷郵件。
  • 限速:確定使用者沒有超過其配置的通知限制(如每天最多3條促銷短信)。

步驟4:定時發送

如果通知計劃需要在未來的某個時刻(例如:每分鐘或基于更細粒度的間隔))發送,通知服務将通知發送到排程服務,後者将通知及其預定發送時間存儲在基于時間的資料庫或允許基于時間高效查詢的 NoSQL資料庫中。

排程服務需要定時功能,當到達預定時間時,排程服務将通知發送到通知隊列。

步驟5:将通知放入隊列

一旦通知服務建立并格式化了所需管道的消息,它将每個消息放入通知隊列系統中的相應主題(如Kafka、RocketMQ等)。

每個管道(電子郵件、短信、推送等)都有自己的專用主題,確定消息由相關的管道處理器獨立處理。

例如:如果通知需要通過電子郵件、短信和推送發送,通知服務将生成三條消息,每條消息都針對相應的管道進行定制。

  • 電子郵件消息放入電子郵件主題。
  • 短信消息放入短信主題。
  • 推送通知消息放入推送主題。

這些主題允許每個管道處理器專注于消費其相關的消息,減少複雜性并提高處理效率。

每條消息包含通知負載、管道特定資訊和中繼資料(如優先級和重試計數)。

步驟6:管道特定的消息處理

通知隊列存儲消息,直到相關的管道處理器拉取它們進行處理。

每個管道處理器作為隊列的消費者,負責消費自己的消息:

  • 電子郵件處理器從電子郵件主題拉取消息。
  • 短信處理器從短信主題拉取消息。
  • 推送處理器從推送主題拉取消息。
  • 應用内處理器從應用内主題拉取消息。

步驟7:發送通知

每個管道處理器負責通過指定的管道發送通知:電子郵件處理器:

  • 連接配接到電子郵件提供商(如SendGrid、Mailgun、Amazon SES)。
  • 發送電子郵件,確定其符合使用者偏好(如HTML或純文字)。
  • 處理錯誤如退信或無效的電子郵件位址。

短信處理器:

  • 連接配接到短信提供商(如Twilio、Nexmo)。
  • 發送短信,并進行任何格式調整以滿足字元限制或區域要求。
  • 處理問題如無效的電話号碼或網絡錯誤。

推送通知處理器:

  • 使用服務如Firebase Cloud Messaging(FCM)用于Android或Apple Push Notification Service(APNs)用于iOS。
  • 發送推送通知,包括任何中繼資料(如應用程式特定的操作或圖示)。
  • 處理失敗如過期的裝置令牌或離線裝置。

應用内通知處理器:

  • 通過WebSockets或長輪詢将應用内通知發送到使用者的活動會話。
  • 格式化消息以在應用程式的UI中顯示,遵循任何應用程式特定的顯示規則。

步驟8:監控和發送确認

每個管道處理器等待來自外部提供商的确認:

  • 成功:消息已發送。
  • 失敗:消息發送失敗(如網絡問題、無效位址)。

管道處理器将每條通知的狀态記錄在通知日志表中,以供将來參考、稽核和報告。

關鍵問題和瓶頸

故障和重試

如果通知發送由于臨時問題(如第三方提供商停機)而失敗,管道處理器将嘗試重發通知。

通常使用指數退避政策,每次重試的延遲時間逐漸增加。

如果通知在設定次數的重試後仍未發送成功,則将其移動到死信隊列(DLQ)以進一步處理。

管理者可以手動稽核和重新處理死信隊列中的消息。

可擴充性

水準擴充

系統應設計為水準擴充,意味着元件可以通過增加執行個體來應對負載增加。

  • 通知服務:随着請求量的增加,可以部署更多執行個體來管理增加的通知請求量。
  • 通知隊列:分布式隊列系統(如Kafka或RabbitMQ)天然具有可擴充性,可以通過将隊列分布在多個節點上來處理更大的工作量。
  • 管道處理器:每個處理器(電子郵件、短信等)應水準擴充以處理大量通知。

分片和分區

為了高效處理大量資料,特别是使用者資料和通知日志,分片和分區将負載分布在多個資料庫或地理區域:

  • 基于使用者的分片:根據地理位置或使用者ID将使用者分布在不同的資料庫或區域,以平衡負載。
  • 基于時間的分區:将通知日志組織成基于時間的分區(如每日或每月),以提高查詢性能并管理大量曆史資料。

緩存

使用Redis或Memcached等解決方案實作緩存,以存儲頻繁通路的資料,如使用者偏好。

緩存減少資料庫負載,并通過避免重複的資料庫查詢來提高實時通知的響應時間。

可靠性

為了高可用性,資料(如使用者偏好、日志)應在多個資料中心或區域之間複制。這確定即使一個區域故障,資料在其他地方仍然可用。

  • 多AZ複制:在多個可用區存儲資料,以提供備援。

使用負載均衡器将傳入流量均勻分布在通知服務的各個執行個體之間,確定沒有單個執行個體成為瓶頸。

監控和日志記錄

為了確定系統在大規模下的平穩運作,系統應具備:

  • 集中式日志記錄:使用ELK Stack或Prometheus/Grafana等工具收集各種元件的日志并監控系統健康。
  • 警報:設定警報以監控故障(如通知發送失敗率超過門檻值)。
  • 名額:跟蹤每個管道的成功率、失敗率、發送延遲和吞吐量等名額。

安全性

對所有傳入通知服務的請求實施強認證(如OAuth 2.0)。使用基于角色的通路控制(RBAC)限制對關鍵服務的通路。

通過在API網關上實施速率限制保護服務免受濫用,防止DoS攻擊。

歸檔舊資料

由于通知系統随着時間的推移會處理大量資料,實施歸檔舊資料的政策非常重要。

歸檔涉及将過時或不常通路的資料(如舊的發送日志、通知内容和使用者曆史記錄)從主存儲移動到成本較低、長期存儲解決方案。