Rook-Ceph における RBD 管理の仕組みを調べてみた

Rook-Ceph における RBD（RADOS Block Device）がどのようにプロビジョニングされ、どのようにノード上に配置されているのかを調べてみた。本記事は CephFS や RadosGW については説明しない。

## Ceph 概要
Ceph はストレージクラスタを構成するための OSS である。Ceph は主に OSD および MON と呼ばれる重要なコンポーネントからなる。

### OSD (Object Storage Device)
OSD は、Ceph クラスタ内部で直接的にストレージを管理するコンポーネントである。OSD は BlueStore と呼ばれる独自のフォーマットを用いてストレージを消費し、実際のデータは RADOS object という単位で保存される。各 OSD は、自身に割り当てられたストレージ上にデータを分散配置し、クラスター全体でのデータの冗長性と可用性を確保する。RADOS object の配置は固定的ではなく、OSD の追加や削除が発生した際に Rebalance と呼ばれる処理によってデータが再分配され、ストレージの利用効率が最適化される。

### MON (Monitor)
MON は、Ceph クラスタの管理やモニタリングを担うコンポーネントである。特に、CRUSH マップと呼ばれる情報の管理し、一貫性を保つ上で重要な役割を果たしている。

RADOS オブジェクトの配置および読み出し時のファイルの復元は CRUSH アルゴリズムに基づいて行われるが、このプロセスには、RADOS オブジェクトの配置やストレージの階層構造が定義された CRUSH マップを参照する必要がある。MON は、 CRUSH マップの一貫性を維持することで、管理する。これにより、クラスター内でのデータの正確な配置が担保される。

### RBD (RADOS Block Device)
RBD (RADOS Block Device) は、Ceph が提供する仮想ブロックストレージであり、実態としては RADOS object として OSD に格納されている。RBD は、クライアント側からは通常のブロックデバイスのように見えるため、ext4 や xfs のような一般的なファイルシステムとしてフォーマット可能である。これを所望のディレクトリにマウントすれば、ユーザーはファイル単位でデータにアクセスすることができる。

## Rook-Ceph 概要
Rook-Cephは、Ceph クラスターを Kubernetes で簡単に構築・管理するための OSS である。簡単に表現すれば、Ceph を Kubernetes のエコシステムに組み込むために、CSI と Operator の仕組みを追加したものと言える。以下、CSI について少し触れる。

### CSI (Container Storage Interface)

CSIは、Kubernetesを含むあらゆるコンテナオーケストレーションシステムとストレージシステムの間で標準化されたインターフェースである。CSI はプラグイン方式を採用しており、様々なストレージベンダーが CSI plugin を実装することで、Kubernetes 上でストレージプロビジョニングを統一されたやり方で提供することができる。


CSI が動作する過程については、RedHat の公式ブログに詳細な解説記事がある：

https://www.redhat.com/ja/blog/persistent-volume-support-peer-pods-technical-deep-dive

CSI plugin は storageClass に関連付けられており、PVC に基づいてノード上でストレージをダイナミック・プロビジョニングを行い、 Pod にそのストレージリソースを割り当てることができる。具体的には、CSI plugin の `NodeStageVolume` および `NodePublishVolume` と呼ばれる機能が該当する。

以下に、上記の記事内 "Mounting a volume" 節内の図 "Figure 10: Workflow when mounting a volume" を引用する。同図はKubelet が CSI plugin の機能を呼び出すフローを示している。

![peer-pods-figure10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/56a91cf2-6680-855a-6e5f-f8d663b5ae4f.png)

それぞれの CSI 機能の簡易な説明と、利用するマウントポイントを表にまとめると以下のようになる：

| CSI の機能 | 説明 |Mount 先の path |
|-|-|-|
|`NodePublishVolume`| Node 上の global mount point にマウントし、ストレージを用意・フォーマットする | `/var/lib/kubelet/plugins/kubernetes.io/csi/<pv>/globalmount/`|
|`NodeStageVolume`| Pod の mount point にマウントする |`/var/lib/kubelet/pods/<pod>/volumes/kubernetes.io~csi/<pvc>/mount` |

## RBD の管理

### Rook-Ceph における CSI plugin の実装

Rook-Ceph における CSI plugin は次の GitHub レポジトリで管理されている。

https://github.com/ceph/ceph-csi/

例として、RBD plugin における [`NodeStageVolume()`](https://github.com/ceph/ceph-csi/blob/devel/internal/rbd/nodeserver.go#L370-L377) を見てみると、以下のような具体的な処理を確認することができる：

- [`rbd map` コマンドによる RBD pool から device への map](https://github.com/ceph/ceph-csi/blob/devel/internal/rbd/rbd_attach.go#L433-L510)
- [`mkfs.<format-type>` コマンドによる RBD のフォーマット](https://github.com/ceph/ceph-csi/blob/4a463533ab3828b389e6e5d084de18159553ee40/internal/rbd/nodeserver.go#L791-L829)
- [global mount point へのマウント](https://github.com/ceph/ceph-csi/blob/4a463533ab3828b389e6e5d084de18159553ee40/internal/rbd/nodeserver.go#L831-L836) 


### 全体的なコンポーネントの関係図
上記を踏まえて、OSD のブロックストレージと RBD へのデータの対応を図示してみると次のようになる：

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3236313732372f62303836653863642d313237382d343932392d336664662d6663353434333762383634312e706e67.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/7716128a-3cc3-ca42-8420-5da1409cef6e.png)

ストレージリソースの提供側である Ceph が BlueStore を消費し OSD を構成している。ストレージリソースを消費するアプリケーション側は RBD を利用する。OSD-RBD 間は CRUSH アルゴリズムによる分散・統合の仕組みによって抽象化されている。

## 実機の確認

上記の概要で説明したような仕組みが、Kubenetes 上では実際どのように見えているのか、いくつかの項目に分けて確認してみる。

普通に Kubernetes + Rook/Ceph で環境を用意するのが王道だが、筆者は OKD4 (Community 版 OpenShift) と OpenShift Data Foundation (Rook-Ceph 含むストレージ統合基盤) の環境があるので、そちらを用いて検証する。筆者の環境構築の方法については下記の Qiita 記事も参考：

https://qiita.com/sawa2d2/items/f8c2b37340b59b4ee579


### storageClass とその消費者の確認

storageClass を確認すると、次のものが確認できる。
```
$ oc get sc
NAME                                    PROVISIONER                             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
localblock                              kubernetes.io/no-provisioner            Delete          WaitForFirstConsumer   false                  15h
ocs-storagecluster-ceph-rbd (default)   openshift-storage.rbd.csi.ceph.com      Delete          Immediate              true                   15h
...
```

この storageClass に紐付く PVC リストアップする：

```
$ oc get pvc --all-namespace
NAMESPACE           NAME                                     STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS                  AGE
openshift-storage   ocs-deviceset-localblock-0-data-0tbv7k   Bound    local-pv-7471b93b                          1Ti        RWO            localblock                    15h
openshift-storage   ocs-deviceset-localblock-0-data-1z247p   Bound    local-pv-af99c388                          1Ti        RWO            localblock                    15h
openshift-storage   ocs-deviceset-localblock-0-data-24jph7   Bound    local-pv-e9f02179                          1Ti        RWO            localblock                    15h
openshift-storage   db-noobaa-db-pg-0                        Bound    pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03   50Gi       RWO            ocs-storagecluster-ceph-rbd   15h
```

それぞれ PVC を発行している Pod を調べてみる。
利用する PVC の情報は Pod 側が持っているため、下記のようなスクリプトを作って探してみる：

```bash:pvc_to_pv.sh
#!/bin/bash

PVC_NAME=$1
oc get pods --all-namespaces -o yaml |\
yq e -N ".items[] | select(.spec.volumes[].persistentVolumeClaim.claimName == \"$PVC_NAME\") | .metadata.name + \" -n \" + .metadata.namespace"
```

それぞれの PVC に紐づく Pod を特定する：

```
$ ./pvc_to_pv.sh ocs-deviceset-localblock-0-data-0tbv7k
rook-ceph-osd-1-d7b7fbd64-655sq -n openshift-storage
$ ./pvc_to_pv.sh ocs-deviceset-localblock-0-data-1z247p
rook-ceph-osd-0-7d87ddb68f-57cp6 -n openshift-storage
$ ./pvc_to_pv.sh ocs-deviceset-localblock-0-data-24jph7
rook-ceph-osd-2-6f7c4b654-swsgb -n openshift-storage

$ ./pvc_to_pv.sh  db-noobaa-db-pg-0
noobaa-db-pg-0 -n openshift-storage
```

次のことが確認できた。

- storageClass `localblock` に紐づく PV は、OSD の pod が消費している
- storageClass `ocs-storagecluster-ceph-rbd` に紐づく PV は、通常のアプリケーション (例:`noobaa-db-pg-0`) の pod が消費している

### PV (RBD) の内容を Node 上から確認
まずはどこのノード上に RBD があるのか探してみる。PV がマウントしているパスには PV 名が含まれているため、各ノード上で `lsblk` を実行して `grep` に引っ掛かればよい。次のスクリプト `find_rbd.sh` を用意する：

```bash:find_rbd.sh
#!/bin/bash

usage()
{
  echo "Usage:"
  echo "  ./find_rbd.sh <pvname>"
}

# Parse arguments
if [ "$#" -ne 1 ]; then
    usage
    exit 1
fi

pvname=$1
nodes=$(oc get nodes --no-headers)

while read -r line; do
  node=$(echo "$line" | awk '{print $1}')
  status=$(echo "$line" | awk '{print $2}')
  if [ "$status" != "Ready" ]; then
    continue
  fi

  lsblk_result=$(oc debug node/${node} -- chroot /host lsblk -d 2> /dev/null)
  echo "${lsblk_result}" | grep "/${pvname}/" > /dev/null
  if [[ $? -eq 0 ]]; then
    echo "An RBD found on $node:"
    # Output until the indentation ends
    echo "${lsblk_result}" | awk -v pvname="${pvname}" '
      {
        if (found) {
          if ($0 ~ /^ {32}/) {
            print $0
          } else {
            found = 0
          }
        }
        if ($0 ~ pvname) {
          print $0
          found = 1
        }
      }'
    exit 0
  fi
done <<< "$nodes"

echo RBD not found.
exit 1
```

まずは適当に、storageClass `ocs-storagecluster-ceph-rbd` によってプロビジョニングされた pv を見繕う：
```
$ oc get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                 STORAGECLASS                  REASON   AGE
...
pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03   50Gi       RWO            Delete           Bound    openshift-storage/db-noobaa-db-pg-0   ocs-storagecluster-ceph-rbd            12h
```

上記のスクリプトで pv が置かれているノードを検索してみる：
```
$ ./find_rbd.sh pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03
An RBD found on master1:
rbd0  251:0    0    50G  0 disk /var/lib/kubelet/pods/978a4ffb-5eb8-474d-aea0-3b0324a35791/volumes/kubernetes.io~csi/pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03/mount
                                /var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.rbd.csi.ceph.com/f3969b7dbf6aac8a24ec3aa7f9ac07f65cda149ebcceedb1446a27f234d507c8/globalmount/0001-0011-openshift-storage-0000000000000001-040c6736-a99c-4ca6-9437-fd34fdc7c584
```

実態の RBD はノード `master1` 上のデバイス `/dev/rbd0` として存在している。また、CSI plugin によって、次の2箇所にマウントされていることが確認できる

次に、`master1` にログインしてみる：
```
$ oc debug node/master1
Starting pod/master1-debug-ngtcq ...
To use host binaries, run `chroot /host`
Pod IP: 10.0.0.12
If you don't see a command prompt, try pressing enter.
sh-4.4# chroot /host
```

中身を確認してみる：
```
sh-5.2# ls -la /var/lib/kubelet/pods/978a4ffb-5eb8-474d-aea0-3b0324a35791/volumes/kubernetes.io~csi/pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03/mount 
total 32
drwxrwsr-x. 4 root  root  4096 Jun 23 17:10 .
drwxr-x---. 3 root  root    40 Jun 24 02:42 ..
drwxr-sr-x. 3 10001 root  4096 Jun 23 17:10 data
drwxrws---. 2 root  root 16384 Jun 23 17:09 lost+found
-rw-r--r--. 1 10001 root   738 Jun 24 02:51 openshift-custom-postgresql.conf
-rw-r--r--. 1 10001 root   832 Jun 24 02:51 passwd
```

ついでに Disk format も確認してみる。ext4 でフォーマットされていることが確認できる。
```
sh-5.2# df -T /var/lib/kubelet/pods/978a4ffb-5eb8-474d-aea0-3b0324a35791/volumes/kubernetes.io~csi/pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03/mount 
Filesystem     Type 1K-blocks  Used Available Use% Mounted on
/dev/rbd0      ext4  51290592 67320  51206888   1% /var/lib/kubelet/pods/978a4ffb-5eb8-474d-aea0-3b0324a35791/volumes/kubernetes.io~csi/pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03/mount
```

### RBD のマウントオプションの確認

上記で調べた `/dev/rbd0` は次の2つのマウントポイントを持っていた：

1. `/var/lib/kubelet/pods/978a4ffb-5eb8-474d-aea0-3b0324a35791/volumes/kubernetes.io~csi/pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03/mount` (Pod の mountpoint)
1. `/var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.rbd.csi.ceph.com/f3969b7dbf6aac8a24ec3aa7f9ac07f65cda149ebcceedb1446a27f234d507c8/globalmount/0001-0011-openshift-storage-0000000000000001-040c6736-a99c-4ca6-9437-fd34fdc7c584` (Node の mountpoint)

それぞれのマウントオプションを調べてみる。

1.) Pod の mount option:
```
core@master1:~$ cat /proc/mounts | grep /var/lib/kubelet/pods/978a4ffb-5eb8-474d-aea0-3b03
tmpfs /var/lib/kubelet/pods/978a4ffb-5eb8-474d-aea0-3b0324a35791/volumes/kubernetes.io~projected/kube-api-access-729v8 tmpfs rw,seclabel,relatime,size=4194304k,inode64 0 0
/dev/rbd0 /var/lib/kubelet/pods/978a4ffb-5eb8-474d-aea0-3b0324a35791/volumes/kubernetes.io~csi/pvc-282f19a9-68f0-49ae-b4c8-3cacf4f10c03/mount ext4 rw,seclabel,relatime,stripe=16 0 0
```

2.) Node の mount option:
```
core@master1:~$ cat /proc/mounts | grep /var/lib/kubelet/plugins/kubernetes.io/csi/opens
/dev/rbd0 /var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.rbd.csi.ceph.com/f3969b7dbf6aac8a24ec3aa7f9ac07f65cda149ebcceedb1446a27f234d507c8/globalmount/0001-0011-openshift-storage-0000000000000001-040c6736-a99c-4ca6-9437-fd34fdc7c584 ext4 rw,seclabel,relatime,stripe=16 0 0
```

`bind` option がついていないことから、それぞれは bind mount ではなく直接デバイス `/dev/rbd0` から mount していることがわかる。