Java >> Java チュートリアル >  >> Tag >> Jdbc

JDBC 接続プールのテスト クエリ SELECT 1 が AWS RDS ライター/リーダーのフェールオーバーをキャッチしない

最初の返信から 2 か月間、私はこれについて多くのことを考えてきました...

Aurora エンドポイントの仕組み

Aurora クラスターを起動すると、クラスターにアクセスするための複数のホスト名が取得されます。この回答の目的のために、私たちが気にするのは、読み書き可能な「クラスター エンドポイント」と、(ご想像のとおり) 読み取り専用である「読み取り専用エンドポイント」の 2 つだけです。クラスター内の各ノードにもエンドポイントがありますが、ノードに直接アクセスすると、Aurora を使用する目的が損なわれるため、再度言及しません。

たとえば、「example」という名前のクラスターを作成すると、次のエンドポイントが取得されます:

  • クラスタ エンドポイント:example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
  • 読み取り専用エンドポイント:example.cluster-ro-x91qlr44xxxz.us-east-1.rds.amazonaws.com

これらのエンドポイントは、フェイルオーバー時にトラフィックをリダイレクトするほどスマートな Elastic Load Balancer のようなものを指していると思うかもしれませんが、それは間違いです。実際、それらは有効期間が非常に短い単なる DNS CNAME エントリです。

dig example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com


; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40120
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. IN A

;; ANSWER SECTION:
example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. 5 IN CNAME example.x91qlr44xxxz.us-east-1.rds.amazonaws.com.
example.x91qlr44xxxz.us-east-1.rds.amazonaws.com. 4 IN CNAME ec2-18-209-198-76.compute-1.amazonaws.com.
ec2-18-209-198-76.compute-1.amazonaws.com. 7199 IN A 18.209.198.76

;; Query time: 54 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Fri Dec 14 18:12:08 EST 2018
;; MSG SIZE  rcvd: 178

フェイルオーバーが発生すると、CNAME が更新されます (example から) example-us-east-1a まで ):

; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27191
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. IN A

;; ANSWER SECTION:
example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com. 5 IN CNAME example-us-east-1a.x91qlr44xxxz.us-east-1.rds.amazonaws.com.
example-us-east-1a.x91qlr44xxxz.us-east-1.rds.amazonaws.com. 4 IN CNAME ec2-3-81-195-23.compute-1.amazonaws.com.
ec2-3-81-195-23.compute-1.amazonaws.com. 7199 IN A 3.81.195.23

;; Query time: 158 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Fri Dec 14 18:15:33 EST 2018
;; MSG SIZE  rcvd: 187

フェールオーバー中に発生するもう 1 つのことは、「クラスター」エンドポイントへのすべての接続が閉じられ、処理中のトランザクションが失敗することです (妥当なクエリ タイムアウトを設定した場合)。

「読み取り専用」エンドポイントへの接続はしない 閉じられます。つまり、どのノードがプロモートされても、読み取り/書き込みトラフィックが加えて取得されます。 読み取り専用トラフィック (もちろん、アプリケーションがすべての要求をクラスター エンドポイントに送信するだけではないと仮定します)。読み取り専用接続は通常、比較的負荷の高いクエリ (レポートなど) に使用されるため、読み取り/書き込み操作でパフォーマンスの問題が発生する可能性があります。

問題:DNS キャッシング

フェールオーバーが発生すると、処理中のすべてのトランザクションが失敗します (ここでも、クエリ タイムアウトを設定したと仮定します)。リカバリが完了する前に接続プールが同じホストへの接続を試みるため、新しい接続も失敗する短い時間があります。私の経験では、フェイルオーバーには約 15 秒かかります。その間、アプリケーションは接続を期待すべきではありません。

その 15 秒 (またはそれくらい) の後、すべてが正常に戻るはずです。接続プールはクラスター エンドポイントへの接続を試み、それが新しい読み取り/書き込みノードの IP アドレスに解決され、すべて問題ありません。しかし、その CNAME チェーンの解決を妨げるものがある場合、接続プールが読み取り専用エンドポイントへの接続を作成し、更新操作を試みるとすぐに失敗することがあります。

OPの場合、彼はタイムアウトが長い独自のCNAMEを持っていました。したがって、クラスタ エンドポイントに直接接続するのではなく、database.example.com のようなものに接続します。 .これは、レプリカ データベースに手動でフェールオーバーする場合に便利な手法です。 Aurora ではあまり役に立たないと思います。いずれにせよ、独自の CNAME を使用してデータベース エンドポイントを参照する場合は、存続時間の値を短くする必要があります (確かに 5 秒以内です)。

私の最初の回答では、Java が DNS ルックアップを場合によっては永久にキャッシュすることも指摘しました。このキャッシュの動作は、Java のバージョンと、セキュリティ マネージャーをインストールして実行しているかどうかによって異なります (私は信じています)。 OpenJDK 8 をアプリケーションとして実行すると、JVM はすべてのネーミング ルックアップを委任し、それ自体は何もキャッシュしないように見えます。ただし、networkaddress.cache.ttl に精通している必要があります。 この Oracle ドキュメントとこの SO の質問で説明されているように、システム プロパティ。

ただし、予期しないキャッシュを削除した後でも、クラスター エンドポイントが読み取り専用ノードに解決される場合があります。この状況をどのように処理するかという問題が残ります.

あまり良くない解決策:チェックアウト時に読み取り専用テストを使用する

OP は、データベース接続テストを使用して、アプリケーションが読み取り専用ノードで実行されていることを確認することを望んでいました。これを行うのは驚くほど困難です。ほとんどの接続プール (OP が使用しているものである HikariCP を含む) は、テスト クエリが正常に実行されることを確認するだけです。それが返すものを見る能力はありません。これは、テスト クエリが失敗するために例外をスローする必要があることを意味します。

スタンドアロンのクエリだけで MySQL に例外をスローさせる方法を思いつくことができませんでした。私が思いついた最善の方法は、関数を作成することです:

DELIMITER EOF

CREATE FUNCTION throwIfReadOnly() RETURNS INTEGER
BEGIN
    IF @@innodb_read_only THEN
        SIGNAL SQLSTATE 'ERR0R' SET MESSAGE_TEXT = 'database is read_only';
    END IF;
    RETURN 0;
END;
EOF

DELIMITER ;

次に、テスト クエリでその関数を呼び出します。

select throwIfReadOnly() 

これはほとんどの場合機能します。テスト プログラムを実行すると、一連の「接続の検証に失敗しました」というメッセージが表示されましたが、その後、不可解なことに、更新クエリが読み取り専用接続で実行されました。 Hikari には、どの接続を発行したかを示すデバッグ メッセージがないため、検証に合格したかどうかを特定できませんでした。

しかし、その可能性のある問題とは別に、この実装にはさらに深刻な問題があります。問題があるという事実を隠してしまうのです。ユーザーがリクエストを送信し、レスポンスが返ってくるまで 30 秒ほど待つことがあります。 (Hikari のデバッグ ログを有効にしない限り) ログには、この遅延の理由を示すものは何もありません。

さらに、データベースにアクセスできない間、Hikari は猛烈に接続を試みています。私のシングルスレッド テストでは、100 ミリ秒ごとに新しい接続を試みます。そして、これらは実際の接続であり、間違ったホストに接続するだけです。数十または数百のスレッドを持つアプリケーション サーバーを投入すると、データベースに重大な波及効果が生じる可能性があります。

より良い解決策:ラッパー Datasource を介して、チェックアウト時に読み取り専用テストを使用します

Hikari に黙って接続を再試行させるのではなく、HikariDataSource をラップすることができます。 あなた自身の DataSource で 実装してテスト/再試行してください。これには、テスト クエリの結果を実際に確認できるという利点があります。つまり、個別にインストールされた関数を呼び出すのではなく、自己完結型のクエリを使用できるということです。また、好みのログ レベルを使用して問題を記録したり、試行の合間に一時停止したり、プールの構成を変更したりすることもできます。

private static class WrappedDataSource
implements DataSource
{
    private HikariDataSource delegate;

    public WrappedDataSource(HikariDataSource delegate) {
        this.delegate = delegate;
    }

    @Override
    public Connection getConnection() throws SQLException {
        while (true) {
            Connection cxt = delegate.getConnection();
            try (Statement stmt = cxt.createStatement()) {
                try (ResultSet rslt = stmt.executeQuery("select @@innodb_read_only")) {
                    if (rslt.next() && ! rslt.getBoolean(1)) {
                        return cxt;
                    }
                }
            }
            // evict connection so that we won't get it again
            // should also log here
            delegate.evictConnection(cxt);
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException ignored) {
                // if we're interrupted we just retry
            }
        }
    }

    // all other methods can just delegate to HikariDataSource

このソリューションには、ユーザー リクエストに遅延が生じるという問題が依然としてあります。確かに、それが起こっていることはわかっています (オンチェックアウト テストではわかりませんでした)。また、タイムアウトを導入することもできます (ループの回数を制限します)。しかし、それでもユーザー エクスペリエンスは悪くなります。

最善の (imo) ソリューション:「メンテナンス モード」に切り替える

ユーザーは非常にせっかちです。応答が返されるまでに数秒以上かかる場合、ユーザーはおそらくページをリロードしようとするか、フォームを再度送信するか、何か何かをしようとします。 それは役に立たず、害を及ぼす可能性があります.

したがって、最善の解決策は、すぐに失敗して、何かがおかしいことを彼らに知らせることだと思います。コール スタックの最上部近くのどこかに、例外に応答するコードが既にあるはずです。今は一般的な 500 ページを返すだけかもしれませんが、もう少し改善することができます:例外を見て、それが読み取り専用のデータベース例外である場合は、「申し訳ありませんが、一時的に利用できません。数分後にもう一度お試しください」というページを返します。

同時に、運用スタッフに通知を送信する必要があります。これは、通常のメンテナンス ウィンドウのフェールオーバーであるか、より深刻な問題である可能性があります (ただし、より深刻であることを知る方法がない限り、彼らを起こさないでください)。 ).


Java コード データソースで接続プールのアイドル接続タイムアウトを設定します。 1000ms前後に設定


Java タグ