喜馬拉雅億級使用者量的離線訊息推送系統架構設計實踐

語言: CN / TW / HK

本文由喜馬拉雅技術團隊李乾坤原創,原題《推送系統實踐》,感謝作者的無私分享。

1、引言

1.1 什麼是離線訊息推送

對於IM的開發者來說,離線訊息推送是再熟悉不過的需求了,比如下圖就是典型的IM離線訊息通知效果。

1.2 Andriod端離線推送真心不易

移動端離線訊息推送涉及的端無非就是兩個——iOS端和Andriod端,iOS端沒什麼好說的,APNs是唯一選項。

Andriod端比較奇葩(主要指國內的手機),為了實現離線推送,各種保活黑科技層出不窮,隨著保活難度的不斷升級,可以使用的保活手段也是越來越少,有興趣可以讀一讀我整理的下面這些文章,感受一下(文章是按時間順序,隨著Andriod系統保活難度的提升,不斷進階的)。

上面這幾篇只是我整理的這方面的文章中的一部分,特別注意這最後一篇《Android保活從入門到放棄:乖乖引導使用者加白名單吧(附7大機型加白示例)》。是的,當前Andriod系統對APP自已保活的容忍度幾乎為0,所以那些曾今的保活手段在新版本系統裡,幾乎統統都失效了。

自已做保活已經沒戲了,保離線訊息推送總歸是還得做。怎麼辦?按照現時的最佳實踐,那就是對接種手機廠商的ROOM級推送通道。具體我就不在這裡展開,有興趣的地可以詳讀《Android P正式版即將到來:後臺應用保活、訊息推送的真正噩夢》。

自已做保活、自建推送通道的時代(這裡當然指的是Andriod端啦),離線訊息推送這種系統的架構設計相對簡單,無非就是每臺終端計算出一個deviceID,服務端通過自建通道進行訊息透傳,就這麼點事。

而在自建通道死翹翹,只能依賴廠商推送通道的如今,小米華為魅族OPPOvivo(這只是主流的幾家)等等,手機型號太多,各家的推送API、設計規範各不相同(別跟我提什麼統一推送聯盟,那玩意兒我等他3年了——詳見《萬眾矚目的“統一推送聯盟”上場了),這也直接導致先前的離線訊息推送系統架構設計必須重新設計,以適應新時代的推送技術要求。

1.3 怎麼設計合理呢

那麼,針對不同廠商的ROOM級推送通道,我們的後臺推送架構到底該怎麼設計合理呢?

本文分享的離線訊息推送系統設計並非專門針對IM產品,但無論業務層的差別有多少,大致的技術思路上都是相通的,希望借喜馬拉雅的這篇分享能給正在設計大使用者量的離線訊息推送的你帶來些許啟發。

* 推薦閱讀:喜馬拉雅技術團隊分享的另一篇《長連線閘道器技術專題(五):喜馬拉雅自研億級API閘道器技術實踐》,有興趣也可以一併閱讀。

2、技術背景

首先介紹下在喜馬拉雅APP中推送系統的作用,如下圖就是一個新聞業務的推送/通知。

離線推送主要就是在使用者不開啟APP的時候有一個手段觸達使用者,保持APP的存在感,提高APP的日活。

我們目前主要用推送的業務包括:

  • 1)主播開播:公司有直播業務,主播在開直播的時候會給這個主播的所有粉絲髮一個推送開播提醒
  • 2)專輯更新:平臺上有非常多的專輯,專輯下面是一系列具體的聲音,比如一本兒小說是一個專輯,小說有很多章節,那麼當小說更新章節的時候給所有訂閱這個專輯的使用者發一個更新的提醒:
  • 3)個性化、新聞業務等。

既然想給一個使用者發離線推送,系統就要跟這個使用者裝置之間有一個聯絡的通道。

做過這個的都知道:自建推送通道需要App常駐後臺(就是引言裡提到的應用“保活”),而手機廠商因為省電等原因普遍採取“激進”的後臺程序管理策略,導致自建通道質量較差。目前通道一般是由“推送服務商”去維護,也就是說公司內的推送系統並不直接給使用者發推送(就是上節內容的這篇裡提到的情況:《Android P正式版即將到來:後臺應用保活、訊息推送的真正噩夢)。

這種情況下的離線推送流轉流程如下:

國內的幾大廠商(小米華為魅族OPPOvivo等)都有自己官方的推送通道,但是每一家介面都不一樣,所以一些廠商比如小米、個推提供整合介面。傳送時推送系統發給整合商,然後整合商根據具體的裝置,發給具體的廠商推送通道,最終發給使用者。

給裝置發推送的時候,必須說清楚你要發的是什麼內容:即title、message/body,還要指定給哪個裝置發推送。

我們以token來標識一個裝置, 在不同的場景下token的含義是不一樣的,公司內部一般用uid或者deviceId標識一個裝置,對於整合商、不同的廠商也有自己對裝置的唯一“編號”,所以公司內部的推送服務,要負責進行uid、deviceId到整合商token 的轉換。

3、整體架構設計

如上圖所示,推送系統整體上是一個基於佇列的流式處理系統。

上圖右側:是主鏈路,各個業務方通過推送介面給推送系統發推送,推送介面會把資料發到一個佇列,由轉換和過濾服務消費。轉換就是上文說的uid/deviceId到token的轉換,過濾下文專門講,轉換過濾處理後發給傳送模組,最終給到整合商介面。

App 啟動時:會向服務端傳送繫結請求,上報uid/deviceId與token的繫結關係。當解除安裝/重灌App等導致token失效時,整合商通過http回撥告知推送系統。各個元件都會通過kafka 傳送流水到公司的xstream 實時流處理叢集,聚合資料並落盤到mysql,最終由grafana提供各種報表展示。

4、業務過濾機制設計

各個業務方可以無腦給使用者發推送,但推送系統要有節制,因此要對業務訊息有選擇的過濾。

過濾機制的設計包括以下幾點(按支援的先後順序):

  • 1)使用者開關:App支援配置使用者開關,若使用者關閉了推送,則不向使用者裝置發推送;
  • 2)文案排重:一個使用者不能收到重複的文案,用於防止上游業務方傳送邏輯出錯;
  • 3)頻率控制:每一個業務對應一個msg_type,設定xx時間內最多發xx條推送;
  • 4)靜默時間:每天xx點到xx點不給使用者發推送,以免打擾使用者休息。
  • 5)分級管理:從使用者和訊息兩維度進行分級控制。

針對第5點,具體來說就是:

  • 1)每一個msg/msg_type有一個level,給重要/高level業務更多傳送機會;
  • 2)當用戶一天收到xx條推送時,不是重要的訊息就不再發給這些使用者。

5、分庫分表下的多維查詢問題

很多時候,設計都是基於理論和經驗,但實操時,總會遇到各種具體的問題。

喜馬拉雅現在已經有6億+使用者,對應的推送系統的裝置表(記錄uid/deviceId到token的對映)也有類似的量級,所以對裝置表進行了分庫分表,以 deviceId 為分表列。

但實際上:經常有根據 uid/token 的查詢需求,因此還需要建立以 uid/token 到 deviceId 的對映關係。因為uid 查詢的場景也很頻繁,因此uid副表也擁有和主表同樣的欄位。

因為每天會進行一兩次全域性推,且針對沉默使用者(即不常使用APP的使用者)也有專門的推送,儲存方面實際上不存在“熱點”,雖然使用了快取,但作用很有限,且佔用空間巨大。

多分表以及快取導致資料存在三四個副本,不同邏輯使用不同副本,經常出現不一致問題(追求一致則影響效能), 查詢程式碼非常複雜且效能較低。

最終我們選擇了將裝置資料儲存在tidb上,在效能夠用的前提下,大大簡化了程式碼。

6、特殊業務的時效性問題

6.1 基本概念

推送系統是基於佇列的,“先到先推”。大部分業務不要求很高的實時性,但直播業務要求半個小時送達,新聞業務更是“慾求不滿”,越快越好。

若進行新聞推送時:佇列中有巨量的“專輯更新”推送等待處理,則專輯更新業務會嚴重干擾新聞業務的送達。

6.2 這是隔離問題?

一開始我們認為這是一個隔離問題:比如10個消費節點,3個專門負責高時效性業務、7個節點負責一般業務。當時佇列用的是rabbitmq,為此改造了 spring-rabbit 支援根據msytype將訊息路由到特定節點。

該方案有以下缺點:

  • 1)總有一些機器很忙的時候,另一些機器在“袖手旁觀”;
  • 2)新增業務時,需要額外配置msgType到消費節點的對映關係,維護成本較高;
  • 3)rabbitmq基於記憶體實現,推送瞬時高峰時佔用記憶體較大,進而引發rabbitmq 不穩定。

6.3 其實是個優先順序問題

後來我們覺察到這是一個優先順序問題:高優先順序業務/訊息可以插隊,於是封裝kafka支援優先順序,比較好的解決了隔離性方案帶來的問題。具體實現是建立多個topic,一個topic代表一個優先順序,封裝kafka主要是封裝消費端的邏輯(即構造一個PriorityConsumer)。

備註:為描述簡單,本文使用 consumer.poll(num) 來描述使用 consumer 拉取 num 個訊息,與真實 kafka api 不一致,請知悉。

PriorityConsumer實現有三種方案,以下分別闡述。

1)poll到記憶體後重新排序:java 有現成的基於記憶體的優先順序佇列PriorityQueue 或PriorityBlockingQueue,kafka consumer 正常消費,並將poll 到的資料重新push到優先順序佇列。

  • 1.1)如果使用有界佇列,佇列打滿後,後面的訊息優先順序再高也put 不進去,失去“插隊”效果;
  • 1.2)如果使用無界佇列,本來應堆在kafka上的訊息都會堆到記憶體裡,OOM的風險很大。

2)先拉取高優先順序topic的資料:只要有就一直消費,直到沒有資料再消費低一級topic。消費低一級topic的過程中,如果發現有高一級topic訊息到來,則轉向消費高優先順序訊息。

該方案實現較為複雜,且在晚高峰等推送密集的時間段,可能會導致低優先順序業務完全失去推送機會。

3)優先順序從高到低,迴圈拉取資料:

一次迴圈的邏輯為:

consumer-1.poll(topic1-num);

cosumer-i.poll(topic-i-num);

consumer-max.priority.poll(topic-max.priority-num)

如果topic1-num=topic-i-num=topic-max.priority-num,則該方案是沒有優先順序效果的。topic1-num 可以視為權重,我們約定:topic-高-num=2 * topic-低-num,同一時刻所有topic 都會被消費,通過一次消費數量的多少來變相實現“插隊效果”。具體細節上還借鑑了“滑動視窗”策略來優化某個優先順序的topic 長期沒有訊息時總的消費效能。

從中我們可以看到,時效問題先是被理解為一個隔離問題,後被視為優先順序問題,最終轉化為了一個權重問題。

7、過濾機制的儲存和效能問題

在我們的架構中,影響推送傳送速度的主要就是tidb查詢和過濾邏輯,過濾機制又分為儲存和效能兩個問題。

這裡我們以xx業務頻控限制“一個小時最多傳送一條”為例來進行分析。

第一版實現時:redis kv 結構為 <deviceId_msgtype,已傳送推送數量>

頻控實現邏輯為:

  • 1)傳送時,incr key,傳送次數加1;
  • 2)如果超限(incr命令返回值>傳送次數上限),則不推送;
  • 3)若未超限且返回值為1,說明在msgtype頻控週期內第一次向該deviceId發訊息,需expire key設定過期時間(等於頻控週期)。

上述方案有以下缺點:

  • 1)目前公司有60+推送業務, 6億+ deviceId,一共6億*60個key ,佔用空間巨大;
  • 2)很多時候,處理一個deviceId需要2條指令:incr+expire。

為此,我們的解決方法是:

  • 1)使用pika(基於磁碟的redis)替換redis,磁碟空間可以滿足儲存需求;
  • 2)委託系統架構組擴充了redis協議,支援新結構ehash。

ehash基於redis hash修改,是一個兩級map <key,field,value>,除了key 可以設定有效期外,field也可以支援有效期,且支援有條件的設定有效期。

頻控資料的儲存結構由<deviceId_msgtype,value>變為 <deviceId,msgtype,value>,這樣對於多個msgtype,deviceId只存一次,節省了佔用空間。

incr 和 expire 合併為1條指令:incr(key,filed,expire),減少了一次網路通訊:

  • 1)當field未設定有效期時,則為其設定有效期;
  • 2)當field還未過期時,則忽略有效期引數。

因為推送系統重度使用 incr 指令,可以視為一條寫指令,大部分場景還用了pipeline 來實現批量寫的效果,我們委託系統架構組小夥伴專門優化了pika 的寫入效能,支援“寫模式”(優化了寫場景下的相關引數),qps達到10w以上。

ehash結構在流水記錄時也發揮了重要作用,比如<deviceId,msgId,100001002>,其中 100001002 是我們約定的一個數據格式示例值,前中後三個部分(每個部分佔3位)分別表示了某個訊息(msgId)針對deviceId的傳送、接收和點選詳情,比如頭3位“100”表示因傳送時處於靜默時間段所以傳送失敗。

附錄:更多訊息推送技術文章

iOS的推送服務APNs詳解:設計思路、技術原理及缺陷等

信鴿團隊原創:一起走過 iOS10 上訊息推送(APNS)的坑

Android端訊息推送總結:實現原理、心跳保活、遇到的問題等

掃盲貼:認識MQTT通訊協議

一個基於MQTT通訊協議的完整Android推送Demo

IBM技術經理訪談:MQTT協議的制定歷程、發展現狀等

求教android訊息推送:GCM、XMPP、MQTT三種方案的優劣

移動端實時訊息推送技術淺析

掃盲貼:淺談iOS和Android後臺實時訊息推送的原理和區別

絕對乾貨:基於Netty實現海量接入的推送服務技術要點

移動端IM實踐:谷歌訊息推送服務(GCM)研究(來自微信)

為何微信、QQ這樣的IM工具不使用GCM服務推送訊息?

極光推送系統大規模高併發架構的技術實踐分享

從HTTP到MQTT:一個基於位置服務的APP資料通訊實踐概述

魅族2500萬長連線的實時訊息推送架構的技術實踐分享

專訪魅族架構師:海量長連線的實時訊息推送系統的心得體會

深入的聊聊Android訊息推送這件小事

基於WebSocket實現Hybrid移動應用的訊息推送實踐(含程式碼示例)

一個基於長連線的安全可擴充套件的訂閱/推送服務實現思路

實踐分享:如何構建一套高可用的移動端訊息推送系統?

Go語言構建千萬級線上的高併發訊息推送系統實踐(來自360公司)

騰訊信鴿技術分享:百億級實時訊息推送的實戰經驗

百萬線上的美拍直播彈幕系統的實時推送技術實踐之路

京東京麥商家開放平臺的訊息推送架構演進之路

瞭解iOS訊息推送一文就夠:史上最全iOS Push技術詳解

基於APNs最新HTTP/2介面實現iOS的高效能訊息推送(服務端篇)

解密“達達-京東到家”的訂單即時派發技術原理和實踐

技術乾貨:從零開始,教你設計一個百萬級的訊息推送系統

長連線閘道器技術專題(四):愛奇藝WebSocket實時推送閘道器技術實踐

喜馬拉雅億級使用者量的離線訊息推送系統架構設計實踐

>> 更多同類文章 ……

本文已同步釋出於“即時通訊技術圈”公眾號。

▲ 本文在公眾號上的連結是:點此進入。同步釋出連結是:http://www.52im.net/thread-3621-1-1.html

「其他文章」