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

FDE総合演習:倉庫に同じ注文が3回届いた

午前3時、引き継ぎ文書のコマンドが間違っていた

TT Labで続きを見る

目標

引き継ぎを受ける運用チームのために、メトリクス → アラート条件 → ランブックの項目 → 実行できる確認コマンドのチェーンを作り、採点ツールがダミーのサービスをさまざまな障害モードで立ち上げて、そのチェーンが正確な項目を指しているかを、訓練で確認します。

なぜ重要なのか

引き継ぎ文書は、書いた日にしか合っていません。アラートがどんな条件で鳴るのか、鳴ったときに何で確認するのかが、コードとは別に書かれていると、サービスが変わった瞬間に文書が古くなり、その事実は、明け方の当番がコマンドを打つときに表に出ます。そのため、アラートのルールは、スクリプトが読むデータとして置き、ランブックの確認の行は、終了コードで答えるコマンドで書き、そのコマンドが実際に動くかを、チェッカーと訓練で繰り返し確認します。静かなダッシュボード(メトリクスをスクレイプできていない状態)を、正常と読まないことも、同じチェーンの一部です。

想定所要時間は75分です。デフォルトの60分のセッションが終わる前に、+時間で延長してください(最大180分)。セッションが終わると、/root/drillのファイルは消えるので、残したいコードは、終了の前に別に保管してください。

用意するもの

ステップ

  1. ダミーのサービスを正常モードで起動し、/metricsの応答を、加工せずに保存します(ファイル: /root/drill/normal.prom)。原文の# TYPEの行から、이름 형식を1行に1つずつ記録します(プレースホルダーは名前と型です。ファイル: /root/drill/families.txt)。
  2. パーサーを書きます(ファイル: /root/drill/promtext.py)。python3 promtext.py FILEが、サンプルごとに{"name","labels","value","timestamp"}オブジェクトのJSON配列を出力します。値は数値、無限大・NaNは文字列"+Inf"・"-Inf"・"NaN"、タイムスタンプがなければnullです。コメント・空行は飛ばし、ラベルの値の中のカンマ・波括弧と、\\・\"・\nのエスケープを、正しく読みます。不正なサンプル行が1つでもあれば、終了コード2です。
  3. 引き継ぎメモの6つのアラートを移します(ファイル: /root/drill/alerts.json)。形式は{"alerts": [{"alert","severity","runbook","rule"}]}で、rule.kindは、threshold(metric・op・value)、ratio(metric・denominator・op・value)、expires_within(metric・seconds)、scrape、absent(metrics)のどれかです。採点ツールは、境界値を含むさまざまな状況で、あなたのルールを判定してみます。
  4. 診断スクリプトを書きます(ファイル: /root/drill/diagnose.py)。python3 diagnose.py --url URL [--rules 경로](デフォルトは/root/drill/alerts.json。プレースホルダーはパスです)が、URL/metricsを1回スクレイプしてルールで判定し、{"target": URL, "firing": [{"alert","severity","runbook","labels"}]}を出力します。labelsは、そのサンプルのラベルです。終了コードは、アラートがなければ0、あれば1です。しきい値は、ルールファイルから読みます。
  5. diagnose.pyを、スクレイプできなかった状況をアラートに上げるように直します(ファイル: /root/drill/diagnose.py)。接続の失敗・HTTP 200ではない・形式のエラー・orders_upがない場合は、OrdersTargetDown(labelsは{})だけを出して、終了コード2です。必須のメトリクスが抜けていれば、抜けたメトリクスごとにOrdersMetricAbsent(labelsは{"metric": 이름})を出します(プレースホルダーは名前です)。
  6. 6つのアラートのランブックの項目を書きます(ファイル: /root/drill/runbook.md)。項目は、## 런북이름の見出し(プレースホルダーはランブック名です)と、- 경보:・- 확인:・- 판단:・- 조치:・- 에스컬레이션:の5行です(5つの行の見出しは、韓国語で「アラート」「確認」「判断」「対処」「エスカレーション」を、順に意味する語です)。確認の行は、バッククォートで囲んだ1行のコマンドで、$ORDERS_URLでサービスを指し、bash -o pipefail -cで動かしたとき、正常なら0、その障害なら0以外の値で、すぐに終わる必要があります。
  7. ランブックチェッカーを書きます(ファイル: /root/drill/check_runbook.py)。python3 check_runbook.py 런북.md --url URLが、項目ごとに、確認コマンドをORDERS_URL環境変数とあわせて、bash -o pipefail -cで、5秒の制限内で動かし、ランブックの順に[{"id","check","exit","status","reason"}]を出力します(プレースホルダーはランブックのファイル名です)。statusは、終了コード0ならok、そうでなければbrokenです。reasonは、ok・timeout・not-found(127)・exit N・no-check(確認の行がない場合)です。1つでもbrokenがあれば、終了コード1です。ベンダーのランブックで動かして、壊れた行を見つけます。
  8. 当番の訓練スクリプトを書きます(ファイル: /root/drill/oncall.sh)。bash oncall.sh URLが、diagnose.pyでアラートを得て、アラートごとにrunbook.mdの項目を探して確認コマンドを動かし、{"target": URL, "incidents": [{"alert","severity","runbook","labels","check_exit","confirmed","escalation"}]}を出力します。confirmedは、確認コマンドが0以外の値で終わったとき(障害の確証)にtrue、escalationは、ランブックのエスカレーションの行のままです。インシデントがあれば終了コード1、なければ0です。

参考

メトリクスの原文をそのままスクレイプしておく

正常モードのダミーのサービスの/metricsの応答を保存し(ファイル: /root/drill/normal.prom)、TYPEの行の이름 형식の一覧を保存してください(プレースホルダーは名前と型です。ファイル: /root/drill/families.txt)。

# TYPE <이름> <형식>の行(プレースホルダーは名前と型です)は、メトリクスのファミリーごとに1つだけです。histogramのファミリーは、TYPEの行の名前が基準で、サンプルには、_bucket・_sum・_countが付いて出てきます。awkで、最初の2つのフィールドが#とTYPEの行だけを選べばよいです。

カンマと引用符にだまされないパーサー

Prometheusのテキスト公開形式を、サンプルオブジェクトのJSON配列に変えるパーサーを書いてください(ファイル: /root/drill/promtext.py)。不正なサンプル行があれば、終了コード2です。

ラベルの部分は、split(',')で切れません。引用符の中では、カンマと}が値の一部で、バックスラッシュの次の文字はエスケープです。文字を1つずつ読む小さな状態機械(引用符の中か・バックスラッシュの直後か)を作ってください。値はfloat()で読み、JSONにない無限大・NaNは、文字列に変えます。

引き継ぎメモの6つのアラートをデータにする

/opt/lab/drill/brief.mdのアラートの表を、ルールに移してください(ファイル: /root/drill/alerts.json)。

「未満」と「以下」、「超過」と「以上」は、別のopです。証明書のメトリクスは有効期限の時刻なので、expires_withinを、キューは、深さではなく、最も古いメッセージの経過時間を使います。スクレイプ失敗はscrape、必須のメトリクスの一覧は、absentのmetricsに書きます。

1回スクレイプして、ルールで判定する

メトリクスをスクレイプしてalerts.jsonで判定し、鳴ったアラートをJSONで出す診断スクリプトを書いてください(アラートなしは0、ありは1。ファイル: /root/drill/diagnose.py)。

サンプルを名前ごとにまとめておくと、ルールの種類ごとに必要なものだけを取り出せます。ratioは、分子と分母を、同じラベルどうしで対応させる必要があります。expires_withinは、「メトリクスの値 − 現在の時刻」を比べます。前のステップのpromtext.pyをimportすれば、パーサーを書き直す必要はありません。

静かなダッシュボードを正常と読まない

スクレイプできなかった場合はOrdersTargetDownと終了コード2で、抜けた必須のメトリクスはOrdersMetricAbsentで、アラートに上げるように直してください(ファイル: /root/drill/diagnose.py)。

urlopenは、接続の失敗にURLError、200ではない応答にHTTPErrorを投げます。パーサーのParseErrorも、スクレイプの失敗です。この3つを空のサンプルのリストに変えると、すべての比較が偽になって、アラートが0件で出ます。それ自体が、アラートでなければなりません。absentは、「この名前のサンプルが1つもないか」で判定します。

終了コードで答えるランブック

6つのアラートの項目を書いてください(ファイル: /root/drill/runbook.md)。確認コマンドは、正常なら0、該当する障害なら0以外で、実際に動作する必要があります。

診断エンドポイント(/debug/deps・/debug/disk・/debug/tls・/debug/queue)はJSONなので、jq -eがよく合います。-eは、結果がfalse・nullなら、終了コード1を出します。curlには、-f(4xx・5xxを失敗として扱う)と-m(時間制限)を付けてください。target-downは、200のメンテナンスページも捕まえる必要があるので、応答の中にorders_upのサンプルがあるかまで見ます。

ベンダーのランブックを動かして、壊れた行を見つける

ランブックの確認コマンドを実際に動かして判定するチェッカーを書き(ファイル: /root/drill/check_runbook.py)、/opt/lab/drill/vendor-runbook.mdで動かしてみてください。

subprocess.run(timeout=…)は、時間が過ぎるとbashだけを殺し、パイプの後ろに残ったcurlが出力のパイプを握って、待ちが終わらないことがあります。Popen(start_new_session=True)で起動し、時間が過ぎたら、os.killpgでグループごと切ってください。終了コード127は、「コマンドが見つからない」です。

アラートからエスカレーションまでの当番の訓練

diagnose.py → runbook.md → 確認コマンド → エスカレーションを、一度に動かすスクリプトを書いてください(ファイル: /root/drill/oncall.sh)。

diagnose.pyの終了コード1・2は、エラーではなく判定の結果なので、set -eでスクリプトを止めないでください。ランブックの解析と時間制限つきの実行は、ステップ7のcheck_runbook.pyの関数をimportして、再利用すればよいです。confirmedは、「確認コマンドが障害を確証したか」なので、終了コードが0ではないときにtrueです。