kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Mackerel で ECS の動的ポートマッピングに対応したタスクのメトリクスを取得する

前回書いた記事に続き,Mackerel を使って ECS のメトリクスを取得する方法を検証していて,今回は「パターン2 : コンテナインスタンスに mackerel-agent をインストールする」の検証結果をまとめる.前提としては前回と同じで,ALB の動的ポートマッピングを使う.

  • パターン1 : mackerel-agent タスクをコンテナインスタンスごとに起動する
  • パターン2 : コンテナインスタンスに mackerel-agent をインストールする

kakakakakku.hatenablog.com

メリデメ

最初に「パターン2」のメリデメを整理しておく.「パターン1」では実現できなかった「タスク特有のメトリクスを取得できる」点が大きなメリットで,今回この「パターン2」を採用することにした.

  • メリット
    • mackerel-plugin-docker でコンテナインスタンスのメトリクスを取得できる
    • mackerel-plugin-docker が Docker API からメトリクスを取得するため,動的ポートマッピングでタスクが増えた場合も,基本的なメトリクスは取得できる
    • mackerel-plugin-gostats など,任意のプラグインを使って,動的ポートマッピングでタスクが増えた場合も,タスク特有のメトリクスを取得できる
      • 少し工夫が必要になる(本記事のポイント)
  • デメリット
    • mackerel-agent のインストールは cloud-init で頑張る(逆に cloud-init でプロビジョニングができるのはメリットと言えるかも?)
    • Mackerel のグラフ生成が難しい

構成図

「パターン2」の構成は以下のようになる.コンテナインスタンスに mackerel-agent をインストールしている点が「パターン1」とは異なる.

f:id:kakku22:20170518202609j:plain

前提

今回は mackerel-plugin-gostats を使って Golang のメトリクスを取得するため,前提として API に fukata/golang-stats-api-handler を導入しておく必要がある.実装は割愛するが,エンドポイントとしては /stats を用意した.

github.com

github.com

コンテナインスタンスの起動設定に指定する cloud-init 実装例

あくまで実装例として紹介するが,コンテナインスタンスに最低限必要になるプロビジョニングを全て cloud-init で実行した.こうすると Chef や Ansible を実行する必要もなく,Auto Scaling で負荷に応じて自動的に増減させることができる.AMI にする案もあるが,ECS で使う ECS-Optimized AMI も定期的に更新するだろうし,今はこのままにしている.

cloud-init に実装した内容をザックリと箇条書きにすると,以下のようになる.

  • timezone
    • タイムゾーンを変更する
  • write_files
    • /etc/mackerel-agent/mackerel-agent.conf を生成する
    • 動的ポートマッピングに対応したメトリクスを取得するための /root/ecs_gostats.sh を生成する
  • runcmd
    • mackerel-agentmackerel-agent-plugins をインストールする
    • /etc/ecs/ecs.config にクラスタ名を書き込む

cloud-init の実装例としては,以下のようになる.ロール名 / コンテナ名 / クラスタ名などは,適宜読み替えてもらえればと!

#cloud-config
timezone: "Asia/Tokyo"

write_files:
 - path: /etc/mackerel-agent/mackerel-agent.conf
   permissions: 0644
   content: |
     apikey = "xxx"
     roles = [ "yyy:zzz" ]
     include = "/etc/mackerel-agent/conf.d/*.conf"
     [host_status]
     on_start = "working"
     [plugin.metrics.gostats]
     command = "/root/ecs_gostats.sh"
     [plugin.metrics.docker]
     command = "/usr/bin/mackerel-plugin-docker"

 - path: /root/ecs_gostats.sh
   permissions: 0755
   content: |
     #!/bin/sh
     if [ "${MACKEREL_AGENT_PLUGIN_META}" != "1" ];then
       for PORT in $(docker ps --format='{{.Ports}}' --filter name=api | cut -f1 -d- | cut -f2 -d:); do
         /usr/bin/mackerel-plugin-gostats -port ${PORT} -path=/stats -metric-key-prefix=gostats.${PORT}
       done
     else
       /usr/bin/mackerel-plugin-gostats -metric-key-prefix=gostats.#
     fi

runcmd:
 - curl -fsSL https://mackerel.io/file/script/amznlinux/setup-yum.sh | sh
 - sudo yum install -y mackerel-agent
 - sudo yum install -y mackerel-agent-plugins
 - echo AUTO_RETIREMENT=1 >> /etc/sysconfig/mackerel-agent
 - mkdir /etc/mackerel-agent/conf.d/
 - service mackerel-agent start
 - sh -c 'echo ECS_CLUSTER=api-cluster >> /etc/ecs/ecs.config'

cloud-init の詳細は以下に載っている.

docs.aws.amazon.com

Amazon Linux に mackerel-agent をインストールする手順は以下に載っている.

mackerel.io

mackerel-agent.conf の詳細は以下に載っている.

mackerel.io

ecs_gostats.sh

今回のポイントは,この ecs_gostats.sh だと思う.

#!/bin/sh
if [ "${MACKEREL_AGENT_PLUGIN_META}" != "1" ];then
  for PORT in $(docker ps --format='{{.Ports}}' --filter name=api | cut -f1 -d- | cut -f2 -d:); do
    /usr/bin/mackerel-plugin-gostats -port ${PORT} -path=/stats -metric-key-prefix=gostats.${PORT}
  done
else
  /usr/bin/mackerel-plugin-gostats -metric-key-prefix=gostats.#
fi

まず,動的ポートマッピングに対応するため docker ps の結果から,ポートを抽出する.以下の例で言うと 32768 / 32770 / 32771 となる.

PORTS
0.0.0.0:32768->8080/tcp
0.0.0.0:32770->8080/tcp
0.0.0.0:32771->8080/tcp

次にそのポートごとに -metric-key-prefix オプションを使って,メトリクスを送信する.具体的には -metric-key-prefix=gostats.${PORT} とする.ポート番号 32768 を例にすると,実際取得できるメトリクスのキーは以下のようになる.

gostats.32768.gc.gc_num
gostats.32768.gc.gc_pause_per_second
gostats.32768.gc.gc_per_second
gostats.32768.heap.heap_idle
gostats.32768.heap.heap_inuse
gostats.32768.heap.heap_released
gostats.32768.heap.heap_sys
gostats.32768.memory.memory_alloc
gostats.32768.memory.memory_stack
gostats.32768.memory.memory_sys
gostats.32768.operation.memory_frees
gostats.32768.operation.memory_lookups
gostats.32768.operation.memory_mallocs
gostats.32768.runtime.cgo_call_num
gostats.32768.runtime.goroutine_num

さらに,重要なのは MACKEREL_AGENT_PLUGIN_META と言う環境変数の存在で,実は今まで知らなかった.簡単に言うと MACKEREL_AGENT_PLUGIN_META=1 でプラグインを実行すると,グラフ定義を出力する仕様になっている.今回はメトリクス名にポート番号を含めているため,グラフ定義も変更する必要がある.具体的には -metric-key-prefix=gostats.# と書いて,ポートの部分をワイルドカードで表現することができる.

すると,以下のようなグラフ定義を出力することができる.

  • gostats.#.gc
  • gostats.#.heap
  • gostats.#.memory
  • gostats.#.operation
  • gostats.#.runtime

JSON の部分は可読性のために整形しているが,実際はこんなグラフ定義を出力することができる.

$ MACKEREL_AGENT_PLUGIN_META=1 /usr/bin/mackerel-plugin-gostats -metric-key-prefix=gostats.#
# mackerel-agent-plugin
{
    "graphs": {
        "gostats.#.gc": {
            "label": "Gostats.# GC",
            "unit": "float",
            "metrics": [
                {
                    "name": "gc_num",
                    "label": "GC Num",
                    "stacked": false
                },
                {
                    "name": "gc_per_second",
                    "label": "GC Per Second",
                    "stacked": false
                },
                {
                    "name": "gc_pause_per_second",
                    "label": "GC Pause Per Second",
                    "stacked": false
                }
            ]
        },
        "gostats.#.heap": {
            "label": "Gostats.# Heap",
            "unit": "bytes",
            "metrics": [
                {
                    "name": "heap_sys",
                    "label": "Sys",
                    "stacked": false
                },
                {
                    "name": "heap_idle",
                    "label": "Idle",
                    "stacked": false
                },
                {
                    "name": "heap_inuse",
                    "label": "In Use",
                    "stacked": false
                },
                {
                    "name": "heap_released",
                    "label": "Released",
                    "stacked": false
                }
            ]
        },
        "gostats.#.memory": {
            "label": "Gostats.# Memory",
            "unit": "bytes",
            "metrics": [
                {
                    "name": "memory_alloc",
                    "label": "Alloc",
                    "stacked": false
                },
                {
                    "name": "memory_sys",
                    "label": "Sys",
                    "stacked": false
                },
                {
                    "name": "memory_stack",
                    "label": "Stack In Use",
                    "stacked": false
                }
            ]
        },
        "gostats.#.operation": {
            "label": "Gostats.# Operation",
            "unit": "integer",
            "metrics": [
                {
                    "name": "memory_lookups",
                    "label": "Pointer Lookups",
                    "stacked": false
                },
                {
                    "name": "memory_mallocs",
                    "label": "Mallocs",
                    "stacked": false
                },
                {
                    "name": "memory_frees",
                    "label": "Frees",
                    "stacked": false
                }
            ]
        },
        "gostats.#.runtime": {
            "label": "Gostats.# Runtime",
            "unit": "integer",
            "metrics": [
                {
                    "name": "goroutine_num",
                    "label": "Goroutine Num",
                    "stacked": false
                },
                {
                    "name": "cgo_call_num",
                    "label": "CGO Call Num",
                    "stacked": false
                }
            ]
        }
    }
}

ワイルドカードの仕様はよく理解できていなくて,難しいなと感じた.#* の違いはなんだろう?とか,個数制限があるのは # だけ?とか.ドキュメントとしては以下に載っている.

mackerel.io

mackerel.io

ホストグラフ : カスタムメトリック(成功 ○)

この時点で,既に動的ポートマッピングに対応したメトリクスが取得できているため,まずは「ホストグラフ : カスタムメトリック」を確認する.以下のように,ちゃんとポート別にメトリクスが確認できているし,タスク数の増加にも対応できている.ただし,これはまだコンテナインスタンスごとのメトリクスなので,実際にはあまり使わなそう.

f:id:kakku22:20170518203032p:plain

ロールグラフ : カスタムメトリック(失敗 ×)

ロールグラフは完全に厳しくて,ワイルドカード指定で表示することができなかった.改善要望出したいなぁ.

f:id:kakku22:20170518202545p:plain

ロールグラフ : カスタマイズグラフ(微妙 △)

こうなったらもうカスタマイズグラフで頑張るしかなさそうだ!ということになり,カッとなって作った.role 関数を使って,ポートの部分を * で表現したらうまくグラフ化できた.けど,シンプルとは言えないし,ツライ.

https://mackerel.io/embed/orgs/xxx/advanced-graph?query=role('yyy:zzz', 'custom.gostats.*.memory.memory_sys')&title=custom.gostats.*.memory.memory_sys&period=1h
https://mackerel.io/embed/orgs/xxx/advanced-graph?query=role('yyy:zzz', 'custom.gostats.*.memory.memory_sys')&title=custom.gostats.*.memory.memory_sys&period=1d
https://mackerel.io/embed/orgs/xxx/advanced-graph?query=role('yyy:zzz', 'custom.gostats.*.memory.memory_sys')&title=custom.gostats.*.memory.memory_sys&period=1w

カスタマイズグラフを大量に作ってダッシュボードを用意した.

f:id:kakku22:20170518202526p:plain

カスタマイズグラフに関しては以下に載っている.

mackerel.io

去年のアドベントカレンダーでカスタマイズグラフで試行錯誤したのを思い出してしまった.

kakakakakku.hatenablog.com

まとめ

今回実現した方法が正しいのかよくわからないし,もっと良い方法があるのかもしれないし,Mackerel が正式に ECS をサポートしてくれるかもしれないけど,現状この「パターン2」で考えていたことを実現することができた.とは言え,まだ検証環境で ECS を動かしている段階で,実環境に投入したらまた別の問題に気付くかもしれない.もっと良い方法があれば教えて欲しい.

  • コンテナインスタンスに mackerel-agent をインストールした
  • コンテナインスタンスのメトリクスを取得できた
  • タスクのポートをワイルドカードで表現することで,タスクのメトリクスも取得できるようになった
  • グラフは一筋縄では実現できず,カスタマイズグラフで頑張った

謝辞

困ったときに相談するとすぐに助けてくれる id:okzk に今回はアイデアをたくさん頂いた!感謝!

okzk.hatenablog.com

関連記事

kakakakakku.hatenablog.com

kakakakakku.hatenablog.com