Help us understand the problem. What is going on with this article?

mackerel-container-agentから学ぶGoにおけるリトライ実装

この記事は、Go Advent Calendarに、代打で出そうとして、クリスマスイブの夜から書き始めて、無事埋まったので野良記事として公開するものです。

TL;DR

  • mackerel-container-agent とは、Mackerel社がコンテナ監視のために用意している監視エージェントである
  • mackerel-container-agent は、Go言語で実装されており、監視ツールの性質上、リトライ処理が用意されている
  • Exponential backoff というリトライのアルゴリズムを実装している

背景

筆者は、業務でAmazon ECS(Elastic Container Service)というAWSのコンテナオーケストレーションサービスを利用したサービスを運用しています。そのサービスで稼働しているコンテナの監視のため、監視SaaSであるMackerelを使用しています。メインのコンテナのサイドカーとして、mackerel-container-agentというコンテナ用の監視エージェントを置くことで、監視を実現する仕組みです。普段、ユーザーとして使っている中でふとその中の実装がどうなっているのだろうと気になったので調べたことをここに記します。

特に、気になった点が、「いかにリトライを実現しているか」というポイントだったので、失敗時のリトライ処理にフォーカスして書きます。

mackerel-container-agentとは

上記で紹介したこちらは、GitHubにOSSとして公開されています。

https://github.com/mackerelio/mackerel-container-agent

調査の起点は、こちらのエラーログからたどっていきます(2019年12月19日当時、メンテナンス中でその際どういう動きをしてるんだろうと気になって調べたのがきっかけのきっかけです)。

2019/12/19 06:04:31 INFO <agent> retry to find host: failed to create a new host: API request failed: Site is under maintenance.

※ 内部実装がGoだと、おそらく errors.Wrap()でエラーがくるまれた結果、このような階層的なエラーメッセージが生成されているのだろう、という推測からです。

エラーメッセージの階層をたどる

まずは、エラーの文字列 retry to find host から、ここでログ出力されているのがわかります。

https://github.com/mackerelio/mackerel-container-agent/blob/697ce4a13eea5e614d4b72fa7ef7fa5374add839/agent/run.go#L78

            case <-time.After(duration):
                host, retryHostID, err := hostResolver.getHost(hostParam)
                if retryHostID {
                    logger.Infof("retry to find host: %s", err)
                    if duration *= 2; duration > 10*time.Minute {
                        duration = 10 * time.Minute
                    }
                    continue
                }       

hostResolver.getHostのなかをたどると、更に続きの階層のエラー failed to create a new host を見つけることが出来ます。

https://github.com/mackerelio/mackerel-container-agent/blob/69372758402f6388019adff2b5662a8e1a21ce31/agent/host_resolver.go#L61

            // create a new host
            hostID, err := r.client.CreateHost(hostParam)
            if err != nil {
                return nil, retryFromError(err), errors.Wrap(err, "failed to create a new host")
            }

r.client.CreateHostを続けて見つけると、Interfaceにたどり着きました。

https://github.com/mackerelio/mackerel-container-agent/blob/36cf0bb103d1645fb733c214fe142207b1318838/api/client.go#L9

package api

import mackerel "github.com/mackerelio/mackerel-client-go"

// Client represents a client of Mackerel API
type Client interface {
    FindHost(id string) (*mackerel.Host, error)
    FindHosts(param *mackerel.FindHostsParam) ([]*mackerel.Host, error)
    CreateHost(param *mackerel.CreateHostParam) (string, error)
    UpdateHost(hostID string, param *mackerel.UpdateHostParam) (string, error)
    UpdateHostStatus(hostID string, status string) error
    RetireHost(id string) error
    PostHostMetricValuesByHostID(hostID string, metricValues []*mackerel.MetricValue) error
    CreateGraphDefs([]*mackerel.GraphDefsParam) error
    PostCheckReports(reports *mackerel.CheckReports) error
}

このInterfaceの実装先は依存関係を解決している上位をたどるとわかりますが、github.com/mackerelio/mackerel-client-goが具象として使用されています。

https://github.com/mackerelio/mackerel-container-agent/blob/90e89ad9946df6dc89dad90f4b7f09528444054f/agent/agent.go#L97

    mackerel "github.com/mackerelio/mackerel-client-go"

mackerel-container-agent内の責務として具体的なAPI Clientとしての役割は持たせず別ライブラリを利用する戦略をとっていることがわかりました。

retry処理がどうなっているか眺める

表題にあげた、リトライ処理はどうなってるんでしょうか。その中身は、以下のコードから読み解くことが出来ます。

https://github.com/mackerelio/mackerel-container-agent/blob/697ce4a13eea5e614d4b72fa7ef7fa5374add839/agent/run.go#L73

        for {
            select {
            case <-time.After(duration):
                host, retryHostID, err := hostResolver.getHost(hostParam)
                if retryHostID {
                    logger.Infof("retry to find host: %s", err)
                    if duration *= 2; duration > 10*time.Minute {
                        duration = 10 * time.Minute
                    }
                    continue
                }

実装にfor-selectパターンという並行処理の実装パターンが使われています。
この中でtime.Afterを用いて一定期間時間が経過するのを待ち受けています。
引数で渡されているdurationですが、現在のリトライ時間を2倍ずつしていって、10分以上であれば10分にする実装となっています。

この手法は、Exponetial backoffという有名なリトライのアルゴリズムと知られています。

https://en.wikipedia.org/wiki/Exponential_backoff

Exponential backoff is an algorithm that uses feedback to multiplicatively decrease the rate of some process, in order to gradually find an acceptable rate.

Microsoftの「アプリケーション回復性パターン」というドキュメントにも、再思考パターンとして紹介されています。

https://docs.microsoft.com/ja-jp/dotnet/architecture/cloud-native/application-resiliency-patterns#retry-pattern

まとめ

簡単でしたが、コードリーディングの一端をご紹介させていただきました。

binc
Eコマースプラットフォーム「BASE」、オンライン決済サービス「PAY.JP」、購入者向けID型決済サービス「PAY ID」の3つのサービスを運営しています。
https://binc.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした