ちょっと話題の記事

MariaDB Connector/JによるAmazon RDS for Auroraの高速フェイルオーバー

2015.09.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ども、大瀧です。
Amazon Linux 2015.09RC版のリリース告知に"Amazon Aurora JDBC driver"という項目があり、調べてみるとAuroraの可用性をアップさせるナイスな機能だったのでご紹介してみたいと思います。

RDS Multi-AZの"普通の"フェイルオーバー

Amazon RDSで提供される可用性機能としてMulti-AZがあります。一般に言うActive-Standbyクラスタで、エンドポイントとなるDNSレコードのIPアドレスをアクティブノードに向ける、DNSベースの仕組みとなります。

aurora-driver01_1

以下のように、Masterノード障害を検知すると、エンドポイントのレコードをSlaveノードのIPアドレスに切り替える要領です。

aurora-driver02_1

AuroraでもCluster Endpointというエンドポイントが提供され、フェイルオーバーを実装することができます。しかし、DNSという仕組み上レコードのTTL(Time To Live)が切り替えまでの遅延時間に追加されるため、遅延を短くするのには限界があります。

MariaDB Connector/Jのフェイルオーバー

MariaDB Connector/JはMariaDBのJDBC(JavaのDBライブラリ)ドライバーです。バージョン1.2でフェイルオーバーおよび高可用性機能が追加され、サポートするクラスタタイプの一つとしてAuroraがサポートされます。

以下の動作の記述は、AWSによる正式な説明に寄るものでは無く私の独自検証による推測が多分にあります。動作を保証できるものではない情報である点、ご了承ください。また、誤りがあればご指摘ください。

MariaDB Connector/JのAurora実装では、Auroraの全ノードと常時接続しWriterノード(AuroraのMaster)をノードとの通信で判断します。Writerノードの判断はAuroraのドキュメントにあるグローバル変数innodb_read_onlyを利用し、OFFとなっているWriterノードを検索、通常時はそのノードにクエリを発行するようにドライバーが動作します。

aurora-driver03_1

クラスタはWriterノードの障害を検知するとinnodb_read_onlyを速やかに変更するので、ドライバーは新たなWriterノード検出のためにReaderノード(Auroraのリードレプリカ)を検索し、OFFのノードを新たなWriterノードとして以後のクエリ発行先を変更します。

aurora-driver04_1

こうすることでDNSに依らない、より短い時間での切り替えを実現しています。具体的なコードはGitHubで確認できます。

動作検証

動作確認環境

  • AMI: amzn-ami-hvm-2015.03.1.x86_64-gp2 *1 (ami-d5c5d1e5) (オレゴンリージョン)

サンプルのJavaプログラムを作成し、実行してみました。

aurora_sample.java

import java.sql.*;

class AuroraSample {
    public static void main (String[] args) {
        Connection conn = null;
        try {
            Class.forName("org.mariadb.jdbc.Driver");
            conn = DriverManager.getConnection(
                "jdbc:mysql:aurora://"
                + "mydb.xxxxxxxxxxxx.us-west-2.rds.amazonaws.com:3306,"
                + "mydb-us-west-2b.xxxxxxxxxxxx.us-west-2.rds.amazonaws.com:3306"
                + "/mydb?user=master&password=masterpass"
            );
        } catch (SQLException e) {
           e.printStackTrace();
        } catch (Exception e) {
           e.printStackTrace();
        }

        while(true) {
            try {
                Statement stmt = conn.createStatement();
                System.out.print(new java.util.Date());
                ResultSet rset = stmt.executeQuery("SELECT id, name FROM users");
                while ( rset.next() ) {
                    System.out.println(rset.getInt(1) + "\t" + rset.getString(2));
                }
                rset.close();
            } catch (SQLException e) {
               e.printStackTrace();
            } catch (Exception e) {
               e.printStackTrace();
            }
            try {
               Thread.sleep(1000);
            } catch (Exception e) {
               e.printStackTrace();
            }
        }
    }
}

1秒おきにSELECT文を発行するシンプルな物です。DriverManager.getConnection()に渡す接続先に、Auroraクラスタの全ノードのInstance Endpointを記述します。そのため、後からのレプリカ追加の度に接続先を変更する必要がある点には注意しましょう。また、Cluster Endpointは使用しません。コンパイル、実行するとこんな感じです。

$ sudo yum install mariadb-connector-java java-1.7.0-openjdk-devel -y
$ echo 'export CLASSPATH=/usr/share/java/mariadb-connector-java.jar:.' >> .bash_profile
$ source .bash_profile
$ javac aurora_sample.java
$ java AuroraSample
Tue Sep 01 10:21:59 UTC 20151	ryuta
Tue Sep 01 10:22:01 UTC 20151	ryuta
  :

では、Auroraクラスタでフェイルオーバーを実行してみます。

Tue Sep 01 10:08:54 UTC 20151	ryuta
Tue Sep 01 10:08:55 UTC 20151	ryuta
Tue Sep 01 10:08:56 UTC 20151	ryuta
Tue Sep 01 10:08:57 UTC 20159 01, 2015 10:08:57 午前 org.mariadb.jdbc.internal.mysql.listener.AbstractMastersSlavesListener handleFailover
警告: SQL Primary node [HostAddress{host='mydb-us-west-2a.cw5eqrkofj2u.us-west-2.rds.amazonaws.com', port=3306, type='null'}] connection fail
9 01, 2015 10:08:57 午前 org.mariadb.jdbc.internal.mysql.listener.impl.AuroraListener searchByStartName
警告: SQL Secondary node [HostAddress{host='mydb-us-west-2a.cw5eqrkofj2u.us-west-2.rds.amazonaws.com', port=3306, type='null'}] connection fail
java.sql.SQLNonTransientConnectionException: Communications link failure with primary host mydb-us-west-2a.cw5eqrkofj2u.us-west-2.rds.amazonaws.com:3306. Could not read resultset: unexpected end of stream, read 0 bytes from 4.  Driver will try to reconnect primary after 49908 milliseconds or after 29 query(s)
	at org.mariadb.jdbc.internal.SQLExceptionMapper.get(SQLExceptionMapper.java:136)
	at org.mariadb.jdbc.internal.SQLExceptionMapper.throwException(SQLExceptionMapper.java:106)
	at org.mariadb.jdbc.MySQLStatement.executeQueryEpilog(MySQLStatement.java:252)
	at org.mariadb.jdbc.MySQLStatement.execute(MySQLStatement.java:278)
	at org.mariadb.jdbc.MySQLStatement.executeQuery(MySQLStatement.java:333)
	at org.mariadb.jdbc.MySQLStatement.executeQuery(MySQLStatement.java:392)
	at AuroraSample.main(aurora_sample.java:25)
Caused by: org.mariadb.jdbc.internal.common.QueryException: Communications link failure with primary host mydb-us-west-2a.cw5eqrkofj2u.us-west-2.rds.amazonaws.com:3306. Could not read resultset: unexpected end of stream, read 0 bytes from 4.  Driver will try to reconnect primary after 49908 milliseconds or after 29 query(s)
	at org.mariadb.jdbc.internal.mysql.MySQLProtocol.getResult(MySQLProtocol.java:968)
	at org.mariadb.jdbc.internal.mysql.MySQLProtocol.executeQuery(MySQLProtocol.java:1038)
	at org.mariadb.jdbc.internal.mysql.MySQLProtocol.executeQuery(MySQLProtocol.java:1020)
	at sun.reflect.GeneratedMethodAccessor3.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.mariadb.jdbc.internal.mysql.listener.AbstractMastersListener.invoke(AbstractMastersListener.java:275)
	at org.mariadb.jdbc.internal.mysql.FailoverProxy.invoke(FailoverProxy.java:124)
	at com.sun.proxy.$Proxy0.executeQuery(Unknown Source)
	at org.mariadb.jdbc.MySQLStatement.execute(MySQLStatement.java:271)
	... 3 more
Caused by: java.io.EOFException: unexpected end of stream, read 0 bytes from 4
	at org.mariadb.jdbc.internal.common.packet.buffer.ReadUtil.readFully(ReadUtil.java:84)
	at org.mariadb.jdbc.internal.common.packet.buffer.ReadUtil.readFully(ReadUtil.java:92)
	at org.mariadb.jdbc.internal.common.packet.RawPacket.nextPacket(RawPacket.java:77)
	at org.mariadb.jdbc.internal.common.packet.SyncPacketFetcher.getRawPacket(SyncPacketFetcher.java:67)
	at org.mariadb.jdbc.internal.mysql.MySQLProtocol.getResult(MySQLProtocol.java:931)
	... 12 more
Tue Sep 01 10:08:58 UTC 20151	ryuta
Tue Sep 01 10:08:59 UTC 20151	ryuta
Tue Sep 01 10:09:00 UTC 20151	ryuta
Tue Sep 01 10:09:01 UTC 20151	ryuta
Tue Sep 01 10:09:02 UTC 20151	ryuta
Tue Sep 01 10:09:04 UTC 20151	ryuta
Tue Sep 01 10:09:05 UTC 20151	ryuta

フェイルオーバーに関する例外の出力がありつつ、例外発生の後1〜2秒で正常な出力になっていることがわかりますね。今回は人為的なフェイルオーバーのため実際の障害に比べて検知時間が短い形にはなりますが、DNSベースのTTL(AuroraのCluster Endpointレコード)が5秒なので、それよりも切り替え時間が短いと言えます。

まとめ

MariaDB Connector/JによるAmazon Auroraの高速フェイルオーバーの仕組みとサンプルをご紹介しました。Java向けの実装でしたが、仕組み自体は汎用のものですので、他のJavaフレームワークや言語への移植に期待したいところですね。

参考URL

脚注

  1. 2015.03.1でもyumリポジトリに含まれていました