如何構建「正確的」雲平臺存儲

做者:ZStack 王爲算法

從 2015 年到如今,ZStack 有一條宗旨一直沒有變過,就是向客戶交付穩定、可靠、高性能的雲平臺,這條宗旨在前幾年讓咱們一直聚焦雲平臺自己,包括虛擬化、雲網絡、雲編排、存儲管理等等這些功能。數據庫

在這裏面最讓咱們頭痛的,即便不是第一也能進前三的存在,就是存儲管理編程

考慮到存儲對業務的無比的重要性,以及咱們做爲一家創業公司的支持能力,咱們一開始一直是基於一些開源的存儲方案對客戶提供服務:緩存

  1. XFS,做爲 RHEL 默認的本地文件系統,咱們本來一直對 XFS 是比較信任的,但實際上 XFS 在使用過程當中問題多多,咱們幫客戶繞過了不少坑,也在考慮別的替代方案;
  2. NFS,NFS 是一個對雲平臺很簡單的方案,由於它屏蔽了不少存儲的複雜性,用文件系統的方式提供了共享存儲,使得咱們能夠用相似本地文件系統的管理方式管理共享存儲,既簡單又支持熱遷移等高級功能,看似完美,但實際上 NFS 幾乎是咱們最不推薦的生產用存儲方案之一,細節將在後面討論;
  3. OCFS2,當用戶只有 SAN 存儲,也沒法提供 NFS 接口時,咱們的選擇並很少,此時 Oracle 的 OCFS2 成爲一個值得青睞的方案,其優勢是在小規模使用時基本上很穩定,部署後也可使用文件系統的方式使用,但在性能、大規模的擴展性和部分功能(例如文件鎖)上支持也並不完美;
  4. Ceph,基於 Ceph 能夠提供很棒的存儲方案,但 Ceph 相對複雜的部署運維對部分客戶仍是比較難接受,特別是在私有云中,不少客戶習慣了 SAN 存儲帶來的性能和安全感,對他們來講也沒有超大容量的需求或者隨時須要靈活擴容,反而大廠商帶來的安全感,或者可以將以前用在VMware 上的 SAN 存儲繼續用起來纔是最重要的。

綜合考慮前面的各類存儲,NFS、OCFS2 的不完美促使咱們提供一個可以管理共享存儲的存儲方案,這個方案要能達到下面的要求:安全

  1. 部署速度要足夠快,ZStack 的部署速度一貫是業界前列,咱們的標準一直是對於 Linux 有基本理解的人可以在 30 分鐘內完成部署,這個時間是包括部署主存儲、鏡像倉庫的時間的。
  2. 可以擴展到足夠大的規模,根據 SAN 存儲的性能,單個集羣應該能夠接管幾十到上百的服務器(由於通常來講單個 SAN 存儲能支撐的服務器數量有限)。
  3. 性能可以完整發揮 SAN 存儲的性能,IO 模式可以發揮 SAN 存儲的 cache 性能,對於 OCFS2 咱們能夠經過調整 block size 來優化 OCFS2 性能,但若是在分層 SAN 存儲上測試就會發現因爲大 block size 帶來的 IO pattern 變化,若是測試 4k 小文件隨機寫,性能並不穩定,沒法像直接在物理機上對 LUN 測試前期所有寫到高速盤上,帶來了測試數據的不理想。
  4. 高穩定性,與互聯網、公有云業務不一樣,私有云均部署在客戶機房,甚至是一些隔離、保密機房,這意味着咱們沒法像互聯網環境同樣執行「反覆試錯」的策略,咱們沒法控制用戶的升級節奏,沒法時刻監控運維存儲狀態,也沒法再客戶環境進行灰度測試、鏡像驗證。

最終,在2018 年咱們決定本身開發一個面向共享塊存儲的存儲方法,命名很直接就叫 SharedBlock。整個方案是這樣的:服務器

  1. 基於塊設備,直接基於塊設備向虛擬機提供虛擬雲盤,經過避免文件系統開銷能夠明顯提高性能和穩定性;
  2. 在塊設備上基於 Paxos 實現分佈式鎖來管理塊設備的分配和節點的加入、心跳、IO 狀態檢查;
  3. 經過 Qemu 的接口實現對用戶磁盤讀寫情況進行監控;

SharedBlock 在推出後,應用在了不少的生產客戶上,特別是能夠利舊 SAN 存儲特色讓 SharedBlock 快速部署在大量以往使用虛擬化的客戶上。網絡

後來隨着 5G 和物聯網、雲端互聯的發展,讓市場迫切須要一個價格不高、能夠簡便部署、軟硬一體的超融合產品,所以咱們就在考慮一個兩節點一體機的產品,經過和硬件廠商合做設計,能夠實現 2U 的一體機包含足夠用戶使用的硬盤、獨立的模塊和雙電冗餘,咱們但願能經過這個產品將客戶的本來單節點運行的應用平滑升級到兩節點備份,讓客戶的運行在軌道站點、製造業工廠這些「端」應用既享受到雲的便利,又不須要複雜的運維和部署。這就是咱們的 Mini Storagesession

在開發這些存儲產品的過程當中,咱們踩了無數的坑,也收穫了不少經驗。架構

下面先說說將存儲作正確有多難,在今年說這個話題有一個熱點事件是避不開的,就是今年的 FOSDEM 19' 上 PostgreSQL 的開發者在會上介紹了 PostgreSQL 開發者發現本身使用 fsync() 調用存在一個十年的 bug——併發

  1. PG 使用 writeback 機制,特別是在過去使用機械硬盤的時代,這樣能夠大大提升速度,但這就須要定時 fsync 來確保把數據刷到磁盤;
  2. PG 使用了一個單獨線程來執行 fsync(),指望當寫入錯誤時可以返回錯誤;
  3. 但其實操做系統可能本身會將髒頁同步到磁盤,或者可能別的程序調用 fsync();
  4. 不管上面的哪一種狀況,PG 本身的同步線程在 fsync 時都沒法收到錯誤信息;

這樣 PG 可能誤覺得數據已經同步而移動了 journal 的指針,實際上數據並無同步到磁盤,若是磁盤持續沒有修復且忽然丟失內存數據就會存在數據丟失的狀況。

在這場 session 上 PG 的開發者吐槽了 kernel 開發以及存儲開發裏的不少問題,不少時候 PG 只是想更好地實現數據庫,但卻發現常常要爲 SAN/NFS 這些存儲操心,還要爲內核的未文檔的行爲買單。

這裏說到 NFS,不得很少提兩句,在 Google 上搜索 "nfs bug" 能夠看到五百萬個結果,其中不乏 Gitlab 之類的知名廠商踩坑,也不乏 Redhat 之類的操做系統嘗試提供遇到 NFS 問題的建議:

從咱們一個雲廠商的角度看來,虛擬機存儲使用 NFS 遇到的問題包括但不限於這幾個:

  1. 部分客戶的存儲不支持 NFS 4.0 帶來一系列性能問題和併發問題,並且 4.0 以前不支持 locking;
  2. nfs 服務自己會帶來安全漏洞;
  3. 對於在 server 上作一些操做(例如 unshare)帶來的神祕行爲;
  4. 使用 async 掛載可能會帶來一些不一致問題,在虛擬化這種 IO 棧嵌套多層的環境可能會放大這一問題,而使用 sync 掛載會有明顯的性能損失;
  5. NFS 自己的 bug

最終咱們的建議就是生產環境、較大的集羣的狀況下,最起碼,少用 NFS 4.0 之前的版本……

另外一個出名的文章是發表在 14 年 OSDI 的這篇 All File Systems Are Not Created Equal,做者測試了數個文件系統和文件應用,在大量系統中找到了不乏丟數據的 Bug, 在此以後諸如 FSE'16 的 Crash consistency validation made easy 又找到了 gmake、atom 等軟件的各類丟數據或致使結果不正確的問題:

上面咱們舉了不少軟件、文件系統的例子,這些都是一些單點問題或者局部問題,若是放在雲平臺的存儲系統上的話,複雜度就會更高:

1. 首先,私有云面臨的是一個離散碎片的環境,咱們都知道 Android 開發者每每有比 iOS 開發者有更高的適配成本,這個和私有云是相似的,由於客戶有:

1)不一樣廠商的設備

2)不一樣的多路徑軟件

3)不一樣的服務器硬件、HBA 卡;

雖然 SCSI 指令是通用的,但實際上對 IO 出錯、路徑切換、緩存使用這些問題上,不一樣的存儲+多路徑+HBA 能夠組成不一樣的行爲,是最容易出現難以調試的問題地方,例若有的存儲配合特定 HBA 就會產生下面的 IO 曲線:

2. 因爲咱們是產品化的私有云,產品化就意味着整套系統不多是託管運維,也不會提供駐場運維,這樣就會明顯受客戶良莠不齊的運維環境和運維水平限制:

1)升級條件不一樣,有的用戶但願一旦部署完就不再要升級不要動了,這就要求咱們發佈的版本必定要是穩定可靠的,由於發出去可能就沒有升級的機會了,這點和互聯網場景有明顯的區別;

2)聯網條件不一樣,通常來講,來自生產環境的數據和日誌是相當重要的,但對產品化的廠商來講,這些數據倒是彌足珍貴,由於有的客戶機房不只不容許鏈接外網,甚至咱們的客戶工程師進機房的時候手機也不容許攜帶;

3)運維水平不一樣,對於一個平臺系統,若是運維水平不一樣,那麼能發揮的做用也是不一樣的,好比一樣是硬件故障,對於運維水平高的客戶團隊可能很快可以確認問題並找硬件廠商解決,而有的客戶就須要咱們先幫忙定位分析問題甚至幫助和硬件廠商交涉,就須要消耗咱們不少精力。

3. 漫長的存儲路徑,對於平臺來講,咱們不只要操心 IO 路徑——Device Mapper、多路徑、SCSI、HBA 這些,還要操心虛擬化的部分——virtio 驅動、virtio-scsi、qcow2…… 還要操心存儲的控制平面——快照、熱遷移、存儲遷移、備份…… 不少存儲的正確性驗證只涉及選舉、IO 這部分,而對存儲管理並無作足夠的關注,而根據咱們的經驗,控制面板一旦有 Bug,破壞力可能比數據面更大。

說了這麼多難處,咱們來講說怎麼解決。提到存儲的正確性,接觸過度布式系統的同窗可能會說 TLA+,咱們先對不熟悉 TLA+ 的同窗簡單介紹下 TLA+。

2002 Lamport 寫了一本書《Specifying Systems》基本上算是 TLA+ 比較正式的第一本書,瞭解的朋友可能知道在此以前 Lamport 在分佈式系統和計算結科學就很出名了——LaTex、Lamport clock、PAXOS 等等,TLA+ 剛開始的時候沒有特別受重視,他的出名是來自 AWS 15 年發表在 ACM 會刊的《How Amazon Web Services Uses Formal Methods》。

從本質上講,形式化驗證並非新東西,大概在上世紀就有了相關的概念,TLA+ 的優點在於它特別適合驗證分佈式系統的算法設計。由於對於一個可驗證的算法來講,核心是將系統時刻的狀態肯定化,並肯定狀態變化的條件和結果,這樣 TLA+ 能夠經過窮舉+剪枝檢查當有併發操做時會不會有違反要求(TLA+ 稱之爲 invariant)的地方——例如帳戶餘額小於 0,系統中存在了多個 leader 等等。

看最近的幾場 TLA Community Meeting,能夠看到 Elasticserach、MongoDB 都有應用。

那麼既然這個東西這麼好,爲何在國內開發界彷佛並無特別流行呢?咱們在內部也嘗試應用了一段時間,在 Mini Storage 上作了一些驗證,感受若是 TLA+ 想應用更普遍的話,可能仍是有幾個問題須要優化:

  1. 狀態爆炸,由於 TLA+ 的驗證方式決定了狀態數量要通過精心的抽象和仔細的檢查,若是一味地增長狀態就可能遇到狀態爆炸的問題;
  2. TLA+ Spec 是沒法直接轉換成代碼的,反過來,代碼也沒法直接轉換成 Spec。那麼換句話說,不管是從代碼到 Spec 仍是從 Spec 到代碼都有出錯的可能,輕則有 Bug,重則可能致使你信心滿滿的算法其實與你的實現根本不一樣;
  3. 外部依賴的正確性,這一點可能有點要求太高,但卻也是可靠系統的重要部分,由於用戶是無論產品裏是否用到了開源組件,不管是 qemu 的問題仍是 Linux 內核的問題,客戶只會認爲是你的問題,而咱們不太可能分析驗證每一個依賴。

固然了,涉及到算法的正確性證實,形式化證實依然是不可替代的,但不得不說目前階段在雲平臺存儲上應用,還沒作到所有覆蓋,固然了咱們也看到 TLA+ 也在不斷進步——

  1. 可視化
  2. 加強可讀性
  3. Spec 的可執行

這裏特別是第三點,若是咱們的 Spec 可以被轉換成代碼,那麼咱們就能夠將核心代碼的算法部分抽象出來,作成一個單獨的庫,直接使用被 Spec 證實過的代碼。

分佈式系統的測試和驗證,這幾年還有一個很熱門的詞彙,就是混沌工程

混沌工程對大多數人來講並非一個新鮮詞彙,能夠說它是在單機應用轉向集羣應用,面向系統編程轉向到面向服務編程的必然結果,咱們已經看到不少互聯網應用聲稱在混沌工程的幫助下提升了系統的穩定性如何如何,那麼對於基礎架構軟件呢?

在必定程度上能夠說 ZStack 很早就開始在用混沌工程的思想測試系統的穩定性,首先咱們有三個關鍵性的外部總體測試

  1. MTBF,這個概念通常見於硬件設備,指的是系統的正常運行的時間,對咱們來講會在系統上根據用戶場景反覆操做存儲(建立、刪除虛擬機,建立、刪除快照,寫入、刪除數據等)在此之上引入故障檢查正確性;
  2. DPMO,這個是一個測試界很老的概念,偏向於單個操做的反覆操做,例如重啓 1000 次物理機,添加刪除 10000 次鏡像等等,在這之上再考慮同時引入故障來考察功能的正確性;
  3. Woodpecker,這是 ZStack 從最開始就實現的測試框架,代碼和原理都是開源的,它會智能的組合 ZStack 的上千個 API自動找到能夠持續下去的一條路徑,根據資源當前的狀態判斷資源能夠執行的 API,這樣一天下來能夠組合執行數萬次乃至上百萬次,與此同時再考慮引入錯誤。

上面這些方法,在大量調用 API、測試 IO 以外,很重要的一點就是注入錯誤,例如強制關閉虛擬機、物理機,經過可編程 PDU 模擬斷電等等,可是這些方法有一些缺陷:

  1. 複雜場景的模擬能力有限,例若有些客戶存儲並非一直 IO 很慢,而是呈現波峯波谷的波浪型,這種狀況和 IO 始終有明顯 delay 是有比較大的區別的;
  2. 不夠靈活,例若有的客戶存儲隨機 IO 不好但順序 IO 性能卻還能夠,也不是簡單的下降 IO 性能就能夠模擬的。

總之大部分混沌工程所提供的手段(隨機關閉節點、隨機殺進程、經過 tc 增長延時和 iproute二、iptables 改變網絡等等)並不能知足 ZStack 的徹底模擬用戶場景的需求。

在這種狀況下,咱們將擴展手段放在了幾個方向上:

  1. libfiu,libfiu 能夠經過 LD_PRELOAD 來控制應用調用 POSIX API 的結果,可讓應用申請內存失敗、打開文件失敗,或者執行 open 失敗。

     使用 fiurun + fiuctl 能夠對某個應用在須要的時刻控制系統調用。

fiu 對注入 libaio 沒有直接提供支持,但好在 fio 擴展和編譯都極爲簡單,所以咱們能夠輕鬆的根據本身的需求增長 module。

2. systemtap,systemtap 是系統界的經典利器了,能夠對內核函數的返回值根據需求進行修改,對內核理解很清晰的話,systemtap 會很好用,若是是對存儲進行錯誤注入,能夠重點搜 scsi 相關的函數,以及參考這裏:Kernel Fault injection framework using SystemTap

3. device-mapper,device-mapper 提供了 dm-flakey、dm-dust、dm-delay,固然你也能夠寫本身的 target,而後能夠搭配 lio 等工具就能夠模擬一個 faulty 的共享存儲,得益於 device-mapper 的動態加載,咱們能夠動態的修改 target 和參數,從而更真實的模擬用戶場景下的狀態;

4. nbd,nbd 的 plugin 機制很是便捷,咱們能夠利用這一點來修改每一個 IO 的行爲,從而實現出一些特殊的 IO pattern,舉例來講,咱們就用 nbd 模擬過用戶的順序寫很快但隨機寫異常慢的存儲設備;

5. 此外,還有 scsi_debug 等 debug 工具,但這些比較面向特定問題,就不細說了。

上面兩張圖對這些錯誤注入手段作了一些總結,從系統角度來看,若是咱們在設計階段可以驗證算法的正確性,在開發時注意開發可測試的代碼,經過海量測試和錯誤注入將路徑完整覆蓋,對遇到的各類 IO 異常經過測試 case 固化下來,咱們的存儲系統必定會是愈來愈穩定,持續的走在「正確」的道路上的。

相關文章
相關標籤/搜索