この記事は、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
から、ここでログ出力されているのがわかります。
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
を見つけることが出来ます。
// 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にたどり着きました。
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が具象として使用されています。
mackerel "github.com/mackerelio/mackerel-client-go"
mackerel-container-agent内の責務として具体的なAPI Clientとしての役割は持たせず別ライブラリを利用する戦略をとっていることがわかりました。
retry処理がどうなっているか眺める
表題にあげた、リトライ処理はどうなってるんでしょうか。その中身は、以下のコードから読み解くことが出来ます。
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の「アプリケーション回復性パターン」というドキュメントにも、再思考パターンとして紹介されています。
まとめ
簡単でしたが、コードリーディングの一端をご紹介させていただきました。