MariaDB 10.4 では、バンドルされているGalera Cluster用ライブラリのバージョンが3系から4系に更新されました。
中でもGalera4の目玉として、Streaming Replicationという新機能が実装されました。
今回は本機能についての検証を行いたいと思います。
ドキュメント
Streaming Replication
Using Streaming Replication
Galera ClusterにおけるレプリケーションとLarge Transactionの実行における問題点
Galera Clusterにおけるレプリケーションはいくつかのトランザクションの集合であるWrite setという単位で同期が行われています。
Write setの最小単位はトランザクションであり、Commitが行われて初めて他のノードに更新処理がレプリケートされます。
そのためトランザクション内に複数のDMLが含まれていて結果的に大量データの更新となるようなケースでは、一度にすべての更新データを反映させる動作となります。
Certification-Based Replication
Write Setはすべてのノードで直列化されて全ノードに適用されるため、このように1つのWrite setで更新するデータのサイズが大きいと1つのWrite setの適用に長時間かかり、後続の処理が軽い処理であっても滞留するという問題があります。
滞留した処理はキューに入れられ実行可能となった時点で処理されますが、このキューのサイズがgcs.fc_limitを超えるとFlow Controlが発生し、gcs.fc_factorの値を下回るまで更新処理が停止してしまいます。
このような事を防止するために、可能な限り細かい単位のcommitが推奨されています。
Streaming Replicationは、この推奨動作を自動的に行うような機能です。
トランザクションをフラグメントに分割し、フラグメントごとに各ノードに更新する動作となっているため、大量のデータを更新するトランザクションであっても各ノード間におけるレプリケーション遅延を最小限に抑えられるという仕組みになっています。
ユースケース
Streaming Replicationは、これまでのレプリケーション方式を置き換えるものでは無く、短所をカバーする機能です。
そのため、利用に適するケース、適さないケースがあります。
適するケースは以下のようなものです。
長時間(大量データ)を必要とする書き込みトランザクションを実行する場合
前述の通り、従来型の同期レプリケーションでは大量データの更新を伴うような、長時間かかるトランザクションでは同期先ノードのキューが滞留しやすい要因となります。
Streaming Replicationは自動的にトランザクションを分割して同期し、ノード間で同期状態の差分を最小化することができます。
ホットレコード
アプリケーションが同じレコードをマルチノードで頻繁に更新する必要がある場合、commitを実行するまでノード間のトランザクションでは競合が起きているか判断できないため、最終的にGalera Deadlockが発生しロールバックが頻発する状況が起こります。
Streaming Replicationを使用するとトランザクション内の更新を都度全ノードに実行できます。
これにより、競合をマルチノードで検知でき、結果的にロールバックを軽減することが可能です。
例えばロックキュー、カウンタ、ジョブキューなどの仕組みで利用することができます。
検証
MariaDB Galera Clusterのセットアップについては完了している前提とします。
環境情報は以下となります。
- AWS EC2
- t2.large
- 3 node
- CentOS 7.5
- MariaDB Server 10.4.7
有効化
Streaming Replicationは以下のパラメータを設定することで有効化されます。
wsrep_trx_fragment_unit
wsrep_trx_fragment_size
wsrep_trx_fragment_unit
はトランザクションを分割したフラグメントの単位です。
bytes, events, rows, statements
から選択する事ができます。
デフォルトは bytes
となっており、トランザクションは更新データのバイトごとに分割されます。
wsrep_trx_fragment_size
はその名の通り、分割するサイズです。
デフォルトは0であり、Streaming Replicationは無効化されています。
1 2 3 4 5 6 7 |
MariaDB [(none)]> show variables like 'wsrep_trx_fragment%'; +-------------------------+-------+ | Variable_name | Value | +-------------------------+-------+ | wsrep_trx_fragment_size | 0 | | wsrep_trx_fragment_unit | bytes | +-------------------------+-------+ |
wsrep_trx_fragment_sizeにフラグメントのサイズを指定することで有効となります。
1 |
MariaDB [(none)]> set session wsrep_trx_fragment_size = 1 * 1024 * 1024; |
上記では1MBごとに更新データが分割されます。
動作確認
試しに簡単なテーブルで動作を確認してみます。
まずはダミーデータを作成します。
1行約10KBのデータが10万行格納された、1GB程度のCSV(dummy.csv)とします。
1 2 |
$ data=$(for i in {1..2000};do echo -n a;done) $ for i in {1..100000};do echo $data,$data,$data,$data,$data >> dummy.csv;done |
以下のスクリプトを使用してテーブルの作成とcsvのロードを行います。
load.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#!/bin/bash [ -z ${2} ] && SET_SESSTION="" || SET_SESSION="SET SESSION wsrep_trx_fragment_size = ${2}*1024;" mysql <<-EOS -- setup table CREATE DATABASE IF NOT EXISTS test; DROP TABLE IF EXISTS test.t; CREATE TABLE test.t (i INT PRIMARY KEY AUTO_INCREMENT, t1 TEXT, t2 TEXT, t3 TEXT, t4 TEXT, t5 TEXT ) ENGINE INNODB; -- set streaming replication $SET_SESSION -- start transaction BEGIN; LOAD DATA LOCAL INFILE "$1" INTO TABLE test.t FIELDS TERMINATED BY ',' (t1,t2,t3,t4,t5); COMMIT; EOS |
load.shを実行します。
1 |
$ ./load.sh dummy.csv 1024 # 1MBごとにフラグメントを生成します |
他のノードでmysql.wsrep_streaming_logを確認すると、ストリーミングされたフラグメントが格納されています。
1 2 3 4 5 6 7 8 |
$ mysql -e "select node_uuid, trx_id, seqno, flags from mysql.wsrep_streaming_log;" node_uuid trx_id seqno flags 0cea0a20-d393-11e9-a739-e31ea12165d2 363 101 1 0cea0a20-d393-11e9-a739-e31ea12165d2 363 102 0 0cea0a20-d393-11e9-a739-e31ea12165d2 363 103 0 0cea0a20-d393-11e9-a739-e31ea12165d2 363 104 0 0cea0a20-d393-11e9-a739-e31ea12165d2 363 105 0 |
※結果が長くなるため上記に含めませんでしたが、flag列に実際の更新処理内容が格納されます。
フラグメントは伝播されていますが、COMMITが実行されるまでは他のノード、他のセッションから見えることはありません。
1 2 3 4 5 6 |
MariaDB> select count(*) from test.t; +----------+ | count(*) | +----------+ | 0 | +----------+ |
Commit遅延のケースについての確認
まずは、先程のload.shを使用して従来のレプリケーション方式でロードを行い、一方で細かいトランザクションを大量に実行し、トランザクションの様子を確認します。
session1
1 |
node1$ while :; ./load.sh dummy.csv; sleep 0.5;done |
session2
1 2 3 4 5 |
node1$ mysql -uroot -e "CREATE DATABASE sbtest" node1$ sysbench --tables=1 --table-size=100 --mysql-user=root --time=999 oltp_write_only prepare node1$ sysbench --threads=8 --tables=1 --table-size=100 --mysql-user=root --time=999 oltp_write_only run : Threads started! |
以下のクエリでしばらく実行状況を確認すると、先行トランザクションのCommit完了待ちとなっているセッションが確認できると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
node1$ while :;do mysql <<-EOS SELECT id, user, host, db, command, time, state FROM INFORMATION_SCHEMA.PROCESSLIST WHERE state LIKE '%commit%' AND user != 'system user' AND time > 1"; EOS sleep 0.5 done : +------+------+-----------+--------+---------+------+-----------------------------------------+ | id | user | host | db | command | time | state | +------+------+-----------+--------+---------+------+-----------------------------------------+ | 5167 | root | localhost | sbtest | Execute | 40 | Waiting for prior transaction to commit | +------+------+-----------+--------+---------+------+-----------------------------------------+ |
このように、LOAD DATA INFILEによるCOMMIT後のCertificationに要した時間に引きずられるトランザクションが発生します。
Streaming ReplicationによりCommit遅延が軽減されていることの確認
先程と同じパターンでStreaming Replicationを使用して状態を確認します。
1GBのデータを1MBのフラグメントに分割します。
1 |
node1$ while :;do ./load.sh dummy.csv 1024; sleep 0.5;done |
するとCommit待ちのセッションは存在したものの、最大11秒の待機にとどまりました。
1 2 3 4 5 6 |
: +------+------+-----------+--------+---------+------+-----------------------------------------+ | id | user | host | db | command | time | state | +------+------+-----------+--------+---------+------+-----------------------------------------+ | 5172 | root | localhost | sbtest | Execute | 11 | Waiting for prior transaction to commit | +------+------+-----------+--------+---------+------+-----------------------------------------+ |
さらに100KBとしてみると全くCommit待ちは発生しない状況となりました。
1 |
node1$ while :;do ./load.sh dummy.csv 100; sleep 0.5;done |
フラグメントのサイズの適正値は環境ごとのI/O性能により異なりますが、今回使用した環境では100KB程度のCommitでは1秒を超える時間を要さないと言えます。
Streaming Replicationの注意点
以下のような注意点があります。
ストリーミングされた操作はmysql.wsrep_streaming_logに書き込まれる
Streaming Replicationを有効化したトランザクションのログはすべてのノードのmysql.wsrep_streaming_logテーブルに書き込まれます。
これは再起動後のトランザクションの一貫性を担保するためですが、通常のレプリケーションと比較してI/Oアクティビティが増加します。
そのためセッションレベルで有効化することが推奨されています。
ロールバック処理も全ノードで実行される
Streaming Replicationではトランザクション中のDMLをフラグメントごとにリアルタイムに実行します。
そのため、ロールバックの必要が発生した場合も、ローカルノードだけではなくリモートノードでも実行する必要が発生します。
したがって、長時間の処理の実行後にロールバックされるケースが頻繁にある場合、Streaming Replicationは向いていません。
フラグメントが作成された時点で該当の行はロックが取られます
従来のレプリケーション方式では、commitを発行するまでトランザクションの競合(Galera Deadlock)はわかりませんでした。
Streaming Replicationではフラグメントは常にリモートノードで実行され続けるので、フラグメントごとにロックが取得されます。
これはメリットでもありますが、動作上の違いとしてご注意ください。
session1(node1)
1 2 3 4 |
MariaDB [test]> SET SESSION wsrep_trx_fragment_unit='statements'; MariaDB [test]> SET SESSION wsrep_trx_fragment_size=1; MariaDB [test]> begin; MariaDB [test]> update tt set i = 6 where i = 3; |
session2(node2)
1 2 3 |
MariaDB [test]> begin; MariaDB [test]> update tt set i = 6 where i = 3; -- 待機 |
まとめ
Streming Replicationを使用することで、これまでレプリケーション遅延の原因となっていたようなトランザクションも、簡単にGaleraに合わせて最適化する事ができるようになりました。
利用方法も非常に簡単ですので、MariaDB 10.4系をご利用のGalera Clusterユーザの皆様は導入を検討してはいかがでしょうか。