こんにちは、メディア事業本部所属の石塚(@ij_spitz)です。こちらはGunosy Advent Calendar 2018、4日目の記事です。なお、昨日の記事は@timakinさんのGoで多層キャッシュ実装と@aibouさんのInfrastructure as Codeの心構えでした。
何を書くか全然決めてなかったのですが、昨日@timakinさんがGoでの多層キャッシュの実装ポイントについて書いていたことに加えて、僕も最近業務で多層キャッシュについて取り組んでいたので、Goで多層キャッシュを実装するときに役立つtipsについて書きたいと思います。
ちなみに僕は多層キャッシュのことを多段キャッシュと呼んでいましたが、曖昧さを解消するために本記事では多層キャッシュで統一します。
なぜ多層キャッシュを使うのか
多層キャッシュ自体については昨日のブログで既に説明されているので詳細は控えますが、下図のようにローカルとリモートで多層にわたってキャッシュする手法です。
利用する目的としては単純にDB負荷を減らすということもあるのですが、主な目的はキャッシュにおけるthundering herd対策となります。 thundering herdとは頻繁にアクセスされるキャッシュデータがエクスパイアした際に、次にデータがキャッシュされるまでの間同一のリクエストが多数バックエンドに飛んでしまうという問題です*1。
多層キャッシュのイメージ画像はこちらのスライドより引用しています。 2年ほど前の資料ですが、キャッシュ戦略について非常に参考になると思うので、興味のある方にはぜひ見ていただきたい資料です。
www.slideshare.net
Goで多層キャッシュを実装する上で役立つtips
singleflightで同時関数呼び出しを1度に抑える
いくら多層キャッシュをしていても、両方のキャッシュがエクスパイアしてしまうとバッグエンドに多数のリクエストが飛んでしまうので、そこの対策をしてくれるpackageがsingleflightです。
同時に多数のリクエストが飛んできて同じ関数を実行した場合に、初めのリクエストだけ関数を実行して他のリクエストは待機させ、初めのリクエストが終了した時に同じ結果を渡してくれます。 使い方は簡単で、以下のように実装できます。
import "golang.org/x/sync/singleflight" func (s *articleService) GetArticleByID(articleID uint64) { var g Group // remote cacheもlocal cacheもエクスパイアした時の処理 article, err, _ := g.Do("key", func() (interface{}, error) { article, err := s.articleRepo.GetArticleByID(articleID) if err != nil { return nil, err } err = s.localCacheRepo.SetArticleByID(articleID, article) if err != nil { return nil, err } err = s.remoteCacheRepo.SetArticleByID(articleID, article) if err != nil { return nil, err } return article, nil }) }
local cacheとremote cacheの機構を揃える
至極単純な話なのですが、localとremoteで別々の機構を使用しているとほとんど同じ動作をするコードを書くことになるため実装のコストが倍になります。 なので特段理由がない限りはlocal cacheとremote cacheの機構を揃えた方がベターだと思います。
僕の最近のプロジェクトではlocal cacheにECSのサイドカーで建てたmemcachedを使用して、remote cacheにはAmazon ElastiCacheで建てたmemcachedを使用していました。 話は少しそれますが、Goのmemcachedのクライアントライブラリはgomemcacheを使用していました。 スターの数が多いので安心感があり、unixドメインソケットでも接続できるのでECSのサイドカーで建てたmemcachedとの相性もよかったです。
おわりに
昨日の記事に引き続き、Goでの多層キャッシュの内容になってしまいましたが、参考になれば幸いです。
明日はGoネタではなくて、遺伝的アルゴリズムになる予定なので、引き続きGunosy Advent Calendar 2018をお楽しみください。