Slide 1

Slide 1 text

DELISH KITCHENにおける マスタデータキャッシュ戦略と その歴史的変遷 Akira Uchihara

Slide 2

Slide 2 text

2 Copyright © 2015 every, Inc. All rights reserved. 目次 1. 自己紹介 2. DELISH KITCHENの紹介 3. DELISH KITCHENにおけるデータ種別とキャッシュの考え方 4. 第一次キャッシュ戦略(2016〜2019あたり) 5. 第二次キャッシュ戦略(2020あたり) 6. 次世代キャッシュ戦略検討における試行錯誤(2023あたり) 7. 第三次キャッシュ戦略(2023) 8. 今後のキャッシュ戦略構想(2024〜) 9. まとめ 10. 宣伝 がっつりGoのお話というより、 DELISH KITCHENにおけるシステム開発の歴史+Goでの実装解説 みたいな感じです

Slide 3

Slide 3 text

3 Copyright © 2015 every, Inc. All rights reserved. 自己紹介 • 内原 章 • 経歴 • 1996〜 • パソコン通信サービス( Nifty-Serve) • 2004〜 ヤフー • 無料ホームページサービス( Geocities) • 社内向けプラットフォーム基盤の開発、運用 • 2010〜 頓智ドット • ARサービス(セカイカメラ)のバックエンド開発、運用 • その後CTOに就任 • 2017〜 エブリー • DELISH KITCHENのバックエンド開発、運用 • インフラ基盤、プッシュ通知基盤、プレミアムサービス基盤など • 現在TIMELINE開発部部長、DELISH KITCHEN開発部副部長 • 趣味 • 文字コードに関する話 • 歴史モノ • 世界史、生物史、人類 史、宇宙史、言語史な ど もののけ姫におけるア シタカは蝦夷の一族で あり史実を踏まえると 室町時代中期の東北 地方から云々

Slide 4

Slide 4 text

4 DELISH KITCHENの紹介

Slide 5

Slide 5 text

5 Copyright © 2015 every, Inc. All rights reserved. レシピ動画サービス『 DELISH KITCHEN』 ■ 日本最大級のレシピ動画サービス ■ 「誰でも簡単においしく作れるレシピ動画」毎日配信 ■ 全レシピ、管理栄養士が監修!蓄積データによるオリジナルレシピを考案 ■ SNS/APP/WEBを通じて延べ50,000本以上のレシピ動画を提供 レシピ提案 / レシピ検索 / 献立機能 買い物リスト / チラシ / 店頭ビーコン連動 調理手順動画 /キッチンモード / 作った・レビュー / お気に入り 菅原千遥  DELISH KITCHEN カンパニー長 料理研究家 斎藤 香織 副編集長 管理栄養士 • 30名以上の食のプロが、蓄積 データに基づきオリジナル レシピを日々考案 • 月間1,500本以上の動画配信 サービス・機能 レシピ検討 買い物 料理中・料理後 専門家によるレシピ考案・監修

Slide 6

Slide 6 text

6 Copyright © 2015 every, Inc. All rights reserved. DELISH KITCHENのこれまでの成長 総ユーザー数 3500万人以上
 DELISH KITCHEN リリース 
 Go v1.7.1


Slide 7

Slide 7 text

7 DELISH KITCHENにおけるデータ種別とキャッシュの考え方

Slide 8

Slide 8 text

8 Copyright © 2015 every, Inc. All rights reserved. DELISH KITCHENのシステム概要 • DELISH KITCHENは複数の機能を提供している • 基本機能 • 課金機能 • 特売情報機能 • サイネージ機能 • etc • 今回のトピックは基本機能のシステムのお話 • レシピを動画で閲覧 • おすすめレシピ • お気に入り管理 • 特集、カテゴリ • 認証 • etc

Slide 9

Slide 9 text

9 Copyright © 2015 every, Inc. All rights reserved. DELISH KITCHENにおけるデータの種別 マスタデータはなるべく積極的かつ長期的にキャッシュしておきたい データ種別 説明 変化タイミング 更新頻度 レコード数 トランザクション データ ユーザ情報、お気に入りな ど ユーザの行動 高 多 マスタデータ レシピ、材料、カテゴリ、タ グなど 管理ツールによる入稿作業 低 少

Slide 10

Slide 10 text

10 Copyright © 2015 every, Inc. All rights reserved. 第一次キャッシュ戦略(2016〜2019あたり)

Slide 11

Slide 11 text

11 Copyright © 2015 every, Inc. All rights reserved. 第一次キャッシュ戦略 • 全テーブル、全レコードを一括してメモリ上に格納 • なので、キャッシュというよりオンメモリストレージというか • データ更新遅延を避けるため定期的に再読み込み処理を行う

Slide 12

Slide 12 text

12 Copyright © 2015 every, Inc. All rights reserved. 実装 // cache全体の初期化処理 func (c *cache) Reset() { for { c.recipeCache.Reset() … time.Sleep(1 * time.Minute) } } // recipe cacheの初期化処理 func (c *recipeCache) Reset() { recipes, err := sql.Recipe.FindAll() c.mutex.Lock() defer c.mutex.Unlock() … c.recipeMap := c.mapping(recipes) } // recipe cacheからidで参照して返却 func (s *recipeService) Get(id int) (*model.Recipe, error) { recipe, err := cache.Recipe.Find(id) … return recipe, nil }

Slide 13

Slide 13 text

問題点

Slide 14

Slide 14 text

14 Copyright © 2015 every, Inc. All rights reserved. 問題点 その1 • ロジックにおいてデータがキャッシュ上に存在する前提になっており、データ格納場所を意識 したコードになってしまっている 単純に綺麗じゃない 本来ロジックとデータの格納場所とは無関係なはず

Slide 15

Slide 15 text

15 Copyright © 2015 every, Inc. All rights reserved. 問題点 その2 • 同じデータ構造であっても、状況によってキャッシュしたいケースとしたくないケースがある • 管理ツール上ではマスタデータはキャッシュする必要がないとか • ロジックを流用できなくなる • アプリで使っているロジックを管理ツールでも使いたいが、データ格納場所に依存するコードになっ ているので流用できない 類似コードを別々に実装しなければならない

Slide 16

Slide 16 text

16 Copyright © 2015 every, Inc. All rights reserved. 実装 // アプリ向けのレシピ参照処理 (cacheから読み込み) func (s *recipeService) Get(id int) (*model.Recipe, error) { recipe, err := cache.Recipe.Find(id) … } … // 管理ツール向けのレシピ参照処理 (rdbから読み込み) func (s *recipeAdminService) Get(id int) (*model.Recipe, error) { recipe, err := sql.Recipe.Find(id) … }

Slide 17

Slide 17 text

17 Copyright © 2015 every, Inc. All rights reserved. 問題点 その3 • テスト実装時にデータ作成が面倒 • DBデータとキャッシュ両方投入しなければならない

Slide 18

Slide 18 text

18 Copyright © 2015 every, Inc. All rights reserved. 実装 func TestRecipeGet(t *testing.T) { // DBにテストデータを投入 sql.Recipe.Create(&recipeModel) // DBからキャッシュに読み込み cache.Recipe.Reset() recipe, err := recipeService.Get(id) … }

Slide 19

Slide 19 text

19 Copyright © 2015 every, Inc. All rights reserved. 第二次キャッシュ戦略(2020あたり)

Slide 20

Slide 20 text

20 Copyright © 2015 every, Inc. All rights reserved. 第二次キャッシュ戦略 • テーブル単位ではなくレコード単位でキャッシュ • DBの前段にキャッシュを配置 • 有効期限あり • キャッシュ格納ストレージを選択可能 要するに普通のキャッシュに立ち帰りたい

Slide 21

Slide 21 text

21 Copyright © 2015 every, Inc. All rights reserved. 実装 package repository type Recipe interface { Find(id int) (*model.Recipe, error) } … // RDB経由でメモリ上にcacheされるrepositoryを保持 func NewRecipeService() RecipeService { return &recipeService{ recipeRepo: memorycache.NewRecipe(sql.NewRecipe()), } } // 抽象化されたrepositoryからrecipeを返却 func (s *recipeService) Get(id int) (*model.Recipe, error) { recipe, err := s.recipeRepo.Find(id) … return recipe, nil }

Slide 22

Slide 22 text

22 Copyright © 2015 every, Inc. All rights reserved. 実装 func TestRecipeGet(t *testing.T) { s := &recipeService{ recipeRepo: recipeMock, // mock経由でデータを返却する場合 // recipeRepo : sql.NewRecipe(), // RDB経由でデータを返却する場合 } recipe, err := s.recipeRepo.Find(id) … }

Slide 23

Slide 23 text

23 Copyright © 2015 every, Inc. All rights reserved. 第二次キャッシュ戦略のその後 • テーブル単位で実装を切り替えていたが、この時点では20/150とかそんな感じの進捗 • キャッシュ効率を考えてリモートキャッシュサーバ(Redisとか)に移行したいが、転送量の問 題もあり一気に進めるのは怖いので慎重に • キャッシュ移行専任の作業担当者を置いていたが、作業が単調すぎてこのまま続けるのは 辛いということでいったん別のタスクにスイッチしたため、改修作業は保留 • その後担当者が退職するなどでそのままコードフリーズ状態に

Slide 24

Slide 24 text

徐々に運用的にも辛く なってくる(2021あたり)

Slide 25

Slide 25 text

25 Copyright © 2015 every, Inc. All rights reserved. 問題点 その4 サービス成長に伴いデータ量アクセス数が増加し以下の問題が無視できなくなってくる • 起動時に一括で読み込みするので立ち上げに時間がかかる • 特にローカルで起動するたびに時間がかかるのが辛い • RDBへの負荷増加 • スケールアウトによりタスク数が増えた場合定期的なRDB読み込みで負荷が高まる • RDBへの負荷を減らす目的でキャッシュしているはずなのに・・・ • CPU負荷増加 • 構造体への変換時やGCでCPU時間を消費

Slide 26

Slide 26 text

26 Copyright © 2015 every, Inc. All rights reserved. 次世代キャッシュ戦略構想における試行錯誤(2023あたり)

Slide 27

Slide 27 text

27 Copyright © 2015 every, Inc. All rights reserved. 次世代キャッシュ戦略構想における試行錯誤 • Hazelcast (In Memory Database) • MySQL binlogによる同期機構があるので移行コストが低そうに思えたが • Cloudだとコストが高い、オンプレだと運用が辛い • リモートキャッシュとローカルキャッシュの多段構成 • 多段にしたところでメモリ効率はそこまで向上しなさそう • Pyroscopeを用いた負荷計測&地道な改善

Slide 28

Slide 28 text

28 Copyright © 2015 every, Inc. All rights reserved. キャッシュ処理のCPU負荷軽減 • キャッシュ構築時にgocraft/dbrがCPUを消費している箇所があった • dbr内部でmodel用の構造体にマッピングする部分で、カラムのデータ構造と構造体メンバ 変数の関係性のためreflectionしている箇所で重くなっていた • またオブジェクトが頻繁に生成←→破棄されることでGC時の負荷が増えていた • 対応策 • model別にハードコードされたGo実装を自動生成 • sync.Pool を用いたオブジェクトの再利用

Slide 29

Slide 29 text

29 Copyright © 2015 every, Inc. All rights reserved. 実装 import ( "sync” "github.com/jmoiron/sqlx" ) var poolRecipe = &sync.Pool{ New: func() interface{} { return &model.Recipe{} }, } func ScanRecipe(scanner sqlx.ColScanner, columns []string, ptr []interface{}) (model.Recipe, error) { v := poolRecipe.Get().(*model.Recipe) defer poolRecipe.Put(v) for i := 0; i < len(columns); i++ { switch columns[i] { case "id": ptr[i] = &v.ID … } } return *v, scanner.Scan(ptr...) }

Slide 30

Slide 30 text

30 Copyright © 2015 every, Inc. All rights reserved. キャッシュ処理のCPU負荷軽減 (全てのmodelを置換したら)CPU負荷が半減 変更前 変更後 2.76 seconds 1.27 seconds

Slide 31

Slide 31 text

31 Copyright © 2015 every, Inc. All rights reserved. 第三次キャッシュ戦略(2023)

Slide 32

Slide 32 text

32 Copyright © 2015 every, Inc. All rights reserved. 第三次キャッシュ戦略 • Remote Cache Loaderの導入 • 定期的にキャッシュをキャッシュサーバに読み込むタスク • キャッシュ読み込み処理を一元管理 • 多数のタスクが一斉にキャッシュを構築する無駄をなくし、負荷のコントロールをしたかった • キャッシュ圧縮の実装 • キャッシュ時にmsgpack+gzipで圧縮←→展開 • キャッシュ不整合の回避 • model構造が変化した場合に旧データ構造がキャッシュされているとおかしなことになる • 当初commit idをキャッシュキーにして不整合を回避していたが、デプロイ時にキャッシュ領域が倍 になる問題があった • model構造をメタ情報としてハッシュ化してキャッシュキーに用いることで効率性が向上

Slide 33

Slide 33 text

33 Copyright © 2015 every, Inc. All rights reserved. 実装(圧縮) import ( “compress/gzip" "github.com/ugorji/go/codec" ) var mh = codec.MsgpackHandle{RawToString: true} func DecodeGZipValue[T any](value []byte) (T, error) { var res T body := bytes.NewBuffer(value) r, err := gzip.NewReader(body) … err = codec.NewDecoder(r, &mh).Decode(&res) … return res, nil }

Slide 34

Slide 34 text

34 Copyright © 2015 every, Inc. All rights reserved. 実装(キャッシュキー) // mから "Recipe{ID:int64;Title:string;Attention:*string}" のようなデータを返却 func SerializeTypeInfo(m interface{}) (bytes []byte) { … } func HashTypeInfo(m interface{}) string { digest := sha1.Sum(SerializeTypeInfo(m)) return hex.EncodeToString(digest[:]) } var recipeCacheKey = "recipe:" + HashTypeInfo(model.Recipe{})

Slide 35

Slide 35 text

35 Copyright © 2015 every, Inc. All rights reserved. 第三次キャッシュ戦略のその後 • キャッシュサーバとの転送量がえらいことになり一部対応のみで中断 • 現状の実装ではデータ取得をカジュアルにやりすぎているため • 本来はEager Loadingを考慮した読み込み&キャッシュが必要

Slide 36

Slide 36 text

36 Copyright © 2015 every, Inc. All rights reserved. 実装 func (s *recipeService) Search(query string) (model.Recipes, error) { recipes, err := s.recipeRepo.Search(query) … for _, recipe := range recipes { // N+1になってしまっているが、オンメモリなら問題が顕在化しない ingredients, err := s.recipeIngredientRepo.FindByRecipeID(recipe.ID) … } }

Slide 37

Slide 37 text

37 Copyright © 2015 every, Inc. All rights reserved. 今後のキャッシュ戦略構想(2024〜)

Slide 38

Slide 38 text

38 Copyright © 2015 every, Inc. All rights reserved. 今後の構想など • オンメモリキャッシュはそのままに、差分更新するアプローチ? • レコード単位ではなくアプリケーションにおける基本データ構造単位でキャッシュ? • recipe serviceのようにデータ構造単位でmicro service化して責務を分担?

Slide 39

Slide 39 text

39 Copyright © 2015 every, Inc. All rights reserved. まとめ

Slide 40

Slide 40 text

なぜたかがキャッシュで そんな苦労を...?

Slide 41

Slide 41 text

41 Copyright © 2015 every, Inc. All rights reserved. まとめ • たかがキャッシュ、されどキャッシュ • 実装の大部分はデータ構造によって定められる。キャッシュと言えど例外ではない • 急いでいるからといって雑に実装すると後々自分の首を絞めることになる • 早め早めの対処が重要

Slide 42

Slide 42 text

42 Copyright © 2015 every, Inc. All rights reserved. エブリーからのお知らせ Go Conference 2024スポンサー4社共催
 アフターイベント開催します🎉 🔍 エブリー オウンドメディア 🔍 エブリー テックブログ ・Go Conference 2024 で高まった Go の余熱を分かち合いたい Gopher ・Go Conference 2024 に参加できなかったけど、カンファレンスの 雰囲気を感じたい Gopher ・Go / Gopher コミュニティが大好きな Gopher やエンジニア 🔍 connpass ANDPAD こんな人におすすめ!!

Slide 43

Slide 43 text

No content