TiDB 實戰:在知乎萬億量級數據下的挑戰

一、業務場景


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

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

二、架構設計

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

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

2.1 高可用

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

2.2 高性能

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

2.3 易擴展

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

2.4 已讀服務最終架構

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

三、關鍵組件

3.1 Proxy

3.2 Cache

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

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

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

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

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

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

3.3 Storage 

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

3.4 性能指標

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

四、All about TiDB 

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

4.1 MySQL to TiDB

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

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

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

這個過程中我們還做了一些開發的工作,比如 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)。

那麼 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,一切就變得有可能了。

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

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

五、總結

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

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

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

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/H1WT94OIobOhVv5xwQauAA