TT Lab
はじめる
学ぶ 学習パス コース

CRDとオペレータ

kubectl scale はどうやって自作の型を知るのか

TT Labで続きを見る

一言でいうと

kubectl scaleとHPAは、対象の種類を知らないまま動作します。この2つが知っているのはscaleサブリソースという共通の窓1つだけで、CRDは、その窓をspecReplicasPath・statusReplicasPath・labelSelectorPathの3行で開いてあげます。

なぜこの契約が必要なのか

Kubernetesには、「レプリカ数」という概念を持つ型がいくつもあります。Deployment、ReplicaSet、StatefulSet、ReplicationController、そして数多くのCRDです。これらのフィールド名はバラバラでありえますし、実際にそうです。ところが、kubectl scaleは1行で、HPAコントローラーも1つです。これらのツールが型ごとに別のコードを持っているなら、新しいCRDができるたびにkubectlを直さなければなりません。そこでKubernetesは、逆の道を選びました。ツールは固定し、型が自分自身を翻訳して差し出すようにするという方法です。

その翻訳の結果が、autoscaling/v1のScaleオブジェクトです。どんな型でも、scaleサブリソースを有効にすると、GET .../shards/a/scaleがShardではなくScaleを返します。中にあるのは、spec.replicasというフィールドが1つ、status.replicasが1つ、そしてstatus.selectorという文字列が1つだけです。kubectlもHPAも、この3つのフィールドしか見ません。

どう動くのか

CRDのバージョンごとに、subresources.scaleの下に3つのパスを書きます。

subresources:
  status: {}
  scale:
    specReplicasPath: .spec.replicas
    statusReplicasPath: .status.replicas
    labelSelectorPath: .status.selector
パス 誰が書くか なかったり間違っていたりすると
specReplicasPath ユーザー・kubectl・HPAが書く 必須。.specの下である必要があります。値が実際のオブジェクトになければ、サブリソースがエラーを返します
statusReplicasPath コントローラーがstatusに書く 必須。.statusの下である必要があります。オブジェクトに値がなければ、実際の数が0に見えます
labelSelectorPath コントローラーがstatusに書き、HPAが読む 任意。なければ、HPAがスケーリングを開始できません

3つのパスはいずれも、ドット表記だけが許され、配列表記は使えません。labelSelectorPathが指す場所は、構造体ではなく文字列1つである必要があり、その中にラベルセレクターをシリアライズした形(app=web)で入れます。

ここで、必ず一緒に覚えておくべきことがあります。statusReplicasPathが指す場所は、必ずstatusサブリソースの中であり、labelSelectorPathも、普通はそこに置きます(.specの下も許されますが、セレクターはコントローラーが計算して埋める値なので、statusが定位置です)。そのため、status: {}を一緒に有効にしないと、コントローラーがその値を書く窓そのものがなく、有効にしておいても、普通のkubectl patchでは書けません。--subresource=statusを付ける必要があります。コントローラーのないクラスターでkubectl scaleを実行すると、望ましい数だけが5に変わり、実際の数は0のままですが、それは故障ではなく、まだ誰もその場所を埋めていないという意味です。

HPA側は、もう少し面白いです。HPAは、scaleTargetRefに書かれたapiVersion・kind・nameで対象を探し、その対象のscaleサブリソースを読みます。この過程が成功したかどうかはstatus.conditionsのAbleToScaleに残り、メトリクスを計算して実際にスケーリングできるかどうかはScalingActiveに残ります。この2つは別々の質問です。セレクターのパスがないと、AbleToScaleはTrue(SucceededGetScale)なのにScalingActiveがFalse(InvalidSelector)になり、メッセージは、対象のscaleにセレクターがないと伝えます。画面の一覧では、HPAが何事もなく1行で表示されます。

現場での姿

1つ目は、成功したと答える失敗です。specReplicasPathを.spec.replicaのように1文字間違えて書いておくと、kubectl scaleは終了コード0とともにscaledと出力します。ところが、オブジェクトの値はそのままです。同じCRDにkubectl get … --subresource=scaleを実行すると、そこではじめて「the spec replicas field ... does not exist」が出力されます。デプロイスクリプトが終了コードだけを見て次のステップに進む構造なら、この故障は何か月も生き延びます。

2つ目は、付いているのにスケーリングしないHPAです。セレクターのパスを抜かしたCRDにHPAをかけても、誰もエラーを目にしません。負荷が上がっても、レプリカはそのままです。人々はたいてい、メトリクスのパイプラインから疑いますが、実際の原因は、CRDの3行のうち1行がないことです。ポストモーテムでこの原因にたどり着くまでに時間がかかるのは、症状が「オートスケーリングが効かない」なので、調査の範囲がまずメトリクスの側に広がるからです。

3つ目は、だからチェックを自動化することです。CRDを作るチームは、CIで2つのことを確認する価値があります。scaleサブリソースを実際に一度読んでみること、そして、オートスケーリングを使う型ならセレクターのパスがあるかを見ることです。定義を読むだけでは、1つ目はわかりません。パスが実際のオブジェクトに届くかどうかは、問い合わせてみてはじめてわかります。

このラボ環境の限界

ラボのPodのクラスターには、メトリクスサーバー(metrics-server)がありません。そのため、HPAのTARGETS列はずっと不明のままで、実際にレプリカ数が負荷に応じて増減する様子は見られません。その代わり、HPAコントローラー自体は動いているため、「対象を読めたか」「なぜスケーリングを開始できなかったのか」は、条件にそのまま記録されます。このモジュールで扱う失敗は、すべてその場所に残ります。また、Shardを管理するコントローラーがないので、statusは人が自分で埋めて、コントローラーの役を真似します。

次のラボですること

3つのパスを備えたCRDを作って、kubectl scaleとHPAを付けてみて、セレクターのパスがないCRDと、パスにタイプミスがあるCRDをそれぞれ作り、どんな沈黙が生じるかを条件のメッセージで確認します。最後に、3つのCRDを表にまとめてok・nosel・brokenに分類するチェックスクリプトを作り、直す前と後の出力を並べて残します。