640 likes | 738 Views
Transaction Puzzlers. appengine ja night #4 あらかわ (@ashigeru). 講演者について. 名前 あらかわ (@ashigeru) 所属 株式会社グルージェント 開発部 普段の業務 研究開発 ( コンパイラ系 ) 教育 (Computer Aided Education) ブログ書き (Song of Cloud Blog). Song of Cloud Blog. 会社ブログ http://songofcloud.gluegent.com App Engine のポータルサイトをコンセプトに
E N D
Transaction Puzzlers appengine ja night #4 あらかわ (@ashigeru)
講演者について • 名前 • あらかわ (@ashigeru) • 所属 • 株式会社グルージェント 開発部 • 普段の業務 • 研究開発 (コンパイラ系) • 教育 (Computer Aided Education) • ブログ書き (Song of Cloud Blog) appengine ja night #4 - @ashigeru
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 • グローバルトランザクション処理のパターン appengine ja night #4 - @ashigeru
今日の内容 • トランザクション処理の考え方 • トランザクション処理のパターン appengine ja night #4 - @ashigeru
トランザクション処理の考え方 • リソースを一時的に独占できる技術 • 同時に変更して不整合が起こる、などを回避 • 今回は悲観的/楽観的をあまり気にしない • App Engineは楽観的並行性制御 • いずれも一時的にリソースを独占できる • 設計/実装時には考慮する必要がある appengine ja night #4 - @ashigeru
App Engineのトランザクション • トランザクションはEntity Group (EG)単位 • 同一EG内のエンティティに対する操作はACID • 複数EGにまたがる操作は対応していない appengine ja night #4 - @ashigeru
Entity Groupの構成 • 同じルートキーを持つエンティティ群 • データストア上で近くに配置される • 例 • Foo(A) • Foo(A)/Hoge(B) • Foo(B) • Bar(A)/Foo(A) • Bar(A)/Foo(B)/Hoge(D) EG: Foo(A) EG: Foo(B) EG: Bar(A) appengine ja night #4 - @ashigeru
Entity Groupの特徴 • ポイント • トランザクションの範囲はエンティティ作成時に決まり、変更できない • EGを大きくするとトランザクションで独占するエンティティが多くなる • EGの設計が非常に重要に • 間違えると並列性が極端に低下する • うまくやればスケールアウトする appengine ja night #4 - @ashigeru
ここまでのまとめ (1) • App EngineのトランザクションはEG単位 • EG内ではACIDトランザクション • EGをまたぐトランザクションは未サポート • EGの設計によっては並列性が落ちる • EGを大きくすると独占範囲が広がる • EGを分断すると整合性を保つのが困難 appengine ja night #4 - @ashigeru
トランザクション処理のパターン • App Engineのトランザクションはやや特殊 • パターンで対応したほうがよさそう • 本日紹介するもの • Read-modify-write • トランザクションの合成 • ユニーク制約 • 冪(べき)等な処理 • Exactly Once • BASE Transaction appengine ja night #4 - @ashigeru
注意点 • プログラムの説明に擬似コードを多用 • 言語は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) appengine ja night #4 - @ashigeru
パターン: read-modify-write • エンティティのプロパティを変更する • 例: • カウンタの増加 • ショッピングカートに商品を追加 • 現在の値をもとに次の値が決まる • 読む、変更、書き戻す、の3ステップが必要 • 途中で割り込まれると不整合が起こる appengine ja night #4 - @ashigeru
read-modify-write (1) • 考え方 • 読んでから書き戻すまでエンティティを独占 100 + 1 100 101 appengine ja night #4 - @ashigeru
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()} appengine ja night #4 - @ashigeru
read-modify-write (3) var tx = beginTransaction()try { var counter = get(tx, KEY:Counter(C)) counter.value++ put(tx, counter)tx.commit()}finally { if (tx.isActive()) tx.rollback()} 読んでから書き戻すまでをACIDに行う appengine ja night #4 - @ashigeru
DSL: atomic (tx) { … } • 以後は下記のように省略 • トランザクションの開始と終了を簡略化 atomic(tx) { var counter = get(tx, KEY:Counter(C)) counter.value++ put(tx, counter)} appengine ja night #4 - @ashigeru
パターン: トランザクションの合成 • 同じEGに対する複数のトランザクション処理を合成 • 例: • 2つのカウンタを同時に変更 (恣意的) • 非正規化した2つの情報を同時に更新 • 注意点 • 分断したトランザクションでは、途中で失敗した際に修復が大変 appengine ja night #4 - @ashigeru
トランザクションの合成 (1) • 考え方 • 同じEGのトランザクションが2つあったら、一度に処理してしまう 15 16 30 31 appengine ja night #4 - @ashigeru
トランザクションの合成 (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)} appengine ja night #4 - @ashigeru
トランザクションの合成 (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のエンティティに対する操作 appengine ja night #4 - @ashigeru
トランザクションの合成 (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に appengine ja night #4 - @ashigeru
パターン: ユニーク制約 • 重複するエンティティの登録を防止する • 例: • 同じIDを持つユーザの登録を防ぐ • ダブルブッキングを防ぐ • 注意点 • データストアは制約機能を組み込んでいない • クエリはトランザクションに参加できない appengine ja night #4 - @ashigeru
ユニーク制約 (1) • 考え方 • エンティティの入れ物ごと独占 • 入れ物が空なら追加するエンティティは一意 @hoge @hoge @hoge appengine ja night #4 - @ashigeru
ユニーク制約 (2) var key = KEY:User(hoge@example.com)atomic(tx) { var user = get(tx, key) if (user != null) { throw new NotUniqueException() } user = new User(key, ...) put(tx, user)} appengine ja night #4 - @ashigeru
ユニーク制約 (3) var key = KEY:User(hoge@example.com)atomic(tx) { var user = get(tx, key) if (user != null) { throw new NotUniqueException() } user = new User(key, ...) put(tx, user)} ユニーク制約をキーで表す(メールアドレス) appengine ja night #4 - @ashigeru
ユニーク制約 (4) var key = KEY:User(hoge@example.com)atomic(tx) {var user = get(tx, key) if (user != null) { throw new NotUniqueException() } user = new User(key, ...) put(tx, user)} そのエンティティがすでにあれば制約違反 appengine ja night #4 - @ashigeru
ユニーク制約 (5) var key = KEY:User(hoge@example.com)atomic(tx) { var user = get(tx, key) if (user != null) { throw new NotUniqueException() }user = new User(key, ...) put(tx, user)} 存在しなければユニークなので追加 appengine ja night #4 - @ashigeru
ユニーク制約 (6) var key = KEY:User(hoge@example.com)atomic(tx) { var user = get(tx, key) if (user != null) { throw new NotUniqueException() } user = new User(key, ...)put(tx, user)} getからputまでを独占 appengine ja night #4 - @ashigeru
ここまでのまとめ (2) • read-modify-write • 最初に読んでから書き戻すまで独占 • トランザクションの合成 • 同一EGに対する操作をまとめる • ユニーク制約 • 入れ物を独占してからエンティティを作成 • すでにあったらユニークじゃないので失敗 appengine ja night #4 - @ashigeru
パターン: 冪(べき)等な処理 • 1回分しか効果を出さない処理 • 2回以上成功しても、1回分しか反映しない • 例: • フォームの多重送信を防止 • お一人様一点限り。 • 注意点 • 英語でidempotentだけど覚えにくい appengine ja night #4 - @ashigeru
冪等な処理 (1) • 考え方 • 「処理がユニークに成功する」ということ • まだ成功していなかったら成功させる • 一度成功していたら何もしない 成功 成功 成功 結果 appengine ja night #4 - @ashigeru
冪等な処理 (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)} appengine ja night #4 - @ashigeru
冪等な処理 (3) 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)} 「ユニークなキー」を表す→ db.allocate_ids()→ DatastoreService.allocateIds() appengine ja night #4 - @ashigeru
冪等な処理 (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回目は失敗 appengine ja night #4 - @ashigeru
冪等な処理 (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)} それ以降の処理は一度だけしか行われない appengine ja night #4 - @ashigeru
冪等な処理 (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に appengine ja night #4 - @ashigeru
冪等な処理 (まとめ) • 冪等な処理 • 「1回分しか効果を出さない」パターン • やりかた • 「成功」済みかどうかについてユニーク制約 • トランザクションを合成 • ユニーク制約で成功フラグを立てる • OKなら続きの処理を行う • 注意点 • ごみ(Flag)が残るが、これを消すのは一手間 appengine ja night #4 - @ashigeru
パターン: Exactly Once • 確実にぴったり1回成功する処理 • 冪等な処理では0回の場合もある (最大1回) • 例: • カウンタの値を正確に更新する(恣意的) • 注意点 • 「確実に失敗する」処理には適用できない appengine ja night #4 - @ashigeru
Exactly Once (1) • 考え方 • 1度しか反映されない操作を執拗に繰り返す • いつかは成功するはず • 間違えて2回以上成功しても効果は1回分 appengine ja night #4 - @ashigeru
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) {}} appengine ja night #4 - @ashigeru
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) {}} 冪等な処理のパターン appengine ja night #4 - @ashigeru
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) } } catch (ignore) {}} 冪等な処理を無限に繰り返す 30秒ルールがあるので確実とはいえない appengine ja night #4 - @ashigeru
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で成功するまで繰り返し appengine ja night #4 - @ashigeru
Exactly Once (まとめ) • Exactly Once • 「確実にぴったり1回成功する」パターン • ただし、いつ成功するかは不明 • やりかた • 冪等な処理を無限に繰り返す • 一度成功したらあとは無駄なので打ち切る • App EngineのTask Queueを使える • 成功するまで無限に繰り返す、という性質 • 30秒ルールがあるからwhile(true)は不適切 appengine ja night #4 - @ashigeru
パターン: BASE Transaction • 複数のEGにまたがるゆるいトランザクション • ACIDほど強い制約がない • 例: • 口座間の送金処理 • 注意点 • 途中の状態が外側に見える • ACIDよりアプリケーションが複雑 appengine ja night #4 - @ashigeru
BASE Transaction (1) • 送金処理で本当にやりたいことは2つ • Aの口座からX円引く • Bの口座にX円足す • 「トランザクションの合成」は困難 • Aの口座とBの口座を同じEGに配置? • Aから送金されうるすべての口座を同じEGに? • トランザクションを分断すると危険 • 失敗例:「Aの口座からX円引いたけどBに届かない」 • 補償トランザクションすら失敗する可能性 appengine ja night #4 - @ashigeru
BASE Transaction (2) • 単純に考えてみる • まずAの口座から5000円引く • そのあと「一度だけ」Bの口座に5000円足す atomic (tx1) { var a = get(tx1, KEY:Account(A)) a.amount -= 5000 put(tx1, a)} Exactly Once atomic (tx2) { var b = get(tx2, KEY:Account(B)) b.amount += 5000 put(tx2, b)} appengine ja night #4 - @ashigeru
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) })} appengine ja night #4 - @ashigeru
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) { 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) })} Read-modify-write(A -= 5000) appengine ja night #4 - @ashigeru
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) { 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) })} Read-modify-write(B += 5000) appengine ja night #4 - @ashigeru