こんにちは、LINEでGame Platformを開発している 趙 です。 この記事はLINE Advent Calendar 2016の9日目の記事です。 LINE Game Platformでは分散型でスケーラブルな高速データベースであるHBaseをメインストレージの一つとして使っています。HBase運用における問題のひとつは、cross row, cross tableトランザクション処理機能がない事です。Client Faultなどが起こった時の対応が難しいです。(例えば、HBaseにテーブルAとBがあり、ABの順番にデータを処理する場合、もしAの挿入の後にBの挿入に失敗した場合、データ不整合が起きます) HBaseは幾つかの単行atomic apiを提供します。(HBase versionによって違うところがあります) 複数cellに対してgetまたmutation操作 CAS(コンペア・アンド・スワップ) API checkAndMutate incrementColumnValue トランザクション処理機能はもともとHBaseでは強く求められませんが、運用中はできるだけデータベースの種類を減らしたい気持ちがありますから、HBaseでcross-row、cross-tableトランザクション処理機能をサポートする必要性がありました。すでにHBaseにおけるトランザクション処理のOSSが幾つか存在していますが、トランザクション処理要件、対応HBaseバージョン、導入の難易さ、社内ツールとの連携性などを検討して内製することになりました。設計中は主にPercolatorとhaeinsaを参考にしました。 スペック トランザクション処理は要件によって設計が異なります。私達の導入予定案件では、トランザクション規模は小さくて、WriteよりReadの方が明らかに多いです。そのため、スペックは以下のように定義しました。 インテグレーション方式は、導入と移行の便利さを考えてクライアントライブラリに決めました。使用者はプロジェクトにこのライブラリを導入するだけで済みます。HBase側の対応は必要ありません。 使い方は以下のような感じです。 分離レベル アプリケーション側でトランザクション処理をする場合に、性能に一番影響するのはHBaseにアクセスするときの通信コストです。高い分離レベルは優れた並列性を提供できますが、通信回数を増やせば性能は逆に落ちることがあります。 例えば、HBase cell versionを利用してMVCC(Multi Version Concurrency Control)で分離レベルSnapshot Isolationを実装してみると、データの取得は二段階になります。 まずは有効なデータバージョンを取得し、そして有効バージョンでデータを取得します。分離レベルSerializableの場合はロック情報とデータは同じ行に保存すると、アクセス時に一緒に取得できます。Snapshot IsolationよりHBaseアクセス回数が少ないです。そしてSnapshot Isolationの場合は、cellごとに有効バージョンを保存するカラムが必要です。テーブルのカラム数が倍になってしまうと(この場合の分離単位はcellです)ストレージとネットワーク通信に対して大きなデメリットがあります。分離レベルSerializableの場合はテーブルにロック情報を保存するカラムを一つだけ追加すればいいです。 Concurrency Control よりよい並列処理能力を提供するため、OCC(Optimistic Concurrency Control/楽観的並行性制御)を採用します。トランザクション処理の一時結果はコミット段階までHBaseに書かずメモリに保存します。ロックもコミット段階までかけないようにします。 例えば、同じresourceを使うトランザクション処理t1、t2があります。 t2(read only)がt1より開始時間が遅くなりますが、t2はt1コミット開始前に完了すると、両方とも成功です。PCC(Pessimistic Concurrency Control/悲観的並行性制御)の場合は、t2は失敗です。 t2がt1より開始時間が遅くなります。t2はt1コミット前にコミット完了した場合、t2は成功ですがt1は失敗です。PCCの場合はt2は失敗です。 コミット コミット段階は実際にHBaseデータを入れる段階です。Two-phase commitプロトコールを使用してトランザクション処理の原子性を保証します。トランザクション処理を4つの状態に分けます。 BEGIN: 最初の状態です。user logicを処理して、結果をメモリに保存します。 PREWRITE: コミットフェーズ1。ロックをかけます。処理結果をHBaseに保存します。 COMMITTED: コミットフェーズ2。PREWRITEでかけたロックを解除します。 ROLLBACK: PREWRITEは失敗したら、トランザクションロールバックを行います。 状態を保存するため、HBaseに二つデータモデルを追加します。 Row status column family アプリケーションで使うすべてのテーブルにこのColumn Familyを追加します。CAS APIのコンペア機能は一つのcellをチェックしますから、isLocked:tid:ts の形で一つのcellに保存します。 ロックを長くかけないように、他のプロセスがロックされた行にアクセス時、もしロック時間(tsからの時間)が長いならロックを解除することができます。 Transaction status table トランザクション状態を保存します。アプリケーションごとに一つだけが必要です。 フェーズ1 トランザクション状態はPREWRITEになっています。CAS APIを利用して、変更した行ごとにデータの更新と同時にロックをかけます。もしCAS操作が失敗した場合は、Conflict(更新するデータはすでにロックがかけされます)が起きてると見なされるのでロールバックを行います。 データ更新の擬似コード 全ての行が更新されたら、トランザクション状態をCOMMITTEDに変更します。 トランザクション状態変更の擬似コード フェーズ2 トランザクション状態はCOMMITTEDになっています。実行プロセスはすべてのrow lockを持っていますのでConflictが起きません。ロックはほかのプロセスから解除することが可能です。二度とロックを解除しないようにCAS APIを利用してロックされた行ごとにロックを解除します。 擬似コードはこうなります。 すべてのロックを解除したらトランザクション処理が終わります。HBaseに保存したトランザクション状態をFINISHEDなどに変更する必要性はありません。 ロールバック コミット中Conflictなどが発生したらすでに更新したデータの取り消しとロックの解除をしなければなりません。HBaseのCell Versionを利用してロールバック機能を実装しました。データをHBaseに入れるときにトランザクションごとでバージョンを付けます。ロールバック中は指定バージョンのデータを削除します。 ロールバック中でもClient Faultなどが起きるかもしれないため、複数のプロセスが同時にロールバックできるように設計しました。それでもロールバックをしようとする時に、他のプロセスがすでにロールバックをしたこともあります。ロールバック時点で行とトランザクションの状態確認が必要です。 プロセスは自分が処理しているトランザクションのロールバックをするときに、トランザクション状態をROLLBACKに変更してみて、成功したらすでに更新した行のロールバックをします。 トランザクション状態をPREWRITEからROLLBACKに変更するときの結果によって行為も違います。 行状態の変更が成功したら、ロックの解除とデータの削除を行います 行状態の変更が失敗したら、行のトランザクション状態を確認します トランザクション状態変更の擬似コード 行のロールバックの擬似コード newStatus、lastStatusともトランザクション状態テーブルに行ごとに保存しています。 トランザクション id トランザクション idについての要件は 一意に識別するための識別子です 更新したcellのバージョンとして使います 単調増加 (データを取る時にいつも最新のデータをとります) トランザクションid生成スピードはトランザクションの生成スピードを制約します (…)
↧