Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cloud Spannerと上手く付き合うコツ

Cloud Spannerと上手く付き合うコツ

\積極的に技術発信を行なっております/

▽ Twitter/COLOPL_Tech
https://twitter.com/colopl_tech

▽ connpassページ
http://colopl.connpass.com

▽ COLOPL Tech Blog
http://blog.colopl.dev

COLOPL Inc.

June 30, 2023
Tweet

More Decks by COLOPL Inc.

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

  3. 今日お話する内容
    3
    Cloud Spanner

    View Slide

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

    View Slide

  5. 5
    Cloud Spanner
    とは?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. 15
    インターリーブ

    View Slide

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

    View Slide

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

    View Slide

  18. Splitを跨ぐ
    ● 軽い気持ちで Split を跨ぐ処理を書いてみる
    ● (例)自分の所持アイテムを取得する
    18
    User
    (PK) UserId

    UserItem
    (複合PK)
    - UserId
    - UserItemId
    ItemId

    SELECT * FROM UserItem WHERE UserId = "自分のUserId";

    View Slide

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


    View Slide

  20. 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

    View Slide

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


    Cさん
    UserItem

    View Slide

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


    View Slide

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

    View Slide

  24. 24
    ステイル読み取り

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. 33
    まとめ

    View Slide

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

    View Slide