キャッシュスタンピードについて
私は、あれこれキャッシュする必要がある状況に陥ることがよくあります。多くの場合、これらの値は一定期間キャッシュされます。おそらくこのパターンに精通しているでしょう。キャッシュから値を取得しようとしましたが、成功した場合は、その値を呼び出し元に返して終了します。値が存在しない場合は、(おそらくデータベースから) 値を取得するか、計算してキャッシュに入れます。ほとんどの場合、これはうまく機能します。ただし、キャッシュ エントリに使用しているキーが頻繁にアクセスされ、データの計算操作に時間がかかる場合、複数の並列リクエストが同時にキャッシュ ミスを起こす状況に陥ります。これらのリクエストはすべて、ソースから独立してロードし、値をキャッシュに保存します。これによりリソースが無駄になり、サービス拒否につながる可能性もあります。
例を挙げて説明しましょう。キャッシュには redis を使用し、その上にシンプルな Go http サーバーを使用します。完全なコードは次のとおりです:
package main import ( "errors" "log" "net/http" "time" "github.com/redis/go-redis/v9" ) type handler struct { rdb *redis.Client cacheTTL time.Duration } func (ch *handler) simple(w http.ResponseWriter, r *http.Request) { cacheKey := "my_cache_key" // we'll use 200 to signify a cache hit & 201 to signify a miss responseCode := http.StatusOK cachedData, err := ch.rdb.Get(r.Context(), cacheKey).Result() if err != nil { if !errors.Is(err, redis.Nil) { log.Println("could not reach redis", err.Error()) http.Error(w, "could not reach redis", http.StatusInternalServerError) return } // cache miss - fetch & store res := longRunningOperation() responseCode = http.StatusCreated err = ch.rdb.Set(r.Context(), cacheKey, res, ch.cacheTTL).Err() if err != nil { log.Println("failed to set cache value", err.Error()) http.Error(w, "failed to set cache value", http.StatusInternalServerError) return } cachedData = res } w.WriteHeader(responseCode) _, _ = w.Write([]byte(cachedData)) } func longRunningOperation() string { time.Sleep(time.Millisecond * 500) return "hello" } func main() { ttl := time.Second * 3 rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) handler := &handler{ rdb: rdb, cacheTTL: ttl, } http.HandleFunc("/simple", handler.simple) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatalf("Could not start server: %s\n", err.Error()) } }
/simple エンドポイントに負荷をかけて、何が起こるかを見てみましょう。これにはベジータを使用します。
ベジータ攻撃を実行します -duration=30s -rate=500 -targets=./targets_simple.txt > res_simple.bin。ベジータは、30 秒間毎秒 500 件のリクエストを行うことになります。それぞれ 100 ミリ秒にわたるバケットを含む HTTP 結果コードのヒストグラムとしてグラフ化します。結果は次のグラフです。
実験を開始すると、キャッシュは空です。そこには値が格納されていません。大量のリクエストがサーバーに到達すると、最初の殺到が起こります。それらはすべてキャッシュをチェックして何も見つからず、longRunningOperation を呼び出してキャッシュに保存します。 longRunningOperation が完了するまでに最大 500 ミリ秒かかるため、最初の 500 ミリ秒以内に行われたリクエストはすべて、longRunningOperation を呼び出すことになります。リクエストの 1 つがキャッシュに値を保存できると、後続のすべてのリクエストがその値をキャッシュから取得し、ステータス コード 200 のレスポンスが表示され始めます。その後、redis の有効期限メカニズムが開始されると、このパターンが 3 秒ごとに繰り返されます。
このおもちゃの例では、これは問題を引き起こしませんが、実稼働環境では、システムへの不必要な負荷、ユーザー エクスペリエンスの低下、さらには自己誘発的なサービス拒否につながる可能性があります。では、どうすればこれを防ぐことができるでしょうか?まあ、方法はいくつかあります。ロックを導入することもできます。キャッシュ ミスが発生すると、コードがロックを達成しようとします。分散ロックは簡単なことではなく、多くの場合、分散ロックには微妙な処理が必要な微妙なエッジケースが存在します。バックグラウンド ジョブを使用して値を定期的に再計算することもできますが、これには追加のプロセスを実行する必要があり、コード内で維持および監視する必要があるさらに別の歯車が導入されます。このアプローチは、動的キャッシュ キーがある場合にも実行できない可能性があります。確率的早期有効期限と呼ばれる別のアプローチがあり、これについてはさらに検討していきたいと考えています。
確率的に早い期限切れになる
この手法を使用すると、確率に基づいて値を再計算できます。キャッシュから値をフェッチするときは、確率に基づいてキャッシュ値を再生成する必要があるかどうかも計算します。既存の価値の有効期限に近づくほど、確率は高くなります。
私は、Optimal Probabilistic Cache Stampede Prevention の A. Vattani、F.Chierichetti、K. Lowenstein による XFetch の具体的な実装に基づいています。
HTTP サーバーに新しいエンドポイントを導入します。これも負荷の高い計算を実行しますが、今回はキャッシュするときに XFetch を使用します。 XFetch が機能するには、高価な操作にかかった時間 (デルタ) とキャッシュ キーの有効期限がいつ切れるかを保存する必要があります。それを達成するために、これらの値とメッセージ自体を保持する構造体を導入します。
type probabilisticValue struct { Message string Expiry time.Time Delta time.Duration }
元のメッセージをこれらの属性でラップし、redis に保存するためにシリアル化する関数を追加します。
func wrapMessage(message string, delta, cacheTTL time.Duration) (string, error) { bts, err := json.Marshal(probabilisticValue{ Message: message, Delta: delta, Expiry: time.Now().Add(cacheTTL), }) if err != nil { return "", fmt.Errorf("could not marshal message: %w", err) } return string(bts), nil }
値を再計算して Redis に保存するメソッドも書きましょう:
func (ch *handler) recomputeValue(ctx context.Context, cacheKey string) (string, error) { start := time.Now() message := longRunningOperation() delta := time.Since(start) wrapped, err := wrapMessage(message, delta, ch.cacheTTL) if err != nil { return "", fmt.Errorf("could not wrap message: %w", err) } err = ch.rdb.Set(ctx, cacheKey, wrapped, ch.cacheTTL).Err() if err != nil { return "", fmt.Errorf("could not save value: %w", err) } return message, nil }
確率に基づいて値を更新する必要があるかどうかを判断するには、probabilisticValue にメソッドを追加します。
func (pv probabilisticValue) shouldUpdate() bool { // suggested default param in XFetch implementation // if increased - results in earlier expirations beta := 1.0 now := time.Now() scaledGap := pv.Delta.Seconds() * beta * math.Log(rand.Float64()) return now.Sub(pv.Expiry).Seconds() >= scaledGap }
すべてをフックすると、次のハンドラーが完成します:
func (ch *handler) probabilistic(w http.ResponseWriter, r *http.Request) { cacheKey := "probabilistic_cache_key" // we'll use 200 to signify a cache hit & 201 to signify a miss responseCode := http.StatusOK cachedData, err := ch.rdb.Get(r.Context(), cacheKey).Result() if err != nil { if !errors.Is(err, redis.Nil) { log.Println("could not reach redis", err.Error()) http.Error(w, "could not reach redis", http.StatusInternalServerError) return } res, err := ch.recomputeValue(r.Context(), cacheKey) if err != nil { log.Println("could not recompute value", err.Error()) http.Error(w, "could not recompute value", http.StatusInternalServerError) return } responseCode = http.StatusCreated cachedData = res w.WriteHeader(responseCode) _, _ = w.Write([]byte(cachedData)) return } pv := probabilisticValue{} err = json.Unmarshal([]byte(cachedData), &pv) if err != nil { log.Println("could not unmarshal probabilistic value", err.Error()) http.Error(w, "could not unmarshal probabilistic value", http.StatusInternalServerError) return } if pv.shouldUpdate() { _, err := ch.recomputeValue(r.Context(), cacheKey) if err != nil { log.Println("could not recompute value", err.Error()) http.Error(w, "could not recompute value", http.StatusInternalServerError) return } responseCode = http.StatusAccepted } w.WriteHeader(responseCode) _, _ = w.Write([]byte(cachedData)) }
ハンドラーは最初のハンドラーとほぼ同じように動作しますが、キャッシュ ヒットを取得するとサイコロを振ります。結果に応じて、フェッチしたばかりの値を返すか、値を早めに更新します。
HTTP ステータス コードを使用して、次の 3 つのケースのどちらかを判断します。
- 200 - キャッシュから値を返しました
- 201 - キャッシュミス、値が存在しません
- 202 - キャッシュ ヒット、トリガーされた確率的更新
今度は新しいエンドポイントに対して実行して vegeta を再度起動します。結果は次のとおりです。
そこにある小さな青い塊は、実際にキャッシュ値を早期に更新し終えた時期を示しています。最初のウォームアップ期間の後にキャッシュミスが発生することはなくなりました。ユースケースにとって重要な場合は、初期のスパイクを回避するために、キャッシュされた値を事前に保存できます。
キャッシュをより積極的に行い、値をより頻繁に更新したい場合は、ベータ パラメーターを使用してみてください。ベータパラメータを 2 に設定した場合の同じ実験は次のようになります:
確率的な更新がより頻繁に行われるようになりました。
全体として、これはキャッシュ スタンピードを回避するのに役立つちょっとした巧妙なテクニックです。ただし、これはキャッシュから同じキーを定期的に取得する場合にのみ機能することに注意してください。そうでない場合は、あまりメリットがありません。
キャッシュスタンピードに対処する別の方法はありますか?間違いに気づきましたか?以下のコメント欄でお知らせください!
以上がGo における確率的な早期有効期限切れの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

GOプログラミングでは、エラーを効果的に管理する方法には、1)例外の代わりにエラー値の使用、2)エラーラッピング技術の使用、3)カスタムエラータイプの定義、4)パフォーマンスの再利用、パフォーマンスと回復の使用、5)エラーメッセージは明確で一貫性があることを保証する、7)エラーの処理を補うエラーこれらのプラクティスとパターンは、より堅牢で保守可能で効率的なコードを書き込むのに役立ちます。

GORoutinesとチャンネルを使用して、GOで同時性を実装できます。 1)音楽を楽しんだり、同時に友人を観察したりするなど、ゴルチンを使用して並行してタスクを実行します。 2)生産者モデルや消費者モデルなどのチャネルを介してゴルチン間でデータを安全に転送します。 3)ゴルチンやデッドロックの過度の使用を避け、同時プログラムを最適化するためにシステムを合理的に設計します。

goooffersmultipreapproaches forbuildingconcurreantdatastructures(mutexes、channels、andatomicoperations.1)mutexexexexexexexexexexexexexprovidesimprovidesedsafetybutcancauseperformancebottlenecks.2)チャネルオフェルスケーリビリティButmaybutlorempty.3)

goserrorhandlingisexplicit、treatingErrorsassedededededededededectectionsは、pythonandjava.1とは異なります

fatestinggocodewithinit functions、useexplicitsetupfunctionsurseSorseparatet fileStoavoidepencyonInitonitisideEffects.1)useexplicitsetupfuncontrollglobalbariaveInitialization.2)createSeparateSteSteSteStobypassInit funtedtententen

Go'serrorhandlingReturnserrorsasasvalues、javaandpython whichuseexceptions.1)go'smethodensuresexpliciterror handling

効果的なインターフェイスリングミニマル、クリア、およびプロモテスルーシューリング。1)インターフェイスForfforfibilityOfimplementation.2)interfacesforact forabstractiontoswapimplementations withingingcallingcode.3)設計の快適性を発信すること

集中型エラー処理は、GO言語でのコードの読みやすさと保守性を向上させることができます。その実装方法と利点には、次のものが含まれます。1。ビジネスロジックからロジックを個別に処理し、コードを簡素化します。 2。中央の取り扱いによるエラー処理の一貫性を確保します。 3. DeferとRecoverを使用してパニックをキャプチャおよび処理して、プログラムの堅牢性を高めます。


ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

Dreamweaver Mac版
ビジュアル Web 開発ツール

Safe Exam Browser
Safe Exam Browser は、オンライン試験を安全に受験するための安全なブラウザ環境です。このソフトウェアは、あらゆるコンピュータを安全なワークステーションに変えます。あらゆるユーティリティへのアクセスを制御し、学生が無許可のリソースを使用するのを防ぎます。

SublimeText3 Linux 新バージョン
SublimeText3 Linux 最新バージョン

SAP NetWeaver Server Adapter for Eclipse
Eclipse を SAP NetWeaver アプリケーション サーバーと統合します。

WebStorm Mac版
便利なJavaScript開発ツール
