最近なんだか仕事で良く聞かれる様になってきたので
CSI Topologyを使ったTopology AwareなPersistentVolume環境を作って試してみたいと思います。
環境の準備
今回用意したのは、こんな環境です。
Compute
- Kubernetes 1.20.5
- Debian 10.9
- containerd 1.4.4-1
- master 1, worker 2 (それぞれ m1, w1, w2 という名前)
Storage
- ONTAP 9.8 (Simulator)
- NetApp Trident 21.01.1
admin@m1:~$ kubectl get node -o wide -L topology.kubernetes.io/region,topology.kubernetes.io/zone
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME REGION ZONE
m1 Ready control-plane,master 15h v1.20.5 192.168.255.100 <none> Debian GNU/Linux 10 (buster) 4.19.0-16-amd64 containerd://1.4.4 myhome myhome-kitchen
w1 Ready <none> 15h v1.20.5 192.168.254.100 <none> Debian GNU/Linux 10 (buster) 4.19.0-16-amd64 containerd://1.4.4 myhome myhome-room1
w2 Ready <none> 15h v1.20.5 192.168.253.100 <none> Debian GNU/Linux 10 (buster) 4.19.0-16-amd64 containerd://1.4.4 myhome myhome-room2
オンプレミス(自宅)だったのでメモリーとの戦い1でした……。
Tridentの設定
CSIの Provisionerである Tridentのバックエンド設定は公式ドキュメントを参考に、こんな感じにしてみました。
ポイントになるのは .supportedTopologies の項目で、
ここに「バックエンドが提供するボリュームを、利用可能なRegion/Zone」を記入します。
{
"version": 1,
"storageDriverName": "ontap-nas",
"managementLIF": "192.168.254.210",
"dataLIF": "192.168.254.216",
"username": "vsadmin",
"password": "UltraSecret",
"supportedTopologies": [
{"topology.kubernetes.io/region": "myhome", "topology.kubernetes.io/zone": "myhome-room1"}
]
}
{
"version": 1,
"storageDriverName": "ontap-nas",
"managementLIF": "192.168.253.210",
"dataLIF": "192.168.253.216",
"username": "vsadmin",
"password": "SuperSecret",
"supportedTopologies": [
{"topology.kubernetes.io/region": "myhome", "topology.kubernetes.io/zone": "myhome-room2"}
]
}
バックエンドを作成します
admin@m1:~$ tridentctl create backend -n trident -f ./backend-ontap-nas-topology-room1.json
+--------------------------+----------------+--------------------------------------+--------+---------+
| NAME | STORAGE DRIVER | UUID | STATE | VOLUMES |
+--------------------------+----------------+--------------------------------------+--------+---------+
| ontapnas_192.168.254.216 | ontap-nas | f117f775-eaa1-4a93-ad14-5a47a0b8756e | online | 0 |
+--------------------------+----------------+--------------------------------------+--------+---------+
admin@m1:~$ tridentctl create backend -n trident -f ./backend-ontap-nas-topology-room2.json
+--------------------------+----------------+--------------------------------------+--------+---------+
| NAME | STORAGE DRIVER | UUID | STATE | VOLUMES |
+--------------------------+----------------+--------------------------------------+--------+---------+
| ontapnas_192.168.253.216 | ontap-nas | 5b922cf6-c807-412f-8273-9906a89dda7a | online | 0 |
+--------------------------+----------------+--------------------------------------+--------+---------+
基本動作の確認: 即時バインド
バックエンドはTopology Awareになりましたが、気にせず普通に使ってみます。
StorageClassとしては 以下を使います。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nas
provisioner: csi.trident.netapp.io
parameters:
backendType: "ontap-nas"
このStorageClassを指定してPVCを単独で作ります。
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: nas-vol1
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Mi
storageClassName: nas
当たり前ですが要求(PVC)に対応(PV作成)されて、PVCとPVがすぐに関連付け=Bindされます。
この StorageClassではTopology Awareな動きをしていないように見えますが……。
admin@m1:~$ kubectl create -f ./pvc1-nas.yaml
persistentvolumeclaim/nas-vol1 created
admin@m1:~$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nas-vol1 Bound pvc-78b792fe-0825-41c6-ac32-321e7c551bbc 100Mi RWX nas 5s
作成されたPVには topology を keyにした Node Affinityが付加されています。
Name: pvc-78b792fe-0825-41c6-ac32-321e7c551bbc
StorageClass: nas
Status: Bound
Claim: default/nas-vol1
Node Affinity:
Required Terms:
Term 0: topology.kubernetes.io/zone in [myhome-room1]
topology.kubernetes.io/region in [myhome]
このため、このPersistentVolumeを利用する Podの配置は myhome-room1 zoneの中に限定されます。
nodeSelectorでPodの配置と矛盾した状況になると Scheduling出来ずに Podは Pendingのままとなりますので注意です。
Zoneにルーズな StorageClassとあわせて構成する場合、
バックエンドとしては supportedTopologiesの有り・無しで分けておく必要があるかもしれません。
WaitForFirstConsumerによるバインド
Podの位置が決まるまで ボリュームをBINDしないようにvolumeBindingModeを
WaitForFirstConsumerに変更します
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nas-wait
provisioner: csi.trident.netapp.io
volumeBindingMode: WaitForFirstConsumer
parameters:
backendType: "ontap-nas"
このStorageClassにしてからPVCを作成すると PVがすぐにBindされず Pendingの状態で待ちになります。
admin@m1:~$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nas-wait-vol1 Pending nas-wait 9s
PVCの状態を確認すると「利用者が現れるまで(使う場所が決まるまで)待ってる」的な理由で止まっているのがわかります。
Name: nas-wait-vol1
StorageClass: nas-wait
Status: Pending
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal WaitForFirstConsumer 4s (x3 over 20s) persistentvolume-controller waiting for first consumer to be created before binding
この状態でPodが作成されると、ボリュームがプロビジョニングされます。
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal WaitForFirstConsumer 22s (x11 over 2m38s) persistentvolume-controller waiting for first consumer to be created before binding
Normal Provisioning 10s csi.trident.netapp.io_trident-csi-759f4b869c-84nv9_70970a49-4b34-4f7a-bbfd-197c862e90f9 External provisioner is provisioning volume for claim "default/nas-wait-vol1"
Normal ExternalProvisioning 7s (x2 over 10s) persistentvolume-controller waiting for a volume to be created, either by external provisioner "csi.trident.netapp.io" or manually created by system administrator
Normal ProvisioningSuccess 7s csi.trident.netapp.io provisioned a volume
Normal ProvisioningSucceeded 7s csi.trident.netapp.io_trident-csi-759f4b869c-84nv9_70970a49-4b34-4f7a-bbfd-197c862e90f9 Successfully provisioned volume pvc-3da6cea8-de34-4b85-a7ea-e2ffd94d321e
このとき PVC には selected-node という annotationが付いてました。
Annotations: pv.kubernetes.io/bind-completed: yes
pv.kubernetes.io/bound-by-controller: yes
volume.beta.kubernetes.io/storage-provisioner: csi.trident.netapp.io
volume.kubernetes.io/selected-node: w1
なお、PVの Source情報から、w1の属する myhome-room1 の backendから
ボリュームが払い出されているのがわかります。
Source:
VolumeAttributes: backendUUID=f117f775-eaa1-4a93-ad14-5a47a0b8756e
特定Zoneだけのプロビジョニング
特定のZoneのみBind可能なStorageClassを作ってみます
先程の WaitForFirstConsumer に加えて、
.allowedTopologiesに Topology の Labelを記述します。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nas-zoned
provisioner: csi.trident.netapp.io
volumeBindingMode: WaitForFirstConsumer
parameters:
backendType: "ontap-nas"
allowedTopologies:
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values:
- myhome-room2
この StorageClassを指定すると zone=myhome-room2 の Backend だけ利用されます。
PVCを作ったときの動きも確認しましたが、普通にZoneが限定されるだけなので、記事からは省略します。
Podと PersistentVolumeで 配置するゾーンが矛盾した場合
TopologyAwareなBackendを使った StorageClassから作った PVには、
前述の通りNodeAffinityが追加されます。
これを利用する Podは myhome-room2 の ノードにしか配置できなくなるわけですが、
もし矛盾したnodeSelectorなどが指定されているとどうなるのか?と思ってやってみました。
nodeSelctorに myhome-room1 の ノードを指定しています。
Podが room1内、PersistentVolumeが room2という状況です。
Name: pv-zoned-pod
Namespace: default
Status: Pending
Containers:
pv-container:
Conditions:
Type Status
PodScheduled False
Volumes:
pv-nas-storage:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: nas-zoned-vol1
Node-Selectors: kubernetes.io/hostname=w1
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m46s default-scheduler 0/3 nodes are available: 1 node(s) didn't find available persistent volumes to bind, 1 node(s) didn't match Pod's node affinity, 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.
利用出来るノードが見つからないという以下の様な Warningになっています。
「3ノードあるが、0/3でノードが使えない。1ノード(room1)は PersistentVolumeが見つからない、もう1つのノード(room2)は Podの Affinityに反する。最後の1ノード(kitchen)は Taintされていて、Podは Tolerateされていない。」
ここで Podの nodeSelectorを取り除いた場合、PersistentVolume側の都合にあわせて Podがスケジューリングされます。
PodのZoneに合わせたプロビジョニング
さて、やっと本命です。StatefulSetを Zoneを跨って展開してみます。
このためには StorageClassが複数のZoneに対応したバックエンドを対象とする必要があります。
先程のallowedTopologiesに、もう一つのゾーン情報を追加します。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nas-topology-aware
provisioner: csi.trident.netapp.io
volumeBindingMode: WaitForFirstConsumer
parameters:
backendType: "ontap-nas"
allowedTopologies:
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values:
- myhome-room1
- myhome-room2
StatefulSet は以下を使ってみました。
ここは特に工夫はありません。単にStorageClassとして Topology Awareな物を指定しているだけです。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ss-topology-aware
spec:
selector:
matchLabels:
app: ss-topology-aware
serviceName: ss-topology-aware
replicas: 4
template:
metadata:
labels:
app: ss-topology-aware
spec:
containers:
- name: ss-topology-aware
image: alpine:latest
volumeMounts:
- name: myvol
mountPath: /mnt/myvol
command: ["/bin/sh"]
args: ["-c","while true; do date >>/mnt/myvol/time.log;sleep 1; done"]
volumeClaimTemplates:
- metadata:
name: myvol
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: nas-topology-aware
resources:
requests:
storage: 100Mi
結果はこんな感じ。
admin@m1:~$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ss-topology-aware-0 1/1 Running 0 101s 192.168.62.71 w1 <none> <none>
ss-topology-aware-1 1/1 Running 0 88s 192.168.62.72 w1 <none> <none>
ss-topology-aware-2 1/1 Running 0 66s 192.168.27.69 w2 <none> <none>
ss-topology-aware-3 1/1 Running 0 51s 192.168.62.73 w1 <none> <none>
admin@m1:~$ kubectl get pod -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\t"}{.spec.volumes[?(@.name=="myvol")].persistentVolumeClaim.claimName}{"\n"}'
ss-topology-aware-0 w1 myvol-ss-topology-aware-0
ss-topology-aware-1 w1 myvol-ss-topology-aware-1
ss-topology-aware-2 w2 myvol-ss-topology-aware-2
ss-topology-aware-3 w1 myvol-ss-topology-aware-3
admin@m1:~$ kubectl get pv -o jsonpath='{ range .items[*]}{.spec.claimRef.name}{"\t"}{.metadata.name}{"\t"}{.spec.nodeAffinity.required.nodeSelectorTerms[*].matchExpressions[1].key}{"\t"}{.spec.nodeAffinity.required.nodeSelectorTerms[*].matchExpressions[1].values[*]}{"\n"}' | sort
myvol-ss-topology-aware-0 pvc-7cecc303-0b74-49d6-b1b2-e290ae25c70d topology.kubernetes.io/zone myhome-room1
myvol-ss-topology-aware-1 pvc-c3de2d8a-d393-4809-a453-5ebae6db4a62 topology.kubernetes.io/zone myhome-room1
myvol-ss-topology-aware-2 pvc-c304f449-2ec6-4bdf-97a4-831c3ea14157 topology.kubernetes.io/zone myhome-room2
myvol-ss-topology-aware-3 pvc-95f29ec4-51fe-4f16-8bab-53832fc9b587 topology.kubernetes.io/zone myhome-room1
ポッドの場所に合わせてゾーンが選択されています。
でも、3:1って なんかすっきりしないバランスですね。
+Topology Spread Constraints
綺麗にゾーンで別れないので Topology Spread Constraintを設定してみます。
.spec.template.spec.topologySpreadConstraintsが新しい要素です。
maxSkewが1になっているので、各ゾーン間のPod数の差がこれ以上広がらない様に
Podを配置してくれます。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ss-zone-constrained
spec:
selector:
matchLabels:
app: ss-zone-constrained
serviceName: ss-zone-constrained
replicas: 6
template:
metadata:
labels:
app: ss-zone-constrained
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: ss-zone-constrained
containers:
- name: ss-zone-constrained
image: alpine:latest
volumeMounts:
- name: myvol
mountPath: /mnt/myvol
command: ["/bin/sh"]
args: ["-c","while true; do date >>/mnt/myvol/time.log;sleep 1; done"]
volumeClaimTemplates:
- metadata:
name: myvol
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: nas-topology-aware
resources:
requests:
storage: 100Mi
上の Manifestを投入すると Podが 2つ DeployされたあとPendingになって止まります(罠)
理由は上の構成図を見るとわかると思います2が、
同じRegion内にKitchenという もう一つの Zoneがあります。
Kitchenには Taintされたマスターノードしかいないので、
Podが配置出来ず、差が1までしか許容されていないので 制約で動けなくなるという事です。
この場合、nodeSelector や Affinityなどで、予め そのZoneのノードが一台も対象に入らない様にしてやることで回避可能です。
適当にノードにラベルをつけて nodeSelector[^3]を追加して実行します。
spec:
template:
spec:
nodeSelector:
boss: me
結果はこんな感じです。綺麗にゾーン毎に 3つずつPodが分散しています。
admin@m1:~$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ss-zone-constrained-0 1/1 Running 0 107s 192.168.62.83 w1 <none> <none>
ss-zone-constrained-1 1/1 Running 0 85s 192.168.27.76 w2 <none> <none>
ss-zone-constrained-2 1/1 Running 0 71s 192.168.62.84 w1 <none> <none>
ss-zone-constrained-3 1/1 Running 0 62s 192.168.27.77 w2 <none> <none>
ss-zone-constrained-4 1/1 Running 0 52s 192.168.62.85 w1 <none> <none>
ss-zone-constrained-5 1/1 Running 0 41s 192.168.27.78 w2 <none> <none>
admin@m1:~$ kubectl get pod -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\t"}{.spec.volumes[?(@.name=="myvol")].persistentVolumeClaim.claimName}{"\n"}'
ss-zone-constrained-0 w1 myvol-ss-zone-constrained-0
ss-zone-constrained-1 w2 myvol-ss-zone-constrained-1
ss-zone-constrained-2 w1 myvol-ss-zone-constrained-2
ss-zone-constrained-3 w2 myvol-ss-zone-constrained-3
ss-zone-constrained-4 w1 myvol-ss-zone-constrained-4
ss-zone-constrained-5 w2 myvol-ss-zone-constrained-5
admin@m1:~$ kubectl get pv -o jsonpath='{ range .items[*]}{.spec.claimRef.name}{"\t"}{.metadata.name}{"\t"}{.spec.nodeAffinity.required.nodeSelectorTerms[*].matchExpressions[?(@.key=="topology.kubernetes.io/zone")].key}{"\t"}{.spec.nodeAffinity.required.nodeSelectorTerms[*].matchExpressions[?(@.key=="topology.kubernetes.io/zone")].values[*]}{"\n"}' | sort
myvol-ss-zone-constrained-0 pvc-636c0031-ee71-4417-a798-9233ba452f1c topology.kubernetes.io/zone myhome-room1
myvol-ss-zone-constrained-1 pvc-ff2780ad-8bf5-49aa-a79a-90b0598ed454 topology.kubernetes.io/zone myhome-room2
myvol-ss-zone-constrained-2 pvc-e47ec00a-71d6-4d1a-9a2c-2cab8f55163b topology.kubernetes.io/zone myhome-room1
myvol-ss-zone-constrained-3 pvc-362c2ce0-1a2a-4413-be88-4b90279e07bb topology.kubernetes.io/zone myhome-room2
myvol-ss-zone-constrained-4 pvc-3e76485e-b29b-451e-b51d-69db3ae8a7f5 topology.kubernetes.io/zone myhome-room1
myvol-ss-zone-constrained-5 pvc-7e9af95e-fd89-4048-9416-af4e12ae23df topology.kubernetes.io/zone myhome-room2
admin@m1:~$ tridentctl -n trident get backend
+--------------------------+----------------+--------------------------------------+--------+---------+
| NAME | STORAGE DRIVER | UUID | STATE | VOLUMES |
+--------------------------+----------------+--------------------------------------+--------+---------+
| ontapnas_192.168.254.216 | ontap-nas | f117f775-eaa1-4a93-ad14-5a47a0b8756e | online | 3 |
| ontapnas_192.168.253.216 | ontap-nas | 5b922cf6-c807-412f-8273-9906a89dda7a | online | 3 |
+--------------------------+----------------+--------------------------------------+--------+---------+
まとめ
というわけで、Topology Awareな バックエンド登録の仕方と基本的な動きについて検証してみました。
- Tridentの構成で supportedToporogiesをバックエンドに追加
- StorageClassに対して Binding Modeを WaitForFirstConsumerに
- 調整は allowedTopologies などを適宜
という感じでしょうか。
こういう構成の導入が、企業で検討されるようになってきたんだなぁなんて、しみじみと感じている今日この頃です。
ポエムな感じですが 終わります。