品牌名稱
知乎
企業規模
5001-10000人

TiDB 在知乎萬億量級業務數據下的實踐和挑戰

931次閱讀

作者:孫曉光,知乎搜索后端負責人,目前承擔知乎搜索后端架構設計以及工程團隊的管理工作。曾多年從事私有云相關產品開發工作關注云原生技術,TiKV 項目 Committer。

undefined

本文根據孫曉光老師在 TiDB TechDay 2019 北京站上的演講整理。

本次分享首先將從宏觀的角度介紹知乎已讀服務的業務場景中的挑戰、架構設計思路,然后將從微觀的角度介紹其中的關鍵組件的實現,最后分享在整個過程中 TiDB 幫助我們解決了什么樣的問題,以及 TiDB 是如何幫助我們將龐大的系統全面云化,并推進到一個非常理想的狀態的。

 

一、業務場景

 

知乎從問答起步,在過去的 8 年中逐步成長為一個大規模的綜合性知識內容平臺,目前,知乎上有多達 3000 萬個問題,共收獲了超過 1.3 億個回答,同時知乎還沉淀了數量眾多的文章、電子書以及其他付費內容,目前注冊用戶數是 2.2 億,這幾個數字還是蠻驚人的。我們有 1.3 億個回答,還有更多的專欄文章,所以如何高效的把用戶最感興趣的優質內容分發他們,就是非常重要的問題。

 

undefined

圖 1

 

知乎首頁是解決流量分發的一個關鍵的入口,而已讀服務想要幫助知乎首頁解決的問題是,如何在首頁中給用戶推薦感興趣的內容,同時避免給用戶推薦曾經看過的內容。已讀服務會將所有知乎站上用戶深入閱讀或快速掠過的內容記錄下來長期保存,并將這些數據應用于首頁推薦信息流和個性化推送的已讀過濾。圖 2 是一個典型的流程:

 

undefined

圖 2

 

當用戶打開知乎進入推薦頁的時候,系統向首頁服務發起請求拉取“用戶感興趣的新內容”,首頁根據用戶畫像,去多個召回隊列召回新的候選內容,這些召回的新內容中可能有部分是用戶曾經看到過的,所以在分發給用戶之前,首頁會先把這些內容發給已讀服務過濾,然后做進一步加工并最終返回給客戶端,其實這個業務流程是非常簡單的。

undefined

圖 3

 

這個業務第一個的特點是可用性要求非常高,因為首頁可能是知乎最重要的流量分發渠道。第二個特點是寫入量非常大,峰值每秒寫入 40k+ 條記錄,每日新增記錄近 30 億條。并且我們保存數據的時間比較長,按照現在產品設計需要保存三年。整個產品迭代到現在,已經保存了約一萬三千億條記錄,按照每月近一千億條的記錄增長速度,大概兩年之后,可能要膨脹到三萬億的數據規模。

undefined

圖 4

 

這個業務的查詢端要求也很高。首先,產品吞吐高。用戶在線上每次刷新首頁,至少要查一次,并且因為有多個召回源和并發的存在,查詢吞吐量還可能放大。峰值時間首頁每秒大概產生 3 萬次獨立的已讀查詢,每次查詢平均要查 400 個文檔,長尾部分大概 1000 個文檔,也就是說,整個系統峰值平均每秒大概處理 1200 萬份文檔的已讀查詢。在這樣一個吞吐量級下,要求的響應時間還比較嚴格,要求整個查詢響應時間(端到端超時)是 90ms,也就意味著最慢的長尾查詢都不能超過 90ms。還有一個特點是,它可以容忍 false positive,意味著有些內容被我們過濾掉了,但是系統仍然能為用戶召回足夠多的他們可能感興趣的內容,只要 false positive rate 被控制在可接受的范圍就可以了。

 

二、架構設計

 

由于知乎首頁的重要性,我們在設計這個系統的時候,考慮了三個設計目標:高可用、高性能、易擴展。首先,如果用戶打開知乎首頁刷到大量已經看過的內容,這肯定不可接受,所以對已讀服務的第一個要求是「高可用」。第二個要求是「性能高」,因為業務吞吐高,并且對響應時間要求也非常高。第三點是這個系統在不斷演進和發展,業務也在不斷的更新迭代,所以系統的「擴展性」非常重要,不能說今天能支撐,明天就支撐不下來了,這是沒法接受的。

接下來從這三個方面來介紹我們具體是如何設計系統架構的。

 

2.1 高可用

undefined

圖 5

 

當我們討論高可用的時候,也意味著我們已經意識到故障是無時無刻都在發生的,想讓系統做到高可用,首先就要有系統化的故障探測機制,檢測組件的健康狀況,然后設計好每一個組件的自愈機制,讓它們在故障發生之后可以自動恢復,無需人工干預。最后我們希望用一定的機制把這些故障所產生的變化隔離起來,讓業務側盡可能對故障的發生和恢復無感知。

 

2.2 高性能

undefined

圖 6

 

對常見的系統來說,越核心的組件往往狀態越重擴展的代價也越大,層層攔截快速降低需要深入到核心組件的請求量對提高性能是非常有效的手段。首先我們通過緩沖分 Slot 的方式來擴展集群所能緩沖的數據規模。接著進一步在 Slot 內通過多副本的方式提升單個 Slot 緩沖數據集的讀取吞吐,將大量的請求攔截在系統的緩沖層進行消化。如果請求不可避免的走到了最終的數據庫組件上,我們還可以利用效率較高的壓縮來繼續降低落到物理設備上的 I/O 壓力。

 

2.3 易擴展

undefined

圖 7

 

提升系統擴展性的關鍵在于減少有狀態組件的范圍。在路由和服務發現組件的幫助下,系統中的無狀態組件可以非常輕松的擴展擴容,所以通過擴大無狀態服務的范圍,收縮重狀態服務的比例,可以顯著的幫助我們提升整個系統的可擴展性。除此之外,如果我們能夠設計一些可以從外部系統恢復狀態的弱狀態服務,部分替代重狀態組件,這樣可以壓縮重狀態組件的比例。隨著弱狀態組件的擴大和重狀態組件的收縮,整個系統的可擴展性可以得到進一步的提升。

 

2.4 已讀服務最終架構

 

在高可用、高性能和易擴展的設計理念下,我們設計實現了已讀服務的架構,圖 8 是已讀服務的最終架構。

undefined

圖 8

 

首先,上層的客戶端 API 和 Proxy 是完全無狀態可隨時擴展的組件。最底層是存儲全部狀態數據的 TiDB,中間這些組件都是弱狀態的組件,主體是分層的 Redis 緩沖。除了 Redis 緩沖之外,我們還有一些其他外部組件配合 Redis 保證 Cache 的一致性,這里面的細節會在下一章詳述。

從整個系統來看,TiDB 這層自身已經擁有了高可用的能力,它是可以自愈的,系統中無狀態的組件非常容易擴展,而有狀態的組件中弱狀態的部分可以通過 TiDB 中保存的數據恢復,出現故障時也是可以自愈的。此外系統中還有一些組件負責維護緩沖一致性,但它們自身是沒有狀態的。所以在系統所有組件擁有自愈能力和全局故障監測的前提下,我們使用 Kubernetes 來管理整個系統,從而在機制上確保整個服務的高可用。

三、關鍵組件

3.1 Proxy

 

undefined

圖 9

 

Proxy 層是無狀態的,設計同常見的 Redis 代理相似,從實現角度看也非常簡單。首先我們會基于用戶緯度將緩沖拆分成若干 Slot,每個 Slot 里有多個 Cache 的副本,這些多副本一方面可以提升我們整個系統的可用性,另外一方面也可以分攤同一批數據的讀取壓力。這里面也有一個問題,就是 Cache 的副本一致性的如何保證?我們在這里選擇的是「會話一致性」,也就是一個用戶在一段時間內從同一個入口進來,就會綁定在這一個 Slot 里面的某個副本上,只要沒有發生故障,這個會話會維持在上面。

如果一個 Slot 內的某個副本發生故障,Proxy 首先挑這個 Slot 內的其他的副本繼續提供服務。更極端的情況下,比如這個 Slot 內所有副本都發生故障,Proxy 可以犧牲系統的性能,把請求打到另外一個完全不相干的一個 Slot 上,這個 Slot 上面沒有當前請求對應數據的緩存,而且拿到結果后也不會緩存相應的結果。我們付出這樣的性能代價獲得的收益是系統可用性變得更高,即便 Slot 里的所有的副本同時發生故障,依舊不影響系統的可用性。

 

3.2 Cache

 

對于緩沖來說,非常重要的一點就是如何提升緩沖利用率。 

第一點是如何用同樣的資源緩沖更大量的數據。在由「用戶」和「內容類型」和「內容」所組成的空間中,由于「用戶」維度和「內容」維度的基數非常高,都在數億級別,即使記錄數在萬億這樣的數量級下,數據在整個三維空間內的分布依然非常稀疏。如圖 10 左半部分所示。

 

undefined

圖 10

 

考慮到目前知乎站上沉淀的內容量級巨大,我們可以容忍 false positive 但依舊為用戶召回到足夠多可能會感興趣的內容。基于這樣的業務特點,我們將數據庫中存儲的原始數據轉化為更加致密的 BloomFilter 緩沖起來,這極大的降低了內存的消耗在相同的資源狀況下可以緩沖更多的數據,提高緩存的命中率。

提升緩存命中率的方式有很多種,除了前面提到的提升緩存數據密度增加可緩沖的數據量級之外,我們還可以通過避免不必要的緩存失效來進一步的提升緩存的效率。

undefined

圖 11

 

一方面我們將緩存設計為 write through cache 使用原地更新緩存的方式來避免 invalidate cache 操作,再配合數據變更訂閱我們可以在不失效緩沖的情況下確保同一份數據的多個緩沖副本能在很短的時間內達成最終一致。

另一方面得益于 read through 的設計,我們可以將對同一份數據的多個并發查詢請求轉化成一次 cache miss 加多次緩沖讀取(圖 11 右半部分),進一步提升緩存的命中率降低穿透到底層數據庫系統的壓力。

接下來再分享一些不單純和緩沖利用率相關的事情。眾所周知,緩沖特別怕冷,一旦冷了, 大量的請求瞬間穿透回數據庫,數據庫很大概率都會掛掉。在系統擴容或者迭代的情況下,往往需要加入新的緩沖節點,那么如何把新的緩沖節點熱起來呢?如果是類似擴容或者滾動升級這種可以控制速度的情況,我們可以控制開放流量的速度,讓新的緩沖節點熱起來,但當系統發生故障的時候,我們就希望這個節點非常快速的熱起來。 所以在我們這個系統和其他的緩沖系統不大一樣的是,當一個新節點啟動起來,Cache 是冷的,它會馬上從旁邊的 Peer 那邊 transfer 一份正在活躍的緩存狀態過來,這樣就可以非常快的速度熱起來,以一個熱身的狀態去提供線上的服務(如圖 12)。

 

undefined

圖 12

 

另外,我們可以設計分層的緩沖,每一層緩沖可以設計不同的策略,分別應對不同層面的問題,如圖 13 所示,可以通過 L1 和 L2 分別去解決空間層面的數據熱度問題和時間層面的熱度問題,通過多層的 Cache 可以逐層的降低穿透到下一層請求的數量,尤其是當我們發生跨數據中心部署時,對帶寬和時延要求非常高,如果有分層的設計,就可以在跨數據中心之間再放一層 Cache,減少在穿透到另外一個數據中心的請求數量。

 

undefined

圖 13

 

為了讓業務之間不互相影響并且針對不同業務的數據訪問特征選擇不同的緩沖策略,我們還進一步提供了 Cache 標簽隔離的機制來隔離離線寫入和多個不同的業務租戶的查詢。剛剛說的知乎已讀服務數據,在后期已經不只是給首頁提供服務了,還同時為個性化推送提供服務。個性化推送是一個典型的離線任務,在推送內容前去過濾一下用戶是否看過。雖然這兩個業務訪問的數據是一樣的,但是它們的訪問特征和熱點是完全不一樣的,相應的緩沖策略也不一樣的。于是我們在做分組隔離機制(如圖 14),緩沖節點以標簽的方式做隔離,不同的業務使用不同的緩沖節點,不同緩沖節點搭配不同的緩沖策略,達到更高的投入產出比,同時也能隔離各個不同的租戶,防止他們之間互相產生影響。

undefined

圖 14

 

3.3 Storage 

 

undefined

圖 15

 

存儲方面,我們最初用的是 MySQL,顯然這么大量的數據單機是搞不定的,所以我們使用了分庫分表 + MHA 機制來提升系統的性能并保障系統的高可用,在流量不太大的時候還能忍受,但是在當每月新增一千億數據的情況下,我們心里的不安與日俱增,所以一直在思考怎樣讓系統可持續發展、可維護,并且開始選擇替代方案。這時我們發現 TiDB 兼容了 MySQL,這對我們來說是非常好的一個特點,風險非常小,于是我們開始做遷移工作。遷移完成后,整個系統最弱的“擴展性”短板就被補齊了。

 

3.4 性能指標

 

undefined

圖 16

 

現在整個系統都是高可用的,隨時可以擴展,而且性能變得更好。圖 16 是前兩天我取出來的性能指標數據,目前已讀服務的流量已達每秒 4 萬行記錄寫入,3 萬獨立查詢和 1200 萬個文檔判讀,在這樣的壓力下已讀服務響應時間的 P99 和 P999 仍然穩定的維持在 25ms 和 50ms,其實平均時間是遠低于這個數據的。這個意義在于已讀服務對長尾部分非常敏感,響應時間要非常穩定,因為不能犧牲任何一位用戶的體驗,對一位用戶來說來說超時了就是超時了。

 

四、All about TiDB 

 

最后分享一下我們從 MySQL 遷移到 TiDB 的過程中遇到的困難、如何去解決的,以及 TiDB 3.0 發布以后我們在這個快速迭代的產品上,收獲了什么樣的紅利。

 

4.1 MySQL to TiDB

 

undefined

)

圖 17

 

現在其實整個 TiDB 的數據遷移的生態工具已經很完善,我們打開 TiDB DM 收集 MySQL 的增量 binlog 先存起來,接著用 TiDB Lightning 快速把歷史數據導入到 TiDB 中,當時應該是一萬一千億左右的記錄,導入總共用時四天。這個時間還是非常震撼的,因為如果用邏輯寫入的方式至少要花一個月。當然四天也不是不可縮短,那時我們的硬件資源不是特別充足,選了一批機器,一批數據導完了再導下一批,如果硬件資源夠的話,可以導入更快,也就是所謂“高投入高產出”,如果大家有更多的資源,那么應該可以達到更好的效果。在歷史數據全部導入完成之后,就需要開啟 TiDB DM 的增量同步機制,自動把剛才存下來的歷史增量數據和實時增量數據同步到 TiDB 中,并近實時的維持 TiDB 和 MySQL 數據的一致。

在遷移完成之后,我們就開始小流量的讀測試,剛上線的時候其實發現是有問題的,Latency 無法滿足要求,剛才介紹了這個業務對 Latency 特別敏感,稍微慢一點就會超時。這時 PingCAP 伙伴們和我們一起不停去調優、適配,解決 Latency 上的問題。圖 18 是我們總結的比較關鍵的經驗。

 

undefined

圖 18

 

第一,我們把對 Latency 敏感的部分 Query 布了一個獨立的 TiDB 隔離開,防止特別大的查詢在同一個 TiDB 上影響那些對 Latency 敏感的的 Query。第二,有些 Query 的執行計劃選擇不是特別理想,我們也做了一些 SQL Hint,幫助執行引擎選擇一個更加合理的執行計劃。除此之外,我們還做了一些更微觀的優化,比如說使用低精度的 TSO,還有包括復用 Prepared Statement 進一步減少網絡上的 roundtrip,最后達到了很好的效果。

 

undefined

圖 19

 

這個過程中我們還做了一些開發的工作,比如 binlog 之間的適配。因為這套系統是靠 binlog 變更下推來維持緩沖副本之間的一致性,所以 binlog 尤為重要。我們需要把原來 MySQL 的 binlog 改成 TiDB 的 binlog,但是過程中遇到了一些問題,因為 TiDB 作為一個數據庫產品,它的 binlog 要維持全局的有序性的排列,然而在我們之前的業務中由于分庫分表,我們不關心這個事情,所以我們做了些調整工作,把之前的 binlog 改成可以用 database 或者 table 來拆分的 binlog,減輕了全局有序的負擔,binlog 的吞吐也能滿足我們要求了。同時,PingCAP 伙伴們也做了很多 Drainer 上的優化,目前 Drainer 應該比一兩個月前的狀態好很多,不論是吞吐還是 Latency 都能滿足我們現在線上的要求。

最后一點經驗是關于資源評估,因為這一點可能是我們當時做得不是特別好的地方。最開始我們沒有特別仔細地想到底要多少資源才能支撐同樣的數據。最初用 MySQL 的時候,為了減少運維負擔和成本,我們選擇了“1 主 1 從”方式部署 ,而 TiDB 用的 Raft 協議要求至少三個副本,所以資源要做更大的準備,不能指望用同樣的資源來支撐同樣的業務,一定要提前準備好對應的機器資源。另外,我們的業務模式是一個非常大的聯合主鍵,這個聯合主鍵在 TiDB 上非聚簇索引,又會導致數據更加龐大,也需要對應準備出更多的機器資源。最后,因為 TiDB 是存儲與計算分離的架構,所以網絡環境一定要準備好。當這些資源準備好,最后的收益是非常明顯的。

 

4.2 TiDB 3.0

 

在知乎內部采用與已讀服務相同的技術架構我們還支撐了一套用于反作弊的風控類業務。與已讀服務極端的歷史數據規模不同,反作弊業務有著更加極端的寫入吞吐但只需在線查詢最近 48 小時入庫的數據(詳細對比見圖 20)。

 

undefined

圖 20

 

那么 TiDB 3.0 的發布為我們這兩個業務,尤其是為反作弊這個業務,帶來什么樣的可能呢?

首先我們來看看已讀服務。已讀服務寫讀吞吐也不算小,大概 40k+,TiDB 3.0 的 gRPC Batch Message 和多線程 Raft store,能在這件事情上起到很大的幫助。另外,Latency 這塊,我剛才提到了,就是我們寫了非常多 SQL Hint 保證 Query 選到最優的執行計劃,TiDB 3.0 有 Plan Management 之后,我們再遇到執行計劃相關的問題就無需調整代碼上線,直接利用 Plan Management 進行調整就可以生效了,這是一個非常好用的 feature。

剛才馬曉宇老師詳細介紹了 TiFlash,在 TiDB DevCon 2019 上第一次聽到這個產品的時候就覺得特別震撼,大家可以想象一下,一萬多億條的數據能挖掘出多少價值, 但是在以往這種高吞吐的寫入和龐大的全量數據規模用傳統的 ETL 方式是難以在可行的成本下將數據每日同步到 Hadoop 上進行分析的。而當我們有 TiFlash,一切就變得有可能了。

 

undefined

圖 21

 

再來看看反作弊業務,它的寫入更極端,這時 TiDB 3.0 的 Batch message 和多線程 Raft Store 兩個特性可以讓我們在更低的硬件配置情況下,達到之前同樣的效果。另外反作弊業務寫的記錄偏大,TiDB 3.0 中包含的新的存儲引擎 Titan,就是來解決這個問題的,我們從 TiDB 3.0.0- rc1 開始就在反作弊業務上將 TiDB 3.0 引入到了生產環境,并在 rc2 發布不久之后開啟了 Titan 存儲引擎,下圖右半部分可以看到 Titan 開啟前后的寫入/查詢 Latency 對比,當時我們看到這個圖的時候都非常非常震撼,這是一個質的變化。

undefined

圖 22

 

另外,我們也使用了 TiDB 3.0 中 Table Partition 這個特性。通過在時間維度拆分  Table Partition,可以控制查詢落到最近的 Partition 上,這對查詢的時效提升非常明顯。

 

五、總結

 

最后簡單總結一下我們開發這套系統以及在遷移到 TiDB 過程中的收獲和思考。

 

undefined

圖 23

 

首先開發任何系統前一定先要理解這個業務特點,對應設計更好的可持續支撐的方案,同時希望這個架構具有普適性,就像已讀服務的架構,除了支撐知乎首頁,還可以同時支持反作弊的業務。

另外,我們大量應用了開源軟件,不僅一直使用,還會參與一定程度的開發,在這個過程中我們也學到了很多東西。所以我們應該不僅以用戶的身份參與社區,甚至還可以為社區做更多貢獻,一起把 TiDB 做的更好、更強。

最后一點,我們業務系統的設計可能看上去有點過于復雜,但站在今天 Cloud Native 的時代角度,即便是業務系統,我們也希望它能像 Cloud Native 產品一樣,原生的支持高可用、高性能、易擴展,我們做業務系統也要以開放的心態去擁抱新技術,Cloud Native from Ground Up。