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](https://mackerel.io/orgs/basebank/dashboard)を使用しています。メインのコンテナのサイドカーとして、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

```go
			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

```go
			// 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

```go
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-client-go)が具象として使用されています。


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


```go
	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

```go
		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](https://golang.org/pkg/time/#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

# まとめ

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