Node2からのトランザクション時にNode1に障害が発生した場合の処理

kota2and3kan氏:では次に、障害が発生した時にどのようにReadをするかという話をします。(スライドを示して)先ほどと同じように、Node1がKey1とKey2に対して書き込みをしていて、Transaction Recordが作成されています。このタイミングで、同じようにNode2からReadのトランザクションを実行するのですが、この状態でNode1に障害が発生するパターンを考えます。

まず、Node1に障害が発生すると、Heartbeatが途切れます。ただし、各Writeがすでに他のNodeに届いていたので、Transaction Recordや各KeyのWriteが一応完了しています。

この状態で他のTransaction2を実行するNode2が、t1の値をReadしてきます。また、Heartbeatが途切れているので、このタイミングでTransaction Recordはすでに期限切れという扱いになっています。

そのため、Write Intentを見つけたNode2がTransaction Recordを確認しにいくと、すでに期限切れになっています。つまり、何らかの問題が発生していることが確認できます。

(スライドを示して)何らかの問題が発生していると確認できた場合、通常の処理であればすぐにABORTEDに書き換えてしまっていたのですが、Parallel Commitsの場合はKeyの値を見にいきます。

ここにはトランザクションで書き換きこまれる予定だったKeyの値がすべて含まれています。Key1は自分で読んだので問題ありませんが、自分がReadしていないKey2が存在していることがわかります。

Readしたトランザクションは自分が読み込むわけではないのですが、Key2のWrite Intentの書き込みが完了しているかどうかを確認しにいきます。

そして、Key2のWrite Intentが書き込まれていることが確認できると、トランザクションで書き込む予定だったすべてのKeyに対する書き込みは、全部完了していることが確認できます。

その場合、「このトランザクションで書き込まれた処理は、暗黙的なCOMMITである」という扱いをします。つまり、書き込み予定だったデータがすべて正しく書き込まれているので、これはもうCOMMITとして扱ってよいことになります。そのため、Write Intentが付いているx1のKeyの値を実際にReadしていきます。

そしてReadした後に、「このトランザクションはもうCOMMIT扱いですよ」というかたちでTransaction RecordのStateの値をCOMMITTEDに書き換える動きをします。これが障害発生時の1つ目のパターンの動きです。

Key2に対する書き込みがまだ実行されていない時に障害が発生した場合の処理

次に、2つ目のパターンを考えます。これも基本的な処理は同じで、CoordinatorであるNode1がTransaction Recordや各Keyの書き込みを実行するのですが、なんらかの理由で、Key2に対する書き込みがまだ実行されていないタイミングで障害が発生するパターンを考えます。

このタイミングでCoordinatorが死んでしまうと、まずHeartbeatが途切れます。死んでいるので、Key2に対するWriteがこの後に実行されることはもちろんありません。この状態でNode2がReadをしてきます。

Key1の書き込みは完了しているので、Write Intentが存在していることが確認できます。

そのため、Write Intentの情報を元にTransaction Recordの値を見にいくのですが、ここではすでにHeartbeatが途切れてしまっていて、トランザクションはすでに期限切れであることが確認できます。

そうすると、先ほどと同じように、トランザクションが書き込む予定だったKeyの一覧を取得します。それによって、トランザクションではKey1以外にKey2にも書き込む予定だったことが確認できるので、Node2はKey2の値を確認しにいきます。

すると今回はKey2に対する書き込みが実行されていない、Key2のWrite Intentが存在していないことが確認されます。

つまりどういうことかというと、Coordinatorは本来実施するはずだった書き込みがすべて完了する前に障害が発生して死んでしまった、と判断できます。

その場合、トランザクションはもう死んでしまっているので、Node2はWrite Intentの値ではなく、Key1の前のバージョンのx0という値を読み込んで、この値をクライアントに返します。

そして「このトランザクションがもう死んでいる」ことを他のNodeにも伝えるために、Transaction Recordの値をABORTEDに書き換えます。これが障害が発生した時の動作のパターン2になります。

これらの処理を、CockroachDBではStatus Recovery Procedureと呼んでいます。その名のとおりStatus Recoveryになっているので、何らかの障害が起きた時に、それを回避するための処理というイメージです。

もう1度まとめます。CockroachDBにおけるParallel Commitsでは、トランザクション内のすべてのStatementの書き込みと、COMMITする処理をまとめて並列で実行することで、処理の最短をO(1)にできるようになっています。

また、クライアントにCOMMITを返した後に、非同期でTransaction Recordのステータスを書き換えたり、Write Intentを削除する処理をすることで、他のトランザクションからも正しくReadできるようにはなっています。万が一Coordinator Nodeに障害が起きた場合は、先ほどのStatus Recovery Procedureを実行して障害有無を判断したり、トランザクションの状態を正しく判断して、正しい値、一貫性のある値を読み込めるようになっています。

タイムスタンプを使ったトランザクションの制御

次に、タイムスタンプを使ったトランザクションの制御という話をします。(スライドを示して)先ほど話した課題の部分について、Atomicityと他のトランザクションの状態を判断するのは、今話したTransaction Recordを使って実行しています。

例えば複数のNodeに対するデータでも、ある1箇所に書き込まれたTransaction Recordの状態をCOMMITやABORTに書き換えることで、一括で状態を変更できます。

また、他のトランザクションはそのTransaction RecordのStateの値を確認することで、そのトランザクション、自分が競合したトランザクションが今どういう状態にあるかを判断できます。

しかし、3つ目のトランザクションの順番を判断するところについては、まだ詳しく話ができていません。先ほどの話の中で、トランザクションやKeyの値にタイムスタンプの情報を持っていて、その時刻の情報を見て順番を判断していることを少し話しましたが、CockroachDBでは各Nodeのローカルクロックを時刻として使っています。

つまり、クラスタ内のNode間に時刻差が発生することになります。この時刻差を完全に一致させる、同期させることは難しいので、このNode間での時刻差がある状態にもかかわらず、タイムスタンプを使ってクラスタ内全体でトランザクションの順番を判断する必要が出てきます。

ということで、このあたりの情報を詳しく話したいところではあるのですが、残念ながら本日は時間が足りません。時間は足りないのですが、資料はあるので少し宣伝をします。

(スライドを示して)こちらは、今回のセッションの元ネタになっている資料で、内容はほぼ同じですが、CockroachDBの概要の部分は少しだけ詳しく情報を載せています。また、今日話していたTransaction Recordは、実際にはKey−Value形式のデータとしてストレージに書き込まれるのですが、どういう形式のKeyやどういう形式のValueで書き込まれているかみたいな、おまけの情報を最後に載せています。興味がある方はご覧ください。

そして、こちらが後半の資料です。これが先ほど話した、クラスタ内のNode間でクロックオフセットがある状態にもかかわらず、その時刻差を考慮してどうやってクラスタ全体でタイムスタンプを使ったトランザクションの制御をするか、という処理の部分について情報をまとめたものです。興味がある方はこちらもぜひご覧ください。

CockroachDBは有用なデータベース

それではまとめに入ります。分散RDBMSには分散トランザクションが必要です。そしてCockroachDBでは、Transaction Recordを使って分散トランザクションを実現しています。さらにParallel Commitsという仕組みを使って、性能向上も図っています。

さらに今回紹介した内容以外の機能もいろいろと実装されていて、それらを使ってタイムスタンプを使ったトランザクションの競合の解決や、クラスタ全体での一貫性の確保をしていたりします。

ということで、CockroachDBは名前はやばいですが、中身はとても真面目なデータベースですので、興味がある方はぜひ触ってみてください。以上です。ありがとうございました。