Read/Write Split環境で更新直後の最新データを取得する ~ProxySQLのnext_query_flagINを活用~

目次

はじめに

ProxySQLは、MySQLとアプリケーションの間に配置することで、接続先の制御やクエリルーティング、コネクションプーリングなどを行うことができるプロキシソフトウェアです。
ProxySQLで利用される代表的な構成の一つに、Read/Write Splitがあります。
Read/Write Splitでは、更新処理をPrimary、参照処理をReplicaへ振り分けることで、Primaryへの負荷を分散できます。

  • 更新処理の場合 : INSERT / UPDATE / DELETE → Primary
  • 参照処理の場合 : SELECT → Replica

一方、PrimaryとReplica間で非同期レプリケーションを利用している場合、Primaryへデータを書き込んだ直後にSELECTを実行すると、Replicaへの反映が間に合っておらず、最新のデータを取得できない場合があります。

「更新した直後は、必ず最新のデータを取得したい」――このような要件がある場合、わずかな反映遅延が問題になることがあります。

本記事では、このようなRead/Write Split環境における課題に対して、ProxySQLのnext_query_flagINを利用し、INSERT直後のSELECTをPrimaryへ振り分ける方法を検証します。

検証環境

今回は、Primary 1台、Replica 1台のMySQLレプリケーション環境の前段にProxySQLを配置します。
使用するバージョンは以下のとおりです。

  • MySQL:9.7.2
  • ProxySQL:3.0.11

構成は以下のとおりです。

ProxySQLでは、Writer用のHostgroupを10、Reader用のHostgroupを20として設定します。
サーバーの設定を確認します。

Writer HostgroupとReader Hostgroupの設定も確認します。

今回はnext_query_flagINの挙動を分かりやすく確認するため、INSERTをWriter Hostgroup、SELECTをReader Hostgroupへ明示的に振り分けるシンプルなクエリルールを使用します。
まず、INSERTをWriter Hostgroupへ振り分けるルールを設定します。

続いて、SELECTをReader Hostgroupへ振り分けます。

設定後、クエリルールを反映します。

反映されたクエリルールを確認します。

この設定では、以下のようにルーティングされます。

なお、今回は検証を分かりやすくするために単純化したクエリルールを使用しています。
実際の環境では、実行されるクエリやトランザクションの扱いなども考慮し、ワークロードに合わせてクエリルールを設計する必要があります。

Read/Write Splitの挙動確認

まずは、通常のRead/Write Splitが想定どおり動作していることを確認します。

検証には以下のitemsテーブルを使用します。

ProxySQL経由でデータを登録します。

続けて、登録したデータを確認します。

あわせて、ProxySQLのAdmin Interfaceから、それぞれのクエリがどのHostgroupへルーティングされたか確認します。

実行結果から、INSERTはWriter Hostgroupの10、SELECTはReader Hostgroupの20へ正しく振り分けられていることが確認できます。

Read/Write Splitの限界

Read/Write Splitでは、参照処理をReplicaへ分散できる一方で、レプリケーション遅延などによってPrimaryとReplica間のデータ反映に時間差が生じると、問題になる場合があります。
例えば、INSERT直後にSELECTを実行するケースです。

  1. INSERT → Primaryへ書き込み
  2. 直後にSELECT → Replicaから読み取り

PrimaryへのINSERTが完了していても、その時点でReplicaへの反映まで完了しているとは限りません。
そのため、直後のSELECTがReplicaへ振り分けられると、INSERTしたばかりのデータを取得できない可能性があります。

実際にレプリケーション遅延が起きている際の挙動を確認してみましょう。
今回はこの状態を再現するため、ReplicaのSQL Threadを一時的に停止します。
Replicaへ接続し、以下を実行します。

この状態で、ProxySQL経由で新しいデータを登録します。

続けてSELECTを実行します。

eraserをINSERTしたにもかかわらず、SELECT結果にはpencilしか表示されていません。
ルーティング先を確認します。

INSERTはWriter Hostgroupの10、SELECTはReader Hostgroupの20へ送られています。
念のため、Primaryへ直接接続してデータを確認します。

Primaryにはeraserが正常に登録されています。

このように、Primaryへの更新が正常に完了していても、Replicaへの反映が遅れている場合、Read/Write SplitによってReplicaへ振り分けられたSELECTでは最新データを取得できないことがあります。
今回は挙動を再現するためSQL Threadを停止しましたが、実環境でも負荷やI/O、ネットワーク状況などによってReplicaへの適用が遅延した場合には、同様の状況が発生する可能性があります。

next_query_flagINによる解決

next_query_flagINとは

ProxySQLのmysql_query_rulesには、next_query_flagINという設定があります。
next_query_flagINを利用すると、あるクエリがクエリルールにマッチした際に、後続クエリがどのflagINからクエリルールの評価を開始するかを指定できます。
今回の検証では、itemsテーブルへのINSERTが実行された場合に、

を設定します。
これにより、INSERTの後に実行されるクエリは、通常のflagIN=0ではなくflagIN=100から評価されます。
さらに、flagIN=100から評価されたSELECTが対象のルールにマッチした際にnext_query_flagIN=0を設定することで、それ以降のクエリは通常のflagIN=0からの評価へ戻します。
今回の設定では、以下のような流れになります。

このように、今回のクエリルールでは、INSERT後のSELECTをPrimaryへ振り分け、そのSELECTが実行された後は通常のRead/Write Splitへ戻す構成とします。

ここで、next_query_flagINを利用する際の注意点です。
next_query_flagINによるルール評価の切り替えは、ProxySQLへのクライアント接続(セッション)単位で保持されます。
そのため、今回の構成では、INSERTとその後のSELECTを同じProxySQL接続から実行する必要があります。本検証でも同一接続上で実行しています。

なお、next_query_flagINはトランザクション単位で動作する機能ではありません。接続単位で制御される点に注意してください。
本検証では、flagIN=100のSELECTがルールにマッチした際にnext_query_flagIN=0へ戻すことで、その後は通常のクエリルール評価へ戻しています。

クエリルールの設定

先ほど設定したrule_id=10は、すべてのINSERTをWriter Hostgroupへ振り分けるルールでした。
今回はnext_query_flagINの対象をitemsテーブルへのINSERTに限定したいため、match_digestを^INSERTから^INSERT INTO itemsへ変更したうえで、next_query_flagIN=100を追加します。
一度既存のルールを削除します。

続いて、next_query_flagINを追加した状態でrule_id=10を登録し直します。

このルールでは、itemsテーブルへのINSERTをWriter Hostgroupの10へ送るとともに、next_query_flagIN=100を設定します。
続いて、flagIN=100から評価されたSELECTをWriter Hostgroupへ振り分けるルールを追加します。

このルールでは、INSERT後のSELECTをWriter Hostgroupの10へ振り分けます。
また、next_query_flagIN=0を指定することで、その後のクエリから通常のクエリルール評価へ戻します。
設定後、クエリルールを反映します。

反映された最終的なクエリルールを確認します。

最終的なルーティングは以下のようになります。

挙動確認

続いて、設定したクエリルールの挙動を確認します。
先ほどの検証で登録したeraserまではReplicaへ反映させたうえで、再度SQL Threadを停止し、これ以降に登録するデータがReplicaへ反映されない状態にしています。

まず、ProxySQL経由で新しいデータを登録します。

続けてデータを確認します。

今回は、Replicaへの反映を待たずにnotebookまで取得することができました。
あわせて、ProxySQLのAdmin Interfaceから、INSERTとSELECTのルーティング先を確認します。

INSERTだけでなく、その後のSELECTもWriter Hostgroupの10へルーティングされていることが確認できます。
念のため、ProxySQLを経由せずReplicaへ直接接続してデータを確認します。

Replicaにはまだnotebookが反映されていません。
つまり、Replicaには最新データが存在しないにもかかわらず、ProxySQL経由のSELECTでは最新データを取得できています。

ルーティング履歴からもSELECTがWriter Hostgroupへ送られているため、next_query_flagINとクエリルールの組み合わせによって、INSERT後のSELECTをPrimaryへ振り分けられていることが確認できました。

まとめ

ProxySQLのRead/Write Splitでは、参照処理をReplicaへ分散することでPrimaryの負荷を軽減できます。
一方、非同期レプリケーションではPrimaryへの更新とReplicaへの反映に時間差が生じるため、更新直後のSELECTで最新データを取得できない場合があります。
今回検証したnext_query_flagINを利用すると、直前に実行されたクエリをきっかけとして、後続クエリのクエリルール評価を切り替えることができます。
今回の検証では、Replicaに最新データが存在しない状態でも、INSERT後のSELECTをPrimaryへ振り分けることで最新データを取得できることを確認しました。
すべてのSELECTをPrimaryへ送るのではなく、必要な場面だけPrimaryへ送ることで、Read/Write Splitによる負荷分散を維持しながら、より柔軟なクエリルーティングが可能になります。

「このSQLはどこへ送るか」だけでなく、「次のSQLをどこへ送るか」まで制御する。

ProxySQLを使って、一歩踏み込んだクエリルーティングを試してみてはいかがでしょうか。

スマートスタイルTECHブログについて

スマートスタイルTECHブログでは、日頃MySQLのサポート業務に従事している有資格者で構成された技術サポートチームがMySQLに関する技術情報を発信しています。データベースのお困りごとはお気軽にご相談下さい。

よかったらシェアしてね!
  • URLをコピーしました!
目次