Categories
程式開發

從零開始入門 K8s:Kubernetes 存儲架構及插件使用


容器存儲是 Kubernetes 系統中提供數據持久化的基礎組件,是實現有狀態服務的重要保證。 Kubernetes 默認提供了主流的存儲卷接入方案(In-Tree),同時也提供了插件機制(Out-Of-Tree),允許其他類型的存儲服務接入 Kubernetes 系統服務。本文將從 Kubernetes 存儲架構、存儲插件原理、實現等方面進行講解,希望大家有所收穫。

一、Kubernetes 存儲體系架構

引例: 在 Kubernetes 中掛載一個 Volume

首先以一個 Volume 的掛載例子來作為引入。

如下圖所示,左邊的 YAML 模板定義了一個 StatefulSet 的一個應用,其中定義了一個名為 disk-pvc 的 volume,掛載到 Pod 內部的目錄是 /data。 disk-pvc 是一個 PVC 類型的數據卷,其中定義了一個 storageClassName。

因此這個模板是一個典型的動態存儲的模板。右圖是數據卷掛載的過程,主要分為 6 步:

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 1

  • 第一步:用戶創建一個包含 PVC的 Pod;

  • 第二步:PV Controller 會不斷觀察 ApiServer,如果它發現一個 PVC 已經創建完畢但仍然是未綁定的狀態,它就會試圖把一個 PV 和 PVC 綁定;

PV Controller 首先會在集群內部找到一個適合的 PV 進行綁定,如果未找到相應的 PV,就調用 Volume Plugin 去做 Provision。 Provision 就是從遠端上一個具體的存儲介質創建一個 Volume,並且在集群中創建一個 PV 對象,然後將此 PV 和 PVC 進行綁定;

  • 第三步:通過 Scheduler 完成一個調度功能;

我們知道,當一個 Pod 運行的時候,需要選擇一個 Node,這個節點的選擇就是由 Scheduler 來完成的。 Scheduler 進行調度的時候會有多個參考量,比如 Pod 內部所定義的 nodeSelector、nodeAffinity 這些定義以及 Volume 中所定義的一些標籤等。

我們可以在數據卷中添加一些標籤,這樣使用這個 pv 的 Pod 就會由於標籤的限制,被調度器調度到期望的節點上。

  • 第四步:如果有一個Pod 調度到某個節點之後,它所定義的PV 還沒有被掛載(Attach),此時AD Controller 就會調用VolumePlugin,把遠端的Volume 掛載到目標節點中的設備上(如:/dev/vdb);

  • 第五步:當Volum Manager 發現一個Pod 調度到自己的節點上並且Volume 已經完成了掛載,它就會執行mount 操作,將本地設備(也就是剛才得到的/dev/vdb)掛載到Pod 在節點上的一個子目錄中。同時它也可能會做一些像格式化、是否掛載到 GlobalPath 等這樣的附加操作。

  • 第六步:綁定操作,就是將已經掛載到本地的 Volume 映射到容器中。

Kubernetes 的存儲架構

接下來,我們一起看一下 Kubernetes 的存儲架構。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 2

  • PV Controller: 負責 PV/PVC 的綁定、生命週期管理,並根據需求進行數據卷的 Provision/Delete 操作;

  • AD Controller:負責存儲設備的 Attach/Detach 操作,將設備掛載到目標節點;

  • Volume Manager:管理卷的 Mount/Unmount 操作、卷設備的格式化以及掛載到一些公用目錄上的操作;

  • Volume Plugins:它主要是對上面所有掛載功能的實現;

PV Controller、AD Controller、Volume Manager 主要是進行操作的調用,而具體操作則是由 Volume Plugins 實現的。

  • Scheduler:實現對 Pod 的調度能力,會根據一些存儲相關的的定義去做一些存儲相關的調度;

接下來,我們分別介紹上面這幾部分的功能。

PV Controller

首先我們先來回顧一下幾個基本概念:

  • Persistent Volume (PV): 持久化存儲卷,詳細定義了預掛載存儲空間的各項參數;

例如,我們去掛載一個遠端的 NAS 的時候,這個 NAS 的具體參數就要定義在 PV 中。一個 PV 是沒有 NameSpace 限制的,它一般由 Admin 來創建與維護;

  • Persistent Volume Claim (PVC):持久化存儲聲明;

它是用戶所使用的存儲接口,對存儲細節無感知,主要是定義一些基本存儲的 Size、AccessMode 參數在裡面,並且它是屬於某個 NameSpace 內部的。

  • StorageClass:存儲類;

一個動態存儲卷會按照 StorageClass 所定義的模板來創建一個 PV,其中定義了創建模板所需要的一些參數和創建 PV 的一個 Provisioner(就是由誰去創建的)。

PV Controller 的主要任務就是完成PV、PVC 的生命週期管理,比如創建、刪除PV 對象,負責PV、PVC 的狀態遷移;另一個任務就是綁定PVC 與PV 對象,一個PVC 必須和一個PV 綁定後才能被應用使用,它們是一一綁定的,一個PV 只能被一個PVC 綁定,反之亦然。

接下來,我們看一下一個 PV 的狀態遷移圖。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 3

創建好一個PV 以後,我們就處於一個Available 的狀態,當一個PVC 和一個PV 綁定的時候,這個PV 就進入了Bound 的狀態,此時如果我們把PVC 刪掉,Bound 狀態的PV 就會進入Released 的狀態。

一個 Released 狀態的 PV 會根據自己定義的 ReclaimPolicy 字段來決定自己是進入一個 Available 的狀態還是進入一個 Deleted 的狀態。如果 ReclaimPolicy 定義的是 “recycle” 類型,它會進入一個 Available 狀態,如果轉變失敗,就會進入 Failed 的狀態。

相對而言,PVC 的狀態遷移圖就比較簡單。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 4

一個創建好的 PVC 會處於 Pending 狀態,當一個 PVC 與 PV 綁定之後,​​PVC 就會進入 Bound 的狀態,當一個 Bound 狀態的 PVC 的 PV 被刪掉之後,該 PVC 就會進入一個 Lost 的狀態。對於一個 Lost 狀態的 PVC,它的 PV 如果又被重新創建,並且重新與該 PVC 綁定之後,​​該 PVC 就會重新回到 Bound 狀態。

下圖是一個 PVC 去綁定 PV 時對 PV 篩選的一個流程圖。就是說一個 PVC 去綁定一個 PV 的時候,應該選擇一個什麼樣的 PV 進行綁定。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 5

  • 首先它會檢查 VolumeMode 這個標籤,PV 與 PVC 的 VolumeMode 標籤必須相匹配。 VolumeMode 主要定義的是我們這個數據卷是文件系統 (FileSystem) 類型還是一個塊 (Block) 類型;

  • 第二個部分是 LabelSelector。當 PVC 中定義了 LabelSelector 之後,我們就會選擇那些有 Label 並且與 PVC 的 LabelSelector 相匹配的 PV 進行綁定;

  • 第三個部分是 StorageClassName 的檢查。如果 PVC 中定義了一個 StorageClassName,則必須有此相同類名的 PV 才可以被篩選中。

這裡再具體解釋一下StorageClassName 這個標籤,該標籤的目的就是說,當一個PVC 找不到相應的PV 時,我們就會用該標籤所指定的StorageClass 去做一個動態創建PV 的操作,同時它也是一個綁定條件,當存在一個滿足該條件的PV 時,就會直接使用現有的PV,而不再去動態創建。

  • 第四個部分是 AccessMode 檢查。

AccessMode 就是平時我們在 PVC 中定義的如 “ReadWriteOnce”、“RearWriteMany” 這樣的標籤。該綁定條件就是要求 PVC 和 PV 必須有匹配的 AccessMode,即 PVC 所需求的 AccessMode 類型,PV 必須具有。

  • 最後一個部分是 Size 的檢查。

一個 PVC 的 Size 必須小於等於 PV 的 Size,這是因為 PVC 是一個聲明的 Volume,實際的 Volume 必須要大於等於聲明的 Volume,才能進行綁定。

接下來,我們看一個 PV Controller 的一個實現。

PV Controller 中主要有兩個實現邏輯:一個是 ClaimWorker;一個是 VolumeWorker。

ClaimWorker 實現的是 PVC 的狀態遷移。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 6

通過系統標籤 “pv.kubernetes.io/bind-completed” 來標識一個 PVC 的狀態。

  • 如果該標籤為 True,說明我們的 PVC 已經綁定完成,此時我們只需要去同步一些內部的狀態;

  • 如果該標籤為 False,就說明我們的 PVC 處於未綁定狀態。

這個時候就需要檢查整個集群中的 PV 去進行篩選。通過 findBestMatch 就可以去篩選所有的 PV,也就是按照之前提到的五個綁定條件來進行篩選。如果篩選到 PV,就執行一個 Bound 操作,否則就去做一個 Provision 的操作,自己去創建一個 PV。

再看 VolumeWorker 的操作。它實現的則是 PV 的狀態遷移。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 7

通過PV 中的ClaimRef 標籤來進行判斷,如果該標籤為空,就說明該PV 是一個Available 的狀態,此時只需要做一個同步就可以了;如果該標籤非空,這個值是PVC 的一個值,我們就會去集群中查找對應的PVC。如果存在該PVC,就說明該PV 處於一個Bound 的狀態,此時會做一些相應的狀態同步;如果找不到該PVC,就說明該PV 處於一個綁定過的狀態,相應的PVC 已經被刪掉了,這時PV 就處於一個Released 的狀態。此時再根據 ReclaimPolicy 是否是 Delete 來決定是刪掉還是只做一些狀態的同步。

以上就是 PV Controller 的簡要實現邏輯。

AD Controller

AD Controller 是 Attach/Detach Controller 的一個簡稱。

它有兩個核心對象,即 DesiredStateofWorld 和 ActualStateOfWorld。

  • DesiredStateofWorld 是集群中預期要達到的數據卷的掛載狀態;

  • ActualStateOfWorld 則是集群內部實際存在的數據卷掛載狀態。

它有兩個核心邏輯,desiredStateOfWorldPopulator 和 Reconcile。

  • desiredStateOfWorldPopulator 主要是用來同步集群的一些數據以及DSW、ASW 數據的更新,它會把集群裡面,比如說我們創建一個新的PVC、創建一個新的Pod 的時候,我們會把這些數據的狀態同步到DSW 中;

  • Reconcile 則會根據 DSW 和 ASW 對象的狀態做狀態同步。它會把 ASW 狀態變成 DSW 狀態,在這個狀態的轉變過程中,它會去執行 Attach、Detach 等操作。

下面這個表分別給出了 desiredStateOfWorld 以及 actualStateOfWorld 對象的一個具體例子。

  • desiredStateOfWorld 會對每一個 Worker 進行定義,包括 Worker 所包含的 Volume 以及一些試圖掛載的信息;

  • actualStateOfWorl 會把所有的 Volume 進行一次定義,包括每一個 Volume 期望掛載到哪個節點上、掛載的狀態是什麼樣子的等等。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 8

下圖是 AD Controller 實現的邏輯框圖。

從中我們可以看到,AD Controller 中有很多 Informer,Informer 會把集群中的 Pod 狀態、PV 狀態、Node 狀態、PVC 狀態同步到本地。

在初始化的時候會調用 populateDesireStateofWorld 以及 populateActualStateofWorld 將 desireStateofWorld、actualStateofWorld 兩個對象進行初始化。

在執行的時候,通過 desiredStateOfWorldPopulator 進行數據同步,即把集群中的數據狀態同步到 desireStateofWorld 中。 reconciler 則通過輪詢的方式把 actualStateofWorld 和 desireStateofWorld 這兩個對象進行數據同步,在同步的時候,會通過調用 Volume Plugin 進行 attach 和 detach 操作,同時它也會調用 nodeStatusUpdater 對 Node 的狀態進行更新。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 9

以上就是 AD Controller 的簡要實現邏輯。

Volume Manager

Volume Manager 實際上是 Kubelet 中一部分,是 Kubelet 中眾多 Manager 的一個。它主要是用來做本節點 Volume 的 Attach/Detach/Mount/Unmount 操作。

它和 AD Controller 一樣包含有 desireStateofWorld 以及 actualStateofWorld,同時還有一個 volumePluginManager 對象,主要進行節點上插件的管理。在核心邏輯上和 AD Controller 也類似,通過 desiredStateOfWorldPopulator 進行數據的同步以及通過 Reconciler 進行接口的調用。

這裡我們需要講一下 Attach/Detach 這兩個操作:

之前我們提到 AD Controller 也會做 Attach/Detach 操作,所以到底是由誰來做呢?我們可以通過 “–enable-controller-attach-detach” 標籤進行定義,如果它為 True,則由 AD Controller 來控制;若為 False,就由 Volume Manager 來做。

它是Kubelet 的一個標籤,只能定義某個節點的行為,所以如果假設一個有10 個節點的集群,它有5 個節點定義該標籤為False,說明這5 個節點是由節點上的Kubelet 來做掛載,而其它5 個節點是由AD Controller 來做掛載。

下圖是 Volume Manager 實現邏輯圖。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 10

我們可以看到,最外層是一個循環,內部則是根據不同的對象,包括 desireStateofWorld, actualStateofWorld 的不同對像做一個輪詢。

例如,對 actualStateofWorld 中的 MountedVolumes 對像做輪詢,對其中的某一個 Volume,如果它同時存在於 desireStateofWorld,這就說明實際的和期望的 Volume 均是處於掛載狀態,因此我們不會做任何處理。如果它不存在於 desireStateofWorld,說明期望狀態中該 Volume 應該處於 Umounted 狀態,就執行 UnmountVolume,將其狀態轉變為 desireStateofWorld 中相同的狀態。

所以我們可以看到:實際上,該過程就是根據 desireStateofWorld 和 actualStateofWorld 的對比,再調用底層的接口來執行相應的操作,下面的 desireStateofWorld.UnmountVolumes 和 actualStateofWorld.AttachedVolumes 的操作也是同樣的道理。

Volume Plugins

我們之前提到的 PV Controller、AD Controller 以及 Volume Manager 其實都是通過調用 Volume Plugin 提供的接口,比如 Provision、Delete、Attach、Detach 等去做一些 PV、PVC 的管理。而這些接口的具體實現邏輯是放在 VolumePlugin 中的

根據源碼的位置可將 Volume Plugins 分為 In-Tree 和 Out-of-Tree 兩類:

  • In-Tree 表示源碼是放在 Kubernetes 內部的,和 Kubernetes 一起發布、管理與迭代,缺點及時迭代速度慢、靈活性差;

  • Out-of-Tree 類的 Volume Plugins 的代碼獨立於 Kubernetes,它是由存儲商提供實現的,目前主要有 Flexvolume 和 CSI 兩種實現機制,可以根據存儲類型實現不同的存儲插件。所以我們比較推崇 Out-of-Tree 這種實現邏輯。

從位置上我們可以看到,Volume Plugins 實際上就是 PV Controller、AD Controller 以及 Volume Manager 所調用的一個庫,分為 In-Tree 和 Out-of-Tree 兩類 Plugins。它通過這些實現來調用遠端的存儲,比如說掛載一個NAS 的操作“mount -t nfs ***”,該命令其實就是在Volume Plugins 中實現的,它會去調用遠程的一個存儲掛載到本地。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 11

從類型上來看,Volume Plugins 可以分為很多種。 In-Tree 中就包含了幾十種常見的存儲實現,但一些公司的自己定義私有類型,有自己的API 和參數,公共存儲插件是無法支持的,這時就需要Out-of-Tree 類的存儲實現,比如CSI、FlexVolume。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 12

Volume Plugins 的具體實現會放到後面去講。這裡主要看一下 Volume Plugins 的插件管理。

Kubernetes會在 PV Controller、AD Controller 以及 Volume Manager 中來做插件管理。通過 VolumePlguinMg 對象進行管理。主要包含 Plugins 和 Prober 兩個數據結構。

Plugins 主要是用來保存Plugins 列表的一個對象,而Prober 是一個探針,用於發現新的Plugin,比如FlexVolume、CSI 是擴展的一種插件,它們是動態創建和生成的,所以一開始我們是無法預知的,因此需要一個探針來發現新的Plugin。

下圖是插件管理的整個過程。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 13

PV Controller、AD Controller 以及 Volume Manager 在啟動的時候會執行一個 InitPlugins 方法來對 VolumePluginsMgr 做一些初始化。

它首先會將所有 In-Tree 的 Plugins 加入到我們的插件列表中。同時會調用Prober 的init 方法,該方法會首先調用一個InitWatcher,它會時刻觀察著某一個目錄(比如圖中的/usr/libexec/kubernetes/kubelet-plugins/volume/exec/),當這個目錄每生成一個新文件的時候,也就是創建了一個新的Plugins,此時就會生成一個新的FsNotify.Create 事件,並將其加入到EventsMap 中;同理,如果刪除了一個文件,就生成一個FsNotify .Remove 事件加入到EventsMap 中。

當上層調用 refreshProbedPlugins 時,Prober 就會把這些事件進行一個更新,如果是 Create,就將其添加到插件列表;如果是 Remove,就從插件列表中刪除一個插件。

以上就是 Volume Plugins 的插件管理機制。

Kubernetes 存儲卷調度

我們之前說到 Pod 必須被調度到某個 Worker 上才能去運行。在調度 Pod 時,我們會使用不同的調度器來進行篩選,其中有一些與 Volume 相關的調度器。例如 VolumeZonePredicate、VolumeBindingPredicate、CSIMaxVolumLimitPredicate 等。

VolumeZonePredicate 會檢查PV 中的Label,比如failure-domain.beta.kubernetes.io/zone 標籤,如果該標籤定義了zone 的信息,VolumeZonePredicate 就會做相應的判斷,即必須符合相應的zone 的節點才能被調度。

比如下圖左側的例子,定義了一個 label 的 zone 為 cn-shenzhen-a。右側的 PV 則定義了一個 nodeAffinity,其中定義了 PV 所期望的節點的 Label,該 Label 是通過 VolumeBindingPredicate 進行篩選的。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 14

二、Flexvolume 介紹及使用

Flexvolume 是 Volume Plugins 的一個擴展,主要實現 Attach/Detach/Mount/Unmount 這些接口。我們知道這些功能本是由 Volume Plugins 實現的,但是對於某些存儲類型,我們需要將其擴展到 Volume Plugins 以外,所以我們需要把接口的具體實現放到外面。

在下圖中我們可以看到,Volume Plugins 其實包含了一部分 Flexvolume 的實現代碼,但這部分代碼其實只有一個 “Proxy”的功能。

比如當 AD Controller 調用插件的一個 Attach 時,它首先會調用 Volume Plugins 中 Flexvolume 的 Attach 接口,但這個接口只是把調用轉到相應的 Flexvolume 的Out-Of-Tree實現上。

Flexvolume是可被 Kubelet 驅動的可執行文件,每一次調用相當於執行一次 shell 的 ls 這樣的腳本,都是可執行文件的命令行調用,因此它不是一個常駐內存的守護進程。

Flexvolume 的 Stdout 作為 Kubelet 調用的返回結果,這個結果需要是 JSON 格式。

Flexvolume默認的存放地址為 “/usr/libexec/kubernetes/kubelet-plugins/volume/exec/alicloud~disk/disk”。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 15

下面是一個命令格式和調用的實例。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 16

Flexvolume 的接口介紹

Flexvolum 包含以下接口:

  • init: 主要做一些初始化的操作,比如部署插件、更新插件的時候做 init 操作,返回的時候會返回剛才我們所說的 DriveCapabilities 類型的數據結構,用來說明我們的 Flexvolume 插件有哪些功能;

  • GetVolumeName: 返回插件名;

  • Attach: 掛載功能的實現。根據 –enable-controller-attach-detach 標籤來決定是由 AD Controller 還是 Kubelet 來發起掛載操作;

  • WaitforAttach: Attach 經常是異步操作,因此需要等待掛載完成,才能需要進行下面的操作;

  • MountDevice:它是 mount 的一部分。這裡我們將 mount 分為 MountDevice 和 SetUp 兩部分,MountDevice 主要做一些簡單的預處理工作,比如將設備格式化、掛載到 GlobalMount 目錄中等;

  • GetPath:獲取每個 Pod 對應的本地掛載目錄;

  • Setup:使用 Bind 方式將 GlobalPath 中的設備掛載到 Pod 的本地目錄;

  • TearDownUnmountDeviceDetach 實現的是上面一些藉口的逆過程;

  • ExpandVolumeDevice:擴容存儲卷,由 Expand Controller 發起調用;

  • NodeExpand: 擴容文件系統,由 Kubelet 發起調用。

上面這些接口不一定需要全部實現,如果某個接口沒有實現的話,可以將返回結果定義成:

{

    "status": "Not supported",

    "message": "error message"

}

告訴調用者沒有實現這個接口。此外,Volume Plugins 中的 Flexvolume 接口除了作為一個 Proxy 外,它也提供了一些默認實現,比如 Mount 操作。所以如果你的 Flexvolume 中沒有定義該接口,該默認實現就會被調用。

在定義 PV 時可以通過 secretRef 字段來定義一些 secret 的功能。比如掛載時所需的用戶名和密碼,就可以通過 secretRef 傳入。

Flexvolume 的掛載分析

從掛載流程和卸載流程兩個方向來分析 Flexvolume 的掛載過程。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 17

我們首先看 Attach 操作,它調用了一個遠端的 API 把我們的 Storage 掛載到目標節點中的某個設備上去。然後通過 MountDevice 將本地設備掛載到 GlobalPath 中,同時也會做一些格式化這樣的操作。 Mount 操作(SetUp),它會把 GlobalPath 掛載 PodPath 中,PodPath 就是 Pod 啟動時所映射的一個目錄。

下圖給出了一個例子,比如我們一個雲盤,其 Volume ID 為 d-8vb4fflsonz21h31cmss,在執行完 Attach 和 WaitForAttach 操作之後,就會將其掛載到目標節點上的 /dec/vdc 設備中。執行 MountDevice 之後,就會把上述設備格式化,掛載到一個本地的 GlobalPath 中。而執行完 Mount 之後,就會將 GlobalPath 映射到 Pod 相關的一個子目錄中。最後執行 Bind 操作,將我們的本地目錄映射到容器中。這樣完成一次掛載過程。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 18

卸載流程就是一個逆過程。上述過程描述的是一個塊設備的掛載過程,對於文件存儲類型,就無需 Attach、MountDevice操作,只需要 Mount 操作,因此文件系統的 Flexvolume 實現較為簡單,只需要 Mount 和 Unmount 過程即可。

Flexvolume 的代碼示例

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 19

其中主要實現的是 init()、doMount()、doUnmount() 方法。在執行該腳本的時候對傳入的參數進行判斷來決定執行哪一個命令。

在 Github 上還有很多 Flexvolume 的示例,大家可以自行參考查閱。阿里雲提供了一個 Flexvolume 的實現,有興趣的可以參考一下。

Flexvolume 的使用

下圖給出了一個 Flexvolume 類型的 PV 模板。它和其它模板實際上沒有什麼區別,只不過類型被定義為 flexVolume 類型。 flexVolume 中定義了 driver、fsType、options。

  • driver 定義的是我們實現的某種驅動,比如圖中的是 aliclound/disk,也可以是 aliclound/nas 等;

  • fsType 定義的是文件系統類型,比如 “ext4”;

  • options 包含了一些具體的參數,比如定義雲盤的 id 等。

我們也可以像其它類型一樣,通過 selector 中的 matchLabels 定義一些篩選條件。同樣也可以定義一些相應的調度信息,比如定義 zone 為 cn-shenzhen-a。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 20

下面是一個具體的運行結果。在 Pod 內部我們掛載了一個雲盤,其所在本地設備為 /dev/vdb。通過mount | grep disk 我們可以看到相應的掛載目錄,首先它會將/dev/vdb 掛載到GlobalPath 中;其次會將GlobalPath 通過mount 命令掛載到一個Pod 所定義的本地子目錄中去;最後會把該本地子目錄映射到/data 上。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 21

三、CSI 介紹及使用

和 Flexvolume 類似,CSI 也是為第三方存儲提供數據卷實現的抽象接口。

有了 Flexvolume,為何還要 CSI 呢?

Flexvolume 只是給 kubernetes 這一個編排系統來使用的,而 CSI 可以滿足不同編排系統的需求,比如 Mesos,Swarm。

其次 CSI 是容器化部署,可以減少環境依賴,增強安全性,豐富插件的功能。我們知道,Flexvolume 是在host 空間一個二進製文件,執行Flexvolum 時相當於執行了本地的一個shell 命令,這使得我們在安裝Flexvolume 的時候需要同時安裝某些依賴,而這些依賴可能會對客戶的應用產生一些影響。因此在安全性上、環境依賴上,就會有一個不好的影響。

同時對於豐富插件功能這一點,我們在Kubernetes 生態中實現operator 的時候,經常會通過RBAC 這種方式去調用Kubernetes 的一些接口來實現某些功能,而這些功能必須要在容器內部實現,因此像Flexvolume這種環境,由於它是host 空間中的二進製程序,就沒法實現這些功能。而 CSI 這種容器化部署的方式,可以通過 RBAC 的方式來實現這些功能。

CSI 主要包含兩個部分:CSI Controller Server 與 CSI Node Server。

  • Controller Server 是控制端的功能,主要實現創建、刪除、掛載、卸載等功能;

  • Node Server 主要實現的是節點上的 mount、Unmount 功能。

下圖給出了 CSI 接口通信的描述。 CSI Controller Server 和 External CSI SideCar 是通過 Unix Socket 來進行通信的,CSI Node Server 和 Kubelet 也是通過 Unix Socket 來通信,之後我們會講一下 External CSI SiderCar 的具體概念。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 22

下圖給出了 CSI 的接口。主要分為三類:通用管控接口、節點管控接口、中心管控接口。

  • 通用管控接口主要返回 CSI 的一些通用信息,像插件的名字、Driver 的身份信息、插件所提供的能力等;

  • 節點管控接口的 NodeStageVolume 和 NodeUnstageVolume 就相當於 Flexvolume 中的 MountDevice 和 UnmountDevice。 NodePublishVolume 和 NodeUnpublishVolume 就相當於 SetUp 和 TearDown 接口;

  • 中心管控接口的 CreateVolume 和 DeleteVolume 就是我們的 Provision 和 Delete 存儲卷的一個接口,ControllerPublishVolume 和 ControllerUnPublishVolume 則分別是 Attach 和 Detach 的接口。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 23

CSI 的系統結構

CSI 是通過 CRD 的形式實現的,所以 CSI 引入了這麼幾個對像類型:VolumeAttachment、CSINode、CSIDriver 以及 CSI Controller Server 與 CSI Node Server 的一個實現。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 24

在 CSI Controller Server 中,有傳統的類似 Kubernetes 中的 AD Controller 和 Volume Plugins,VolumeAttachment 對象就是由它們所創建的。

此外,還包含多個 External Plugin組件,每個組件和 CSI Plugin 組合的時候會完成某種功能。比如:

  • External Provisioner 和 Controller Server 組合的時候就會完成數據卷的創建與刪除功能;

  • External Attacher 和 Controller Server 組合起來可以執行數據卷的掛載和操作;

  • External Resizer 和 Controller Server 組合起來可以執行數據卷的擴容操作;

  • External Snapshotter 和 Controller Server 組合則可以完成快照的創建和刪除。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 25

CSI Node Server 中主要包含 Kubelet 組件,包括 VolumeManager 和 VolumePlugin,它們會去調用 CSI Plugin 去做 mount 和 unmount 操作;另外一個組件 Driver Registrar 主要實現的是 CSI Plugin 註冊的功能。

以上就是 CSI 的整個拓撲結構,接下來我們將分別介紹不同的對象和組件。

CSI 對象

我們將介紹 3 種對象:VolumeAttachment,CSIDriver,CSINode。

VolumeAttachment 描述一個 Volume 卷在一個 Pod 使用中掛載、卸載的相關信息。例如,對一個卷在某個節點上的掛載,我們通過 VolumeAttachment 對該掛載進行跟踪。 AD Controller 創建一個 VolumeAttachment,而 External-attacher 則通過觀察該 VolumeAttachment,根據其狀態來進行掛載和卸載操作。

下圖就是一個VolumeAttachment 的例子,其類別(kind) 為VolumeAttachment,spec 中指定了attacher 為ossplugin.csi.alibabacloud.com,即指定掛載是由誰操作的;指定了nodeName 為cn-zhangjiakou.192.168. 1.53,即該掛載是發生在哪個節點上的;指定了source 為persistentVolumeName 為oss-csi-pv,即指定了哪一個數據捲進行掛載和卸載。

status 中 attached 指示了掛載的狀態,如果是 False, External-attacher 就會執行一個掛載操作。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 26

第二個對像是 CSIDriver,它描述了集群中所部署的 CSI Plugin 列表,需要管理員根據插件類型進行創建。

例如下圖中創建了一些 CSI Driver,通過 kuberctl get csidriver 我們可以看到集群裡面創建的 3 種類型的 CSI Driver:一個是雲盤;一個是 NAS;一個是 OSS。

在 CSI Driver 中,我們定義了它的名字,在 spec 中還定義了 attachRequired 和 podInfoOnMount 兩個標籤。

  • attachRequired 定義一個 Plugin 是否支持 Attach 功能,主要是為了對塊存儲和文件存儲做區分。比如文件存儲不需要 Attach 操作,因此我們將該標籤定義為 False;

  • podInfoOnMount 則是定義 Kubernetes 在調用 Mount 接口時是否帶上 Pod 信息。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 27

第三個對像是 CSINode,它是集群中的節點信息,由 node-driver-registrar 在啟動時創建。它的作用是每一個新的 CSI Plugin 註冊後,都會在 CSINode 列表裡添加一個 CSINode 信息。

例如下圖,定義了 CSINode 列表,每一個 CSINode 都有一個具體的信息(左側的 YAML)。以 一 cn-zhangjiakou.192.168.1.49 為例,它包含一個雲盤的 CSI Driver,還包含一個 NAS 的 CSI Driver。每個 Driver 都有自己的 nodeID 和它的拓撲信息 topologyKeys。如果沒有拓撲信息,可以將 topologyKeys 設置為 “null”。也就是說,假如有一個有 10 個節點的集群,我們可以只定義一部分節點擁有 CSINode。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 28

CSI 組件之 Node-Driver-Registrar

Node-Driver-Registrar 主要實現了 CSI Plugin 註冊的一個機制。我們來看一下下圖中的流程圖。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 29

  • 第 1 步,在啟動的時候有一個約定,比如說在 /var/lib/kuberlet/plugins_registry 這個目錄每新加一個文件,就相當於每新加了一個 Plugin;

啟動 Node-Driver-Registrar,它首先會向 CSI-Plugin 發起一個接口調用 GetPluginInfo,這個接口會返回 CSI 所監聽的地址以及 CSI-Plugin 的一個 Driver name;

  • 第 2 步,Node-Driver-Registrar 會監聽 GetInfo 和 NotifyRegistrationStatus 兩個接口;

  • 第 3 步,會在 /var/lib/kuberlet/plugins_registry 這個目錄下啟動一個Socket,生成一個Socket 文件,例如:“diskplugin.csi.alibabacloud.com-reg.sock”,此時Kubelet 通過Watcher 發現這個Socket 後,它會通過該Socket 向Node-Driver-Registrar 的GetInfo 接口進行調用。 GetInfo 會把剛才我們所獲得的的 CSI-Plugin 的信息返回給 Kubelet,該信息包含了 CSI-Plugin 的監聽地址以及它的 Driver name;

  • 第 4 步,Kubelet 通過得到的監聽地址對 CSI-Plugin 的 NodeGetInfo 接口進行調用;

  • 第 5 步,調用成功之後,Kubelet 會去更新一些狀態信息,比如節點的 Annotations、Labels、status.allocatable 等信息,同時會創建一個 CSINode 對象;

  • 第 6 步,通過對 Node-Driver-Registrar 的 NotifyRegistrationStatus 接口的調用告訴它我們已經把 CSI-Plugin 註冊成功了。

通過以上 6 步就實現了 CSI Plugin 註冊機制。

CSI 組件之 External-Attacher

External-Attacher 主要是通過 CSI Plugin 的接口來實現數據卷的掛載與卸載功能。它通過觀察 VolumeAttachment 對象來實現狀態的判斷。 VolumeAttachment 對象則是通過 AD Controller 來調用 Volume Plugin 中的 CSI Attacher 來創建的。 CSI Attacher 是一個 In-Tree 類,也就是說這部分是 Kubernetes 完成的。

當 VolumeAttachment 的狀態是 False 時,External-Attacher 就去調用底層的一個 Attach 功能;若期望值為 False,就通過底層的 ControllerPublishVolume 接口實現 Detach 功能。同時,External-Attacher 也會同步一些 PV 的信息在裡面。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 30

CSI 部署

我們現在來看一下塊存儲的部署情況。

之前提到 CSI 的 Controller 分為兩部分,一個是 Controller Server Pod,一個是 Node Server Pod。

我們只需要部署一個 Controller Server,如果是多備份的,可以部署兩個。 Controller Server 主要是通過多個外部插件來實現的,比如說一個 Pod 中可以定義多個 External 的 Container 和一個包含 CSI Controller Server 的 Container,這時候不同的 External 組件會和 Controller Server 組成不同的功能。

而 Node Server Pod 是個 DaemonSet,它會在每個節點上進行註冊。 Kubelet 會直接通過 Socket 的方式直接和 CSI Node Server 進行通信、調用 Attach/Detach/Mount/Unmount 等。

Driver Registrar 只是做一個註冊的功能,會在每個節點上進行部署。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 31

文件存儲和塊存儲的部署情況是類似的。只不過它會把 Attacher 去掉,也沒有 VolumeAttachment 對象。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 32

CSI 使用示例

和 Flexvolume 一樣,我們看一下它的定義模板。

可以看到,它和其它的定義並沒什麼區別。主要的區別在於類型為 CSI,裡面會定義 driver,volumeHandle,volumeAttribute,nodeAffinity 等。

  • driver 就是定義是由哪一個插件來去實現掛載;

  • volumeHandle 主要是指示 PV 的唯一標籤;

  • volumeAttribute 用於附加參數,比如 PV 如果定義的是 OSS,那麼就可以在 volumeAttribute 定義 bucket、訪問的地址等信息在裡面;

  • nodeAffinity 則可以定義一些調度信息。與 Flexvolume 類似,還可以通過 selector 和 Label 定義一些綁定條件。

中間的圖給出了一個動態調度的例子,它和其它類型的動態調度是一樣的。只不過在定義 provisioner 的時候指定了一個 CSI 的 provisioner。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 33

下面給出了一個具體的掛載例子。

Pod 啟動之後,我們可以看到 Pod 已經把一個 /dev/vdb 掛載到 /data 上了。同理,它有一個 GlobalPath 和一個 PodPath 的集群在裡面。我們可以把一個 /dev/vdb 掛載到一個 GlobalPath 裡面,它就是一個 CSI 的一個 PV 在本節點上唯一確定的目錄。一個 PodPath 就是一個 Pod 所確定的一個本地節點的目錄,它會把 Pod 所對應的目錄映射到我們的容器中去。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 34

CSI 的其它功能

除了掛載、卸載之外,CSI 化提供了一些附加的功能。例如,在定義模板的時候往往需要一些用戶名和密碼信息,此時我們就可通過 Secret 來進行定義。之前我們所講的 Flexvolume 也支持這個功能,只不過 CSI 可以根據不同的階段定義不同的 Secret 類型,比如掛載階段的 Secret、Mount 階段的 Secret、Provision 階段的 Secret。

Topology 是一個拓撲感知的功能。當我們定義一個數據卷的時候,集群中並不是所有節點都能滿足該數據卷的需求,比如我們需要掛載不同的 zone 的信息在裡面,這就是一個拓撲感知的功能。這部分在第 10 講已有詳細的介紹,大家可以進行參考。

Block Volume 就是 volumeMode 的一個定義,它可以定義成 Block 類型,也可以定義成文件系統類型,CSI 支持 Block 類型的 Volume,就是說掛載到 Pod 內部時,它是一個塊設備,而不是一個目錄。

Skip Attach PodInfo On Mount 是剛才我們所講過的 CSI Driver 中的兩個功能。

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 35

CSI 的近期 Features

從零開始入門 K8s:Kubernetes 存儲架構及插件使用 36

CSI 還是一個比較新的實現方式。近期也有了很多更新,比如ExpandCSIVolumes 可以實現文件系統擴容的功能;VolumeSnapshotDataSource 可以實現數據卷的快照功能;VolumePVCDataSource 實現的是可以定義PVC 的數據源;我們以前在使用CSI 的時候只能通過PVC、PV 的方式定義,而不能直接在Pod 裡面定義Volume,CSIInlineVolume 則可以讓我們可以直接在Volume 中定義一些CSI 的驅動。

阿里雲在 GitHub 上開源了 CSI 的實現,大家有興趣的可以看一下,做一些參考。

總結

本文主要介紹了 Kubernetes 集群中存儲卷相關的知識,主要有以下三點內容:

  • 第一部分講述了 Kubernetes 存儲架構,主要包括存儲卷概念、掛載流程、系統組件等相關知識;

  • 第二部分講述了 Flexvolume 插件的實現原理、部署架構、使用示例等;

  • 第三部分講述了 CSI 插件的實現原理、資源對象、功能組件、使用示例等;

希望上述知識點能讓各位同學有所收穫,特別是在處理存儲卷相關的設計、開發、故障處理等方面有所幫助。

本文轉載自阿里巴巴雲原生微信公眾號(ID:Alicloudnative)。

相關閱讀:

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制

從零開始入門 K8s:調度器的調度流程和算法介紹

從零開始入門 K8s:Kubernetes 調度和資源管理

從零開始入門 K8s:etcd 性能優化實踐

從零開始入門 K8s:手把手帶你理解 etcd

從零開始入門 K8s:深入剖析 Linux 容器

從零開始入門 K8s:Kubernetes 中的服務發現與負載均衡

從零開始入門 K8s:Kubernetes 網絡概念及策略控制

從零開始入門 K8s:監控與日誌的可觀測性

從零開始入門 K8s:應用存儲和持久化數據卷:存儲快照與拓撲調度

從零開始入門 K8s:應用存儲和持久化數據卷的核心知識

從零開始入門 K8s:應用配置管理

從零開始入門 K8s:應用編排與管理:Job & DaemonSet

從零開始入門 K8s:應用編排與管理

從零開始入門 K8s:K8s 的應用編排與管理

從零開始入門 K8s:詳解 Pod 及容器設計模式

從零開始入門 K8s:詳解 K8s 容器基本概念

從零開始入門 K8s:詳解 K8s 核心概念