Grafana の scripted dashboards を利用してダッシュボードを自動生成する

技術部 SRE グループの鈴木 (id:eagletmt) です。

去年クックパッド開発者ブログでも紹介した hako-console の延長として、メトリクス表示に Grafana の scripted dashboards を利用するようにしているのでその紹介をしようと思います。

アプリケーション毎のダッシュボード

クックパッドではダッシュボードの作成に Grafana を利用しており、主に Amazon CloudWatch と Prometheus に保存されているメトリクスを Grafana で可視化しています。それ以外にも一部開発用のメトリクスは InfluxDB に保存しており、その可視化にも Grafana が利用されています。

Grafana の variables 機能を利用すればリソースの種類毎にダッシュボードを作成することは簡単です。 ELB のロードバランサー名、RDS のクラスタ名、ECS のサービス名を variable として受け取るようにして CloudWatch の dimensions や Prometheus の PromQL にその variable を入れるようにすれば、各リソースの状況を閲覧することができるようになります。

ではアプリケーション毎のダッシュボードはどうでしょうか。典型的な Web アプリケーションの状態やパフォーマンスを知りたいときには

  • ALB のリクエスト数やレスポンスタイムの95パーセンタイルはどうなっているか
  • cAdvisor から得られるコンテナの CPU 使用率やメモリ使用率はどうなっているか
  • RDS の CPU 使用率やクエリのレイテンシはどうなっているか
  • Memcached の CPU 使用率や Eviction はどうなっているか

等の情報を一覧したいでしょう。

hako-console がその一覧するための役割を担っていたのですが、私が実装した hako-console のメトリクス表示画面よりも Grafana でのダッシュボードのほうが圧倒的に見やすく使いやすいため、アプリケーション毎に Grafana にダッシュボードを自動生成する方法を考えました。

Scripted Dashboards

そこで Grafana の scripted dashboards 機能を利用することにしました。 これは Grafana サーバに public/dashboards/nanika.js という JS ファイルを設置して Grafana 上で /dashboard/script/nanika.js にアクセスすると設置した JS ファイルを評価し、その結果をダッシュボードの JSON 表現として解釈しダッシュボードを表示するという機能です。この JS ファイルでは任意の JS コードを実行できるため、以下のように別のサーバが返した JSON をそのまま返すような JS ファイルを設置することで、サーバから Grafana のダッシュボードを制御することが可能になります。

'use strict';

var ARGS;

return async function(callback) {
  const fallback = {
    schemaVersion: 18,
    title: 'Failed to load dashboard from hako-console',
    panels: [
      {
        id: 1,
        type: 'text',
        gridPos: {
          w: 24,
          h: 3,
          x: 0,
          y: 0,
        },
      },
    ],
  };
  try {
    const response = await fetch(`https://hako-console.example.com/grafana_dashboards/${ARGS.app_id}`, { credentials: 'include' });
    if (response.status === 200) {
      const dashboard = await response.json();
      callback(dashboard);
    } else {
      fallback.panels[0].content = `hako-console returned ${response.status} error.`;
      callback(fallback);
    }
  } catch (e) {
    fallback.panels[0].content = `Failed to fetch API response from hako-console: ${e}`;
    callback(fallback);
  }
};

この例ではクエリストリングで app_id というアプリケーションの識別子を受け取り、それを hako-console に問い合わせています。hako-console はこの問い合わせに対してこのアプリケーションに関連する ALB、RDS、Memcached 等のリソースを見つけ、それぞれに対応するメトリクスを表示するようなダッシュボードの JSON 表現を返すようになっています。ダッシュボードの JSON 表現についてはドキュメントがあるのですが、すべてを網羅できているわけではないので、Grafana 上で実際にダッシュボードを作ってその JSON Model を見てそれに合わせる、と進めたほうが私は分かりやすかったです。 https://grafana.com/docs/reference/dashboard/

たとえばとある Web アプリケーションの自動生成されたダッシュボードは以下のようなものです。このダッシュボードを表示する JSON 表現として hako-console は https://gist.github.com/eagletmt/45f8c8bffcbe34f48e937a756aac2a34 のようなレスポンスを返しています (※一部の値はマスキングしてます)。 f:id:eagletmt:20190724111907p:plain f:id:eagletmt:20190724112029p:plain Grafana はダッシュボードの JSON 表現を介して import することもできます。したがって自動生成されたダッシュボードでは物足りず、たとえばアプリケーションが独自に Prometheus に保存しているメトリクスも表示したい場合にも、簡単に拡張することもできます。hako-console 上で固定のメトリクスを表示していたときと比べて、この点も Grafana を利用するメリットだと思っています。

ダッシュボードの工夫

アプリケーション毎のダッシュボードを自動作成するにあたって、見やすさと実用性を重視するために各リソースについて頻繁に参照するメトリクスのみを表示するようにしました。たとえば ElastiCache Memcached の場合、使用可能な空きメモリの量 (FreeableMemory) やキャッシュされた容量 (BytesUsedForCacheItems) 等が役に立つこともありますが、多くのケースで役立つメトリクスは CPU 使用率 (CPUUtilization) や eviction の発生回数 (Evictions)、キャッシュのヒット率等でしょう。公式のドキュメントも参考になります。 https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/mem-ug/CacheMetrics.WhichShouldIMonitor.html

ちなみにキャッシュのヒット率は CloudWatch の基本的なメトリクスには含まれていませんが、GetHits と GetMisses から算出することができます。Grafana は CloudWatch の Metric Math 機能をサポートしているため、これを利用して GetHits / (GetHits + GetMisses) の値を Grafana 上に表示できます。

見やすさのために一部の主要なメトリクスに絞って表示することにしたとはいえ、主要でないメトリクスが手掛かりになることがあるのはたしかです。そこで Grafana パネルのリンク機能を使い、別途用意された詳細なメトリクスが表示されたダッシュボードへ移動できるようにしています。 この機能を利用すると現在のダッシュボードで選択されている time range をリンク先のダッシュボードに引き継ぐことができます。とくに1日以上前の障害を調査したり振り返ったりするときには time range の引き継ぎは便利でしょう。

また、このアプリケーション毎のダッシュボードにはデプロイのタイミングを annotation として表示するようにしています。 f:id:eagletmt:20190724112414p:plain メトリクスの傾向が変化する原因の多くはデプロイです。プロモーション等によるユーザ数の急激な変化や下流のマイクロサービスのデプロイによるアクセス傾向の変化といった他の要因でメトリクスの傾向が変化することもありますが、それらよりもそのアプリケーション自身の変更が原因であることが多いでしょう。ダッシュボード上でデプロイのタイミングを分かりやすくすることで、意図しないパフォーマンス劣化が発生していないか、パフォーマンス改善を狙った変更がうまくいったかどうか、といったことが分かりやすくなることを狙っています。

なお、このデプロイのタイミングはどうやって取得しているかというと、クックパッドではほとんどのアプリケーションが ECS で動いているため、ECS の UpdateService API が実行されたタイミングをデプロイのタイミングとすることができます。そこで、S3 バケットに配信された CloudTrail のログファイルを加工して Prism に渡すことで Redshift Spectrum で読める状態にし、Redshift にクエリすることで UpdateService API が実行されたタイミングを取得して InfluxDB に保存し、Grafana からそれをデータソースとして annotation の query に設定しています。CloudTrail のログは他にも用途があるため、このように一度 Redshift に入れてからそれぞれが使うようになっています。 f:id:eagletmt:20190724112506p:plain

まとめ

Grafana の scripted dashboards という機能と、それを利用してどのようなダッシュボードを自動生成しているかについて紹介しました。Grafana は手軽に見やすいダッシュボードを作成できて便利な反面、variables 機能ではカバーできないような個別のダッシュボードを1つ1つ作るのが面倒に感じている方は、自由度の高い scripted dashboards 機能を利用してみてはどうでしょうか。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/