kubectl scale はどうやって自作の型を知るのか
一言でいうと
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に分類するチェックスクリプトを作り、直す前と後の出力を並べて残します。