一度だけ回るはずの精算が二度回った
一言でいうと
繰り返される作業は、「何回動いたか」で正常を判定できません。飛ばされた1回と、2度動いた1回が互いを隠し、合計が合ってしまいます。
なぜ必要なのか
定期ジョブの監視は、たいてい2つだけを見ます。失敗したか、そして何回動いたか。この2つで捕まらない事故が、このコースの最後の部品です。
夏時間が終わる日の明け方に、ローカル時刻01:00の精算が2回動きます。どちらも成功なので、失敗アラートはありません。夏時間が始まる日の明け方には、02:00の精算が1度も動きません。起きなかったことなので、やはりアラートがありません。そして、この2日の実行回数を合わせると、期待値とちょうど同じになります。合計を数える監視は、この事故を構造的に捕まえられません。
どう動くのか
問題の根は、ジョブが処理する期間に名前を付ける方式です。「1時の精算」のようにローカル時刻で名前を付けると、ローカル時刻が2度来る日に同じ名前が2度出て、存在しない日には1度も出ません。名前を瞬間(UTC)で付ければ、名前は常に1度ずつしか出ません。
スケジューラー側でも、同じ選択をしています。KubernetesのCronJobは、.spec.timeZoneで、どの地域の時刻でスケジュールを読むかを指定させ(v1.27からstable)、指定しなければ、コントローラーマネージャーのローカル時刻で読みます。Etc/UTCを明示すれば、夏時間とは無関係になります。参考までに、スケジュール文字列の中にTZやCRON_TZを入れる方式は、公式にはサポートされておらず、入れると、リソースの作成が検証エラーで拒否されます。
しかし、タイムゾーンをUTCに固定しても、「ちょうど1回」は保証されません。同じドキュメントが、こう書いています。CronJobは、スケジュールごとにおおよそ1回Jobを作り、2つ作られたり、1つも作られなかったりすることがあり、Kubernetesはそれを避けようとしますが、完全には防げません。そのため、あなたが定義するJobは冪等でなければならないと明記しています。これが、このコースの最後の文でもあります。
重なりは、別のノブで扱います。.spec.concurrencyPolicyは、3つの値を受け付けます。
Allow (기본) 앞 실행이 안 끝났어도 새 실행을 만든다
Forbid 앞 실행이 안 끝났으면 이번 실행을 건너뛴다
Replace 앞 실행을 새 실행으로 갈아 끼운다
このコードブロックの韓国語の説明は、Allow(デフォルト)は前の実行が終わっていなくても新しい実行を作る、Forbidは前の実行が終わっていなければ今回の実行を飛ばす、Replaceは前の実行を新しい実行に差し替える、という意味です。
そして、.spec.startingDeadlineSecondsは、予定時刻からどれだけ遅れて開始してよいかを決めます。この値を超えて開始できなかった実行は飛ばされ、Kubernetesはそれを失敗したJobとして扱います。ドキュメントが別に警告していることが2つあります。この値を10秒より小さくすると、コントローラーが10秒ごとに確認するせいで、そもそもスケジュールされないことがあり、見逃したスケジュールが100個を超えると、コントローラーが開始をあきらめます。
Linux側のsystemdタイマーも、同じ区別を持っています。壁時計に合わせるOnCalendarと、経過時間で数えるOnUnitActiveSec系が分かれていて、前者は、時計を直すと、次の発火時刻が再計算されます。
現場での姿
冪等性を実際に作る方法は、たいてい実行キーです。その実行が何を処理するのかを一意に指す文字列を作り、そのキーで、すでに処理した記録があれば、もう一度書き込みません。キーを選ぶときの基準は1つです。同じことを2回行えば同じキーが出て、別のことなら別のキーが出る必要があります。実行IDや開始時刻のように、実行のたびに変わる値をキーに入れると、2度動いたものを、2回とも新しい仕事と見ます。実際に、このミスが最もよくあります。
重なりの防止と冪等性が別の問題だということも、よく混同されます。重なりの防止は、同時に2回動くことを防ぎ、冪等性は、時間が離れていても2回目の実行が何の害も及ぼさないようにします。夏時間の重複は1時間離れているので、重なりの防止では捕まえられません。逆に、前の実行が長引いて生じる重なりは、冪等性だけでは、リソース競合を防げません。両方が必要です。
最後に、監視の形を変える必要があります。実行回数の代わりに、期待される期間の一覧と、実際に処理した期間の一覧を突き合わせます。抜けた期間と、2度出た期間がそれぞれ見えるようにすれば、合計が互いを隠すことがなくなります。
この監視は、時計とは無関係な事故まで一緒に捕まえてくれる点で、さらに価値があります。ノードが死んで1回飛ばされたこと、デプロイ中にスケジューラーが止まっていたこと、前の実行が長引いて次のものが飛ばされたこと。原因はばらばらですが、症状は、すべて「その期間が処理されなかった」で同じです。期間の一覧を突き合わせる監視は、原因を問わず、その症状1つだけを見ます。原因ごとにアラートを別々に作るよりも、はるかに少ない数のルールで、より多くの事故をカバーします。
そして、このコース全体を1文に縮めると、こうなります。時刻が書かれた記録は事実ではなく、それを書いたマシンの主張であり、期限を判定するコードは、その主張を信じて、他者の運命を決めます。そのため、時刻を扱うコードは、2つのことをはっきりさせる必要があります。何を保存し、何を表示するだけなのか。そして、どの計算が「今何時か」に頼り、どの計算が「どれだけ経ったか」に頼るのか。この2つの区別を守るだけで、このコースで見た5つの事件のうち4つは起きません。
次のラボですること
ラボでは、ずれた時計で記録されたログの束、ローカル時刻だけで残った送信記録、壁時計と単調時計を一緒に残したタイマー記録、有効期間の入った証明書、定期精算の実行履歴を、順番に読みます。ずれの大きさと、重なった区間と、飛ばされた実行を自分で測って書き、時刻を正しく扱う関数を3つ、自分で書きます。採点ツールは、その数字を材料から再計算して突き合わせ、あなたが書いた関数を直接呼び出して、性質を確認します。