\積極的に技術発信を行なっております/
▽ Twitter/COLOPL_Tech https://twitter.com/colopl_tech
▽ connpassページ http://colopl.connpass.com
▽ COLOPL Tech Blog http://blog.colopl.dev
Cloud Spannerと上手く付き合うコツ西川 蒼太郎
View Slide
氏名 :部署 :● 2020年度新卒入社(4年目)● 運用タイトルのサーバーエンジニアとしてバリバリ運用開発中● 最近「セメント」と呼ばれる減量飯を食べ続けて-10kgのダイエットに成功したらしい西川 蒼太郎2自己紹介技術基盤本部 第2バックエンドエンジニア部第1グループ 第1チーム
今日お話する内容3Cloud Spanner
今日お話する内容● Cloud Spanner とは?● 開発する上での Cloud Spanner との付き合い方○ トランザクションの扱い○ インターリーブ○ ステイル読み取り4
5Cloud Spannerとは?
● Google Cloud Platform にある DB サービス● コロプラでは2018年ごろから新作タイトルの「ユーザーデータ」の管理をMySQL から Spanner に移行6Cloud Spanner とは?
● 当然 SQL が使えるし、トランザクションもしっかりある● 自前シャーディングが一切不要● スケールイン・アウトが非常に容易7Cloud Spanner の特徴MySQL Spannerシャーディング 自前シャーディングが必要 内部で自動シャーディング!スケールイン・アウト オペレーションが大変 WebUIからワンクリックで完了!接続先管理どのシャードに接続するかアプリ側で管理が必要管理不要!コロプラにおける MySQL 運用との比較
● だいたいその通り● ただし Spanner の特性に寄り添った設計・実装は不可欠○ Spanner ≠ MySQL● 現場で意識すると良いポイントを3点ほど紹介○ トランザクションの扱い○ インターリーブ○ ステイル読み取り8Cloud Spanner は夢のデータベース?
9トランザクションの扱い
● 読み書きトランザクション(Read-Write)○ MySQL(InnoDB) の SERIALIZABLE とだいたい同じ感覚○ select したデータに問答無用で共有ロックをかける● 読み取り専用トランザクション(Read-Only)○ MySQL(InnoDB) の REPEATABLE READ とだいたい同じ感覚○ ロックを取らずに select できる○ ただし更新処理はできない● ゲームバックエンドの処理は大抵更新処理を伴う● =ほぼ Read-Write しか使わない● =複数のトランザクションでロックが衝突しやすい!10Spannerにおける2種類のトランザクション
● select したデータに問答無用で共有ロックをかける● commit するときに書き込む行の占有ロックを取得する● ロックが衝突した場合、優先度の高いトランザクションが勝つ● 負けたトランザクションは Abort される11Read-Write のロック解決法RW-Txn1RW-Txn2Read A Write ARead AtWrite AcommitcommitAbort !!
● Abort されたトランザクションはリトライされる○ リトライされる前提でコードを書く必要アリ○ (Spanner クライアントの実装による)● Spanner 以外のデータを更新する際は常に注意○ static 変数や Redis のキャッシュなど12トランザクションのリトライ// Transaction の中で...DB::transaction(function (){...// static 変数をインクリメントしたりstatic::$value++;// Redis に値を詰めたりRedis::set('key', $value);});NG例
● 基本的に static 変数は使わない方がよい● もし使いたい場合、ロールバックイベントに初期化処理を登録する13リトライを意識したコード例①static $value = null;// トランザクションがロールバックしたら値を初期化するapp()['events']->listen(TransactionRolledBack::class, function () {$value = null;});OK例
● キャッシュ更新, キューイングなどはcommit 後のコールバックとして登録する14リトライを意識したコード例②DB::transaction(function () {...// commit 後のコールバックに処理を登録するDB::afterCommit(function ($value){// Redis に値を詰めたりRedis::set('key', $value);// キューにデータを詰めたりJob::dispatch($value);// 重要なログを出力したり(調査用ログなど)Log::info("調査用ログ", ["value" => $value]);});});OK例
15インターリーブ
16Spannerの分散アーキテクチャクライアントNode Node Node NodeNode (Spanner Servers)4TBまで管理できるサーバーColossus(分散ストレージ)実データはここに格納される各 Node は複数の Split のオーナーSplit参考:Cloud Spanner のハイレベルアーキテクチャ解説
● Split を跨ぐクエリはパフォーマンス劣化につながる○ 低QPS→高QPSになるまで顕在化しないケースも○ できるだけ Split を跨がないクエリが肝要● そのための「インターリーブ」○ 特定のデータに親子関係を付与できる○ 親子データは物理的に同じ Split に配置される17Spannerの分散アーキテクチャ
Splitを跨ぐ● 軽い気持ちで Split を跨ぐ処理を書いてみる● (例)自分の所持アイテムを取得する18User(PK) UserId…UserItem(複合PK)- UserId- UserItemIdItemId…SELECT * FROM UserItem WHERE UserId = "自分のUserId";
インターリーブしないとデータごとに Split がバラバラ19Splitを跨ぐSplit Split Split SplitAさんUserBさんUserItemAさんUserItemBさんUserAさんUserItemCさんUser……
Splitを跨がない● インターリーブを適用してみる● 「User:UserItem=親:子」の関係にする20SELECT * 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
インターリーブすると物理的に同じ Split にデータが格納される!(※ただし1Splitの合計容量8GBを超えると別Splitになる)21Splitを跨がないSplit SplitAさんUserAさんUserItemAさんUserItemBさんUserBさんUserItemBさんUserItemCさんUser……CさんUserItem
● Spanner のテーブル設計においてインターリーブはマスト● コロプラでは User を親にした設計がスタンダード22インターリーブの例UserAさんUserChara UserItem …SplitUserBさんUserChara UserItem …UserCさんUserChara UserItem …Split ………
それでもインターリーブは跨ぎたい● とはいえインターリーブを跨ぎたくなるケースは出てくる● 特にソーシャルゲームでは「フレンド」を取得しがち○ フレンドのユーザー情報を閲覧する○ 自分宛のフレンド申請を取得する○ フレンドと対戦する○ etc...● そのための「ステイル読み取り」23
24ステイル読み取り
インターリーブを跨ぐ● 軽い気持ちでインターリーブを跨ぐ処理を書いてみる● (例)自分宛のフレンド申請を取得する25FriendRequest(PK) FriendRequestIdfromUserIdtoUserIdfrom_to_unique_index1: fromUserId2: toUserIdto_from_unique_index1: toUserId2: fromUserIdTABLE INDEX
インターリーブを跨ぐ● 軽い気持ちでインターリーブを跨ぐ処理を書いてみる● (例)自分宛のフレンド申請を取得する26// 1ユーザー辺り最大100件を想定SELECT * FROM FriendRequest WHERE toUserId = "自分のUserId";
2727インターリーブを跨ぐ● ある時、突然大量のエラーが・・・(実話)● 低QPS→高QPSになった途端、Spanner が詰まり始めた● ほとんどが Spanner の通信タイムアウト(DEADLINE_EXCEEDED)
● 「ステイル読み取り」を駆使して可能な限りパフォーマンスを上げる● 過去のタイムスタンプを使った読み取りになる○ 整合性:✕○ パフォーマンス:◯● ロックを取らない○ Read-Write トランザクション内でもノーロックで select が可能■ トランザクションから独立した読み取りになる○ ロック解放待ちの時間が減る!○ Abort(リトライ)の危険性も減る!28ステイル読み取りする
● Split には「リーダー」「レプリカ」の2種類が存在する○ リーダーは常に最新○ レプリカはちょっと古いことがある● 過去のタイムスタンプを指定すると「そのタイムスタンプより新しいデータを持つレプリカ」からデータを読み取ることができる29過去のタイムスタンプを使った読み取り?リーダー レプリカ レプリカちょっと古いことがある10秒に一度リーダーと同期する常に最新
30● Spanner のデフォルトはコレ● 古いレプリカに当たるとリーダーへの問い合わせが必要● データの整合性は取れる強力な読み取り(Strong Read)リーダー レプリカクライアントレプリカ
31● 古いレプリカから直接データを返せる● リーダーへの問い合わせが無くなる分、パフォーマンスがUPする● データの整合性は保証されないので、あくまで読み取り専用ステイル読み取り(Stale Read)リーダー レプリカクライアントレプリカ
32ステイル読み取りにしてみた無事、鎮火に成功!
33まとめ
34まとめ● Spanner が夢のデータベースかどうかは設計・実装次第● 開発する上で意識すると良いポイントを3点ほど紹介した○ トランザクションのリトライに気をつけるべし○ インターリーブを心がけるべし○ インターリーブ跨ぎにはステイル読み取りが効果的