1 of 30

Google Cloud Spanner

沼への誘い

Write

GCPUG Admin

Google Developers Expert

Mercari / Merpay Solution Team

@sinmetal

https://gcpug.jp

2 of 30

Spanner 沼への誘い Session List

  • Basic
  • Write
  • Read
  • Advanced

2

https://gcpug.jp

3 of 30

編集APIの種類

https://gcpug.jp

4 of 30

編集APIの種類

  • mutation API
    • PKを指定して編集を行う
    • 値を上書きするので、値を読まずに+1することはできない
  • DML
    • 条件を指定したDMLを利用して編集を行う
    • WHEREの範囲が広いとLockを必要以上に取ってしまうことがあるので注意

4

https://gcpug.jp

5 of 30

5

https://gcpug.jp

6 of 30

複数行の更新 (mutation API)

source code

複数行を更新しているが、

mutation applyするのは

最後に1回

6

https://gcpug.jp

7 of 30

複数行の更新 (DML)

source code

UPDATE文を都度実行するので、何度もSpannerにアクセスする

7

https://gcpug.jp

8 of 30

更新がTx内で反映されるか? (DML)

source code

UPDATE文はtx.Update()呼び出し時点で実行されるので、その後ReadするとUpdateした内容が反映されている。

8

https://gcpug.jp

9 of 30

更新がTx内で反映されるか? (mutation)

source code

mutation APIではInsert/Updateの反映は最後に行われるので、途中でReadしても、更新は反映されない

9

https://gcpug.jp

10 of 30

BatchUpdateを使うと複数のDMLを一度で実行

source code

UPDATE文を使いたいが、RPCをまとめたい場合はBatchUpdateを使えばまとめられる

ただ、mutation APIに比べるとRPCは多い

10

https://gcpug.jp

11 of 30

Abortしにくい設計

https://gcpug.jp

12 of 30

Aborted due to transient fault

  • 一時的な何かによってTransactionがAbortされた時に出てくるエラー
    • 自分のApplicationのTransactionの実行の仕方が悪い
    • Spanner側で何らかの理由によりAbortされた
  • Spanner側で何らかの理由によりAbortされた は稀にしか発生しないので、このエラーが頻発する場合、自分のApplicationに問題がある可能性が高い
  • Abortされた場合、Retryするので、Latencyが数十ms増加する

12

https://gcpug.jp

13 of 30

Transactionの範囲が広い

  • SELECT * FROM Tweet0 TABLESAMPLE RESERVOIR (1 ROWS);

13

Resultは1だが

Txの範囲は2322

https://gcpug.jp

14 of 30

存在しないRowを取得する

  • UUIDを生成し、SELECTして、存在していなければ、衝突していない

14

Not Foundを期待しているコードを書くとAbortされやすいので

INSERTでぶつけに行く方が良い

https://gcpug.jp

15 of 30

WriteSessionsの値が微妙

cli, err := spanner.NewClientWithConfig(ctx, database, spanner.ClientConfig{

SessionPoolConfig: spanner.SessionPoolConfig{

WriteSessions: 0.1, // Defaults to 0.2.

},

})

15

WriteSessionsはSession Poolの中でRWTxを発行しておく割合

Writeを恒常的に行う場合はPerformanceの向上が期待できるが、アクセスが少ない場合、Txが行われてからの時間が経ちすぎていて、TxがAbortされることがある

https://gcpug.jp

16 of 30

Abort頻度をより減らすために

  • 複数のSplitを含むTxより、1つのSplitで完結するTxの方がコストが安い
  • 同じTxで操作する複数のRowはInterleaveできるなら、した方がよい
    • e.g. Rowの変更履歴を保持する場合は親のRowにInterleaveする

16

https://gcpug.jp

17 of 30

冪等性

https://gcpug.jp

18 of 30

Tx func は自動的にRetryされるので、冪等に

_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {

var balance int64

row, err := txn.ReadRow(ctx, "Accounts", spanner.Key{"alice"}, []string{"balance"})

if err != nil { return err }

if err := row.Column(0, &balance); err != nil { return err }

if balance <= 10 {

return errors.New("insufficient funds in account")

}

balance -= 10

m := spanner.Update("Accounts", []string{"user", "balance"}, []interface{}{"alice", balance})

txn.BufferWrite([]*spanner.Mutation{m})

return nil

})

18

The transaction function will be called again if the error code of this error is Aborted.

The backend may automatically abort any read/write transaction if it detects a deadlock or other problems.

https://gcpug.jp

19 of 30

mutationの数を数える

https://gcpug.jp

20 of 30

Transaction Limit

  • Txに含めることができるmutationの数には上限がある
  • これは80000 rowではなく、Txに含まれる column * row の数で計算される。更にセカンダリインデックスがある場合、これも考慮される。
  • DBを更新する処理を作る時に、1行のmutationはいくつになるのか?最大何行同時に更新できるのか?を気にしておく

20

https://gcpug.jp

21 of 30

mutationの数え方

  • Insert
    • INSERTに含むColumnの数 + INSERTするTableに存在するIndexの数
  • Update
    • UPDATEに含むColumnの数 + UPDATEするColumnを含むIndexの数 * 2
  • Delete
    • 1 + DELETEするTableに存在するIndexの数

21

https://gcpug.jp

22 of 30

Insert

  • INSERTに含むColumnの数 + INSERTするTableに存在するIndexの数
  • Indexに関しては、Nullであっても、追加される

22

ID

Col1

Col2

A

Hello

INSERT INTO Measure (ID, Col1)

VALUES ('A', 'Hello')

ID

NULL-A

Data Table

Col2 Index Table

Col2がNULLの場合でも、NullのRowを作成する

Data Table (ID, Col1)

Col2 Index Table (ID)

mutationは 3

https://gcpug.jp

23 of 30

Update

  • UPDATEに含むColumnの数 + UPDATEするColumnを含むIndexの数 * 2
  • Index TableにはInsert, Deleteが走るので、2 mutation かかる

23

ID

Col1

Col2

A

Hello

hoge

UPDATE Measure

SET Col2 = "hoge"

WHERE ID = "A"

ID

NULL-A

hoge-A

Data Table

Col2 Index Table

Keyになっている部分を更新はできないので

既存の削除と新規追加をする

DELETE NULL-A

INSERT hoge-A

Data Table (ID, Col2)

Col2 Index Table (ID) * 2

mutationは 4

https://gcpug.jp

24 of 30

Delete

  • 1 + DELETEするTableに存在するIndexの数
  • Columnとかは関係なくなり、シンプル

24

ID

Col1

Col2

A

Hello

hoge

DELETE Measure

WHERE ID = "A"

ID

hoge-A

Data Table

Col2 Index Table

Data Table (Row)

Col2 Index Table (Row)

mutationは 2

https://gcpug.jp

25 of 30

UPDATE, DELETE は慎重に

  • UPDATE, DELETEのWHEREにPK以外を設定した場合、Txの範囲が難しくなる
  • 実行しないと行数も分からないので、mutationの数も分からない
  • Read Only TxでSELECTしてPKを確定させてから、Read Write Txを発行して、更新するのが無難

25

https://gcpug.jp

26 of 30

  • 1行のmutationの数から、同時にTxに参加できるRowの数を計算する

26

resp, err := client.ReadWriteTransactionWithOptions(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {

stmt := spanner.Statement{

SQL: `INSERT Singers (SingerId, FirstName, LastName)

VALUES (110, 'Virginia', 'Watson')`,

rowCount, err := txn.Update(ctx, stmt)

if err != nil {

return err

}

fmt.Fprintf(w, "%d record(s) inserted.\n", rowCount)

return nil

}, spanner.TransactionOptions{CommitOptions: spanner.CommitOptions{ReturnCommitStats: true}})

if err != nil {

return fmt.Errorf("commitStats.ReadWriteTransactionWithOptions: %v", err)

}

fmt.Fprintf(w, "%d mutations in transaction\n", resp.CommitStats.MutationCount)

https://gcpug.jp

27 of 30

Data Migration

https://gcpug.jp

28 of 30

  • Spannerが裏で分割しているPartitionごとにDMLを発行することで、40000mutationを超えるRowにDMLを実行する仕組み
  • Atomicに操作されるのはPartitionごとなので、一度のPartitioned DML実行がAtomicに行われるわけではない。そのため、途中で失敗した場合は中途半端に適用されている状態となる。
  • そのため、DMLには冪等性が必要

28

https://gcpug.jp

29 of 30

Partitioned DML VS Batch App

  • Default ValueをセットするようなシンプルなDMLはPartitioned DMLが向いてる
  • 複数のTableが必要になるものや外部APIが必要になるもの、SQLだけでは表現できないものはBatch Appを組むことになる

29

Partitioned DML

Batch App

DMLさえ書けば実行できる

コードを書き、それを動かすインフラも必要

スケーラビリティはSpanner任せ

スケーラビリティを自分でコントロールする

JOINのような他のTableへのアクセスはできない

複雑なロジックも組み込める

クエリは冪等である必要がある

price * 1.3 のようなことも実行できる

https://gcpug.jp

30 of 30

Resources

30

https://gcpug.jp