# design pattern (notification) ## notification 種類 通知向用戶發出重要訊息提醒,例如新聞、產品更新、活動等等。引經在日常生活中是不可或缺的一部分,通知系統並不只有 mobile 的通知提醒而已,而是有三種格式。 * push notification * SMS * Email ![](https://hackmd.io/_uploads/Hyek7XC6h.png) ## notification design pattern 問題定義 構建一個每天發送數百萬條通知的可擴展系統並不是一件容易的事。它需要對通知生態系統有深入的了解。這邊簡單給大家看一下面試過程。 `主考官:`系統支持哪些類型的通知? `面試者:`推送通知、短信、電子郵件。 `主考官:`是實時系統嗎? `面試者:`就說它是一個軟實時系統吧。我們希望用戶盡快收到通知。但是,如果系統工作負載較高,輕微的延遲是可以接受的。 `主考官:`支持哪些設備? `面試者:`iOS 設備、Android 設備、PC/mobile。 `主考官:`什麼會觸發通知? `面試者:`通知可以由客戶端應用程序觸發。它們也可以在服務器端進行安排。 `主考官:`用戶可以選擇退出嗎? `面試者:`是的,選擇退出的用戶將不再收到通知。 `主考官:`每天發出多少條通知? `面試者:`1000萬條移動推送通知、100萬條短信、500萬封電子郵件。 `補充一下` : 這邊指的退出是可以是關閉通知,或是取消訂閱某一個頻道跟 email ## muti notifaction design pattern 這邊主要介紹,iOS 推送通知、Android 推送通知、短信和電子郵件。它的內容如下: • 不同類型的通知 • 聯繫信息收集流程 • 通知發送/接收流程 ### iOS push notification ![](https://hackmd.io/_uploads/SkF1c7CT3.png) 主要有三個部分完成 iOS push notification * Provider : 用來發送 push noticiaction request 到 APNS,可以是你的本地終端機,或是其他 microservices 服務,其中 request 包含。 **Device token** : 每個 user 的裝置 id。 **Payload** : notification 的 Payload ![](https://hackmd.io/_uploads/HJjvsmCa3.png) * APNS (Apple Push Notification Service) : 這是 APPLE 用來處理 propagate 的 remote service ,用來 push notification 到 IOS decives。 * iOS Device: client device 用來獲取 push noticiaction ### android push notification 在 android 上主要是透過 FCM 去完成這件事 ( firebase cloud message),而這邊可以搭配的 provider 目前筆者習慣是透過 `cloud functions` 搭配 `fire store`去做 `realtime database` 去觸發 `cloud message` 。 這邊補充下狐狸提問的可不可以用 `cloud run` 去執行這件事,本人覺得 `cloud run` 本身的定位不太適合放在這邊,雖然 `cloud run` 跟`cloud functions` 都是 Serverless,但 `cloud run` 主要是為了更好搭配 `GCP` 上面的服務例如 `Cloud Monitoring`、`Cloud Logging、Cloud Trace `和 `Error Reporting`等等同時 `cloud run` 是屬於 `container base` 應用,所以在 `cloud run` 會用到的環境都需要透過 `image` 去打包,但其實我們只是需要簡簡單單的 `serverless function`,用 `cloud run` 來做 web api 過程相對比較複查,同時延遲性或是穩定不一定有保證,有點大材小用,而且也不好搭配 firebase 做 real time database。 對於快速建置小批量的工作流程或單一 Function-based 的服務,這邊比較推薦用 `cloud functions` ![](https://hackmd.io/_uploads/r1s3fZkR3.png) ![](https://hackmd.io/_uploads/SyXL6e102.png) ### SMS 常見的 server 例如 : Twilio 、Nexmo ![](https://hackmd.io/_uploads/SyqQbZyC3.png) ### email 通常會搭配第三方工具例如 : `nodemailer` 但本人目前適用 `nextjs` 推薦的 sender server (Resend) : https://resend.com/docs/send-with-nodejs template (react email): https://react.email/docs/introduction ![](https://hackmd.io/_uploads/ry3GbW10h.png) 整體架構如下 ![](https://hackmd.io/_uploads/SJPM4-JRh.png) ## store user token 要發送通知,我們需要收集 decive token、電話號碼或電子郵件地址。當用戶安裝我們的應用程序或第一次註冊時,API服務器會收集用戶聯繫信息並將其存儲在數據庫中。 ![](https://hackmd.io/_uploads/S10qNZkRn.png) ![](https://hackmd.io/_uploads/ryMh4-kAh.png) ## 通知發送/接收流程 ### High-level design ![](https://hackmd.io/_uploads/SkOAH-k03.png) * Service 1 to N: 服務可以是微服務、cron 作業或觸發通知發送事件的分佈式系統。例如,計費服務會發送電子郵件提醒客戶到期付款,或者購物網站會通過短信告訴客戶他們的包裹將於明天送達。 * Notification system: 通知系統是發送/接收通知的核心。從簡單的事情開始,只使用一個通知服務器。它為服務 1 到 N 提供 API,並為第三方服務構建通知負載。 * Third-party services: 如上一節介紹的 notification services ,這邊要注意的是對於新市場或未來不可用,例如 fmc 不能在中國使用,所以改成 Jpush、PushY 等等。 * iOS, Android, SMS, Email: User 獲取裝置的地方. 這樣的設計模式就衍生出以下問題: * Single point of failure (SPOF) : 只要是單體是架構就會出現的問題。 * Hard to scale : notification systems 只在一台 services 中負責所有通知的訊息,對於要獨立 database 的擴展或是 cache 都是不太容易做到。 * Performance bottleneck :處理和發送通知會非常佔用系統資源,例如 email 一邊要處理 html 內容,還要等第三方服務 api 結果,在一個系統處理所有事情會造成系統過載狀況,尤其實在高峰時段,而衍生 SPOF 等問題。 ## improved • 將 database 和 cache 移出 notification services。 • 添加更多 notification servers 並設置自動水平縮放。 • 添加 message queue 並設置自動水平縮放,透過 retry error 減少 api 來回,同時解耦系統組件。 ![](https://hackmd.io/_uploads/Hy1moW1R3.png) **Service 1 to N:** 它們代表通過通知服務器提供的API發送通知的不同服務。 **Notification servers:** * 為服務提供API 以發送通知。這些 API 只能在內部訪問 或由經過驗證的客戶來防止垃圾郵件。 * 執行基本驗證以驗證電子郵件、電話號碼等。 * 查詢數據庫或緩存以獲取呈現通知所需的數據。 將通知數據放入消息隊列以進行並行處理。 例如: POST https://api.example.com/v/sms/send Request body ![](https://hackmd.io/_uploads/B1mqJfk0h.png) **消息對列** :他們刪除組件之間的依賴關係。當要發送大量通知時,消息隊列充當緩衝區。每種通知類型都分配有不同的消息隊列,因此第三方服務的中斷不會影響其他通知類型。 **Workers**:用於監聽 handle notification service event。 小結: 1. 服務調用通知服務器提供的 API 來發送通知。 2. 通知服務器從緩存或數據庫中獲取元數據,例如用戶信息、設備令牌和通知設置。 3. 將通知事件發送到相應的隊列進行處理。例如,iOS 推送通知事件被發送到 iOS PN 隊列。 4. Workers 從消息隊列中拉取通知事件。 5. 第三方服務向用戶設備發送通知。 ## Design deep dive * 可靠性。 * 其他組件和注意事項:通知模板、通知設置、速率限制、重試機制、推送通知的安全性、監控排隊通知和事件跟踪。 * 更新設計。 ### Reliability 可靠性 在分布式系統設計中一定會考量的問題點。 #### 我們要怎麼防止資料遺失? 我們可以透過 workders 中觸發 notification log 去記錄每次推波的時間點與使用者相關數據,此用意可以來對照持久話 db 中的資料是否有匹配。 ![](https://hackmd.io/_uploads/SyDE8GkC2.png) #### 收件人只會收到一次通知嗎? 理想情況是不會,但分布式特性可能會導致重複通知,也許是延遲或是資料不齊全的情形,為了繳少重複狀況發生,需要新增預防重複新增數據的機制,例如當第一次事件發送時隨機生成指定 event_id,當下次要再重新發送推波時會去比對 db 中是否已經存在的 event_id,以此來避免推波多次狀況。 ### Additional components and considerations 通知還有其他考量的點例如: * 模板重用 * 通知設置 * 事件跟踪 * 系統監控 * 速率限制 #### 模板重用 ( Notification template ) 通知系統每天都會發出數百條的通知內容,總不可能每次都重新生成模板,這時你可以考慮先 prebuild 格式化模板的通知,之後只需要自定義參數或是相關 link 內容就好,減少模板建制時間。 例如你可以寫一個 `serverless function` 去 build email template。 ```typescript import type { NextApiRequest, NextApiResponse } from 'next'; import { EmailTemplate } from '../../components/EmailTemplate'; import { Resend } from 'resend'; const resend = new Resend(process.env.RESEND_API_KEY); export default async (req: NextApiRequest, res: NextApiResponse) => { try { const data = await resend.emails.send({ from: 'Acme <onboarding@resend.dev>', to: ['delivered@resend.dev'], subject: 'Hello world', react: EmailTemplate({ firstName: 'John' }), }); res.status(200).json(data); } catch (error) { res.status(400).json(error); } }; ``` #### 通知設置 ( Notification setting ) 堆用戶來說每天都要接受非常多的通知內容,所以勢必需要針對推波進行分類,實際情況中我們在推波信息的前置作業是,需要先判斷用戶是否有訂閱某一個 channel 或是用戶是否取消訂閱,這些都是需要考慮的因素,如果 user 要訂閱每一個 channel (topic) 的話會去綁定 devicedtoken ,也就是下方的 `YOUR_REGISTRATION_TOKEN` 內容。 ```typescript // These registration tokens come from the client FCM SDKs. const registrationTokens = [ 'YOUR_REGISTRATION_TOKEN_1', // ... 'YOUR_REGISTRATION_TOKEN_n' ]; // Subscribe the devices corresponding to the registration tokens to the // topic. getMessaging().subscribeToTopic(registrationTokens, topic) .then((response) => { console.log('Successfully subscribed to topic:', response); }) .catch((error) => { console.log('Error subscribing to topic:', error); }); getMessaging().unsubscribeFromTopic(registrationTokens, topic) .then((response) => { console.log('Successfully unsubscribed from topic:', response); }) .catch((error) => { console.log('Error unsubscribing from topic:', error); }); ``` #### Rate limiting 之前讀書會有提到。 #### Retry mechanism 當地三中服務發送通知失敗,會將該通知重新放到訊息隊列中,也就是上面的 retry ,如果超過 retry 次數時就通知開發人員並發送警報。 #### Security in push notifications 對於 IOS 或是 android 用戶有提供 appKey 跟 appSecret 來保護推波 api 的安全因為會需要身份驗正。 #### Events tracking 透過 Monitor queued notifications 我們可以分析用戶行為,了解用戶參與度,甚至分析服務實現,與事件追蹤情況例如 `sentry`。 ![](https://hackmd.io/_uploads/SJ_jZX1R3.png) ![](https://hackmd.io/_uploads/Sy5oZ7kA2.png) #### finally design ![](https://hackmd.io/_uploads/SyMdMmyC3.png) #### 最後總結一下優化哪些功能: • 通知服務器配備了兩個更關鍵的功能:身份驗證和速率限制。 • 透過重試機制來處理通知失敗。如果系統無法發送通知,它們將被放回消息隊列中,開發者定義重識次數,都失敗發送警報。 • 通知模板提供一致且高效的通知創建過程。 • 添加了監控和跟踪系統,用於系統健康檢查和未來改進。 ### demo 時間 ![](https://hackmd.io/_uploads/BkAM-m1Rh.png) ![](https://hackmd.io/_uploads/rJim-XJCn.png)