CUBE SUGAR CONTAINER

技術系のこと書きます。

Galera Cluster の書き込みノードはひとつに制限した方が良さそう

Galera Cluster は MySQL/MariaDB をマルチマスタ構成で冗長化するためのアプリケーション。 マルチマスタなので、本来はどのノードに対しても書き込みを行うことができる。 ただ、後述する理由からどうやら書き込みに使うノードはひとつに制限した方が良さそうだと考えるようになった。

まず、Galera Cluster がノード間でデータの整合性を保つのに使用するメカニズムは楽観的排他制御にもとづいている。 楽観的排他制御は、対象のリソース (テーブル・行) を操作する際に、自分が知っている状態から変更されているかを確認することで、割り込んでしまうのを自ら防ぐというもの。 そのため、リソースに排他ロックをかけたとしてもノードをまたぐと別のトランザクションからのアクセスをブロックすることができない。 代わりに、対象が別のノードから既に変更されていることをノードが検出した場合、クライアントにはそれがデッドロックとして見えることになる。 つまり、複数のノードに対して書き込みを行っていると、デッドロックによるロールバックが多発することになる。

この明示的なロックが動作しない (サポートされていない) という点については MariaDB Galera Cluster - Known Limitations の中でも語られている。

blog.amedama.jp

実際に試してみる

論より証拠ということで、Galera Cluster を使ってノードをまたいだ排他ロックが効かないことと、その際にデッドロックが起こることを確認してみよう。

環境には CentOS7 を使った。

$ cat /etc/redhat-release 
CentOS Linux release 7.1.1503 (Core) 
$ uname -r
3.10.0-229.14.1.el7.x86_64

Galera Cluster には MariaDB の 10.0 系を使う。

MariaDB [(none)]> show variables like 'version';
+---------------+-----------------------+
| Variable_name | Value                 |
+---------------+-----------------------+
| version       | 10.0.21-MariaDB-wsrep |
+---------------+-----------------------+
1 row in set (0.00 sec)

具体的な構築手順は以下の記事を参照のこと。

blog.amedama.jp

尚、各ノードの操作が区別できるようにプロンプトの表示をノード別で次のように変更している。

MariaDB [(none)]> prompt prompt1>
PROMPT set to 'prompt1> '

MariaDB [(none)]> prompt prompt2> 
PROMPT set to 'prompt2> '

動作確認用のデータを投入する

動作確認用として users という名前でテーブルを作る。

MariaDB [(none)]> use test;
Database changed

MariaDB [test]> create table users (
    ->     id Integer auto_increment,
    ->     name Text not null,
    ->     primary key (id)
    -> );
Query OK, 0 rows affected (0.12 sec)

そして、行をひとつ挿入しておく。

MariaDB [test]> insert into users (name) values ('foo');
Query OK, 1 row affected (0.01 sec)

動作確認

これから行う操作では、ふたつのトランザクションから同じ行に排他ロックをかけた上で更新を行う。 結果として、両方のトランザクションをコミットすると後から実施した方についてはデッドロックを起こすことになる。

まず、トランザクション 1 が行に排他ロックをかける。

prompt1> begin;
Query OK, 0 rows affected (0.00 sec)

prompt1> select * from users where name like 'foo' for update;
+----+------+
| id | name |
+----+------+
|  2 | foo  |
+----+------+
1 row in set (0.00 sec)

次に、トランザクション 2 が排他ロックをかける。 通常は、後から排他ロックを取得しようとしたこのトランザクションはブロックするはずだが、前述した通り Galera Cluster はノードをまたいだ排他ロックをブロックすることができない。

prompt2> begin;
Query OK, 0 rows affected (0.00 sec)

prompt2> select * from users where name like 'foo' for update;
+----+------+
| id | name |
+----+------+
|  2 | foo  |
+----+------+
1 row in set (0.00 sec)

トランザクション 1 が行を更新する。

prompt1> update users set name = 'bar' where name like 'foo';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

トランザクション 2 も行を更新する。

prompt2> update users set name = 'baz' where name like 'foo';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

トランザクション 1 がコミットすると、正常に完了する。

prompt1> commit;
Query OK, 0 rows affected (0.01 sec)

しかし、後からコミットしたトランザクション 2 は、既に行が更新されたことからデッドロックになってしまう。

prompt2> commit;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

以上のように、Galera Cluster ではノードをまたいだ排他ロックがトランザクションをブロックせず後からデッドロックになる。

同一ノード内のトランザクションであれば問題なし

次に、同一ノード内のトランザクションであれば排他ロックが効くことも確認しておこう。

同一ノード内の別端末の操作を区別できるように、プロンプトは以下のように変えておく。

MariaDB [(none)]> prompt prompt1.1>
PROMPT set to 'prompt1.1> '

MariaDB [(none)]> prompt prompt1.2> 
PROMPT set to 'prompt1.2> '

動作確認用のテーブルと行は先ほどのものを使い回す。

MariaDB [test]> select * from users;
+----+------+
| id | name |
+----+------+
|  2 | bar  |
+----+------+
1 row in set (0.00 sec)

まず、トランザクション 1 から排他ロックをかける。

prompt1.1> begin;
Query OK, 0 rows affected (0.00 sec)

prompt1.1> select * from users where name like 'bar' for update;
+----+------+
| id | name |
+----+------+
|  2 | bar  |
+----+------+
1 row in set (0.00 sec)

次にトランザクション 2 からも排他ロックをかけようとする。 しかし、同一ノード内のトランザクションなので、これはブロックする。

prompt1.2> begin;
Query OK, 0 rows affected (0.00 sec)

prompt1.2> select * from users where name like 'bar' for update;
...

トランザクション 1 はその間に行を更新してコミットする。

prompt1.1> update users set name = 'hoge' where name like 'bar';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

prompt1.1> commit;
Query OK, 0 rows affected (0.01 sec)

トランザクション 1 がコミットすると排他ロックが外れるので処理が継続できる。

prompt1.2> select * from users where name like 'bar' for update;
Empty set (6.29 sec)

prompt1.2> rollback;
Query OK, 0 rows affected (0.00 sec)

以上のように、同一ノード内であれば排他ロックがトランザクションをブロックする。

まとめ

今回の記事では、Galera Cluster で複数のノードに対して書き込みを行うとデッドロックが発生するという問題について書いてみた。 ロードバランサを前段に置いて Galera Cluster を使う際に、通常のラウンドロビンなどを使って全ノードに散らしてしまうと前述する問題が発生することになる。 デッドロックを許容しない場合には、通常ハンドオフ先はひとつのノードに集めるようにした上で、障害が発生した場合のみフェイルオーバーするように設定しよう。