近年來,通知系統已成為許多應用程式非常受歡迎的功能。通知會用重要資訊提醒使用者,例如即時新聞、產品更新、活動、優惠等。它已成為我們日常生活中不可或缺的一部分。在本章中,你會被要求設計一個通知系統。

通知不只是行動推播通知。三種通知格式為:

  • 行動推播通知(mobile push notification)
  • 簡訊(SMS message)
  • 電子郵件(Email)

圖 1 顯示這三種通知各自的範例。

圖 1

Step 1 - 理解問題並確立設計範圍#

建立一個每天可送出數百萬則通知的可擴展系統並不容易。它需要對通知生態系統有深入的理解。這個面試題目刻意設計得開放且模糊,因此你有責任提出問題以釐清需求。

應徵者:系統支援哪些類型的通知?

面試官:推播通知、簡訊與電子郵件。

應徵者:它是即時系統嗎?

面試官:我們把它當作一個軟即時(soft real-time)系統。我們希望使用者能盡快收到通知。然而,若系統處於高負載狀態,輕微的延遲是可以接受的。

應徵者:支援哪些裝置?

面試官:iOS 裝置、Android 裝置以及筆記型/桌上型電腦。

應徵者:什麼會觸發通知?

面試官:通知可由客戶端應用程式觸發,也可在伺服器端排程。

應徵者:使用者能選擇退訂嗎?

面試官:可以,選擇退訂的使用者將不再收到通知。

應徵者:每天會送出多少則通知?

面試官:1000 萬則行動推播通知、100 萬則簡訊以及 500 萬則電子郵件。

Step 2 - 提出高階設計並取得認可#

本節展示支援多種通知類型的高階設計:iOS 推播通知、Android 推播通知、簡訊以及電子郵件。結構如下:

  • 不同類型的通知
  • 聯絡資訊收集流程
  • 通知傳送/接收流程

不同類型的通知#

我們先從高層次了解每種通知類型如何運作。

iOS 推播通知#

圖 2

我們主要需要三個元件來傳送 iOS 推播通知:

  • Provider:provider 建立並送出通知請求到 Apple Push Notification Service(APNS)。要建立推播通知,provider 提供以下資料:

    • Device token:這是用於傳送推播通知的唯一識別碼。

    • Payload:這是包含通知 payload 的 JSON 字典。以下是範例:

      {
         "aps":{
            "alert":{
               "title":"Game Request",
               "body":"Bob wants to play chess",
               "action-loc-key":"PLAY"
            },
            "badge":5
         }
      }
  • APNS:這是 Apple 提供的遠端服務,用以將推播通知傳送到 iOS 裝置。

  • iOS Device:是最終客戶端,接收推播通知。

Android 推播通知#

Android 採用類似的通知流程。Android 通常使用 **Firebase Cloud Messaging(FCM)**而非 APNs 來傳送推播通知到 Android 裝置。

圖 3

簡訊#

對於簡訊,常用第三方簡訊服務,例如 Twilio [1]、Nexmo [2] 等。其中大多數是商業服務。

圖 4

電子郵件#

雖然公司可以自行架設郵件伺服器,但許多公司選擇使用商業電子郵件服務。Sendgrid [3] 與 Mailchimp [4] 是最受歡迎的電子郵件服務之一,它們提供更佳的送達率與資料分析。

圖 5

圖 6 顯示加入所有第三方服務後的設計。

圖 6

聯絡資訊收集流程#

要傳送通知,我們需要收集行動裝置的 device token、電話號碼或電子郵件地址。如圖 7 所示,當使用者安裝我們的 App 或首次註冊時,API 伺服器會收集使用者聯絡資訊並儲存到資料庫。

圖 7

圖 8 顯示用於儲存聯絡資訊的簡化資料表。電子郵件地址與電話號碼儲存在 user 資料表中,而 device token 則儲存在 device 資料表中。

一個使用者可以擁有多個裝置,這表示一則推播通知可送到該使用者所有裝置。

圖 8

通知傳送/接收流程#

我們會先呈現初始設計,然後再提出一些優化。

高階設計#

圖 9 顯示設計,每個系統元件說明如下。

圖 9

  • Service 1 to N:服務可以是微服務、cron job 或會觸發通知傳送事件的分散式系統。例如,計費服務寄送電子郵件提醒客戶繳費,或購物網站透過簡訊告知客戶包裹明天送達。
  • Notification system:通知系統是傳送/接收通知的核心。從簡單做法開始,只使用一台通知伺服器。它為 service 1 到 N 提供 API,並為第三方服務建立通知 payload。
  • Third-party services:第三方服務負責將通知送到使用者。在與第三方服務整合時,我們需要特別注意可擴充性。良好的可擴充性意味著系統具備彈性,能輕易接上或移除某個第三方服務。另一個重要的考量是某第三方服務可能在新市場或未來無法使用。例如,FCM 在中國無法使用,因此當地會使用 Jpush、PushY 等替代第三方服務。
  • iOS、Android、SMS、Email:使用者在他們的裝置上接收通知。

此設計中發現三個問題:

  • 單一故障點(SPOF):單一通知伺服器意味著 SPOF。
  • 不易擴展:通知系統把所有與推播通知相關的事都在一台伺服器中處理。難以獨立擴展資料庫、快取以及不同的通知處理元件。
  • 效能瓶頸:處理與傳送通知可能耗用大量資源。例如,建構 HTML 頁面與等待第三方服務回應都可能花費時間。把所有事情都在一個系統中處理可能導致系統過載,特別是在尖峰時段。

將資料庫、快取與通知處理邏輯全都綁在單一伺服器,不僅無法獨立擴展,也容易因第三方服務回應緩慢而造成整體瓶頸。

高階設計(改善版)#

列出初始設計中的挑戰後,我們改善設計如下:

  • 資料庫與快取移出通知伺服器。
  • 增加更多通知伺服器並設置自動水平擴展。
  • 引入訊息佇列以解耦系統元件。

圖 10 顯示改善後的高階設計。

圖 10

理解上圖最好的方式是由左至右閱讀:

  • Service 1 to N:代表透過通知伺服器提供的 API 來傳送通知的不同服務。

  • Notification servers:提供以下功能:

    • 提供 API 給服務以傳送通知。這些 API 只能由內部或經驗證的客戶端存取,以防止垃圾訊息。
    • 進行基本驗證,例如驗證電子郵件、電話號碼等。
    • 查詢資料庫或快取以取得渲染通知所需的資料。
    • 將通知資料放入訊息佇列以進行平行處理。

    以下是傳送電子郵件的 API 範例:

    POST https://api.example.com/v/sms/send

    Request body:

    {
       "to":[
          {
             "user_id":123456
          }
       ],
       "from":{
          "email":"from_address@example.com"
       },
       "subject":"Hello World!",
       "content":[
          {
             "type":"text/plain",
             "value":"Hello, World!"
          }
       ]
    }
  • Cache:使用者資訊、裝置資訊與通知範本被快取。

  • DB:儲存關於使用者、通知、設定等資料。

  • Message queues:消除元件間的依賴。當大量通知要送出時,訊息佇列充當緩衝區。每種通知類型分配到不同的訊息佇列,這樣某個第三方服務的中斷不會影響其他類型的通知。

  • Workers:workers 是一群伺服器,從訊息佇列拉取通知事件並傳送到對應的第三方服務。

  • Third-party services:已在初始設計中說明。

  • iOS、Android、SMS、Email:已在初始設計中說明。

接下來,讓我們檢視每個元件如何協同運作以送出一則通知:

  1. 服務呼叫通知伺服器提供的 API 以送出通知。
  2. 通知伺服器從快取或資料庫取得 metadata,例如使用者資訊、device token 與通知設定。
  3. 通知事件被送到對應的佇列進行處理。例如,一個 iOS 推播通知事件會送到 iOS PN 佇列。
  4. Workers 從訊息佇列拉取通知事件。
  5. Workers 將通知傳送到第三方服務。
  6. 第三方服務將通知送達使用者裝置。

Step 3 - 設計深入探討#

在高階設計中,我們討論了不同類型的通知、聯絡資訊收集流程以及通知傳送/接收流程。我們將在深入探討中討論以下內容:

  • 可靠性
  • 額外元件與考量:通知範本、通知設定、速率限制、重試機制、推播通知中的安全性、監控佇列中的通知以及事件追蹤
  • 更新後的設計

可靠性#

在分散式環境中設計通知系統時,我們必須回答幾個重要的可靠性問題。

如何防止資料遺失?#

通知系統最重要的需求之一是不能遺失資料。通知通常可以延遲或重新排序,但絕不能遺失。為了滿足此需求,通知系統會將通知資料持久化在資料庫中並實作重試機制。為了資料持久化,我們加入通知日誌資料庫,如圖 11 所示。

圖 11

收件者會剛好收到一次通知嗎?#

簡短的答案是否定的。雖然大多數情況下通知會剛好送達一次,但分散式特性可能導致重複的通知。

為了減少重複的發生,我們引入去重(dedupe)機制並謹慎處理每種失敗情況。以下是簡單的去重邏輯:

當一個通知事件首次到達時,我們透過檢查 event ID 來判斷是否已看過。若已看過則丟棄;否則就送出通知。

對於想了解為何無法做到剛好一次傳送的讀者,請參考參考資料 [5]。

額外元件與考量#

我們已經討論了如何收集使用者聯絡資訊、傳送與接收通知。通知系統不只是這些。在這裡我們討論額外的元件,包括範本重用、通知設定、事件追蹤、系統監控、速率限制等。

通知範本#

大型通知系統每天送出數百萬則通知,許多通知遵循相似的格式。我們引入通知範本以避免從頭建立每則通知。通知範本是一個預先格式化的通知,可以透過自訂參數、樣式、追蹤連結等來建立你獨特的通知。

以下是推播通知的範本範例。

BODY:
You dreamed of it. We dared it. [ITEM NAME] is back — only until [DATE].

CTA:
Order Now. Or, Save My [ITEM NAME]

使用通知範本的好處包括:

  • 維持一致的格式
  • 減少錯誤邊際
  • 節省時間

通知設定#

使用者每天通常收到太多通知,很容易感到不堪負荷。因此,許多網站與 App 提供使用者對通知設定的細緻控制。這些資訊儲存在通知設定資料表中,包含以下欄位:

  • user_id bigInt
  • channel varchar # push notification、email 或 SMS
  • opt_in boolean # 是否選擇接收通知

在傳送任何通知給使用者之前,我們會先檢查該使用者是否選擇接收這類通知。

速率限制#

為避免讓使用者收到太多通知而感到不堪負荷,我們可以限制使用者能接收的通知數量。

這很重要,因為如果送得太頻繁,收件者可能會完全關閉通知。

重試機制#

當第三方服務傳送通知失敗時,該通知會被加回訊息佇列以便重試。如果問題持續存在,會發送警報給開發者。

推播通知中的安全性#

對於 iOS 或 Android App,使用 appKey 與 appSecret 來保護推播通知 API [6]。只有經過驗證或核可的客戶端才能透過我們的 API 發送推播通知。有興趣的讀者請參考參考資料 [6]。

監控佇列中的通知#

一個關鍵指標是佇列中的通知總數。如果該數字很大,表示 workers 處理通知事件的速度不夠快。為避免通知傳送的延遲,需要更多 workers。圖 12(來源 [7])顯示一個待處理佇列訊息的範例。

圖 12

事件追蹤#

通知指標,例如開啟率、點擊率與互動率,對於了解客戶行為很重要。分析服務實作事件追蹤。通常需要將通知系統與分析服務整合。圖 13 顯示可能為了分析目的而被追蹤的事件範例。

圖 13

更新後的設計#

把所有元件放在一起,圖 14 顯示更新後的通知系統設計。

圖 14

在此設計中,相較於先前的設計加入了許多新元件:

  • 通知伺服器配備了兩個更關鍵的功能:身份驗證與速率限制
  • 我們也加入重試機制以處理通知失敗。如果系統傳送通知失敗,它們會被放回訊息佇列,workers 將重試預定次數。
  • 此外,通知範本提供一致且高效的通知建立流程。
  • 最後,加入監控與追蹤系統以進行系統健康檢查與未來改進。

Step 4 - 總結#

通知不可或缺,因為它讓我們持續獲得重要資訊。它可能是 Netflix 上你最愛電影的推播通知、新產品折扣的電子郵件,或關於線上購物付款確認的訊息。

在本章中,我們描述了一個可擴展通知系統的設計,支援多種通知格式:推播通知、簡訊與電子郵件。我們採用訊息佇列來解耦系統元件。

除了高階設計之外,我們深入探討了更多元件與優化。

  • 可靠性:我們提出穩健的重試機制以將失敗率降到最低。
  • 安全性:使用 AppKey/appSecret 對來確保只有經驗證的客戶端能傳送通知。
  • 追蹤與監控:在通知流程的任一階段都實作這些,以擷取重要的統計數據。
  • 尊重使用者設定:使用者可選擇退訂通知。我們的系統在傳送通知前會先檢查使用者設定。
  • 速率限制:使用者會感謝對通知數量的頻率上限。

恭喜你看到這裡!現在給自己一個鼓勵。做得好!