文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

Spring BootでMySQLのconnectorjのreplication schemeで参照クエリをreaderに向ける

リードレプリカを活用しろ警察に怒られる前に、connectorjのマルチポスト機能を学ばなくては・・・!!


f:id:treeapps:20180802010416p:plain

知らないのは私だけだと思いますが、実はconnectorjにはマルチホストのコネクションを管理する機能が内蔵されています。

マルチホストコネクションの種類

connectorjには実は以下が標準機能として存在しています。

  • Server Failover
  • Client-Side Failover when using the X Protocol
  • Load Balancing with Connector/J
  • Master/Slave Replication with Connector/J

これらの詳細については以下の公式ドキュメントをご覧下さい。
dev.mysql.com

この中で今回「Master/Slave Replication with Connector/J」をピックアップします。

Master/Slave Replication with Connector/Jってどんな機能?

名前でネタバレしてますが、マスターの場合とスレーブの場合で接続先を変更する事ができる設定方法です。

実際のユースケースとしては以下のようになります。

マスター(更新クエリ)の場合はホストAに接続
スレーブ(参照クエリ)の場合はホストBに接続

Replicationと書かれていますが、どちらかというとレプリケーション云々よりも、更新クエリと参照クエリを別々の接続先に投げてくれる、と考えるといいと思います。

何に使うの?

データベースを使う場合、特別な事情(どうしてもUDFを使いたい等)が無い限り、マネージドサービス(Amazon RDS等)を使う場合がほとんどです。

オンプレ時代とは異なり、リードレプリカ機能を使えば、職人技が必要なレプリケーション設定の必要も無く、参照専用インスタンスを画面をポチポチするだけで用意できるようになりました。更に、Amazon Auroraの登場により、writerエンドポイント・readerエンドポイントという便利なものが登場しました。

しかし、です。いくら参照専用インスタンスが有る、readerエンドポイントが有る、といってもそこに参照クエリを流さないと、完全に宝の持ち腐れになります。そこで今回誰もが使うconnectorjの標準機能を使ってそれを実現します。

MySQL RouterやMariaDB MaxScale等のミドルウェアを使わずにとりあえずreaderを活用できるので、ミドルウェアのSPOFやメンテを考えなくていいので便利ですね。機能は両者には勝てませんけどね。

Spring bootで簡単に参照クエリのみを振り分ける

今回はkotlin + Spring bootで参照クエリを参照専用ホストに流してみます。

設定自体はたった2つで完了します。

1個目の設定:application.ymlのdatasouce設定

spring:
  datasource:
    url: "jdbc:mysql:replication://127.0.0.1:3306,127.0.0.1:3307,127.0.0.1:3308/work"
    username: worker
    password: worker
    driverClassName: com.mysql.cj.jdbc.Driver

2個目の設定:@Transactional(readOnly = true)

@Transactional(readOnly = true)
fun findReaderHost(): HostModel = hostRepository.findOne()

以上で参照クエリが参照専用ホストに流れます。たったこれだけです。

では両者の設定についてもう少し確認してみましょう。

spring.datasource.url

dev.mysql.com

公式ドキュメントを見れば解りますが、一応説明すると超ざっくり以下のように設定します。

url: "jdbc:mysql:replication://${更新+参照ホスト}:${port},${参照専用ホスト1}:${port},${参照専用ホスト2}:${port}/${DB名}"

ホスト+portの組み合わせを半角カンマ区切りで設定します。master(更新+参照が可能なホスト)が1個目固定、2個目移行は全部slave(参照専用ホスト)です。

RDS+リードレプリカの場合は1個づつ設定、Auroraの場合はwriterエンドポイントを1個目、readerエンドポイントを2個目に設定するだけですね。

@Transactional(readOnly = true)

AOPで@Serviceを付けたクラスに無条件に@Transactionalを設定するケースもありますが、今回@Transactionalを手動で定義している場合の話になります。

この設定でポイントは「readOnly = true」の部分です。このreadOnlyがtrueなら、自動的にspring.datasource.urlのslaveの方に接続が流れるのです。

masterに流れるのかslaveに流れるのかは本当にここだけで決まります。insert・update・deleteだからmasterに接続されるわけではありません。(やっちゃダメですが)更新クエリをslaveにも流せますし、参照クエリをmasterに流す事もできます。

Spring bootのソースコードは追ってませんが、恐らく@Transactional(readOnly = false)を設定すると、内部で Connection.setReadOnly(true) といった事を自動でしてくれているのだと思われます。なので、他のフレームワークでは異なる指定が必要になるか、そもそもできない可能性があるかもしれません。

レプリケーションの遅延について

Auroraでの話になりますが、ストレージを共有しているAuroraでさえ、レプリケーション遅延、所謂レプリカラグは発生します。

Aurora レプリカは、Aurora DB クラスター内の独立したエンドポイントであり、読み取りオペレーションのスケーリングと可用性の向上に最適です。最大 15 個の Aurora レプリカを、1 つの AWS リージョンの中で DB クラスターが処理するアベイラビリティーゾーン全体に分散できます。DB クラスターボリュームは DB クラスターのデータの複数のコピーで構成されます。ただし、クラスターボリュームのデータは、DB クラスターのプライマリインスタンスおよび Aurora レプリカの 1 つの論理ボリュームとして表されます。

この結果、すべての Aurora レプリカは、最短のレプリカラグでクエリの結果として同じデータを返します。レプリカラグは、通常はプライマリインスタンスが更新を書き込んだ後、100 ミリ秒未満です。レプリカラグは、データベースの変更レートによって異なります。つまり、データベースに対して大量の書き込みオペレーションが発生している間、レプリカラグが増加することがあります。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/Aurora.Replication.html

readerエンドポイント使用時はこのレプリカラグを考慮する必要があります。ここで先程の「参照クエリをmasterに流す事もできます」の部分に注目します。

例えば、「参照クエリ -> 更新クエリ -> 参照クエリ」といった参照と更新が入り乱れ、すぐに更新結果が欲しい場合、readerにクエリを投げてもレプリカラグで最新データが取得できない可能性があります。ここで「敢えて参照クエリをmasterに流す」事で、レプリカラグを気にせず最新データを取得できるわけです。

auroraの100 ミリ秒未満のレプリカラグを許容できるケースは、可能な限りreaderに参照クエリを流せるといいですね。その判断がちょっと難しい可能性はありますが。

GIFアニメで動きを見てみる

実は例によって事前に専用プロジェクトを用意しています。
github.com

writerを1台、readerを2台用意しており、挙動を解りやすするため敢えてレプリケーションをせず、初期データで異なる値をinsertしておき、サーバ毎に異なる値が返るようにしています。

readerに参照クエリを投げる例

docker起動時にhostというテーブルを生成し、writerには「writer1」、reader1には「reader1」、reader2には「reader2」という値をinsertしてあります。

以下の例は、@Transactional(readOnly = true)を設定し、hostテーブルをselectした結果を連続して取得しています。writerにクエリが投げられず、readerの何れかのサーバに接続される事を確認できます。

f:id:treeapps:20190210223855g:plain

writerに参照クエリを投げる例

以下の例は、@Transactionalを設定し、hostテーブルをselectした結果を連続して取得しています。readerにクエリが投げられず、必ずwriterに接続される事を確認できます。(@TransactionalのreadOnlyは初期値がfalseなのでこの場合readOnly = falseが自動的に設定された事になります)

f:id:treeapps:20190210224424g:plain




readerのGIFアニメで気づいたと思いますが、バランシングがラウンドロビンではなくランダムです。

このバランシング設定をドキュメントから見つける事ができませんでしたが、jdbc:mysql:loadbalanceと同じであれば、デフォルト値はrandomですね。

ha.loadBalanceStrategy

If using a load-balanced connection to connect to SQL nodes in a MySQL Cluster/NDB configuration (by using the URL prefix "jdbc:mysql:loadbalance://"), which load balancing algorithm should the driver use: (1) "random" - the driver will pick a random host for each request. This tends to work better than round-robin, as the randomness will somewhat account for spreading loads where requests vary in response time, while round-robin can sometimes lead to overloaded nodes if there are variations in response times across the workload. (2) "bestResponseTime" - the driver will route the request to the host that had the best response time for the previous transaction. (3) "serverAffinity" - the driver initially attempts to enforce server affinity while still respecting and benefiting from the fault tolerance aspects of the load-balancing implementation. The server affinity ordered list is provided using the property 'serverAffinityOrder'. If none of the servers listed in the affinity list is responsive, the driver then refers to the "random" strategy to proceed with choosing the next server.

Default: random

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html

google翻訳すると以下となります。

(1) "random" - ドライバはリクエストごとにランダムなホストを選びます。

これはラウンドロビンよりもうまくいく傾向があります。ランダムさはリクエストの応答時間が異なる負荷を分散させるためですが、ラウンドロビンはワークロード全体の応答時間にばらつきがあるとノードが過負荷になることがあります。

という事で、ラウンドロビンよりランダムの方がいい結果になるぞ〜、との事で初期値がランダムなようですね。

雑感

最近会社でAuroraはごくごく普通に使用されるようになりましたが、readerを活用できているプロジェクトが少ないなあと思って調べてみると、connectorjにこんな機能が有る事を知りました。MySQL RouterとMax Scaleは知っているのにこっちを知らないという・・・autoReconnect等のオプション設定は気にするのに、スキーム節は気にした事が無かったです。

大規模プロジェクトの場合、このクエリの場合はwriterに、あのクエリの場合はreaderに、という判断ができる人とできない人が入り乱れますが、その場合どうしよう?といったルール決めが必要になりそうですね。

闇雲に使用すると、記事投稿後に何故か記事件数が増えないバグが有る〜、画面操作が速すぎると何故か最新データが取得できないんです〜、みたいな事になるので、それらの対応策を事前に検討した方が良さそうです。