🧑‍🚒

永続ボリュームの呪縛:失敗しすぎた Kubernetes ワークフローの末路

2023/11/14に公開1

Acompany プロダクト部門 DCR チームのテックリード イナミです。
この記事は アカンクリスマスアドベントカレンダー2023 9日目 の記事です。

この予告風タイトルは何?

最近 Argo Workflows を使用して Kubernetes によるワークフローオーケストレーションを組み上げたのですが、複数人で同時に作業しているととある呪縛によって作業が滞る状態になってしまいました。

なぜ呪縛に囚われてしまったのか、どうすれば解決できたのかを記します。

Argo Workflows

Argo Workflows は Kubernetes 上で実行されるオープンソースのワークフローエンジンであり、複雑なジョブのオーケストレーションやパイプラインの作成を容易にします。
Kubernetes のネイティブな仕組みを活かし分散環境でのタスクのスケジューリングや実行を効率的に行います。

Argo Workflows ではワークフローを宣言的に定義します。
YAML形式のファイルにワークフローのステップや依存関係を記述することでシンプルかつ柔軟なワークフローを作成できます。

https://argoproj.github.io/argo-workflows/#try-argo-workflows

永続ボリューム

Google Kubernetes Engine(GKE) における永続ボリュームはコンテナ化されたアプリケーションのデータを永続的に保存し、データの保存と共有を容易にするためのストレージです。

永続ボリュームは複数のコンテナやポッドで共有できるため複数のコンテナが同じデータにアクセスできます。
複数のマイクロサービスが共通のデータを使用する場合や、データの共有が必要なアプリケーションで役立ちます。

ワークフローと相性がよい

ワークフローはその種類によって確保したい容量が異なることが大いにあります。
永続ボリュームは自由にサイズを変更しワークフロー実行時に動的に確保することが可能です。

これによってワークフローごとに容量の異なるファイルに適宜ボリュームを割り当てることができ、更には Pod 同士もデータを受け渡すことが可能です。

https://cloud.google.com/kubernetes-engine/docs/concepts/persistent-volumes?hl=ja

https://argoproj.github.io/argo-workflows/walk-through/volumes

Argo Workflows の VolumeClaimTemplates については下記スライドの真ん中より少し後、87,88ページがわかりやすいです。

永続ボリュームの呪縛

1人、また1人と苦しみ出した

開発環境を構築してしばらく経つと

「あれ、ワークフローが開始されない。。。」

そんな声が各人からチラホラ出始めるようになります。

しかしこのときはまだ各人が状況をよくわかっておらず、その上開発環境が開発次第で変更されることで根本原因の発見に時間を要しました。

また多くのメンバーに Kubernetes の知見が少なく Kubernets Events を確認するタイミングが遅れてしまいました。

ついにワークフロー実行が停止した

ワークフローがきな臭い雰囲気を出し始めてから数週間後が経ち紆余曲折があったのちに、ついにワークフローが新規実行できない状態に陥りました。

新規実行ができないという状況では作業ができないため調査を開始し、一旦 Pod を確認することにしました。

原因究明

kubectl describe pod コマンドを実行して Events を見てみると怪しそうな文面が大量に表示されたので、必要な部分のみ抜粋してみると思ってもみないエラーが表示されました。

bash
$ kubectl events wf/workflow-2wt77 | grep 'SSD' | tail -n 1
18m (x148 over 9h)          Warning   ProvisioningFailed       PersistentVolumeClaim/workflow-g26zp-intermediate-id     (combined from similar events): failed to provision volume with StorageClass "standard-rwo": rpc error: code = Internal desc = CreateVolume failed to create single zonal disk pvc-03b8c702-f3a8-4dc4-b1d9-1cb86ac74f7d: failed to insert zonal disk: unknown Insert disk operation error: rpc error: code = ResourceExhausted desc = operation operation-1698365957001-608a7a390d029-daddc962-feb02ef3 failed (QUOTA_EXCEEDED): Quota 'SSD_TOTAL_GB' exceeded.  Limit: 1500.0 in region asia-northeast1.

GCP の永続ボリューム上限に引っかかってしまっていました。
私達の使用していた GCP アカウントは Argo Workflows 以外にも GCP リソースを使用している関係もあるのですが、想定していたワークフローの使用方法では到達しないと想定していたために意外な現象でした。

https://cloud.google.com/compute/resource-usage?hl=ja#disk_quota

GKE の永続ボリュームは Compute Engine の Disk に表示されるので永続ボリュームの実体を確認してみます。

すると「In use by」が空になっており誰もマウントしてない実体が大量に生成されてしまっていました。
(下記画像は上の行が問題の行であり、下の行が想定していた行です。また容量も問題の時点よりも少ないものです。)

また失敗しているワークフローを削除することで削除した分だけ実行できるようになることにも経験的に気付きました。

この経験によってついに根本原因が判明します。

根本原因の判明

ここまでを整理すると、

  1. 一次災害としてワークフローが実行できなくなった
  2. 二次災害として Compute Engine の Disk に誰もマウントしてないものが大量発生した
  3. ワークフローを削除すると削除した分だけ実行できるようになりそう

という状況です。

これら情報を頼りに調査を進めた結果、VolumeClaimGC のデフォルト値を考慮できていないことが原因でした。

そもそも永続ボリュームは誰が削除しているか

永続ボリューム(Persistent Volume)は Reclaim policy のデフォルト値 Delete によって PVC が削除されれば同時に削除されるはずです。

https://cstoku.dev/posts/2018/k8sdojo-12/#reclaimpolicy

しかし今回の場合、ワークフローが失敗や停止した場合その PVC が放置されてしまっていたようでした。

対策

注目している VolumeClaimGC は Persistent Volume Claim (PVC) の削除を行ってくれるガベージコレクタであり、PVC に対してワークフローの状態から削除を行ってくれる存在です。

VolumeClaimGC のデフォルト値 OnWorkflowSuccess によって、ワークフローが成功した場合にしか PVC が削除されない設定になっていたことが原因のようです。

結論として VolumeClaimGC を OnWorkflowCompletion に設定すればよいことが判明しました。

コード差分

VolumeClaimGC の設定は至ってシンプルです。
公式サンプルのコードで差分を示します。

サンプルコード

https://github.com/argoproj/argo-workflows/blob/6805c9132c88674bc6913c58e33af3695d75d946/examples/volumes-pvc.yaml

修正したコード

下記のように spec.volumeClaimGC.strategy を追加します。

volumes-pvc.yaml
# This example demonstrates the ability for a workflow to create a 
# temporary, ephemeral volume used by the workflow, and delete it
# when the workflow completes. It uses the same volumeClaimTemplates
# syntax as statefulsets.
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: volumes-pvc-
spec:
  entrypoint: volumes-pvc-example
+ volumeClaimGC:
+   strategy: OnWorkflowCompletion
  volumeClaimTemplates:
  - metadata:
      name: workdir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

  templates:
  - name: volumes-pvc-example
    steps:
    - - name: generate
        template: whalesay
    - - name: print
        template: print-message

  - name: whalesay
    container:
      image: docker/whalesay:latest
      command: [sh, -c]
      args: ["echo generating message in volume; cowsay hello world | tee /mnt/vol/hello_world.txt"]
      volumeMounts:
      - name: workdir
        mountPath: /mnt/vol

  - name: print-message
    container:
      image: alpine:latest
      command: [sh, -c]
      args: ["echo getting message from volume; find /mnt/vol; cat /mnt/vol/hello_world.txt"]
      volumeMounts:
      - name: workdir
        mountPath: /mnt/vol

これでとりあえずはいくらワークフローが失敗しても安心になりました。

宣伝

Acompanyはプライバシー保護とデータ活用の両立を追求するデータクリーンルームをベースに、次なるデータ市場を拓くプラットフォームを展開しています!

https://prtimes.jp/main/html/rd/p/000000057.000046917.html

Acompanyでは一緒に困難を乗り越えてくれる仲間を募集しています!

https://recruit.acompany.tech

Acompany

Discussion

kackykacky

ちなみにですが、PVC(というかGCEの永続ボリューム)にはZone縛りがある点もご注意ください。workflow内のPodが同一Node上あるいは同一ZoneのNodeで実行される場合はよいですが、別のZoneのNode上で実行しようとしてもボリュームをattachできずworkflowが失敗します。NodeSelectorにZoneを設定する、Cloud Storage FUSEなどのZoneによらないStorage Classを利用するなどといった対策が必要になります。