元ドキュメント: パフォーマンスホワイトペーパー概要
性能テストの概要
1. はじめに
ロックメカニズムは、リレーショナルデータベースにおける並行アクセス制御の中核的なメカニズムの一つであり、その動作原理を理解することがアクセス競合問題のトラブルシューティングにおける重要な出発点です。例えば、高並行処理や複雑なトランザクション処理シナリオでよく発生するロック競合エラー(「Lock wait timeout exceeded」など)は、本質的に現在のトランザクションが要求するデータリソース(行やテーブルなど)が他のトランザクションによってロックされており、現在のトランザクションがロックを取得できないままロック待ちタイムアウトの閾値に達して能動的に中断されることを反映しています。
ロック競合を解決するには、ロックを保持しているセッションがロックを解放する必要があります。セッションにロックを解放させる最善の方法は、長時間ロックを保持しているセッションの発行元を特定し、ユーザーに連絡してトランザクションを完了(コミットまたはロールバック)させることです。緊急の場合、DBA はロックを保持しているセッションを終了させることができます。
本ドキュメントでは、Tencent Cloud データベース TDSQL シリーズの最新製品である TDSQL Boundless を紹介し、代表的な問題事例と組み合わせて、トランザクション並行処理におけるロックメカニズムの中核的な役割を解析し、データの整合性保証とシステムスループット向上のバランスについて説明します。
2. TDSQL Boundless アーキテクチャの紹介
TDSQL Boundless は、Tencent が金融グレードのアプリケーションシナリオ向けに開発した、高性能・高可用のエンタープライズグレード分散データベースソリューションです。コンテナ化されたクラウドネイティブアーキテクチャを採用し、クラスタの高性能コンピューティング能力と低コストの大規模ストレージを提供します。
TDSQL Boundless のアーキテクチャと機能特性: 完全分散型 + ストレージ・コンピュート統合/分離 + データプレーン/コントロールプレーン分離 + 高スケーラビリティ + グローバル整合性 + 高圧縮率。
3. TDSQL Boundless におけるロック
TDSQL Boundless は典型的な分散システムとして、単一ノード内での並行アクセス制御メカニズムだけでなく、複数ノード間でも相互排他性(Mutual Exclusion)を確保し、複数ノードが同一リソースを同時に変更してデータの不整合を引き起こすことを防止する必要があります。
コアロックタイプと実装レベル
テーブルレベルロック(コンピューティング層): 粗粒度ロック。MySQL ネイティブのメタデータロック(Metadata Lock、MDL)を使用し、単一ノード内での DDL/DML 間の並行競合を解決します。
行レベルロック & 範囲ロック(ストレージ層): 細粒度ロック。複数セッションによる同一レコードの並行変更を防止し、精密な並行制御を実現します。
グローバルオブジェクトロック(コンピューティング層で申請、TDMC 層で永続化): コンピューティング層のテーブルレベルロックは単一ノード内での並行 DDL 操作をブロックするために使用されますが、ノード間の DDL 調整はグローバルオブジェクトロックが担い、テーブルレベルロックに属します。
テーブルレベルロックはコンピューティング層で動作し、その競合シナリオは同一ノードとクロスノードの 2 種類に分類できます。以下、それぞれのシナリオにおける具体的な競合状況を分析します。
テーブルレベルロックの競合
1. 同一ノード DDL-DML 競合
以下の例では、Transaction 1 と Transaction 2 が同じピアノード hybrid-1 に接続していると仮定します。Transaction 1 は明示的にトランザクションを開始してテーブル sbtest1 をクエリし、トランザクション終了前にそのテーブルの MDL 共有読み取りロック【テーブルレベルロック】を保持します。Transaction 1 が終了していない状態で Transaction 2 が DDL を実行するとブロックされ、テーブル sbtest1 のメタデータが保護されます。
2. 同一ノード DDL-DDL 競合
同様に、Transaction 1 と Transaction 2 が同じピアノード hybrid-1 に接続していると仮定します。Transaction 1 がテーブル sbtest1 で DDL 操作を実行中で、そのテーブルの MDL 共有ロックと排他ロック【テーブルレベルロック】を段階的に保持し、後続の Transaction 2 の DDL がテーブル sbtest1 のメタデータを破壊することを防止します。
同一ピアノード上では、DDL と DML は対等であり、先に MDL ロックを取得した方が実行され、優先順位はありません。MDL ロックはプロセス内のメモリ状態です。しかし、TDSQL Boundless は分散データベースであり、セッション接続はすべてのノードに均等に分散されます。2 つの異なるピアノードに接続されたセッションが同じテーブルを操作した場合はどうなるでしょうか。
3. クロスノード DDL-DML 競合
以下の例では、Transaction 1 と Transaction 2 はそれぞれピアノード hybrid-1、hybrid-2 に接続しています。
Transaction 1 は明示的にトランザクションを開始してテーブル sbtest1 をクエリします。その後、Transaction 2 が別のノード hybrid-2 で DDL を正常に実行(このノードでは他のセッションがテーブル sbtest1 にアクセスしていないためロック競合なし)し、テーブル sbtest1 の schema version を上げます。
その後、Transaction 1 がテーブル sbtest1 のクエリを続行するとエラーが発生します。Repeatable Read 分離レベルでは、トランザクション内の最初のクエリ時にのみ一貫性ビュー(Consistent Read View)が生成されるため、新しいバージョンのテーブル構造のレコードを返すことはこの原則に反します。そのため ERROR 1412 が報告され、トランザクションのリトライが促されます(アプリケーションはこの例外をキャッチした際にロールバックしてトランザクションをリトライする必要があります)。
4. クロスノード DDL-DDL 競合
クロスノードの DDL 操作は、TDMC 層のグローバルオブジェクトロックメカニズムに依存して操作の相互排他性を実現し、分散環境でのデータ整合性と操作の秩序を確保します。
以下の例は同じテーブルの DDL が互いにブロックする状況を示しています。以前の例との違いは、異なるノードで実行される点です。この場合、相互排他性はノードレベルの MDL ロックではなく、TDMC のグローバルオブジェクトロックによって保護されます。
範囲ロックの競合
行レベルロック & 範囲ロックはストレージ層で動作します。2 つのセッションが行ロックまたは範囲ロックの競合を起こした場合、最終的に両方が同じストレージノードのプライマリレプリカにアクセスしたことを意味します。したがって、セッションが接続している SQLEngine ノードが同一かどうかに関わらず、結果は同じです。
テーブルの一定範囲に対して操作を行う場合、TDSQL Boundless のストレージ層は範囲ロック(range lock)を適用します。ロックの範囲は左閉右開の key 区間であり、Repeatable Read 分離レベルでは InnoDB の next-key lock と同様の動作をします。
以下の例は、Repeatable Read 分離レベルで id 範囲 [5, 11] を更新する際に、ファントムリードを防止するために id 範囲のギャップにロックがかかることを示しています。
行レベルロックの競合
テーブルの単一 key に対して操作を行う場合、行レベルロックが適用されます。TDSQL Boundless は精密なロックメカニズムによってデータベースの並行処理能力を最大化します。
デッドロックの競合
デッドロックは行レベルロック競合の特殊なケースです。2 つ以上のセッションが相手がロックしているデータを待っている状態で発生します。双方が互いに待っているため、どちらもトランザクションを完了させて競合を解決できません。
TDSQL Boundless は自動デッドロック検出機能を備えており、デフォルトで書き込みデータ量が少ないトランザクションをロールバックしてエラーを返します。これにより、そのセッション内の他のすべてのロックが解放され、もう一方のセッションがトランザクションを続行できます。
ストレージ層にテーブルレベルロックはあるか?
InnoDB はテーブルレベルの S ロックと X ロックを提供していますが、実質的に追加の保護を提供せず、並行処理能力を低下させるだけです。そのため TDStore ストレージ層ではテーブルレベルロックを実装しておらず、構文解析のみをサポートし、実際には機能しません。
lock tables sbtest1 read;
Query OK, 0 rows affected, 1 warning (0.02 sec)
show warnings;
+---------+------+-----------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------------------------------------------------+
| Warning | 8533 | LOCK/UNLOCK option is used for compatibility only, and it does not actually work. |
+---------+------+-----------------------------------------------------------------------------------+4. ベストプラクティス
ロックタイムアウトとデッドロックは高並行データベースシステムで一般的な問題です。効果的なロック管理には、ビジネスロジック、データベース設定、トラブルシューティングの 3 つの観点からの体系的な最適化が必要です。
ビジネスロジック
トランザクション設計はロック問題を予防するための最重要環節です。「短いトランザクション、軽い操作、順序アクセス」の 3 つの原則を守ることで、ロック競合の確率を効果的に低減できます。
トランザクションの実行時間をできるだけ短縮してください。1 回のトランザクション操作の行数を 2000 以内に制御することを推奨します。トランザクション内の SQL ステートメントが多いほど、操作行数が多いほど、ロック保持時間が長くなり、他のトランザクションとの競合確率が高まります。
トランザクション内でのユーザーインタラクションを避け、コアとなるデータ操作のみをトランザクション内に含めて、トランザクションの迅速な完了とリソース解放を確保してください。
トランザクション内でのリソースアクセス順序の一貫性を確保してください。複数のトランザクションが複数のリソースにアクセスする場合、統一された順序でリソースにアクセスする必要があります。これはデッドロック防止の鍵です。例えば、在庫引き落としと注文作成のトランザクションでは、すべてのトランザクションが先に在庫テーブルをロックしてから注文テーブルをロックするか、その逆とする必要がありますが、トランザクションによってロック順序が異なることがあってはなりません。循環待ちを回避してください。
TDSQL Boundless はほとんどのシナリオで Online DDL 機能をサポートしていますが、DDL 操作の実行前に確認することを推奨します。Online DDL の説明を参照し、ビジネスの低負荷時間帯に操作することを推奨します。
データベース設定
ロック待ちタイムアウトが本番システムで発生した場合、まず関連パラメータの設定が適切かどうかを確認する必要があります。旧バージョンからアップグレードしたインスタンスでは、後続バージョンで追加されたロック制御パラメータが互換性の要件でオフのままになっていないか特に注意が必要です。必要に応じて手動で有効化してください。
データベース関連パラメータ:
tdsql_lock_wait_timeout: ロック待ちの最大時間を制御します。デフォルト値は 50 秒です。ブロックされたセッションが 50 秒以内にロックを取得できない場合、「Lock wait timeout exceeded」エラーが報告されます。通常はデフォルト値の調整は不要ですが、深刻なロック競合がすぐに解決できない場合は、この値を適切に下げて応急対応できます。
tdstore_deadlock_detect(スーパー管理者権限が必要): デッドロック自動検出機能です。新規購入インスタンスのデフォルト値は ON で、有効のまま維持することを推奨します。旧バージョンからアップグレードしたインスタンスではデフォルトで無効であり、必要に応じて手動で有効化できます。
tdstore_deadlock_victim(スーパー管理者権限が必要): デッドロック発生時にどのトランザクションをロールバックするかを決定します(
tdstore_deadlock_detectが有効な場合にのみこのパラメータが機能します)。デフォルト値はWRITE_LEASTで、InnoDB の動作と一致します。通常はデフォルト値の調整は不要です。WRITE_LEASTに設定すると、書き込みデータ量が少ないトランザクションが優先的にロールバックされます。START_LATESTに設定すると、後に開始されたトランザクションが優先的にロールバックされます。
トラブルシューティング
実際のビジネスシナリオでのエラーは DDL タイムアウト失敗と DML タイムアウト失敗の 2 種類に分類されます。以下の手順で調査と対処を行います。
説明: 以下で使用するディクショナリテーブルの定義については「システムテーブルとシステムビュー」を参照してください。
1. DDL タイムアウト失敗
DDL 実行で「Lock wait timeout exceeded」エラーが報告された場合、DDL を実行したセッションが同一ノードの DML または DDL によってブロックされていることを示します。DDL 実行で ERROR 8542 (HY000): Acquire object lock 'test.sbtest1' from MC wait timeout, sql-node: node-tdsql3-xxxxxxxx-xxx エラーが報告された場合、DDL を実行したセッションが他のノードの DDL によってブロックされていることを示します。
performance_schema.metadata_locks をクエリして、現在のノードの SQLEngine MDL ロックの占有状況を確認できます(TDSQL Boundless では performance_schema システム変数を ON に設定する必要はありません)。
-- session1 と session2 が同一ノードに接続していることを確認
show variables like 'hostname';
-- session1
BEGIN;
UPDATE sbtest1 SET k = 0 WHERE id = 999;
-- session2
ALTER TABLE sbtest1 ADD COLUMN new_column VARCHAR(255);
-- metadata_locks を確認。broadcast HINT を先頭に追加して全ノードでクエリをブロードキャスト
/*#broadcast*/ select * from performance_schema.metadata_locks
where OBJECT_NAME='sbtest1' \G繰り返しクエリしても LOCK_STATUS: GRANTED のセッションが変化しない場合、OWNER_THREAD_ID からロックを保持しているスレッドに対応するセッション ID を特定し、そのセッションを安全に終了できることを確認した上で、KILL コマンドでセッションを終了し、DDL 操作を再実行してください。
/*#broadcast*/ select * from performance_schema.threads
where THREAD_ID=4879164 \G2. DML タイムアウト失敗
DML 実行で「Lock wait timeout exceeded」エラーが報告された場合、基本的に行レベルロックの取得失敗またはデッドロックに遭遇しています。旧バージョンからアップグレードしたインスタンスでは、まずデッドロック自動検出機能を確認し、無効な場合は有効化(再起動不要)することを推奨します。有効化後にデッドロックが発生した場合、システムが自動的に解消します。
それでもロックタイムアウト問題が続く場合は、以下の方法でロック保持者(Lock Holder)のセッション ID を特定し、KILL でブロッキングセッションを終了して行ロックリソースを解放し、待機セッション(Lock Waiter)が実行を続行できるようにします。
performance_schema.data_locks と performance_schema.data_lock_waits をクエリして、TDStore のロック保持およびロック待ちセッション情報を確認できます(TDStore では performance_schema システム変数を ON に設定する必要はありません)。
-- session1 と session2 が同一ノードに接続しているかどうかに関わらず、結果は同じです
-- 行ロックはストレージ層のため、2 つのセッションの行ロック競合は最終的に
-- 同じストレージノードのプライマリレプリカにアクセスしたことを意味します
-- session1
SELECT id FROM sbtest1 ORDER BY id limit 10;
BEGIN;
UPDATE sbtest1 SET k=50000 WHERE id<=11 AND id>=5;
-- session2
INSERT INTO sbtest1(id) VALUES(8);
-- Lock Holder のペシミスティックロック情報をクエリ
SELECT * FROM performance_schema.data_lock_waits\G
-- BLOCKING_ENGINE_LOCK_ID を使用して data_locks テーブルで保持者情報を取得
SELECT data_locks.* FROM performance_schema.data_locks
WHERE engine_lock_id='...' \G説明:
data_locksテーブルのTHREAD_IDは Performance Schema の内部スレッド ID です。対応するセッション情報をクエリするには、PROCESSLIST_IDフィールドを使用してinformation_schema.processlistテーブルと結合してください。また、NODE_IDとNODE_NAMEフィールドで、そのトランザクションが存在する SQLEngine ノードを特定できます。
select * from information_schema.processlist where id=1093359\GLock Holder を KILL した後、ブロックされていたセッションが正常に実行されます。
注意: 高並行シナリオでは、待機セッション(Lock Waiter)のキューが長い場合があり、再び新しい Lock Holder が発生する可能性があります。複数回の終了が必要になる場合があります。