Transaction Puzzlers
appengine ja night #4
あらかわ (@ashigeru)
講演者について

名前
 あらかわ

(@ashigeru)
所属
 株式会社グルージェント

開発部
普段の業務
 研究開発
(コンパイラ系)
 教育 (Computer Aided Education)
 ブログ書き (Song of Cloud Blog)
2010/01/22
appengine ja night #4 - @ashigeru
2
Song of Cloud Blog

会社ブログ



http://songofcloud.gluegent.com
App Engineのポータルサイトをコンセプトに
By arakawa (App Engine関連)








Slim3 Datastoreに乗り換える
テキスト部分一致検索
送金のトランザクション処理パターン
分散トランザクション処理の最適化
SDK 1.2.8 Release Notesで語られなかったこと
App Engine SDK 1.3.0 (overview)
App Engine JDP Tips
グローバルトランザクション処理のパターン
2010/01/22
appengine ja night #4 - @ashigeru
3
今日の内容
トランザクション処理の考え方
 トランザクション処理のパターン

2010/01/22
appengine ja night #4 - @ashigeru
4
トランザクション処理の考え方

リソースを一時的に独占できる技術
 同時に変更して不整合が起こる、などを回避

今回は悲観的/楽観的をあまり気にしない
 App
Engineは楽観的並行性制御
 いずれも一時的にリソースを独占できる
 設計/実装時には考慮する必要がある
2010/01/22
appengine ja night #4 - @ashigeru
5
App Engineのトランザクション

トランザクションはEntity Group (EG)単位
 同一EG内のエンティティに対する操作はACID
 複数EGにまたがる操作は対応していない
2010/01/22
appengine ja night #4 - @ashigeru
6
Entity Groupの構成

同じルートキーを持つエンティティ群
 データストア上で近くに配置される

例
 Foo(A)
 Foo(A)/Hoge(B)
EG: Foo(B)
 Foo(B)
 Bar(A)/Foo(A)
 Bar(A)/Foo(B)/Hoge(D)
2010/01/22
EG: Foo(A)
EG: Bar(A)
appengine ja night #4 - @ashigeru
7
Entity Groupの特徴

ポイント
 トランザクションの範囲はエンティティ作成
時に決まり、変更できない
 EGを大きくするとトランザクションで独占す
るエンティティが多くなる

EGの設計が非常に重要に
 間違えると並列性が極端に低下する
 うまくやればスケールアウトする
2010/01/22
appengine ja night #4 - @ashigeru
8
ここまでのまとめ (1)

App EngineのトランザクションはEG単位
 EG内ではACIDトランザクション
 EGをまたぐトランザクションは未サポート

EGの設計によっては並列性が落ちる
 EGを大きくすると独占範囲が広がる
 EGを分断すると整合性を保つのが困難
2010/01/22
appengine ja night #4 - @ashigeru
9
トランザクション処理のパターン

App Engineのトランザクションはやや特殊
 パターンで対応したほうがよさそう

本日紹介するもの
 Read-modify-write
 トランザクションの合成
 ユニーク制約
 冪(べき)等な処理
 Exactly
Once
 BASE Transaction
2010/01/22
appengine ja night #4 - @ashigeru
10
注意点

プログラムの説明に擬似コードを多用
 言語はJavascriptライク
 APIはJavaのLow-Level

APIライク
見慣れない言語要素
 キーリテラル

– KEY:…
KEY:Foo(A), KEY:Foo(A)/Bar(B), など
 データストア

get(tx, key), put(tx, entity), beginTransaction()
 タスクキュー

enqueue([tx,] statement)
2010/01/22
appengine ja night #4 - @ashigeru
11
パターン: read-modify-write
エンティティのプロパティを変更する
 例:

 カウンタの増加
 ショッピングカートに商品を追加

現在の値をもとに次の値が決まる
 読む、変更、書き戻す、の3ステップが必要
 途中で割り込まれると不整合が起こる
2010/01/22
appengine ja night #4 - @ashigeru
12
read-modify-write (1)

考え方
 読んでから書き戻すまでエンティティを独占
100 + 1
100
2010/01/22
101
appengine ja night #4 - @ashigeru
13
read-modify-write (2)
var tx = beginTransaction()
try {
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
tx.commit()
}
finally {
if (tx.isActive()) tx.rollback()
}
2010/01/22
appengine ja night #4 - @ashigeru
14
read-modify-write (3)
var tx = beginTransaction()
try {
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
読んでから書き戻す
tx.commit()
までをACIDに行う
}
finally {
if (tx.isActive()) tx.rollback()
}
2010/01/22
appengine ja night #4 - @ashigeru
15
DSL: atomic (tx) { … }

以後は下記のように省略
 トランザクションの開始と終了を簡略化
atomic(tx) {
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
2010/01/22
appengine ja night #4 - @ashigeru
16
パターン: トランザクションの合成
同じEGに対する複数のトランザクション
処理を合成
 例:

 2つのカウンタを同時に変更
(恣意的)
 非正規化した2つの情報を同時に更新

注意点
 分断したトランザクションでは、途中で失敗
した際に修復が大変
2010/01/22
appengine ja night #4 - @ashigeru
17
トランザクションの合成 (1)

考え方
 同じEGのトランザクションが2つあったら、
一度に処理してしまう
15
30
2010/01/22
16
31
appengine ja night #4 - @ashigeru
18
トランザクションの合成 (2)
atomic(tx) {
var a = get(tx, KEY:Eg(C)/Counter(A))
a.value++
put(tx, a)
var b = get(tx, KEY:Eg(C)/Counter(B))
b.value++
put(tx, b)
}
2010/01/22
appengine ja night #4 - @ashigeru
19
トランザクションの合成 (3)
atomic(tx) {
var a = get(tx, KEY:Eg(C)/Counter(A))
a.value++
put(tx, a)
var b = get(tx, KEY:Eg(C)/Counter(B))
b.value++
put(tx, b)
同じEGのエンティティ
}
に対する操作
2010/01/22
appengine ja night #4 - @ashigeru
20
トランザクションの合成 (4)
atomic(tx) {
var a = get(tx, KEY:Eg(C)/Counter(A))
a.value++
put(tx, a)
var b = get(tx, KEY:Eg(C)/Counter(B))
b.value++
put(tx, b)
複数のトランザクション
}
を合成, 全体がACIDに
2010/01/22
appengine ja night #4 - @ashigeru
21
パターン: ユニーク制約
重複するエンティティの登録を防止する
 例:

 同じIDを持つユーザの登録を防ぐ
 ダブルブッキングを防ぐ

注意点
 データストアは制約機能を組み込んでいない
 クエリはトランザクションに参加できない
2010/01/22
appengine ja night #4 - @ashigeru
22
ユニーク制約 (1)

考え方
 エンティティの入れ物ごと独占
 入れ物が空なら追加するエンティティは一意
@hoge
2010/01/22
@hoge
@hoge
appengine ja night #4 - @ashigeru
23
ユニーク制約 (2)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null) {
throw new NotUniqueException()
}
user = new User(key, ...)
put(tx, user)
}
2010/01/22
appengine ja night #4 - @ashigeru
24
ユニーク制約 (3)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null)
{
ユニーク制約をキーで
throw new NotUniqueException()
表す(メールアドレス)
}
user = new User(key, ...)
put(tx, user)
}
2010/01/22
appengine ja night #4 - @ashigeru
25
ユニーク制約 (4)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null) {
throw new NotUniqueException()
}
user = new User(key, ...)
そのエンティティが
put(tx, user)
すでにあれば制約違反
}
2010/01/22
appengine ja night #4 - @ashigeru
26
ユニーク制約 (5)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null) { 存在しなければ
throw new NotUniqueException()
ユニークなので追加
}
user = new User(key, ...)
put(tx, user)
}
2010/01/22
appengine ja night #4 - @ashigeru
27
ユニーク制約 (6)
var key = KEY:User([email protected])
atomic(tx) {
var user = get(tx, key)
if (user != null) {
throw new NotUniqueException()
}
user = new User(key, ...)
put(tx, user)
getからputまでを独占
}
2010/01/22
appengine ja night #4 - @ashigeru
28
ここまでのまとめ (2)

read-modify-write
 最初に読んでから書き戻すまで独占

トランザクションの合成
 同一EGに対する操作をまとめる

ユニーク制約
 入れ物を独占してからエンティティを作成
 すでにあったらユニークじゃないので失敗
2010/01/22
appengine ja night #4 - @ashigeru
29
パターン: 冪(べき)等な処理

1回分しか効果を出さない処理
 2回以上成功しても、1回分しか反映しない

例:
 フォームの多重送信を防止
 お一人様一点限り。

注意点
 英語でidempotentだけど覚えにくい
2010/01/22
appengine ja night #4 - @ashigeru
30
冪等な処理 (1)

考え方
 「処理がユニークに成功する」ということ
 まだ成功していなかったら成功させる
 一度成功していたら何もしない
成功
成功
成功
結果
2010/01/22
appengine ja night #4 - @ashigeru
31
冪等な処理 (2)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
2010/01/22
appengine ja night #4 - @ashigeru
32
冪等な処理 (3)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
「ユニークなキー」を表す
if (flag
!= null) {
→ db.allocate_ids()
return
} → DatastoreService.allocateIds()
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
2010/01/22
appengine ja night #4 - @ashigeru
33
冪等な処理 (4)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
ユニーク制約をユニークなキーで。
put(tx, counter)
1回目は確実に成功、
}
キーを使いまわせば2回目は失敗
2010/01/22
appengine ja night #4 - @ashigeru
34
冪等な処理 (5)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
if (flag != null)それ以降の処理は
{
return
一度だけしか行われない
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
2010/01/22
appengine ja night #4 - @ashigeru
35
冪等な処理 (6)
var key = KEY:Counter(C)/Flag(unique)
atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
全体を合成してACIDに
}
2010/01/22
appengine ja night #4 - @ashigeru
36
冪等な処理 (まとめ)

冪等な処理
 「1回分しか効果を出さない」パターン

やりかた
 「成功」済みかどうかについてユニーク制約
 トランザクションを合成
ユニーク制約で成功フラグを立てる
 OKなら続きの処理を行う


注意点
 ごみ(Flag)が残るが、これを消すのは一手間
2010/01/22
appengine ja night #4 - @ashigeru
37
パターン: Exactly Once

確実にぴったり1回成功する処理
 冪等な処理では0回の場合もある

(最大1回)
例:
 カウンタの値を正確に更新する(恣意的)

注意点
 「確実に失敗する」処理には適用できない
2010/01/22
appengine ja night #4 - @ashigeru
38
Exactly Once (1)

考え方
 1度しか反映されない操作を執拗に繰り返す
 いつかは成功するはず
 間違えて2回以上成功しても効果は1回分
2010/01/22
appengine ja night #4 - @ashigeru
39
Exactly Once (2)
var key = KEY:Counter(C)/Flag(unique)
while (true) {
try {
atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
} catch (ignore) {}
}
2010/01/22
appengine ja night #4 - @ashigeru
40
Exactly Once (3)
var key = KEY:Counter(C)/Flag(unique)
while (true) {
try {
atomic(tx) {
var flag = get(tx, key)
冪等な処理の
if (flag != null) {
パターン
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
}
} catch (ignore) {}
}
2010/01/22
appengine ja night #4 - @ashigeru
41
Exactly Once (4)
var key = KEY:Counter(C)/Flag(unique)
while (true) {
try {
冪等な処理を
atomic(tx) {
無限に繰り返す
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
30秒ルールがあるので
}
確実とはいえない
} catch (ignore) {}
}
2010/01/22
appengine ja night #4 - @ashigeru
42
Exactly Once (5)
var key = KEY:Counter(C)/Flag(unique)
enqueue(atomic(tx) {
var flag = get(tx, key)
if (flag != null) {
return
}
put(tx, new Flag(key))
var counter = get(tx, KEY:Counter(C))
counter.value++
put(tx, counter)
代わりにTask Queueで
})
成功するまで繰り返し
2010/01/22
appengine ja night #4 - @ashigeru
43
Exactly Once (まとめ)

Exactly Once
 「確実にぴったり1回成功する」パターン
 ただし、いつ成功するかは不明

やりかた
 冪等な処理を無限に繰り返す
 一度成功したらあとは無駄なので打ち切る

App EngineのTask Queueを使える
 成功するまで無限に繰り返す、という性質
 30秒ルールがあるからwhile(true)は不適切
2010/01/22
appengine ja night #4 - @ashigeru
44
パターン: BASE Transaction

複数のEGにまたがるゆるいトランザク
ション
 ACIDほど強い制約がない

例:
 口座間の送金処理

注意点
 途中の状態が外側に見える
 ACIDよりアプリケーションが複雑
2010/01/22
appengine ja night #4 - @ashigeru
45
BASE Transaction (1)

送金処理で本当にやりたいことは2つ
 Aの口座からX円引く
 Bの口座にX円足す

「トランザクションの合成」は困難
 Aの口座とBの口座を同じEGに配置?
 Aから送金されうるすべての口座を同じEGに?

トランザクションを分断すると危険
 失敗例:「Aの口座からX円引いたけどBに届かない」
 補償トランザクションすら失敗する可能性
2010/01/22
appengine ja night #4 - @ashigeru
46
BASE Transaction (2)

単純に考えてみる
 まずAの口座から5000円引く
 そのあと「一度だけ」Bの口座に5000円足す
atomic (tx1) {
Exactly Once
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
}
atomic (tx2) {
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
}
2010/01/22
appengine ja night #4 - @ashigeru
47
BASE Transaction (3)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
var flag = get(tx2, key)
if (flag != null) {
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
48
BASE Transaction (4)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
Read-modify-write
var flag = get(tx2, key)
if (flag != null) {
(A -= 5000)
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
49
BASE Transaction (5)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
Read-modify-write
var flag = get(tx2, key)
if (flag != null) {
(B += 5000)
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
50
BASE Transaction (6)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
var flag = get(tx2, key)
if (flag != null) {
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
Exactly Once
(B += 5000)
51
BASE Transaction (7)
var key = KEY:Account(B)/Flag(unique)
atomic (tx1) {
var a = get(tx1, KEY:Account(A))
a.amount -= 5000
put(tx1, a)
enqueue(tx1, atomic(tx2) {
全体を合成
var flag = get(tx2, key)
if (flag != null) {
(A -= 5000, B += 5000)
return
}
put(tx2, new Flag(key))
var b = get(tx2, KEY:Account(B))
b.amount += 5000
put(tx2, b)
})
}
2010/01/22
appengine ja night #4 - @ashigeru
52
BASE Transaction (まとめ)

BASE Transaction
 EGをまたいだゆるいトランザクション
 いつか確実に完了する、という性質

やりかた
 トランザクションを合成
 一つ目のEGに対して操作を行う
 Exactly Onceで二つ目のEGに対して操作を行う

注意点
 操作が行われるまでタイムラグがある
 Eventual Consistency: いずれ整合性が取れる
 二つ目のEGに対する操作は制約をかけられない
 送金先に受け取り拒否されるとすごく困る
2010/01/22
appengine ja night #4 - @ashigeru
53
ここまでのまとめ (3)

パターン: 冪等な処理
 操作自体を最大一回だけ(ユニーク)にする
=

ユニーク制約 + トランザクションの合成
パターン: Exactly Once
 最大一回だけ成功する処理を無限に繰り返す
=

冪等な処理 + Task Queue
パターン: BASE Transaction
 自分を変更後、相手の変更を確実に一度だけ適用
=
read-modify-write + Exactly Once + 合成
2010/01/22
appengine ja night #4 - @ashigeru
54
おわりに

App Engineのトランザクションは「パズ
ル」になりがち
 複雑な制約を考慮しつつ、時間内に解く
 ルールも定石もあるので、積み重ねが大切

「仮説→検証」のサイクルが必要な段階
 みんなで情報共有
 パターンやアンチパターンを持ち寄ろう
2010/01/22
appengine ja night #4 - @ashigeru
55
参考文献

Programming Google App Engine
 Oreilly

& Associates Inc, 2009/11
リレーショナルデータベース入門
 サイエンス社,

トランザクション処理
 日経BP社,

2003/03
2001/10
BASE: An Acid Alternative
 http://queue.acm.org/detail.cfm?id=1394128
2010/01/22
appengine ja night #4 - @ashigeru
56
Question + Discussion

実はあと数枚残ってる
2010/01/22
appengine ja night #4 - @ashigeru
57
Transaction Puzzlers
Advanced Topics
appengine ja night #4
@ashigeru
2種類の並行性制御

悲観的並行性制御

 リソースを独占できな
 リソースを独占し、
他人を待たせる

特徴
楽観的並行性制御
かったら、やり直し

特徴
 他人に迷惑をかける
 他人に迷惑をかけない
 成功確率が高い
 失敗し続ける場合も
 常にオーバーヘッド
 成功すれば速い
誰に迷惑が掛かるか、という点で異なる
2010/01/22
appengine ja night #4 - @ashigeru
59
その他のパターン

Sharding
 エンティティを複数のEGに分散

Long Running Transaction
 リクエストをまたぐ長いトランザクション

Software Global Transaction
 ソフトウェアで2PCプロトコルを実装

Software MVCC
 ソフトウェアでMVCCを実装
2010/01/22
appengine ja night #4 - @ashigeru
60
トランザクション設計のポイント
データの局所性を生かす
 更新頻度を意識する
 本当の制約を見極める

2010/01/22
appengine ja night #4 - @ashigeru
61
ポイント: データの局所性を生かす

ユーザ情報はユーザごとに独立している
 同一ユーザは同時にリクエストしないことが多い
 多くのユーザを同時に捌いても並行稼動

「複数のユーザが共有するリソース」に注意
 カウンタなどは典型例
 チケット予約なども該当する

ユースケース分析は重要
 単一ユースケースの並列性から考える
 いざとなれば非正規化+Eventual
2010/01/22
Consistency
appengine ja night #4 - @ashigeru
62
ポイント: 更新頻度を意識する

更新頻度の観点でエンティティを分類
 稀に更新/常に更新のプロパティを共存させない
 更新しないプロパティはどこに混ぜてもいい

マスタ/トランザクションデータでは不十分
 トランザクションデータも参照系と更新系に
 参照系はトランザクション処理しなくてもよい場合
がある

もちろん、更新に局所性があれば問題ない
 ローカルトランザクションを衝突させない設計
2010/01/22
appengine ja night #4 - @ashigeru
63
ポイント: 本当の制約を見極める


Strong Consistencyはオーバースペック
どの程度の不整合がどの順で許されるか考える
 自動販売機の支払い/商品受け取り
(数秒)
 コンビニの支払い/商品受け取り (数十秒)
 ファーストフードの支払い/食事 (数分)
 レストランの支払い/食事 (▲数十分)

同時にアプリケーション作成コストも考える
 不整合の許容は制御することが増えすぎる
 提供先のビジネス特性を考慮し、合意を得る必要性
 あらゆる観点で「必要十分」に
2010/01/22
appengine ja night #4 - @ashigeru
64
ダウンロード

unique