如何構建一套高可用的移動消息推送平台?
作者|李曉清、董澤光
編輯|小智
消息推送作為移動 APP 運營中的一項關鍵技術,已經被越來越廣泛的運用。本文追溯了推送技術的發展歷史,剖析了其核心原理,並對推送服務的關鍵技術進行深入剖析,圍繞消息推送時產生的服務不穩定性,消息丟失、延遲,接入複雜性,統計缺失等問題,提供了一整套平台級的高可用消息推送解決方案。實踐中,藉助於該平台,不僅能提能顯著提高消息到達率,還能提高研發效率,並道出了移動開發基礎設施的平台化架構思路。
推送基礎
移動互聯網蓬勃發展的今天,大部分手機 APP 都提供了消息推送功能,如新聞客戶端的熱點新聞推薦,IM 工具的聊天消息提醒,電商產品促銷信息,企業應用的通知和審批流程等等。推送對於提高產品活躍度、提高功能模塊使用率、提升用戶粘性、提升用戶留存率起到了重要作用,作為 APP 運營中一個關鍵的免費渠道,對消息推送的合理運用能有效促進目標的實現。
推送最早誕生於 Email 中,用於提醒新的消息,而移動互聯網時代則更多的運用在了移動客戶端程序。要獲取伺服器的數據,通常有兩種方式:第一種是客戶端 PULL(拉)方式,即每隔一段時間去伺服器獲取是否有數據;第二種是服務端 PUSH(推)方式,伺服器在有數據的時候主動發給客戶端。
很顯然,PULL 方案優點是簡單但是實時性較差,我們也可以通過提高查詢頻率來提高實時性,但這又會造電量、流量的消耗過高,反之 PUSH 方案基於 TCP 長連接方式實現,消息實時性好,但是由於要保持 APP 客戶端和服務端的長連接心跳,也會帶來額外的電量和流量消耗。因此在整體架構設計中需要折中平衡,目前主流的推送實現方式都是基於 PUSH 的方案。
移動推送的三種實現方式
目前移動推送技術實現方式主要有以下三種:
輪詢方式(PULL)
客戶端和伺服器定期的建立連接,通過消息隊列等方式來查詢是否有新的消息,需要控制連接和查詢的頻率,頻率不能過慢或過快,過慢會導致部分消息更新不及時,過快會消耗更多的資源(流量、電量等),對用戶體驗有較大傷害。
簡訊推送方式(SMS PUSH)
通過簡訊發送推送消息,並在客戶端植入簡訊攔截模塊(主要針對 Android 平台),可以實現對簡訊進行攔截並提取其中的內容轉發給 App 應用處理,這個方案藉助於運營商的短消息,能夠保證最好的實時性和到達率,但此方案對於成本要求較高,開發者需要為每一條 SMS 支付費用。
長連接方式(PUSH)
移動 Push 推送基於 TCP 長連接實現, 客戶端主動和伺服器建立 TCP 長連接之後, 客戶端定期向伺服器發送心跳包用於保持連接, 有消息的時候, 伺服器直接通過這個已經建立好的 TCP 連接通知客戶端。儘管長連接也會造成一定的開銷,對於輪詢和 SMS 方案的硬傷來說,目前已經是最優的方式,而且通過良好的設計,可以將損耗降至最低。不過,隨著客戶端數量和消息並發量的上升,對於消息伺服器的性能和穩定性要求提出了非常大的考驗。因此,就難度而言,此方式代價最高。
推送解決方案
基於 TCP 長連接的方式是主流的推送方式,基於該推送方式逐步發展出系統級、應用級一系列的推送解決方案。
系統級方案
iOS 平台(APNs)
iOS 在系統層面與蘋果 APNs(Apple Push Notification service)伺服器建立連接,應用通過觀察者模式向 ioS 系統註冊關注的消息,系統收到 APNs Server 消息後轉發到相應的應用程序,整個過程很清晰,並且所有 APP 都共用同一個系統級的連接,減少了系統開銷,雖然 APNs 能無障礙的訪問,但實際使用過程中,發現延時和丟消息的情況偶有發生。
Android 平台(C2DM)
Android 的 C2DM(Android Cloud to Device Messaging)採取與 iOS 類似的機制,都是由系統層面來支持消息推送,但是由於 Google 的服務在國內不能穩定的訪問,此方案對於中國用戶來說基本是無法使用的。
除了 Google 官方提供的方案,中國眾多的手機廠商在其定製的系統中也內置了推送功能,如小米、華為等。
應用級方案
1. 第三方推送服務
鑒於 Android 平台 C2DM 推送的不可用性,國內湧現出大量的第三方推送服務提供商,採用第三方推送服務的系統流程如下圖:
圖 1:消息推送流程
目前應用最為廣泛的第三方推送服務提供商包括個推、極光、友盟、小米、華為、BAT 等,絕大部分 APP 都會優先考慮採用第三方推送服務。
2. 自建推送服務
第三方服務在開發成本和消息到達率上表現都不錯,但所有信息會經過第三方伺服器,對於信息敏感類 APP 而言,有必要考慮自建一套消息推送服務,能最大化保證安全,但對於自建推送服務,如果從零開始來做需要解決幾個難點:
第一,移動推送伺服器對 App 客戶端海量長連接的維護管理。第二,App 客戶端如何保證 Push Service 常駐,對於 Android 我們可以通過發現 push service 不存在可以定時拉起的方式。第三,通信協議的制定,我們可以採用開源的 XMPP 方式實現,也可以自定義協議,不管哪種方式我們都要保證消息傳送的到達率的準確性。第四,在移動互聯網網路環境下,經常出現弱網環境,特別是 2G、3G 等網路不穩定的情況下,如果保證消息在弱網環境下不重、不丟也是一個挑戰。
存在問題
無論是第三方推送服務,還是自建推送服務,在實際的使用過程中,發現都存在以下問題:
應用服務端與推送服務強耦合。當推送服務不可用時,造成整個業務系統無法推送,甚無法正常工作。
缺乏 ACK 機制。推送的過程是非同步的,從應用服務端發送到推送服務時,可以得知發送是否成功,但是從第三方推送服務下發到 APP 時,無法得知客戶端是否接收到。iOS 平台中,從推送服務發送到蘋果 APNs 服務時,同樣無法確定 APNs 是否收到。同時,第三方推送服務通常使用共享的推送通道,受其他推送方的影響,可能造成消息的延遲和丟失。
服務會被殺死。尤其在 Android 平台上,後台推送 service 會被各種主動或者被動原因 kill 掉,導致消息丟失。
缺乏消息的持久化。對於推送服務而言,消息推送是來一條推一條,無法追溯歷史消息和消息狀態。
缺乏重傳機制。整個推送過程涉及多個環節,當其中某個環節出現問題,造成客戶端接收不到推送的消息時,就導致消息丟失,再無法接收到。
客戶端接入邏輯複雜。每接入一個新的 APP,都要進行重複的接入工作,接入邏輯完全一致,代碼無法復用,需要在不同項目中拷貝。
客戶端與推送服務的 SDK 強耦合。客戶端使用推送服務的介面,而各推送服務提供的介面不統一,如果需要替換推送服務,那麼接入部分代碼需完全重寫。
缺乏數據監控和統計。每個應用每天推送了多少消息,成功到達 app 多少,失敗多少,目前均沒有統計。
解決之道
為了解決以上問題,我們考慮基於第三方消息推送服務構建一套移動消息推送中間件平台,該消息平台採用了低耦合的分層架構設計(如圖 2 所示),分為三層:接入層、傳輸層和應用層。其中接入層是業務方調用的入口,我們採用非同步消息隊列的方式提供了較高的業務系統發送消息的速度,並且具備了消息緩衝功能,即使高峰期的海量消息推送對整個平台衝擊較少,保護了推送系統;
傳輸層會從接入層接收消息並進行解析,對推送消息進行合法性檢查校驗,如果消息不合法直接丟棄,同時將合法的消息進行協議轉換並發送到對應的第三方推送平台;應用層主要是提供統一的 SDK 供業務使用,封裝適配第三方推送平台的 SDK 介面到統一的介面 SDK 中,這樣業務 APP 使用方只關注統一封裝的 SDK 即可實現業務消息的操作,而不需要考慮各種濾重、校驗等通用操作。主要功能包括:
屏蔽推送介面,實現業務與推送服務解耦,提供一套通用的客戶端 SDK,簡化客戶端接入。
實現多點接入,可同時接入多套推送服務,根據歷史推送成功率動態選擇最優推送路徑,當一條路徑失效可選擇備用路徑進行推送,保證消息推送萬無一失。
引入消息持久化機制,方便追溯和統計。
引入消息的 ACK 機制和重傳機制,提高消息的到達率。
實現數據監控和統計機制,提供相關數據的統計分析,和報警預警功能。
提供 web 管理後台,便於進行 APP 設置、推送設置、查看數據報表,提高系統維護的工作效率。
整個系統設計由三部分組成:移動推送平台、客戶端 SDK、應用管理界面(第三方推送服務和自建推送服務統稱為推送服務)。
圖 2:系統架構
移動推送平台提供統一的服務,對於應用層屏蔽推送服務介面,且實現推送服務可動態輪替。推送平台將接收到的消息持久化到資料庫中,方便進行消息推送失敗後的重發,以及後續數據的統計分析。
客戶端 SDK 對 App 提供統一的使用介面,屏蔽推送服務 SDK 使用細節,且實現多種推送 SDK 可替換,隱藏 SDK 複雜的接入過程,方便使用。
應用管理系統面向 App 開發人員,實現應用申請,推送服務配置,消息查詢與管理,數據統計與分析。
主要流程
消息推送涉及的主要模塊是消息推送平台和客戶端 SDK,主要流程如下圖所示:
圖 3:消息推送中間件核心流程
正常情況下,消息推送過程如下:
系統接收到業務方的推送請求後,首先進行許可權的驗證,這包括應用 appKey 的驗證、介面參數的驗證、黑名單驗證等。
驗證不通過,返回錯誤信息;驗證通過後,為此條消息分配一個唯一 id(uuid),將消息內容持久化到資料庫中,此時消息的狀態為待發送。
消息進入推送隊列中,將之後推送介面請求的響應返回給業務方。
推送隊列的消費者從隊列中取出待發送的消息,標記該條消息的狀態為發送中,然後調用第三方推送服務介面進行發送。
如果調用成功,那麼標記該消息的狀態為發送成功客戶端未收到。
客戶端 SDK 在收到推送後,回調服務端介面,發送收到推送的回執;服務端收到客戶端回執後,標記消息狀態為發送成功客戶端已收到。
對於推送過程中可能出現的異常情況,總結如下:
在調用第三方推送服務介面時,可能出現調用失敗的情況;此時需要標記消息的狀態為發送失敗,留待重發。
在調用第三方推送服務介面成功後、第三方推送服務在下發至客戶端的過程中,可能由於某種原因,造成客戶端無法收到消息;此時消息的狀態為發送成功客戶端未收到,對於這種狀態,需要重發。
客戶端在收到推送的消息後、向服務端發送 ACK 回執時,可能由於網路環境的問題,造成服務端沒有收到客戶端發送的回執,此時消息的狀態為發送成功客戶端未收到,對於這種狀態,需要重發。
消息在重發 N 次(N 次可配置)、仍然沒有進入發送成功客戶端已收到的狀態,那麼將不再進行自動重發;管理界面將提供手動重發消息的操作入口,如有需要,可以手動再進行重發。監控平台對於一直重複不成功的消息會報警通知操作人員,這樣操作人員可以及時通過手動方式處理。
根據消息發送流程,可以得到消息在生命周期中狀態的變遷如下圖:
圖 4:消息狀態機
重發機制
消息重發主要存在三種場景:系統啟動時,查詢所有的發送失敗或發送成功未收到客戶端回執的消息,載入到推送隊列重發;系統運行時,後台線程定時查詢需要重發的消息,進入推送隊列;手動觸發時,直接將消息加入推送隊列。
由於消息推送中間件服務通常要求高可用,為分布式部署,消息重發必須保證在單一節點執行,且保證只發送一次。需採用分布式鎖的方式,保證重發只發一次,主流實現方式有三種:
ZooKeeper:通過競爭創建臨時節點的方式獲取鎖。
Redis:Redlock 是 Redis 作者的提出了一種分布式鎖的演算法,基於 Redis 實現,該演算法實現了一種更安全、可靠的分布式鎖管理。
資料庫:如使用 MySQL 的 GET_LOCK 函數
對於每種鎖機制的特點本文不詳細介紹,根據實際應用需要任選一種即可。
由於 iOS 平台和 Android 平台的差異,消息重發需要考慮平台差異性。
使用第三方推送時,如果 iOS 應用在前台運行,那麼將通過第三方推送維護的長連接,以透傳的方式直接下發到 APP,稱為應用內消息;而當 APP 在後台時,則第三方推送將消息推送到 APNs,由 APNs 推送到 APP,稱為 APNs 通知。當通過 APNs 推送時,手機在收到消息後將在頂部的通知欄出現相關推送內容,這一行為是系統級別的,APP 無法控制。可能會出現這一問題:當 APP 在後台或者手機鎖屏的情況下,如果服務端重發了消息,手機的通知欄將出現多條通知。
因此,考慮當 APP 在後台時,針對 iOS 平台的消息不再進行重發;只有當 APP 進入前台,才重新進行重發。APP 的活動狀態通過第三方推送服務的 api 可以獲取到。
Android 平台不存在該問題。
由於消息重發可能會造成客戶端收到重複消息,需要在客戶端進行消息去重。服務端為每一條消息分配了一個唯一 id,重發時唯一 id 不變。客戶端需要保存收到的每一條消息,在接收到新消息時首先根據唯一 id 判斷是否已經收到了這條消息,如收到則不響應。客戶端保存消息可以採用 sqlite 資料庫。
安全和控制
客戶端 SDK 與服務端的通信過程使用 appKey 和 appSecret 進行許可權控制。appKey 是服務端為每個 app 分配的唯一標識,appSecret 是服務端為每個 app 分配的秘鑰。
客戶端 SDK 在請求服務端 HTTP 介面時,會將 appKey+appSecret 做一次簽名,將簽名值作為簽名 sign 參數,與其他請求參數(業務參數 +appKey)一同傳到服務端;服務端拿到請求參數後,也先用 appKey+appSecret 做一次簽名,比較和客戶端傳來的 sign 參數是否一致,從而完成許可權驗證過程。為了能夠實現靈活控制推送與否,可實現黑名單管理的功能。處於黑名單內的 app 客戶端不再進行消息的推送。黑名單控制的粒度到賬號級別,也可以根據實際業務需要進行分組管理。
在某些業務場景中,需要對消息進行過濾,分析,做出相應的處理甚至預警,藉助於消息推送平台,都能方便的實現。
SDK 設計
客戶端 SDK 是基於推送服務的 SDK 封裝實現,對外提供統一的使用介面。SDK 的使用者不再關注具體使用了哪些第三方推送、推送服務的接入細節。實現與推送服務的充分解耦,降低開發和使用成本。
由於 iOS 和 Android 平台的差異性,在客戶端 SDK 的封裝上存在差異,下面分別介紹兩個平台的 SDK 封裝方式。
iOS 平台
SDK 提供啟動和停止的方法;同時定義一個 protocol,包含 SDK 提供的介面。SDK 在收到消息或出現錯誤時將會回調 protocol 中的介面。
由於推送的接入涉及 AppDelegate 的生命周期方法,為避免 SDK 使用者關注這些繁瑣的細節,SDK 使用 Aspects 的方式,將推送時相應的處理函數 hook 到 AppDelegate 的生命周期方法上。
Android 平台
在 Android 中使用 Receiver 組件來接收收到的消息。一個基本的配置如下所示:
流程如下:當推送服務的 SDK 在接收到推送過來的消息後,將發送廣播,這個廣播的用 intent-filter 標識,當應用中的 Receiver 代碼註冊了這個 intent-filter,就可以接收到廣播,並進行後續處理。
系統管理
圖 5:後台管理示意圖
消息後台管理系統提供應用申請、應用服務配置、推送服務配置、消息查詢與管理等功能。
1、應用申請
填寫應用名、應用描述等信息後,生成該應用唯一的 appKey 和 appSecret。
2、應用服務配置
為應用選擇要使用的移動端通用服務,可供選擇的有推送、反饋、版本發布。
3、推送服務配置
為應用配置推送服務,可供選擇個推、極光等;以及推送時使用的優先順序順序。
4、消息查詢與管理
查看應用所發出的消息,包括消息所屬應用、所屬賬號、消息的狀態、最終發送成功的第三方渠道、消息的來源、發送者 ip 等信息
5、數據統計
通過分析 message 表中的各消息的狀態,可統計各應用消息的發送成功率和到達率,以及哪個第三方推送的更優,方便選擇。同時,提供每日、每周、每月推送消息量的統計,並提供統計圖表。
高可用、高性能、高穩定性
消息推送平台通過無狀態設計、統一存儲、冗餘部署方式保證了高可用,對應的狀態數據統一存儲到 MySQL、Redis 中保證各個無狀態實例共享數據。
對於消息的接收處理我們通過純非同步、動態多線程的方式提供了推送平台的高性能。同時對於非同步接收的消息我們通過 log append 的方式保證消息先落地然後再進行處理,進一步確保系統在異常過程中我們可以隨時恢復消息,保證不丟失。
通過質量保障、全方位多維度監控體系(基礎監控、錯誤日誌監控、發送數據波動監控、進程監控等監控指標)保障系統在出現問題時實現秒級報警、及時處理保證了消息推送平台的高穩定性。
寫在最後
本文介紹了一種基於第三方或自建推送服務、但又不強依賴特定推送服務的通用移動消息推送中間件平台,可以實現安全、穩定、可靠的消息推送功能,並提供完善的數據統計,在實際應用中,可以結合郵件、簡訊、網站消息、用戶留言等打造成更加通用的企業消息平台。
推薦一本好書
「作者寫書的口吻完全是教條主義,感覺高高在上。」
「作者不讓找 KOL 寫推薦語?那這書還怎麼賣?」
「作者個人履歷挺豐富的啊,為什麼不同意加作者介紹?十年了,我第一次遇到這情況」
「你確定這作者在 InfoQ 上受歡迎?」
這是一位出版社金牌編輯的原話,他所說的就是 InfoQ 出品的第一本書——《聊聊架構》。社區中並不缺少架構圖,而是缺少架構相關的基礎知識。我們花了近兩年的時間,打磨這本可能註定無法暢銷的技術書。不為別的,只希望為這個行業貢獻一點點力量,能夠引起一些思考也是好的,如果能夠幫助一些軟體工程師們獲得更好的工作效率和工作品質,就超出期望值了。
今日薦文


TAG:InfoQ |
※為什麼純文本按鈕會損害移動應用的可用性?
※高性能、高可用平台架構的演變過程
※全面!一文理解微服務高可用的常用手段
※拒絕空談 AI 設想!手把手教你構建實時、高可用的 AI 調度平台
※如何構建真實世界可用的 ML 模型?
※如何基於 Flink 構建高可用的實時計算平台?
※壞消息!iPhone手機迎來壞消息,如蘋果公司不妥協,或將無網可用
※究竟啥才是互聯網架構「高可用」
※保時捷儀錶報「混合動力電動模式不可用」故障
※在未來,能不能造出一種飛行器,即可輕易用來捕獲衛星,又可用來商業營運?
※動不動就抽筋,怕是肝不好了,可用這2道湯劑補補
※搭港鐵將可用二維碼外,還可用信用卡入閘?
※ofo再次復活?小黃車推新模式,不需交押金,可用戶還會選擇嗎
※電動汽車算什麼,電動坦克要來了:可用尿當燃料,能紅外隱身
※這個健身動作可用在激烈運動前的熱身,看看實用嗎?
※可摺疊的智能電水壺,全球可用,旅行必備
※機器人用噴氣動力引擎調整重心 未來可用於救災
※有哪些簡單的運動可用於瘦身?
※帶感測器的3D列印裝置竟然可用於測試水果新鮮度
※綿陽造:將來你的汽車可用激光清洗 想試試嗎?