3 回答
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 頁,但你可以做得更好一點:查看異常,如果是只讀數據庫異常,則返回“抱歉,暫時不可用,請在幾分鐘后重試”頁面。
同時,你應該向你的運維人員發送一個通知:這可能是一個正常的維護窗口故障轉移,也可能是更嚴重的事情(但不要叫醒他們,除非你有辦法知道它更嚴重)。
添加回答
舉報
