搜尋線上服務的儲存計算分離

alizhen發表於2018-11-30

背景

隨著網路和儲存硬體向著高吞吐低延遲的方向不斷髮展,儲存計算分離成為了集團的一個重要技術方向,在節約成本、簡化運維、提高混布能力有著重要的作用:

  1. 無需或者少量分發索引,使線上服務可以快速遷移,從而提升容災效率。
  2. 對於索引資料較大,訪問集中的業務,使用儲存計算分離後,可以突破單機記憶體和磁碟空間限制、並釋放大量記憶體。
  3. 將線上服務的 IO 隔離拆到專有的儲存叢集,降低了線上服務排程維度。
  4. 計算節點不再需要考慮儲存容量與記憶體容量的比例。計算節點與儲存節點使用不同的伺服器硬體,並獨立地進行定製和發展。
  5. 多個節點上的儲存資源能夠形成單一的儲存池,這能降低儲存空間碎化、節點間負載不均衡和空間浪費的風險,儲存容量和系統吞吐量也能容易地進行水平擴充套件。
  6. 資料與計算分離,使線上服務的全圖化、運算元化更易實現。

現狀

儲存計算分離前架構.png

在管控系統的統一管理下,先是由 BuildService 產出索引到離線 DFS。再經由 DeployExpress 服務,將索引以鏈式分發的形式複製到所有 Searcher 上,並被 Searcher 載入,以提供服務。搜尋的幾大引擎,HA3、DII、IGraph,都採用了這種架構。在這個框架下:

  1. 索引分發一般時間較長,特別是長尾業務,制約了 Searcher 的遷移和擴容效率。
  2. 索引分發還會產生大量 IO,由於 SSD 寫對讀的影響較大,而像IGraph、OpenSearch 這些業務都會在查詢過程中訪問磁碟,寫盤會造成這些業務的抖動,為了避免這種情況,需要進行 IO 隔離和限速,這又大大增加了索引分發的時間。
  3. 為了能夠較快的分發索引,一些資料量較大的業務,不得不將索引分成多個 Partition,分發到不同的例項上去。對於表較多,且有一定關聯的業務,這會使得他們不得不巧妙設計分列方式,以保證查詢效能,使得廣播查詢不可避免。
  4. 如果索引很大,還要考慮磁碟與記憶體的比例,增加運維複雜度。

儲存計算分離架構

儲存計算分離後架構.png

這是線上服務的儲存計算分離架構,主目標物件是那種索引很大,冷資料很多,Cache 可以擋住大部分的查詢的業務場景,這類業務,普遍存在於 IGraph、OpenSearch、DII。這些業務雖然使用了不同的引擎來實現,受益於統一服務框架,只在底層和管控系統進行了改造就得以實現。下面先介紹一下整體架構設計:

  1. 索引仍然由 BuildService 產出到離線 DFS,這個 DFS 可以是盤古、也可以是 HDFS,可以使用機械盤,降低成本。
  2. 相比於分離前,去除了 DeployExpress 鏈式分發,取而代之的是 Madrox 分散式索引同步服務。
  3. 本地磁碟也被移除,當然,實際情況還是會有一塊盤的,配置、日誌、Binary、甚至實時落盤,還是要用的。
  4. 索引不再分發到線上 Searcher,而是線上的儲存叢集:盤古。出於穩定性和效能考慮,同時部署了兩套盤古,內容一致,互為備份。
  5. 索引檔案以 4KB 為單位劃分為若干 Block,查詢在需要訪問索引時,會先檢查 BlockCache 是否存在需求的 Block。僅當 BlockCache 不命中的情況時,才會產生真正的 IO 操作,通過網路讀取盤古上的索引檔案,並會更新 BlockCache。

由於沒有了索引到本地磁碟的分發過程,Searcher 變得輕快無比,想飛到哪就飛到哪。解決了快速遷移和擴容的問題。也沒有了本地磁碟 IO 的隔離問題,當然,線上盤古上現在要考慮這個問題了,好在,盤古可以做單盤寫限速和 QoS,此外 Madrox 做為唯一的寫者,也可以調節併發來控制總量。計算與儲存機型可以分開演進,成本自然的下降,儲存節點也會形成統一的儲存資源池,提高利用率。大索引單 Partition 成為可能,不再受單機容量限制。

盤古叢集

盤古.png

盤古提供了一種稱為 FlatLogFile 的⽂件介面,對使用者呈現順序寫和隨機讀的語義,類似於本地檔案系統的流,不支援隨機寫。這與我們的全量/增量索引檔案用法完全契合,我們的全量/增量索引檔案採用的是 Append Only 方式寫出,產出後立即封閉,不會再進行任何修改。讀取則是以隨機方式為主,順序讀取方式為輔。

這使得我們不依賴於塊裝置,也就不需要使用盤古的 BlockServer,而直接訪問 ChunkServer,這樣可以節省 BlockServer 到 ChunkServer 的一跳網路延遲開銷,以及 BlockServer 的資源開銷。

藉助 FSLIB 的外掛式設計,我們將盤古提供的 Clinet SDK 封裝成 FSLIB 的外掛,使得線上服務可以直接訪問盤古叢集。

雙盤古叢集

這裡解釋一下雙盤古叢集設計的考慮。

  • 穩定性

雙盤古互為備份,內容一致,是容災上的經典解決方案。盤古承諾 99.95% 的穩定性,已經很高了,但搜尋線上服務有自己更高的要求。分離之前,索引被分發到本地磁碟,離線索引不可訪問,並不會影響到線上服務,一臺 Searcher 的問題,不會影響到整體服務。但在儲存計算分離的架構下,一但線上盤古故障,不能快速恢復,線上業務就會癱瘓,影響是不敢想象的。單一盤古叢集,極容易受到,人為誤操作、升級過程中的意外和程式問題的影響,雙盤古叢集可以有效避免這些意外的發生。

  • 低延遲需求

由於經過了網路和磁碟,長尾是不可避免的。如果我們兩個盤古同時發出請求,取最先回來的結果,可以拿到更低、更平穩的延遲。這就是我們的雙讀盤古策略,主要用於 Query 觸發的 IO 請求。

  • 服務能力

有些索引為了效能,需要採用全記憶體載入方式,如:PrimaryKey 和 Attribute。在那些行數較多的業務中,Open階段,會同時順序大塊讀取相同的檔案,造成某些盤壓力過大。雙盤古輪詢策略可以起到分擔的作用。這就是我們的單讀盤古策略,主要用於全記憶體索引檔案 Open 時的,連續大塊一次性讀取。

  • 副作用:成本問題

雙盤古帶來的主要問題是儲存成本的上漲,為此我們採用了每盤古 2 副本的方案,加一起 4 副本,相較於單盤古 3 副本,只多 1 個副本的開銷。

未來,我們還會採用 EC(Erasure Coding)的方案來進一步降成本。預設 8+3 的 EC 配置將儲存的成本降到約 1.375 份,即雙盤古 2.7 份。

Madrox 分散式索引分發服務

Madrox.png

Madrox 用來解決索引從離線 DFS 到線上盤古的分發問題的。

有兩個角色:一個是 API Server,也就是 Master,接受管控系統的分發請求,將索引檔案的分發任務派給各個 Worker 也就是 Slave,並收集 Slave 的執行情況,以便反饋給管控系統;另一個是Slave,負責將指定的檔案複製到兩個盤古之上,並通過心跳彙報進度。

這個服務在設計上重點考慮了以下兩個因素:

  1. 有限且昂貴的機房間長傳頻寬
  2. 有限的盤古叢集的出頻寬

出於這樣的限制,slave 被設計到了計算節點上,並且採取的是一讀多寫的方式:

  • 一讀:保證資料在長傳頻寬上只出現一次。即 Clinet 只從離線 DFS 上讀取一次資料。
  • 多寫:採用星型寫,即從 Client 寫到兩個盤古的多個 ChunkServer。這樣,寫出時只會佔用盤古的入頻寬。

如果採用盤古叢集對拷方式,雙盤古要麼讀取離線 DFS 兩次,要麼鏈式複製佔用其中一個盤古叢集的出頻寬。

此外,Madrox 還要考慮一些問題:

  • 大檔案併發:需要盤古能提供小檔案合併大檔案,或者 Chunk 級複製的能力。目前,我們只能通過修改索引結構、增量 Segment 等方式實現。
  • 僅同步有效資料:因為離線 DFS 索引目錄中存在大量的中間及不需要分發的檔案。粗暴的目錄樹同步,會浪費頻寬和延長分發時間。
  • 雙盤古並行寫

分析與優化

線上服務不同於離線,延遲極為敏感,對長尾延遲也有很高的要求。我們的目標是:從網路發出請求到收回資料包的平均延遲在 500us 以內,99.9% 的分位數 1ms 以內。對網路、磁碟、盤古軟體棧、都提出了很高的要求。平均延遲比較容易達到,長尾延遲這一目標還在努力中。

延遲分析

一次 IO 的延遲開銷,主要有這樣幾個因素:

  1. 讀取磁碟,對於 NVME 的 SSD 來說,平均大約在 100us+,受寫影響,大塊讀影響明顯。
  2. 網路,對於核心態 TCP 協議棧來說,往返平均大約在 120us+,受網路波動、丟包、交換機排隊、機架位置等因素影響嚴重。
  3. ChunkServer 軟體棧、Client 軟體棧、執行緒切換等,平均大約在 50us+,受 IOPS 量影響明顯。

合併讀

前面說過,索引檔案是按照 4KB Block 來組織的。實際使用中,是難以保證一次請求的全部資料都落在一個 4KB Block 中的。如果強制要求每篇文件的索引資料都按照 4KB 對齊,必然造成大量索引空洞,空間浪費大,格式也會不相容。因此,跨 Block 的請求、超過 4KB 的大塊請求是必然存在的,這些都會產出兩次或者兩次以上的 IO 請求。

為此我們引入了合併讀,即一次請求全部所需的 Block,這樣可以節省傳送請求的網路及磁碟時延開銷。

與此同時,我們還通過改進索引查詢流程,引入了預取技術,當一個 Block 需要傳送 IO 請求時,先分析出其後續的 Block 是否很快被訪問到,如果是的,通過合併讀,夾帶回來,放進 BlockCache 中,這樣下次訪問時,避免了再次傳送 IO 請求。

非同步併發讀

相較於同步的序列 IO,非同步對提升 IOPS,降低延遲的效果是顯而易見的。但由於需要對引擎的查詢流程做較大變更,還在規劃中,很快就會開始實施。

丟包造成的長尾

丟包的直接表現就是長尾的出現,丟包的因素有很多,比如:

  • 網路裝置的硬體問題:網路卡、光模組、網路等不時的抖動
  • 交換機連線口的 outdrop:

    • 介面有突發流量,導致交換機buffer打滿,這種是交換機硬體架構影響。
    • 訪問流量是多打一,也就是多臺機器同時訪問一臺機器時。
    • 萬兆的機器訪問千兆機器,很容易有outdrop。
    • 報文從高速鏈路進入交換機,由低速鏈路轉發出去;或者報文從相同速率的多個埠同時進入交換機,由一個相同速率的埠轉發出去。
  • 伺服器負載、核心引數不恰當等軟體問題

丟包在網路上是難以避免的,特別是 TCP 的網路環境,有個不知道哪來的數字,0.03% 的丟包率都是正常的。

對於丟包,我們只能通過重傳來解決,當然,有些時候換一臺計算節點更加明智。在分析了 tcpdump 的結果後發現,核心預設的重傳間隔是 200ms,這源自 rto_min 引數的預設值 200ms。也就是請求發出後,如果資料包丟失,重發請求已經是 200ms 以後的事了。這時 Query 早已經超時返回了。

通過修改 rto_min=10ms 有效的緩解了客戶端請求丟包的情況,將重傳時間縮小到 10ms。這裡貼一張修改 rto_min 前後的對比圖,中間密集的部分,是預設 rto_min=200ms 情況。兩邊是 rto_min=10ms 的情況。可見長尾延遲有明顯的降低。當然程式碼就是重傳率明顯上升。

rto_min.png

也可以看出,rto_min 沒能完全解決問題,還有一些情況,比如資料包被網路延遲,甚至重傳的包也都延遲超過50ms,rto_min也是無能為力的。重傳也會造成網路的進一步惡化。

冷熱表分離

在實際的使用當中,我們發現,純粹的儲存計算分離,完全依靠 SearchCache/BlockCache 的方案,存在如下問題:

  1. Cache 填充慢、預熱時間過長,且受 QPS 影響,如果 QPS 較低,可能需要幾十分鐘。
  2. 冷啟動時,命中率低,延遲過高
  3. 冷啟動時,盤古叢集壓力過大
  4. 缺少降級方案,一旦網路或者盤古故障,服務能力完全喪失。

為解決這些問題,我們在引擎的全圖片版本上,實現了冷熱表分離方案。

冷熱表分離是指,根據提前設定好的規則,在離線構建索引的時候,就將文件分成冷、熱兩張資料表。熱表採用不分離架構,索引分發到本地;冷表則會採用分離架構,在不能命中 SearchCache/BlockCache 的情況下,直接訪問盤古的一個折中方案。可以簡單的把熱表理解成一種靜態 Cache,如果規則合理,這個 Cache 可以擋住大部分的訪問,極大的降低對儲存叢集的依賴,從而大幅提高線上服務的效能,並降低延遲。

如果設定規則,是這個方案的難點。

對於主搜尋 Summary,由於資料已經做了 Excellent/Good/Bad 的分層,得益於一階段的查詢模式,使用 Excellent 和 Good 做熱表,分得能夠獲得 90%+ 和 98%+ 的命中率。極大的降低了對儲存叢集的訪問壓力

對於海神,起初簡單的按 KKV 索引的 SKey 鏈長進行劃分,獲得了近 80% 的命中率,但熱表索引也較大,後來通過與演算法團隊合作,尋找到了更低索引量情況下,超過 90% 命中率的規則。

冷熱分離方案,不僅降低了線上儲存叢集的訪問壓力,同時還是一個可靠的降級方案。

分散式Cache

對於行數很多的頭部業務,在冷啟動時,會對線上儲存叢集造成較大的衝擊,且讀取的資料相對集中,由於儲存叢集規模較小,可能會耗盡儲存叢集的可用頻寬。因此,在計算叢集中部署一套分散式Cache,是一個不錯的思路。副作用是對於不能命中分散式 Cache 的請求,會增加網路跳數,造成延遲上漲。

展望

搜尋線上服務的儲存計算分離,還有很長的路要走。特別是需要繼續探索,降低儲存叢集的平均延遲,長尾延遲,提高 IOPS 能力等方面。用好 100G 網路、多核 CPU,引入使用者態 TCP、甚至是 RDMA。擴充應用場景,服務好長尾業務。

我們相信,儲存計算分離會對搜尋架構產生深遠的變革性影響。


相關文章