三、03.30 Tomcat 假死后續——C3P0 連接池參數配置問題
昨晚上正在看有關 B+Tree 相關的內容,收到業務組的微信消息:
最帥氣的大龍龍:現場數據庫連接不上,他們排查問題,懷疑與連接池或者日志有關系,最后發現從昨天下午到現在產生 30 萬條日志,其中我們就有 22 萬條,明天查一下我們服務 @琦小蝦
好吧,那就和師父一起查問題好了。第二天早上,果然數據庫組的同事過來和我們說了說情況,說現場傳來的具體情況:現場忽然之間所有業務都不能連接 Oracle,后來查詢了下原因,看到 Oracle 的監聽日志過大,導致所有業務不能連接數據庫。后來通過某些手段打開 Oracle 監聽日志 (listener.log),發現總共產生了 30 萬條日志,我們業務組相關的日志占了 20+ 萬條。所以建議我們檢查一下數據庫連接池相關的參數。
注:Oracle 監聽日志文件過大導致無法數據庫無法連接的相關問題參考連接:
《ORACLE的監聽日志太大,客戶端無法連接 BUG:9879101》
《ORACLE清理、截斷監聽日志文件(listener.log)》
數據庫組老大專門來我師父的機器上的 C3P0 的數據庫連接池相關參數,大佬感覺沒什么問題(然而是個小坑)。那是為什么呢?最好的方法還是調過來開發環境的 Oracle 監聽日志看看吧。
經過我一番猛如虎的操作,我們把日志分析的準備工作做好了:
- 安裝 XShell 用 sftp 連接 Oracle 所在的 CentOS 服務器,把數據庫監聽日志 listener.log 宕到本機;
- 監聽日志記錄了兩個月的日志信息,大小大概有 5 個多 G;記事本與 NotePad 都不能打開這么大的日志文件;
- 由于不能連接外網下載第三方工具,我在網上找了個 Java 方法,用 NIO 的方法把 5G 的日志文件分成了 200 個文件,這樣就可以進行分析了。
public static void splitFile(String filePath, int fileCount) throws IOException {
FileInputStream fis = new FileInputStream(filePath);
FileChannel inputChannel = fis.getChannel();
final long fileSize = inputChannel.size();
long average = fileSize / fileCount;//平均值
long bufferSize = 200; //緩存塊大小,自行調整
ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.valueOf(bufferSize + "")); // 申請一個緩存區
long startPosition = 0; //子文件開始位置
long endPosition = average < bufferSize ? 0 : average - bufferSize;//子文件結束位置
for (int i = 0; i < fileCount; i++) {
if (i + 1 != fileCount) {
int read = inputChannel.read(byteBuffer, endPosition);// 讀取數據
readW:
while (read != -1) {
byteBuffer.flip();//切換讀模式
byte[] array = byteBuffer.array();
for (int j = 0; j < array.length; j++) {
byte b = array[j];
if (b == 10 || b == 13) { //判斷\n\r
endPosition += j;
break readW;
}
}
endPosition += bufferSize;
byteBuffer.clear(); //重置緩存塊指針
read = inputChannel.read(byteBuffer, endPosition);
}
}else{
endPosition = fileSize; //最后一個文件直接指向文件末尾
}
FileOutputStream fos = new FileOutputStream(filePath + (i + 1));
FileChannel outputChannel = fos.getChannel();
inputChannel.transferTo(startPosition, endPosition - startPosition, outputChannel);//通道傳輸文件數據
outputChannel.close();
fos.close();
startPosition = endPosition + 1;
endPosition += average;
}
inputChannel.close();
fis.close();
}
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
splitFile("/Users/yangpeng/Documents/temp/big_file.csv",5);
long endTime = System.currentTimeMillis();
System.out.println("耗費時間: " + (endTime - startTime) + " ms");
}
好吧,終于可以分析日志了。
既然所有業務組都在和這個 Oracle 連接,那么就統計一下幾個流量比較大的服務的 IP 出現頻率吧。隨手打開分割的 200 個中隨便一個日志 (25M),首先用 NotePad 統計了一下所有業務都會訪問的共享數據 DAO 服務 IP,總共 300+ 的頻率。
我們業務 DAO 服務幾個 IP 的頻率呢?52796 + 140293 + 70802 + 142 = 264033 次……
文件里看到了滿屏熟悉的 IP…… 沒錯,這些 IP 就是我們曾經以及正在運行過 DAO 服務的四臺主機 IP 地址…… (其中 28.1.25.91 就是區區在下臭名昭著的開發機 IP 地址)
...
22-MAR-2019 13:23:41 * (CONNECT_DATA=(SERVICE_NAME=DBdb)(CID=()(HOST=SC-201707102126)(USER=Administrator))) * (ADDRESS=(PROTOCOL=tcp)(HOST=28.1.25.91)(PORT=53088)) * establish * DBdb * 0
22-MAR-2019 13:23:41 * (CONNECT_DATA=(SERVICE_NAME=DBdb)(CID=()(HOST=SC-201707102126)(USER=Administrator))) * (ADDRESS=(PROTOCOL=tcp)(HOST=28.1.25.91)(PORT=53088)) * establish * DBdb * 0
...
“完了,這次真的要背血鍋了。哈哈哈哈哈?!?/strong>這是我第一反應,發現了服務的坑,我竟然這么興奮哇哈哈哈哈~
隨手選了一個文件就是這樣,檢查了一下其他分割的日志文件,也全都是這種情況。但為什么這個日志文件里,我們四個不同的服務地址總共出現了 26 萬次 IP 地址,其中一個只有 142 次,和其他三個 IP 頻率差了這么多?
原來這個 142 次的 IP 是我師父的開發機 IP,而且他自己也說不清楚什么時候出于什么思考,把數據庫的連接池給改小了(就是數據庫組大佬親自檢查后說沒有問題的那組參數),然而我和其他小伙伴的 DAO 服務 C3P0 參數沒有改過,所以只有我們三臺服務的 IP 地址顯得那么不正常。哈哈哈我師父果然是個心機 BOY~
注:關于 C3P0 參數設置的相關內容筆者總結到了另一篇博客里:《C3P0 連接池相關概念》
之前的 C3P0 參數是這樣的:
# unit:ms
cpool.checkoutTimeout=60000
cpool.minPoolSize=200
cpool.initialPoolSize=200
cpool.maxPoolSize=500
# unit:s
cpool.maxIdleTime=60
cpool.maxIdleTimeExcessConnections=20
cpool.acquireIncrement=5
cpool.acquireRetryAttempts=3
修改后的 C3P0 參數是這樣的:
# unit:ms
cpool.checkoutTimeout=5000
cpool.minPoolSize=5
cpool.maxPoolSize=500
cpool.initialPoolSize=5
# unit:s
cpool.maxIdleTime=0
cpool.maxIdleTimeExcessConnections=200
cpool.idleConnectionTestPeriod=60
cpool.acquireIncrement=5
cpool.acquireRetryAttempts=3
04.21 記:cpool.maxIdleTimeExcessConnections=200 依舊是個大坑,后續解析。
可以看出來,差距最大的幾個參數是:
- cpool.checkoutTimeout: 60000 -> 5000 (單位 ms)
- cpool.minPoolSize: 200 -> 5
- cpool.initialPoolSize: 200 -> 5
- cpool.maxIdleTime: 60 -> 0 (單位 s)
所以,可以總結現場出現的現象如下:
- 我們的 DAO 服務由于設置了 initialPoolSize 的值為 200,所以 DAO 服務在一開始啟動的時候,就已經和 Oracle 建立了 200 個連接;
- 由于服務大部分時間都不會有太多人使用,所以運行過程中每超過 maxIdleTime 的時間即 60 秒后,沒有被使用到的數據庫連接被釋放。一般釋放的連接數量大約會在 195 ~ 200 個左右;
- 剛剛釋放了大量的數據庫連接(數量計作 size),由于 minPoolSize 設置為 200,所以立即又會發起 size 個數據庫連接,使數據庫連接數量保持在 minPoolSize 個;
- 每 60s (maxIdleTime) 重復 2~3 步驟;
所以現場的 Oracle 的監聽日志也會固定每 60 秒 (maxIdleTime) 添加約 200 條,運行了一段時間后,就出現了 Oracle 監聽日志過大(一般情況下指一個 listener.log 監聽文件大于 4G),Oracle 數據庫無法被連接的情況。
所以,前面三月六日我發現的大量出現 1521 端口的 TIME_WAIT,就應該是 DAO 服務端檢測到有 200 個空閑連接,便為這些連接向數據庫發送關閉請求,然后這些連接在等待 maxIdleTime 時間的過程中就進入了 TIME_WAIT 狀態。釋放這些連接后由于 minPoolSize 設置值為 200,所以又重新發起了約 200 個新的數據庫連接。所以我如果在 cmd 中隨一定時間周期 (每 60s) 輸入 netstat -ano
| findstr "1521" 的指令,列出來的與數據庫 1521 端口應該是變動的。
至此,數據庫連接池的問題應該是解決了。但我認為服務假死問題應該不是出在這里。目前懷疑的問題,有因為虛擬機開啟了 -XDebug, -Xrunjdwp 參數,也可能是由于我們使用線程池的方式有誤。還是需要繼續進一步檢查啊。
四、04.15 100 插入并發假死問題——C3P0 連接池參數配置問題
參考地址:
《c3p0 不斷的輸出debug錯誤信息》
很長一段時間里,在忙一些其他雜事,沒有時間開發。終于把雜事忙完之后,筆者和師父在修正了 C3P0 參數之后,開始嘗試測試并發性能。
用 LoadRunner 寫了一個腳本,同時 50 個用戶并發插入一條數據,無思考時間的插入一分鐘。腳本跑起來之后,很快服務就出現了問題。
首先,DAO 服務直接完全假死。而且由于筆者在虛擬機參數中添加了 -XX:+PrintGCDetails 參數,觀察到打印出來的 GC 日志,竟然有一秒鐘三到四次的 FullGC!而且虛擬機的舊生代已經完全被填滿,每次 FullGC 幾乎完全沒有任何的釋放。此外,DAO 服務也會偶爾報出 OutofMemoryError,只是沒有引起虛擬機崩潰而已。
當然,軟件服務也由于大量的插入無響應,報出了大量的 Read Time Out 錯誤。
開始分析問題的時候,筆者也是一臉懵逼。打開 JVisualVM 監控 Java 堆,反復試了多次,依舊是長時間的內存不釋放的現象。正當有一次對著 JVisualVM 監控畫面發呆,發呆到執行并發腳本幾分鐘之后,忽然我看到有一次 FullGC 直接令 Java 堆有了一次斷崖式的下降,堆內存直接下降了 80%??!
我當時就意識到這就是問題的突破點。所以由重新跑了一次并發腳本復現問題。再次卡死時,我用 jmap 指令把堆內存 Dump 下來,加載到前幾天準備好的 Eclipse 插件 Memory Analyse Tool (MAT) 中進行分析。
果然看到了很異常的 HeapDump 餅圖:1.5G 的堆內存,有 70%-80% 的容量都在存著一個名為 newPooledConnection 的對象,這種對象的數量大概有 60 個,每個對象大小 20M 左右。這個對象是在 c3p0 的包里,所以用腳指頭想就知道,肯定是我們的 C3P0 配置還有問題。
查了一下 C3P0 的配置參數,觀察到有一條信息:
- maxIdelTimeExcessConnections: 這個配置主要是為了快速減輕連接池的負載,比如連接池中連接數因為某次數據訪問高峰導致創建了很多數據連接,但是后面的時間段需要的數據庫連接數很少,需要快速釋放,必須小于 maxIdleTime。其實這個沒必要配置,maxIdleTime 已經配置了。
而此時我看了一眼我們的 C3P0 參數,有這樣兩個參數:
cpool.maxIdleTime=0
cpool.maxIdleTimeExcessConnections=200
所以由于 cpool.maxIdleTimeExcessConnections=200 這個參數,在并發發生之后,C3P0 持續持有并發后產生的數據庫連接,直到 200s 之內沒有再復用到這些連接,才會將其釋放。所以我之前發呆后忽然的斷崖式內存釋放,肯定就是因為這個原因……
果然把 maxIdleTime, maxIdleTimeExcessConnections 都設置為 0,并發插入立即變得順滑了很多。
至此,DAO 服務最重要的問題找到,對它的優化過程基本告一段落。但我們的服務依舊有很多待優化的點,也有很多業務邏輯可以優化,這是后面一段時間需要考慮的問題。
未完待續。下篇《服務假死問題解決過程實記(三)——緩存問題優化》