KMC活動ブログ

京大マイコンクラブの活動の様子を紹介します!!

部内 k8s クラスタで使う CSI Driver を Longhorn に移行しました

あたーっちゅ!最近は動いてないのに暑くて大変です。

部内のサーバーには Kubernetes クラスタが立っており、 Persistent Volume (PV) を提供する CSI Driver には TopoLVM を使っていました。

blog.kmc.gr.jp

TopoLVM 自体は非常に安定しておりノートラブルだったのですが(issue にもすぐ対応してもらって助かってます)、 1年運用していると単独で Dynamic Volume Provisioner として使うには足りない面が目立ってきていました。

主な問題は PV の Node 間の移動が簡単にできないことです。 TopoLVM は Node に LVM Logical Volume を切り出して PV として提供するものなので、レプリケーション機能はありません。そのため、 PVC を紐つけた Pod は必ず Logical Volume がある特定の Node に立つようになります。

PVC が紐ついた Pod を Node 間で移動するには PV が移動先の Node で使える必要があります。TopoLVM 単体で PV の移動を行うには手作業のマイグレーションが必要であり、アップグレードなどのメンテナンス時は Pod の移動を諦めて単に止める状態になっていました。

1年たってノードが増えたこともあり、 TopoLVM を卒業して分散ストレージに手を出すことにしました。

クラウドネイティブな分散ストレージを検討しよう

今回分散ストレージを選ぶにあたって重視した点は2つです。

  • 運用が楽なこと
    • 今回はあくまでも k8s を快適にしたいのであり、すごい最強分散ストレージを運用したいわけではない。とにかく楽であるほど良い。
    • CSI Driver や PV の存在を意識せずにノードの追加やメンテナンスを行いたい
      • 例えば Node 上で専用デバイスを切り出すといったことは面倒
  • 2台でも(少なくともデグレ状態で)使えること
    • 現時点で Node は3台なので、2台にデグレしてもとりあえず使える必要がある
    • 3台の状態でしっかり Drain ができることが必須

CNCF グッズの中から Rook, OpenEBS, Longhorn をざっくり見た後に、シンプルで運用が楽そうな Longhorn と OpenEBS を実際に使ってみて、Longhorn を採用することにしました。

Longhorn

Longhorn は Rancher Labs が開発した分散ブロックストレージで、今は CNCF Incubating プロジェクトです。安心感がすごい。なんと Web UI もあります。

仕組み

仕組みがわからないことには使いようがないので、ざっくり見てみました。これ以降全ての文末に暗黙の "(たぶん)" が含まれます。

Longhorn は各 Node 上の指定されたパスに PV の実体である Replica を保存します。その Replica が指定された数以上の Node に置かれることで冗長性を確保します。

Pod に PV を提供するのは各 Node に立つ instance-manager-e と Node の役割です。instance-manager-e は PV を読み書きするための iSCSIスカジー) Target として振る舞い、その背後で各 Replica (instance-manager-r) 間との Read / Write を行います。この iSCSI Target に Node から iSCSI セッションを貼ることで Node に PV のデバイスが生まれ、そのデバイスをコンテナが Volume として使用します。

ざっくり書くとこんな感じ。

[Pod] <-- local mount --> [Node (iSCSI initiator)] <-- iSCSI session --> [isntance-manager-r Pod (iSCSI target)] <-- TCP --> [Replica がある Node 上の instance-manager-r]

Replica

Replica のデータの実体は ext4 か xfs でフォーマットされたループデバイスが差分ディスクの形で保存されたもので、指定したディレクトリの下に PVC ごとのディレクトリが切られて保存されていきます。

$ sudo tree /var/lib/longhorn
/var/lib/longhorn
├── engine-binaries
│   └── longhornio-longhorn-engine-v1.3.1
│       └── longhorn
├── longhorn-disk.cfg
└── replicas
    ├── pvc-a62e9db1-35be-44da-9f2c-03222ebd889c-6446a662
    │   ├── revision.counter
    │   ├── volume-head-001.img
    │   ├── volume-head-001.img.meta
    │   ├── volume.meta
    │   ├── volume-snap-3adb6421-bc79-40c1-ad02-994a63b280fb.img
    │   └── volume-snap-3adb6421-bc79-40c1-ad02-994a63b280fb.img.meta
...

この差分ディスクは独自形式らしく、単純に mount コマンドでマウントすることはできませんが longhorn-engine 経由でデータを取り出すことが可能です。

このようにシンプルにディレクトリ以下に保存されていく形なので、特別な Node のセットアップをしなくても動いてくれるのが楽*1

instance-manager-r,e 詳細

Longhorn を入れると各 Node に instance-manager-r と instance-manager-e の Pod が一つずつ立ちます。

instance-manager-r (Replica の r) は instance-manager-e (Engine の e) からの Read / Write リクエストに応じて Replica への Read / Write を行います。

ソースコードを読んだところでは、Engine から Replica への Read リクエストはラウンドロビン形式で選ばれた1つの Replica へ行われ、 Write リクエストは全ての Replica へ行われ、全て書き終わるまで待つ、という形のようです。

Engine と Replica 間の通信は TCP 上の独自プロトコルで行われているようでした。ここが iSCSI だと思ってる人も多そうです。

Replica で TCP server を立ち上げているところ: longhorn-engine/dataserver.go at 18fa3c566aedb4080dc50add7479a1a266aa8cc7 · longhorn/longhorn-engine · GitHub

iSCSI や Node 周り詳細

先述した通り instance-manager-e Pod は外向きには PV を提供する iSCSI Target として振る舞います。そして、各 Node が iSCSI initiator として振る舞い *2 、Node 上にたっている instance-manager-e Pod とセッションを貼ります。

$ sudo iscsiadm -m node
10.100.0.253:3260,1 iqn.2019-10.io.longhorn:pvc-90fd4ba4-e2c5-40ed-a1fe-9f0f5290753f
10.100.0.253:3260,1 iqn.2019-10.io.longhorn:pvc-be3d8c6f-9605-404f-a28b-a4e8785f5d12
10.100.0.253:3260,1 iqn.2019-10.io.longhorn:pvc-e5036d59-a3b6-4350-8bbd-72f8b8800ed2

$ sudo iscsiadm -m session
tcp: [1] 10.100.0.253:3260,1 iqn.2019-10.io.longhorn:pvc-90fd4ba4-e2c5-40ed-a1fe-9f0f5290753f (non-flash)
tcp: [2] 10.100.0.253:3260,1 iqn.2019-10.io.longhorn:pvc-be3d8c6f-9605-404f-a28b-a4e8785f5d12 (non-flash)
tcp: [3] 10.100.0.253:3260,1 iqn.2019-10.io.longhorn:pvc-e5036d59-a3b6-4350-8bbd-72f8b8800ed2 (non-flash)

$ sudo iscsiadm -m host
tcp: [2] 10.100.0.216,[<empty>],<empty> <empty>
tcp: [3] 10.100.0.216,[<empty>],<empty> <empty>
tcp: [4] 10.100.0.216,[<empty>],<empty> <empty>

node や session に出ているアドレスが instance-manager-e Pod のアドレス、 host に出ているアドレスは Node から Pod へアクセスするときのネットワークインターフェースについたアドレスです。PVC ごとに Node から Engine へ session が貼られていることがわかります。

ちなみに iSCSI を使っている理由はブロックデバイスを userspace から提供するための手段を検討した結果だそうです。

github.com

この時点で PV を読み書きできるデバイスが Node 上に生えます。このデバイスが色々とマウントされていき、下のように Pod で Volume として見えるようになります。

$ mount
...
/dev/longhorn/pvc-e5036d59-a3b6-4350-8bbd-72f8b8800ed2 on /var/lib/kubelet/pods/d674d860-744b-4dd7-84b7-235d3f2eb01b/volumes/kubernetes.io~csi/pvc-e5036d59-a3b6-4350-8bbd-72f8b8800ed2/mount type ext4 (rw,relatime)
...

$ sudo crictl inspect ac0a56409c28d
...
        {
          "destination": "/var/lib/mysql",
          "type": "bind",
          "source": "/var/lib/kubelet/pods/d674d860-744b-4dd7-84b7-235d3f2eb01b/volumes/kubernetes.io~csi/pvc-e5036d59-a3b6-4350-8bbd-72f8b8800ed2/mount",
          "options": [
            "rbind",
            "rprivate",
            "rw"
          ]
        },
...

こうして眺めると案外シンプルな仕組みで分かりやすいですね!

Drain 時の挙動

01~03 の3台で構成したクラスタLonghorn をインストールし、 Node を Darin してどのような挙動になるか確かめてみました。

03 に PVC を使った Pod があり、02 と 03 に replica がある状態で 03 の Drain を行ったところ、しばらく PDB によって Drain が抑制されたのち、 02 に代わりの Pod が立って無事 Drain されました。

一方 Replica については、 Drain から数分立たないと 01 には作られませんでした(数分間は冗長でない状態だった)。

これは想定された挙動のようで、まず Drain 直後の longhorn-manager のログを見ると Replica の Replenishment が数分後にされると出ていました。

time="2022-08-29T17:28:34Z" level=debug msg="Replica replenishment is delayed until 2022-08-29 17:33:10 +0000 UTC"

その後実際に数分待つと、01 に新しい Replica が作られました。

time="2022-08-29T17:33:11Z" level=info msg="Cannot find a reusable failed replicas"
time="2022-08-29T17:33:11Z" level=debug msg="A new replica pvc-2dcbd883-26d7-44a4-ace4-5e94978a2440-r-2b7f57c0 will be replenished during rebuilding" accessMode=rwo controller=longhorn-volume frontend=blockdev migratable=false node=02 owner=02 state=attached volume=pvc-2dcbd883-26d7-44a4-ace4-5e94978a2440
time="2022-08-29T17:33:11Z" level=debug msg="Schedule replica to node 01" dataDirectoryName=pvc-2dcbd883-26d7-44a4-ace4-5e94978a2440-d1ac5f6a disk=c25e7a5c-4d6a-4ab0-a229-c63316142ed2 diskPath=/var/lib/longhorn/ replica=pvc-2dcbd883-26d7-44a4-ace4-5e94978a2440-r-2b7f57c0

Replica が増えるまで数分待つのは、 Failed な Replica が復活して Reuse できるかどうかを待つためらしいです*3

…ということで、 Drain はスムーズにできたし数分後には冗長性も復活してくれました。Volume について特に気にせず Node を落とすことができて便利!

ちなみに、ノードが落ちたときも数分待って復活しなければ Replica が新 Node に移動するようです*4

OpenEBS (ざっくり)

CNCF sandbox project である OpenEBS の Jiva も試してみました。仕組みはだいたい Longhorn と同じです。

ただ、ドキュメントが弱かったり、PVC を作るごとに (Replica 数 + 1) つ分の Pod が立って*5リソースの無駄遣いや運用の面倒さが気になったり、Node の Drain がスタックしてうまく動かなかったり*6 だったりと Longhorn より運用が大変そうなのでやめました。cStor も Pool が lost したときのドキュメント を読むと大変そうに見えるし、MayaStor はまだ機能不足感が否めませんでした。

移行

というわけで PV を TopoLVM から Longhorn に移行します。特にマネージドにやってくれるものはなさそうですし、移行が必要な PV がそう多くなかったこともあって気合で頑張ることにしました。

  1. 一旦マウントしている Pod を削除する
  2. 一時的に移行用の PVC を既存の PVC と同じスペックで作る
  3. 旧 PV と移行用 PV をマウントした適当な Pod を立ち上げる
  4. rsync PV 間のデータを同期する
    • # rsync -a /before/ /after
  5. 移行用 Pod を削除する
  6. 旧 PVC を削除し、同じ名前の新 PVC を longhorn 指定で作る
  7. 移行用 PV と新 PV をマウントして再び適当な Pod を立ち上げる
  8. rsync でデータを同期する
  9. 移行用 Pod と PVC を削除する
  10. 削除していた Pod を復活させる

気合です。

こうして TopoLVM の PVC を全て Longhorn に移すことができ、 Node の Drain が気軽にできるクラスタになりました。

Longhorn 導入後、(Longhorn と関係ない理由で) Node がオフラインになったりアップグレードをミスって Unschedulable の嵐になったりとトラブルが続発したのですが、そんな中でも Longhorn は Replica が2つある状態をノーメンテで維持してくれ、PVC を使う Pod もちゃんと Healthy な Node に移動してくれました。

とにかく運用が楽な Longhorn 、ワンダちゅです!

*1:公式には専用のディスクを使うことが推奨されています https://longhorn.io/docs/1.3.1/best-practices/#node-and-disk-setup

*2:Longhorn のセットアップで Node への open-iscsi のインストールを要求される

*3:設定で調整可能。https://longhorn.io/docs/1.3.0/references/settings/#replica-replenishment-wait-interval

*4:通常、 StatefulSet は手動で Node を削除しない限りリスケジュールされない Statefulset pods are not evicted when we shutdown the node manually · Issue #103175 · kubernetes/kubernetes · GitHubLonghorn はその対策として StatefulSet を削除する機能があり、自動で Volume とともにリスケジュールできるようになる https://longhorn.io/docs/1.3.1/references/settings/#pod-deletion-policy-when-node-is-down

*5:+1 は Controller (Longhorn でいう Engine)。iSCSI initiator は Controller Pod が紐付く Service のアドレスとセッションを張ることになる。k8s ネイティブな感じで綺麗ではあるんだけどね…

*6:退避する機能はあるようだが、3台2レプリカじゃ無理そう feat(operator): automate movement replicas when node is removed from cluster by shubham14bajpai · Pull Request #97 · openebs/jiva-operator · GitHub