背景
Docker Hostの中で複数のContainerが走っている場合、どのContainerがどれくらい負荷がかかっているか?といったようなモニタリングをしたいなと思いました。AWSの場合ですとECSなど高機能でHostedなソリューションもございますが、今回はそういったケースではない場合においてDocker HostのContainerの負荷状況を監視するような仕組みを考えたいと思いました。そこでなんとかいい感じにSlackに流せないかな?と思いました。
構成
Deploy Serverから4台のDocker Hostに対して内部で実行されているDocker ContainerのContainer Nameを取得してdocker stats
して整形します。
[EC2] Deploy Server
├ [EC2] docker-host-1
│ ├ [Docker Container] elevennines-web-1
│ └ [Docker Container] elevennines-web-2
├ [EC2] docker-host-2
│ ├ [Docker Container] elevennines-web-1
│ └ [Docker Container] elevennines-web-2
├ [EC2] docker-host-3
│ ├ [Docker Container] elevennines-job-1
│ └ [Docker Container] elevennines-job-2
└ [EC2] docker-host-4
├ [Docker Container] elevennines-job-1
└ [Docker Container] elevennines-job-2
~/.ssh/config
からHostを持ってきて使う
Deploy ServerのDeployユーザーの~/.ssh/config
でdocker-hostたちを宣言しているので普段からssh docker-host-1 docker ps
とかしていたりしたワケです。今回はこれも利用します。Host *****
の部分を使います。HostName
でもよかったんですけどね…
<pts/7> % cat ~/.ssh/config
Host docker-host-1
HostName 10.0.11.***
Host docker-host-2
HostName 10.0.1.**
Host docker-host-3
HostName 10.0.11.**
Host docker-host-4
HostName 10.0.1.**
docker stats
について
Docker containerのステータスを取得するコマンドの一種ですが--no-stream
を付けることでその瞬間のステータスを取得します。以下の場合、docker-host-1
でelevennines-web-1
というContainerのステータスを取得しています。
<pts/5> % docker stats --no-stream elevennines-web-1
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
elevennines-web-1 0.00% 559 MB / 8.375 GB 6.67% 1.926 MB / 1.259 MB 72.22 MB / 3.944 MB
詳しくは公式Documentをご覧ください
定期的に上記の以下フィールドをRubyで整形して閾値を超えた場合Slackの監視用Channelに流したいと思います。
- CONTAINER
- CPU %
- MEM USAGE / LIMIT
- MEM %
- NET I/O
- BLOCK I/O
container_watch.rb
とでもしておきましょうか
ユーザーのホームディレクトリに置きましょうか。aws-cli周りの初期設定等は事前に行っておいてください またSlackのIncoming Webhookの取得もお忘れなく。URLをslack_incoming_webhook_urlに設定してください。
require 'net/http'
require 'uri'
require 'json'
# AWS EC2 Instance ID (AWSじゃない場合は要らないです)
instanceid = %x[ curl -s 169.254.169.254/latest/meta-data/instance-id/ ]
cpu_max = 80 # CPU %が80%を超えたら通知
mem_max = 80 # MEM %が80%を超えたら通知
def post2slack(channel, bot_name, icon_emoji, text, subsys_name, cpu, mem_usage_limit, mem, net_io, block_io)
# Slack Incoming Webhook URL
slack_incoming_webhook_url = URI.parse("https://hooks.slack.com/services/*********/*********/************************")
payload = {
text: text,
channel: channel,
username: bot_name,
icon_emoji: icon_emoji,
attachments: [
{
fields: [
{
title: "CONTAINER",
value: subsys_name,
short: true
},
{
title: "CPU %",
value: cpu,
short: true
},
{
title: "MEM USAGE / LIMIT",
value: mem_usage_limit,
short: true
},
{
title: "MEM %",
value: mem,
short: true
},
{
title: "NET I/O",
value: net_io,
short: true
},
{
title: "BLOCK I/O",
value: block_io,
short: true
}
],
color: "red"
} ]
}
Net::HTTP.post_form(slack_incoming_webhook_url, { payload: payload.to_json })
end
# ~/.ssh/configのHostを取ってくる
hosts = %x[cat ~/.ssh/config | grep "Host " | sed -e 's/Host //' | sed '/[*]/d']
hosts.each_line do |host|
channel = "channel_name" # Slack Channel Name
bot_name = "[#{instanceid}] #{host.strip}" # Slack Bot Name
icon_emoji = ":thumbs_up:" # Slack Icon Emoji
text = "" # Slack text message
subsys_name, cpu, mem_usage_limit, mem, net_io, block_io = ''
subsys_name_to, cpu_from, cpu_to, mem_usage_limit_from, mem_usage_limit_to, mem_from, mem_to, net_io_from, net_io_to, block_io_from, block_io_to = 0
# Docker Host内のDocker ContainerのNameを取得
docker_stats = %x[ssh #{host.strip} docker ps --format "{{.Names}}" | ssh #{host.strip} xargs docker stats --no-stream]
docker_stats.each_line do |docker_stat|
if docker_stat.strip.index('CPU %') then
# 各フィールドの開始位置
cpu_from = docker_stat.strip.index('CPU %')
mem_usage_limit_from = docker_stat.strip.index('MEM USAGE / LIMIT')
mem_from = docker_stat.strip.index('MEM %')
net_io_from = docker_stat.strip.index('NET I/O')
block_io_from = docker_stat.strip.index('BLOCK I/O')
# 各フィールドの終了位置
subsys_name_to = cpu_from - 1
cpu_to = mem_usage_limit_from - cpu_from - 1
mem_usage_limit_to = mem_from - mem_usage_limit_from - 1
mem_to = net_io_from - mem_from - 1
net_io_to = block_io_from - net_io_from - 1
block_io_to = docker_stat.strip.length
else
# 各フィールドの値をセット
subsys_name = docker_stat.strip[0, subsys_name_to].strip
cpu = docker_stat.strip[cpu_from, cpu_to].strip
mem_usage_limit = docker_stat.strip[mem_usage_limit_from, mem_usage_limit_to].strip
mem = docker_stat.strip[mem_from, mem_to].strip
net_io = docker_stat.strip[net_io_from, net_io_to].strip
block_io = docker_stat.strip[block_io_from, block_io_to].strip
# 閾値を超えたらSlack通知
if cpu.gsub('%', '').to_f >= cpu_max || mem.gsub('%', '').to_f >= mem_max then
post2slack(channel, bot_name, icon_emoji, text, subsys_name, cpu, mem_usage_limit, mem, net_io, block_io)
end
end
end
end
スクリプトを実行してみる
CPUが閾値を超えているケースで以下みたいにSlackに通知されます。意図したようにSlackに投稿されていたらテストはOKです。用途に応じて閾値を下げて確認すると良いかもしれません。
crontabに登録しておく
5分毎に実行する場合の例だとこんな感じでしょうか。
<pts/7> % crontab -l
*/5 * * * * ruby /home/tokifujp/container_watch.rb
最後にというか所感的なもの
どうしてもコストとの兼ね合いなどで同じDocker Hostに複数のContainerを実行しなくてはならなくて、ContainerごとにDocker Hostのリソースを取り合って落ちたりすることがあります。そうならば予めContainerの負荷状況をこうして監視できると少し楽ができるかもしれませんね。