LoginSignup
5
7

More than 5 years have passed since last update.

GoでCloudWatchメトリクスを高速に取得

Last updated at Posted at 2018-12-09

この記事は千 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をみてもらえばすぐに実行できるはず。

今回の実装の肝となる、並列化&レート制限の部分のみ説明します。

main.go
// 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ネタです。

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7