編者按:本文來自微信公眾號“InfoQ”(ID:infoqchina),作者李曉清、董澤光;36氪經授權發布。
消息推送作為移動 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 官方提供的方案,中國眾多的手機廠商在其定制的系統中也內置了推送功能,如小米、華為等。
應用級方案
第三方推送服務
鑒于 Android 平臺 C2DM 推送的不可用性,國內涌現出大量的第三方推送服務提供商,采用第三方推送服務的系統流程如下圖:
圖 1:消息推送流程
目前應用最為廣泛的第三方推送服務提供商包括個推、極光、友盟、小米、華為、BAT 等,絕大部分 APP 都會優先考慮采用第三方推送服務。
自建推送服務
第三方服務在開發成本和消息到達率上表現都不錯,但所有信息會經過第三方服務器,對于信息敏感類 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 無法控制??赡軙霈F這一問題:當 APP 在后臺或者手機鎖屏的情況下,如果服務端重發了消息,手機的通知欄將出現多條通知。
因此,考慮當 APP 在后臺時,針對 iOS 平臺的消息不再進行重發;只有當 APP 進入前臺,才重新進行重發。APP 的活動狀態通過第三方推送服務的 api 可以獲取到。
Android 平臺不存在該問題。
由于消息重發可能會造成客戶端收到重復消息,需要在客戶端進行消息去重。服務端為每一條消息分配了一個唯一 id,重發時唯一 id 不變??蛻舳诵枰4媸盏降拿恳粭l消息,在接收到新消息時首先根據唯一 id 判斷是否已經收到了這條消息,如收到則不響應??蛻舳吮4嫦⒖梢圆捎?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 的方式保證消息先落地然后再進行處理,進一步確保系統在異常過程中我們可以隨時恢復消息,保證不丟失。
通過質量保障、全方位多維度監控體系(基礎監控、錯誤日志監控、發送數據波動監控、進程監控等監控指標)保障系統在出現問題時實現秒級報警、及時處理保證了消息推送平臺的高穩定性。
寫在最后
本文介紹了一種基于第三方或自建推送服務、但又不強依賴特定推送服務的通用移動消息推送中間件平臺,可以實現安全、穩定、可靠的消息推送功能,并提供完善的數據統計,在實際應用中,可以結合郵件、短信、網站消息、用戶留言等打造成更加通用的企業消息平臺。