Kubernetesでローカルストレージを有効活用しよう

こんにちは、Necoプロジェクトの池添(@zoetro)です。

今回は、Kubernetes向けにTopoLVMというCSI (Container Storage Interface) プラグインを開発したので紹介したいと思います。 TopoLVMは以下のような特徴を持っており、Kubernetesでローカルストレージを有効に利用することができます。

  • LVM (Logical Volume Manager) によるボリューム管理
  • トポロジーを考慮したダイナミックプロビジョニング
  • ノードの空き容量に応じたスケジューリング

github.com

本記事では、CSIプラグインを自作するに至った経緯とTopoLVMの特徴の紹介、Elasticsearchのストレージとして利用する例を紹介します。

ストレージへの取り組み

Kubernetesでストレージを扱うことは難しいとよく言われています。しかし実際のところ、 「Kubernetesのストレージが難しいのではない、ストレージが難しいのだ」 と言われるように、どのようなプラットフォームであってもストレージを扱うことは難しいのです。

幸いなことに、最近のKubernetesではストレージに関する機能追加が活発におこなわれています。 特にCSI(Container Storage Interface)プラグインと呼ばれる仕組みがKubernetes 1.13からGA(General Availability)となったことが大きいですね。

CSIとは、Kubernetesなどのコンテナオーケストレーションシステムにおいて、コンテナから任意のストレージを利用するための標準化された仕組みです。 CSIを利用することで、Kubernetesのソースコードを変更することなく、Kubernetesに任意のストレージシステムを組み込むことが簡単にできるようになりました。

さて、ストレージと一口に言っても、様々なタイプのストレージが存在します。 我々は現在のところ、Kubernetes上で以下のようなストレージを扱えるようにしようとしています。

  1. 高速なローカルストレージ
    • ワーカーノードのローカルストレージをKubernetesのPersistentVolumeとして扱う
    • NVMe SSDの性能を活かして、 Elasticsearch や MySQL など高IOPSが必要なミドルウェアのストレージとして利用
    • 可用性はミドルウェアのレプリケーションで担保する
  2. 高可用ネットワークストレージ
    • ネットワーク経由で利用可能なストレージをKubernetesのPersistentVolumeとして扱う
    • ステートフルなアプリケーションの永続化ボリュームとして利用
  3. 大容量な分散オブジェクトストレージ
    • ネットワーク経由で利用可能なストレージをAmazon S3互換のAPI経由で扱う
    • 文書、画像、動画などの大容量データを保存

「2. 高可用ネットワークストレージ」と「3. 分散オブジェクトストレージ」については、Ceph/Rookを利用する予定です。

今回紹介するTopoLVMは、「1. 高速なローカルストレージ」を実現するために開発したソフトウェアです。

Kubernetesのローカルストレージ事情

Kubernetesでローカルストレージを扱うための仕組みはいくつか用意されています。 例えば、hostPath PersistentVolumeは、ローカルストレージのパスを指定してPodからマウントすることができる仕組みです。 しかし、hostPathはマルチノードクラスタに対応しておらず、主にシングルノードクラスタでテスト用途として使うためのものです。

local PersistentVolumeもhostPathと同じようにローカルストレージのパスを指定してPodからマウントすることができます。nodeAffinityによりボリュームを配置するノードを指定することができます。 しかし、Kubernetes 1.16時点では Dynamic Provisioning に対応していないため、手動でPersistentVolumeリソースを作成し、ローカルストレージの準備をしておく必要があります。

hostPath や local PersistentVolume は、ホスト上のディレクトリをマウントして利用する単純なものです。 そのため、複数のハードディスクやSSDを扱ったり、ストレージのサイズやアクセス権を制限するためには何らかの仕組みが必要になります。

そのような仕組みとしては、ノードが提供可能なストレージをボリュームグループとしてまとめ、論理的なボリュームを切り出して扱える LVM (Logical Volume Manager) がひとつの解決策となるでしょう。

Kubernetes向けのLVMを利用したCSIプラグインを探してみるといくつかの実装が見つかります。 しかし、対応CSIのバージョンが古かったり、我々が必要とする機能が実装されていなかったりしました。

TopoLVMの特徴

ないのなら作ってしまおう。 ということで、以下のような特徴を持つTopoLVMというCSIプラグインを開発しました。

  • LVM (Logical Volume Manager) によるボリューム管理
  • トポロジーを考慮したダイナミックプロビジョニング
  • ノードの空き容量に応じたスケジューリング

これらの特徴を以下に解説していきます。

LVM (Logical Volume Manager) によるボリューム管理

TopoLVMでは、LVMを利用してローカルストレージの管理をおこないます。

サーバーごとに異なる台数のHDDやSSDを搭載している場合でも、これらを1つのVG(Volume Group)として扱うことにより管理が楽になります。

Kubernetesからの要求に応じて、VGからLV(Logical Volume)を切り出して利用します。 このとき、LVごとにボリュームのサイズやアクセス権限、rawブロックとして利用するのか、ファイルシステム経由で利用するのかなどを指定することができます。

トポロジーを考慮したダイナミックプロビジョニング

Kubernetesではボリュームを扱うために、PersistentVolumeClaim(PVC)とPersistentVolume(PV)という2種類のリソースを用意しています。 PVは具体的なボリュームを管理するためのリソースで、ストレージのタイプや場所、サイズ、アクセスモードなどの情報を管理しています。 一方のPVCは、利用したいボリュームの要求をおこなうためのリソースです。サイズやアクセスモードなど利用したいボリュームの条件を指定できます。

PodからはPVCを指定して利用したいボリュームの要求を出します。 そうするとKubernetesが条件にマッチした適切なPVをPVCに結びつけ、Podからボリュームを利用できるようにしてくれます。 このようにPodを具体的なボリューム(PV)に依存させないことで、Podの可搬性を高めることができます。

ダイナミックプロビジョニングとは、PVCを宣言すると、PVを事前に用意しておかなくても動的にPVをプロビジョニングしてくれる仕組みです。 TopoLVMではPVCが宣言されると、PVCに指定されたサイズのLV(LogicalVolume)を切り出し、PVとして扱えるようにします。

このとき、切り出したLVはどのノード上からでもアクセスできるわけではありません。 スケジュールされたPodと同じノード上にLVを切り出す必要があります。 このように、Podの位置を考慮してVolumeのプロビジョニングをおこなうことを Topology-Aware Volume Provisioning と呼びます。

TopoLVMは Topology-Aware Volume Provisioning に対応しており、Podのスケジュールされたノード上にボリュームが作成されます。

ノードの空き容量に応じたスケジューリング

Topology-Aware Volume Provisioningにより、Podのスケジュールされたノード上にボリュームが作成されると説明しました。 ではこのとき、Podはどのノードに配置するのがよいでしょうか?

Kubernetesのデフォルトのスケジューラに任せると、CPUやメモリ量、SelectorやAffinityなどの情報に基づいてPodのデプロイ先が決定されます。 そのため、ストレージの空きがないノードにスケジューリングされてしまう可能性があります。 また、空き容量がギリギリのノードにスケジューリングされてしまうと、その後にボリュームのリサイズをおこなうことが難しくなってしまいます。

KubernetesにはScheduler extenderという仕組みがあり、Podのスケジューリングアルゴリズムを拡張することができます。

TopoLVMではScheduler Extenderにより、スケジューラを以下のように拡張しています。

  • ディスク容量不足しているノードはスケジューリング対象から外す
  • 空き容量が多いノードほどスケジューリングスコアが高くなる

これにより、TopoLVMのボリュームを利用するPodは、適切なノードにスケジューリングされるようになります。

ElasticsearchのストレージにTopoLVMをつかってみる

TopoLVMの利用例として、Elasticsearchのストレージとして利用する方法を紹介します。

Kubernetes上にElasticsearchを構築するには、Elastic社の提供しているECK(Elastic Cloud on Kubernetes)を利用することができます。

github.com

ECKではダイナミックプロビジョニングが可能なストレージプラグインを提供しており、このプラグインを利用することでローカルストレージをElasticsearchのストレージとして利用することができます。

しかし、このlocal-volumeプラグインではノードの空き容量に応じたスケジューリングは実現できません*1

そこで、local-volumeプラグインの代わりにTopoLVMを利用してみましょう。

なお、ここでは下記のバージョンを利用することとします。

  • ECK: v1.0.0-beta.1
  • TopoLVM: v0.2.0

まず、kindのKubernetesクラスタ上にTopoLVMのデモ環境を構築します。 DockerのインストールされたUbuntu 18.04環境を用意し、下記のコマンドを実行してください*2

$ git clone https://github.com/cybozu-go/topolvm.git
$ cd topolvm/example
$ make setup
$ make run

次に、ECKをKuberntesクラスタ上にデプロイします*3

$ export KUBECONFIG=$(kind get kubeconfig-path)
$ kubectl apply -f https://download.elastic.co/downloads/eck/1.0.0-beta1/all-in-one.yaml

ログを表示してECKが立ち上がるまで少し待ちます。

$ kubectl -n elastic-system logs -f statefulset.apps/elastic-operator

次に、Elasticsearchをデプロイするためのマニフェストファイル(elastcisearch.yaml)を用意します。 ここでvolumeClaimTemplates を指定することで、Elasticsearchが利用するストレージを変更することができます。 storageClassNameフィールドにtopolvm-provisionerを指定すると、TopoLVMのストレージが利用されることになります。

apiVersion: elasticsearch.k8s.elastic.co/v1beta1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 7.4.2
  nodeSets:
  - name: default
    count: 3
    config:
      node.master: true
      node.data: true
      node.ingest: true
      node.store.allow_mmap: false
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi
        storageClassName: topolvm-provisioner

では、このマニフェストを適用してElasticsearchをデプロイしてみましょう。

$ kubectl apply -f elastcisearch.yaml

しばらくするとElasticsearchが立ち上がり、利用可能な状態になります。

下記のコマンドを実行すると、Elasticsearchの各ノードにLV(Logical Volume)が割り当てられていることを確認することができます。

$ kubectl get pvc,pv
NAME                                                               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
persistentvolumeclaim/elasticsearch-data-quickstart-es-default-0   Bound    pvc-01c8da19-c7f9-4702-b4bd-f9d027d91953   1Gi        RWO            topolvm-provisioner   20m
persistentvolumeclaim/elasticsearch-data-quickstart-es-default-1   Bound    pvc-37c45f53-5bfe-4b11-b1c9-498d2507edfb   1Gi        RWO            topolvm-provisioner   20m
persistentvolumeclaim/elasticsearch-data-quickstart-es-default-2   Bound    pvc-a382b88b-f164-4215-9625-1151868d5791   1Gi        RWO            topolvm-provisioner   20m

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                STORAGECLASS          REASON   AGE
persistentvolume/pvc-01c8da19-c7f9-4702-b4bd-f9d027d91953   1Gi        RWO            Delete           Bound    default/elasticsearch-data-quickstart-es-default-0   topolvm-provisioner            20m
persistentvolume/pvc-37c45f53-5bfe-4b11-b1c9-498d2507edfb   1Gi        RWO            Delete           Bound    default/elasticsearch-data-quickstart-es-default-1   topolvm-provisioner            20m
persistentvolume/pvc-a382b88b-f164-4215-9625-1151868d5791   1Gi        RWO            Delete           Bound    default/elasticsearch-data-quickstart-es-default-2   topolvm-provisioner            20m


$ sudo lvscan
  ACTIVE            '/dev/myvg/07fb3a9d-f8a1-4163-841a-ec41677205ae' [1.00 GiB] inherit
  ACTIVE            '/dev/myvg/8b7bd0fd-5564-4d88-97ba-91b704fe9c55' [1.00 GiB] inherit
  ACTIVE            '/dev/myvg/64ba3ca7-93fa-47ea-b9df-4cccb6c65ed4' [1.00 GiB] inherit

まとめ

今回はLVMを利用したKubernetes向けのローカルストレージ用CSIプラグインについて紹介しました。

現在のところ、ストレージの空き容量だけをみてスケジューリングをおこなっていますが、将来的にはIOPSを考慮したスケジューリングもおこないたいと考えています。 また、Kubernetse 1.16ではCSIのリサイズ機能が導入されました。TopoLVMではこれに対応して、オートリサイズ機能を実現したいと考えています。