TIS Chapter 3
この章ではConcurrency Controlについて述べる.
3.1 Goals and OverView
ある一連の処理(Transaction)が他のTransactionにInterleaveされたとする.
このとき,処理の結果がSerial Exectuionの結果と等価であるかを証明する方法を説明する.
Note: ここはかなり重要で,Concurrency Controlの基本的なCorrectnessはこれを指す. failure-free environmentを仮定して定式化を行う.
failureは往々にしてrecoveryの結果abortに帰結するがcounterintuitiveであるため.
ここではConcurrency Controlのみを扱うのでそうしている.
3.2 Canonical Concurrency Problems
Dirty Read, Inconsistent Read, Lost-update Anomalyの紹介.
既知かつ想像に容易いのでスキップ
一応,一例をあげる
たとえば r1(x) r2(x) w2(x) w1(x) C2 C1 とする
x++ が二回実行されたようなものだ.
しかし結果は一回しか実行されていないものと同じとなる.
これが問題だよねというのが議論の出発点
3.3 Syntax of Histories and Schedules
この節はterminologyの話なので,いっそ飛ばしても読める.
厳密な理解の助けにはなる.
前節で紹介されたConcurrency Problemsは当然MECEではない. ユーザがどんなときに「この処理結果は問題である」と考えるか,全パターンをすくい上げるのは無理だろう.
なのでここでは,Concurrency Problemsという概念そのものを定義する.
この関数は,とりあえず「Concurrency Problemsが起きているか?」を検証するboolだと思えばよい.
correctnessが真となる限りにおいて,その関数で定義したConcurrency Problemsは起こらない.
これで,強いcorrectnessを使えばあらゆるConcurrency Problemsは起こらないし,弱いcorrectnessがどのようなProblemsを含むのかも定義可能となる.
最も強いcorrectnessが「直列実行したか?」である,と考えるとわかりやすいだろう.
そしてその生成アルゴリズムであるschedulerというものを説明する.これがheart of concurrency control. まず,全てのTxnsの述語が与えられていることを前提とする.
r, w は読み書きでdata operationという.
cはコミット, a はabortで共に termination operationである.
トランザクションの成功とはcommitを意味する
失敗とは,abortの呼出(いわゆるuser abort)か,concurrency controlの結果としてのabort(いわゆるsysabort)を指す
abortしたトランザクションの処理内容はDBに一切の影響を与えない
termination operationはリカバリや永続性の議論の際に重要となる
トランザクションとは,data opsの順序集合にtermination opsがくっついたものと定義できる.
次に,全てのトランザクションの処理内容(r,w,c,a)は他の全てのトランザクションから可視であるという前提を置く.
この前提を置かないと以降のhistory/scheduleの議論ができない
historyとscheduleというtermを定義する
history: 処理の結果を示す.全てのtermination operationを含む.complete scheduleとも呼ぶ.
schedule: termination operationを含まず,実行中の処理が含まれていてもよい.
この二者を区別する理由は,静的ではなく動的にトランザクションを実行したいから.
アドホックなクエリが飛んでくること(当たり前だが)を想定している
schedulerを用いてscheduleを生成し,この処理の結果がhistoryとなる.
トランザクションの見方は二つある.
全順序: 全Txの順序関係を見る.すなわち並列実行が一切されていない場合の順序.
半順序: 各Txの順序関係を見る.並列実行されている場合がありうる.
ここでは半順序のみを考える.
historyとscheduleはトランザクションのopsを並べることで表記できる.
ここでいう並び順が,半順序を意味する.
たとえば r1(x) w2(x) c2 r2(y) a1 など.
全ての$ Ti のopsが $ Tj のopsに対して同じ順序である時,この二者は Serial Historyという.
集合論でかけるけど面倒なのでskip
r1(x) c1 r2(x) c2 はSerial.
r1(x) r2(x) r1(y) c2 c1 はnot Serial.
これらのr-wの順序をDAGとして書くことで半順序は表現できる. DAGを一度描いてしまえば,あとは任意の順番でトポロジカルソートすることで,任意の全順序が生成できる.
3.4 Correctness of Histories and Schedules
すなわち schedule(S)に対して$ correctness(S) := \{s \in S | \sigma(s) = 1\} と使う.
correct(S) < 0なら部分スケジュールのどれかがcorrect.
$ s \in correct(S) ならcorrect schedule.
で,問題のcorrectかどうか?という議論については,「異論はあるかもしれないが」以下とする.
Assuming that schedules should maintain the integrity of the underlying data(i.e. keep it consistent) and assuming that each individual transaction is able to assure is, it is reasonable to conclude (by induction) that serial histories are correct.
すなわち直列実行した際に起こりうるスケジュールであるか?ということ.
半順序のDAGから生成した全順序が直列実行した際のものと等価であるか?ということ.
で,ここまで長々と定義したことは全て以下に帰結する:
define a notion of equivalence for schedules,
define serializability via equivalence to serial histories.
3.5 Herbrand Semantics of Schedules
では,correctness関数の一つを実際に考えてみる.
あるトランザクション $ Ti \in sの $ ri(x)を考える.
これは $ the\ last\ wj(x) \in s を読んでいる.そして 半順序は $ j \to iである.
$ Ti が $ wj(x)を行うとすると,これは $ Tjの処理に依存しているわけである
このように,あるタプル $ x を見た時にそのエルブラン関数は各Txの半順序が表現されているものとなる.
このエルブラン関数が同一であることをcorrectとすればcorrectness関数が作れる.
すなわち,ある時点での全てのタプルのエルブラン関数が直列実行した際と一致するかを調べる.
これによって,直列実行した際と同じ最終値が生成されているかがわかる.
文字だけだとやや厳しいので直感的な数式を.
$ s = w_0(x)c_0r_1(x)r_2(x)w_2(x)c_2c_1w_3(x)c_3 というscheduleがあるとする
ノリとしてはこんな感じ
code:FSR
x = 1; // w0(x)
Thread 1 do {
localx = x;
}
Thread 2 do {
localx = x;
x = localx + 1;
}
Thread 3 do {
x = 42;
}
各ステップは以下のようなエルブラン関数 $ H_sになる.
1. $ H_s(w_0(x)) = f_0x() # f0は初期値.nullでも0でもなんでもいい
2. $ H_s(r_1(x)) = H_s(w_0(x)) = f_0x()
3. $ H_s(r_2(x)) = H_s(w_0(x)) = f_0x()
4. $ H_s(w_2(x)) = f2x(H_s(r_2(x)) = f_2x(f_0x()) # 書込みは読み込みの結果に依存する
5. $ H_s(w_3(x)) = f_3x()
4までは理解しやすいが,5が面白い.
エルブランセマンティクスがシンプルな関数一つに戻っている.
これは$ T3が何も読まずにxを書いているためである.
ということは以下のスケジュールとも実は Final State Equivalent である.
$ s = a_1a_2w_3(x) c_3
いきなり問答無用でT1とT2を殺してT3だけ実行してやるパターン.
↑のプログラムだとx = 42 だけやればFinal Stateは同じだよねという話
ここでは$ T1と$ T2は何もすることなく終了させられている.
しかしエルブランセマンティクスは $ H_s(w_3(x)) = f_3x() と同じなので等価である.
つまり$ T1 だろうが$ T2だろうが途中の経過にあるトランザクションが何をしてもよいというのがFSR.
なので,エルブラン関数をcorrectness関数に用いるという実際例は無い.
3.6 Final State Serializability
さて,Reads-From Relation という概念で新たなcorrectness関数を作る.
まず,readの結果は直前のwriteに依存する,という前提を置く.
これはエルブランセマンティクスの話と同じ.
$ Tj の $ r_j(x) は $ w_i(x) \in sを読んでいて, $ w_i(x) <_s r_j(x)ということ.
以下のtermを整理する.
useful
ある $ w_i(x)を後続のトランザクションが読んだ場合,そのwriteステップをusefulとする.
$ w_i(x) r_j(x)ということ
または,あるトランザクションがread-writeをした場合.このreadステップもwriteステップもuseful.
$ w_0(x) r_j(x) w_j(x)ということ
useful closureは連鎖していく.この場合全ステップuseful.
alive
usefulなステップで,かつ$ t_\inftyにおいてusefulであるもの.
端的に言うと「今DBから読める値に影響を及ぼしているステップ」のこと
先ほどの $ w_0(x) r_j(x) w_j(x) を考える.
これは全ステップalive.
ところが$ w_0(x) r_j(x) w_j(x)\ \ w_k(x)であったとする.
この場合先ほどのステップがusefulであることは変わらない.
が $ w_k(x)が入ったことによって過去のステップはaliveでなくなる.
$ w_k(x) \to r_\infty(x)と考えて,$ w_k(x)だけalive.
dead
aliveでないこと.
エルブランセマンティクスで言い換えてもよい.
その値を再現するために必要な関数群に含まれないステップはdead.
含まれるならalive.
あまりにしんどいので擬似コードで示す.
code: useful, alive, dead
x = 1; // useful dead
x += 1; // useful dead
x = 2; // useful alive
x += 1; // useful alive
さて,reads-from relationは以下と定義する.
$ RF(s) := \{(t_i, x, t_j)| an\ r_j(x)\ reads\ x\ from\ a\ w_i(x)\}
alive reads-from relationというものも持ち出す.(なぜかARFではなくLRFと呼ぶ)
$ LRF(s) := \{(t_i, x, t_j) | an\ r_j(x)\ alive \ reads\ x\ from\ a\ w_i(x)\}
で以下のスケジュールを考える
$ s = r_1(x) r_2(y) w_1(y) w_2(y) c_1 c_2
$ s' = r_1(x) w_1(y) r_2(y) w_2(y) c_1 c_2
ここで$ s'は直列実行されたのとほぼ同じ. termination operatorが後ろにあるだけ.
しかし$ sの順序関係は良くわからない.T1とT2のどちらが先に起こったとみなせるだろうか?
でReads-fromをやってみる.
$ RF(s) = \{(t_0, x, t_1), (t_0, y, t_2), (t_0, x, t_\infty), (t_2, y, t_\infty)\}
$ RF(s') = \{(t_0, x, t_1), (t_1, y, t_2), (t_0, x, t_\infty), (t_2, y, t_\infty)\}
どちらのスケジュールでも$ r_2(y) \to w_2(y) \to r_\infty(y)なので $ r_2(y)はalive.
加えて,$ s'においては $ r_1(x) \to w_1(y) \to r_2(y)である
ということは $ r_1(x) \to r_\infty(y) まで続いている.
このことをLRFで示す.
$ LRF(s) = \{(t_0, y, t_2), (t_0, x, t_\infty), (t_2, y, t_\infty) \}
$ LRF(s') = \{(t_0, y, t_2), (t_1, x, t_2), (t_0, x, t_\infty), (t_2, y, t_\infty) \}
差分を見ると,$ RF(s)に対して $ LRF(s)はひとつステップが抜けている.
つまりひとつステップがdeadになっている.
だから何だ?ということになるが,要するにこの2つのスケジュールはLRFが一致しないのである.
LRFが一致しないということは,最終状態の値に至るステップが異なるということ.
つまり異なるDBの状態となる.
長い証明があるが,要するに,LRFが一致しなければFinal State Equivalentではない.
最終状態が一致することはLRFの比較で確認できるというわけである.
つまり,FSRのためのcorrectness関数にLRFの比較演算を用いることができる.
Serializable historyを用意し,そのLRFと比較すればよい.
これはエルブランセマンティクスというcorrectness関数と同じ機能を果たす.
3.7 View Serializability
FSRのスケジュールでは最終状態が一致していても途中のトランザクションがなんかおかしくなるということは示した.
なんかおかしくなる == Anomalyが発生するということはわかる.
が,まだAnomalyとは何か,ということについて定式化できていない.
そこでまず,$ s \in t_\inftyとする.
つまりトランザクションは無限にありますと考える.
こうすると最終状態(Final State)が存在しない.
このためエルブランセマンティクスによる検証が不可となる.
LRFもなにそれ?という話になる.今aliveでも今後deadになるかもしれない.
逆に言うと,dead stepもaliveだった時代はあったのだ.
この状況化でのcorrectness関数を考える.
そうすると,常に,今aliveなステップがcorrectnessであることが望ましい.
i.e「常に走り続けているDBのどのタイミングを観測してもserializableである」correctness関数が欲しい.
やっと実用的になってきた.
非常に長い道のりだったが,ここでようやくAnomalyとは何かがわかる. writeの内容は直前のreadに依存するという前提を置いていることは先に述べた.
この特性は非常に重要.
株式なり検索なりどんなアプリケーションでもいいが,読み書きがView Serializableなら,プログラムは直列実行した際と並列実行した際で同じセマンティクスを得られる.
このあとつらつら書いてあるが,要するに「readの誤謬のことをAnomalyと呼ぶ」のである.
まあ,ただの x=3 と x=0 が並行に走って,x の値がどちらになったとしても,Anomalyにはなりえない.
結局どちらも読まれていないなら,たとえば x=2という謎の値が保存されていても,とりあえず困らないだろう.
困るのは,以後にy = xとかして読みだしたときで,それ以外は困らないはずだ.
実際,コンパイラは未使用の変数や使われない代入文を最適化で潰すことがあるが,プログラマはそれで困っていないわけだし,ここは異論が(最大公約数的に)ないだろう.
だから,readが間違っているときAnomalyだとよばれる.
3.7.1 View Equivalence and the Resulting Correctness Criterion
というわけで,最終状態が一致することを検証するFSRに加えて,Viewが直列実行と等価であることも検証したい.
これは実用上最も有効なcorrectness関数となりうる.
ここまで出てきたtermでそれは実装できる: RFを比較して一致しないことを検証すればよい.
RFを比較して一致しているということは,view が一致しているということ.
そのReadに至った全てのread and writeステップが一致しているということ.
すなわち処理されるwriteも一致する.
で,このcorrectness関数は多項式時間で解ける.つまりVSRの検証はNP-Complete.
ちなみにFSRの検証も同じく(まあロジックほぼ同じですし)NP-Complete.
さらに,$ VSR \subset FSR .
以下証明.
$ s = w_1(x)r_2(x)r_2(y)w_1(y)c_1c_2とする
ありうるSerial Orderは以下
$ s'_1 = w_1(x)w_1(y)c_1\ \ r_2(x)r_2(y)c_2 つまり$ T_1 \to T_2
$ s'_2 = r_2(x)r_2(y)c_2\ \ w_1(y)c_2 つまり $ T_2 \to T_1
で,$ sはFinal State Equivalentである.
$ LRF(s, s'_1, s'_2) = \{(t_1, x, t_\infty), (t_1, x, t_\infty) \} で全部同じ.
alive && usefulだけを洗い出しているのでこうなる.
$ T2はread-onlyであるためusefulではない.このためaliveの条件を満たさない.
だが当然RFは違う.
$ RF(s) = \{(t_2, x, t_1), (t_0, y, t_1)\}
$ RF(s'_1) = \{(t_1, x, t_2), (t_1, y, t_2)\}
$ RF(s'_2) = \{(t_0, x, t_2), (t_0, y, t_2)\}
直列実行している$ s'_1, s'_2とRFが一致しないということはReadに誤謬がある.
つまりanomalyが何がしかあるということになる.
3.7.2 On the Complexity of Testing View Serializability
では実際にVSRであることを検証するアルゴリズムはどう書けるだろうか?
トランザクションがたとえば $ T1, T2, T3...Tnとあると考える.
直列実行した際のhistoryは $ T3 \to T2 \to T1なり $ T1 \to T3 \to T2なりと考えられる.
で,これらの各historyのRFを作りまして,このRFが実際の実行順序$ sと一致するか調べるというのがナイーブ.
順列の計算量は$ n!であるから完全に厳しい.
まして100万TPSとか言う時代に,1Tx追加するたびにこれをやるのは無理.
この計算はいわゆるNP完全である.
マジメに説明するとpolygraphというものを作ってやるのだが一旦割愛
3.8 Conflict Serializability
これについては今回は飛ばす.
実際的に最も使われているSerializabilityであり,correct関数であるが,考える必要はない.
より具体的にいうと,MVSR等価が必ず保証されるVersion Functionの一例がCSRなのである.
この説明で腑に落ちるようになったほうがCSRを理解するより健全なので,ここは飛ばす.
要約すると,
これまではReads-Fromさえ一致すればSerial Equivalentであると判定できるという話だったが,計算量が重かった
そこでWrite Orderもさらに考慮する.WriteとWriteのOrderも含めてグラフを描く.
こうするとVSRよりありうるscheduleは狭くなるが,簡単にonline schedulerを実装できる.
ロックを用いてwrite-writeを順序付けて,後はreadの順序がこのw-wの順序と一致しているか調べればよい.