Archive for 1月, 2020

大きなファイルを 各 Pod から参照する際、DaemonSet をキャッシュとして扱う

今回とあるお客様と出会い、ハックフェストを実施した際に出てきた課題と、私からお客様に提案した対処方法が、他でも同様のニーズがあった場合に有効になるのではないかと想定し、ここにその方法を共有します。

今回のやり方にあうユースケース:

  1. 各 Pod から同一の巨大ファイル(GBクラスのファイル)を読み込む必要がある
  2. ファイルは読み込みのみで、書き込みは発生しない
    (書き込みが必要な場合、別の手法で対応可能)
  3. 巨大ファイルをできるだけ早く読み込みたい

※ ご注意:ユースケースに応じて手法は各自でご検討ください。

上記のようなユースケースの場合、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 の部分を他の実装に変更するなどで、さらなるパフォーマンス向上も見込まれるかと思います。

ぜひ、こちらの情報を元にさらなるチューニングなどを施していただければ誠に幸いです。

2020年1月17日 at 6:05 PM


Java Champion & Evangelist

Translate

ご注意

このエントリは個人の見解であり、所属する会社の公式見解ではありません

カレンダー

2020年1月
 12345
6789101112
13141516171819
20212223242526
2728293031  

カテゴリー

Twitter

clustermap

ブログ統計情報

  • 1,187,317 hits

RSSフィード

アーカイブ