Kubernetesで利用可能な分散ストレージのOpenEBSを探求してみた

こんにちは、アプリケーション基盤チームの池添(@zoetro)です。

サイボウズでは、cybozu.comのアーキテクチャ刷新プロジェクト(Necoプロジェクト)を実施しています。

Necoプロジェクトでは、現在以下のようなテーマに取り組んでいます。

  • ハードウェアプロビジョニングの容易化・自動化
  • 障害に強く、スケールするネットワークアーキテクチャの検討
  • Kubernetesクラスタの構築
  • 高耐久性・高可用性ストレージアーキテクチャの調査

今回は、Kubernetesで利用可能な分散ストレージのOpenEBSを調査したので、その結果を報告したいと思います。

TL; DR

  • OpenEBSのアーキテクチャを解説
  • 設計がシンプルで使い勝手はよさそうだが、耐久性を担保するための機能がやや不足
  • 性能評価の結果は芳しくなかった。高IOPS/高スループットでの利用は難しいか
  • 今回は採用を見送るが、今後の成長に期待

背景

cybozu.comでは数百テラバイトの顧客データを扱っています。
現在はRAID6による冗長化をおこなってハードディスクの故障に備えたり、 RAID1によりレプリケーション用ストレージサーバとミラーリングして、サーバや電源装置の故障に備えています。
さらに1日1回、東日本のデータセンターから西日本のデータセンターへのバックアップをおこない、 大規模なハードウェアの故障や、災害などによるデータセンターの障害に備えた構成を実現しています。

現状の構成でも高い耐久性と可用性を実現できているわけですが、 下記のような要求を満たしたいことから、ストレージアーキテクチャの見直しを考えています。

  • 今後の顧客数やサービスの拡大が見込まれるため、よりスケールするアーキテクチャにしたい
  • Kubernetesの導入に伴い、アプリケーション開発者が容易にストレージを利用できるようにしたい

簡単に言うと、AWSのEBSやGCPのPersistentDiskに相当するようなものを自社データセンターに構築したいということです。

OpenEBS

OpenEBSはKubernetesなどの環境上において、コンテナ化されたストレージを提供するための分散ストレージ技術です。

Kubernetesで利用可能な分散ストレージ技術には、Ceph(+Rook)やCinderなどの選択肢もあるのですが、 今回はシンプルで扱いやすそうな印象のOpenEBSを調査対象としました。

なお、本記事での調査結果は下記のバージョンに基づいています。
今後のバージョンアップで、本記事に記載されている内容と実態が異なる可能性もあるのでご注意ください。

  • OpenEBS 0.5.1
  • Kubernetes 1.9.3

ソースコード

OpenEBSの調査にあたり、下記のリポジトリのコードリーディングをおこないました。

設計も理にかなっているし、シンプルな実装なのでとても読みやすいコードでした。
しかし、ほとんどコメントが書かれておらず、ドキュメントやテストも少ないことが少々気になります。

アーキテクチャ

まずはOpenEBSのアーキテクチャを解説していきます。 architecture.png

OpenEBSを利用するためには、事前にProvisionerとmaya-apiserverをKubernetesクラスタにデプロイしておきます。

最初に、アプリケーション開発者がアプリケーションをKubernetesクラスタにデプロイします。
このアプリケーションはPersistentVolumeClaim(PVC)を使って、 OpenEBSのストレージの利用を要求します。

ProvisionerはPVCが作成されたことを検出(①)すると、maya-apiserverに対して新しいボリュームの作成を依頼します(②)。

maya-apiserverは、ControllerとReplicaをKubernetesクラスタ上にデプロイします(③)。
ControllerはiSCSIターゲットのインタフェースを持ったPodで、Replicaは実際のデータの読み書きをおこなうためのPodです。
このとき、それぞれのReplicaは必ず異なるノード上にデプロイされます。

最後に、ProvisionerがPersistentVolume(PV)を作成(④)し、アプリケーションのPodからControllerにiSCSI接続されます。

この状態で、アプリケーション内からボリュームの読み書きをおこなうと、そのリクエストがControllerに送られます。
Controllerが読み込みのリクエストを受けると、いずれかのReplicaにデータの読み込みをリクエストし、その結果をアプリケーションに返します。
書き込みのリクエストの場合は、全Replicaに対してデータの書き込みをリクエストし、すべてが完了したらアプリケーションに返します。

データの読み書き/スナップショットの仕組み

続いて、Replicaにおけるデータの読み書きの仕組みについて解説します。
読み書きの仕組みには、スナップショット機能が大きく関わってくるので併せて解説していきます。

スナップショット機能を利用すると、特定の時点のデータを保持しておき、いつでもその時点の状態に戻すことが可能になります。
さらに、取得したスナップショットをS3などにアップロードし、長期的なバックアップを実現することも可能です。

Replicaはデータの読み書きとスナップショットをおこなうためのイメージファイルを、 ファイルシステム上のファイルとして扱うので、その構成を見ていきましょう。 snapshot.png

上図に示すように、現在の書き込み対象のイメージファイル(head.img)が先頭にあります。
そこからスナップショットを取得した時点のイメージファイル(snap001.img, snap002.img...)に対して、新しい順に参照が張られています。

これらのイメージファイルはSparse Fileであるため、 見た目的には1つの大きなファイルですが、実際にはデータの書き込まれた領域の分だけしかディスク容量を消費しないようになっています。
(各ファイルの赤い部分が実際にデータが書き込まれている領域、灰色の部分が書き込まれていない領域を示しています)

データを書き込む際には、Headが指しているファイルに対して書き込みをおこないます。

データを読み込む際には、Headが指しているファイルからデータを読み込みます。
ただし指定したブロックにデータが書き込まれていなかった場合、データが見つかるまでParentを辿って古いスナップショットのファイルからデータを読み込みます。

スナップショットを作成するときは、新しいSparse Fileを作成しHeadをそのファイルに向け、Parentとして以前のファイルを指定します。

スナップショットのRevertをおこなうときは、Headの指し先を対象のスナップショットファイルに向けます。

評価

OpenEBSが採用可能かどうかについて、下記の観点から評価をおこないました。

  • 可用性
  • 耐久性
  • セキュリティ
  • 性能

可用性

データセンターの規模が大きくなってくると、ハードウェアは日常的に故障します。
そのため、ハードウェアが1台故障しただけで分散ストレージの機能が提供できないようでは困ります。

OpenEBSでは、ControllerとReplicaはKubernetesのDeploymentによりデプロイされています。
ControllerおよびReplica Podが異常終了したり、Podをホストしているノードに障害が発生した場合、 DeploymentによってPodは自動的に再スケジューリングされます。
再スケジューリングされると、ReplicaからControllerに対して再接続をおこない、Replica間でのデータ同期をおこなって復旧します。

ただし、Controllerは1つしか存在しないため、Controllerが落ちた場合はPodが再スケジューリングされるまでの間ストレージが利用できなくなります。

ReplicaのPodは、KubernetesのAffinity and anti-affinityにより、必ず異なるノードに配置されます。 これにより、1つのノードが故障しても同時に複数のReplicaが落ちることはなく、ストレージの機能を提供し続けることが可能です。
また、Replicaが1つになったときは、書き込みができないリードオンリーモードになります。

なお、OpenEBSでのReplica数はデフォルトで2になっていますが、実運用の際にはこれを増やしていくことになります。
ただし、Controllerから全Replicaに対して同期的に書き込むという実装になっているため、書き込みのコストとのトレードオフが発生します。
ちなみに、ソースコード上にはQuorumReplicaというものも存在するのですが、現状ではまだ利用できないようです。

また、データセンターをまたいだ非同期でレプリケーションをおこなうような機能は、特に提供されていないようです。

耐久性

サイボウズではデータの耐久性をもっとも重視しています。
ハードウェアの故障に備えてレプリケーションしたり、バックアップを取得しておくことはもちろんですが、 下記のような分散システム特有の問題にも対策が必要となってきます。

  • Split Brain: ネットワークの分断によりクラスタ内で複数のサービスが動き、データの競合が発生する。
  • データ不整合: Replica間でデータの書き込み順序が変わるなどして、データの不整合が発生する。
  • Bit Rot: ディスク上の一部のビットが反転し、データの破損が発生する。

Split Brain対策としては、Kubernetesの Taint based Evictions が利用されています(Kubernetes 1.9時点ではアルファ版の機能です)。 この機能を利用すると、ネットワークが分断された場合、Kubernetesのマスターノードに到達できないノード上のController Podは削除されます。 これにより1つのPersistentVolumeに対して複数のControllerが立ち上がる事態を防ごうとしています。
しかし、前述したようにOpenEBSのController PodはStatefulSetではなく、 Deploymentによりデプロイされています。
このため、リソースの不足やノードの入れ替え、PodのバージョンアップのためにPodを再スケジューリングするときに、 複数のControllerが起動してしまう可能性があります。
Controllerが複数立ち上がったからと言ってデータの不整合が発生するとは限りませんが、 Split Brain発生の可能性は否定しきれません。

書き込みの順序が入れ替わってデータの不整合が発生することはなさそうです。
Controllerから全Replicaにシーケンシャルかつ同期的にデータを書き込む動きとなっているため、 書き込みの順序が入れ替わったり、Replicaごとに異なるデータを持つことがありません。
ただし、後述するようにこれがパフォーマンスに大きな影響を与えています。

Bit Rot対策としては、一般的にはData Scrubbingと呼ばれる手法が用いられます。
OpenEBSでは、現在のところこのような機能は実装されていないようです。
(Replica復旧時にチェックサムの確認はおこなっていますが、定期的には実行されないようでした)

セキュリティ

顧客のデータを扱う上では、セキュリティの確保も非常に重要です。
セキュリティに関しては、OpenEBSの機能を利用するのではなく、以下の対策を組み合わせて実施していくことになります。 OpenEBSのリポジトリ上にはRBACの設定例が公開されているので、それを参考にするのがよさそうです。

  • Replicaの書き込み先のディスクを暗号化する
  • KubernetesのRBACPod Security Policyによるアクセス制限をおこなう
  • CNI(CalicoやRomana)のネットワークポリシーによるPod間の通信制限をおこなう

性能

下記の機材とソフトウェアを利用して、OpenEBSの性能評価をおこないました。

  • 利用機材

    • DELL PowerEdge R630
    • Intel NVMe SSD DC P3600
    • Ethernet: 10Gbps
  • 利用ソフトウェア

    • OS: Ubuntu 16.04 (Kernel: 4.4.0-93-generic)
    • Kubernetes: 1.9.3
    • CNI Plugin: Romana 2.0.2
    • OpenEBS: 0.5.1
    • fio: 3.5.0

OpenEBSを利用することでどれほどのオーバーヘッドが発生するのかを計測するために、下記の条件での計測結果を比較します。

  • ローカルのNVMe SSDへのアクセス
    • ファイルシステムは置かず、ブロックデバイスへの読み書き性能を計測する。
    • dm-cryptによる暗号化をおこなう。
    • ネットワークは介さず、ローカルにあるNVMeデバイスへの読み書き性能を計測する。
  • iSCSI経由でのNVMe SSDへのアクセス
    • 上記の暗号化をおこなったNVMeデバイスをiSCSIターゲットとして読み書きをおこなう。
    • iSCSIターゲットとイニシエータ間でネットワーク通信が発生する。
    • fioとiSCSIイニシエータは同一マシン上に配置する。
  • OpenEBS
    • 上記の暗号化をおこなったNVMe上にext4のファイルシステムを配置し、そこをReplicaの書き込み先とする。
    • fioとControllerを同一マシン上に配置する。
    • Replica数は2とする。1つはControllerと同一マシン、もう1つは異なるマシンに配置する。Controllerと後者のReplica間でのみネットワーク通信が発生する。
    • コンテナ間のネットワーク通信のオーバーヘッドが発生しないよう、Romanaを利用したPure L3ネットワークを構築。

性能計測にはfioを利用します。 実行時のオプションは下記の通りです。

$ fio --kb_base=1024 --direct=1 --numjobs=$NUM_JOBS --time_based=1 \
    --runtime=3 --filename=/dev/sdb --rw=$RW_MODE --bs=$BLOCK_SIZE --size=1G -group_reporting

ランダムリードとライト、ブロックサイズ、並列実行数を下記の組み合わせで実行します。

  • RW_MODE: randread, randwrite
  • BLOCK_SIZE: 4KB, 16KB, 64KB, 1MB
  • NUM_JOBS: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512

まずはIOPSの計測結果をみてみましょう。
グラフの縦軸がIOPS、横軸が並列実行数になっています。
上段の4つがランダムリードの結果、下段がランダムライトの結果となっています。
そして、読み書き時のブロックサイズを4KB, 16KB, 64KB, 1MBと変化させたときの結果を左から順に4つ並べています。

ローカルのNVMeへの読み込みで50万、書き込みで35万、iSCSI経由では読み書きともに6万程度のIOPSが出ていますが、OpenEBSの場合はかなり小さな値になっています。 nvme_iops.png

スケールが違いすぎてOpenEBSの計測値が分からないので、OpenEBSのデータだけを抜き出してみます。
ピークでも2000弱しかIOPSが出ていないことがわかります。 iops.png

同様にスループットも見てみます。縦軸がスループット[MiB/s]になったほかはIOPSのグラフと同じ構成になっています。

こちらもNVMeおよびiSCSIとOpenEBSの差は非常に大きくなっています。 nvme_throughput.png throughput.png

ネットワークを介しているためローカルNVMeとの差が大きくなってしまうことは理解できますが、
iSCSI経由の場合と比べてもこれほどの性能差が生じてしまうのはなぜでしょうか?

各種プロファイラやツールを活用して調べてみたところ、下記の原因が判明しました。

  • ボトルネックはControllerでのIO待ち。
  • IOコマンドの並列化やマージなどがおこなわれておらず、単純に1つずつシーケンシャルに処理されている。
  • ControllerとReplica間の通信のオーバーヘッドが大きく、1回のデータの読み書きに500μsec程度を要している。

このため、最大でも2000程度のIOPSしかでないことになります。

データの読み書きに時間がかかっている理由を探るために、Replicaのプロファイリング結果からFlame Graphを表示してみました。 実際のデータ書き込みよりも、通信のオーバーヘッドなどが大きいことがわかります。 flamegraph.png

ただし、通信のオーバーヘッドを減らしたとしても改善できるのは多くても数十%程度です。
根本的に解決するにはControllerのスケジューラの改善と、それに対応したReplicaの修正が必要となってくるでしょう。

以上のことから、単体のVolumeによるアクセスではIOPSが頭打ちとなり、 高IOPS・高スループットが必要となるアプリケーションでの利用は難しいと言えます。
一方で性能を必要としない多数のアプリケーションがVolumeを利用するケースであれば、 Controllerが並列化されるため、クラスタ全体としてはNVMeの性能を活かすことができそうです。

まとめ

今回の調査により、性能やデータの耐久性の面から我々の用途での利用は難しいと判断しました。
しかし、OpenEBSはまだアルファ版であり、今後に期待したいソフトウェアであると言えるでしょう。

今後は、Ceph等の他の分散ストレージ技術についても調査を進めていきたいと考えています。

Necoプロジェクトでは、じっくりと腰を据えてアーキテクチャの刷新を推し進めています。
まだまだ人員募集中ですので、この取り組みに興味がある方はぜひご連絡ください。

www.wantedly.com