做者:孫曉光,知乎搜索後端負責人,目前承擔知乎搜索後端架構設計以及工程團隊的管理工做。曾多年從事私有云相關產品開發工做關注雲原生技術,TiKV 項目 Committer。
本文根據孫曉光老師在 TiDB TechDay 2019 北京站上的演講整理。數據庫
本次分享首先將從宏觀的角度介紹知乎已讀服務的業務場景中的挑戰、架構設計思路,而後將從微觀的角度介紹其中的關鍵組件的實現,最後分享在整個過程當中 TiDB 幫助咱們解決了什麼樣的問題,以及 TiDB 是如何幫助咱們將龐大的系統全面雲化,並推動到一個很是理想的狀態的。後端
知乎從問答起步,在過去的 8 年中逐步成長爲一個大規模的綜合性知識內容平臺,目前,知乎上有多達 3000 萬個問題,共收穫了超過 1.3 億個回答,同時知乎還沉澱了數量衆多的文章、電子書以及其餘付費內容,目前註冊用戶數是 2.2 億,這幾個數字仍是蠻驚人的。咱們有 1.3 億個回答,還有更多的專欄文章,因此如何高效的把用戶最感興趣的優質內容分發他們,就是很是重要的問題。緩存
<center>圖 1</center>網絡
知乎首頁是解決流量分發的一個關鍵的入口,而已讀服務想要幫助知乎首頁解決的問題是,如何在首頁中給用戶推薦感興趣的內容,同時避免給用戶推薦曾經看過的內容。已讀服務會將全部知乎站上用戶深刻閱讀或快速掠過的內容記錄下來長期保存,並將這些數據應用於首頁推薦信息流和個性化推送的已讀過濾。圖 2 是一個典型的流程:多線程
<center>圖 2</center>架構
當用戶打開知乎進入推薦頁的時候,系統向首頁服務發起請求拉取「用戶感興趣的新內容」,首頁根據用戶畫像,去多個召回隊列召回新的候選內容,這些召回的新內容中可能有部分是用戶曾經看到過的,因此在分發給用戶以前,首頁會先把這些內容發給已讀服務過濾,而後作進一步加工並最終返回給客戶端,其實這個業務流程是很是簡單的。併發
<center>圖 3</center>運維
這個業務第一個的特色是可用性要求很是高,由於首頁多是知乎最重要的流量分發渠道。第二個特色是寫入量很是大,峯值每秒寫入 40k+ 條記錄,每日新增記錄近 30 億條。而且咱們保存數據的時間比較長,按照如今產品設計須要保存三年。整個產品迭代到如今,已經保存了約一萬三千億條記錄,按照每個月近一千億條的記錄增加速度,大概兩年以後,可能要膨脹到三萬億的數據規模。工具
<center>圖 4</center>oop
這個業務的查詢端要求也很高。首先,產品吞吐高。用戶在線上每次刷新首頁,至少要查一次,而且由於有多個召回源和併發的存在,查詢吞吐量還可能放大。峯值時間首頁每秒大概產生 3 萬次獨立的已讀查詢,每次查詢平均要查 400 個文檔,長尾部分大概 1000 個文檔,也就是說,整個系統峯值平均每秒大概處理 1200 萬份文檔的已讀查詢。在這樣一個吞吐量級下,要求的響應時間還比較嚴格,要求整個查詢響應時間(端到端超時)是 90ms,也就意味着最慢的長尾查詢都不能超過 90ms。還有一個特色是,它能夠容忍 false positive,意味着有些內容被咱們過濾掉了,可是系統仍然能爲用戶召回足夠多的他們可能感興趣的內容,只要 false positive rate 被控制在可接受的範圍就能夠了。
因爲知乎首頁的重要性,咱們在設計這個系統的時候,考慮了三個設計目標:高可用、高性能、易擴展。首先,若是用戶打開知乎首頁刷到大量已經看過的內容,這確定不可接受,因此對已讀服務的第一個要求是「高可用」。第二個要求是「性能高」,由於業務吞吐高,而且對響應時間要求也很是高。第三點是這個系統在不斷演進和發展,業務也在不斷的更新迭代,因此係統的「擴展性」很是重要,不能說今天能支撐,明天就支撐不下來了,這是無法接受的。
接下來從這三個方面來介紹咱們具體是如何設計系統架構的。
<center>圖 5</center>
當咱們討論高可用的時候,也意味着咱們已經意識到故障是無時無刻都在發生的,想讓系統作到高可用,首先就要有系統化的故障探測機制,檢測組件的健康情況,而後設計好每個組件的自愈機制,讓它們在故障發生以後能夠自動恢復,無需人工干預。最後咱們但願用必定的機制把這些故障所產生的變化隔離起來,讓業務側儘量對故障的發生和恢復無感知。
<center>圖 6</center>
對常見的系統來講,越核心的組件每每狀態越重擴展的代價也越大,層層攔截快速下降須要深刻到核心組件的請求量對提升性能是很是有效的手段。首先咱們經過緩衝分 Slot 的方式來擴展集羣所能緩衝的數據規模。接着進一步在 Slot 內經過多副本的方式提高單個 Slot 緩衝數據集的讀取吞吐,將大量的請求攔截在系統的緩衝層進行消化。若是請求不可避免的走到了最終的數據庫組件上,咱們還能夠利用效率較高的壓縮來繼續下降落到物理設備上的 I/O 壓力。
<center>圖 7</center>
提高系統擴展性的關鍵在於減小有狀態組件的範圍。在路由和服務發現組件的幫助下,系統中的無狀態組件能夠很是輕鬆的擴展擴容,因此經過擴大無狀態服務的範圍,收縮重狀態服務的比例,能夠顯著的幫助咱們提高整個系統的可擴展性。除此以外,若是咱們可以設計一些能夠從外部系統恢復狀態的弱狀態服務,部分替代重狀態組件,這樣能夠壓縮重狀態組件的比例。隨着弱狀態組件的擴大和重狀態組件的收縮,整個系統的可擴展性能夠獲得進一步的提高。
在高可用、高性能和易擴展的設計理念下,咱們設計實現了已讀服務的架構,圖 8 是已讀服務的最終架構。
<center>圖 8</center>
首先,上層的客戶端 API 和 Proxy 是徹底無狀態可隨時擴展的組件。最底層是存儲所有狀態數據的 TiDB,中間這些組件都是弱狀態的組件,主體是分層的 Redis 緩衝。除了 Redis 緩衝以外,咱們還有一些其餘外部組件配合 Redis 保證 Cache 的一致性,這裏面的細節會在下一章詳述。
從整個系統來看,TiDB 這層自身已經擁有了高可用的能力,它是能夠自愈的,系統中無狀態的組件很是容易擴展,而有狀態的組件中弱狀態的部分能夠經過 TiDB 中保存的數據恢復,出現故障時也是能夠自愈的。此外系統中還有一些組件負責維護緩衝一致性,但它們自身是沒有狀態的。因此在系統全部組件擁有自愈能力和全局故障監測的前提下,咱們使用 Kubernetes 來管理整個系統,從而在機制上確保整個服務的高可用。
<center>圖 9</center>
Proxy 層是無狀態的,設計同常見的 Redis 代理類似,從實現角度看也很是簡單。首先咱們會基於用戶緯度將緩衝拆分紅若干 Slot,每一個 Slot 裏有多個 Cache 的副本,這些多副本一方面能夠提高咱們整個系統的可用性,另一方面也能夠分攤同一批數據的讀取壓力。這裏面也有一個問題,就是 Cache 的副本一致性的如何保證?咱們在這裏選擇的是「會話一致性」,也就是一個用戶在一段時間內從同一個入口進來,就會綁定在這一個 Slot 裏面的某個副本上,只要沒有發生故障,這個會話會維持在上面。
若是一個 Slot 內的某個副本發生故障,Proxy 首先挑這個 Slot 內的其餘的副本繼續提供服務。更極端的狀況下,好比這個 Slot 內全部副本都發生故障,Proxy 能夠犧牲系統的性能,把請求打到另一個徹底不相干的一個 Slot 上,這個 Slot 上面沒有當前請求對應數據的緩存,並且拿到結果後也不會緩存相應的結果。咱們付出這樣的性能代價得到的收益是系統可用性變得更高,即使 Slot 裏的全部的副本同時發生故障,依舊不影響系統的可用性。
對於緩衝來講,很是重要的一點就是如何提高緩衝利用率。
第一點是如何用一樣的資源緩衝更大量的數據。在由「用戶」和「內容類型」和「內容」所組成的空間中,因爲「用戶」維度和「內容」維度的基數很是高,都在數億級別,即便記錄數在萬億這樣的數量級下,數據在整個三維空間內的分佈依然很是稀疏。如圖 10 左半部分所示。
<center>圖 10</center>
考慮到目前知乎站上沉澱的內容量級巨大,咱們能夠容忍 false positive 但依舊爲用戶召回到足夠多可能會感興趣的內容。基於這樣的業務特色,咱們將數據庫中存儲的原始數據轉化爲更加緻密的 BloomFilter 緩衝起來,這極大的下降了內存的消耗在相同的資源情況下能夠緩衝更多的數據,提升緩存的命中率。
提高緩存命中率的方式有不少種,除了前面提到的提高緩存數據密度增長可緩衝的數據量級以外,咱們還能夠經過避免沒必要要的緩存失效來進一步的提高緩存的效率。
<center>圖 11</center>
一方面咱們將緩存設計爲 write through cache 使用原地更新緩存的方式來避免 invalidate cache 操做,再配合數據變動訂閱咱們能夠在不失效緩衝的狀況下確保同一份數據的多個緩衝副本能在很短的時間內達成最終一致。
另外一方面得益於 read through 的設計,咱們能夠將對同一份數據的多個併發查詢請求轉化成一次 cache miss 加屢次緩衝讀取(圖 11 右半部分),進一步提高緩存的命中率下降穿透到底層數據庫系統的壓力。
接下來再分享一些不單純和緩衝利用率相關的事情。衆所周知,緩衝特別怕冷,一旦冷了, 大量的請求瞬間穿透回數據庫,數據庫很大機率都會掛掉。在系統擴容或者迭代的狀況下,每每須要加入新的緩衝節點,那麼如何把新的緩衝節點熱起來呢?若是是相似擴容或者滾動升級這種能夠控制速度的狀況,咱們能夠控制開放流量的速度,讓新的緩衝節點熱起來,但當系統發生故障的時候,咱們就但願這個節點很是快速的熱起來。 因此在咱們這個系統和其餘的緩衝系統不大同樣的是,當一個新節點啓動起來,Cache 是冷的,它會立刻從旁邊的 Peer 那邊 transfer 一份正在活躍的緩存狀態過來,這樣就能夠很是快的速度熱起來,以一個熱身的狀態去提供線上的服務(如圖 12)。
<center>圖 12</center>
另外,咱們能夠設計分層的緩衝,每一層緩衝能夠設計不一樣的策略,分別應對不一樣層面的問題,如圖 13 所示,能夠經過 L1 和 L2 分別去解決空間層面的數據熱度問題和時間層面的熱度問題,經過多層的 Cache 能夠逐層的下降穿透到下一層請求的數量,尤爲是當咱們發生跨數據中心部署時,對帶寬和時延要求很是高,若是有分層的設計,就能夠在跨數據中心之間再放一層 Cache,減小在穿透到另一個數據中心的請求數量。
<center>圖 13</center>
爲了讓業務之間不互相影響而且針對不一樣業務的數據訪問特徵選擇不一樣的緩衝策略,咱們還進一步提供了 Cache 標籤隔離的機制來隔離離線寫入和多個不一樣的業務租戶的查詢。剛剛說的知乎已讀服務數據,在後期已經不僅是給首頁提供服務了,還同時爲個性化推送提供服務。個性化推送是一個典型的離線任務,在推送內容前去過濾一下用戶是否看過。雖然這兩個業務訪問的數據是同樣的,可是它們的訪問特徵和熱點是徹底不同的,相應的緩衝策略也不同的。因而咱們在作分組隔離機制(如圖 14),緩衝節點以標籤的方式作隔離,不一樣的業務使用不一樣的緩衝節點,不一樣緩衝節點搭配不一樣的緩衝策略,達到更高的投入產出比,同時也能隔離各個不一樣的租戶,防止他們之間互相產生影響。
<center>圖 14</center>
<center>圖 15</center>
存儲方面,咱們最初用的是 MySQL,顯然這麼大量的數據單機是搞不定的,因此咱們使用了分庫分表 + MHA 機制來提高系統的性能並保障系統的高可用,在流量不太大的時候還能忍受,可是在當每個月新增一千億數據的狀況下,咱們內心的不安與日俱增,因此一直在思考怎樣讓系統可持續發展、可維護,而且開始選擇替代方案。這時咱們發現 TiDB 兼容了 MySQL,這對咱們來講是很是好的一個特色,風險很是小,因而咱們開始作遷移工做。遷移完成後,整個系統最弱的「擴展性」短板就被補齊了。
<center>圖 16</center>
如今整個系統都是高可用的,隨時能夠擴展,並且性能變得更好。圖 16 是前兩天我取出來的性能指標數據,目前已讀服務的流量已達每秒 4 萬行記錄寫入, 3 萬獨立查詢和 1200 萬個文檔判讀,在這樣的壓力下已讀服務響應時間的 P99 和 P999 仍然穩定的維持在 25ms 和 50ms,其實平均時間是遠低於這個數據的。這個意義在於已讀服務對長尾部分很是敏感,響應時間要很是穩定,由於不能犧牲任何一位用戶的體驗,對一位用戶來講來講超時了就是超時了。
最後分享一下咱們從 MySQL 遷移到 TiDB 的過程當中遇到的困難、如何去解決的,以及 TiDB 3.0 發佈之後咱們在這個快速迭代的產品上,收穫了什麼樣的紅利。
<center>圖 17</center>
如今其實整個 TiDB 的數據遷移的生態工具已經很完善,咱們打開 TiDB DM 收集 MySQL 的增量 binlog 先存起來,接着用 TiDB Lightning 快速把歷史數據導入到 TiDB 中,當時應該是一萬一千億左右的記錄,導入總共用時四天。這個時間仍是很是震撼的,由於若是用邏輯寫入的方式至少要花一個月。固然四天也不是不可縮短,那時咱們的硬件資源不是特別充足,選了一批機器,一批數據導完了再導下一批,若是硬件資源夠的話,能夠導入更快,也就是所謂「高投入高產出」,若是你們有更多的資源,那麼應該能夠達到更好的效果。在歷史數據所有導入完成以後,就須要開啓 TiDB DM 的增量同步機制,自動把剛纔存下來的歷史增量數據和實時增量數據同步到 TiDB 中,並近實時的維持 TiDB 和 MySQL 數據的一致。
在遷移完成以後,咱們就開始小流量的讀測試,剛上線的時候其實發現是有問題的,Latency 沒法知足要求,剛纔介紹了這個業務對 Latency 特別敏感,稍微慢一點就會超時。這時 PingCAP 夥伴們和咱們一塊兒不停去調優、適配,解決 Latency 上的問題。圖 18 是咱們總結的比較關鍵的經驗。
<center>圖 18</center>
第一,咱們把對 Latency 敏感的部分 Query 布了一個獨立的 TiDB 隔離開,防止特別大的查詢在同一個 TiDB 上影響那些對 Latency 敏感的的 Query。第二,有些 Query 的執行計劃選擇不是特別理想,咱們也作了一些 SQL Hint,幫助執行引擎選擇一個更加合理的執行計劃。除此以外,咱們還作了一些更微觀的優化,好比說使用低精度的 TSO,還有包括複用 Prepared Statement 進一步減小網絡上的 roundtrip,最後達到了很好的效果。
<center>圖 19</center>
這個過程當中咱們還作了一些開發的工做,好比 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 是存儲與計算分離的架構,因此網絡環境必定要準備好。當這些資源準備好,最後的收益是很是明顯的。
在知乎內部採用與已讀服務相同的技術架構咱們還支撐了一套用於反做弊的風控類業務。與已讀服務極端的歷史數據規模不一樣,反做弊業務有着更加極端的寫入吞吐但只需在線查詢最近 48 小時入庫的數據(詳細對比見圖 20)。
<center>圖 20</center>
那麼 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,一切就變得有可能了。
<center>圖 21</center>
再來看看反做弊業務,它的寫入更極端,這時 TiDB 3.0 的 Batch message 和多線程 Raft Store 兩個特性可讓咱們在更低的硬件配置狀況下,達到以前一樣的效果。另外反做弊業務寫的記錄偏大,TiDB 3.0 中包含的新的存儲引擎 Titan,就是來解決這個問題的,咱們從 TiDB 3.0.0- rc1 開始就在反做弊業務上將 TiDB 3.0 引入到了生產環境,並在 rc2 發佈不久以後開啓了 Titan 存儲引擎,下圖右半部分能夠看到 Titan 開啓先後的寫入/查詢 Latency 對比,當時咱們看到這個圖的時候都很是很是震撼,這是一個質的變化。
<center>圖 22</center>
另外,咱們也使用了 TiDB 3.0 中 Table Partition 這個特性。經過在時間維度拆分 Table Partition,能夠控制查詢落到最近的 Partition 上,這對查詢的時效提高很是明顯。
最後簡單總結一下咱們開發這套系統以及在遷移到 TiDB 過程當中的收穫和思考。
<center>圖 23</center>
首先開發任何系統前必定先要理解這個業務特色,對應設計更好的可持續支撐的方案,同時但願這個架構具備普適性,就像已讀服務的架構,除了支撐知乎首頁,還能夠同時支持反做弊的業務。
另外,咱們大量應用了開源軟件,不只一直使用,還會參與必定程度的開發,在這個過程當中咱們也學到了不少東西。因此咱們應該不只以用戶的身份參與社區,甚至還能夠爲社區作更多貢獻,一塊兒把 TiDB 作的更好、更強。
最後一點,咱們業務系統的設計可能看上去有點過於複雜,但站在今天 Cloud Native 的時代角度,即使是業務系統,咱們也但願它能像 Cloud Native 產品同樣,原生的支持高可用、高性能、易擴展,咱們作業務系統也要以開放的心態去擁抱新技術,Cloud Native from Ground Up。
更多 TiDB 用戶實踐:https://pingcap.com/cases-cn/