Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
Genericsを使った 効率的なキャッシュの実装 @toki
Slide 2
Slide 2 text
@toki traP所属 一般学生Gopher • ISUCON11本選作問 • ISUCON10, 12, 13 準優勝 近況 • ISUCON13でNaruseJunにまた負けた • traP部内向けPaaS「NeoShowcase」を作った motoki317 motoki1_
Slide 3
Slide 3 text
キャッシュとその罠 Genericなライブラリ 制作裏話 おまけ: 即時反映チェック 目次
Slide 4
Slide 4 text
(インメモリ)キャッシュの使い所 サーバーが高負荷時 データベース等へのクエリ数を減らす レスポンスタイムを短く 毎秒5000兆リクエストが送られて 固まるサーバーの図 [1] [1] 元ネタ https://trap.jp/post/2072/
Slide 5
Slide 5 text
キャッシュの罠 よくあるキャッシュ 1. キャッシュから取得 2. キャッシュに無ければ取得 3. キャッシュに保存 不具合、見抜けますか? よくありそうな感じの Webサーバー内キャッシュ
Slide 6
Slide 6 text
例題の不具合 (1/3) 保存キーが違う! キーが複雑 キーの定義が複数箇所 → 実装したと思ったら 一切動いていなかった!
Slide 7
Slide 7 text
例題の不具合 (2/3) map同時読み書き sync.Map を使うべき → よくある感じの並行 Webサーバーだとpanicする
Slide 8
Slide 8 text
例題の不具合 (3/3) 更新ロックが無い 大量にリクエストが 来たら? fetchData 関数が 遅かったら? → 更新時に過負荷に (Cache Stampede Problem)
Slide 9
Slide 9 text
キャッシュの色々な罠 罠1: キーが違う 罠2: 並行処理対応 罠3: 更新時の負荷 Genericsで 解決しよう (今日の話題)
Slide 10
Slide 10 text
キャッシュとその罠 Genericなライブラリ 制作裏話 おまけ: 即時反映チェック 目次
Slide 11
Slide 11 text
GoのGenerics (since 1.18, 2022-03-15) Genericsが実装されて久しい Goのサポート対象は最新 2 minor versions 現在 (as of 2023-12-23) は 1.20, 1.21 Goは後方互換性が強い → Genericsは既に問題なく導入できる [2] https://github.com/tottie000/GopherIllustrations 嬉しそうな Gohperくん [2]
Slide 12
Slide 12 text
ライブラリを作った キャッシュライブラリ “sc” [3] を作った Genericsを使用し、型安全 前述の3つの課題を解決 名前が何の略かは知らない すごい(sugoi)キャッシュ(cache) [3] https://github.com/motoki317/sc
Slide 13
Slide 13 text
“sc” での書き方 sc ではどう書く? 1. 事前にデータ取得関数を定義 2. キャッシュから透過的に取得 3. データを返す 書き方を強制することで 課題を解決
Slide 14
Slide 14 text
ライブラリの恩恵 (1/3) データ取得関数を キャッシュへ事前に渡す キーのみが取得関数へ渡る Closureによる意図しない 変数のキャプチャを防ぐ → 値の更新時に キーを間違えようが無い!
Slide 15
Slide 15 text
ライブラリの恩恵 (2/3) 取得と更新時の ロックは自動的に行う 値は自動でセット → 難しい並行処理対応を 自分で行う必要が無い!
Slide 16
Slide 16 text
ライブラリの恩恵 (3/3) 重複呼び出しの抑制 非同期更新もできる stale-while-revalidate と似た処理 遅い更新処理でも大丈夫 → 値の更新時に負荷がかからない
Slide 17
Slide 17 text
キャッシュとその罠 Genericなライブラリ 制作裏話 おまけ: 即時反映チェック 目次
Slide 18
Slide 18 text
ライブラリ制作のよもやま話 Genericsの使い方 ライブラリインターフェイスの設計 キャッシュのお掃除方法
Slide 19
Slide 19 text
Genericsの使い方 Cache型にtype parameter キーにstructも指定可能 空のstruct{} = 引数が無いことを 擬似的に表現可能 キャッシュインスタンス生成 New() のシグネチャ 値を取得する Get() のシグネチャ
Slide 20
Slide 20 text
インターフェイス設計 (1/2) Get() はあれど Set() は無い Get() に動的に更新関数を渡せない → 更新関数でキーや処理を 取り違えないための意図的な設計
Slide 21
Slide 21 text
インターフェイス設計 (2/2) 設定にFunctional Options Pattern 破壊的な変更を避け、 機能追加を行いやすい Genericな設定を取りたい場合は 相性が悪いかも 出来なくは無いが、類推が弱い [4] command center: Self-referential functions and the design of options, https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html Functional Options Pattern [4]
Slide 22
Slide 22 text
キャッシュのお掃除 Expire した値を捨てるワーカー GC用に、値の参照を切るために定期実行 runtime.Finalizer により、キャッシュ自体の破棄で自動で停止 細かい話はコードを参照
Slide 23
Slide 23 text
キャッシュとその罠 Genericなライブラリ 制作裏話 おまけ: 即時反映チェック 目次 ちょっと難しいです
Slide 24
Slide 24 text
即時反映チェック ISUCONベンチマーカーは整合性チェックが厳しい だいたいのエンドポイントでは「ベンチマーカーは データの更新が即時反映されていることを期待して 検証を行います。」[5] ちょっとでもキャッシュするとバレてFailに [5] ISUCON12本選 当日マニュアル, https://gist.github.com/shirai-suguru/770d30d16688a07ba78e0a188cd99f9f
Slide 25
Slide 25 text
即時反映チェックを倒す方法 クエリ数は減らしたいが、即時反映もしたい 「singleflightを使えばいいじゃん」 [5] ISUCON12本選 当日マニュアル, https://gist.github.com/shirai-suguru/770d30d16688a07ba78e0a188cd99f9f → 実はダメです
Slide 26
Slide 26 text
singleflight [6] では ダメな理由 [6] https://pkg.go.dev/golang.org/x/sync/singleflight 取得中に更新リクエストが 来ると詰む
Slide 27
Slide 27 text
singleflight [6] では ダメな理由 [6] https://pkg.go.dev/golang.org/x/sync/singleflight 取得中に更新リクエストが 来ると詰む
Slide 28
Slide 28 text
singleflight [6] では ダメな理由 [6] https://pkg.go.dev/golang.org/x/sync/singleflight 取得中に更新リクエストが 来ると詰む ベンチマーカーはこの時点で 次の取得リクエスト(B)が 更新されていることを期待
Slide 29
Slide 29 text
singleflight [6] では ダメな理由 [6] https://pkg.go.dev/golang.org/x/sync/singleflight 取得中に更新リクエストが 来ると詰む ベンチマーカーはこの時点で 次の取得リクエスト(B)が 更新されていることを期待 しかしBは前の取得結果を 利用してしまう! Bで古い結果が返る
Slide 30
Slide 30 text
即時反映チェックを倒す方法 [7] x/sync/singleflight の注意点とゼロタイムキャッシュ #Go – Qiita, https://qiita.com/methane/items/27ccaee5b989fb5fca72 Zero Time Cache [7] ベンチマーカーに気づかれない範囲でキャッシュ 処理の圧縮ともいう
Slide 31
Slide 31 text
ttl = 0 かつ sc.EnableStrictCoalescing() で利用可能 Zero Time Cache [7] [7] https://qiita.com/methane/items/27ccaee5b989fb5fca72
Slide 32
Slide 32 text
ttl = 0 かつ sc.EnableStrictCoalescing() で利用可能 Zero Time Cache [7] [7] https://qiita.com/methane/items/27ccaee5b989fb5fca72
Slide 33
Slide 33 text
ttl = 0 かつ sc.EnableStrictCoalescing() で利用可能 Zero Time Cache [7] [7] https://qiita.com/methane/items/27ccaee5b989fb5fca72
Slide 34
Slide 34 text
ttl = 0 かつ sc.EnableStrictCoalescing() で利用可能 Zero Time Cache [7] BのGet開始前の結果 BにとってStale [7] https://qiita.com/methane/items/27ccaee5b989fb5fca72
Slide 35
Slide 35 text
ttl = 0 かつ sc.EnableStrictCoalescing() で利用可能 Zero Time Cache [7] BのGet開始前の結果 BにとってStale Aが返り次第、 ZTCは次のSELECTを開始 [7] https://qiita.com/methane/items/27ccaee5b989fb5fca72
Slide 36
Slide 36 text
ttl = 0 かつ sc.EnableStrictCoalescing() で利用可能 Zero Time Cache [7] BのGet開始前の結果 BにとってStale Aが返り次第、 ZTCは次のSELECTを開始 BのGet開始後の結果 BにとってFresh Bで更新後の結果が返る [7] https://qiita.com/methane/items/27ccaee5b989fb5fca72
Slide 37
Slide 37 text
ttl = 0 かつ sc.EnableStrictCoalescing() で利用可能 Zero Time Cache [7] BのGet開始前の結果 BにとってStale Aが返り次第、 ZTCは次のSELECTを開始 BのGet開始後の結果 BにとってFresh Bで更新後の結果が返る ポイント: 「即時反映」は 0秒以上前の結果 を許可しない = リクエスト前開始の SELECT結果を利用できない [7] https://qiita.com/methane/items/27ccaee5b989fb5fca72
Slide 38
Slide 38 text
即時更新チェックを倒す別の方法 データの更新箇所が全て分かっているとき 可能な限りキャッシュしながら 更新時に明示的にInvalidate → sc で利用可能!
Slide 39
Slide 39 text
ttl = 任意 更新時に Forget() で利用可能 明示的なInvalidate
Slide 40
Slide 40 text
ttl = 任意 更新時に Forget() で利用可能 明示的なInvalidate
Slide 41
Slide 41 text
ttl = 任意 更新時に Forget() で利用可能 明示的なInvalidate
Slide 42
Slide 42 text
ttl = 任意 更新時に Forget() で利用可能 明示的なInvalidate 更新時に Forget() を呼ぶことで 以降の Get() は値を再利用せず 直ちにSELECTが飛ぶ
Slide 43
Slide 43 text
ttl = 任意 更新時に Forget() で利用可能 Illust: https://github.com/tottie000/GopherIllustrations 明示的なInvalidate 更新時に Forget() を呼ぶことで 以降の Get() は値を再利用せず 直ちにSELECTが飛ぶ 前回のSELECTを無視して 直ちにSELECTが飛ぶ
Slide 44
Slide 44 text
ttl = 任意 更新時に Forget() で利用可能 Illust: https://github.com/tottie000/GopherIllustrations 明示的なInvalidate 更新時に Forget() を呼ぶことで 以降の Get() は値を再利用せず 直ちにSELECTが飛ぶ 前回のSELECTを無視して 直ちにSELECTが飛ぶ Bで更新後の結果が返る
Slide 45
Slide 45 text
ttl = 任意 更新時に Forget() で利用可能 Illust: https://github.com/tottie000/GopherIllustrations 明示的なInvalidate 更新時に Forget() を呼ぶことで 以降の Get() は値を再利用せず 直ちにSELECTが飛ぶ 前回のSELECTを無視して 直ちにSELECTが飛ぶ Bで更新後の結果が返る ポイント: 「即時反映」は 0秒以上前の結果 を許可しない = リクエスト前開始の SELECT結果を利用できない
Slide 46
Slide 46 text
まとめ
Slide 47
Slide 47 text
(インメモリ)キャッシュは銀の弾丸ではない TTLの設定やinvalidate処理はユーザーが行う ここの不具合はライブラリ側では防げない インメモリキャッシュは サーバー分散時にinvalidate処理が辛い groupcache [8] 等の分散キャッシュを用いる Redis, memcached 等でも可 [8] https://github.com/golang/groupcache
Slide 48
Slide 48 text
まとめ Generic(型安全)でバグを埋めにくい キャッシュライブラリ “sc” を作った 対ISUCONベンチマーカー即時反映チェック対応 github.com/motoki317/sc このスライドのリンク