1 of 110

Google Cloud Spanner Deep Dive

GCPUG Admin

Google Developers Expert

Mercari / Merpay Solution Team

@sinmetal

深海まではたどり着けなかった・・・

https://gcpug.jp

2 of 110

What’s Google Cloud Spanner

  • Googleが開発した分散型データベース
    • 論文は公開されているが、OSSではない
  • KVSとRDBが合体したような特性を持っている
    • 開発当初はKVSだった
    • 従来のRDBと互換性があるわけではないので、New SQLとか呼ばれていることもある
  • 最初のSpannerの論文が2012年に公開されているので、Googleはそれ以前からSpannerを利用している
  • Google Cloud Spannerとして登場したのは2017年

2

https://gcpug.jp

3 of 110

Spannerの特徴

  • 水平方向にスケールするのが得意
    • マシンタイプなどはなく、Nodeの数を指定するのみ
    • Nodeの数は好きなタイミングで変更できる
  • ストレージに限界はない
    • 利用量に対する課金
    • 1Node 2TBというパフォーマンスに対するガイドラインはある
  • Strong Consistency
    • 基本的にすべてStrong Consistency
    • マルチリージョンの場合は、自分でEventual Consistencyっぽいクエリを投げることもできる

3

https://gcpug.jp

4 of 110

GCP Databaseの系譜 (sinmetalの主観)

4

Firebase Realtime DB, Datastore, Spannerのノウハウを融合し、よりGCPらしいDBへ

Bigtable

Datastore

Spanner

Realtime DB

Firestore

Datastoreよりパフォーマンスを改善し、大量のトラフィックを高速に捌けるようになった。

RDBのような柔軟なQueryにも対応

Bigtableをアプリケーションで使いやすいようにQueryやTxを追加し、Bigtableのインフラを共有することでコストを抑えた

https://gcpug.jp

5 of 110

Spanner Component

https://gcpug.jp

6 of 110

Spanner Component

6

Instance

  • Region
  • Node数

DB

  • IAM

Table

Index

https://gcpug.jp

7 of 110

Nodeの数

  • すべてがRead/WRITE レプリカ
    • Read Only レプリカは存在しない
  • DownTime無しで、増減可能
  • Production環境は3 Node以上が推奨
  • Session数
    • DBごとにNode数 * 10,000
  • QPS
    • Read : Max 10,000 QPS
    • WRITE : Max 2,000 QPS (Row Data 2KB)

7

https://gcpug.jp

8 of 110

Region or Multi-region

  • Region
    • Availability : 99.99%
    • asia-northeast1
      • Price : $1.17 / 1node h
  • Multi-region
    • Availability : 99.999%
    • nam3 (us-east4, us-east1)
      • Price : $3.0 / 1node h
    • nam-eur-asia1 (us-central1, us-central2, europe-west1, asia-east1)
      • Price : $9.0 / 1node h

8

europe-west1, asia-east1は

Read Onlyにしかできない

https://gcpug.jp

9 of 110

Spanner Architecture

https://gcpug.jp

10 of 110

Spanner Architecture

10

Node

Split

Colossus

Split

Split

Split

Split

Split

Split

Node

Root Server

Insert, Update, Delete, Queryを処理するレイヤー

それぞれSpan Serverを管理する

1台のサーバというわけではなく、複数Zoneに存在している

Table, Indexが保存されているレイヤー

数は動的に変更される

https://gcpug.jp

11 of 110

Splitの決定方法

  • SpannerはPKの辞書順にデータを保存する
    • PKの先頭付近の文字が重要

11

Split

Split

Split

Split

a ~ e

f ~ j

k ~ o

p ~ z

今、Splitが管理しているKeyの範囲を管理しておけば、指定されたKeyのRowを持っている可能性があるSplitが分かり、そのSplitを担当しているNodeが分かる

Node

Node

Root Server

https://gcpug.jp

12 of 110

Splitの分割

  • 1つのSplitが大きくなりすぎたり、負荷が高くなりすぎると、Splitを分割する

12

Split

Split

Split

Split

a

f ~ j

k ~ o

p ~ z

Node

Node

Root Server

Split

b ~ e

a のPKのデータばっかり増えたので、a ~ eを分割

https://gcpug.jp

13 of 110

Splitの再配置

  • Nodeの増減時はSplitの割当が再配置される
  • 1 ~ 2 secぐらい

13

Split

Split

Split

Split

a

f ~ j

k ~ o

p ~ z

Node

Node

Root Server

Split

b ~ e

Node

https://gcpug.jp

14 of 110

Splitのhotspot問題

  • PKに偏りがあると、Splitにhotspotができてしまう

14

Split

Split

Split

Split

aa ~ af

f ~ j

k ~ o

p ~ z

Node

Node

Root Server

Split

b ~ e

Split

ag ~ az

hot

spot

hot

spot

hot

spot

hotspotが発生しないようにSpannerはSplitの分割や、Splitの担当のNodeを動的に変更していくが、どうにもならん時もある

ひたすらaのPKのデータが増えていく

https://gcpug.jp

15 of 110

Primary Key

https://gcpug.jp

16 of 110

PKの戦略

  • PKはデータの性質に関係なく、分散させておく
  • 公式のベストプラクティスはUUID
  • バッドパターン
    • シーケンシャルナンバー
    • タイムスタンプ
    • 大分類 + 中分類 + 小分類 + ID
      • いずれかの分類にhotspotがあると困る

16

https://gcpug.jp

17 of 110

PKの戦略

17

10,20,30,40,50

INSERT 10, 20, 30, 40, 50

10,20

30,40,50

INSERT 60,70,80,90

30,40,50, 60, 70, 80 ,90

10,20

30,40,50

60,70,80,90

10,20

分割

分割

後ろのSplitにINSERTが偏る

https://gcpug.jp

18 of 110

PKの戦略

18

30,40,50

60,70,80,90

10,20

INSERT 15,23,35,67

30,35,40,50

60,67,70,80,90

10,15,20,23

データが増えていく時に別々のSplitに書き込まれるようにしていく

データがない状態だと、分散していない状態からスタートするので、じわじわデータを入れていく

Cloud Datastoreのベストプラクティス "500/50/5" rule が参考になる

ローンチした瞬間に凄まじいトラフィックが予想される場合は、ダミーデータを入れておくなども有効

NOTE : “500/50/5” rule

新しいKindに対するオペレーションは毎秒 500 回を上限とし、その後でトラフィックを50%ずつ、5 分ごとに増やしていく

理論上は、この増加スケジュールを使用すると 90 分後に毎秒 740,000 回までオペレーションを増やすことをできる

https://gcpug.jp

19 of 110

PKの戦略

  • PKにしたい値から、順序がランダムになる値を生成する
    • ハッシュ
      • Hash(1) -> Xjead = Xjead@1
    • 逆順
      • ID + 小分類 + 中分類 + 大分類
    • Shard
      • crc32c(2018/06/10 18:36:37 …) % 10 = Modulo @2018/06/10 18:36:37 …

19

https://gcpug.jp

20 of 110

複合Keyの戦略

  • 複数カラムをPKに設定
  • ハッシュ
  • UUID + UNIQUE INDEX

20

https://gcpug.jp

21 of 110

複数カラムをPKに設定

  • 先頭のカラムの値が分散している必要がある
  • 途中でIDに含めるカラムを増やせない

21

CREATE TABLE TweetCompositeKey (

Id STRING(MAX) NOT NULL,

Author STRING(MAX) NOT NULL,

) PRIMARY KEY (Id,Author)

https://gcpug.jp

22 of 110

ハッシュ

  • Hash(カラム1 + カラム2 ...)
    • クライアントはHashの値を知らなくても、カラム1, カラム2の値でクエリできる

22

CREATE TABLE TweetHashKey (

Key STRING(MAX) NOT NULL,

Id STRING(MAX) NOT NULL,

Author STRING(MAX) NOT NULL,

) PRIMARY KEY (Key)

func NewKey(id string, author string) string {

return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%s-_-%s", id, author))))

}

https://gcpug.jp

23 of 110

UUID + UNIQUE INDEX

  • UNIQUE INDEXの先頭のカラムの値が分散している必要がある
  • UUIDでクエリしないと遅い

23

CREATE TABLE TweetUniqueIndex (

Id STRING(MAX) NOT NULL,

TweetId STRING(MAX) NOT NULL,

Author STRING(MAX) NOT NULL,

) PRIMARY KEY (Id)

CREATE UNIQUE INDEX natural_key

ON TweetUniqueIndex (

TweetId,

Author

)

https://gcpug.jp

24 of 110

INDEXの仕組み

  • INDEXも結局テーブル
    • INDEXとして指定したカラムをPKとしたテーブルを作成する
      • PKにすれば勝手に辞書順に並ぶ
    • INDEXを設定するカラムもなるべく分散させる
      • 分散しないカラムにはShardなどを併用する

24

https://gcpug.jp

25 of 110

単調増加する値のINDEX

  • TIMESTAMPを保存する場合、必ず既存より大きな値になる。
    • このような性質を持つ値にINDEXを設定する場合はhotspotにならないように注意する必要がある

25

https://gcpug.jp

26 of 110

Shard利用時のクエリ

CREATE INDEX Order1MShardCreatedAtDesc

ON Order1M (ShardCreatedAt, CreatedAt DESC)

SELECT *

FROM Order1M@{FORCE_INDEX=Order1MShardCreatedAtDesc}

WHERE ShardCreatedAt BETWEEN 0 AND 9

ORDER BY CreatedAt DESC

LIMIT 10

26

https://gcpug.jp

27 of 110

Shard利用時のクエリ

27

Split

Split

Split

0 ~ 1

6 ~ 7

8 ~ 9

Node

Node

Root Server

Split

4 ~ 5

Split

2 ~ 3

クエリ時は ShardId BETWEEN 0 AND 9を指定することで、全Splitに対してクエリを実行する

INSERT時はShardIdによってSplitに分散される

https://gcpug.jp

28 of 110

PKを分散させればhotspotは起きないのか?

  • PKにUUIDを使っていてもhotspotが発生する可能性はある
    • データが少なくて、Splitが少ない
    • 同じPKへのアクセス頻度が多い

28

https://gcpug.jp

29 of 110

Spannerの気持ちになって、都度考える

  • WRITEの頻度や件数は?
    • エンドユーザのリクエストでWRITEする?
    • バッチ処理でWRITEする?
  • どうやってREADする?
    • PKを指定して取得?
    • 最新の10件が欲しい?
    • プロパティにフィルターをかけたい?
  • WRITEとREADどっちが速い方がよい?
    • WRITEはバッチで、READはエンドユーザなら、READが速い方がよい?

29

https://gcpug.jp

30 of 110

Shard利用時のクエリ

CREATE INDEX Order1MShardCreatedAtDesc

ON Order1M (ShardCreatedAt, CreatedAt DESC)

SELECT *

FROM Order1M@{FORCE_INDEX=Order1MShardCreatedAtDesc}

WHERE ShardCreatedAt BETWEEN 0 AND 9

ORDER BY CreatedAt DESC

LIMIT 10

30

実はこのクエリ、フルスキャンするので、別に早くない。

ShardはREADを早くするものではなく、WRITEを早くするもの

https://gcpug.jp

31 of 110

Shard利用時のクエリ

31

フルスキャン!!!!!!

https://gcpug.jp

32 of 110

こうすると早くなる

SELECT c.*

FROM UNNEST(GENERATE_ARRAY(0, 9)) AS OneShardCreatedAt,

UNNEST(ARRAY(

SELECT AS STRUCT *

FROM Order1M@{FORCE_INDEX=Order1MShardCreatedAtDesc}

WHERE ShardCreatedAt = OneShardCreatedAt

ORDER BY CreatedAt DESC LIMIT 10

)) AS c

ORDER BY c.CreatedAt DESC

LIMIT 10

32

各ShardのORDER BY CreatedAt DESC LIMIT 10を取得した後、選別

https://gcpug.jp

33 of 110

各Shardの10件を取得

33

https://gcpug.jp

34 of 110

Query

https://gcpug.jp

35 of 110

FORCE_INDEX

  • INDEXを利用して欲しいクエリを実行する場合、必ずFORCE_INDEXを指定する

35

https://gcpug.jp

36 of 110

FORCE_INDEX

# INDEX

CREATE INDEX OrderDetail1GItemIdAsc ON OrderDetail1G ( ItemId )

# QUERY

SELECT * FROM OrderDetail1G WHERE ItemId = "1" LIMIT 100

36

https://gcpug.jp

37 of 110

FORCE_INDEX

フルスキャン・・・!

37

https://gcpug.jp

38 of 110

FORCE_INDEX

# INDEX

CREATE INDEX OrderDetail1MItemIdAsc ON OrderDetail1M ( ItemId )

# QUERY

SELECT * FROM OrderDetail1M@{FORCE_INDEX=OrderDetail1MItemIdAsc}

WHERE ItemId = "1" LIMIT 100

38

https://gcpug.jp

39 of 110

FORCE_INDEX

39

https://gcpug.jp

40 of 110

Pagination

https://gcpug.jp

41 of 110

SpannerでPaginationは難しい

  • Cursorは無い
  • OFFSETだと、取得した後、OFFSET分捨ててる

41

https://gcpug.jp

42 of 110

Paginationを頑張る

  • 最初のクエリの最後の値をWHEREに入れてクエリを投げる

SELECT * FROM Order1M@{FORCE_INDEX=Order1MCreatedAtDesc} ORDER BY CreatedAt DESC, OrderId LIMIT 10

Last Row

  • OrderId = “b28e0a2d-f131-497b-8eb3-71d5cdeddcf0”
  • CreatedAt = “2018-08-26T16:15:09.436784119Z”

42

https://gcpug.jp

43 of 110

Paginationを頑張る

SELECT *

FROM Order1M@{FORCE_INDEX=Order1MCreatedAtDesc}

WHERE

(CreatedAt < "2018-08-26T16:15:09.436784119Z")

OR (CreatedAt = "2018-08-26T16:15:09.436784119Z" AND OrderId > "b28e0a2d-f131-497b-8eb3-71d5cdeddcf0")

ORDER BY CreatedAt DESC, OrderId LIMIT 10

43

UNIQUEになるようにPKを指定する。

INDEXにも末尾にOrderIdを入れる。

https://gcpug.jp

44 of 110

JOIN HINT

https://gcpug.jp

45 of 110

  • APPLY_JOIN
    • 値をぶつけあわせて探す。INDEXがある場合は、基本こいつが強い
  • HASH_JOIN
    • Hash Tableを作成して探す
    • データ量が多いとHash Tableを作るのに時間がかかる
  • LOOP_JOIN
    • 全探索して探す

45

https://gcpug.jp

46 of 110

JOIN HINT

SELECT

I.ItemSubId,

Count(1) as Count

FROM (SELECT SUBSTR(ItemId, 0, 2) as ItemSubId FROM Item1K) I

JOIN@{JOIN_TYPE=HASH_JOIN}

(SELECT SUBSTR(ItemId, 0, 2) as ItemSubId FROM OrderDetail1M) OD

ON I.ItemSubId = OD.ItemSubId

GROUP BY 1

ORDER BY Count DESC

LIMIT 10

46

未指定の場合は、大抵APPLY_JOIN使ってる気がする

https://gcpug.jp

47 of 110

APPLY JOIN

SELECT OD.OrderId, OD.OrderDetailID, OD.ItemId, I.Name, I.CategoryId, I.Price

FROM OrderDetail1M OD

JOIN Item1K I ON OD.ItemId = I.ItemId

WHERE OD.ItemId = "145f7a9a-58df-40c1-96e0-2184395a922d"

LIMIT 10

47

https://gcpug.jp

48 of 110

MAPを作ってぶつける

48

https://gcpug.jp

49 of 110

HASH JOIN

SELECT

I.ItemSubId,

Count(1) as Count

FROM (SELECT SUBSTR(ItemId, 0, 2) as ItemSubId FROM Item1K) I

JOIN@{JOIN_TYPE=HASH_JOIN}

(SELECT SUBSTR(ItemId, 0, 2) as ItemSubId FROM OrderDetail1M) OD

ON I.ItemSubId = OD.ItemSubId

GROUP BY 1

ORDER BY Count DESC

LIMIT 10

49

SUBSTRで取得した値で、JOINしてるので、INDEXが効かない

https://gcpug.jp

50 of 110

Hashを作ってぶつける

50

https://gcpug.jp

51 of 110

APPLY JOINでやると・・・?

SELECT

I.ItemSubId,

Count(1) as Count

FROM (SELECT SUBSTR(ItemId, 0, 2) as ItemSubId FROM Item1K) I

JOIN

(SELECT SUBSTR(ItemId, 0, 2) as ItemSubId FROM OrderDetail1M) OD

ON I.ItemSubId = OD.ItemSubId

GROUP BY 1

ORDER BY Count DESC

LIMIT 10

51

https://gcpug.jp

52 of 110

フルスキャン・・・!

52

https://gcpug.jp

53 of 110

LOOP JOIN

ループでくるくる回すJOINのはずだが、Join HintでLOOP_JOIN指定しても、特に何も起こらない悲しみを背負ったしんめたる

for _, r1 := range t1 {

for _, r2 := range t2 {

}

}

53

LOOP_JOINはARRAY_JOINと同じになったらしい... !

https://gcpug.jp

54 of 110

INTERLIVE

https://gcpug.jp

55 of 110

INTERLEAVE

  • 親子関係があるTableを同じ場所に保存しておく機能
  • ほとんどのケースでJOINして一緒に利用するTableを作る場合に使う

55

Order(1)

name:sinmetal

price:1100

OrderDetails(1,1)

itemName:ビスケット

price:100

count:10

OrderDetails(1,2)

itemName:ガム

price:10

count:10

Order(2)

name:gcpug

price:500

OrderDetails(2,1)

itemName:ガム

price:10

count:50

https://gcpug.jp

56 of 110

INTERLEAVE

CREATE TABLE Order (

OrderId STRING(MAX) NOT NULL,

) PRIMARY KEY (OrderId)`

CREATE TABLE OrderDetail (

OrderId STRING(MAX) NOT NULL,

OrderDetailId STRING(MAX) NOT NULL,

) PRIMARY KEY (OrderId, OrderDetailId),

INTERLEAVE IN PARENT Order ON DELETE CASCADE`

56

PRIMARY KEYの先頭に親のKeyを入れる

https://gcpug.jp

57 of 110

Sub Queryによる順序指定

https://gcpug.jp

58 of 110

Priceが多い100件に、UserをJoin

SELECT OrderId, O.UserId, U.Name, O.Price

FROM Order1M@{FORCE_INDEX=Order1MPriceDesc} O

JOIN User1K U ON O.UserId = U.UserId

WHERE Price > 10000

ORDER BY Price DESC

LIMIT 100

58

https://gcpug.jp

59 of 110

JOINしてから100件に絞ってる

59

https://gcpug.jp

60 of 110

FilterしてからJOIN

SELECT OrderId, O.UserId, U.Name, O.Price

FROM

(SELECT OrderId, UserId, Price

FROM Order1M@{FORCE_INDEX=Order1MPriceDesc}

WHERE Price > 10000

ORDER BY Price DESC

Limit 100) O

JOIN User1K U ON O.UserId = U.UserId

ORDER BY Price DESC

60

https://gcpug.jp

61 of 110

100件取得した後、JOINしている

61

https://gcpug.jp

62 of 110

順序を指定したい気持ちになるやつの例

  • JOIN, Filter, Sort
  • Filterの重みが異なるもの
    • 正規表現
    • INDEXがあるものへのフィルター

62

クエリの実行方法はBigQueryに似てる

https://gcpug.jp

63 of 110

Queryの実行プラン

  • サブプランのリモート実行を開始します(必要な場合)
  • リモート実行からの結果を待機します
  • 結果の集計など、残っているローカル実行ステップを処理します
  • クエリの結果を返します

63

https://gcpug.jp

64 of 110

Timestamp Bounds

https://gcpug.jp

65 of 110

  • PerformanceやBackupのために、TimestampやX秒以内みたいな指定して、データを取得する機能

65

https://gcpug.jp

66 of 110

複数カラムのINDEXの活用

https://gcpug.jp

67 of 110

複数カラムのINDEXの活用

  • 複数のカラムを含むINDEXを作る場合、カラムの順番が重要
    • Queryによって最適なINDEXが変わる

67

https://gcpug.jp

68 of 110

カラムの順番の重要性

  • Status = 1 AND Price > 100
  • この条件の時はどちらのINDEXがよいか?
    • Status ASC, Price ASC
    • Price ASC, Status ASC

68

https://gcpug.jp

69 of 110

カラムの順番の重要性

  • Status = 1 AND Price > 100
  • この条件の時はどちらのINDEXがよいか?
    • Status ASC, Price ASC
    • Price ASC, Status ASC

69

EQUALのカラムを前に持ってくる

https://gcpug.jp

70 of 110

なんで?

  • INDEXはソートされた状態で保存される

70

Status

Price

1

100

1

150

1

200

2

70

2

100

2

170

3

10

Price

Status

10

3

70

2

100

1

100

2

150

1

170

2

200

1

Statusを前に持ってくれば、Status = 1 が固まってくれるので、そこを見ればよいだけになる

Status = 1 AND Price > 100 はここに並んでる

Priceが前にあると、該当データがちらばる

https://gcpug.jp

71 of 110

複数カラムへのinequality filter

  • Price > 100 AND CreatedAt > 2018-01-17
  • この条件の時はどちらのINDEXがよいか?
    • Price ASC, CreatedAt ASC
    • CreatedAt ACS, Price ASC

71

https://gcpug.jp

72 of 110

複数カラムへのinequality filter

  • Price > 100 AND CreatedAt > 2018-01-17
  • この条件の時はどちらのINDEXがよいか?
    • Price ASC, CreatedAt ASC
    • CreatedAt ACS, Price ASC

72

データがどのように入っているか次第で、どっちがいいかが変わる!

https://gcpug.jp

73 of 110

なんで?

  • INDEXはソートされた状態で保存される

73

Price

CreatedAt

70

2018-01-20

100

2018-01-01

100

2018-01-18

150

2018-01-10

150

2018-01-18

200

2018-01-01

300

2018-01-25

CreatedAt

Price

2018-01-01

100

2018-01-01

150

2018-01-10

150

2018-01-18

100

2018-01-18

150

2018-01-20

70

2018-01-25

300

どちらを前にしても、対象のRowは色んな所に散らばる。

なるべく固まってる方を前にしてあげれば、早くなる。

https://gcpug.jp

74 of 110

このケースだと、sinmetalはどう考えるか?

74

Price

CreatedAt

70

2018-01-20

100

2018-01-01

100

2018-01-18

150

2018-01-10

150

2018-01-18

200

2018-01-01

300

2018-01-25

CreatedAt

Price

2018-01-01

100

2018-01-01

150

2018-01-10

150

2018-01-18

100

2018-01-18

150

2018-01-20

70

2018-01-25

300

Price, CreatedAtの場合、データが増えていくと、Price > 100 に該当するCreatedAtに過去のデータが延々と積まれていって、つらくなっていく。

なので、直近1ヶ月分みたいなCreatedAtの指定が基本的にあるのであれば、CreatedAtが前の方が無難かなぁ?みたいな気持ち

https://gcpug.jp

75 of 110

ORDER BYは重要な要素

  • WHERE Price > 100 AND CreatedAt > 2018-01-17
  • ORDER BY CreatedAt ASC
  • この条件の時はどちらのINDEXがよいか?
    • Price ASC, CreatedAt ASC
    • CreatedAt ACS, Price ASC

75

https://gcpug.jp

76 of 110

ORDER BY CreatedAt ASC

76

Price

CreatedAt

70

2018-01-20

100

2018-01-01

100

2018-01-18

150

2018-01-10

150

2018-01-18

200

2018-01-01

300

2018-01-25

CreatedAt

Price

2018-01-01

100

2018-01-01

150

2018-01-10

150

2018-01-18

100

2018-01-18

150

2018-01-20

70

2018-01-25

300

ORDER BYがある場合、高い確率でORDER BYで指定したカラムを一番前に持ってくることが多い。

Priceを前に持ってきてしまうとCreatedAtでの並び替えのために、Price > 100のデータをすべて読み込む必要が出てくる

https://gcpug.jp

77 of 110

NOT EQUAL より EQUAL

  • NOT EQUALはデータを検索する範囲が広くなる
    • Status NOT EQUAL 1
      • INDEXで判定する場合、 Status > 1 OR Status < 1
  • NOT IN (1, 3, 5)
    • (Status > 1 OR Status < 1) AND (Status > 3 OR Status < 3) AND (Status > 5 OR Status < 5) になるので、つらい...
    • Statusの範囲が少ない数なのであれば、存在する方を指定する
      • Statusが1 ~ 7の値なのであれば、IN (2, 4, 6, 7)
      • Status = 2, Status = 4… をそれぞれ並列に実行してMergeすればよい

77

https://gcpug.jp

78 of 110

1つのQueryで完結させるかどうか?

  • SpannerのINDEXはソートされているだけのシンプルなものなので、ものすごい飛び地になっているデータを取得する場合は、複数のQueryに分けるのも選択肢に入る

78

https://gcpug.jp

79 of 110

Transaction

https://gcpug.jp

80 of 110

Abortしにくい設計

https://gcpug.jp

81 of 110

Aborted due to transient fault

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

81

https://gcpug.jp

82 of 110

Abortされやすい状況

  • Transactionの範囲が広い
  • 存在しない行をReadする

82

https://gcpug.jp

83 of 110

Transactionの範囲が広い

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

83

Resultは1だが

Txの範囲は2322

https://gcpug.jp

84 of 110

存在しないRowを取得する

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

84

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

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

https://gcpug.jp

85 of 110

mutationの数を数える

https://gcpug.jp

86 of 110

Transaction Limit

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

86

https://gcpug.jp

87 of 110

mutationの数え方

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

87

https://gcpug.jp

88 of 110

Insert

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

88

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

89 of 110

Update

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

89

ID

Col1

Col2

A

Hello

a

UPDATE Measure

SET Col2 = "a"

WHERE ID = "A"

ID

NULL-A

a-A

Data Table

Col2 Index Table

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

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

DELETE NULL-A

INSERT a-A

Data Table (ID, Col2)

Col2 Index Table (ID) * 2

mutationは 4

https://gcpug.jp

90 of 110

Delete

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

90

ID

Col1

Col2

A

Hello

a

DELETE Measure

WHERE ID = "A"

ID

a-A

Data Table

Col2 Index Table

Data Table (Row)

Col2 Index Table (Row)

mutationは 2

https://gcpug.jp

91 of 110

mutationの数え方

91

https://gcpug.jp

92 of 110

UPDATE, DELETE は難しい

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

92

https://gcpug.jp

93 of 110

Timestamp Boundsの使い所

  • マルチリージョン
    • 必ずしも最新の結果が必要ない場合、X秒以内のデータというような指定をして、結果を取得することで、Performanceを上げることができる
  • Backup
    • 特定のTimestamp時点のデータを取得することで、一貫性のあるBackupを取得することができる

93

https://gcpug.jp

94 of 110

Unit Test

https://gcpug.jp

95 of 110

Unit Test

  • https://github.com/gcpug/handy-spanner という3rd partyのEmulatorが作成されている
    • すべての機能が実装されているわけではないが、機能追加をする時に、Spannerの制限にがっつり引っかかることは少ないので、ユースケースを満たしてくれる

95

https://gcpug.jp

96 of 110

Unit Test

  • handy-spannerはSpannerとは違う部分もあるので、いずれかのタイミングで自動テストを本物のSpannerで走らせるようにした方が安心
    • master branchにmergeされた時
    • 1日に1回
  • DBを作った時に時間かかったりすることがあるので、DBを先に作っておいて、空いてるDBを割り当てたりすると便利
  • DB作ったり消したりを繰り返すと、裏に大量にSplitができて、新しくDBを作れなくなったりするので、Nodeごと作り直しすのも有効

96

https://gcpug.jp

97 of 110

Monitoring

https://gcpug.jp

98 of 110

Metrics

  • Spanner Metrics List
    • CPU Utilization
    • Latency
    • Operations per second
    • Throughput (MB/sec)
    • Total storage

98

DBごとの粒度でチェックできる

https://gcpug.jp

99 of 110

Alerts for high CPU utilization

  • CPU利用率はいくつか区分がある
    • Priority
      • high : アプリからのRequestに起因して上がることが多い
      • low : Spannerが裏で何かやる時とか
    • IsSystem
      • true : Spannerが実行しているもの
      • false : アプリからのRequest

99

single-region

multi-region

High priority total

65%

45%

24-hour smoothed aggregate

90%

90%

https://gcpug.jp

100 of 110

ex : low priority

  • Spannerは暇な時にデータをコンパクションを行っている
  • 土日とかで使ってないのにCPU利用率が上がってる時とかは、だいたい裏でコンパクションしている
  • これにより、データサイズがちょいちょい削減される

100

https://gcpug.jp

101 of 110

Resources

101

https://gcpug.jp

102 of 110

Resources

102

https://gcpug.jp

103 of 110

Resources

103

https://gcpug.jp

104 of 110

Additional

https://gcpug.jp

105 of 110

Googleの思想

  • 小さなものを水平に並べていく
    • Borg
    • Colossus
    • Dremel

105

https://gcpug.jp

106 of 110

Colossus

  • Googleの分散ファイルシステム
    • Google File Systemの後継
  • ファイルへのアクセスで最も遅いのはDisk
  • 100MB/secの場合、1GBのファイルを読み込むのに10sec
  • 1GBのファイルを100MBごとに分割して複数のDiskに保存して、同時に読み込めば、1sec !

106

https://gcpug.jp

107 of 110

Dremel

  • BigQueryの中で動いている分散クエリシステム
    • 1TBのテーブルをスキャンする時にボトルネックになるのはDisk
    • テーブルを100MBごとぐらいに区切って別々のDiskに保存しておく
    • 保存されているDiskすべてにコンテナを割り当て並列に処理する

107

https://gcpug.jp

108 of 110

Dremel v1 Architecture

108

Leaf

Leaf

Mixer1

Colossus

Jupiter

Leaf

Leaf

Mixer1

Mixer0

http://gcpug.jp

109 of 110

Dremel

SELECT

state,

COUNT(1) AS count

FROM

`bigquery-public-data.samples.natality`

WHERE

year = 1987

GROUP BY

state

ORDER BY

count DESC

109

https://gcpug.jp

110 of 110

Dremel v1 Query Processing

110

Leaf

Leaf

Mixer1

Colossus

Jupiter

Leaf

Leaf

Mixer1

Mixer0

Leafはそれぞれ取得したデータに対して以下を実行してMixerに渡す

year = 1987

COUNT

GROUP BY state

MixerはLeafから取得したデータに対して以下を実行して、上のMixerにわたす

COUNT

GROUP BY state

http://gcpug.jp