這篇文章是寫給有緣人的,為什麼這麼說呢,因為本篇主要講講資料庫連線池之c3p0-0.9.1.2版本。
年輕的朋友,可能沒怎麼聽過c3p0了,或者也僅限於聽說,這都很正常,因為c3p0算是200幾年時比較流行的技術,後來,作者消失了好幾年,12年重新開始維護,這時候已經出現了很多第二代執行緒池了,c3p0已經不佔優勢,就這樣,又維護了幾年,直到19年徹底停止更新。
看下其版本歷史吧,一開始的maven座標是這樣的:
<!-- https://mvnrepository.com/artifact/c3p0/c3p0 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
</dependency>
07年發了最後一個版本c3p0-0.9.1.2:
再下一個版本是2012年的0.9.2-pre2-RELEASE,來到了2012年,座標改成了:
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
</dependency>
後續的更新版本如下:
可以看到,維護到15年後,又消失了幾年,直到19年又重新維護了一年,然後就再無動靜。
所以,為啥我覺得還是可以講講c3p0-0.9.1.2這個版本呢,因為據說當年還是比較火的,很多那時候的專案都用了這個版本,然後就一直再沒有升級(想升也沒得升啊),所以,我估計,如果那些老專案還在維護的話,估計有不少有緣人還在和這個c3p0-0.9.1.2打交道,我,就是其中一個。
在一些求穩的行業,線上能跑的專案,那肯定是沒人會去大動的,只會不斷地添磚加瓦,而這也導致更難大動,如果沒被重構掉的話,就遺留到了現在。
我現在手裡的維護的一個專案,就是用的這個框架,而且,它很容易有bug,不信的話,搜尋看看:
本文,就打算來講講我遇到的問題和這個框架的0.9.1.2版本的大概的原始碼邏輯。
我目前手裡這套服務的程式碼框架應該是0幾年誕生的,不是市面上曾經流行的框架,如struts、spring mvc那些,而是c++開發的類比netty、servlet容器的東西,在監聽埠收到使用者端請求後,能根據請求中的功能id來反向呼叫對應的java程式碼,還是有點東西的。而java程式碼裡也是一套框架,框架原始碼還失傳了,框架里程式碼定死了用c3p0這個來建立資料庫連線池,導致我想換也不好換,比較費勁。
業務層呢,託了jdbc規範的福,就是隻和jdbc的api打交道,比如找datasource拿connection,這個拿,一般也就是從連線池裡面取,用完了,再呼叫connection.close(內部會把連線再還回連線池)。
所以,我們線上到底有啥問題呢?具體表現就是,業務會突然在某個時刻,呼叫datasource.getConnection的時候,取不到連線,直接超時,而且是全部的業務請求都出這個問題,這時候,服務基本就hang死了,前端一直轉圈。
這個是完全隨機的,不定時地炸,每次炸了後,就要靠運維同事重啟服務,重啟後,服務就好了。
下面來說說定位的過程吧,現在其實也沒找到根本原因,只是有瞭解決的辦法和一些猜測,可以等下次再出現的時候,驗證一下。
剛開始的時候,線上服務只有紀錄檔,而且只有error紀錄檔,那基本看不出個啥,就是大片大片的等待從連線池獲取連線,最後直到超時都獲取不到的報錯。
當時苦於沒有其他手段,又是偶現,也看不出個啥,找dba了也看過db,dba表示執行穩定,當然,dba說的也不一定準,反正是沒收穫。
後來,2月份的時候,搞了個指令碼,服務出現問題的時候,先執行下指令碼,列印下jstack、jmap、netstat、top等一些東西,而一開始的時候,運維經常忘記執行,直接就重啟了,於是只能等下次,直到2月底的某一天吧,總算是執行了下指令碼,拿到了jmap等資訊。
分析jmap,個人習慣用MAT。MAT支援object query language語言進行堆物件查詢,具體語法可以自己學一學。
我就如下圖所示,查詢連線池的情況,我這邊有多資料來源,所以有多個連線池,其中有問題的那個連線池,池子裡維護的連線有40個:
這裡有必要說一下,這個managed:
/* keys are all valid, managed resources, value is a PunchCard */
HashMap managed = new HashMap();
這個hashmap,就是連線池。
那麼,它是怎麼初始化的呢,以下面的引數舉例:
<property name="minPoolSize">10</property>
<property name="maxPoolSize">50</property>
在BasicResourcePool的建構函式中,就會呼叫如下方法:
//start acquiring our initial resources
ensureStartResources();
private void ensureStartResources()
{ recheckResizePool(); }
具體就會呼叫:
private void expandPool(int count)
{
for (int i = 0; i < count; ++i)
taskRunner.postRunnable( new AcquireTask() );
}
c3p0會計算出,需要建10個連線出來,上面的count就是10,那麼會new 10個runnable,提交給執行緒池執行,在每個執行緒執行時:
private void doAcquire() throws Exception
{
// 1 交給具體的manager去獲取底層連線
Object resc = mgr.acquireResource();
...
// 2 拿到連線後,維護到池子裡
assimilateResource(resc);
}
這裡的mgr,負責具體去建立資料庫連線,由於涉及到多種資料庫,因此mgr就負責具體髒活累活,連線池這邊就不和這些髒話累活打交道,就是類似於我們程式碼分層架構中的,用來操作redis、es、第三方服務等的一個層,相當於把一些通用的業務邏輯下沉。
而上面2處的程式碼,就負責池子維護:
private void assimilateResource( Object resc ) throws Exception
{
// 1
managed.put(resc, new PunchCard());
// 2
unused.add(0, resc);
// 3
this.notifyAll();
}
這裡的1處,就會往managed裡面存放連線,key就是建立的連線,那麼value是啥呢?
final static class PunchCard
{
long acquisition_time;// 建立時間
long last_checkin_time; //上次歸還到連線池的時間
long checkout_time; // 從連線池借出的時間,未借出時,值為-1
Exception checkoutStackTraceException; // 被借出時,該借出執行緒的堆疊
PunchCard()
{
this.acquisition_time = System.currentTimeMillis();
this.last_checkin_time = acquisition_time;
this.checkout_time = -1;
this.checkoutStackTraceException = null;
}
}
Punchcard這個詞,翻譯的意思是:穿孔卡(舊時把資訊打成一排排的小孔,用以將指令輸入計算機等),我這邊就理解成這個資料庫連線的一些記錄出借/歸還資訊的卡片。
裡面有個checkout_time欄位,初始化的值是 -1,表示未被出借。
另外,還有個重要欄位,unused,這個主要是存放可供出借的連線。
/* all valid, managed resources currently available for checkout */
LinkedList unused = new LinkedList();
上面的2處,就會把新的連線往這裡面放,放完後,用notify通知其他消費者執行緒。
綜上所述,剛開始的時候,
<property name="minPoolSize">10</property>
<property name="maxPoolSize">50</property>
managed的size是10,unused也是10
private synchronized Object prelimCheckoutResource( long timeout ){
int available = unused.size();
if (available == 0){
// 檢查是否可以擴容,可以的話,觸發擴容後開始等待。擴容也是非同步的,擴容成功的話,unused的size就大於0
...
}
Object resc = unused.get(0);
// 檢查連線是否過期了,如果過期了,這個連線不能要,得銷燬
if ( shouldExpire( resc ) )
{
removeResource( resc );
ensureMinResources();
return prelimCheckoutResource( timeout );
}
else
{ // 連線可用,那就從unused中摘除本連線並返回
unused.remove(0);
return resc;
}
}
boolean refurb = attemptRefurbishResourceOnCheckout( resc );
if (!refurb)
{
removeResource( resc );
ensureMinResources();
resc = null;
}
這個步驟類似於在連線上執行一個select 1,檢查連線到底能不能用。不能用的話,銷燬連線。
PunchCard card = (PunchCard) managed.get( resc );
card.checkout_time = System.currentTimeMillis();
if (debug_store_checkout_exceptions)
card.checkoutStackTraceException = new Exception("DEBUG ONLY: Overdue resource check-out stack trace.");
這裡就是,獲取到這個連線的punchCard資訊卡,然後登記出借的時間為當前時間,那麼,是誰借了呢,這裡是通過new一個異常的方式,通過這個異常,就能知道當前執行緒的堆疊。
if (managed.keySet().contains(resc))
doCheckinManaged( resc );
這個歸還呢,如下,也不是直接歸還,竟然也是new一個runnable去歸還,個人覺得,這個有巨大的隱患,因為執行緒池是可能會堵的,而這個就極有可能導致還不進去。
private void doCheckinManaged( final Object resc ){
Runnable doMe = new RefurbishCheckinResourceTask();
taskRunner.postRunnable( doMe );
}
class RefurbishCheckinResourceTask implements Runnable
{
public void run()
{
// 1 歸還前試著測試下連線是否能用,比如select 1
boolean resc_okay = attemptRefurbishResourceOnCheckin( resc );
// 2 獲取卡片並更新卡片
PunchCard card = (PunchCard) managed.get( resc );
if ( resc_okay && card != null)
{
// 3
unused.add(0, resc );
card.last_checkin_time = System.currentTimeMillis();
card.checkout_time = -1;
}
BasicResourcePool.this.notifyAll();
}
}
這裡主要就是,歸還前先測試下連線是不是好的,免得還個壞的進去;再就是,拿到之前的出借卡,更新歸還時間為當前時間、借出時間改為-1;再把連線放回到unused空閒連結串列。
問題如下,空閒連結串列為空,連線池被出借一空:
隨便找了個連線看出借時間:
這個時間,距離執行jmap的時候,已經過去了一分鐘了,而大部分的punchCard都是這樣,這說明了什麼,說明了這些連線被借出去一分鐘了,都還沒有歸還到unused空閒連結串列,導致空閒連結串列size為0,後續的請求在unused上死等也等不到連線(因為managed已經達到池子的最大值了,也沒法擴容),於是超時。
現在看起來,直接原因是找到了,就是有連線洩露,但是具體是哪裡有洩露呢?是不是真的有洩露呢?感覺長路仍漫漫,繼續努力吧。
留到下篇繼續吧,天也晚了,現在早上上班早,晚上不早點睡真是扛不住。