このエントリーは、Go5 Advent Calendar 2019の14日目のエントリーです。
今回、singleflight Packageの紹介をしたいと思います。 利用したことがない方は、一度、検討する材料となってもらえればと幸いです。
What is singleflight?
singleflight とはなにかというと、次の通りになります。
Package singleflight provides a duplicate function call suppression mechanism.
Package singleflightは、重複したfunction callをまとめるメカニズムを提供します。
私は、もしアプリケーションが同じリソースのための複数のリクエストを受けるようなことがあるのであれば、singleflightは、とても役に立つPackage、と考えています。例えば、RDB(マスタデータ)、画像ファイル、IPアドレスのLookup、 Client証明書 など、こういった普段変わることがないリソースの参照が必要となるケースが該当するケースです。
また、singleflightの手法では、Thundering herd problem を回避することができます。
客観的な指標として、実際にどれくらいのPackageから参照されているかというと、現時点(2019/12/13)では、
120 packagesから参照されています。
その中でも、どんなプロダクトで利用されているか参考までにあげてみると、HashiCorp社のConsulだったり、Fabioだったり、証明書の取得する処理で利用されているUse caseがあります。
https://github.com/hashicorp/consul/blob/master/agent/consul/acl.go
https://github.com/fabiolb/fabio/blob/master/cert/source.go
singleflightが提供しているfunctionは、次の3つになります。
- func (g *Group) Do(key string, fn func() (interface{}, error)) (v -interface{}, err error, shared bool)
- func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result
- func (g *Group) Forget(key string)
実際のソースファイルは、singleflight.goが用意されています。
a recent trend of singleflight
Goのソースの中身をみてみると、singleflightは、標準ライブラリとして使われていて、Goのinternal Packageにもあります。Goの内部ではどういったUse caseでcallされてるかというと、net/lookup.go がその例としててあげられます。GoDocも2種類存在しています。
Go Syncという名前の別のリポジトリでも管理されてあり、このリポジトリは、言語や sync
や sync/atomic
Packageで提供されるものに加えて、Goで並列実行の基本機構を提供しています。
GoのRelease dashboardによると、 internal/singleflight
は、Go1.14
から削除するかも、x/sync/singleflight
を利用することを推奨する予定だそうです。
詳しい背景や議論のポイントが知りたい場合はこちらのissuesとこのgo-reviewのリンクより確認できる。ただし、go-reviewでのstausは、2019/11/22にAbandoned
へ変更されているため、変わらないかもしれません。
https://github.com/golang/go/issues/31697
https://go-review.googlesource.com/c/go/+/174080/
Introduction to Use case
ここでは、singleflight Packageを利用したUse caseを2つ紹介しようと思います。
Use case 1 - net/lookup
lookupGroupは、LookupIPAddr 呼び出しが同じhostをlookupするために、一緒にマージしています。そして、LookupIPAddrは、そのlookupGroupをlocal resolverを介して使い、hostを参照します。
以下に関連する処理のコードを引用しておきます。
// lookupGroup merges LookupIPAddr calls together for lookups for the same
// host. The lookupGroup key is the LookupIPAddr.host argument.
// The return values are ([]IPAddr, error).
lookupGroup singleflight.Group
func (r *Resolver) getLookupGroup() *singleflight.Group {
if r == nil {
return &DefaultResolver.lookupGroup
}
return &r.lookupGroup
}
// We don't want a cancellation of ctx to affect the
// lookupGroup operation. Otherwise if our context gets
// canceled it might cause an error to be returned to a lookup
// using a completely different context. However we need to preserve
// only the values in context. See Issue 28600.
lookupGroupCtx, lookupGroupCancel := context.WithCancel(withUnexpiredValuesPreserved(ctx))
lookupKey := network + "\000" + host
dnsWaitGroup.Add(1)
ch, called := r.getLookupGroup().DoChan(lookupKey, func() (interface{}, error) {
defer dnsWaitGroup.Done()
return testHookLookupIP(lookupGroupCtx, resolverFunc, network, host)
})
if !called {
dnsWaitGroup.Done()
}
Use case 2 - DataLayerとBFFのMicroservices
Microservicesで開発していたときに、次のような仕様の場合、適用可能かと考えています。
Client
- Microservice A に対して、GetProduct, ListProductsのAPIを呼び出します。
- APIから受け取った結果を、一覧として表示します。
Microservice A - BFF layer
- Microservice Aは、API:GetProductComponent を提供しています。
- API:GetProductComponentは、Microservice Bの2つのAPI(GeProduct, ListProducts)を呼び出します。その後に、受け取った情報を集約して、ProductComponentとして、結果を返します。
Microservice B - Database layer
- Microservice Bは、2つのAPI(GeProduct, ListRecommendedProduct)を提供しています。
- API: GeProductは、IDを利用して、Productのマスタ情報を結果として返します。
- API: ListProductsは、Productのマスタに登録してから1週間以内のProduct情報を結果として返します。
図示すると以下のような関連になります。
以上のような形で、このUse caseでは、Microservice Bが提供しているAPIで参照されるマスタ情報があまり変わらないリソースであるととらえて、呼び出し元であるMicroservice A側にsingleflightを使うようにすることで重複したAPI呼び出しをまとめることが可能となります。
Conclusion
- singleflight packageは、重複したfunction callをまとめるメカニズムを提供してくれます。もし同じリソースを要求するAPIをチューニングする必要があるときは、とても役に立つかと思います。
- MicroservicesのBFF patternを使用している場合には、簡単に適用させるUse Caseが見つかるかと思います。
- もしsingleflightを興味を持ったようでしたら、他のプロダクトでどのように使っているかを調べてみるとより理解が深まるかと思います。
REF: https://godoc.org/golang.org/x/sync/singleflight?importers
以上になります。このエントリーをもとにsingleflightを使ってみようと思える機会になったのであれば、うれしく思います。