entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

k0sクラスターで安全なログ転送を実現するためk0sctlへコントリビューションしました

こんにちは、lyluckです。

ピクシブではオンプレミスKubernetes(以下、k8s)クラスターを運用しています。

既存アプリケーションのk8s移行は依然として進行中で、クラスターの規模は以前と比べて倍の20台程度になっています。

アプリケーションをk8s移行するには、アプリケーションログの転送機能も一緒にk8sへ移行する必要があります。

今回はk8sにおけるログ転送時のログ欠損を抑えるために取り組んだことを紹介します。

背景

k8s移行対象のアプリケーションには転送が必要なログを生み出すものがあります。

次の図のように、Fluentdがログをアプリケーションがあるサーバーとは別のサーバーへ転送しています。

左側のサーバーの中身すべてをk8sクラスター上で動かすにはFluentdもk8sクラスターへ移行する必要があります。

ログ転送のためのFluentd DaemonSetがあったのですが、運用面で課題がありました。

まとめ

ログ転送に使っているFluentd DaemonSetにはk8sクラスター更新時に正常終了できないという問題がありました。

この問題を解決するため、k0sctlへコントリビューションしました。v0.24.0でリリースされています。

github.com

github.com

詳細

Fluentd DaemonSetでログ転送する

DaemonSetとはk8sクラスターのすべてのワーカーノードで1つずつPodを起動させるワークロードのことです。

このようにHostPathにログを書き込ませ、Fluentdに転送させます。posファイルfileバッファーもHostPathへ保存することで、Fluentd Podが再起動しても続きからログ転送を再開できます。

なぜHostPathを使うのか

管理が面倒になるからです。

localボリュームはノードごとに作成が必要です。それらに紐づくPersistentVolumeClaim(以下、pvc)もノードごとに必要になります。

そのような複数の名前の違うpvcを、Podの割り振られたノードごとに適切にマウントするようなDeploymentマニフェストは書けません。

たとえばアプリケーションをStatefulSetにするとアプリケーションのマウント問題は解決するかもしれませんが、結局Fluentd DaemonSetでも同じマウント問題が発生します。

k8sクラスターの更新時にFluentdを正常終了できない問題

k8sクラスターはk0sctlを使って管理しています。k8sクラスターのk0sのバージョンを更新する場合、ワーカーノードごとに次の処理が走ります。

  1. cordon: 新規Podがスケジュール不可にする
  2. drain: このノード上のPodを削除する
  3. k0s バージョンアップ: ワーカープロセスを再起動する
  4. uncordon: 新規Podをスケジュール可能にする

1の処理ではDaemonSetのPodはスケジュール不可になりません。2の処理ではDaemonSetのPodは削除されません

Fluentd Podは生き続けたまま3の処理を迎えることになります。ログ転送処理中かもしれないFluentdがワーカーノードごといきなり消えるかもしれないということです。たとえfileバッファーを使っていても、Fluentdプロセスがクラッシュする場合はログ欠損する可能性が残ります。

Fluentd DaemonSetをどうやって正常終了させるか

2通り考えました。

  1. クラスター更新時にはすべてのログ転送を止める
  2. k0s更新のタイミングにフックを追加する
1. クラスター更新時にはすべてのログ転送を止める

クラスター更新処理を開始する前にログ転送を停止し、更新完了したら開始します。

ログ転送停止は、例えばFluentd DaemonSetのcommandをsleepのような何もしない処理に差し替えれば可能です。既存のPodは正常終了し、何もしないPodに入れ替わります。

しかしこのやり方ではログの遅延が大きいです。ノード一つ一つの更新処理は数分程度なのですが、クラスター全体だと数十分かかります。

2. k0s更新のタイミングにフックを追加する

ノードごとに更新が開始したタイミングでログ転送を停止し、そのノードが更新完了したら開始します。

アプリケーションPodもノードの更新開始時に削除されるので、ログの遅延は小さいはずです。

しかしこれは不可能でした。k0sctl v0.23.0ではクラスター更新の開始と完了時のフック(apply)はあるものの、ノードの更新開始・完了時のフックはありませんでした(applyフックはhostごとに設定できますが実行タイミングはノード個別ではありません)。

ノード更新開始時にフックする方法としてk0sctlが呼び出すk0sバイナリのパスを詐称する、のようなものも考えましたが、k0sctlのバージョンアップ時に不意に壊れるリスクがあるため見送りました。

なので、そのようなフックを使いたければk0sctlをフォークして使うか、PRを送ってマージしてもらう必要があります。

フォークするとk0sctl全体を自前でメンテナンスすることになり、管理が難しくなるため後者を選びました。 github.com

なぜDaemonSetはdrainで削除されないのか

cordonされたノードに付与されるtaintsに対応するtolerationsを持っているからです。

cordonはノードに次のtaintを付与します。

effect: NoSchedule
key: node.kubernetes.io/unschedulable

drainはこのtaintを付与した上で、ノード上からPodを削除します。なので、drainされたノードにはこのtaintに対応するtolerationを持たないPodは新しくスケジュールされません。

DaemonSetのPodは次のtolerationsを持ちます。

  tolerations:
    - effect: NoExecute
      key: node.kubernetes.io/not-ready
      operator: Exists
    - effect: NoExecute
      key: node.kubernetes.io/unreachable
      operator: Exists
    - effect: NoSchedule
      key: node.kubernetes.io/disk-pressure
      operator: Exists
    - effect: NoSchedule
      key: node.kubernetes.io/memory-pressure
      operator: Exists
    - effect: NoSchedule
      key: node.kubernetes.io/pid-pressure
      operator: Exists
    - effect: NoSchedule
      key: node.kubernetes.io/unschedulable
      operator: Exists

このtoleration

    - effect: NoSchedule
      key: node.kubernetes.io/unschedulable
      operator: Exists

を持っているのでcordonされたノードにもDaemonSetのPodはスケジュールされます。

そもそもdrain対象のノードにDaemonSetが含まれる場合には --ignore-daemonsets オプションを付与する必要がありますが、これは付与しないとDaemonSetのPodは消されても復活するので、処理が終わらないからです。

DaemonSetが未対応のtaintを付与する

DaemonSetはあくまでtaints/tolerationsの仕組みによってcordonを無視していました。なので、対応していないtaintがノードにあればDaemonSetであろうとも無視できません。例えば

key: "pixiv.net/not-ready"
operator: "Exists"
effect: "NoExecute"

のようなtaintを付与すると、そのノードから対応するtolerationを持たないPodは削除され、新しくスケジュールされなくなります。effectに NoSchedule ではなく NoExecute を指定することで即座にPodの削除をすることができます。

Fluentd以外のDaemonSetが巻き添えになってしまいますが、対策は簡単です。対応するtaintをDaemonSetのtolerationで許容すればいいです。 kube-proxy などの主要なDaemonSetは operator: Exists というすべてのtaintsに対応するtolerationをもともと持っていたりします。

k0sctlへPRを送る

issueには新しいhookを追加したいと書きましたが、しかるべきタイミングでのtaint付与・剥奪で十分なことがわかりました。なのでそのようなオプションをk0sctlへ追加するPRを送りました。 github.com

無事v0.24.0でリリースされました。 github.com

私が関わったのは evictTaint という機能です。 これはノードのアップグレードなどが実行されるとき、drainの前にそのノードに指定したtaintを付与します。そのノードがアップグレードされ、uncordonされるとそのtaintを剥奪します。

k0sctl.yamlにはこのように設定して活用しています。

apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata:
  name: k0scluster
spec:
  options:
    evictTaint:
      enabled: true
      taint: pixiv.net/not-ready=true
      effect: NoExecute
lyluck
2022年にピクシブ株式会社に中途入社。趣味はピアノと音ゲー。オンプレミスのミドルウェアやKubernetesを担当。