如何構建一個分佈式塊存儲產品?

構建一個企業級分佈式存儲系統對於任何一個團隊來說,都是一件極具挑戰性的工作。不僅需要大量的理論基礎,還需要有良好的工程能力。

本文是系列的第一篇,着重介紹分佈式存儲的相關背景和元數據服務,整理自 SmartX 聯合創始人兼 CTO 張凱,在 QCon 2018 大會上的演講。

我今天跟大家分享的題目是『ZBS:SmartX 自研分佈式塊存儲系統』。

SmartX 是我們公司的名字,我本人是這個公司的聯合創始人兼 CTO。ZBS 是 SmartX 研發的分佈式塊存儲產品的名字。

SmartX 於 2013 年成立,是一個以技術爲主導的公司,目前主要專注於分佈式存儲及虛擬化這個兩個領域。我們的產品均爲自己研發,目前已經運行在數千臺物理服務器上,存儲了數十 PB 的數據。

SmartX 跟國內主流的硬件服務商、雲服務商都有合作,我們的產品已經服務了包括公有云、私有云以及金融業、製造業等核心領域的關鍵業務,其中也包括核心應用和核心數據庫等應用場景。今天我將主要圍繞分佈式塊存儲進行介紹。

一般來說,我們根據存儲的訪問接口以及應用場景,把分佈式存儲分爲三種類型,包括分佈式塊存儲,分佈式文件存儲,和分佈式對象存儲。

其中,分佈式塊存儲的主要應用場景包括:

虛擬化:比如像 KVM,VMware,XenServer 等 Hypervisor,以及像 Openstack,AWS 等雲平臺。塊存儲在其中的角色是支撐虛擬機中的虛擬盤的存儲。

數據庫:比如 MySQL,Oracle 等。很多 DBA 都將數據庫的數據盤運行在一個共享的塊存儲服務上,例如分佈式塊存儲。此外也有很多客戶直接把數據庫運行在虛擬機中。

容器:容器最近幾年在企業中使用越來越廣泛。一般來說,容器中運行的應用都是無狀態的,但在很多應用場景下,應用也會有數據持久化的需求。應用可以選擇將數據持久化到數據庫中,也可以選擇將數據持久化到一個共享虛擬磁盤上。這個需求對應到 Kubernetes 中,就是 Persistent Volume 這個功能。

今天我將主要圍繞 SmartX 如何打造分佈式塊存儲進行介紹。SmartX 從 2013 年成立開始,到目前已經積累了 5 年左右的分佈式塊存儲的研發經驗,所以今天我們除了分享 SmartX 如何實現我們自己研發的分佈式塊存儲 ZBS 以外,還會詳細介紹我們在分佈式塊存儲的研發過程中的一些思考和選擇。此外也將介紹一下我們產品未來的規劃。

從廣泛意義上講,分佈式存儲中通常需要解決三個問題,分別是元數據服務,數據存儲引擎,以及一致性協議。

其中,元數據服務提供的功能一般包括:集羣成員管理,數據尋址,副本分配,負載均衡,心跳,垃圾回收等等。數據存儲引擎負責解決數據在單機上存儲,以及本地磁盤的管理,磁盤故障處理等等。每一個數據存儲引擎之間是隔離的,在這些隔離的存儲引擎之間,需要運行一個一致性協議,來保證對於數據的訪問可以滿足我們期望的一致性狀態,例如強一致,弱一致,順序一致,線性一致等等。我們根據不同的應用場景,選擇一個適合的一致性協議,這個協議將負責數據在不同的節點之間的同步工作。

有了這三部分,我們基本上就掌握了一個分佈式存儲的核心。不同的分佈式存儲系統之間的區別,基本也都來自於這三個方面的選擇不同。

接下來我會分別從這三個方面介紹一下我們在做 ZBS 系統設計的時候是怎樣思考的,以及最終決定採用哪種類型的技術和實現方法。

首先我們來介紹一下元數據服務。我們先來談談我們對元數據服務的需求。

所謂元數據就是『數據的數據』,比如說數據放在什麼位置,集羣中有哪些服務器,等等。如果元數據丟失了,或者元數據服務無法正常工作,那麼整個集羣的數據都無法被訪問了。

由於元數據的重要性,所以對元數據的第一個需求就是可靠性。元數據必須是保存多份的,同時元數據服務還需要提供 Failover 的能力。

第二個需求就是高性能。儘管我們可以對 IO 路徑進行優化,使得大部分 IO 請求都不需要訪問元數據服務,但永遠都有一些 IO 請求還是需要修改元數據,比如數據分配等等。爲避免元數據操作成爲系統性能的瓶頸,元數據操作的響應時間必須足夠短。同時由於分佈式系統的集羣規模在不斷的擴大,對於元數據服務的併發能力也有一定的要求。

最後一個需求是輕量級。由於我們產品大部分使用場景是私有部署,也就是我們的產品是部署在客戶的數據中心的,且由客戶自己運維,而非我們的運維人員運維。這個場景和很多互聯網公司自己來運維自己的產品是完全不同的場景。所以對於 ZBS 來說,我們更強調整個系統,尤其是元數據服務的輕量級,以及易運維的能力。我們期望元數據服務可以輕量級到可以把元數據服務和數據服務混合部署在一起。同時我們希望大部分的運維操作都可以由程序自動完成,或用戶只需要在界面上進行簡單的操作就可以完成。

如果大家瞭解 HDFS 的話,HDFS 中的元數據服務的模塊叫做 Namenode,這是一個非常重量級的模塊。Namenode 需要被獨立部署在一臺物理服務器上,且對硬件的要求非常高,且非常不易於運維,無論是升級還是主備切換,都是非常重的操作,非常容易因操作問題而引發故障。

以上就是我們對元數據服務的需求。接下來我們來看一下具體有哪些方法可以構造一個元數據服務。

談到存儲數據,尤其是存儲結構化的數據,我們第一個想到的就是關係型數據庫,例如 MySQL,以及一些成熟的 KV 存儲引擎,例如 LevelDB,RocksDB 等。但這種類型的存儲最大的問題就是無法提供可靠的數據保護和 Failover 能力。LevelDB 和 RocksDB 雖然非常輕量級,但都只能把數據保存在單機上。而儘管 MySQL 也提供一些主備方案,但我們認爲 MySQL 的主備方案是一個太過笨重的方案,且缺乏簡易的自動化運維方案,所以並不是一個十分好的選擇。

其次,我們來看一下一些分佈式數據庫,例如 MongoDB 和 Cassandra。這兩種分佈式數據庫都可以解決數據保護和提供 Failover 機制。但是他們都不提供 ACID 機制,所以在上層實現時會比較麻煩,需要額外的工作量。其次就是這些分佈式數據庫在運維上也相對複雜,不是很易於自動化運維。

也有一種選擇是基於 Paxos 或者 Raft 協議自己實現一個框架。但這樣實現的代價非常大,對於一個創業公司不是一個很划算的選擇。並且我們創業的時間是 2013 年,當時 Raft 也只是剛剛提出。

第四種是選擇 Zookeeper。Zookeeper 基於 ZAB 協議,可以提供一個穩定可靠地分佈式存儲服務。但 Zookeeper 的最大的問題是能夠存儲的數據容量非常有限。爲了提高訪問速度,Zookeeper 把存儲的所有數據都緩存在內存中,所以這種方案導致元數據服務所能支撐的數據規模嚴重受限於服務器的內存容量,使得元數據服務無法做到輕量級,也無法和數據服務混合部署在一起。

最後還有一種方式是基於 Distributed Hash Table(DHT)的方法。這種方法的好處元數據中不需要保存數據副本的位置,而是根據一致性哈希的方式計算出來,這樣就極大地降低了元數據服務的存儲壓力和訪問壓力。但使用 DHT 存在的問題,就喪失了對數據副本位置的控制權,在實際生產環境中,非常容易造成集羣中的產生數據不均衡的現象。同時在運維過程中,如果遇到需要添加節點,移除節點,添加磁盤,移除磁盤的情況,由於哈希環會發生變化,一部分數據需要重新分佈,會在集羣中產生不必要的數據遷移,而且數據量往往非常大。而這種於運維操作在一個比較大規模的環境中幾乎每天都會發生。大規模的數據遷移很容易影響到線上的業務的性能,所以 DHT 使得運維操作變得非常麻煩。

以上介紹的方法都存在各種各樣的問題,並不能直接使用。最終 ZBS 選擇了使用 LevelDB(也可以替換成 RocksDB) 和 Zookeeper 結合的方式,解決元數據服務的問題。首先,這兩個服務相對來說都非常輕量級;其次 LevelDB 和 Zookeeper 使用在生產中也非常穩定。

我們採用了一種叫做 Log Replication 的機制,可以同時發揮 LevelDB 和 Zookeeper 的優點,同時避開他們自身的問題。

這裏我們簡單的介紹一下 Log Replication。簡單來說,我們可以把數據或者狀態看作是一組對數據操作的歷史集合,而每一個操作都可以通過被序列化成 Log 記錄下來。如果我們可以拿到所有 的 Log,並按照 Log 裏面記錄的操作重複一遍,那麼我們就可以完整的恢復數據的狀態。任何一個擁有 Log 的程序都可以通過重放 Log 的方式恢復數據。如果我們對 Log 進行復制,實際上也就相當於對數據進行了複製。這就是 Log Replication 最基本的想法。

我們具體來看一下 ZBS 是如何利用 Zookeeper + LevelDB 完成 Log Replication 操作的。首先,集羣中有很多個 Meta Server,每個 Server 本地運行了一個 LevelDB 數據庫。Meta Server 通過 Zookeeper 進行選主,選出一個 Leader 節點對外響應元數據請求,其他的 Meta Server 則進入Standby 狀態。

當 Leader 節點接收到元數據的更新操作後,會將這個操作序列化成一組操作日誌,並將這組日誌寫入 Zookeeper。由於 Zookeeper 是多副本的,所以一旦 Log 數據寫入 Zookeeper,也就意味着 Log 數據是安全的了。同時這個過程也完成了對 Log 的複製。

當日志提交成功後,Meta Server 就可以將對元數據的修改同時提交到本地的 LevelDB 中。這裏 LevelDB 中存儲的是一份全量的數據,而不需要以 Log 的形式存儲。

對於非 Leader 的 Meta Server 節點,會異步的從 Zookeeper 中拉取 Log,並將通過反序列化,將 Log 轉換成對元數據的操作,再將這些修改操作提交到本地的 LevelDB 中。這樣就能保證每一個 Meta Server 都可以保存一個完整的元數據。

前面提到,由於 Zookeeper 存儲數據的容量受限於內存容量。爲了避免 Zookeeper 消耗過多內存,我們對 Zookeeper 中的 Log 定期執行清理。只要 Log 已經被所有的 Meta Server 同步完, Zookeeper 中保存的 Log 就可以被刪除了,以節省空間。通常我們在 Zookeeper 上只保存 1GB 的 Log,已經足夠支撐元數據服務。

Failover 的邏輯也非常簡單。如果 Leader 節點發生故障,其他還存活的的 Meta Server 通過 Zookeeper 再重新進行一次選主,選出一個新的 Meta Leader。這個新的 Leader 將首先從 Zookeeper 上同步所有還未消耗的日誌,並在提交到本地的 LevelDB 中,然後就可以對外提供元數據服務了。

現在我們總結一下 ZBS 中元數據服務實現的特點。

首先,這個原理非常容易理解,而且實現起來非常簡單。由 Zookeeper 負責選主和 Log Replication,由 LevelDB 負責本地元數據的存儲。背後的邏輯就是儘可能的將邏輯進行拆分,並儘可能的複用已有項目的實現。

其次,速度足夠快。Zookeeper 和 LevelDB 本身的性能都不錯,而且在生產中,我們將 Zookeeper 和 LevelDB 運行在 SSD 上。在實際測試中,對於單次元數據的修改都是在毫秒級完成。在併發的場景下,我們可以對元數據修改的日誌做 Batch,以提高併發能力。

此外,這種方式支持 Failover,而且 Failover 的速度也非常快。Failover 的時間就是選主再加上 Log 同步的時間,可以做到秒級恢復元數據服務。

最後說一下部署。在線上部署的時候,我們通常部署 3 個或 5 個 Zookeeper 服務的實例以及至少 3 個 Meta Server 服務的實例,以滿足元數據可靠性的要求。元數據服務對資源消耗都非常小,可以做到和其他服務混合部署。

以上是一些基本的原理,我們再來看一下 ZBS 內部的對於元數據服務的具體實現。

我們將上述的 Log Replication 邏輯封裝在了一個 Log Replication Engine 中,其中包含了選主、向 Zookeeper 提交 Log、向 LevelDB 同步數據等操作,進一步簡化開發複雜度。

在 Log Replication Engine 的基礎之上,我們實現了整個 Meta Sever 的邏輯,其中包含了 Chunk Manager,NFS Manger,iSCSI Manager,Extent Manager 等等很多管理模塊,他們都可以通過 Log Replication Engine,管理特定部分的元數據。RPC 模塊是 Meta Server 對外暴露的接口,負責接收外部的命令,並轉發給對應的 Manager。例如創建/刪除文件,創建/刪除虛擬卷等等。此外,Meta Server 中還包含了一個非常複雜的調度器模塊,裏面包含了各種複雜的分配策略,恢復策略,負載均衡策略,以及心跳,垃圾回收等功能。

以上就是關於元數據服務部分的介紹。

作者簡介

張凱,畢業於清華計算機系,畢業以後加入百度基礎架構部工作了兩年,主要從事分佈式系統和大數據相關的工作。張凱也是開源社區的代碼貢獻者,參與的項目包括 Sheepdog 和 InfluxDB。其中 Sheepdog 是一個開源的分佈式塊存儲項目,InfluxDB 是一個時序數據庫(Time Series Database,TSDB)項目。2013 年張凱從百度離職,和清華的兩個師兄一起創辦了 SmartX 公司。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章