Rook/Ceph でラック間のデータの引っ越しを安全かつ効率的に行う

この記事は、CYBOZU SUMMER BLOG FES '26 の記事です。

こんにちは、クラウド基盤本部の伴野です。クラウド基盤本部では、サイボウズのクラウド基盤である cybozu.com を稼働させるため、 Neco と呼ばれる Kubernetes クラスタを当社で調達したサーバー上に構築しています。

この記事では、Neco のサーバーをラックごとまとめて追加・撤去する際に、 そのラックに搭載された多数のサーバー上にあるデータを安全かつ効率的に、無停止で別のラックに引っ越すためのテクニックについて紹介します。

背景

私はクラウド基盤本部の中でも CSA(Cloud Storage Agency)というチームに属しています。CSA は「cybozu.com にスケーラブルで信頼性のあるストレージを提供する」というミッションのもと、Neco 上のストレージに関わる開発や運用を担当するチームです。CSA が提供するストレージシステムの詳細に関しては、筆者が書いた別の記事がありますので、そちらもご覧ください:

blog.cybozu.io

CSA が運用しているストレージシステムの一つに Rook/Ceph1 があります。 Rook/Ceph はオープンソースの分散ストレージシステムで、 複数台のサーバーをクラスタ化して使用することにより、スケーラブルなストレージプールを提供します。 Rook/Ceph が提供するストレージには仕様上のサイズの制限はなく、サーバーを追加すればするだけストレージプールを大きくできます。 サーバーの追加は Rook/Ceph の稼働中に無停止で行うことができ、追加すると、Rook/Ceph が自律的に既存のサーバーから新規のサーバーへデータを移動してくれます。

また Rook/Ceph は耐障害性を重視して設計されています。保存される全てのデータは冗長化されていて、一部のディスクやサーバーが故障したとしても、 ダウンタイムなくストレージを提供し続けることができます。デフォルトでは、Rook/Ceph は 一つのデータに対して 2 つのレプリカを自動的に作成し、計 3 つのデータを保存します2。また設定を調整することで、これらのデータをそれぞれ別のラックに配置できます。 これにより、ディスクやサーバー、ラックが故障してデータの冗長性が失われた場合にも、残り 2 つのデータを使ってサービスを継続できます。

さらに Rook/Ceph は自律的にデータをコピーして冗長性を回復する機能も備えています。 そのため、サーバー故障時にも人が介入することなく、3 つのデータがある正常な状態に戻ることができます。

このような Rook/Ceph の仕組みによって、Neco では日々起こるサーバーやディスクの故障に対して頑強なストレージを提供しています。

サーバーラックのローリングリプレース

Neco ではサーバーを我々自身が調達している都合上、新しいサーバーの追加や、サーバーの保守期限切れによる撤去について考慮しなくてはなりません。 通常 Neco では、サーバーラック単位で機材を購入します。そのため、新しいサーバーはラックに格納された状態でデータセンターに導入され、 そのラックに搭載された数十台のサーバーが同じ保守期限を持つことになります。 逆に、保守期限が迫ってきたサーバーは、ラックごとデータセンターから撤去されます。このラックの追加・撤去をまとめて、 この記事ではローリングリプレースと呼ぶことにします。

Rook/Ceph が管理するストレージプールも、このラックのローリングリプレースの影響を受けます。 ラックが追加された際は、そこに搭載されたサーバーを Rook/Ceph クラスタにまとめて追加します。 逆にラックの撤去時には、サーバーを Rook/Ceph クラスタの管理下から削除します。 CSA ではローリングリプレースのライフサイクルを考えながらストレージの管理を行っています。

ところで、前節で説明した Rook/Ceph の特性を考えると、ローリングリプレースの際に特別な作業は不要に思えるかもしれません。 つまり、まずデータ移行先の新しいラックが追加された段階で、新しいラックにあるサーバーを Rook/Ceph の管理下に追加します。 すると、存在する全てのサーバーを利用するように Rook/Ceph が自動的にデータの移動を行います。 その結果、データは古いラックと新しいラックの両方にまたがって配置されます。

次に、撤去するラックに搭載されたサーバーを Rook/Ceph の管理から外します。すると、 管理から外れたサーバーに載っていたデータが失われたと Rook/Ceph が判断して、 他のレプリカから自律的にデータを復元し、別のサーバーへ載せ替えてくれます。

そのため、ラックの追加・撤去それぞれに合わせて Rook/Ceph 管理下にサーバーを追加・削除さえすれば、あとは人の介入なくローリングリプレースに対応できそうです。

しかし実際に Rook/Ceph を運用するにつれて、この方法には二つの問題があることが分かりました。 以下のセクションでは、CSA がどのような問題を見つけ、それにどう対処したかを説明します。

ラックの追加

一つ目の問題点は、ラックの追加時、新しいサーバーを Rook/Ceph の管理下に追加するという作業がそれほど簡単ではないという点です。

Rook/Ceph では OSD(Object Storage Daemon)と呼ばれるコンポーネントを各サーバー上でディスクごとに立ち上げることで、 そのディスクを Rook/Ceph クラスタに組み込みます。 従って新しいサーバーが追加された際には、既存のサーバーで動作している OSD の Pod を保ったまま、 新規サーバー上でも OSD の Pod を立ち上げなければなりません。

しかし Rook/Ceph が OSD を追加する際は、空いているディスクを単に見つけて OSD を起動します。 Neco では機材に余裕を持たせており、撤去したいサーバーにも空いているディスクが存在します。そのため、Rook/Ceph に OSD の追加を指示すると、 撤去予定のサーバーで新規 OSD を起動してしまうことがあります。 そうなると、所望のサーバーに OSD が載るまで OSD の作成をトライし続けなければならず、極めて非効率的です。Node affinity を OSD の Pod に指定して 対策しようにも、Rook/Ceph の実装上、OSD の Pod には同一の node affinity しか指定できません。そのため、 既存の OSD にも新規 OSD 用の node affinity が影響を与えてしまい、望ましくありません。

そこで CSA では、Rook/Ceph が OSD を追加する際に作る OSD Prepare Job に着目しました。 OSD Prepare Job は、OSD を新規に作成する直前に Rook/Ceph が自動的に作成する Kubernetes の Job で、その OSD が使用するディスクの初期化などを行います。 つまり OSD Prepare Job と OSD はディスクを共用しており、OSD Prepare Job が動作した後に同じサーバー上・同じディスク上で OSD が動作を開始します。

逆に言えば、OSD Prepare Job に node affinity を設定して所望のサーバーに誘導することで、直後に起動する OSD も同じサーバー上で動作することになります。 また OSD Prepare Job は OSD の新規作成時に一度のみ動作する Job のため、既存の OSD に影響を与えることもありません。

Rook/Ceph クラスタの管理は CephCluster カスタムリソースを介して行います。 このリソースの .spec.storage.storageClassDeviceSets[*].preparePlacement を編集することで、 OSD Prepare Job に node affinity を設定できます。そこで、新規 OSD を載せたくないサーバーにはあらかじめ retired-soon ラベルを つけておき、これを preparePlacement に指定することで、CSA では一つ目の問題を解決することができました。

apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph
spec:
  storage:
    storageClassDeviceSets:
      - name: set1
        preparePlacement:
          nodeAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
                - matchExpressions:
                    # 撤去予定のサーバーには新規 OSD が載らないようにする。
                    - key: retired-soon
                      operator: DoesNotExist

ラックの撤去

二つ目の問題はラックの撤去に関わるもので、ラックを撤去したあとデータが復元されるまでの間は、 データの冗長性が損なわれるという問題です。例えばデフォルトの Rook/Ceph クラスタの場合、先述したように 1 つのデータに対して 2 つのレプリカが 作成されるため本来データは 3 つあるはずですが、データの復元中は 2 つしかない状態が継続します。 これは危険な状態です。もしデータの復元中に偶発的にディスクが故障すると、2 つのデータのうち片方が失われ、冗長性が完全に失われてしまうかもしれません。

冗長性が下がるのを避けるには、ラックの撤去によってデータが失われるよりも前に、撤去対象ラック上のレプリカをそれ以外のラックへ載せ替える必要があります。 ただラックの撤去前では、Rook/Ceph から見るとまだクラスタの容量構成や冗長性が変わっていないため、自動的なデータの移動は当然始まりません。

そこで CSA では、OSD の weight を調整することでデータ移動を引き起こすという手法を採用しています。

各 OSD にはいくつかのパラメーターが設定されており、Rook/Ceph はそれらを使って、 どの OSD にどの程度データを載せるかを決定します。Weight はそのようなパラメーターの一つで、 OSD の容量に比例した値が通常は設定されます。それにより、容量が大きい OSD に、より多くのデータが配分されるようになっています。 どの OSD にどのような weight が割り当たっているかは ceph osd df を実行すると WEIGHT 列で確認できます:

$ kubectl exec -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
 0    ssd  0.00099   1.00000  1 GiB   27 MiB  732 KiB    1 KiB  26 MiB  997 MiB  2.65  1.00   20      up
 1    ssd  0.00099   1.00000  1 GiB   27 MiB  732 KiB    1 KiB  26 MiB  997 MiB  2.65  1.00   57      up
 2    ssd  0.00099   1.00000  1 GiB   27 MiB  732 KiB    1 KiB  26 MiB  997 MiB  2.65  1.00   12      up
                       TOTAL  3 GiB   81 MiB  2.1 MiB  4.7 KiB  79 MiB  2.9 GiB  2.65                   
MIN/MAX VAR: 1.00/1.00  STDDEV: 0

CSA ではラック撤去に先立ち、撤去されるサーバーにある(つまりデータを退避させる)OSD の weight を 0 にします3。これには ceph osd crush reweight コマンドを使います。 このコマンドは第一引数に OSD の ID(osd.0)、第二引数に weight(0)をとります。

$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph osd crush reweight osd.0 0
reweighted item id 0 name 'osd.0' to 0 in crush map

$ kubectl exec -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
 0    ssd        0   1.00000  1 GiB   27 MiB  840 KiB    1 KiB  26 MiB  997 MiB  2.66  1.00   20      up
 1    ssd  0.00099   1.00000  1 GiB   27 MiB  840 KiB    1 KiB  26 MiB  997 MiB  2.66  1.00   69      up
 2    ssd  0.00099   1.00000  1 GiB   27 MiB  816 KiB    1 KiB  26 MiB  997 MiB  2.65  1.00   20      up
                       TOTAL  3 GiB   82 MiB  2.4 MiB  4.7 KiB  79 MiB  2.9 GiB  2.66
MIN/MAX VAR: 1.00/1.00  STDDEV: 0.00

これによって、古いラックにある OSD は Rook/Ceph から認識はされているものの、 その容量が 0 として扱われます。その結果、撤去予定の OSD から他の OSD へのデータ移動が Rook/Ceph によって行われます。 データ移動の完了後に空っぽになった OSD を purge し、ラックを撤去します。

ここで重要なのは、いきなりラックの撤去を行う先述の手法とは異なり、この方法ではデータの冗長性が損なわれないという点です。 古い OSD は Rook/Ceph クラスタに組み込まれたままで、その中のデータも残存しています。移動対象のデータは、新しい OSD に移動して冗長性が担保された後に 古い OSD から削除されるため、仮に OSD が偶発的に故障したような場合でもデータの冗長性を維持できます。

ところで、古いバージョンの Rook/Ceph では weight が 0 になった OSD を正しく扱うことができず、 PodDisruptionBudget の設定に失敗することがありました。 CSA がアップストリームへ働きかけを行った結果、現在はこのバグは修正されています。詳細は以下のリンク先をご参照ください。

github.com

github.com

まとめ

この記事では、Rook/Ceph で管理しているデータを、ラックの追加・撤去に際して安全かつ効率的に移動させる手法について紹介しました。

紹介した作業は、CephCluster のようなリソースを変更すれば最終的に理想的な状態へ自動的に収束する という Kubernetes の思想とは相容れないものですが、残念ながら現在の Rook/Ceph では自動的に行うことができません。 そのため CSA では、上述した preparePlacement の変更や OSD の weight 調整といった作業を一部スクリプト化し、運用コストを下げています。 今後もより安全かつ簡単にローリングリプレースを進められるよう、運用の改善を続けていきます。

ところで、これらの手法はラックの追加・撤去の時だけでなく、Rook/Ceph のデータを大規模に移動するような場合にはいつでも便利です。 例えば、CephCluster カスタムリソースの storageClassDeviceSets の設定を変えたい場合、 単にマニフェストを書き換えるだけでは Rook/Ceph が追従してくれないため、OSD の交換が必要になります。 このような場合も、新規要素を storageClassDeviceSets に追加し、 古い storageClassDeviceSets にあるデータを reweight で押し出し、 その後古い storageClassDeviceSets を削除するという方法で交換を行うことができます。 Rook/Ceph クラスタを管理されている方は、ぜひ活用してみてください。

この記事は CYBOZU SUMMER BLOG FES '26 の記事でした。 ここまで読んでいただきありがとうございました。


  1. この記事では「Rook/Ceph」とまとめて呼称しますが、厳密には Ceph が分散ストレージシステムそのもので、Rook は Ceph を Kubernetes で動かすためのソフトウェア(オーケストレーター)です。
  2. Ceph のプールには replicated と erasure-coded の 2 種類がありますが、この記事では replicated を前提にしています。
  3. OSD には weight の他に、ceph osd dfREWEIGHT 列で出力されるパラメーターもあります。こちらを ceph osd reweightceph osd out といったコマンドで変更してもデータ移動を引き起こすことができるのですが、空になった OSD を削除した後に再度のデータ移動が発生してしまうようなので避けています。