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

Grafana — ダッシュボードは問いだ

縦線1本が調査の30分を消す

TT Labで続きを見る

目標

エラー率のパネル1つだけのダッシュボードに、デプロイと障害をアノテーションとして載せて、グラフが「いつ」だけでなく「そのとき何があったのか」まで答えるようにします。アノテーションはAPIで残して読み直して確認し、デプロイパイプラインが自動で残す記録ツールも作ります。

なぜ重要なのか

指標は、値が変わった時刻までしか答えません。その時刻に人が何をしたかは、指標の外にある出来事なので同じ画面になく、そのため、明け方にページを受けた人は、デプロイの記録とチャットと設定リポジトリを行き来して、時計を合わせます。調査時間のかなりの部分が、その行き来です。アノテーションは、その出来事を同じ時間軸の上に載せて、行き来をなくします。ただし、アノテーションはあとから作れない記録なので、そのときに残す必要があり、そのためには、人の手ではなくパイプラインが残す必要があります。何を書くかも、あらかじめ決めておく必要があります。次の人がその場で元に戻せるようにするには、バージョンと担当者と元に戻すコマンドが、アノテーションの中にある必要があります。

ステップ

  1. lab-start-grafanaでGrafanaを起動し、uidがgfd-annotのダッシュボードを作成してください。パネルは、shop-apiの5xx割合を描くtimeseriesが1枚です。そして/root/gfd-annotations/01-blind.txtに3行書いてください。question=の後ろにこのパネルが答える質問、unanswerable=の後ろにこのパネルだけでは答えられない質問、reason=の後ろになぜ答えられないのかを40文字以上で書きます。
  2. POST /api/annotationsで、このダッシュボード(dashboardUIDがgfd-annot)にデプロイのアノテーションを1つ残してください。タグにdeployが入っている必要があり、本文にはversion=vX.Y.Zの形のバージョンが入っている必要があります。時刻は今から30分前にします(ミリ秒のエポック)。応答で返ってきたidでGET /api/annotationsを読み直して確認したあと、/root/gfd-annotations/02-annot.txtに2行、id=<그 id>とversion=<적은 버전>を書いてください(プレースホルダーは順に、そのidと、書いたバージョンです)。
  3. 開始と終了のある区間アノテーションを1つ残してください。タグはincidentで、開始は今から25分前、終了は今から5分前です(したがって長さは20分)。本文には、何があったのかを1行で書きます。そして/root/gfd-annotations/03-region.txtに2行、id=<그 id>とduration_min=<구간 길이(분)>を書いてください(プレースホルダーは順に、そのidと、区間の長さ(分)です)。
  4. ダッシュボードのannotations.listに、アノテーションクエリを2つ宣言してください。1つはdeployタグを、もう1つはincidentタグを取得します。2つの項目とも、オンになっている必要があり(enable)、互いに異なる色(iconColor)を持ち、対象はタグで絞り込む形(target.typeがtags)である必要があります。両方のタグを持つアノテーションだけが取得されることのないよう、1つの項目にはタグを1つずつだけ置きます。
  5. /root/gfd-annotations/annotate-deploy.shを作成してください。最初の引数でバージョンを受け取って、gfd-annotダッシュボードにアノテーションを残し、タグはdeployとautoの2つ、本文にはversion=、by=、rollback=の3つの値が入ります。環境変数DRY_RUN=1が指定されたら、送信せずに、送る予定のJSON本文だけを標準出力に出力して終了する必要があります(そのときの出力は、JSON1つだけでなければなりません)。そして、そのスクリプトで、互いに異なる2つのバージョンを実際に記録してください。
  6. /root/gfd-annotations/annotation-fields.txtに、デプロイのアノテーションに必ず書くフィールド名を、1行に1つずつ書いてください。version、by、rollbackの3つは必ず入っている必要があります。そして、その形式を守るアノテーションを1つ、gfd-annotダッシュボードに残してください。タグはdeployとrunbookの2つで、本文には、書いておいたすべてのフィールドが필드이름=값の形で入っている必要があります(プレースホルダーは順に、フィールド名と値です)。rollbackの値は、そのまま打てるコマンドである必要があるので、10文字以上です。runbookタグが付いたアノテーションは、この1つだけでなければなりません。
  7. /root/gfd-annotations/07-timeline.tsvを作成してください。gfd-annotダッシュボードに付いたすべてのアノテーションを、時刻の昇順(同じならidの昇順)で1行ずつ、タブで区切った3列<id> <태그 하나> <요약>で書きます(プレースホルダーは順に、id、タグ1つ、要約です)。2列目は、そのアノテーションに実際に付いているタグのうちの1つで、3列目は4文字以上の要約です。
  8. /root/gfd-annotations/08-finding.txtに4行書いてください。deploy_id=はステップ2で残したデプロイのアノテーションのid、incident_id=はステップ3で残した区間アノテーションのid、gap_min=はデプロイ時刻と障害開始時刻の差を分に丸めた整数(0以上)、verdict=はこの2つの出来事をつなげて出した結論を60文字以上で書いた文です。2つの時刻は、APIから読み直して計算してください。

参考

グラフは「いつ」までしか答えない

lab-start-grafanaでGrafanaを起動し、uidがgfd-annotのダッシュボードを作成してください。パネルは、shop-apiの5xx割合を描くtimeseriesが1枚です。そして/root/gfd-annotations/01-blind.txtに3行書いてください。question=の後ろにこのパネルが答える質問、unanswerable=の後ろにこのパネルだけでは答えられない質問、reason=の後ろになぜ答えられないのかを40文字以上で書きます。

Grafanaの起動には、20–40秒かかります。curl -s http://127.0.0.1:3000/api/healthが"database": "ok"を返すまで待ってください。

5xx割合は、件数ではなく割合です。5xxリクエストの毎秒の件数を、全リクエストの毎秒の件数で割ります。promq "<PromQL>"でまず投げてみて、数字が出るかどうかを確認してください。

パネルタイプがtimeseriesである必要がある理由は、次のステップで明らかになります。公式ドキュメントは、アノテーションに対応する可視化はTime series、State timeline、Candlestickだと書いています。数字1マスだけのパネルには、出来事を載せる場所がありません。

デプロイをアノテーションとして残し、読み直して確認する

POST /api/annotationsで、このダッシュボード(dashboardUIDがgfd-annot)にデプロイのアノテーションを1つ残してください。タグにdeployが入っている必要があり、本文にはversion=vX.Y.Zの形のバージョンが入っている必要があります。時刻は今から30分前にします(ミリ秒のエポック)。応答で返ってきたidでGET /api/annotationsを読み直して確認したあと、/root/gfd-annotations/02-annot.txtに2行、id=<그 id>とversion=<적은 버전>を書いてください(プレースホルダーは順に、そのidと、書いたバージョンです)。

必須フィールドはtextの1つだけです。dashboardUIDを書かないと、組織全体のアノテーションになり、このダッシュボードで絞り込めません。

curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"dashboardUID":"gfd-annot","time":1700000000000,"tags":["deploy"],"text":"..."}' \
  http://127.0.0.1:3000/api/annotations
curl -sG http://127.0.0.1:3000/api/annotations --data-urlencode 'dashboardUID=gfd-annot' | jq

時刻の単位を間違える失敗が、いちばんよくあります。秒単位で入れると、1970年代のどこかに打たれて、画面から消えます。30分前は$(( ($(date +%s) - 1800) * 1000 ))です。

バージョンは、あとのステップでも使います。デプロイ履歴は/opt/lab/gfd/gfd-annotations/releases.tsvにあります。

区間アノテーションで障害の時間を示す

開始と終了のある区間アノテーションを1つ残してください。タグはincidentで、開始は今から25分前、終了は今から5分前です(したがって長さは20分)。本文には、何があったのかを1行で書きます。そして/root/gfd-annotations/03-region.txtに2行、id=<그 id>とduration_min=<구간 길이(분)>を書いてください(プレースホルダーは順に、そのidと、区間の長さ(分)です)。

区間アノテーションは、点のアノテーションと同じ場所に作ります。違うのは、timeEndを一緒に送ることだけです。公式ドキュメントは、Grafana 6.4から、区間がtimeとtimeEndを持つ1つの項目で表現されると書いています。

2つの時刻をそれぞれdateで別々に計算すると、秒がずれて、長さが20分からずれてしまいます。基準の時刻を1回だけ求めておいて、そこから引いてください。

NOW=$(date +%s)
echo $(( (NOW - 1500) * 1000 )) $(( (NOW - 300) * 1000 ))

採点ツールは、duration_minをそのまま信じるのではなく、アノテーションの2つの時刻から計算し直して照合します。

タグで分けて、ダッシュボードに取得させる

ダッシュボードのannotations.listに、アノテーションクエリを2つ宣言してください。1つはdeployタグを、もう1つはincidentタグを取得します。2つの項目とも、オンになっている必要があり(enable)、互いに異なる色(iconColor)を持ち、対象はタグで絞り込む形(target.typeがtags)である必要があります。両方のタグを持つアノテーションだけが取得されることのないよう、1つの項目にはタグを1つずつだけ置きます。

ダッシュボードJSONのannotations.listは、項目の配列です。項目1つが、画面上部のトグル1つになり、名前がそのトグルの名前です。

組み込みのアノテーションデータソースを指すには、datasourceを{"type": "grafana", "uid": "-- Grafana --"}にします。

curl -s http://127.0.0.1:3000/api/dashboards/uid/gfd-annot \
  | jq '.dashboard.annotations.list'

種類が混ざると、トグルが役に立たなくなります。デプロイだけを見たいときに障害区間まで一緒にオンになってしまうと、結局誰もトグルを使わなくなります。

パイプラインが自動で残すようにする

/root/gfd-annotations/annotate-deploy.shを作成してください。最初の引数でバージョンを受け取って、gfd-annotダッシュボードにアノテーションを残し、タグはdeployとautoの2つ、本文にはversion=、by=、rollback=の3つの値が入ります。環境変数DRY_RUN=1が指定されたら、送信せずに、送る予定のJSON本文だけを標準出力に出力して終了する必要があります(そのときの出力は、JSON1つだけでなければなりません)。そして、そのスクリプトで、互いに異なる2つのバージョンを実際に記録してください。

自動記録ツールは、忙しい日にも抜けないことがすべてです。そのため、形式を固定して、人が手で呼び出すことがないようにします。

出力して確認するモードを用意する理由は2つあります。パイプラインの中でしか動かないスクリプトは、壊れたときに確認しにくいことと、テストが副作用なしで動かせる必要があることです。採点ツールも、このモードで本文を検査します。そして、その前後でアノテーションの数が増えていないかも、一緒に見ます。

本文を作るときに、文字列を手でつなぎ合わせると、引用符のところで壊れます。jq -n --argで作ってください。時刻を数字として入れるには、--argjsonです。

デプロイ履歴は、/opt/lab/gfd/gfd-annotations/releases.tsvにあります。

次の人がその場で行動できるか

/root/gfd-annotations/annotation-fields.txtに、デプロイのアノテーションに必ず書くフィールド名を、1行に1つずつ書いてください。version、by、rollbackの3つは必ず入っている必要があります。そして、その形式を守るアノテーションを1つ、gfd-annotダッシュボードに残してください。タグはdeployとrunbookの2つで、本文には、書いておいたすべてのフィールドが필드이름=값の形で入っている必要があります(プレースホルダーは順に、フィールド名と値です)。rollbackの値は、そのまま打てるコマンドである必要があるので、10文字以上です。runbookタグが付いたアノテーションは、この1つだけでなければなりません。

アノテーションの内容は、好みではなく取り決めです。午前3時にそのアノテーションを見た人が、ほかの画面を開かずに次の行動を取れる必要があります。コミットハッシュ1つだけが書かれたアノテーションは、その人をリポジトリへ向かわせます。

元に戻すコマンドを書いておくことが、特に重要です。デプロイした人が寝ていても、ほかの人が元に戻せる必要があり、そのためには、コマンドがアノテーションの中にある必要があります。実際のコマンドは、/opt/lab/gfd/gfd-annotations/releases.tsvにあります。

採点ツールは、あなたが書いたフィールドの一覧を読み、その一覧どおりにアノテーションが埋まっているかを照合します。

アノテーションを時系列順に読み直して、調査記録を作る

/root/gfd-annotations/07-timeline.tsvを作成してください。gfd-annotダッシュボードに付いたすべてのアノテーションを、時刻の昇順(同じならidの昇順)で1行ずつ、タブで区切った3列<id> <태그 하나> <요약>で書きます(プレースホルダーは順に、id、タグ1つ、要約です)。2列目は、そのアノテーションに実際に付いているタグのうちの1つで、3列目は4文字以上の要約です。

並べ替えの基準を、採点ツールと合わせる必要があります。jqのsort_by(.time, .id)を使えば、同じミリ秒に打たれたアノテーションも、順序が揺れません。

curl -sG http://127.0.0.1:3000/api/annotations --data-urlencode 'dashboardUID=gfd-annot' \
  | jq -r 'sort_by(.time, .id)[] | [(.id|tostring), .tags[0], .text] | @tsv'

この表が、調査記録の骨格です。時系列順に並べてみると、「デプロイ → エラー → 元に戻す」のような順序が目に入り、その順序がそのまま原因の仮説です。

デプロイと障害をつなげて、結論を書く

/root/gfd-annotations/08-finding.txtに4行書いてください。deploy_id=はステップ2で残したデプロイのアノテーションのid、incident_id=はステップ3で残した区間アノテーションのid、gap_min=はデプロイ時刻と障害開始時刻の差を分に丸めた整数(0以上)、verdict=はこの2つの出来事をつなげて出した結論を60文字以上で書いた文です。2つの時刻は、APIから読み直して計算してください。

採点ツールは、2つのidをAPIで読み直して間隔を自分で計算し、あなたが書いた値と照合します。そのため、手で見積もった数字は通りません。

curl -sG http://127.0.0.1:3000/api/annotations --data-urlencode 'dashboardUID=gfd-annot' \
  | jq -c '.[] | {id, time, timeEnd, tags}'

結論は、「デプロイのせいだ」と断定する必要はありません。間隔が短いというのは、疑う根拠であって、証拠ではありません。次の人が何を先に確認すべきかまで書けば、そのアノテーションは調査記録になります。