はじめに:ご存じのとおり、K8sの永続ストレージ(永続ストレージ)は、アプリケーションのデータがアプリケーションのライフサイクルとは独立して存在することを保証しますが、その内部実装についてはほとんど言及されていません。K8sの内部ストレージプロセスとは何でしょうか? PV、PVC、StorageClass、Kubelet、CSIプラグインなどの間の呼び出し関係は何でしょうか。これらの疑問は、この記事でひとつずつ明らかにされます。

K8s 永続的ストレージの基本

K8sストレージプロセスを説明する前に、まずK8sの永続ストレージの基本概念を確認します。

1. 用語集

  • in-treeコードロジックはK8sに組み込まれる形で開発されます。
  • out-of-treeコードロジックは、K8sコードからの分離を実現するために、K8の外で開発されます。
  • PVPersistentVolume,クラスターレベルのリソースは、クラスター管理者または外部プロビジョナーによって作成されます。PVのライフサイクルはPVを使用するPodとは無関係であり、ストレージデバイスの詳細はPVの.Specに保存されます。
  • PVCPersistentVolumeClaim,名前空間(namespace)レベルのリソースは、ユーザーまたはStatefulSetコントローラー(VolumeClaimTemplateに従って)によって作成されます。PVCはPodに似ています。Podはノードリソースを消費し、PVCはPVリソースを消費します。Podは特定のレベルのリソース(CPUとメモリー)を要求でき、PVCは特定のストレージボリュームサイズとアクセスモード(アクセスモード)を要求できます。
  • StorageClassStorageClassは、クラスター管理者が作成したクラスターレベルのリソースです。SCは、ストレージボリュームを動的に提供する「クラス」テンプレートを管理者に提供します。SCの.Specは、ストレージボリュームPVのさまざまなサービス品質レベル、バックアップポリシーなどを詳細に定義します。
  • CSIContainer Storage Interface,目的は業界標準の「コンテナストレージインターフェース」を定義し、ストレージベンダー(SP)をCSI標準に基づいて開発されたプラグインを使いさまざまなコンテナオーケストレーション(CO)システムで作業させるようにすることです。COシステムには、Kubernetes、Mesos、Swarmなどが含まれます。

2. コンポーネント紹介

  • PV Controller:PV / PVCバインディングおよびサイクル管理を担当し、要件に従ってデータボリュームのプロビジョニング/削除操作を実行します。
  • AD Controller:データボリュームのAttach / Detach操作を担当し、デバイスをターゲットノードに接続します。
  • Kubelet:Kubeletは、各ノードノードで実行される主要な「ノードエージェント」であり、その機能は、Podのライフサイクル管理、コンテナヘルスチェック、コンテナモニタリングなどです。
  • Volume Manager:Kubeletのコンポーネントは、データボリュームのマウント/アンマウント操作(データボリュームのアタッチ/デタッチ操作も担当します。この機能をオンにするには、kubelet関連のパラメーターを構成する必要があります)、ボリュームデバイスのフォーマットなどを管理します。
  • Volume Plugins:ストレージベンダーによって開発されたストレージプラグインは、さまざまなストレージタイプのボリューム管理機能を拡張し、サードパーティストレージのさまざまな運用機能を実装するように設計されています。ボリュームプラグインは、ツリー内とツリー外にあります。
  • External Provisioner:外部Provisionerは、ボリュームプラグインのCreateVolume関数とDeleteVolume関数を呼び出してプロビジョニング/削除操作を実行するサイドカーコンテナです。K8のPVコントローラーはボリュームプラグインの関連機能を直接呼び出すことができないため、gRPCを介して外部プロビジョナーによって呼び出されます。
  • External Attacher:External Attacherは一種のサイドカーコンテナです。その機能は、ボリュームプラグインのControllerPublishVolumeおよびControllerUnpublishVolume関数を呼び出して、アタッチ/デタッチ操作を実行することです。 K8s ADコントローラーはボリュームプラグインの関連機能を直接呼び出すことができないため、gRPCを介して外部アタッチメントによって呼び出されます。

3. 永続的なボリュームの使用

KubernetesはPVとPVCを導入して、アプリケーションとその開発者が通常ストレージリソースをリクエストし、ストレージファシリティの詳細の処理を回避できるようにしました。PVを作成するには2つの方法があります。

  • 1つは、クラスター管理者がアプリケーションに必要なPVを手動で静的に作成することです。
  • もう1つは、ユーザーが手動でPVCを作成し、対応するPVがProvisionerコンポーネントによって動的に作成されることです。

手順1:クラスター管理者がNFS PVを作成します。NFSは、K8sでネイティブにサポートされるツリー内ストレージタイプに属しています。
yaml ファイルは次のとおりです。


apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.4.1
    path: /nfs_storage

手順2:ユーザーがPVCを作成すると、yamlファイルは次のようになります。


apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

kubectl get pvコマンドは、PVとPVCがBoundされていることを示しています。


[root@huizhi ~]# kubectl get pvc
NAME      STATUS   VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nfs-pvc   Bound    nfs-pv-no-affinity   10Gi       RWO                           4s

手順3:ユーザーはアプリケーションを作成し、2番目のステップで作成したPVCを使用します。


apiVersion: v1
kind: Pod
metadata:
  name: test-nfs
spec:
  containers:
  - image: nginx:alpine
    imagePullPolicy: IfNotPresent
    name: nginx
    volumeMounts:
    - mountPath: /data
      name: nfs-volume
  volumes:
  - name: nfs-volume
    persistentVolumeClaim:
      claimName: nfs-pvc

このとき、NFSのリモートストレージはPodのnginxコンテナの/ dataディレクトリにマウントされます。

ストレージボリュームを動的に作成する

ストレージボリュームを動的に作成するには、nfs-client-provisionerと対応するストレージクラスをクラスターにデプロイする必要があります。 静的なストレージボリュームの作成と比較して、ストレージボリュームを動的に作成すると、クラスター管理者の介入が軽減されます。このプロセスを次の図に示します。

クラスター管理者は、環境にNFS関連のストレージクラスがあることを確認するだけで済みます。


kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: nfs-sc
provisioner: example.com/nfs
mountOptions:
  - vers=4.1

手順1:ユーザーはPVCを作成します。PVCのstorageClassNameは、上記のNFSのstorageclass名として指定されています。


kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: nfs
  annotations:
    volume.beta.kubernetes.io/storage-class: "example-nfs"
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Mi
  storageClassName: nfs-sc

手順2:クラスター内のnfs-client-provisionerは、対応するPVを動的に作成します。この時点で、環境内のPVが作成され、PVCにBoundされていることがわかります。


[root@huizhi ~]# kubectl get pv
NAME                                       CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM         REASON    AGE
pvc-dce84888-7a9d-11e6-b1ee-5254001e0c1b   10Mi        RWX           Delete          Bound       default/nfs             4s

手順3:ユーザーはアプリケーションを作成し、ストレージボリュームを静的に作成する3番目のステップと同じように、2番目のステップで作成されたPVCを使用します。

K8s 永続的なストレージプロセス

1. プロセスの概要

ここから学ぶ@郡宝 クラウドネイティブストレージコースの中のフローチャート です。

フローは以下の通りです。

  1. ユーザーは、動的ストレージボリュームを必要とするPVCを含むPodを作成します。
  2. Schedulerは、Pod構成、ノードのステータス、PV構成、およびその他の情報に基づいて、Podを適切なWorkerノードにスケジュールします。
  3. PVコントローラーは、Podが使用するPVCがPending状態であることを監視するため、ボリュームプラグイン(ツリー内)を呼び出してストレージボリュームを作成し、PVオブジェクトを作成します(ツリー外は外部プロビジョナーによって処理されます)。
  4. ADコントローラーは、PodとPVCが保留状態であることを検出するため、ボリュームプラグインを呼び出して、ストレージデバイスをターゲットWorkerノードにマウントします。
  5. Workerノードで、Kubeletのボリュームマネージャーはストレージデバイスがマウントされるのを待ち、ボリュームプラグインを介してデバイスをグローバルディレクトリにマウントします:/ var / lib / kubelet / pods / [pod uid] /volumes/kubernetes.io 〜iscsi / [PV名](例としてiscsiを使用)。
  1. KubeletはDockerを介してPodのコンテナを起動し、bind mountを使用して、ローカルグローバルディレクトリにマウントされているボリュームをコンテナにマップします。
    さらに詳細なフローは以下の通りです。

2. プロセス詳細

K8sのバージョンにより、永続的なストレージプロセスが少し異なります。この記事は、Kubernetesバージョン1.14.8に基づいています。

上記のフローチャートからわかるように、ストレージボリュームは作成からアプリケーション使用までの3つの段階に分かれています。
Provision/Delete、Attach/Detach、Mount/Unmount
provisioning volumes

PVコントローラーには2つのWorkerがあります。

  • ClaimWorker関連イベントのPVC追加/更新/削除およびPVC状態移行を処理します。
  • VolumeWorkerPVの状態移行を担当します。

PVステータスの移行(UpdatePVStatus):

  • PVの初期状態はAvailableであり、PVがPVCにBoundされると、状態がBoundに変わります。
  • PVにBoundされたPVCが削除されると、ステータスはReleasedに変わります。
  • PVリサイクルポリシーがRecycledになるか、PVの.Spec.ClaimRefを手動で削除すると、PVステータスがAvailableになります。
  • PVリサイクルポリシーが不明であるか、Recycleが失敗したか、ストレージボリュームの削除が失敗した場合、PVステータスはFailedになります。
  • PVの.Spec。ClaimRefを手動で削除すると、PVステータスがAvailableになります。

PVCステータスの移行(UpdatePVCStatus):

  • クラスターにPVC条件を満たすPVがない場合、PVCステータスはPendingです。 PVとPVCがBoundされると、PVCステータスがPendingからBoundに変わります。
  • PVCにBoundされたPVが環境内で削除され、PVCステータスが失われます。
  • 同じ名前のPVに再度Boundすると、PVCステータスはBound済みになります。
  • プロビジョニングプロセスは次の通りです(ここでは、疑似ユーザーが新しいPVCを作成します)。
  • 静的ストレージボリュームプロセス(FindBestMatch):PVコントローラーは、最初に、環境内でステータスがAvailableのPVをフィルターして、新しいPVCと一致させます。
  • DelayBinding:PVコントローラーは、PVCに遅延バインディングが必要かどうかを判断します。1. PVCアノテーションにvolume.kubernetes.io / selected-nodeが含まれているかどうかを確認します。存在する場合は、PVCがスケジューラーによって指定されていることを意味します(ProvisionVolumeに属する)、したがって、遅延バインディングの必要はありません。 2. PVCアノテーションにvolume.kubernetes.io / selected-nodeがなく、StorageClassがない場合、デフォルトは遅延バインディングが不要であることを意味します。StorageClassがある場合は、VolumeBindingModeフィールドを確認します。 WaitForFirstConsumerの場合は遅延バインディングが必要です。Immediateの場合はバインディングする必要はありません。
  • FindBestMatchPVForClaim:PVコントローラーは、PVCの要件を満たす環境で既存のPVを見つけようとします。 PVコントローラーはすべてのPVを一度にスクリーニングし、条件を満たすPVから最適なPVを選択します。 スクリーニング規則:1. VolumeModeが一致するかどうか。 2. PVがPVCにBoundされているかどうか。 3. PVの.Status.Phaseが使用可能かどうか。4. LabelSelectorチェック、PVとPVCのラベルが一致している必要があります。 5. PVと PVCのStorageClassが一致しているかどうか。6.各繰り返し処理は、要求された最小のPVCを満たすPVを更新し、最終結果として返します。
  • Bind:PVコントローラーは、選択したPVとPVCをBoundします。 1. PV Spec.ClaimRef情報を現在のPVCに更新します。 2. PV Status.PhaseをBoundに更新します。 3. PVのアノテーションを追加します。pv.kubernetes.io/bound-by-controller: “yes” 4. PVCの.Spec.VolumeNameをPV名に更新します。 5. PVCの.Status.PhaseをBoundに更新します。 6. PVCのアノテーションを追加します:pv.kubernetes.io/bound-by-controller: “yes”とpv .kubernetes.io / bind-completed: “yes”;
  • 動的ストレージボリュームプロセス(ProvisionVolume):環境に適切なPVがない場合は、動的プロビジョニングシナリオを入力します。
  • Before Provisioning:1. PVコントローラーはまず、PVCで使用されているStorageClassがツリー内かツリー外かを判断します。StorageClassのProvisionerフィールドに “kubernetes.io/”プレフィックスが含まれているかどうかを確認します。2。PVコントローラーはPVCのアノテーションを更新します:Claim.Annotations [“volume.beta.kubernetes.io/storage-provisioner”] = storageClass.Provisioner;
  • in-tree Provisioning(internal provisioning):1.ツリー内のProvionerは、ProvisionableVolumePluginインターフェースのNewProvisionerメソッドを実装して、新しいProvisionerを返します。2.PVコントローラーは、ProvisionerのProvision関数を呼び出して、PVオブジェクトを返します。3.前の手順でPVコントローラーが作成されます。返されたPVオブジェクトはPVCにBoundされ、Spec.ClaimRefはPVCに設定され、.Status.PhaseはBoundに設定され、.Spec.StorageClassNameはPVCと同じStorageClassNameに設定されます。その間、アノテーションが追加されます: “pv.kubernetes.io / bound-by-controller “=” yes “および” pv.kubernetes.io/provisioned-by “= plugin.GetPluginName();
  • out-of-tree Provisioning(external provisioning):1.外部プロビジョナーは、PVC内のClaim.Spec.VolumeNameが空かどうかをチェックし、空でない場合はPVCを直接スキップします。2.外部プロビジョナーは、PVCのClaim.Annotations [“volume.beta.kubernetes.io/storage-provisioner”]が独自のプロビジョナー名と等しいかどうかを確認します(外部プロビジョナーは、起動時に–provisionerパラメーターを渡して自己のProvisioner Nameを決定します)。 3. もしPVCのVolumeMode = Blockの場合、外部プロビジョナーがブロックデバイスをサポートしているかどうかを確認します。4.外部プロビジョナーがProvision関数を呼び出します。gRPCを介してCSIストレージプラグインのCreateVolumeインターフェースを呼び出します。5.外部プロビジョナーは、ボリュームを表すPVを作成し、PVを前のPVCにBoundします。

deleting volumes

Deleting プロセスはProvisioningの逆の操作です。ユーザーはPVCを削除し、PVコントローラーを削除し、PV.Status.PhaseをReleasedに変更します。
PV.Status.Phase == Releasedの場合、PVコントローラーは最初にSpec.PersistentVolumeReclaimPolicyの値を確認します。Retainの時は直接スキップし、Deleteの時は、

  • in-tree Deleting1.ツリー内のProvisionerは、DeleteableVolumePluginインターフェースのNewDeleterメソッドを実装して新しいDeleterを返します。 2.コントローラーはDeleterのDelete関数を呼び出して対応するボリュームを削除します。 3.ボリュームが削除されると、PVコントローラーはPVオブジェクトを削除します。
  • out-of-tree Deleting1.外部プロビジョナーはDelete関数を呼び出し、gRPCを介してCSIプラグインのDeleteVolumeインターフェースを呼び出します。2.ボリュームが削除された後、外部プロビジョナーはPVオブジェクトを削除します。

Attaching Volumes

KubeletコンポーネントとADコントローラーの両方がアタッチ/デタッチ操作を実行できます。Kubeletの起動パラメーターで–enable-controller-attach-detachが指定されている場合、Kubeletはそれを実行します。それ以外の場合、ADはデフォルトでそれを実行します。 アタッチ/デタッチ操作を説明するために、例としてADコントローラーを取り上げます。

ADコントローラーには2つのコア変数があります

  • DesiredStateOfWorld(DSW):クラスターで予想されるデータボリュームのマウントステータス、nodes->volumes->pods の情報を含む。
  • ActualStateOfWorld(ASW):クラスター内の実際のデータボリュームのマウントステータス、volumes->nodes の情報を含む。

Attaching フローを以下に示します:
ADコントローラーは、クラスター内のリソース情報に従ってDSWおよびASWを初期化します。
ADコントローラーには、DSWとASWを定期的に更新する3つのコンポーネントがあります。

  • Reconciler。GoRoutineは定期的に実行され、ボリュームが確実にマウント/削除されます。この期間中、ASWは継続的に更新されます。
    in-tree attaching:1.ツリー内のAttacherは、AttachableVolumePluginインターフェースのNewAttacherメソッドを実装して、新しいAttacherを返します。 2. ADコントローラーは、AttacherのAttach関数を呼び出してデバイスを接続します。 3. ASWを更新します。
    out-of-tree attaching:1.ツリー内のCSIAttacherを呼び出して、接続するAttacher情報、ノード名、およびPV情報を含むVolumeAttachement(VA)オブジェクトを作成します。2.外部Attacherは、クラスター内のVolumeAttachementリソースを監視し、接続する必要があることを検出します。データボリュームが使用中の場合、Attach関数が呼び出され、CSIプラグインのControllerPublishVolumeインターフェースがgRPCを介して呼び出されます。
  • DesiredStateOfWorldPopulator。周期的に実行されるGoRoutineの主な機能は、DSWを更新することです。
    findAndRemoveDeletedPods – DSW内のすべてのPodを反復処理し、クラスターから削除されている場合はDSWから削除します。
    findAndAddActivePods – PodListerのすべてのPodを反復処理し、PodがDSWに存在しない場合は、PodがDSWに追加されます。
  • PVC Worker。PVCの追加/更新イベントを監視し、PVCに関連するPodを処理して、DSWをリアルタイムで更新します。

Detaching Volumes
Detaching フローは以下の通りです:

  • Podが削除されると、ADコントローラーはイベントを監視します。 最初に、ADコントローラーは、Podが配置されているノードリソースに「volumes.kubernetes.io/keep-terminated-pod-volumes」ラベルが含まれているかどうかを確認します。含まれている場合、操作は実行されません。含まれている場合、DSWからボリュームが削除されます。
  • ADコントローラーは、Reconcilerを介して、ActualStateOfWorld状態をDesiredStateOfWorld状態に近づけます。DSWに存在しないボリュームがASWにあることが検出されると、Detach操作を実行します。
  • in-tree detaching1. ADコントローラーはAttachableVolumePluginインターフェースのNewDetacherメソッドを実装して新しいDetacherを返します。2.コントローラーはDetacherのDetach関数を呼び出し、ボリュームに対応するデタッチを行います。 3. ADコントローラーはASWを更新します。
  • out-of-tree detaching1. ADコントローラーは、ツリー内のCSIAttacherを呼び出して、関連するVolumeAttachementオブジェクトを削除します。2. 外部アタッチメントは、クラスター内のVolumeAttachement(VA)リソースを監視し、削除する必要があるデータボリュームを見つけ、Detach関数を呼び出し、gRPCを通じてCSIプラグインのControllerUnpublishVolumeインターフェイスを呼び出します。 3. ADコントローラーがASWを更新します。

Mounting/Unmounting Volumes

Volume Managerには2つのコア変数もあります。

  • DesiredStateOfWorld(DSW):クラスターで予想されるデータボリュームのマウントステータス、volumes->pods の情報を含みます。
  • ActualStateOfWorld(ASW):クラスター内の実際のデータボリュームのマウントステータス、volumes->pods の情報を含みます。

Mounting/UnMounting のフローは以下のとおりです:

グローバルディレクトリ(グローバルマウントパス)の目的:ブロックデバイスはLinuxで1回だけマウントできます。K8sシナリオでは、PVは同じノード上の複数のPodインスタンスにマウントできます。ブロックデバイスがフォーマットされ、ノードの一時グローバルディレクトリにマウントされている場合、LinuxのBoundマウントテクノロジーを使用して、このグローバルディレクトリをPodの対応するディレクトリにマウントすると、要件を満たすことができます。 上記のフローChart では、グローバルディレクトリは/ var / lib / kubelet / pods / [pod uid] /volumes/kubernetes.io~iscsi/ [PV name]です。
VolumeManager クラスター内のリソース情報に従ってDSWおよびASWを初期化します。
VolumeManager DSWとASWを定期的に更新する2つの内部コンポーネントがあります。

  • DesiredStateOfWorldPopulator:周期的に実行されるGoRoutineの主な機能は、DSWを更新することです。
  • Reconciler:GoRoutineは周期的に実行され、ボリュームが確実にマウント/アンマウントされるようにします。この期間中、ASWは継続的に更新されます。
    unmountVolumes:Podを削除した後、ボリュームがアンマウントされていることを確認してください。ASWのすべてのPodを反復処理し、それらがDSWにない場合(Podが削除されていることを示す)、例としてVolumeMode = FileSystemを使用して、次の操作を実行します。
  1. Remove all bind-mounts:UnmounterのTearDownインターフェースを呼び出します(ツリー外の場合は、CSIプラグインのNodeUnpublishVolumeインターフェースを呼び出します)。
  2. Unmount volume:DeviceUnmounterのUnmountDevice関数を呼び出します(ツリー外の場合は、CSIプラグインのNodeUnstageVolumeインターフェースを呼び出します)。
  3. ASWを更新します。
    mountAttachVolumesPodで使用されるボリュームが正常にマウントされていることを確認します。 DSW内のすべてのPodを反復処理し、それがASW内にない場合(ディレクトリがマウントされ、Podにマップされることを示す)、ここでは例としてVolumeMode = FileSystemを使用し、次の操作を実行します。
  1. ボリュームがノードに接続されるのを待ちます(External AttacherまたはKubelet本体によって接続されます)。
  2. ボリュームをグローバルディレクトリにマウントします。DeviceMounterのMountDevice関数を呼び出します(ツリー外の場合は、CSIプラグインのNodeStageVolumeインターフェースを呼び出します)。
  3. ASWを更新します。ボリュームはグローバルディレクトリにマウントされています。
  4. volume を Pod にbind-mountします。 MounterのSetUpインターフェースを呼び出します(それがツリー外の場合は、CSIプラグインのNodePublishVolumeインターフェースを呼び出します)。
  5. ASWを更新します。
    unmountDetachDevicesアンマウントする必要があるボリュームがアンマウントされていることを確認してください。すべてのASWでUnmountedVolumeを反復処理し、DSWにない場合(ボリュームが不要になったことを示す)、次の操作を実行します。
  1. Unmount volume:DeviceUnmounterのUnmountDevice関数を呼び出します(ツリー外の場合は、CSIプラグインのNodeUnstageVolumeインターフェースを呼び出します)。
  2. ASWを更新します。

まとめ

この記事では、まずK8s永続ストレージの基本的な概念と使用方法を紹介し、K8sの内部ストレージプロセスを深く分析しました。K8sでは、あらゆる種類のストレージの使用は上記プロセスと切り離せません(一部のシナリオではアタッチ/デタッチを使用しません)。また、同環境のストレージに問題がある場合は、リンクの1つに障害があるはずです。
特にプロプラエタリーな独自のクラウド環境では、コンテナストレージに多くの落とし穴があります。しかし、課題が増えるほど、チャンスも増えます!現在、中国国内のプロプライエタリクラウド市場もストレージ分野のリーダーであり、私達PaaSコンテナチームは、皆さんが一緒に未来を創造するために私達に加わることを歓迎しています!

PAGE TOP