解 Bug 之路 - 數據庫主從切換” 未成功”?

前言

數據庫主從切換是個非常有意思的話題。能夠穩定的處理主從切換是保證業務連續性的必要條件。今天筆者就來講講主從切換過程中一個小小的問題。

故障場景

最近線上進行主從切換,大部分應用都切過去了,但是某些應用的連接確還在老的主 (新的從) 上面。

這讓對應應用的開發百思不得其解,於是求助了筆者一探究竟。

怎麼發現的

應用開發收到 Cat 監控告警,發現這個應用 (A) 中的請求在好幾臺機器中一直穩定失敗。聯想到昨晚剛做過數據庫主從切換演練,於是上機器 netstat -anp 下,發現機器一直連的是舊的從庫!

netstat -anp | grep 1521
tcp 0 0 1.2.3.4:54100 1.1.1.1:1521 ESTABLISHED

開發感覺肯定是主從沒有切換過去導致請求失敗。乍一看,好像非常有道理的樣子。

着手調查

神馬情況?距離切換成功已經 8 個小時了,爲什麼連接還連在上面呢?於是筆者 ping 了下對應數據庫的域名:

ping db.prd
64byres from db.prd (2.2.2.2): icmp_seq=1 ttl=64 time=0.02ms


好奇怪,DNS 已經切換過去了。應用怎麼還連到老庫呢?

第一個猜想,DNS 延遲

最先想到的是主從切換到 DNS 反應過來有延遲。例如主從切換完,DNS 在 2min 後才能生效, 所以在此期間新建的連接還是到從庫。

這種情況很正常,對於這種情況需要 DBA 將舊主的連接全都殺掉即可。諮詢了下 DBA, 他們反饋他們已經把連接全部殺掉了。而且當場給我看了下數據庫的統計連接 SQL, 確實沒有對應機器的連接。這就奇怪了,應用機器上的連接是 ESTABLISHED 狀態啊!

應用大部分機器都連的是老庫!

這時候,開發向筆者反應,這個應用對應的大部分機器都是連的老庫!如果是 DNS 延遲,不可能這麼巧吧,40 多臺呢!

而且這些機器的 DNS 都是指向新庫的。

DB 沒有 kill 連接?

難道是 DBA 漏了 kill 連接的步驟?但是和他和我展示的 DB 統計信息矛盾啊。於是筆者讓 DBA 在對應老庫的機器上 netstat 了一把。發現,連接還真的存在!

netstat -anp | grep 1.2.3.4
tcp 0 0 1.1.1.1:1521 1.2.3.4:54100 ESTABLISHED

難道統計信息真的有問題?

獲取連接創建時間

爲了驗證筆者對於 DNS 延遲的猜想,就通過一些技巧來獲取這個連接的創建時間。首先
netstat -anp | grep 1.2.3.4 找出來這個連接。由於明顯是屬於應用 java 進程的,所以
直接找到進程 pid:8299

netstat -anp | grep 1521
tcp 0 0 1.2.3.4:54100 1.1.1.1:1521 ESTABLISHED

netstat -anp | grep java
abc 8299 java

既然有了進程 pid, 我們直接 cat /proc/8299/net/tcp,直接獲取到其所有的連接信息, 然後在其中 grep 1521 的 16 進制 05F1(當前機器上 1521 的連接只有一個)

...... local_address rem_address inode     ......
...... xxx:D345      xxx:05F1    23456789  ......

找到這個 socket(1.2.3.4:54100<->1.1.1.1:1521) 對應的 inode 號。
有了這個 inode 號就簡單了,我們直接

ls -all -h /proc/8299/fd | grep 23456789 (inode號)
...... Jan 29 17:43 222 -> socket:[23456789]

這麼一看,這個連接是 1 月 29 日創建的。但是主從切換的時間點確是 3 月 19 日,
這個連接已經建了 2 個月了!那麼就不可能是筆者所說的 DNS 失效問題了。因爲連接就沒有重連過。

DB 都重啓了,怎麼還有舊的連接保持?

看到這個連接創建時間,筆者第一反應,DBA 確定殺連接了嗎?問了下 DBA 有沒有可能是統計問題。DBA 聽了後,告訴筆者,他們都重啓過數據庫了,怎麼可能還有連接存在呢?看了下 DB 進程的創建時間。

ps -eo lstart,cmd | grep db進程名
Mar 19 17:52:32 2021 db進程名

從進程啓動時間來看,真的是在 3 月 19 日啓動的。而這個詭異的連接還確實屬於這個 3 月 19 日啓動的進程。這個怎麼看邏輯上都不通啊。

但是,既然 linux 的統計信息在這 (還是要先暫時認爲是靠譜的),那肯定是又有什麼其它的詭異邏輯在裏面了。

子進程繼承了父進程的連接

稍微思考了一會,筆者就找到了一種可能。父進程先新建了連接進行處理,在創建子進程 fork 的時候,子進程會繼承父進程的連接,這時候父進程退出,只保留子進程的話。就會出現連接在進程啓動之前就已經存在的詭異現象。

爲了驗證這個問題,筆者自己寫了段簡單的 C 程序,執行了一下確實如此。代碼例子爲:

main.c
......
int main(int argc,char* argv[]){
    ......
    if((client_fd = accept(sockfd,(struct sockaddr*)&remote_addr,&sin_size)) == -1){
        printf("accept error!\n");
    }
    printf("Received a connection \n");
    // 製造兩分鐘延遲,以造成上面的現象
    sleep(2 * 60);
    if(!fork()){
        // 子進程保持
        while(1){
           sleep(100000);
        }
    }else{
        // 父進程關閉連接
        close(client_fd);
    }
    return 0;
}

問了下 DBA,他們不會 kill -9 所有進程, 都是按照標準的數據庫重啓流程來操作的。kill -9 所有進程的同時並關閉這些進程所擁有的連接。
如果我們使用的商業數據庫用了上圖的機制,那就會造成前面的現象。但是由於 DB 本身保持的 session 都已經沒了,那麼這個連接在數據庫維度肯定是已經 gg 了 (這也是數據庫統計不出來的原因)。既然還保留在上面,這個連接肯定再也沒有處理過請求!不然肯定出錯了。

業務代碼邏輯

如果按照上面的論斷的話,那麼沒有執行過請求,也就不會有報錯嘍?如果按照這個邏輯的話,那豈不是隻有出現業務報錯的纔會有新的正常連接。筆者去報錯的機器看了下,既然報錯了,那肯定是執行過 SQL 了,然後觸發 Druid 丟棄連接再新建連接。
果然,一直報錯的機器上連接都連到新庫了 (但應用開發發現其它機器還是連到老庫,所以找到了我求助),而且創建時間是 3 月 29 日,而不報錯的應用的連接掛在老庫上面,挑了幾臺看一下,這些掛在老庫的連接依舊是 1 月 29 日創建的。

但爲什麼還在報錯?

既然連接都正常了 (到新庫了),爲何還在報錯呢?難道說業務代碼寫的有問題,一旦報錯,就永遠錯下去?於是筆者直接翻起了應用的源碼。其使用這個數據庫的連接用來獲取(sequence) 序列號。然後細細分析了源碼後發現。其在數據庫報錯之後沒有處理好,走了一個有問題的代碼分支,導致永遠不會再從數據庫獲取 sequence(業務代碼就不放上來了)。

爲什麼只有幾臺機器報錯?

因爲這個序列號是取一段很大的範圍到機器的內存中使用的,不耗盡之前不會執行 SQL。所以只有一些內存中序列耗盡的機器纔會運行到那一段有問題的代碼分支。

爲什麼心跳沒有檢測出來?

到這裏大家可能會疑問?沒有心跳檢測麼?確實沒有,應用採用的是 Druid 數據源,而他們使用的那個版本的 Druid 是沒有定時心跳檢測的。

主從切換到底有沒有成功呢?

主從切換當然是成功的。這從其它的應用切過去之後運行良好可以判斷出來。主從切換當中的數據庫流量損失是我們可預期的正常現象。但是,數據庫切換完之後,應用確恢復不回來,那就要仔細看看應用代碼本身有什麼問題了。

總結

數據庫主從切換是個頻繁而又重要的動作, 是保證業務連續性的必要條件。這不僅要看 DBA 的努力,還需要我們在應用層寫出健壯的代碼,才能夠讓我們的產線更加穩定。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/erW0BXg7ndeNCvWkB2LsaQ