Slide 1

Slide 1 text

Cloud Spannerと 上手く付き合うコツ 西川 蒼太郎

Slide 2

Slide 2 text

氏名  : 部署  : ● 2020年度新卒入社(4年目) ● 運用タイトルのサーバーエンジニアとして バリバリ運用開発中 ● 最近「セメント」と呼ばれる減量飯を食べ続け て-10kgのダイエットに成功したらしい 西川 蒼太郎 2 自己紹介 技術基盤本部 第2バックエンドエンジニア部 第1グループ 第1チーム

Slide 3

Slide 3 text

今日お話する内容 3 Cloud Spanner

Slide 4

Slide 4 text

今日お話する内容 ● Cloud Spanner とは? ● 開発する上での Cloud Spanner との付き合い方 ○ トランザクションの扱い ○ インターリーブ ○ ステイル読み取り 4

Slide 5

Slide 5 text

5 Cloud Spanner とは?

Slide 6

Slide 6 text

● Google Cloud Platform にある DB サービス ● コロプラでは2018年ごろから 新作タイトルの「ユーザーデータ」の管理を MySQL から Spanner に移行 6 Cloud Spanner とは?

Slide 7

Slide 7 text

● 当然 SQL が使えるし、トランザクションもしっかりある ● 自前シャーディングが一切不要 ● スケールイン・アウトが非常に容易 7 Cloud Spanner の特徴 MySQL Spanner シャーディング 自前シャーディングが必要 内部で自動シャーディング! スケールイン・アウト オペレーションが大変 WebUIからワンクリックで完了! 接続先管理 どのシャードに接続するか アプリ側で管理が必要 管理不要! コロプラにおける MySQL 運用との比較

Slide 8

Slide 8 text

● だいたいその通り ● ただし Spanner の特性に寄り添った設計・実装は不可欠 ○ Spanner ≠ MySQL ● 現場で意識すると良いポイントを3点ほど紹介 ○ トランザクションの扱い ○ インターリーブ ○ ステイル読み取り 8 Cloud Spanner は夢のデータベース?

Slide 9

Slide 9 text

9 トランザクションの 扱い

Slide 10

Slide 10 text

● 読み書きトランザクション(Read-Write) ○ MySQL(InnoDB) の SERIALIZABLE とだいたい同じ感覚 ○ select したデータに問答無用で共有ロックをかける ● 読み取り専用トランザクション(Read-Only) ○ MySQL(InnoDB) の REPEATABLE READ とだいたい同じ感覚 ○ ロックを取らずに select できる ○ ただし更新処理はできない ● ゲームバックエンドの処理は大抵更新処理を伴う ● =ほぼ Read-Write しか使わない ● =複数のトランザクションでロックが衝突しやすい! 10 Spannerにおける2種類のトランザクション

Slide 11

Slide 11 text

● select したデータに問答無用で共有ロックをかける ● commit するときに書き込む行の占有ロックを取得する ● ロックが衝突した場合、優先度の高いトランザクションが勝つ ● 負けたトランザクションは Abort される 11 Read-Write のロック解決法 RW-Txn1 RW-Txn2 Read A Write A Read A t Write A commit commit Abort !!

Slide 12

Slide 12 text

● Abort されたトランザクションはリトライされる ○ リトライされる前提でコードを書く必要アリ ○ (Spanner クライアントの実装による) ● Spanner 以外のデータを更新する際は常に注意 ○ static 変数や Redis のキャッシュなど 12 トランザクションのリトライ // Transaction の中で... DB::transaction(function (){ ... // static 変数をインクリメントしたり static::$value++; // Redis に値を詰めたり Redis::set('key', $value); }); NG例

Slide 13

Slide 13 text

● 基本的に static 変数は使わない方がよい ● もし使いたい場合、ロールバックイベントに初期化処理を登録する 13 リトライを意識したコード例① static $value = null; // トランザクションがロールバックしたら値を初期化する app()['events']->listen(TransactionRolledBack::class, function () { $value = null; }); OK例

Slide 14

Slide 14 text

● キャッシュ更新, キューイングなどは commit 後のコールバックとして登録する 14 リトライを意識したコード例② DB::transaction(function () { ... // commit 後のコールバックに処理を登録する DB::afterCommit(function ($value){ // Redis に値を詰めたり Redis::set('key', $value); // キューにデータを詰めたり Job::dispatch($value); // 重要なログを出力したり(調査用ログなど) Log::info("調査用ログ", ["value" => $value]); }); }); OK例

Slide 15

Slide 15 text

15 インターリーブ

Slide 16

Slide 16 text

16 Spannerの分散アーキテクチャ クライアント Node Node Node Node Node (Spanner Servers) 4TBまで管理できるサーバー Colossus(分散ストレージ) 実データはここに格納される 各 Node は 複数の Split の オーナー Split 参考:Cloud Spanner のハイレベルアーキテクチャ解説

Slide 17

Slide 17 text

● Split を跨ぐクエリはパフォーマンス劣化につながる ○ 低QPS→高QPSになるまで顕在化しないケースも ○ できるだけ Split を跨がないクエリが肝要 ● そのための「インターリーブ」 ○ 特定のデータに親子関係を付与できる ○ 親子データは物理的に同じ Split に配置される 17 Spannerの分散アーキテクチャ

Slide 18

Slide 18 text

Splitを跨ぐ ● 軽い気持ちで Split を跨ぐ処理を書いてみる ● (例)自分の所持アイテムを取得する 18 User (PK) UserId … UserItem (複合PK) - UserId - UserItemId ItemId … SELECT * FROM UserItem WHERE UserId = "自分のUserId";

Slide 19

Slide 19 text

インターリーブしないとデータごとに Split がバラバラ 19 Splitを跨ぐ Split Split Split Split Aさん User Bさん UserItem Aさん UserItem Bさん User Aさん UserItem Cさん User … …

Slide 20

Slide 20 text

Splitを跨がない ● インターリーブを適用してみる ● 「User:UserItem=親:子」の関係にする 20 SELECT * FROM UserItem WHERE UserId = "自分のUserId"; CREATE TABLE UserItem ( userId STRING(36) NOT NULL, userItemId STRING(36) NOT NULL, itemId INT64 NOT NULL, ... ) PRIMARY KEY(userId, userItemId), INTERLEAVE IN PARENT User ON DELETE CASCADE

Slide 21

Slide 21 text

インターリーブすると物理的に同じ Split にデータが格納される! (※ただし1Splitの合計容量8GBを超えると別Splitになる) 21 Splitを跨がない Split Split Aさん User Aさん UserItem Aさん UserItem Bさん User Bさん UserItem Bさん UserItem Cさん User … … Cさん UserItem

Slide 22

Slide 22 text

● Spanner のテーブル設計においてインターリーブはマスト ● コロプラでは User を親にした設計がスタンダード 22 インターリーブの例 User Aさん UserChara UserItem … Split User Bさん UserChara UserItem … User Cさん UserChara UserItem … Split … … …

Slide 23

Slide 23 text

それでもインターリーブは跨ぎたい ● とはいえインターリーブを跨ぎたくなるケースは出てくる ● 特にソーシャルゲームでは「フレンド」を取得しがち ○ フレンドのユーザー情報を閲覧する ○ 自分宛のフレンド申請を取得する ○ フレンドと対戦する ○ etc... ● そのための「ステイル読み取り」 23

Slide 24

Slide 24 text

24 ステイル読み取り

Slide 25

Slide 25 text

インターリーブを跨ぐ ● 軽い気持ちでインターリーブを跨ぐ処理を書いてみる ● (例)自分宛のフレンド申請を取得する 25 FriendRequest (PK) FriendRequestId fromUserId toUserId from_to_unique_index 1: fromUserId 2: toUserId to_from_unique_index 1: toUserId 2: fromUserId TABLE INDEX

Slide 26

Slide 26 text

インターリーブを跨ぐ ● 軽い気持ちでインターリーブを跨ぐ処理を書いてみる ● (例)自分宛のフレンド申請を取得する 26 // 1ユーザー辺り最大100件を想定 SELECT * FROM FriendRequest WHERE toUserId = "自分のUserId";

Slide 27

Slide 27 text

27 27 インターリーブを跨ぐ ● ある時、突然大量のエラーが・・・(実話) ● 低QPS→高QPSになった途端、Spanner が詰まり始めた ● ほとんどが Spanner の通信タイムアウト(DEADLINE_EXCEEDED)

Slide 28

Slide 28 text

● 「ステイル読み取り」を駆使して可能な限りパフォーマンスを上げる ● 過去のタイムスタンプを使った読み取りになる ○ 整合性:✕ ○ パフォーマンス:◯ ● ロックを取らない ○ Read-Write トランザクション内でもノーロックで select が可能 ■ トランザクションから独立した読み取りになる ○ ロック解放待ちの時間が減る! ○ Abort(リトライ)の危険性も減る! 28 ステイル読み取りする

Slide 29

Slide 29 text

● Split には「リーダー」「レプリカ」の2種類が存在する ○ リーダーは常に最新 ○ レプリカはちょっと古いことがある ● 過去のタイムスタンプを指定すると 「そのタイムスタンプより新しいデータを持つレプリカ」から データを読み取ることができる 29 過去のタイムスタンプを使った読み取り? リーダー レプリカ レプリカ ちょっと古いことがある 10秒に一度 リーダーと同期する 常に最新

Slide 30

Slide 30 text

30 ● Spanner のデフォルトはコレ ● 古いレプリカに当たるとリーダーへの問い合わせが必要 ● データの整合性は取れる 強力な読み取り(Strong Read) リーダー レプリカ クライアント レプリカ

Slide 31

Slide 31 text

31 ● 古いレプリカから直接データを返せる ● リーダーへの問い合わせが無くなる分、パフォーマンスがUPする ● データの整合性は保証されないので、あくまで読み取り専用 ステイル読み取り(Stale Read) リーダー レプリカ クライアント レプリカ

Slide 32

Slide 32 text

32 ステイル読み取りにしてみた 無事、鎮火に成功!

Slide 33

Slide 33 text

33 まとめ

Slide 34

Slide 34 text

34 まとめ ● Spanner が夢のデータベースかどうかは設計・実装次第 ● 開発する上で意識すると良いポイントを3点ほど紹介した ○ トランザクションのリトライに気をつけるべし ○ インターリーブを心がけるべし ○ インターリーブ跨ぎにはステイル読み取りが効果的