キャッシュブレイクダウンからRobustnessへ: singleflight in Go
Emily Parker
Product Engineer · Leapcell

Preface
高性能なサービスを構築する際、キャッシングはデータベースの負荷を最適化し、応答速度を向上させるための重要な技術です。しかし、キャッシングを使用すると、いくつかの課題も生じます。その中でもキャッシュブレイクダウンは大きな問題です。キャッシュブレイクダウンは、データベースへの負荷の急増を引き起こし、データベースのパフォーマンスを低下させ、最悪の場合、データベースをダウンさせて利用不能にする可能性があります。
Goでは、golang.org/x/sync/singleflight
パッケージは、特定のキーに対する同時リクエストが同時に一度だけ実行されるようにするメカニズムを提供します。このメカニズムは、キャッシュブレイクダウンの問題を効果的に防ぎます。
この記事では、Goでのsingleflight
パッケージの使用法を詳しく掘り下げます。キャッシュブレイクダウン問題の基本から始めて、singleflight
パッケージを詳細に紹介し、それを使用してキャッシュブレイクダウンを回避する方法を示します。
Cache Breakdown
キャッシュブレイクダウンとは、高並行下でホットキーが突然期限切れになり、大量のリクエストがデータベースに直接アクセスし、データベースに過負荷をかけ、クラッシュさせる可能性さえある状況を指します。
一般的な解決策は次のとおりです。
- ホットデータを期限切れにならないように設定する: 一部の明確に定義されたホットデータについては、期限切れにならないように設定して、キャッシュの有効期限が切れたためにリクエストがキャッシュをバイパスしてデータベースに直接アクセスしないようにすることができます。
- Mutexロックの使用: キャッシュの有効期限が切れたときに、すべてのリクエストが同時にデータベースをクエリするのを防ぐために、ロックメカニズムを採用して、1つのリクエストのみがデータベースをクエリしてキャッシュを更新し、他のリクエストはキャッシュが更新されるまで待機してからアクセスするようにすることができます。
- プロアクティブな更新: バックグラウンドでキャッシュの使用状況を監視し、キャッシュの有効期限が切れようとしているときに、非同期的に更新して有効期限を延長します。
The singleflight Package
Package singleflightは、重複した関数呼び出し抑制メカニズムを提供します。
この文は公式ドキュメントからのものです。
言い換えれば、複数のゴルーチンが同じ関数を(指定されたキーに基づいて)同時に呼び出そうとすると、singleflightは、関数が最初に到着したゴルーチンによってのみ実行されるようにします。他のゴルーチンは、この呼び出しの結果を待機し、複数の呼び出しを同時に開始する代わりに、結果を共有します。
要するに、singleflightは複数のリクエストを1つのリクエストにマージし、複数のリクエストが同じ結果を共有できるようにします。
Components
-
Group: これはsingleflightパッケージのコア構造です。すべてのリクエストを管理し、常に同じリソースに対するリクエストが一度だけ実行されるようにします。Groupオブジェクトを明示的に作成する必要はありません。単純に宣言して使用できます。
-
Do method: Group structはDoメソッドを提供します。これはリクエストをマージする主なメソッドです。このメソッドは、リソースを識別するための文字列キーと、実際のタスクを実行する関数
fn
の2つの引数を取ります。Doを呼び出すときに、同じキーを持つリクエストがすでに進行中の場合、Doはこのリクエストが完了するのを待機し、その結果を共有します。それ以外の場合は、fn
を実行して結果を返します。 -
Doメソッドには3つの戻り値があります。最初の2つは
fn
の戻り値で、それぞれinterface{}
型とerror
型です。最後の戻り値はブール値で、Doの結果が複数の呼び出しで共有されたかどうかを示します。 -
DoChan: このメソッドはDoに似ていますが、操作が完了したときに結果を受信するチャネルを返します。戻り値はチャネルであるため、ブロックしない方法で結果を待機できます。
-
Forget: このメソッドは、キーとそれに関連付けられたリクエストレコードをGroupから削除するために使用されます。これにより、同じキーでDoを次に呼び出すと、前の結果を再利用する代わりに新しいリクエストが実行されるようになります。
-
Result: これは、DoChanメソッドによって返される構造体型です。リクエストの結果をカプセル化し、次の3つのフィールドが含まれています。
Val
(interface{}):リクエストによって返される結果。Err
(error):リクエスト中に発生したエラー情報。Shared
(bool):結果が現在のリクエスト以外のリクエストと共有されたかどうかを示します。
Installation
次のコマンドを使用して、Goアプリケーションにsingleflight依存関係をインストールします。
go get golang.org/x/sync/singleflight
Example Usage
package main import ( "errors" "fmt" "golang.org/x/sync/singleflight" "sync" ) var errRedisKeyNotFound = errors.New("redis: key not found") func fetchDataFromCache() (any, error) { fmt.Println("fetch data from cache") return nil, errRedisKeyNotFound } func fetchDataFromDataBase() (any, error) { fmt.Println("fetch data from database") return "Leapcell", nil } func fetchData() (any, error) { cache, err := fetchDataFromCache() if err != nil && errors.Is(err, errRedisKeyNotFound) { fmt.Println(errRedisKeyNotFound.Error()) return fetchDataFromDataBase() } return cache, err } func main() { var ( sg singleflight.Group wg sync.WaitGroup ) for range 5 { wg.Add(1) go func() { defer wg.Done() v, err, shared := sg.Do("key", fetchData) if err != nil { panic(err) } fmt.Printf("v: %v, shared: %v\n", v, shared) }() } wg.Wait() }
このコードは、キャッシュからデータをフェッチし、キャッシュミスが発生した場合はデータベースから取得するという典型的な同時アクセスシナリオをシミュレートします。このプロセス中、singleflightライブラリは重要な役割を果たします。これにより、複数の同時リクエストが同時に同じデータにアクセスしようとすると、実際のフェッチ操作(キャッシュからかデータベースからかにかかわらず)が一度だけ実行されるようになります。これにより、データベースの負荷が軽減されるだけでなく、高並行シナリオでのキャッシュブレイクダウンを効果的に防ぐことができます。
コードの出力は次のとおりです。
fetch data from cache redis: key not found fetch data from database v: Leapcell, shared: true v: Leapcell, shared: true v: Leapcell, shared: true v: Leapcell, shared: true v: Leapcell, shared: true
示されているように、5つのゴルーチンが同時に同じデータをフェッチすると、データフェッチ操作は実際には1つのゴルーチンによって一度だけ実行されます。さらに、返されたすべての共有値がtrueであるため、結果が他の4つのゴルーチンと共有されたことを意味します。
Best Practices
Key Design
キーを生成するときは、一意性と一貫性を確保する必要があります。
- Uniqueness: Doメソッドに渡されるキーが一意であることを確認して、Groupが異なるリクエストを区別できるようにします。
{type}:{identifier}
のような構造化された命名規則をキーに使用することをお勧めします。たとえば、ユーザー情報をフェッチする場合、キーはuser:1234
にすることができます。ここで、user
はデータ型を示し、1234
は特定のユーザー識別子です。 - Consistency: 同じリクエストの場合、生成されるキーは、いつ呼び出されても常に一貫している必要があります。これにより、Groupは同一のリクエストを適切にマージし、予期しないエラーを防ぐことができます。
Timeout Control
Group.Doを呼び出すと、最初に着信したゴルーチンはfn
関数を正常に実行できますが、後続の他のゴルーチンはブロックされます。ブロックされた状態が長すぎると、システムが応答性を維持するために、ダウングレード戦略が必要になる場合があります。このような場合、Group.DoChanをselect
ステートメントと組み合わせて使用して、タイムアウト制御を実装できます。
タイムアウト制御を示す簡単な例を以下に示します。
package main import ( "fmt" "golang.org/x/sync/singleflight" "time" ) func main() { var sg singleflight.Group doChan := sg.DoChan("key", func() (interface{}, error) { time.Sleep(4 * time.Second) return "Leapcell", nil }) select { case <-doChan: fmt.Println("done") case <-time.After(2 * time.Second): fmt.Println("timeout") // Implement other downgrade strategies here } }
Summary
- この記事では、最初にキャッシュブレイクダウンの概念と一般的な解決策を紹介しました。
- 次に、singleflightパッケージについて、その基本的な概念、コンポーネント、インストール、および使用例について詳しく説明しました。
- 次に、シミュレートされた同時アクセス例を通じて、singleflightを使用して高並行シナリオでのキャッシュブレイクダウンを防ぐ方法を示しました。
- 最後に、同時処理ロジックを最適化するために、singleflightをよりよく理解し、適用するために、キーを設計し、リクエストのタイムアウトを実際に制御するためのベストプラクティスについて説明しました。
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ