亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定
已解決430363個問題,去搜搜看,總會有你想問的

JDBC 連接池測試查詢“SELECT 1”未捕獲 AWS RDS Writer/Reader

JDBC 連接池測試查詢“SELECT 1”未捕獲 AWS RDS Writer/Reader

LEATH 2021-10-27 10:19:53
我們在一個集群中運行 AWS RDS Aurora/MySQL 數據庫,其中包含一個寫入器和一個讀取器實例,其中將寫入器復制到讀取器。訪問數據庫的應用程序是使用 HikariCP 連接池的標準 java 應用程序。該池配置為"SELECT 1"在結帳時使用測試查詢。我們注意到,有時 RDS 會將寫入器故障轉移到讀取器。也可以通過單擊 AWS 控制臺中的“實例操作/故障轉移”來手動復制故障轉移。連接池無法檢測故障轉移以及它現在連接到讀取器數據庫的事實,因為"SELECT 1"測試查詢仍然成功。但是,任何后續的數據庫更新都會失敗并顯示"java.sql.SQLException: The MySQL server is running with the --read-only option so it cannot execute this statement"錯誤。似乎"SELECT 1"連接池可以檢測到它現在已連接到閱讀器,而不是測試查詢,而是使用"SELECT count(1) FROM test_table WHERE 1 = 2 FOR UPDATE"測試查詢。有沒有人遇到過同樣的問題?"FOR UPDATE"在測試查詢中使用有什么缺點嗎?是否有任何替代或更好的方法來處理 AWS RDS 集群寫入器/讀取器故障轉移?非常感謝您的幫助
查看完整描述

3 回答

?
aluckdog

TA貢獻1847條經驗 獲得超7個贊

自從我最初的回復以來的兩個月里,我一直在思考這個問題......


Aurora 端點的工作原理

當您啟動 Aurora 集群時,您會獲得多個主機名來訪問集群。就本回答而言,我們唯一關心的兩個是“集群端點”,它是讀寫的,以及“只讀端點”,它是(你猜對了)只讀的。集群中的每個節點也有一個端點,但直接訪問節點違背了使用 Aurora 的目的,因此我不再贅述。


例如,如果我創建一個名為“example”的集群,我將獲得以下端點:


集群端點: example.cluster-x91qlr44xxxz.us-east-1.rds.amazonaws.com

只讀端點: example.cluster-ro-x91qlr44xxxz.us-east-1.rds.amazonaws.com

您可能認為這些端點指的是彈性負載均衡器之類的東西,它足夠智能,可以在故障轉移時重定向流量,但您錯了。事實上,它們只是具有非常短的生存時間的 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

故障轉移期間發生的另一件事是所有與“集群”端點的連接都關閉,這將使任何進程內事務失?。僭O您設置了合理的查詢超時)。


與“只讀”端點的連接不會關閉,這意味著任何被提升的節點都將獲得除只讀流量之外的讀寫流量(當然,假設您的應用程序不只是發送對集群端點的所有請求)。由于只讀連接通常用于相對昂貴的查詢(例如,報告),這可能會導致讀寫操作的性能問題。


問題:DNS 緩存

當故障轉移發生時,所有進程內事務都將失敗(同樣,假設您已設置查詢超時)。任何新連接也會在短時間內失敗,因為連接池在完成恢復之前嘗試連接到同一主機。根據我的經驗,故障轉移大約需要 15 秒,在此期間您的應用程序不應期望獲得連接。


大約 15 秒后,一切都應該恢復正常:您的連接池嘗試連接到集群端點,它解析為新讀寫節點的 IP 地址,一切正常。但是,如果有任何因素阻止解析該 CNAME 鏈,您可能會發現您的連接池與只讀端點建立連接,一旦您嘗試更新操作,該端點就會失敗。


在 OP 的情況下,他有自己的 CNAME,超時時間更長。因此,他不會直接連接到集群端點,而是連接到類似database.example.com. 在您手動故障轉移到副本數據庫的世界中,這是一項有用的技術;我懷疑它對 Aurora 的用處不大。無論如何,如果您使用自己的 CNAME 來引用數據庫端點,則需要它們具有較短的生存時間值(當然不超過 5 秒)。


在我最初的回答中,我還指出 Java 會緩存 DNS 查找,在某些情況下會永遠緩存。此緩存的行為取決于(我相信)Java 版本,以及您是否在安裝安全管理器的情況下運行。隨著 OpenJDK 8 作為應用程序運行,JVM 似乎將委托所有命名查找,而不是自己緩存任何內容。但是,您應該熟悉networkaddress.cache.ttl系統屬性,如this Oracle doc和this SO question中所述。


但是,即使在您消除了任何意外緩存之后,仍有可能將集群端點解析為只讀節點。這就留下了你如何處理這種情況的問題。


不太好的解決方案:在結帳時使用只讀測試

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 頁,但你可以做得更好一點:查看異常,如果是只讀數據庫異常,則返回“抱歉,暫時不可用,請在幾分鐘后重試”頁面。


同時,你應該向你的運維人員發送一個通知:這可能是一個正常的維護窗口故障轉移,也可能是更嚴重的事情(但不要叫醒他們,除非你有辦法知道它更嚴重)。


查看完整回答
反對 回復 2021-10-27
?
侃侃爾雅

TA貢獻1801條經驗 獲得超16個贊

在 Java 代碼數據源中設置連接池空閑連接超時。設置在 1000 毫秒左右


查看完整回答
反對 回復 2021-10-27
  • 3 回答
  • 0 關注
  • 416 瀏覽
慕課專欄
更多

添加回答

舉報

0/150
提交
取消
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號