引き継ぎ文書はなぜ午前3時に間違っているのか
一言でいうと
運用の引き継ぎの単位は、文書ではなくチェーンです。サービスが出すメトリクスからアラート条件が決まり、アラートごとにランブックの項目が付き、項目ごとに、コピーしてそのまま動く確認コマンドがなければなりません。そのチェーンのどの輪が切れているかは、訓練でしか表に出ません。
なぜ必要なのか
納品が終わりに近づくと、FDEは、顧客の運用チームにサービスを引き渡します。よくある引き継ぎ物は、wikiの1ページです。ダッシュボードのアドレス、アラートの一覧、「問題が起きたらこのコマンドを打ってください」という数行。この文書は、書いた日には正しいです。3か月後の午前3時に、当番がページを受けて、そのコマンドを打つと、ポートが変わっていて、診断のパスの名前が違っていて、当番のノートパソコンには、そのツールがありません。しかも、あるコマンドは、応答なしで止まって、30秒を消費します。文書が間違っていたという事実が、最も高くつく瞬間に表に出ます。
Google SREの書籍のオンコールの章は、この状況の重みを数字で書いています。ユーザーに見えるサービスなら、ページへの応答目標を5分に置くことが多く、緊急度が低いシステムは30分です。1件のインシデントは、原因分析と事後レビューまでで平均6時間かかるため、12時間の当番1回につき、インシデント2件を上限とします。そして、ストレスの下では、人は熟考ではなく直感と習慣で動き、その反応は間違いやすいと警告しています。だから手順が必要です。明け方の当番に必要なのは、判断を代わってくれるヒーローではなく、あまり考えなくても間違えないようにしてくれる、準備された確認の手段です。
どう動くのか
チェーンを、輪1つずつ見ていきます。
1. メトリクス。Prometheusのテキスト公開形式は、行単位の形式です。行は改行で区切られ、最後の行も改行で終わらなければならず、空行は無視されます。# HELPと# TYPEの行が、説明と型(counter、gauge、histogram、summary、untyped)を伝え、1つのメトリクス名にTYPEの行は1つだけで、最初のサンプルより前に来なければなりません。サンプルは、이름{레이블="값"} 값 [타임스탬프]の形です(プレースホルダーは、名前・ラベル名・ラベルの値・サンプルの値・タイムスタンプです)。ラベルの値の中では、バックスラッシュ・二重引用符・改行の3つだけを、\\、\"、\nでエスケープします。そのため、値の中にカンマや波括弧が入ることがあり、カンマで切るパーサーは、静かに間違います。histogramは、_bucket{le="..."}・_sum・_countに展開され、le="+Inf"のバケットが必ずなければなりません。HTTPでは、text/plain; version=0.0.4で出ていきます。
# HELP orders_queue_oldest_age_seconds Age of the oldest waiting message in seconds.
# TYPE orders_queue_oldest_age_seconds gauge
orders_queue_oldest_age_seconds{queue="fulfillment"} 2.4
orders_build_info{version="2.4.1",note="handoff \"v2\", see RB"} 1
2. アラート条件。同じSREの書籍のモニタリングの章は、症状と原因を分けるように言います。ユーザーが経験すること(注文が10分間、出荷されない)が症状で、CPUが高いというのは、原因の候補です。ページは、緊急で、対処でき、人の判断が必要なものでなければなりません。機械的に同じコマンドを打つだけのページは、自動化するか、なくす対象です。この基準で見ると、「キューの深さが1000超過」は、悪い条件です。昼の混雑では、深さが数千になっても、数秒で減ります。「最も長く待っているメッセージの経過時間」のほうが、ユーザーが感じる症状に近いです。証明書も同じです。多くの場合、メトリクスは、残り時間ではなく有効期限の時刻なので、現在の時刻を引く必要があります。
Prometheusのアラートルールは、exprが真の状態が、forの期間のあいだ続いてはじめて、pendingからfiringに移り、annotationsに、説明とランブックのリンクを置きます。このラボには、Prometheusのサーバーがないので、同じ考え方を、小さなJSONのルールとスクリプトに移します。
3. 静かなことは正常ではない。比較式は、メトリクスがあるときだけ、真・偽になります。メトリクスがまったくなければ、結果が空になって、何も鳴りません。Prometheusは、このために2つの仕組みを置いています。スクレイプのたびに、対象ごとにupの時系列を作って、成功なら1、失敗なら0を入れ、absent()関数は、入力が空のとき、値が1の要素を1つ返します。ドキュメントが、この関数の用途を「時系列がないことをアラートするとき」と書いている理由です。収集ツールが落ちた夜に、ダッシュボードが穏やかに見えるのが、最も危険です。
4. ランブックの項目と、実行できる確認。項目ごとに、確認・判断・対処・エスカレーションの4行を置きます。核心は、確認の行です。当番に出力の形を解釈させず、終了コードで答えさせます。正常なら0、その障害なら0以外の値。そうすれば、人が明け方に読んでも、スクリプトが訓練で動かしても、同じ答えが出ます。このとき、パイプが落とし穴です。bashで、パイプの終了コードは、デフォルトで最後のコマンドのものなので、df 없는경로 | awk ...は、dfが失敗しても0になります(プレースホルダーは、存在しないパスです)。set -o pipefailで動かしてはじめて、前のコマンドの失敗が見えます。逆に、grep -qは、最初の一致ですぐに終わるので、前のcurlがSIGPIPEを受けることがあり、pipefailのもとでは、入力を最後まで読む形を選びます。
現場での姿
ベンダーが残したランブックを、正常なサービスに対して、1行ずつ動かしてみると、故障の種類が、いくつかに集まります。サービスではなく、当番のマシンを見るコマンド(df -h /var/lib/ordersは、当番のノートパソコンのディスクを見ます)、名前が変わったエンドポイント(404)、古いポート、当番の環境にないツール(ssがないコンテナでは、終了コード127)、時間制限がなくて止まるコマンド。最後のものは、チェッカーも一緒に止めます。subprocessのtimeoutでbashだけを殺すと、パイプの後ろのcurlが、出力のパイプを握ったまま残るので、新しいセッションで起動して、プロセスグループごと切る必要があります。
このチェックは、一度で終わりません。サービスが変わるたびに、引き継ぎ文書も古くなります。そこで、「ランブックチェッカー」を引き継ぎ物にあわせて入れ、定期的な訓練で、障害モードを1つずつ有効にして、アラート → ランブック → 確認コマンド → エスカレーションが、最後までつながるかを見ます。これが、記録1枚(再現可能なランブック)や、ポストモーテムと違う点です。その2つは、すでに起きたことを残し、訓練は、まだ起きていない夜を、あらかじめ経験します。
実務で本当に大切なこと
- アラート名・等級・ランブックの項目は、1つの表にまとめておき、スクリプトがその表を読むようにします。しきい値をコードに埋め込むと、表とコードが別々に古くなります。
- 境界値を言葉で決めます。「10%未満」と「10%以下」は、別のアラートです。ちょうど境界の値で試験します。
- スクレイプ失敗(接続の失敗・500・メンテナンスページのHTML・必須のメトリクスがない)を、アラートに上げます。HTTP 200は、正常の証拠ではありません。
- 確認コマンドは、終了コードで答え、
bash -o pipefailと時間制限のもとで試験します。 - エスカレーションの行には、人の名前より、当番のチャンネルを書きます。人は休暇を取ります。
- 訓練の結果が、そのまま引き継ぎの受け入れ基準です。「文書を渡した」ではなく、「当番がこのチェーンで障害を確証した」で終えます。
次のラボですること
ダミーのordersサービスを、さまざまな障害モードで立ち上げながら、チェーンを自分で作ります。メトリクスの原文をスクレイプして形式を確認し、エスケープまで読めるパーサーを書き、引き継ぎメモの6つのアラートをJSONのルールに移したあと、診断スクリプトで判定します。スクレイプできなかった状況をアラートに上げ、ランブックの確認コマンドが、正常なら0、障害なら0以外で、実際に動くかを、採点ツールがさまざまなモードで動かして確かめます。ベンダーのランブックの壊れた行を、チェッカーで見つけ出し、最後に、アラートからエスカレーションまでを一度に動かす、当番の訓練スクリプトを作ります。
参考ドキュメント: Prometheusのテキスト公開形式、Prometheusのアラートルール、Prometheusの関数(absent)、ジョブとインスタンス(upの時系列)、Google SREの書籍: Being On-Call、Google SREの書籍: Monitoring Distributed Systems