TT Lab
はじめる
学ぶ 学習パス コース

良いサービスを作る CS — 教科書の概念を計測で学び直す

割り込み順序でキャッシュの嘘を測る

TT Labで続きを見る

目標

決定的なシミュレーターで、キャッシュの書き込み・読み取りの戦略6つを、同じ割り込みの順序に入れてみて、シナリオごとに古い読み取りの数と最大の古さ(ms)を測ります。最後に、永遠に直らない組み合わせを見分け、数字を根拠に戦略を1つ選びます。

なぜ重要なのか

キャッシュの不整合は、再現が困難です。数msの差で重なった2つのリクエストが作り、翌日に再実行すると消えます。そのため、「書き込み後に削除すれば大丈夫」「TTLがあるから、最大でもTTLの分だけ間違う」のような思い込みが、検証されないまま残ります。操作が終わる時刻を整数のmsで固定したシミュレーターなら、同じ順序を何度でも再実行でき、戦略だけを変えて同じ順序に入れてみることができます。パターンの説明は「Redisとキャッシュ」コースにあるので、ここでは順序と数字だけを見ます。

用意するもの

/opt/fixtures/svccs/cache/の下にあります。読み取り専用で使ってください。

sim.py                 시뮬레이터. 맨 위 설명에 원시 연산·제너레이터 규칙·명령줄 사용법이 있다
example_strategies.py  시작 틀 — write_update 와 read_aside
scenarios/*.json       시나리오 다섯. name · about(설명) · initial · warm(미리 채운 키) · ttl_ms · delay_ms ·
                       actors[{name, kind(read|write), key, value, start, lat{연산: ms}}]

このコードブロックの韓国語の説明は、順に、sim.pyはシミュレーターで、冒頭の説明に基本操作・ジェネレーターのルール・コマンドラインの使い方があること、example_strategies.pyは開始用の雛形でwrite_updateとread_asideがあること、scenarios/*.jsonは5つのシナリオで、name・about(説明)・initial・warm(事前に埋めたキー)・ttl_ms・delay_ms・actors(name・kind(readまたはwrite)・key・value・start・操作ごとのミリ秒を持つlat)を持つこと、を述べています。

コマンドライン: python3 /opt/fixtures/svccs/cache/sim.py <시나리오.json> <전략파일.py> <전략이름> > 기록.json(プレースホルダーは、順にシナリオのファイル、戦略のファイル、戦略の名前、記録のファイルです) 戦略の名前はupdate・delete_after・delete_before・double_delete・cas・ttlで、それぞれが使う関数は、sim.pyのSTRATEGIESの表にあります(ttlは、write_delete_after + read_asideに、シナリオのttl_msを渡します)。

記録の形

events(操作が終わった順序、seq・t・actor・op …)、commits(seq・key・version・value・t = コミット時刻・writer)、reads(seq = その読み取りの最後の操作のseq・actor・key・start・t = 終わった時刻・value・version)、final_db、final_cache(キーごとのvalue・version・set_t・expires、またはnull)です。

測定のルール

오래된 읽기  r 보다 작은 seq 로 커밋된 같은 키의 버전 중 가장 큰 것이 r.version 보다 크다
오래됨(ms)   r.t − (버전 r.version+1 커밋의 t)            ← 쓰기를 시작한 시각이 아니다
stuck        끝났을 때 final_cache 의 항목이 expires 가 null 이고 version 이 final_db 보다 작다
돌려줄 것    {"stale_reads": 정수, "max_stale_ms": 정수 또는 null, "stuck": 불리언}
             stuck 이면 max_stale_ms 는 null(상한 없음), 오래된 읽기가 없으면 0

このコードブロックの韓国語の説明は、順に、古い読み取りとは、rより小さいseqでコミットされた同じキーのバージョンのうち最大のものがr.versionより大きいこと、古さ(ms)はr.t − (バージョンr.version+1のコミットのt)であり、書き込みを開始した時刻ではないこと、stuckとは、終わったときにfinal_cacheの項目がexpiresがnullでversionがfinal_dbより小さいこと、返す値はstale_reads(整数)・max_stale_ms(整数またはnull)・stuck(ブール値)で、stuckならmax_stale_msはnull(上限なし)、古い読み取りがなければ0であること、を述べています。

ステップ

  1. /root/svccs/cache/を作成し、example_strategies.pyを/root/svccs/cache/strategies.pyにコピーしたあと、s1-two-writersシナリオをupdate戦略で実行した記録を、/root/svccs/cache/s1-update.log.jsonに保存します。
  2. strategies.pyにmeasure(log)を「測定のルール」のとおりに作成し、ステップ1の記録を測った結果を、/root/svccs/cache/s1-update.measure.jsonに保存します。採点ツールは、隠しシナリオの記録でmeasureを再び呼び出します。
  3. write_delete_after(コミット → キャッシュ削除)とwrite_delete_before(キャッシュ削除 → コミット)を作成します。書き込み関数は、(key, value, opts)を受け取るジェネレーターです。
  4. is_newer(cached, new)(キャッシュが空か、newが厳密に大きいときだけ真)、write_cas(コミット → 受け取ったバージョンでcache_set_if)、read_aside_cas(read_asideと同じだが、埋めるときにcache_set_if)を作成します。cache_set_ifの最後の引数としてis_newerを渡します。
  5. write_double_delete(キャッシュ削除 → コミット → ("sleep", opts["delay_ms"]) → キャッシュ削除)を作成します。
  6. 5つのシナリオ × 6つの戦略をすべて実行してmeasureした結果を、/root/svccs/cache/results.jsonに{시나리오 name: {전략 이름: measure 결과}}の形式で保存します(コード内の韓国語は、順に「シナリオ」「戦略の名前」「結果」を意味する語です)。
  7. ttl戦略のシナリオごとのmax_stale_msを集めて、/root/svccs/cache/ttl.jsonに、ttl_ms(シナリオのttl_ms、整数)・max_stale_ms_by_scenario・exceeds_ttl(max_stale_msがttl_msより大きいシナリオの名前、名前の昇順のリスト)を書きます。採点ツールは、あなたのttl戦略も隠しシナリオで再び実行します。
  8. /root/svccs/cache/choice.jsonに、stuck(戦略ごとに、stuckが真だったシナリオの名前のリスト、ソート済み)・safe(どのシナリオでもstuckでない戦略、ソート済み)・chosen(safeのうち、シナリオごとのmax_stale_msの最大値がもっとも小さいもの、同じならstale_readsの合計が小さいもの、それでも同じなら名前順)・chosen_worst_ms(その最大値)を書きます。

参考

シミュレーターを1回実行する

example_strategies.pyを/root/svccs/cache/strategies.pyにコピーし、s1-two-writers.jsonをupdate戦略で実行した記録を、/root/svccs/cache/s1-update.log.jsonに保存してください。

sim.pyは、標準出力に記録のJSONを出します。>でファイルに受け取ってください。記録のeventsを見て、W1の遅いcache_setがW2よりあとに到着する行を探してみてください。

古さをコミットを基準に測る

strategies.pyに、測定のルールどおりmeasure(log)を作成し、ステップ1の記録を測った結果を、/root/svccs/cache/s1-update.measure.jsonに保存してください。採点ツールは、隠しシナリオの記録でmeasureを再び呼び出します。

読み取りのseqとコミットのseqを比較して、「読み取りが終わる前にコミットされたもの」だけを見ます。古さの基準は、返されたバージョンの次のバージョンがコミットされた時刻です。キャッシュに有効期限のない古い値が残っているなら、観測した最大値がいくつであっても、上限はありません。

書き込みのあとに削除、削除のあとに書き込み

strategies.pyにwrite_delete_after(コミット → キャッシュ削除)とwrite_delete_before(キャッシュ削除 → コミット)を作成してください。採点ツールが、隠しシナリオで2つの戦略の操作の記録を基準と突き合わせます。

書き込み関数は、(key, value, opts)を受け取るジェネレーターで、操作を2つyieldします。結果を使わない操作もyieldしないと、シミュレーターが時間を進めません。

バージョンを比較したときだけ上書きする

strategies.pyにis_newer(cached, new)・write_cas・read_aside_casを作成してください。採点ツールは、is_newerを直接呼び出し、cas戦略を隠しシナリオで基準と突き合わせます。

is_newerは、キャッシュにあるバージョン(cached、なければNone)と、入れようとしているバージョン(new)を受け取ります。古い値が新しい値を上書きできないようにするには、どちらが大きい必要があるでしょうか。write_casは、db_writeが返したバージョンを使います。

遅延二重削除

strategies.pyにwrite_double_delete(キャッシュ削除 → コミット → ("sleep", opts["delay_ms"]) → キャッシュ削除)を作成してください。採点ツールが、隠しシナリオで基準と突き合わせます。

sleepは結果のない操作ですが、yieldして初めて、その分の時間が流れます。待つ時間はシナリオごとに異なるので、optsから読み取ってください。

5つのシナリオ × 6つの戦略

5つのシナリオを6つの戦略ですべて実行してmeasureした結果を、/root/svccs/cache/results.jsonに{シナリオのname: {戦略の名前: measureの結果}}の形式で保存してください。

sim.pyをimportして、sim.run_strategy(シナリオ, 戦略モジュール, 名前)を呼び出すと、記録のdictがそのまま得られます。シナリオの名前は、ファイルの中のnameです。

TTLは古さをどこまで抑えるか

ttl戦略のシナリオごとのmax_stale_msを集めて、/root/svccs/cache/ttl.jsonにttl_ms・max_stale_ms_by_scenario・exceeds_ttlを書いてください。採点ツールは、あなたのttl戦略も隠しシナリオで再び実行します。

TTLは、キャッシュに入れた時刻から数えます。コミットよりかなりあとに古い値が入ってくると、どうなるでしょうか。ttl戦略は、read_asideがopts["ttl_ms"]をsetに渡して初めて動作します。

数字で戦略を選ぶ

results.jsonを使って、/root/svccs/cache/choice.jsonにstuck・safe・chosen・chosen_worst_msを書いてください。

1つのシナリオででもstuckなら、その戦略は古さに上限がないので、safeではありません。あるシナリオで古い読み取りが0だったことは、安全の根拠になりません。