大きなファイルを 各 Pod から参照する際、DaemonSet をキャッシュとして扱う
今回とあるお客様と出会い、ハックフェストを実施した際に出てきた課題と、私からお客様に提案した対処方法が、他でも同様のニーズがあった場合に有効になるのではないかと想定し、ここにその方法を共有します。
今回のやり方にあうユースケース:
- 各 Pod から同一の巨大ファイル(GBクラスのファイル)を読み込む必要がある
- ファイルは読み込みのみで、書き込みは発生しない
(書き込みが必要な場合、別の手法で対応可能) - 巨大ファイルをできるだけ早く読み込みたい
※ ご注意:ユースケースに応じて手法は各自でご検討ください。
上記のようなユースケースの場合、DaemonSet をキャッシュとして利用する方法もご検討ください。各 Node に一つ Pod を起動する DaemonSet を利用し、DaemonSetの各 Pod にファイルのコピーを持たせます。そしてファイルを参照・取得したい Pod は、DaemonSet の Pod にあるコピー・ファイルを取得・参照する事で、ネットワークを跨いだ通信を削減できるだけでなく、ローカルでのファイル転送になるため短時間でファイルを取得できるようになります。
まぁ、普通に考えて当たり前の事を当たり前に説明しているだけなのですが。
一般的な Kubernetes での Volume のマウント方法
Kubernetes の各 Pod で永続的なボリュームを扱う際、Volume を利用して各 Pod にマウントする手法が用意されています。そして多くのクラウド・プロバイダは各クラウドのストレージ上で Persistence Volume を扱うためのプラグインを提供し、各 Pod から ストレージ内のディスクをマウントできる手法を用意しています(方法は3種類:ReadWriteOnce、ReadOnlyMany、ReadWriteMany)。
Kubernetes のドキュメントをご参照:
* Volumes
* Persistent Volumes
Azure で各 Pod からディスクを 1対1 でマウントする場合 (ReadWriteOnce)、Azure Disk を利用します。また複数の Pod から同一 Disk を共有したい場合 (ReadOnlyMany、ReadWriteMany)、Azure Files を利用します。 今回のユースケースでは、サイズの大きなファイルを複数の Pod から参照したいので、通常であれば Azure Files (ReadOnlyMany) を選択します。
具体的には、下記の手順で Azure Files を作成し、Kubernetes の Persistence Volume として Azure Files を 利用できます。
$ export AKS_PERS_STORAGE_ACCOUNT_NAME=myfilestorageaccount $ export AKS_PERS_RESOURCE_GROUP=Yoshio-Storage $ export AKS_PERS_LOCATION=japaneast $ export AKS_PERS_SHARE_NAME=aksshare $ az storage account create -n $AKS_PERS_STORAGE_ACCOUNT_NAME -g $AKS_PERS_RESOURCE_GROUP -l $AKS_PERS_LOCATION --sku Standard_LRS $ export AZURE_STORAGE_CONNECTION_STRING=`az storage account show-connection-string -n $AKS_PERS_STORAGE_ACCOUNT_NAME -g $AKS_PERS_RESOURCE_GROUP -o tsv` $ az storage share create -n $AKS_PERS_SHARE_NAME $ STORAGE_KEY=$(az storage account keys list --resource-group $AKS_PERS_RESOURCE_GROUP --account-name $AKS_PERS_STORAGE_ACCOUNT_NAME --query "[0].value" -o tsv) $ kubectl create secret generic azure-secret --from-literal=azurestorageaccountname=$AKS_PERS_STORAGE_ACCOUNT_NAME --from-literal=azurestorageaccountkey=$STORAGE_KEY
続いて、これを Pod からマウントするために volumeMounts, volumes の設定を各 Pod で行います。
apiVersion: apps/v1 kind: Deployment metadata: name: ubuntu spec: replicas: 2 selector: matchLabels: app: ubuntu template: metadata: labels: app: ubuntu version: v1 spec: containers: - name: ubuntu image: ubuntu command: - sleep - infinity volumeMounts: - mountPath: "/mnt/azure" name: volume resources: limits: memory: 4000Mi requests: cpu: 1000m memory: 4000Mi env: - name: NODE_IP valueFrom: fieldRef: fieldPath: status.hostIP volumes: - name: volume azureFile: secretName: azure-secret shareName: aksshare readOnly: true
※ 今回ファイルサイズの大きなファイルをコピーするため4GBのメモリをアサインしています。
上記のファイルを deployment.yaml として保存し下記のコマンドを実行してください。
$ kubectl apply -f deployment.yaml
Pod が正常に起動したのち、Pod 上で mount コマンドを実行すると Azure Files が /mnt/azure にマウントされていることを確認できます。
$ kubectl exec -it ubuntu-884df4bfc-7zgkz mount |grep azure //**********.file.core.windows.net/aksshare on /mnt/azure type cifs (rw,relatime,vers=3.0,cache=strict,username=********,domain=,uid=0,noforceuid,gid=0,noforcegid,addr=40.***.***.76,file_mode=0777,dir_mode=0777,soft,persistenthandles,nounix,serverino,mapposix,rsize=1048576,wsize=1048576,echo_interval=60,actimeo=1)
この時、このファイルの実態は Azure Storage の Files Share (***************.file.core.windows.net/aksshare) 上に存在しています。
Pod から /aksshare に存在するファイルを参照する際にファイルを SMB プロトコルを利用してダウンロードします。
実際に、/mnt/azure にあるサイズの大きなファイルを Pod 内にコピーします。
# date Wed Jan 15 16:29:03 UTC 2020 # cp /mnt/azure/Kuberenetes-Operation-Demo.mov ~/mount.mov # date Wed Jan 15 16:29:54 UTC 2020 (51秒)
上記を確認すると Pod に約 3 Gb の動画ファイルをコピーするのに 51 秒ほど時間を要している事がわかります。仮に複数の Pod が起動し同一ファイルを参照した場合、ネットワークをまたいでファイルコピーが繰り返されます。これが通常の利用方法になります。
そして今回は、このファイル共有におけるネットワーク転送回数、スピードなどを DaemonSet を利用し改善します。
DaemonSet をキャッシュとして利用するための環境構築
今回、DaemonSet 上で稼働する Pod を nginx を利用して作成します。nginx のコンテキスト・ルート (/app) 配下に、Azure Blog Storage にあるファイルをコピーし、HTTP でファイルを取得できるようにします。
下記の設定により /app にあるコンテンツを http://podIP/ 経由で取得できるように設定します。
FROM alpine:3.6 RUN apk update && \ apk add --no-cache nginx RUN apk add curl ADD default.conf /etc/nginx/conf.d/default.conf EXPOSE 80 RUN mkdir /app RUN mkdir -p /run/nginx WORKDIR /app CMD nginx -g "daemon off;"
上記の内容を Dockerfile として保存してください。
server { listen 80 default_server; listen [::]:80 default_server; root /app; location / { } }
上記の内容を default.conf として保存してください。
このイメージをビルドし Azure Container Registry に Push します。
$ docker build -t tyoshio2002/nginxdatamng:1.1 . $ docker tag tyoshio2002/nginxdatamng:1.1 yoshio.azurecr.io/tyoshio2002/nginxdatamng:1.1 $ docker login -u yoshio yoshio.azurecr.io Password: $ docker push yoshio.azurecr.io/tyoshio2002/nginxdatamng:1.1
次に Kubernetes から Azure Container Registry に接続するための Secret を作成します。
kubectl create secret docker-registry docker-reg-credential --docker-server=yoshio.azurecr.io --docker-username=yoshio --docker-password="***********************" --docker-email=foo-bar@microsoft.com
Azure Container Registry にイメージを Push したので、Daemonset のマニュフェストを記述します。
apiVersion: apps/v1 kind: DaemonSet metadata: name: mydaemonset labels: app: mydaemonset spec: selector: matchLabels: name: mydaemonset template: metadata: labels: name: mydaemonset spec: tolerations: - key: node-role.kubernetes.io/master effect: NoSchedule imagePullSecrets: - name: docker-reg-credential volumes: - name: host-pv-storage hostPath: path: /mnt type: Directory containers: - name: nginx image: yoshio.azurecr.io/tyoshio2002/nginxdatamng:1.1 volumeMounts: - name: host-pv-storage mountPath: /mnt resources: limits: memory: 4000Mi requests: cpu: 1000m memory: 4000Mi lifecycle: postStart: exec: command: - sh - -c - "curl https://myfilestorageaccount.blob.core.windows.net/fileshare/Kuberenetes-Operation-Demo.mov -o /app/aaa.mov" terminationGracePeriodSeconds: 30
※ 今回は検証を簡単にするため、単一ファイル (Kuberenetes-Operation-Demo.mov) を postStart のフェーズで /app 配下にダウンロードしています。複数ファイルを扱う場合、コンテナのイメージ内で複数ファイルをダウンロードする仕組みを別途実装してください。また Nginx をカスタマイズ、もしくは他の Web サーバ、App サーバを利用する事でコンテンツキャッシュを効かせるなど効率化をはかることも可能かと想定します。
上記マニュフェストファイルを daemonset.yaml として保存し、下記のコマンドを実行してください。
$ kubectl apply -f daemonset.yaml
DaemonSet を作成したのち、DaemonSet を Service として登録します。Service を作成する事で各ノードの 30001 番ポート番号にアクセスし Nginx に Node の IP アドレスとポート番号でアクセスできるようにします。
apiVersion: v1 kind: Service metadata: labels: app: daemonset-service name: daemonset-service spec: ports: - port: 80 name: http targetPort: 80 nodePort: 30001 selector: name: mydaemonset sessionAffinity: None type: NodePort
上記を service.yaml として保存し、下記のコマンドを実行してください。
$ kubectl apply -f service.yaml
Nginx の DaemonSet と Ubuntu の Deployment をそれぞれ配備したのち、下記のコマンドを実行し、どのノードでどの Pod が動作しているかを確認します。
$ kubectl get no -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME aks-agentpool-41616757-vmss000000 Ready agent 176m v1.15.7 10.240.0.4 Ubuntu 16.04.6 LTS 4.15.0-1064-azure docker://3.0.8 aks-agentpool-41616757-vmss000001 Ready agent 176m v1.15.7 10.240.0.35 Ubuntu 16.04.6 LTS 4.15.0-1064-azure docker://3.0.8 aks-agentpool-41616757-vmss000002 Ready agent 176m v1.15.7 10.240.0.66 Ubuntu 16.04.6 LTS 4.15.0-1064-azure docker://3.0.8 virtual-node-aci-linux Ready agent 175m v1.14.3-vk-azure-aci-v1.1.0.1 10.240.0.48 $ kubectl get po -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES mydaemonset-gqlrw 1/1 Running 0 72m 10.240.0.24 aks-agentpool-41616757-vmss000000 mydaemonset-nnn5l 1/1 Running 0 72m 10.240.0.50 aks-agentpool-41616757-vmss000001 mydaemonset-pzvzx 1/1 Running 0 72m 10.240.0.82 aks-agentpool-41616757-vmss000002 ubuntu-884df4bfc-7zgkz 1/1 Running 0 64m 10.240.0.69 aks-agentpool-41616757-vmss000002 ubuntu-884df4bfc-gd26h 1/1 Running 0 63m 10.240.0.33 aks-agentpool-41616757-vmss000000 ubuntu-884df4bfc-vh7rg 1/1 Running 0 63m 10.240.0.49 aks-agentpool-41616757-vmss000001
上記では、ubuntu-884df4bfc-vh7rg は aks-agentpool-41616757-vmss000001 のノード上で動作しており、aks-agentpool-41616757-vmss000001 のノード上では mydaemonset-nnn5l の DaemonSet の Pod がうごしています。
そして aks-agentpool-41616757-vmss000001 のノードの IP アドレスは 10.240.0.35 である事がわかります。Ubuntu の Pod 内では Pod が稼働している Node の IP アドレスを環境変数 $NODE_IP で取得できるように設定しています。
$ kubectl exec -it ubuntu-884df4bfc-vh7rg env|grep NODE_IP NODE_IP=10.240.0.35
補足
最後に、ここでは環境構築方法の詳細説明を割愛しますが、Azure Blob Storage で Standard と Premium (高速) のそれぞれ作成し、それぞれのストレージに同一の動画ファイルを配置しておきます。
※ Azure Blob の Premium Storage は AKS の VNET と接続し VNET 経由でファイル取得しています。
その他の参考情報
* Azure Premium Storage: 高パフォーマンス用に設計する
* Troubleshoot Azure Files problems in Linux
* Troubleshoot Azure Files performance issues
検証内容
今回、下記の 4 点を検証しました。
1. Azure Files をマウントしたディレクトリに存在するファイルを Pod 内にコピーする際にかかる所用時間
2. Azure Blob (Standard) に存在するファイルを Pod で取得する際の所用時間
3. Azure Blob (Premium) に存在するファイルを Pod で取得する際の所用時間
4. DaemonSet 内にコピーしたファイルから取得する際の所用時間
1回目(実際の検証内容の動画)
下記のコマンド実行で確認しています
kubectl exec -it ubuntu-884df4bfc-vh7rg ## File Put on Azure Files (File shares : Samba) $ date $ cp /mnt/azure/Kuberenetes-Operation-Demo.mov ~/mount.mov $ date ## File Put on Azure Blob (Containers) $ curl https://myfilestorageaccount.blob.core.windows.net/fileshare/Kuberenetes-Operation-Demo.mov -o ~/direct.mov $ curl https://mypremiumstorageaccount.blob.core.windows.net/fileshare/Kuberenetes-Operation-Demo.mov -o ~/premium2.mov $ curl http://$NODE_IP:30001/aaa.mov -o ~/fromNodeIP.mov
回数 | PV からのコピー | Standard Storage からの入手 | Premium Storage からの入手 | DaemonSet キャッシュからの入手 |
1回目 | 51 秒 | 69 秒 | 27 秒 | 20 秒 |
2回目 | 60 秒 | 60 秒 | 23 秒 | 11 秒 |
3回目 | 53 秒 | 57 秒 | 20 秒 | 8 秒 |
※ ちなみに、DaemonSet の Nginx を利用した際も、初回アクセス時は時間がかかっています。これはおそらく Nginx のメモリに乗っていない為、取得に時間がかかっているように想定します。他の環境でも2回目アクセス以降は早くなっています。
検証結果のまとめ
上記、3回の実行結果より、一旦 DaemonSet にファイルを取得し、DaemonSet のキャッシュからファイルを入手するのが最も高速に入手できる事がわかりました。
Premium のストレージを利用する事で Standard のストレージよりも高速にファイル転送ができますが、Premium にすると Standard より高コストになります。また、Storage からの入手の場合、毎回ネットワークを経由してのダウンロードになる事から、それを考えても、一旦同一のノード(VM) にファイルを取ってきて、そこから入手する方が最も効率的で早い事は当たり前ですね。
DaemonSet を利用する利点
ネットワーク通信量の削減
ファイル取得スピードの高速化
DaemonSet 内のコンテナの実装を機能豊富にすれば、他にも用途が広がる(例:書き込み対応など)
懸念事項
* 現在は、特定の単一ファイルを取得していますが、複数ファイルを扱う場合、もしくはファイルの更新があった場合は、DaemonSet の内部で定期的に更新をチェックし、更新分をダウンロードする仕組みなどを実装する必要あり
* DaemonSet の Pod 内のファイルサイズが肥大化する可能性がありクリーン・アップをする必要がある
* ファイルの書き込みについては、今回の検証方法をそのまま利用した場合は対応は難しいが、DaemonSet 内のコンテナで独自に In Memory Grid 製品のような機能を実装すれば対応かと想定します。
最後に:
その他にも、内部的に検証をしたのですが、説明内容が増え視点がぼやける可能性があった為、特に効果的な点だけに絞ってまとめました。また、極力簡単にするために、DaemonSet で Nginx を使用していますが、Nginx の部分をさらにチューニングをしたり、Nginx の部分を他の実装に変更するなどで、さらなるパフォーマンス向上も見込まれるかと思います。
ぜひ、こちらの情報を元にさらなるチューニングなどを施していただければ誠に幸いです。
Entry filed under: Java.