招商銀行2面:如何實現一個通知系統?
在實際工作中,我們經常會用到通知系統,比如,用戶完成在線購買后,需要發送訂單確認郵件、支付處理成功的短信以及包裹發貨的推送通知。那么,什么是通知系統?如何設計一個通知系統?這篇文章,我們來聊一聊!
需求收集
在設計之前,我們先來詳細了解下通知系統的需求,本文從功能需求和非功能需求兩個方面來介紹。
1.功能需求
- 通知類型:例如消息通知、警告通知、活動通知等。
- 用戶群體:需要通知的用戶群體是誰,是否有分組。
- 通知渠道:例如郵件、短信、推送通知、應用內通知等。
- 通知頻率:通知的發送頻率和限流策略。
- 優先級:不同通知的優先級管理。
- 用戶偏好:用戶是否可以自定義接收通知的偏好。
- 重試機制:處理通知發送失敗的情況,必要時重試(如短信或電子郵件發送失敗)。
2.非功能需求
- 可擴展性:系統應能夠每分鐘處理數百萬條通知,支持數百萬并發用戶。
- 高可用性:確保最小的停機時間,即使在故障情況下也能發送通知。
- 可靠性:保證至少一次的通知傳遞,對于某些使用場景可能需要保證只有一次傳遞。
- 低延遲:通知應盡快發送,以確保及時交付。
容量預估
在深入設計之前,讓我們先估算下系統規模以更好地做出設計決策。假設系統服務于 1000萬日活用戶,每個用戶平均每天接收 5條通知。
1.峰值負載
假設在峰值時間內(如秒殺期間)1分鐘內發送 100萬條通知,這意味著系統應能夠處理:
- 每天的通知數量:10,000,000 x 5 = 50,000,000條通知
- 峰值每秒通知數量:1,000,000 / 60 = ~17,000條通知/秒
2.存儲需求
假設每條通知的數據量大小是 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": "user1@example.com"
},
"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:監控和發送確認
每個渠道處理器等待來自外部提供商的確認:
- 成功:消息已發送。
- 失敗:消息發送失敗(如網絡問題、無效地址)。
渠道處理器將每條通知的狀態記錄在通知日志表中,以供將來參考、審核和報告。
關鍵問題和瓶頸
1.故障和重試
如果通知發送由于臨時問題(如第三方提供商停機)而失敗,渠道處理器將嘗試重發通知。
- 通常使用指數退避策略,每次重試的延遲時間逐漸增加。
- 如果通知在設定次數的重試后仍未發送成功,則將其移動到死信隊列(DLQ)以進一步處理。
- 管理員可以手動審核和重新處理死信隊列中的消息。
2.可擴展性
(1) 水平擴展
系統應設計為水平擴展,意味著組件可以通過增加實例來應對負載增加。
- 通知服務:隨著請求量的增加,可以部署更多實例來管理增加的通知請求量。
- 通知隊列:分布式隊列系統(如Kafka或RabbitMQ)天然具有可擴展性,可以通過將隊列分布在多個節點上來處理更大的工作量。
- 渠道處理器:每個處理器(電子郵件、短信等)應水平擴展以處理大量通知。
(2) 分片和分區
為了高效處理大量數據,特別是用戶數據和通知日志,分片和分區將負載分布在多個數據庫或地理區域:
- 基于用戶的分片:根據地理位置或用戶ID將用戶分布在不同的數據庫或區域,以平衡負載。
- 基于時間的分區:將通知日志組織成基于時間的分區(如每日或每月),以提高查詢性能并管理大量歷史數據。
(3) 緩存
使用Redis或Memcached等解決方案實現緩存,以存儲頻繁訪問的數據,如用戶偏好。
緩存減少數據庫負載,并通過避免重復的數據庫查詢來提高實時通知的響應時間。
3.可靠性
為了高可用性,數據(如用戶偏好、日志)應在多個數據中心或區域之間復制。這確保即使一個區域故障,數據在其他地方仍然可用。
多AZ復制:在多個可用區存儲數據,以提供冗余。
使用負載均衡器將傳入流量均勻分布在通知服務的各個實例之間,確保沒有單個實例成為瓶頸。
4.監控和日志記錄
為了確保系統在大規模下的平穩運行,系統應具備:
- 集中式日志記錄:使用ELK Stack或Prometheus/Grafana等工具收集各種組件的日志并監控系統健康。
- 警報:設置警報以監控故障(如通知發送失敗率超過閾值)。
- 指標:跟蹤每個渠道的成功率、失敗率、發送延遲和吞吐量等指標。
5.安全性
對所有傳入通知服務的請求實施強認證(如OAuth 2.0)。使用基于角色的訪問控制(RBAC)限制對關鍵服務的訪問。
通過在API網關上實施速率限制保護服務免受濫用,防止DoS攻擊。
6.歸檔舊數據
由于通知系統隨著時間的推移會處理大量數據,實施歸檔舊數據的策略非常重要。
歸檔涉及將過時或不常訪問的數據(如舊的發送日志、通知內容和用戶歷史記錄)從主存儲移動到成本較低、長期存儲解決方案。
這樣可以減少主存儲的負載并提高系統的整體性能。
總結
這篇文章,我們從需求分析出發,再到宏觀層面的設計,最后到詳細的設計,通過本文詳細地分析了,我們不僅能夠學到如何設計一個可擴展的通知服務,同時我們還能通過通知服務的設計更好去理解系統設計的思路。