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

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

この記事は千 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ネタです。

コメント

このブログの人気の投稿

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-04-30 23:37:32 RSSフィード2021-04-30 23:00 分まとめ(42件)

投稿時間:2023-02-05 02:09:04 RSSフィード2023-02-05 02:00 分まとめ(9件)