接上文《服務(wù)假死問題解決過程實(shí)記(一)——問題發(fā)現(xiàn)篇》
三围详、03.30 Tomcat 假死后續(xù)——C3P0 連接池參數(shù)配置問題
昨晚上正在看有關(guān) B+Tree 相關(guān)的內(nèi)容浪册,收到業(yè)務(wù)組的微信消息:
最帥氣的大龍龍:現(xiàn)場數(shù)據(jù)庫連接不上燎斩,他們排查問題,懷疑與連接池或者日志有關(guān)系绊袋,最后發(fā)現(xiàn)從昨天下午到現(xiàn)在產(chǎn)生 30 萬條日志涤久,其中我們就有 22 萬條,明天查一下我們服務(wù) @琦小蝦
好吧趁冈,那就和師父一起查問題好了歼争。第二天早上拜马,果然數(shù)據(jù)庫組的同事過來和我們說了說情況,說現(xiàn)場傳來的具體情況:現(xiàn)場忽然之間所有業(yè)務(wù)都不能連接 Oracle沐绒,后來查詢了下原因俩莽,看到 Oracle 的監(jiān)聽日志過大,導(dǎo)致所有業(yè)務(wù)不能連接數(shù)據(jù)庫乔遮。后來通過某些手段打開 Oracle 監(jiān)聽日志 (listener.log)扮超,發(fā)現(xiàn)總共產(chǎn)生了 30 萬條日志,我們業(yè)務(wù)組相關(guān)的日志占了 20+ 萬條蹋肮。所以建議我們檢查一下數(shù)據(jù)庫連接池相關(guān)的參數(shù)出刷。
注:Oracle 監(jiān)聽日志文件過大導(dǎo)致無法數(shù)據(jù)庫無法連接的相關(guān)問題參考連接:
《ORACLE的監(jiān)聽日志太大,客戶端無法連接 BUG:9879101》
《ORACLE清理坯辩、截?cái)啾O(jiān)聽日志文件(listener.log)》
數(shù)據(jù)庫組老大專門來我師父的機(jī)器上的 C3P0 的數(shù)據(jù)庫連接池相關(guān)參數(shù)馁龟,大佬感覺沒什么問題(然而是個小坑)。那是為什么呢漆魔?最好的方法還是調(diào)過來開發(fā)環(huán)境的 Oracle 監(jiān)聽日志看看吧坷檩。
經(jīng)過我一番猛如虎的操作,我們把日志分析的準(zhǔn)備工作做好了:
- 安裝 XShell 用 sftp 連接 Oracle 所在的 CentOS 服務(wù)器改抡,把數(shù)據(jù)庫監(jiān)聽日志 listener.log 宕到本機(jī)矢炼;
- 監(jiān)聽日志記錄了兩個月的日志信息,大小大概有 5 個多 G雀摘;記事本與 NotePad 都不能打開這么大的日志文件;
- 由于不能連接外網(wǎng)下載第三方工具八拱,我在網(wǎng)上找了個 Java 方法阵赠,用 NIO 的方法把 5G 的日志文件分成了 200 個文件,這樣就可以進(jìn)行分析了肌稻。
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; //緩存塊大小清蚀,自行調(diào)整
ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.valueOf(bufferSize + "")); // 申請一個緩存區(qū)
long startPosition = 0; //子文件開始位置
long endPosition = average < bufferSize ? 0 : average - bufferSize;//子文件結(jié)束位置
for (int i = 0; i < fileCount; i++) {
if (i + 1 != fileCount) {
int read = inputChannel.read(byteBuffer, endPosition);// 讀取數(shù)據(jù)
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);//通道傳輸文件數(shù)據(jù)
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("耗費(fèi)時間: " + (endTime - startTime) + " ms");
}
好吧,終于可以分析日志了爹谭。
既然所有業(yè)務(wù)組都在和這個 Oracle 連接枷邪,那么就統(tǒng)計(jì)一下幾個流量比較大的服務(wù)的 IP 出現(xiàn)頻率吧。隨手打開分割的 200 個中隨便一個日志 (25M)诺凡,首先用 NotePad 統(tǒng)計(jì)了一下所有業(yè)務(wù)都會訪問的共享數(shù)據(jù) DAO 服務(wù) IP东揣,總共 300+ 的頻率。
我們業(yè)務(wù) DAO 服務(wù)幾個 IP 的頻率呢腹泌?52796 + 140293 + 70802 + 142 = 264033 次……
文件里看到了滿屏熟悉的 IP…… 沒錯嘶卧,這些 IP 就是我們曾經(jīng)以及正在運(yùn)行過 DAO 服務(wù)的四臺主機(jī) IP 地址…… (其中 28.1.25.91 就是區(qū)區(qū)在下臭名昭著的開發(fā)機(jī) 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
...
“完了,這次真的要背血鍋了凉袱。哈哈哈哈哈芥吟≌焱”這是我第一反應(yīng),發(fā)現(xiàn)了服務(wù)的坑钟鸵,我竟然這么興奮哇哈哈哈哈~
隨手選了一個文件就是這樣钉稍,檢查了一下其他分割的日志文件,也全都是這種情況棺耍。但為什么這個日志文件里贡未,我們四個不同的服務(wù)地址總共出現(xiàn)了 26 萬次 IP 地址,其中一個只有 142 次烈掠,和其他三個 IP 頻率差了這么多羞秤?
原來這個 142 次的 IP 是我?guī)煾傅拈_發(fā)機(jī) IP,而且他自己也說不清楚什么時候出于什么思考左敌,把數(shù)據(jù)庫的連接池給改小了(就是數(shù)據(jù)庫組大佬親自檢查后說沒有問題的那組參數(shù))瘾蛋,然而我和其他小伙伴的 DAO 服務(wù) C3P0 參數(shù)沒有改過,所以只有我們?nèi)_服務(wù)的 IP 地址顯得那么不正常矫限。哈哈哈我?guī)煾腹皇莻€心機(jī) BOY~
注:關(guān)于 C3P0 參數(shù)設(shè)置的相關(guān)內(nèi)容筆者總結(jié)到了另一篇博客里:《C3P0 連接池相關(guān)概念》
之前的 C3P0 參數(shù)是這樣的:
# 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 參數(shù)是這樣的:
# 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 依舊是個大坑哺哼,后續(xù)解析。
可以看出來叼风,差距最大的幾個參數(shù)是:
- cpool.checkoutTimeout: 60000 -> 5000 (單位 ms)
- cpool.minPoolSize: 200 -> 5
- cpool.initialPoolSize: 200 -> 5
- cpool.maxIdleTime: 60 -> 0 (單位 s)
所以取董,可以總結(jié)現(xiàn)場出現(xiàn)的現(xiàn)象如下:
- 我們的 DAO 服務(wù)由于設(shè)置了 initialPoolSize 的值為 200,所以 DAO 服務(wù)在一開始啟動的時候无宿,就已經(jīng)和 Oracle 建立了 200 個連接茵汰;
- 由于服務(wù)大部分時間都不會有太多人使用,所以運(yùn)行過程中每超過 maxIdleTime 的時間即 60 秒后孽鸡,沒有被使用到的數(shù)據(jù)庫連接被釋放蹂午。一般釋放的連接數(shù)量大約會在 195 ~ 200 個左右;
- 剛剛釋放了大量的數(shù)據(jù)庫連接(數(shù)量計(jì)作 size)彬碱,由于 minPoolSize 設(shè)置為 200豆胸,所以立即又會發(fā)起 size 個數(shù)據(jù)庫連接,使數(shù)據(jù)庫連接數(shù)量保持在 minPoolSize 個巷疼;
- 每 60s (maxIdleTime) 重復(fù) 2~3 步驟晚胡;
所以現(xiàn)場的 Oracle 的監(jiān)聽日志也會固定每 60 秒 (maxIdleTime) 添加約 200 條,運(yùn)行了一段時間后嚼沿,就出現(xiàn)了 Oracle 監(jiān)聽日志過大(一般情況下指一個 listener.log 監(jiān)聽文件大于 4G)估盘,Oracle 數(shù)據(jù)庫無法被連接的情況。
所以骡尽,前面三月六日我發(fā)現(xiàn)的大量出現(xiàn) 1521 端口的 TIME_WAIT忿檩,就應(yīng)該是 DAO 服務(wù)端檢測到有 200 個空閑連接,便為這些連接向數(shù)據(jù)庫發(fā)送關(guān)閉請求爆阶,然后這些連接在等待 maxIdleTime 時間的過程中就進(jìn)入了 TIME_WAIT 狀態(tài)燥透。釋放這些連接后由于 minPoolSize 設(shè)置值為 200沙咏,所以又重新發(fā)起了約 200 個新的數(shù)據(jù)庫連接。所以我如果在 cmd 中隨一定時間周期 (每 60s) 輸入 netstat -ano
| findstr "1521" 的指令班套,列出來的與數(shù)據(jù)庫 1521 端口應(yīng)該是變動的肢藐。
至此,數(shù)據(jù)庫連接池的問題應(yīng)該是解決了吱韭。但我認(rèn)為服務(wù)假死問題應(yīng)該不是出在這里吆豹。目前懷疑的問題,有因?yàn)樘摂M機(jī)開啟了 -XDebug, -Xrunjdwp 參數(shù)理盆,也可能是由于我們使用線程池的方式有誤痘煤。還是需要繼續(xù)進(jìn)一步檢查啊。
四猿规、04.15 100 插入并發(fā)假死問題——C3P0 連接池參數(shù)配置問題
參考地址:
《c3p0 不斷的輸出debug錯誤信息》
很長一段時間里衷快,在忙一些其他雜事,沒有時間開發(fā)姨俩。終于把雜事忙完之后蘸拔,筆者和師父在修正了 C3P0 參數(shù)之后,開始嘗試測試并發(fā)性能环葵。
用 LoadRunner 寫了一個腳本调窍,同時 50 個用戶并發(fā)插入一條數(shù)據(jù),無思考時間的插入一分鐘张遭。腳本跑起來之后邓萨,很快服務(wù)就出現(xiàn)了問題。
首先菊卷,DAO 服務(wù)直接完全假死缔恳。而且由于筆者在虛擬機(jī)參數(shù)中添加了 -XX:+PrintGCDetails 參數(shù),觀察到打印出來的 GC 日志的烁,竟然有一秒鐘三到四次的 FullGC褐耳!而且虛擬機(jī)的舊生代已經(jīng)完全被填滿诈闺,每次 FullGC 幾乎完全沒有任何的釋放渴庆。此外,DAO 服務(wù)也會偶爾報(bào)出 OutofMemoryError雅镊,只是沒有引起虛擬機(jī)崩潰而已襟雷。
當(dāng)然,軟件服務(wù)也由于大量的插入無響應(yīng)仁烹,報(bào)出了大量的 Read Time Out 錯誤耸弄。
開始分析問題的時候,筆者也是一臉懵逼卓缰。打開 JVisualVM 監(jiān)控 Java 堆计呈,反復(fù)試了多次砰诵,依舊是長時間的內(nèi)存不釋放的現(xiàn)象。正當(dāng)有一次對著 JVisualVM 監(jiān)控畫面發(fā)呆捌显,發(fā)呆到執(zhí)行并發(fā)腳本幾分鐘之后茁彭,忽然我看到有一次 FullGC 直接令 Java 堆有了一次斷崖式的下降,堆內(nèi)存直接下降了 80%7鐾帷理肺!
我當(dāng)時就意識到這就是問題的突破點(diǎn)。所以由重新跑了一次并發(fā)腳本復(fù)現(xiàn)問題善镰。再次卡死時妹萨,我用 jmap 指令把堆內(nèi)存 Dump 下來,加載到前幾天準(zhǔn)備好的 Eclipse 插件 Memory Analyse Tool (MAT) 中進(jìn)行分析炫欺。
果然看到了很異常的 HeapDump 餅圖:1.5G 的堆內(nèi)存乎完,有 70%-80% 的容量都在存著一個名為 newPooledConnection 的對象,這種對象的數(shù)量大概有 60 個竣稽,每個對象大小 20M 左右囱怕。這個對象是在 c3p0 的包里,所以用腳指頭想就知道毫别,肯定是我們的 C3P0 配置還有問題娃弓。
查了一下 C3P0 的配置參數(shù),觀察到有一條信息:
- maxIdelTimeExcessConnections: 這個配置主要是為了快速減輕連接池的負(fù)載岛宦,比如連接池中連接數(shù)因?yàn)槟炒螖?shù)據(jù)訪問高峰導(dǎo)致創(chuàng)建了很多數(shù)據(jù)連接台丛,但是后面的時間段需要的數(shù)據(jù)庫連接數(shù)很少,需要快速釋放砾肺,必須小于 maxIdleTime挽霉。其實(shí)這個沒必要配置,maxIdleTime 已經(jīng)配置了变汪。
而此時我看了一眼我們的 C3P0 參數(shù)侠坎,有這樣兩個參數(shù):
cpool.maxIdleTime=0
cpool.maxIdleTimeExcessConnections=200
所以由于 cpool.maxIdleTimeExcessConnections=200 這個參數(shù),在并發(fā)發(fā)生之后裙盾,C3P0 持續(xù)持有并發(fā)后產(chǎn)生的數(shù)據(jù)庫連接实胸,直到 200s 之內(nèi)沒有再復(fù)用到這些連接,才會將其釋放番官。所以我之前發(fā)呆后忽然的斷崖式內(nèi)存釋放庐完,肯定就是因?yàn)檫@個原因……
果然把 maxIdleTime, maxIdleTimeExcessConnections 都設(shè)置為 0,并發(fā)插入立即變得順滑了很多徘熔。
至此门躯,DAO 服務(wù)最重要的問題找到,對它的優(yōu)化過程基本告一段落酷师。但我們的服務(wù)依舊有很多待優(yōu)化的點(diǎn)讶凉,也有很多業(yè)務(wù)邏輯可以優(yōu)化染乌,這是后面一段時間需要考慮的問題。
未完待續(xù)懂讯。下篇《服務(wù)假死問題解決過程實(shí)記(三)——緩存問題優(yōu)化》