深入淺出聊聊Kubernetes儲存(二):搞定持久化儲存
回 顧
在本系列文章的上一篇中,我們講到了PV,PVC,Storage Class以及Provisioner
簡單回顧一下:
PV在最一開始是設計成了一個需要管理員預先分配的儲存塊。引入Storage Class和Provisioner之後,使用者可以動態地供應PV。
PVC是對PV的請求,當和Storage Class一起使用時,它將觸發與相匹配PV的動態供應。
PV和PVC總是一一對應的。
Provisioner是給使用者提供PV的外掛。它可以把管理員從為持久化建立工作負載的繁重角色中解脫出來。
Storage Class是PV的分類器。相同的Storage Class中的PV可以共享一些屬性。在大多數情況下,Storage Class和Provisioner一起使用時,可以把它當作具有預定義屬性的Provisioner。因此,當用戶請求它時,它能夠用這些預定義的屬性動態地提供PV。
不過上述這些只是在Kubernetes中使用持久化儲存的其中一種方法而已
Volume
在前一篇文章中,我們提到Kubernetes中還有一個卷(Volume)的概念。為了把Volume和持久卷(Persistent Volume)區分開,大家有時會稱它為In-line Volume或者Ephemeral Volume。
這裡我們引用Volume的定義:
Kubernetes Volume…有一個顯式的生命週期——這和包含它的pod的生命週期相同。因此,Volume的生命週期比在pod中執行的任何容器都長,並且在容器重啟的時候會儲存資料。當然,當Pod終止時,Volume也將終止。更重要的是,Kubernetes支援多種型別的Volume,一個pod中也可以同時使用任何數量的Volume。
在其核心部分,Volume只是一個目錄,可能其中包含了一些資料,這些資料可由pod中的容器訪問。這些目錄是如何產生的、支援它的介質、以及它的內容都是由所使用的特定volume的型別決定的。
Volume一個重要屬性是,它與所屬的pod具有相同的生命週期。如果pod消失了,它也會消失。這與Persistent Volume不同,因為Persistent Volume將繼續存在於系統中,直到使用者刪除它。Volume還可以在同一個pod中的容器間共享資料,不過這不是主要的用例,因為通常情況下使用者只會在每個pod中使用一個容器。
因此,這更可以把Volume看作是pod的屬性而不是一個獨立的物件。正如它的定義所說,Volume表示pod中的目錄,而Volume的型別定義了目錄中的內容。例如,Config Map Volume型別將會在Volume目錄中從API伺服器建立配置檔案;PVC Volume型別將從目錄中相應的PV裡掛在檔案系統等等。實際上,Volume幾乎是在pod中本地使用儲存的唯一方法。
Volume、Persistent Volume和持久卷宣告(Persistent Volume Claim)之間很容易弄混淆。假設有一個數據流,它是這樣PV->PVC->Volume。PV包含了真實資料,繫結到PVC上,最終變成pod中的Volume。
然而,除了PVC,Volume還可以由Kubernetes直接支援的各種型別的儲存庫支援,從這個意義上來說,Volume的定義也挺令人困惑的。
我們需要知道的事,我們已經有了Persistent Volume,它支援不同型別的儲存解決方案。我們還有Provisioner,它支援類似(並不完全相同)的解決方案。而且我們還有不同型別的Volume。
那麼,它們到底有什麼不同呢?如何在它們之間選擇?
持久化資料的多種方式
以AWS EBS為例。讓我們來細數Kubernetes中的持久化資料方式吧。
Volume方式
awsElasticBlockStore是一個Volume型別。
你可以建立一個Pod,定義一個awsElasticBlockStore型別的volume,設定好volumeID,接著使用pod中存在的EBS volume。
該EBS volume在直接和Volume使用前必須已經存在。
PV方式
AWSElasticBlockStore還是一個PV型別。
所以你可以建立一個PV,用它來表示EBS volume(假設你有這樣的許可權),然後建立一個和它繫結的PVC卷。最後,令PVC作為volume,然後就可以在pod中使用它了。
和Volume方法類似,EBS volume在建立PV之前就必須存在。
Provisioner方式
kubernetes.io/aws-ebs是一個Kubernetes中用於EBS的內建Provisioner。
你可以用Provisioner kubernetes.io/aws-ebs來建立一個Storage Class,通過Storage Class建立PVC。Kubernetes會自動為你建立相對應的PV。接下來指定PVC為volume就可以在pod中使用了。
在本用例中,你不需要在使用使用之前建立EBS,EBS Provisioner會為你建立的。
第三方方式
上面列出的都是Kubernetes內建選項,如果你不太滿意的話,其實還有一些使用Flexvolume driver格式的第三方EBS實現,它們可以幫助你和Kubernetes連線起來。
如果Flexvolume不適合你,還可以使用具備同樣功能的CSI drivers(為什麼這麼說?稍後會對此進行詳細介紹)
VolumeClaimTemplate方式
如果你在使用StatefulSet,那麼恭喜你!你現在有額外多了一種使用工作負載中EBS的方式——VolumeClaimTemple。
VolumeClaimTemple是StatefulSet規範屬性,它為StatefulSet所建立的Pod提供了建立匹配PV和PVC的方式。這些PVC將通過Storage Class建立,這樣當StatefulSet擴充套件時就可以自動建立它們。當StatefulSet縮小時,多餘的PV/PVCs會保留在系統中。因此,當StatefulSet再一次擴充套件時,它們會再次作用於Kubernetes建立的新pods中。稍後我們會詳細講StatefulSet。
舉個例子說明,假設你用replica 3建立了一個名為www的StatefulSet,並用它建立了名為data的VolumeClaimTemplate。Kubernetes會建立3個pods,分別起名www-0、www-1、www-2。Kubernetes還會建立PVC,其中www-data-0用於pod www-0,www-data-1給www-1,www-data-2給www-2。如果你把StatefulSet擴充套件到5,Kubernetes就會分別建立www-3、www-data-3、www-4、www-data-4。如果接著將StatefulSet降為1,www-1到www-4全都會刪除,而www-data-1到www-data-4會保留在系統中。因此當你決定再次擴充套件到5的時候,pod www-1到www-4又回被創建出來,而PVC www-data-1仍然會服務於Pod www-1,www-data-2對應www-2,以此類推。這是因為StatefulSet中pod的身份在是stable的。使用StatefulSet時,名稱和關係都是可以預測的。
VolumeClaimTemple對於像EBS和Longhorn這樣的塊儲存解決方案非常重要。因為這些解決方案本質上是ReadWriteOnce,你不能在Pod之間共享它們。如果你有不止一個運行了持久化資料的pod,那麼就無法順利地進行部署。因此,VolumeClaimTemplate的出現為塊儲存解決方案提供了一種水平擴充套件Kubernetes工作負載的方式。
如何在Volume、Persistent Volume和Provisioner之間做出選擇
正如你所看到的,現在有了內建的Volume型別、PV型別、Provisioner型別、以及使用Flexvolume和/或CSI的外部外掛。讓人比較頭大的是,它們之間提供的功能基本相同,不過也有略微的區別。
我認為,至少應該有一個準則來確定如何在它們之間選擇。
但是我並沒有找到。
所以我翻遍了程式碼和文件,畫出了下面的比較表格,以及對我來說最有意義的準則,從Volume、Persistent Volume和Provisioner幾個方面進行對比。
這裡我只涉及到Kubernetes中in-tree所支援的,除此之外一些官方的out-of-tree的Provisioners:
https://github.com/kubernetes-incubator/external-storage
可以看到,Volume、Persistent Volume以及Provisioner在一些細微的地方還是不一樣的。
1. Volume支援大部分的volume外掛。
A.它是連線PVC和pod的唯一方法
B.它也是唯一一個支援Config Map、Secret、Downward API以及Projected的。這些所有都與Kubernetes API伺服器密切相關。
C.它還是唯一一個支援EmptyDir的,EmptyDir可以自動給pod分配和清理臨時volume。(注:早在2015年,Clayton Coleman就提出了一個關於支援EmptyDir的問題。這對於需要持久化儲存但只有本地卷可用的工作負載,這非常有用。可是這一觀點並沒有得到太多的關注。沒有scheduler的支援,這一目標在當時很難做到。而現在,在2018年,Kubernetes v1.11版本的Local Volume已經加入scheduler和PV的節點親和支援(node affinity support),但是仍然沒有EmptyDir PV。而且Local Volume特性並不是我所期望的那樣,因為它並不具備在節點上使用新目錄建立新卷的能力。因此,我編寫了Local Path Provisioner,它利用scheduler和PV節點親和更改,為工作負載提供動態的Host Path type PV。)
2. PV支援的外掛是Provisioner支援的超集,因為Provisioner需要在工作負載使用它之前建立PV。但是,還有一些PV支援而Provisioner不支援的外掛,比如Local Volume(正在進行修改中)。
3. 還有兩種型別Volume是不支援的。他們是兩個最新的特性:CSI和Local Volume,現在還有一些正在進行的工作,會在之後把它們用於Volume。
在Volume、Persistent Volume和Provisioner之間選擇的準則
那麼使用者到底應該選擇哪種方式呢?
在我看來,使用者們應該堅持一個原則:
在條件允許的情況下,選擇Provisioner而不是Persistent Volume,接著再是Volume。
詳細來說:
1. 對於Config Map、Downward API、Secret或者Projected,請使用Volume,因為PV不支援它們。
2. 對於EmptyDir,直接使用Volume,或者使用Host Path來代替。
3. 對於Host Path,通常是直接使用Volume,因為它繫結到一個特定的節點,並且節點之間它是同構的。
a. 如果你想用異構的Host Path Volume,它在Kubernetes v1.11版之後才能使用,因為之前缺少對PV的節點親和知識,使用v1.11+版本,你可以使用我的Local Path Provisioner建立帶有節點親和的Host Path PV:
https://github.com/rancher/local-path-provisioner。
4. 對於其他的情況,除非你需要和現有的卷掛鉤(這種情況下你應該使用PV),否則就使用Provisioner代替。有些Provisioner並不是內建的選項,但是你應該能在此連結(https://github.com/kubernetes-incubator/external-storage)或者供應商的官方倉庫中找到它們。
這個準則背後的原理很簡單。在Kubernetes內部進行操作時,物件(PV)比屬性(Volume)更容易管理,而且和手動建立PV相比,自動建立PV容易得多(Provisioner)。
不過這裡有一個例外:如果你喜歡在Kubernetes外面進行儲存,那麼最好使用Volume,儘管使用這種方式需要用到另一組API進行建立/刪除。此外,由於缺少VolumeClaimTemplate,會失去使用StatefulSet自動伸縮的能力。我不認為這是多數Kubernetes使用者會選擇的方式。
為什麼做同樣的事會有這麼多選項?
當我開始研究Kubernetes儲存時,首先想到的就是這個問題。由於缺乏一致性和直觀性,Kubernetes儲存看起來就像是事後才想到的。於是我試圖研究這些設計決策背後的歷史緣由,可是在2016之前都毫無收穫。
最後,我傾向於相信這些是由於一些早期的設計造成的,這可能是為獲取供應商支援的迫切需求,導致安排給Volume比原本更多的責任。在我看來,所有複製了PV的內建volume外掛都不應該存在。
在研究歷史的過程中,我發現在2016初發布的Kubernetes v1.2中,dynamic provisioning就已經成為了alpha特性。它需要兩個釋出版週期變成beta,在兩個週期實現穩定,這都是非常合理的。
SIG Storage(它推動了Kubernetes儲存開發)還進行了大量的工作,使用Provisioner和CSI將Volume外掛從tree中移出來。我認為這是朝著更加一致、更加精簡的系統邁出了一大步。
可另一方面,我也不認為這一大堆Volume型別會消失。這像是和矽谷非官方的格言唱反調:快速行動,打破常規。有時候,快速迭代的專案所遺留下來的設計,修改它們實在是太難了。我們只能和它們共處,在它們身邊小心工作,不要用錯誤的方式呼叫它們。
下一步
本系列的下一節中,我們將討論擴充套件Kubernetes儲存系統的機制,即Flexvolume和CSI。一個小小的提示:你可能注意到了,我並不是Flexvolume的粉絲,而且這不是儲存子系統的問題。