本当にtransactionは必要なのか?

前提

前提ですが。
 transaction=Consistency/Isolationを担保する仕組みの話とする。
一般にtransactionが持つべき属性はACIDと言われる。C/Iに比べて、A/Dが“わかりやすい”のでAtomic/Durableの属性の方が人口に膾炙しているが、現在のtransactionではA/Dネタはあまり話題にならない。A/Dネタはローカルだけで見るのであれば普通にfile system /storageの話になる。元来Atomic/Durableはtransactionのコンテクストでは専らlogging / recoveryの話だった。そして、これは非同期のepoch-basedになるとそれ自体の取り扱い優先度が下がる。現代的なtransactionでは、「現時点ではread committedが保証されているFS/storageでA/Dの問題は(ある程度)解決しましょう」ということになっている。

 なのでtransactionといった場合はC/Iの話が普通にメインになる。実際、現状では、MVCCでインメモリー主軸でepochベースだと、よほど細かくepochを切らない限りは問題にはならない。version pressure が強いので、log以前に不要なversionを消す方で頭に血が上っているのが現実だ。

 という前提で、

 

さて「本当にTransactionは必要なのか?」

 

DBにかかわる人間のほぼ100%が疑問に思うことではある。

 

大体理由は以下だ。
・そもそも難しい。理解している人間は圧倒的にすくない。
・パフォーマンス優先のためにぬるいisolationレベルで平気で運用している。
・なんだかんだで、結局DBの使い方は高機能ファイルサーバ。
よって、「別になくてもいんじゃね?」ということになる。

 

まず個人的な印象から言っていくと、たいていの場合は、
1.「あったほうがよいに決まっているが、今の実装は使いものにならない」派
2.「serializableでしょ?別にそこまでの一貫性はいらないよ」派
の2通りが多い。さすがに頭から「そんなモノイラナイ」という人は「あまり」いない。ときどき居るけど例外に近い。

 

 1の場合は、transactionの必要性は認めているが、現状のままでは使えないというスタンス。transactionに相当する手当てをなんらかの形で行うか、または回避策をとっているケースが多い。

 2の場合は、高機能ファイルサーバ的にRDBを使っている分には問題ない。そうではなくて、それなりに更新処理があるにもかかわらず、ぬるいisolationレベルで運用していることが、ままある。この場合、「serializableではない」とどういう問題がおきるか?ということがよくわかっていないことが多い。

 transactionがまともに働かないときのバグ(ではないです。仕様なのです。)というかエラー(anomaly)はなにかと言えばviewの異常だ。ところが残念ながら教科書的な記述はanomalyはlost updateとinconsistent readだったりする。lost updateは特定のレコードのread modify writeが重ねるという特殊なケースだし、inconsistent readはread committedであれば回避できる。この二つでanomalyを代表させるのは無理がある。(phantom readは実は一番not -serializableの例としては良いのだが、この場合はキーの存在・不存在が話題になっているので論点が変わってしまい適切ではない)

 もっとも典型的なanomalyはread skewの類いになる。読んではいけないデータセットをそのまま読んでいる可能性が高い。この手のanomalyが起きると、間違った計算結果が正常処理で返ることがあり、かつ再現しない。これは実は検出することが非常に困難だ。DBMS的には正常処理で“異常”なデータを返すので、エラー処理にはひっかからない。(なお補足しておくと、lost updateの排除のようなwrite abortは、これは単純にそのまま書き込むと”その結果“をどう読んでもviewがぶっ壊れているので、write側をabortしますって話になる。)

 経験的には「serializableでしょ?別にそこまでの一貫性はいらないよ」派の人も、read skewの類いの面倒さがわかれば一概に「transactionは不要」とは言えないと思う。

なので、まともに個々人でtransactionの要不要の話をすると大抵は、「あったほうがよいよね」という話になってしまう。これはこれで、もやもやがやはり残る。

実証的な意味合いから

 「たぶんtransactionはいらない。まともに設定されてなくても社会は動いているから。誰も困ってない。」こっちから考えたほうがよい。たぶん、実務面からみるとこれはこれで正解だと思う。実際なくてもなんとかなるなら、別段必要ではない。(もっとも、これは実はtransactionの話にとどまらない。IT全般にもあり得る話で、たとえば「ITなんかいらん。紙・Fax・判子で十分」という文化は未だにここらそこらにある。)

 んでviewがぶっ壊れるのはまずくないか?って話で、これが普通にtransactionの要・不要につながる。

 

viewが壊れてまずいのか?

1.別にときどきぶっ壊れてもまずくないですよ。

 まぁこういう話はどこまでいっても必ず起きる。対処は大抵の場合、あれなんかおかしいなと思ってもリロードしておしまいで、問題なしで片付く。まぁ現実の大半はこれだったりする。DBを高機能ファイルサーバ的に使うのであれば、まったく問題ない。確かにtransactionはイラナイ。単純に正規化して格納したデータセットに対してSQLでクエリーを発行できれば十分です、というものだ。更新はあっても頻繁じゃないし。

 また、バッチでの洗い替えを定期的にいれて補正しているケースもviewが壊れても問題はとりあえずない。viewが壊れたままで書き込みを続行していくと、だんだんDB自体がぶっ壊れていくが、夜間バッチで全部洗い替えて整合性が取れるように「塗りなおす」ということであれば、特に問題もない。

 実際、長期にわたって運用している業務システムでは、最初にバッチを組んだ人がリスクを見て意図的にデータのskewのクリーニングを(データ更新のタイムチャートを見て)入れていたりする。ただし、月日が経つとそーゆーのも忘れ去られて、「一貫性?必要ないんじゃない?」とまぁこういう感じになったりもするが・・・要は「運用でカバーしている」という場合になる。この場合もtransactionは必要ではない

 上記の場合は、どうしてもデータはstaleになる。今の正確な値が欲しいというときには役に立たない。正確な現在の値は知りたいよね、という場合は当然あるし、そもそも普通はできればviewは壊れて欲しくない。ということで・・・

 

2.自分でアプリ側でlock制御するのでtransactionはイラナイよ

 とまぁこういう理由が地味にアリガチだと思う。実際問題、isolationだけほしいのであれば、lockを使うことである程度保証はとることができる。「あったほうがよいに決まっているが、今の実装は使いものにならない」派の人は普通こちらだ。

 ただし、この選択はトレードオフとして、自分での品質維持のコストとパフォーマンス劣化があからさまになる。なので、基本的になにも考えずに一方的にlockを利用するというのは、たいていの場合はコスト高になる。多少コストを払ってもよい、という位置づけであれば、アプリケーションレイヤーで、またはSQLレベルで明示的に、自分でlock制御をおこなえばいいので、transactionは必要ない。

 

 要はtransactionが必要ではない、という実際的な面からみれば、上記の「それほどシステムは分離性・一貫性は“実は”必要とされていない」ということと「実際に必要な場合はコストをはらって自分で準備できる」ということに尽きるように見える。

 まず「それほどシステムは分離性・一貫性は“実は”必要とされていない」ということは、ある程度は真実で、これは実際、一貫性が不要なケースで、不必要に過剰にRDBを使っているだけだろう。大抵の場合、しっかりしたfile systemかExcelあたりで十分なはずだ。(もちろんベースのデータサイズがExcelでは上限を超えるということはあるので、その場合はRDBを使うというケースもあるが、これはデータの取り回しの話で、普通にCSVあたりでちゃんと処理ができれば、わざわざRDBでの処理も必要もないことが多い)

 ここでは、「多少コストを払ってもよいので、アプリケーションレイヤーで、またはSQLレベルで明示的に、自分でlock制御をおこなえばいいので、transactionは必要ない。」ということがあり得るかどうか?である。

 

lock制御での回避

これは実は二点論点があって
1.そもそもちゃんと制御できているのか?という話、と
2.仮に出来ていたとしてそれは今後も可能なのか?という話
になる。

 

 1については、まさにDBAの腕次第というところだ。2PL-CSRの理屈がわかっているのであれば、ある程度の並行性を保ちながら、整合性をもったまま着地させられる。ただ大方は、排他制御によるsingle versionでのserial 実行の担保が現実だろう。この場合は工夫がなければスケールアウトどころかスケールアップすら難しい。その上、lock-releaseの管理をきっちり行わないと無用のトラブルも巻き込みかねない。出来なくはないがハイコスト、というところだろう。

 lockの原則論から言えば、これは簡単で「読むべきもの全部(predicate)」にコンバージョンできないread lockを取る、でよい。これで普通にread fromが保護されるので、以上おしまいになる(ただし自分だけではなく、自分がoverwriteするconcurrentなtx全部にも同じことをやる必要がある)。なのだが、predicate lockは言うほど簡単ではないので、いろいろ面倒な場合は一気にtable lockやgiant lockを取りまくることが多い。それだとやりすぎるとほぼDB全体がほぼ線形実行になって使い物にならない。なので、conflictの範囲/タイミング/CRUDの依存等々をみながら、single versionであれば、lockとreleaseのタイミングを合わせながら、また、MVであれば必ずanti-dependencyのchainが切れるようにover-writerへの制約をかけるように、慎重に設計していくというのが普通だと思う。まぁ腕と経験は要る。ただ可能であるし、実際そういうSIはいくらでもある。現実のたいていのDBのserializableのisolationレベルが使い物にならないのであれば、そういう選択をせざるを得ないという事情もある。

 

 確かにこれができるならtransactionはイラナイ、という考え方はその通りだと思う。同じ一貫性の担保をアプリで実装できるわけだから、別段にDB/ミドルで制御してもらう必要はない。とはいえ、これ完ぺきにやるには相応の技術と経験が必要で、そういうDBAがごろごろ転がっているわけでもなく、よってちょいちょい具合が悪いことになるが、その辺は例外処理+エラートラップの鬼守備SIとの合わせ技で乗りきる、というのが相場には見える。

 それでこの「職人芸的なlockコントロール」がどこまで持つか?という話で、これが今後も可能なのか?という話、になる。

 

今後のアーキテクチャの変更と、lockとtransactionの位置づけの変化

この10-20年ぐらいのコンピューティングの変化では、まずやはり分散処理が大小さまざまに普及しつつあることだろう。小はNUMAから大は大規模分散ストレージまで、ムーアの限界/単ノードの限界からのスケールアウト戦略は、従前の単ノード/Diskベース/少量コア/少ないDRAMの状況を一変させている。

 この状況下での一貫性担保のためにlockコントロールは極めて旗色が悪い。大規模クラスターでの分散lockは2PCの時代から「やったら負け」の代名詞のままだし、大規模サーバのrack scale構成もSOA/microserviceのかけ声はいいが、一貫性担保のための分散Txはlock取ったとたんにパフォーマンス劣化が激しい。さらにそもそもの足下のサーバのメニーコア/大容量メモリーですら、既存の2PL-CSRのDBではコアスケールすらできない始末になっている。明確になっているのは、この分散環境下では「lockのコスト」が想定以上に大きくなっているという現実だろう。

 lockはある意味で分かりやすい。たとえば、CCの意味論とresource managementを区別しなくてもよい。特にcritical sectionの管理としてのlock制御は鉄板の手法であり、その延長線としてのread保護は実装容易性(ただし運用が容易かどうかは別)から、慣れていればいくらでも利用できる。その弊害としてはserializableの確保のためのlockなのか、serial実行のためのlockなのかの区別がつかない、ということになるが、パフォーマンス劣化が許容範囲に収まるのであれば、実装容易性に勝るものはないだろう。

 このlock制御による一貫性担保は、centralizedな仕組みであれば非常にうまく機能する。コア数・メモリーが少ない環境下では目端の利く手元での自分制御が、杓子定規のアルゴリズム制御よりも効率的であることは言うまでもない。ただ、残念ながら分散環境下ではそうはいかない。lock制御で管理工数が嵩む上に、そもそものlock-releaseのturn aroundのコストが支配的になり始める。これはlockベースの本質的なものなので、いかんともしがたい。

 

 こういったことを背景に技術的に最新のtransaction実装は、ほぼすべてtime stamp(TS)ベースになってる。TSとlockベースは実は論理的には同じことだが、実装がまるで異なる。アルゴリズムは(大抵の場合は)別物と言って良い。2PL-CSRの牧歌的な理論/実装に比べれば、次元の異なる難易度になってしまっている。これをスクラッチのハンドメイドで作り上げるのは至難の業だ。端的に言えば、lock/releaseの手順を、timestampをよりどころにしたアルゴリズムで解決する(lockであれば「見ればわかる」ところを「計算する必要」になる)必要がある。計算のためのデータ構造は用意する必要があるし、オーバーヘッドもかかる。そもそも計算コストを見積もる必要もある。

 

・・・・というわけで

「自分でアプリ側でlock制御するのでtransactionはイラナイ」というのは今まではよいが、今後は通用しない。なので、「transactionはイラナイ」というのは、「DB=高機能ファイルサーバ」に使っているケースや、「今の値は要りませんOLAP」だろう。たしかにそれは「transactionはイラナイ」。ただし、今後、今現時点の正しい値を知りたいということであれば、従前の自分lock制御ではおそらく抑えが効かなくなりつつなる。よってRDBでtransaction機能を提供する、ということは必要不可欠になる。まぁそんな感じだ。

・・・だからlockベースでできたから、このままいけるぜ、とかって無理だと思うよ。

 

 個人的には、遠い将来には(近い未来では無理だと思う)TSベースでの制御アルゴリズムを各自がバリバリ実装していく時代にはなると思うけど、僕が生きている間には無理だと思う。

補足:TSベースで制御アルゴリズムを作るには

 現状の実践の延長線では、OCCとかMVTOライクな形で行くのは手だと思う。この場合TSベースの仕組みはlockと違って、どうしても楽観になる。よって長いtxはほぼ確実に死ぬ。なので、単純なアルゴリズムではうまくいかない。ただ、すべてを悲観ベースでやる場合は、どうしても分散処理との相性は悪い。よって、TSベースの良さを生かすのであれば、OCC/MVTO/MVSGライクなアルゴリズム・コンポネントを下位に、またより上位にセミカスタムなアルゴリズムをワークロードに合わせて自分で開発して、楽観と悲観の間ぐらいの仕組みに「仕上げていく」ことが必要になる。データ構造・ワークロード・アプリケーションを睨みながら、この手のアルゴリズムまで作り上げることはlockベース実装とは次元が異なる難しさだろう。これはうまく組み上げた時の爽快感はなにものにも代えがたいが、うまくいかない場合は延々と悶絶してメンタルにくるので、おすすめはしない。が、腕に覚えのあるミドルウェア・エンジニアであればぜひ挑戦してほしい未開の地ではある。