作者:ZStack 王爲
從 2015 年到現在,ZStack 有一條宗旨一直沒有變過,就是向客戶交付穩定、可靠、高性能的雲平臺,這條宗旨在前幾年讓我們一直聚焦雲平臺本身,包括虛擬化、雲網絡、雲編排、存儲管理等等這些功能。
在這裏面最讓我們頭痛的,即使不是第一也能進前三的存在,就是存儲管理。
考慮到存儲對業務的無比的重要性,以及我們作爲一家創業公司的支持能力,我們一開始一直是基於一些開源的存儲方案對客戶提供服務:
- XFS,作爲 RHEL 默認的本地文件系統,我們原本一直對 XFS 是比較信任的,但實際上 XFS 在使用過程中問題多多,我們幫客戶繞過了很多坑,也在考慮別的替代方案;
- NFS,NFS 是一個對雲平臺很簡單的方案,因爲它屏蔽了很多存儲的複雜性,用文件系統的方式提供了共享存儲,使得我們可以用類似本地文件系統的管理方式管理共享存儲,既簡單又支持熱遷移等高級功能,看似完美,但實際上 NFS 幾乎是我們最不推薦的生產用存儲方案之一,細節將在後面討論;
- OCFS2,當用戶只有 SAN 存儲,也無法提供 NFS 接口時,我們的選擇並不多,此時 Oracle 的 OCFS2 成爲一個值得青睞的方案,其優點是在小規模使用時基本上很穩定,部署後也可以使用文件系統的方式使用,但在性能、大規模的擴展性和部分功能(例如文件鎖)上支持也並不完美;
- Ceph,基於 Ceph 可以提供很棒的存儲方案,但 Ceph 相對複雜的部署運維對部分客戶還是比較難接受,特別是在私有云中,很多客戶習慣了 SAN 存儲帶來的性能和安全感,對他們來說也沒有超大容量的需求或者隨時需要靈活擴容,反而大廠商帶來的安全感,或者能夠將之前用在VMware 上的 SAN 存儲繼續用起來纔是最重要的。
綜合考慮前面的各種存儲,NFS、OCFS2 的不完美促使我們提供一個能夠管理共享存儲的存儲方案,這個方案要能達到下面的要求:
- 部署速度要足夠快,ZStack 的部署速度一向是業界前列,我們的標準一直是對於 Linux 有基本理解的人能夠在 30 分鐘內完成部署,這個時間是包括部署主存儲、鏡像倉庫的時間的。
- 能夠擴展到足夠大的規模,根據 SAN 存儲的性能,單個集羣應該可以接管幾十到上百的服務器(因爲一般來說單個 SAN 存儲能支撐的服務器數量有限)。
- 性能能夠完整發揮 SAN 存儲的性能,IO 模式能夠發揮 SAN 存儲的 cache 性能,對於 OCFS2 我們可以通過調整 block size 來優化 OCFS2 性能,但如果在分層 SAN 存儲上測試就會發現由於大 block size 帶來的 IO pattern 變化,如果測試 4k 小文件隨機寫,性能並不穩定,無法像直接在物理機上對 LUN 測試前期全部寫到高速盤上,帶來了測試數據的不理想。
- 高穩定性,與互聯網、公有云業務不同,私有云均部署在客戶機房,甚至是一些隔離、保密機房,這意味着我們無法像互聯網環境一樣執行“反覆試錯”的策略,我們無法控制用戶的升級節奏,無法時刻監控運維存儲狀態,也無法再客戶環境進行灰度測試、鏡像驗證。
最終,在2018 年我們決定自己開發一個面向共享塊存儲的存儲方法,命名很直接就叫 SharedBlock。整個方案是這樣的:
- 基於塊設備,直接基於塊設備向虛擬機提供虛擬雲盤,通過避免文件系統開銷可以明顯提升性能和穩定性;
- 在塊設備上基於 Paxos 實現分佈式鎖來管理塊設備的分配和節點的加入、心跳、IO 狀態檢查;
- 通過 Qemu 的接口實現對用戶磁盤讀寫狀況進行監控;
SharedBlock 在推出後,應用在了很多的生產客戶上,特別是可以利舊 SAN 存儲特點讓 SharedBlock 快速部署在大量以往使用虛擬化的客戶上。
後來隨着 5G 和物聯網、雲端互聯的發展,讓市場迫切需要一個價格不高、可以簡便部署、軟硬一體的超融合產品,因此我們就在考慮一個兩節點一體機的產品,通過和硬件廠商合作設計,可以實現 2U 的一體機包含足夠用戶使用的硬盤、獨立的模塊和雙電冗餘,我們希望能通過這個產品將客戶的原本單節點運行的應用平滑升級到兩節點備份,讓客戶的運行在軌道站點、製造業工廠這些“端”應用既**享受到雲的便利,又不需要複雜的運維和部署。**這就是我們的 Mini Storage。
在開發這些存儲產品的過程中,我們踩了無數的坑,也收穫了很多經驗。
下面先說說將存儲做正確有多難,在今年說這個話題有一個熱點事件是避不開的,就是今年的 FOSDEM 19’ 上 PostgreSQL 的開發者在會上介紹了 PostgreSQL 開發者發現自己使用 fsync() 調用存在一個十年的 bug——
- PG 使用 writeback 機制,特別是在過去使用機械硬盤的時代,這樣可以大大提高速度,但這就需要定時 fsync 來確保把數據刷到磁盤;
- PG 使用了一個單獨線程來執行 fsync(),期望當寫入錯誤時能夠返回錯誤;
- 但其實操作系統可能自己會將髒頁同步到磁盤,或者可能別的程序調用 fsync();
- 無論上面的哪種情況,PG 自己的同步線程在 fsync 時都無法收到錯誤信息;
這樣 PG 可能誤以爲數據已經同步而移動了 journal 的指針,實際上數據並沒有同步到磁盤,如果磁盤持續沒有修復且突然丟失內存數據就會存在數據丟失的情況。
在這場 session 上 PG 的開發者吐槽了 kernel 開發以及存儲開發裏的很多問題,很多時候 PG 只是想更好地實現數據庫,但卻發現經常要爲 SAN/NFS 這些存儲操心,還要爲內核的未文檔的行爲買單。
這裏說到 NFS,不得不多提兩句,在 Google 上搜索 “nfs bug” 可以看到五百萬個結果,其中不乏 Gitlab 之類的知名廠商踩坑,也不乏 Redhat 之類的操作系統嘗試提供遇到 NFS 問題的建議:
從我們一個雲廠商的角度看來,虛擬機存儲使用 NFS 遇到的問題包括但不限於這幾個:
- 部分客戶的存儲不支持 NFS 4.0 帶來一系列性能問題和併發問題,而且 4.0 之前不支持 locking;
- nfs 服務本身會帶來安全漏洞;
- 對於在 server 上做一些操作(例如 unshare)帶來的神祕行爲;
- 使用 async 掛載可能會帶來一些不一致問題,在虛擬化這種 IO 棧嵌套多層的環境可能會放大這一問題,而使用 sync 掛載會有明顯的性能損失;
- 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)運維水平不同,對於一個平臺系統,如果運維水平不同,那麼能發揮的作用也是不同的,比如同樣是硬件故障,對於運維水平高的客戶團隊可能很快能夠確認問題並找硬件廠商解決,而有的客戶就需要我們先幫忙定位分析問題甚至幫助和硬件廠商交涉,就需要消耗我們很多精力。
- 漫長的存儲路徑,對於平臺來說,我們不僅要操心 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+ 想應用更廣泛的話,可能還是有幾個問題需要優化:
- 狀態爆炸,因爲 TLA+ 的驗證方式決定了狀態數量要經過精心的抽象和仔細的檢查,如果一味地增加狀態就可能遇到狀態爆炸的問題;
- TLA+ Spec 是無法直接轉換成代碼的,反過來,代碼也無法直接轉換成 Spec。那麼換句話說,無論是從代碼到 Spec 還是從 Spec 到代碼都有出錯的可能,輕則有 Bug,重則可能導致你信心滿滿的算法其實與你的實現根本不同;
- 外部依賴的正確性,這一點可能有點要求過高,但卻也是可靠系統的重要部分,因爲用戶是不管產品裏是否用到了開源組件,不論是 qemu 的問題還是 Linux 內核的問題,客戶只會認爲是你的問題,而我們不太可能分析驗證每個依賴。
當然了,涉及到算法的正確性證明,形式化證明依然是不可替代的,但不得不說目前階段在雲平臺存儲上應用,還沒做到全部覆蓋,當然了我們也看到 TLA+ 也在不斷進步——
- 可視化
- 增強可讀性
- Spec 的可執行
這裏特別是第三點,如果我們的 Spec 能夠被轉換成代碼,那麼我們就可以將核心代碼的算法部分抽象出來,做成一個單獨的庫,直接使用被 Spec 證明過的代碼。
分佈式系統的測試和驗證,這幾年還有一個很熱門的詞彙,就是混沌工程。
混沌工程對大多數人來說並不是一個新鮮詞彙,可以說它是在單機應用轉向集羣應用,面向系統編程轉向到面向服務編程的必然結果,我們已經看到很多互聯網應用聲稱在混沌工程的幫助下提高了系統的穩定性如何如何,那麼對於基礎架構軟件呢?
在一定程度上可以說 ZStack 很早就開始在用混沌工程的思想測試系統的穩定性,首先我們有三個關鍵性的外部整體測試:
- MTBF,這個概念一般見於硬件設備,指的是系統的正常運行的時間,對我們來說會在系統上根據用戶場景反覆操作存儲(創建、刪除虛擬機,創建、刪除快照,寫入、刪除數據等)在此之上引入故障檢查正確性;
- DPMO,這個是一個測試界很老的概念,偏向於單個操作的反覆操作,例如重啓 1000 次物理機,添加刪除 10000 次鏡像等等,在這之上再考慮同時引入故障來考察功能的正確性;
- Woodpecker,這是 ZStack 從最開始就實現的測試框架,代碼和原理都是開源的,它會智能的組合 ZStack 的上千個 API自動找到可以持續下去的一條路徑,根據資源當前的狀態判斷資源可以執行的 API,這樣一天下來可以組合執行數萬次乃至上百萬次,與此同時再考慮引入錯誤。
上面這些方法,在大量調用 API、測試 IO 之外,很重要的一點就是注入錯誤,例如強制關閉虛擬機、物理機,通過可編程 PDU 模擬斷電等等,但是這些方法有一些缺陷:
- 複雜場景的模擬能力有限,例如有些客戶存儲並不是一直 IO 很慢,而是呈現波峯波谷的波浪型,這種情況和 IO 始終有明顯 delay 是有比較大的區別的;
- 不夠靈活,例如有的客戶存儲隨機 IO 很差但順序 IO 性能卻還可以,也不是簡單的降低 IO 性能就可以模擬的。
總之大部分混沌工程所提供的手段(隨機關閉節點、隨機殺進程、通過 tc 增加延時和 iproute2、iptables 改變網絡等等)並不能滿足 ZStack 的完全模擬用戶場景的需求。
在這種情況下,我們將擴展手段放在了幾個方向上:
-
libfiu,libfiu 可以通過 LD_PRELOAD 來控制應用調用 POSIX API 的結果,可以讓應用申請內存失敗、打開文件失敗,或者執行 open 失敗。
使用 fiurun + fiuctl 可以對某個應用在需要的時刻控制系統調用。
fiu 對注入 libaio 沒有直接提供支持,但好在 fio 擴展和編譯都極爲簡單,因此我們可以輕鬆的根據自己的需求增加 module。 -
systemtap,systemtap 是系統界的經典利器了,可以對內核函數的返回值根據需求進行修改,對內核理解很清晰的話,systemtap 會很好用,如果是對存儲進行錯誤注入,可以重點搜 scsi 相關的函數,以及參考這裏:Kernel Fault injection framework using SystemTap
-
device-mapper,device-mapper 提供了 dm-flakey、dm-dust、dm-delay,當然你也可以寫自己的 target,然後可以搭配 lio 等工具就可以模擬一個 faulty 的共享存儲,得益於 device-mapper 的動態加載,我們可以動態的修改 target 和參數,從而更真實的模擬用戶場景下的狀態;
-
nbd,nbd 的 plugin 機制非常便捷,我們可以利用這一點來修改每個 IO 的行爲,從而實現出一些特殊的 IO pattern,舉例來說,我們就用 nbd 模擬過用戶的順序寫很快但隨機寫異常慢的存儲設備;
-
此外,還有 scsi_debug 等 debug 工具,但這些比較面向特定問題,就不細說了。
上面兩張圖對這些錯誤注入手段做了一些總結,從系統角度來看,如果我們在設計階段能夠驗證算法的正確性,在開發時注意開發可測試的代碼,通過海量測試和錯誤注入將路徑完整覆蓋,對遇到的各種 IO 異常通過測試 case 固化下來,我們的存儲系統一定會是越來越穩定,持續的走在“正確”的道路上的。