この記事は千 Advent Calendar 2018の10日目の記事です。
はじめに
監視システムの構成は各社いろいろなパターンがあると思います。
弊社の場合は、複数AWSアカウントを一元管理でき、監視をカスタマイズしやすい、など
の理由から、Zabbixを利用したマスターサーバ/複数Proxy構成を採用しています。
ただ、AWS上で動くシステムの場合は、CloudWatchメトリクスも監視したいので、
CloudWatchから定期的にメトリクスを収集して、Zabbixに投入する方式にしています。
この記事では、上記の目的でgolang実装したCloudWatchメトリクス収集ツールについて
紹介したいと思います。
道のり
詳細は省きますが、Golangでの初回実装時は収集速度が満足いくものではありません
でした。現在のように高速化するまでに、辿った経緯も下記に紹介しておきます。
- GetMetricStatistics APIを使いメトリクスを順次取得
- GetMetricStatisticsは1メトリクスずつ取得するAPI
- 取得対象メトリクスが多いと収集に時間がかかる
- GetMetricData APIを使い複数メトリクス一括取得
- GetMetricDataは、1リクエストで複数メトリクス取得できるAPI(最大100)
- だいぶ速くなったが、golang使うなら並列化したいよねー、となり
- goroutineで並列化
- 速い!! でも、レート制限に達してスロットリングされる事象が頻発した
panic: Throttling: Rate exceeded
status code: 400, request id: 10368e95-e311-11e8-aabd-2d59e6c47896
- time.Tickを使ってレート制限回避
- 低負荷で安定して高速に動くようになった!
実装
コード自体は、こちらにあります。
README.mdをみてもらえばすぐに実行できるはず。
今回の実装の肝となる、並列化&レート制限の部分のみ説明します。
// CloudWatchの制限参照
// see) https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/cloudwatch_limits.html
// API制限(秒間最大実行数)
const MaxRateLimitListMetrics = 25
// 指定したMetricNameのメトリクス一覧を並列に取得する
func parallelListMetrics(service *Service, metricNames []string) (metricList []*cloudwatch.Metric) {
var wg sync.WaitGroup
var mu sync.Mutex
// rate-limit. see https://gobyexample.com/rate-limiting
requests := make(chan int, len(metricNames))
for i := 0; i < len(metricNames); i++ {
requests <- i
}
close(requests)
limiter := time.Tick(1000 / MaxRateLimitListMetrics * time.Millisecond)
for req := range requests {
metricName := metricNames[req]
wg.Add(1)
go func(service *Service, metricName string) {
defer wg.Done()
<-limiter
resp := listMetrics(service, metricName)
mu.Lock()
defer mu.Unlock()
for _, metric := range resp {
metricList = append(metricList, metric)
}
}(service, metricName)
}
wg.Wait()
return
}
説明
レート制限処理
// API制限(秒間最大実行数)
const MaxRateLimitListMetrics = 25
APIの制限である1秒あたりに実行可能なトランザクション数を指定しています。
ListMetricsの制限値が25なので、40ミリ秒(1000ミリ秒/25)間隔でAPIを実行することで
レート制限にかからない、ということになります。
下記のように、40ミリ秒間隔で実行するためにtime.Tickを使いました。参考1 参考2
limiter := time.Tick(1000 / MaxRateLimitListMetrics * time.Millisecond)
各goroutineの中の<-limiter
でlimiterチャンネルを40ミリ秒間隔で受け取り
処理を行います。
goroutineの待ち合わせ
全てのgoroutine処理を待ち合わせするために、sync.WaitGroupを使っています。
これは、wg.Addでインクリメント、wg.Doneでデクリメントし、関数最後のwg.Waitで
全ての処理が終わる(ゼロになる)まで、待ち合わせをします。
var wg sync.WaitGroup
〜
for req := range requests {
wg.Add(1)
go func(service *Service, metricName string) {
defer wg.Done()
〜
}(service, metricName)
}
wg.Wait()
return
}
変数書き込み時の排他ロック
戻り値変数(metricList)に、それぞれのgoroutineが同時に書き込むのを防ぐために
sync.Mutexを使っています。
Lockを取得して、API応答をmetricList変数に加えたあと、goroutineを抜ける際に
deferによりUnlockされます。
これによりmetricListアクセスが排他的になり安全にデータが格納されます。
resp := listMetrics(service, metricName)
mu.Lock()
defer mu.Unlock()
for _, metric := range resp {
metricList = append(metricList, metric)
}
性能
弊社のある環境(t2.smallインスタンス)で実行すると、1400メトリクスを8秒ほどで取得でき
CPU使用は10%程度でした。
#最初の実装に比べて、10倍ほど速くなってます^^
まとめ
CloudWatchメトリクスをレート制限にかからない範囲で並列に取得する方法について
ざっくり説明しました。
CloudWatchメトリクスをZabbixに投入している環境はそう多くない気がしますが、
取得対象を柔軟にyamlで指定できるなど汎用的な作りにしているので、もし同じこと
されている環境であればすぐに使えると思います。試してみて下さい。
(ちなみに、Golang実装経験少ないのでもっとよい書き方あればツッコミ大歓迎です)
明日は @jumperson さんのiOSネタです。