天天看點

【精選實踐】TiDB 在喜馬拉雅推送系統中的實踐

作者:李乾坤,喜馬拉雅 Java 工程師。本文系 2019 年 12 月份上海 “​​TUG 走進喜馬拉雅​​”活動分享實錄。

項目背景

關于推送系統,舉個例子,比如說喜馬拉雅現在有直播,有的主播有幾百萬粉絲,那麼當他的直播開播以後,需要大量地推送出來,提醒所有的粉絲進入到直播間去觀看。是以後面那麼複雜的系統,一切目的都是為了這個推送能夠快速、準确地到達。

今天分享首先會介紹一下推送的基本原理。因為幾十分鐘的分享是沒有辦法完全說清楚推送原理的,是以後面主要講一下存儲相關的東西,傳統存儲方式的困難,基于 TiDB 存儲的優勢。最後分享一些關于 TiDB 的實踐。

推送業務的基本原理

​​

【精選實踐】TiDB 在喜馬拉雅推送系統中的實踐

​​

現在,先說一下基本原理,推送主要就是兩個過程:一個是綁定,一個是下發,先說下發。

先跟大家解釋一個基本的問題,喜馬不是直接給使用者裝置發推送的。為什麼呢?如果我不說的話,很多人可能會以為,比如我有一個裝置叫 xx,我訂閱的專輯更新了,通過推送系統直接給這個裝置去發一個消息。但實際上我們一般都是通過第三方服務商發送裝置通知的。為什麼?因為大家知道,首先 IOS 背景前一陣子殺背景非常狠,包括安卓很多用戶端的系統為了省電,殺背景也殺得非常狠。我們的系統沒辦法跟 APP 建立一個非常直接的通道,是以說我們是沒有辦法做這件事情。在國内,如果你想跟 IOS 系統發推送的話,隻有去走蘋果的官方推送系統。比如說我要給蘋果裝置發一個推送,那麼就是業務方把需求給到我這個推送系統,推送系統處理完之後,再發給蘋果,蘋果再發給蘋果的裝置。Google 也有這樣一個系統,但在國内沒辦法用,是以國内有很多第三方服務商去填補 Google 的官方推送服務,比如說小米,個推,還有極光。

回顧一下,我們要給蘋果裝置推,就發給蘋果官方的推送系統,官方的推送系統再發給裝置;我們要給安卓裝置推,我們公司和第三方合作,就把推送先發給第三方,然後第三方再發給安卓這樣一個過程。推送系統繞了一個推送服務的第三方,就帶來一個問題:比如業務方說,現在給我的裝置發一個通知,業務方隻知道我的 UID/deviceID,deviceID 是我們公司内部對一個裝置的辨別,我們所有的資料行為都是基于 UID/deviceID。那麼這時候,我把 UID 直接給了第三方,第三方肯定推不了,人家不認我們這個 UID,他們認什麼呢?他們認自己的 Token。他們自己的裝置也有自己的辨別,這就産生一個過程,就是推送系統業務方說,給裝置推一個通知,然後業務方把 UID 傳給推送系統,推送系統在這個時候做一個轉換,把 UID/deviceID 轉換為第三方能夠識别的 Token,然後第三方拿着這個 Token 裝置再做推送。有轉換的過程,就有綁定的問題,我們是怎麼解決的?我們的 APP 在啟用的時候,因為用戶端能拿到第三方的 Token,也能拿到我的 UID/deviceID,那麼用戶端在啟動的時候,同時把 UID 還有 Token 發給我的推送系統。

剛才說那個綁定和下發的流程,如果再細化一下,大概是這樣一個系統:綁定服務拿到 UID,Token 的綁定,會存在一個裝置管理存儲問題。然後呢,我們推送系統對外使用一個推送的接口給到業務方,比如專輯更新的時候,專輯訂閱的程式員拿到這個使用者資訊,使用接口直接就推就好了。這個接口之後接着消息隊列,消息隊列有個削峰作用,隊列收到之後,會經過一個轉化,就我剛才說的,把我們 UID 轉成第三方 token,當然中間還有一些過濾的一些服務,比如說頻控什麼的,比如說我們約定好一天不能給使用者發超過三個推送。

推送裡面有大量的技術難題,比如說直播,我們相關的人員統計下來說我們一個直播一般是一個小時,那麼這個時候你要給一個主播的所有的粉絲發開播提醒,必須半個小時内推到,是以說直播業務時效性會非常高。但有些業務,比如專輯更新,一個小時還是兩個小時之後告訴使用者專輯更新,不是特别的關鍵,是以說這個時候就有一個隔離的問題,這個是我們花了很大了精力但今天不是我們主要的話題。

原存儲方案

分庫分表

​​

【精選實踐】TiDB 在喜馬拉雅推送系統中的實踐

​​

今天主要講存儲這塊裝置表的處理,先說一下我們存儲的第一個痛點,就是我們要做分庫分表。喜馬拉雅現在有六億的使用者,将近三千萬的日活使用者,使用者不管他打開我們 APP 之後,是允許推送還是拒絕推送,我們都要把這個資料記錄下來,因為我們有一些服務,他想知道,使用者到底把推送打開了沒有。也就是公司每天新增幾十萬的使用者,我們這個表裡面每天新增幾十萬,一直累計到現在大概六億的使用者。當然正常的想法就是用 MySQL 去存,大家都知道,MySQL 單表去扛一個六億級别的資料量,反應就很感人了,上遊稍微有點壓力就受不了,很可能會挂。我們最直接的想法就是對 MySQL 進行分庫分表,内部的習慣是把它弄成一百張分表。每一個分表記錄了 UID,deviceID,token,Open _push(推送開關),還有一些其他的我們内部認為有意義的一些字段。

分庫分表有一個基本的問題是,要指定一個分表列,比如說一個使用者過來,我把它存到哪張表裡面。我們用的是 deviceID,也就是說一個新使用者過來,我們會根據它的 deviceID,先計算一下它到底在哪個分表裡。當然分庫分表現在業界方案很成熟,隻是麻煩一點。

第二個痛點就讓人非常難受了,我剛才說過,我們這邊是分庫分表之後是根據 deviceID 分庫分表,但是有的業務,它非得按 UID 去查,一旦 deviceID 分了表之後,我想判斷一下某個 UID 是否打開推送,隻能每張表都查一遍,一百張表要查一百次,這個就很難讓人接受了。我們還有個場景就是按照 Token 去更新裝置資料,因為使用者會一直不停的解除安裝、重裝,導緻 Token 會改變,老的 Token 會失效,為了解決這個問題,蘋果還有小米,他們會提供一個回調的服務,把那些失效的 Token 告訴我們,這時候我們要根據 Token 把那個裝置的 Open_push 改成 false。

​​

【精選實踐】TiDB 在喜馬拉雅推送系統中的實踐

​​

這就是分庫分表之後常見的困難,為了解決這個問題,最簡單最直接的想法,就是把 UID 到 deviceID,Token 到 deviceID 的映射關系再存儲一下。

MySQL 與 Pika 資料同步

大家可以發現我們資料都存了三份,每份都是一百個分表,其實非常浪費存儲空間的,處理性能也很感人。光這個還不算完,因為即便是我們分了一百張表,要 MySQL 直接去扛查詢性能還是不行。就像我剛才說的,我們直播要求 30 分鐘就把所有的粉絲都要推到,有的主播上百萬的粉絲,開播時一下子上百萬的請求過來,對性能要求非常高,拿 MySQL 直接去扛請求受不了。

那麼怎麼辦呢?當然還是直接上 Redis,在 MySQL 前面加個 Redis,把資料緩存一下,但 Redis 是基于記憶體的,容量就比較有限,扛不住六億。為了解決這個問題,我們用了 Pika,Pika 是 360 開源的一個基于磁盤的 Redis,大家可以認為它還是一個存儲,支援 Redis 協定,隻不過是基于磁盤的。容量問題解決了還不算完,推送不存在熱點,我們一般用 Redis,都是利用資料的局部性原理。但是我們有個業務是全局推送,每天早上要所有使用者全部推一遍,這個時候就沒有熱點了。不過好在,我們 Pika 基于磁盤容量也比較大,把所有的資料全部又存了一份。

​​

【精選實踐】TiDB 在喜馬拉雅推送系統中的實踐

​​

經過剛才鋪墊,大家可以發現,現在的存儲就已經很複雜了,首先資料要存三份,每份是一百張分表,然後 Pika 是三個執行個體,它的 KeyValue 也分别按照UID,deviceId,Token 又存了三份,多元多份。我們剛才說過 Pika,你可以認為是 MySQL 的一個從庫,日常的綁定請求改了 MySQL 之後,還得同步下Pika。我們自己寫了一個服務,消費 MySQL 的 Binlog 日志同步到 Pika,理論上應該是一緻的,但有的時候,發現同步的資料不太對,然後有一個全量同步的服務。

我們總結一下原來存儲方案的問題:

第一,就是業務複雜,新人要一個月的熟悉才能接手。我們推送今年換了兩波人,新過來一個人我問他,你什麼時候可以開始接需求?他說不行,你再給我一個月的時間。大家可以看到存儲是分庫分表的,Pika 是分庫分表的,中間還有全量增量的同步,一個新人接手要花一個月。

第二,使用者資料更新的時候,要更新三個表,理論上,要用事務去保證一緻的,但是因為請求量比較大,沒有用事務,存在資料不一緻的可能性。

第三,就是批量推送一千個 deviceID 的時候,為了優化使用了 Redis 的 Pipeline,代碼很複雜。

第四,MySQL Pika 同步的問題,兩者有的時候不一緻。我回答分析師疑問的時候,就不硬氣,因為 Binlog 量大的時候會有延遲,随便一想,就十幾個原因導緻使用者沒收到推送。營運問一次查一次,經常無疾而終,不硬氣。

第五,每日推送總量在十億量級,一次批處理的請求處理耗時是比較長。推送本質上是個資料處理系統,它跟 Web 請求的處理是有差異的。舉個例子,比如一個請求,耗時是一毫秒還是兩毫秒,使用者是無感覺的。但是推送是一個消息隊列,如果消息平均下來能從兩毫秒優化到一毫秒,意味着一次全局推的時間可以節省一半。我們在三四月份的時候,一次全局推要三四個小時,五六億使用者掃表處理一次,它是累加的,隻要稍微優化一下,整體的響應時間就可以很客觀的縮減。

新存儲方案 – TiDB

在今年的六七月份的時候開始去想新的存儲方案,推送有互相沖突的幾個特點:

首先,它的業務是有多種多樣的,有大量的 KV 查詢,根據 deviceID 查 Token就可以。

第二,有的時候隻需要安卓的 6.3 以上的版本才需要發推送,這需要關系型條件查詢。我們優化産品的時候,直覺上你可以換個更大的,比如 Hbase,但是它沒有辦法支援關系型查詢。我們需要關系型查詢,還希望它性能好一點,基于這樣一個考慮我們選用了 TiDB。

關于 TiDB,我簡單說一下,大家可以認為它是一個無限容量的 MySQL。它支援 MySQL 協定,如果有一個 TiDB 執行個體,還有一個本來通路 MySQL 的項目代碼,你隻要把 MySQL 的 IP 改成 TiDB 的 IP,就可以直接用。除非有些特别的語句,一般都沒有關系。其次,無限也不是說無限容量,容量不夠加個機器,支援橫向擴充。

TiDB 可以認為是個無限的 MySQL,首先分庫分表的事就不用了,我們就一個表就好了,多元查詢的事怎麼搞呢?我們 UID、deviceID、Token 都建了索引,就搞定了。綁定和查詢請求直接打到 TiDB 裡邊。因為隻有簡單的 SQL 通路,任何一個新手過來,看一看就知道這個是怎麼回事。

​​

【精選實踐】TiDB 在喜馬拉雅推送系統中的實踐

​​

然後說一下 TiDB 的優點。

第一,代碼的複雜性大大下降,一個人就可以維護。 TiDB的本質是什麼?它的分庫分表,多元索引,事務,副本,這些基本特性,下沉到資料庫層面,之前我們做的那麼多工作,相當于在業務層去解決這些問題。

第二,我們認為它的性能基本滿足要求,它底層是固态硬碟,四個九的響應時間大概是 70 毫秒,并且這個 70 毫秒通常也是寫操作,整體是滿足需求的。

應用實踐

全局分頁周遊

我們先不說TiDB,正常的 MySQL 怎麼分頁呢?大家最直接的想法就是用 Limit,從一頁開始,長度是一千。我們周遊的時候,通常還要加個條件,比如 iOS大于某個版本,這時 Limit 效率非常低。一般分頁如何優化呢?在周遊的時候給它一個起始的 ID,然後比如現在能查到最大的 ID 是—億,決定分頁是一千,相當于每頁起始的 ID 是直接可以算出來的。但是這個方式在 TiDB 上不成立了,因為 TiDB 索引不是 B+ tree ,于是我們幹脆周遊的時候,直接寫起始 ID、結尾 ID。假設你有一億,分頁是一千,那麼你每次起始 ID,直接可以算出來的。但是這樣有個缺點:你不能保證每一次分頁,剛好是一千條記錄,好在這個對我們業務影響不大。除此之外,即便是分頁,也不可能說一個線程慢慢分,通常你要用多線程,我們 MySQL 時代 100 個分表,幹脆一百個線程,每個線程管一個表。在 TiDB 時代,每次廣播的時候,先找到這個 ID 的最大值,計算每個線程 ID 的範圍。以前用 MySQL 一次全局推要三四小時,現在 TiDB 30 分鐘就可以完成。

線上和離線業務共用

線上和離線業務共用這個問題現在還沒有解決,我們隻是給大家分享一下這個問題。我們希望在支援 toC 請求的時候,能夠支援一下離線計算,什麼意思呢?比如說剛才前面有一個 tb_device 表,每時每刻它都在應付推送查詢,但這個時候,想去做一個資料庫的分析,聚合裝置資料的時候,相當于 toc 正查着,大資料系統也在查。我們曾經試過一次,直接就逾時了。大家可能會有疑問,為什麼 TiDB 不建一個從庫,基于從庫做 OLAP?這個其實是經濟上的考量,要是資料量小,沒必要放在 TiDB 上,能放在 TiDB 的業務資料量都很大,另搭一個成庫的成本很高。

然後我們說一下為什麼會互相影響,我們可以認為這邊是 KV 的協定,這邊是 MySQL 的協定,tidb server 在過程中起一個解釋/轉化的作用,所有的使用者請求最終去查詢 tikv server,每一個 region 三個副本,分為 Leader 和 Follower 兩個角色,Leader 扛峰值的請求。因為所有的請求都要過這個 Leader,就會互相影響。

好在,這個問題後續也會得到解決,我們跟 TiDB 官方溝通下來,他們會在一個比較新的版本裡面為副本再抽象一個角色,正常的請求走 Leader,被标記的請求(比如資料分析)走新的副本角色,這樣實作 toC和 toB 的請求不共用 Leader 節點,不互相影響。

未來的設想

繼續閱讀