Cephが分散ストレージであることを確認してみた

# はじめに

Cephはスケーラブルで高パフォーマンスな分散ストレージシステムです。分散ストレージなので、ファイルやデータを複数のストレージ（HDDやSSD等）にまたがって保存します。

より特徴的なのが、ファイルが一つのストレージにまとまって保存されるのではなく、分割されて複数のストレージに分散して保存される点です。この手法は「ストライピング（RAID0）」と呼ばれます。

ストライピングにより、データの読み書きが並列処理で行われるため、I/Oパフォーマンスの向上が見込めます。例えば、あるファイルがHDD1台に格納されている場合と、HDD3台に分割して保存されている場合を比較すると、後者の方が各HDDにかかる負荷が分散されます。その結果、ストレージ全体でのスループットやIOPSが向上することが期待されます。

:::note info
Cephのストライピングの仕組みについては、以下の記事が参考になります
https://docs.ceph.com/en/mimic/architecture/#data-striping
:::

今回は「Cephが本当に分散ストレージなのか？」を検証するために、大容量のファイルを作成し、そのデータが複数のストレージに分散して格納される様子を確かめてみました。

# Cephクラスターの概要

Cephクラスターは、Kubernetes上でRook Cephを使って作成します。

## スペックおよび利用するツール

AWSのEC2インスタンスに作成したKubernetesを使います。以下のようなスペックです。

* Control Plane（1台）
  * EC2インスタンス
    * OS : Ubuntu 22.04
    * インスタンスタイプ : t3a.small（2vCPU, 2GiBメモリ）
* Worker Node（3台）
  * EC2インスタンス
    * OS : Ubuntu 22.04
    * インスタンスタイプ : t3a.small（2vCPU, 2GiBメモリ）
  * EBSボリューム（OSD用）
    * タイプ : gp3
    * サイズ : 5GiB
    * スループット : 125（デフォルト）
    * IOPS : 3000（デフォルト）

図で表すと、以下のような状態になります。AWSリソースを黒色、k8sのコンポーネントを青色で書いています。

![k8s-ec2-rook-ceph.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/45806127-20a0-bc1f-e329-4a1232e81fd7.png)

## Cephクラスターの構成

Cephクラスター上に、RADOS Block Device（RBD）用のプールを作ります。ファイルやデータを読み書きするCephのクライアントから見ると、RBDは外付けHDDのように認識されます。
クライアントがファイルを作成すると、クライアント側では一つのファイルとして認識されますが、実際のデータはストライピングによって3つのストレージデバイス（＝EBS）に分割されて保存されます。以下がその様子を表したものです。

![ceph-rbd.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/88139baf-1f6f-3226-00b6-b08d052ff47b.png)


# Cephクラスターの構築

筆者記事になりますが、以下のQiitaを参考にしてCephクラスターを作ります。

https://qiita.com/showchan33/items/69bac3db4a831c8a5c15

大きくは、以下の3つの手順に分かれます。

* AWSリソース（EC2やEBS等）の作成
* KubernetesクラスターおよびCNIの構築
* Rook Cephをインストールして、Cephクラスターを構築

今回利用したCeph関連ツールのバージョンは以下です。

* Rook Ceph：v1.16.1
* Ceph：v19.2.0

# RBDの作成

## CephBlockPool の作成

外付けHDDに相当するRBDを作成します。
Kubernetesでは、CephBlockPool というカスタムリソースを使ってRBD用のプールを作成します。以下がそのマニフェストです。

```yaml:singlepool.cephblockpool.yaml
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: singlepool
  namespace: rook-ceph
spec:
  replicated:
    size: 1
```

### レプリカ数を1にしている理由

データのレプリカ数（=``spec.replicated.size``）は、今回はデータがストライピングにより分散されることを検証したいため、1にしています。
本来はレプリカ数は3以上に設定するのが推奨です。しかし、レプリカ数を増やすと、データの分散がストライピングだけでなくレプリケーションによっても発生します。その結果、データの分散がストライピングによるものかレプリケーションによるものかの判別が必要となります。今回はストライピングによるデータの分散のみを確認したいので、レプリカ数は1（レプリカ無し）にしています。
<br>

以下のコマンドで、リソースを作成します。
```
kubectl apply -f cephblockpool.yaml
```

リソースが作成されたことを確認します。
```
$ kubectl get cephblockpool -n rook-ceph
NAME         PHASE   TYPE         FAILUREDOMAIN   AGE
singlepool   Ready   Replicated   host            38h
```

CephのCLIから、``singlepool``というプールが生成されたことも確認できます。
```
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph osd pool ls
.mgr
singlepool
```

## StorageClass の作成

RBDのプールをKubernetes経由で利用可能にするため、StorageClass を作成します。
YAMLが長いので、折りたたみでマニフェストを載せます。

<details><summary>rook-ceph-block.storageclass.yaml（折りたたみ）</summary>

```yaml:rook-ceph-block.storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com # csi-provisioner-name
parameters:
  # clusterID is the namespace where the rook cluster is running
  # If you change this namespace, also change the namespace below where the secret namespaces are defined
  clusterID: rook-ceph # namespace:cluster

  # If you want to use erasure coded pool with RBD, you need to create
  # two pools. one erasure coded and one replicated.
  # You need to specify the replicated pool here in the `pool` parameter, it is
  # used for the metadata of the images.
  # The erasure coded pool must be set as the `dataPool` parameter below.
  #dataPool: ec-data-pool
  pool: singlepool

  # (optional) mapOptions is a comma-separated list of map options.
  # For krbd options refer
  # https://docs.ceph.com/docs/master/man/8/rbd/#kernel-rbd-krbd-options
  # For nbd options refer
  # https://docs.ceph.com/docs/master/man/8/rbd-nbd/#options
  # mapOptions: lock_on_read,queue_depth=1024

  # (optional) unmapOptions is a comma-separated list of unmap options.
  # For krbd options refer
  # https://docs.ceph.com/docs/master/man/8/rbd/#kernel-rbd-krbd-options
  # For nbd options refer
  # https://docs.ceph.com/docs/master/man/8/rbd-nbd/#options
  # unmapOptions: force

  # (optional) Set it to true to encrypt each volume with encryption keys
  # from a key management system (KMS)
  # encrypted: "true"

  # (optional) Use external key management system (KMS) for encryption key by
  # specifying a unique ID matching a KMS ConfigMap. The ID is only used for
  # correlation to configmap entry.
  # encryptionKMSID: <kms-config-id>

  # RBD image format. Defaults to "2".
  imageFormat: "2"

  # RBD image features
  # Available for imageFormat: "2". Older releases of CSI RBD
  # support only the `layering` feature. The Linux kernel (KRBD) supports the
  # full complement of features as of 5.4
  # `layering` alone corresponds to Ceph's bitfield value of "2" ;
  # `layering` + `fast-diff` + `object-map` + `deep-flatten` + `exclusive-lock` together
  # correspond to Ceph's OR'd bitfield value of "63". Here we use
  # a symbolic, comma-separated format:
  # For 5.4 or later kernels:
  #imageFeatures: layering,fast-diff,object-map,deep-flatten,exclusive-lock
  # For 5.3 or earlier kernels:
  imageFeatures: layering

  # The secrets contain Ceph admin credentials. These are generated automatically by the operator
  # in the same namespace as the cluster.
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph # namespace:cluster
  csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph # namespace:cluster
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph # namespace:cluster
  # Specify the filesystem type of the volume. If not specified, csi-provisioner
  # will set default as `ext4`. Note that `xfs` is not recommended due to potential deadlock
  # in hyperconverged settings where the volume is mounted on the same node as the osds.
  csi.storage.k8s.io/fstype: ext4
# uncomment the following to use rbd-nbd as mounter on supported nodes
# **IMPORTANT**: CephCSI v3.4.0 onwards a volume healer functionality is added to reattach
# the PVC to application pod if nodeplugin pod restart.
# Its still in Alpha support. Therefore, this option is not recommended for production use.
#mounter: rbd-nbd
allowVolumeExpansion: true
reclaimPolicy: Delete

```

</details>

同じマニフェストは、以下のGitHubリポジトリにも格納しています。

https://github.com/showchan33/rook-ceph-playground/blob/helm-v1.16.1/manifests/rook-ceph-block.storageclass.yaml

上記のマニフェストを使って、次のコマンドでStorageClassを作成します。

```
kubectl apply -f rook-ceph-block.storageclass.yaml
```

StorageClassが作成されたことを確認します。

```
$ kubectl get storageclass
NAME              PROVISIONER                  RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
rook-ceph-block   rook-ceph.rbd.csi.ceph.com   Delete          Immediate           true                   43h
```


※ CephBlockPoolとStorageClassのマニフェストは、[こちらのサンプル](https://github.com/rook/rook/blob/v1.16.1/deploy/examples/csi/rbd/storageclass.yaml)をもとにして作っています。

# ボリューム（PVC）の作成

RBDのStorageClassを使って、クライアントからマウント可能なボリュームを作成します。

```yaml:rook-ceph-block.pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rook-ceph-block-pvc
  namespace: default
spec:
  storageClassName: rook-ceph-block
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
```

次のコマンドでPVCリソースを作成します。

```
kubectl apply -f rook-ceph-block.pvc.yaml
```

PVCが作られたのを確認します。
```
$ kubectl get pvc -n default
NAME                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      VOLUMEATTRIBUTESCLASS   AGE
rook-ceph-block-pvc   Bound    pvc-0be27c77-4311-4711-a0dd-9749ed58b84d   10Gi       RWO            rook-ceph-block   <unset>                 43h
```

## （参考）PVCとRBDイメージの対応

上記でPVCの情報を出力した際、3列目の``VOLUME``に、``pvc-0be27c77-4311-4711-a0dd-9749ed58b84d``と書かれています。これは、PVC作成時に動的プロビジョニングによってPVも同時に作られた事を意味します。

このPVのマニフェストを覗いてみると、``volumeHandle``に``rook-ceph``というワードが含まれたボリュームIDが書かれているのを確認できます。

```
$ kubectl get pv pvc-0be27c77-4311-4711-a0dd-9749ed58b84d -oyaml | grep volumeHandle
    volumeHandle: 0001-0009-rook-ceph-0000000000000002-9c911d92-3168-4920-b12d-1730f8f7d948
```

このPVCに対応するRBDのイメージは、Ceph CLIで以下のコマンドで確認できます。
```
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- rbd ls singlepool
csi-vol-9c911d92-3168-4920-b12d-1730f8f7d948
```

後半の ``9c911d92-3168-4920-b12d-1730f8f7d948`` がPVの``volumeHandle``とRBDイメージで共通しており、Kubernetesで作成したリソースがCephとしっかり連動している事がわかります。

# データが分散して格納されることの確認

ようやく準備が整いました。
ここからは、Ceph(RBD)のクライアントが作成した大容量のファイルが、それぞれのストレージ（≒OSD）に分散して格納されることを確認していきます。

## ディスク利用状況の確認（ファイル作成前）

まずは、現在の各OSDの利用状況をCephのCLIで確認します。
```
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph osd df
ID  CLASS  WEIGHT   REWEIGHT  SIZE    RAW USE  DATA     OMAP    META    AVAIL    %USE  VAR   PGS  STATUS
 1    ssd  0.00490   1.00000   5 GiB   30 MiB  1.2 MiB  10 KiB  28 MiB  5.0 GiB  0.58  1.00   11      up
 0    ssd  0.00490   1.00000   5 GiB   29 MiB  1.2 MiB  11 KiB  28 MiB  5.0 GiB  0.58  1.00    9      up
 2    ssd  0.00490   1.00000   5 GiB   29 MiB  1.2 MiB  12 KiB  28 MiB  5.0 GiB  0.58  1.00   15      up
                       TOTAL  15 GiB   89 MiB  3.7 MiB  34 KiB  85 MiB   15 GiB  0.58
MIN/MAX VAR: 1.00/1.00  STDDEV: 0
```
``DATA``の列を見ると、それぞれのOSDで合計1.2MiBのデータが格納されていることがわかります。

## RBDをマウントするPodの作成

RBDイメージにファイルを作成するためのPodを、以下のマニフェストでデプロイします。

```yaml:mnt-rbd.pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: mnt-rbd
  namespace: default
spec:
  containers:
  - name: mnt-rbd
    image: busybox
    command:
    - tail
    - -f
    - /dev/null
    volumeMounts:
    - name: rbd-volume
      mountPath: /data
  volumes:
  - name: rbd-volume
    persistentVolumeClaim:
      claimName: rook-ceph-block-pvc
```

Pod作成コマンドは以下です。
```
kubectl apply -f mnt-rbd.pod.yaml
```

程なくして、RBDをマウントしたPodが作成されます。
```
$ kubectl get po -n default
NAME      READY   STATUS    RESTARTS   AGE
mnt-rbd   1/1     Running   0          19s
```

Pod内の``/data``ディレクトリが、RBDでマウントされていることを確認できます。
```
$ kubectl exec -it -n default mnt-rbd -- df -h | grep /data
/dev/rbd0                 9.7G     24.0K      9.7G   0% /data
```

## ファイル作成 → 分散されていることの確認

``mnt-rbd``のPod上で3GiBの大容量ファイルを作成します。

```
kubectl exec -it -n default mnt-rbd -- sh
```

```
(Pod内で実行)
/ # dd if=/dev/zero of=/data/test_file bs=1M count=3072
```

``/data``ディレクトリに、確かに3GiBのファイルが作成されたのが確認できます。
```
(Pod内で実行)
/ # ls -l /data
total 3145748
drwx------    2 root     root         16384 Jan  5 04:08 lost+found
-rw-r--r--    1 root     root     3221225472 Jan  5 04:18 test_file
```

続いて、CephのCLIでOSDの利用状況を確認してみます。

```
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph osd df
ID  CLASS  WEIGHT   REWEIGHT  SIZE    RAW USE   DATA     OMAP    META    AVAIL    %USE   VAR   PGS  STATUS
 1    ssd  0.00490   1.00000   5 GiB  1006 MiB  973 MiB  10 KiB  32 MiB  4.0 GiB  19.64  0.95   11      up
 0    ssd  0.00490   1.00000   5 GiB   758 MiB  730 MiB  11 KiB  28 MiB  4.3 GiB  14.80  0.72    9      up
 2    ssd  0.00490   1.00000   5 GiB   1.4 GiB  1.3 GiB  12 KiB  32 MiB  3.6 GiB  27.53  1.33   15      up
                       TOTAL  15 GiB   3.1 GiB  3.0 GiB  34 KiB  93 MiB   12 GiB  20.66
MIN/MAX VAR: 0.72/1.33  STDDEV: 5.25
```

``DATA``の列を見ると、各OSDの値が1.2MiBから1GiB程度に増えて、3GiBのファイルが分散されて格納されていることを確認できました。


# おわりに

「Cephのデータが一つのファイルでも複数ストレージに分散されている」ことを何となくは理解していたのですが、今回の検証を行うことで、より理解を深められました。