自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一次線上故障:數(shù)據(jù)庫(kù)連接池泄露后的思考

數(shù)據(jù)庫(kù)
早上作為能效平臺(tái)系統(tǒng)的使用高峰期,系統(tǒng)負(fù)載通常比其它時(shí)間段更大一些,某個(gè)時(shí)間段會(huì)有大量用戶登錄。當(dāng)天系統(tǒng)開(kāi)始有用戶報(bào)障,發(fā)布系統(tǒng)線上無(wú)法構(gòu)建發(fā)布,然后后續(xù)有用戶不能登錄系統(tǒng),系統(tǒng)發(fā)生假死,當(dāng)然系統(tǒng)不是真的宕機(jī),而是所有和數(shù)據(jù)庫(kù)有關(guān)的連接都被阻塞,隨后查看日志發(fā)現(xiàn)有大量報(bào)錯(cuò)。

 一:初步排查

早上作為能效平臺(tái)系統(tǒng)的使用高峰期,系統(tǒng)負(fù)載通常比其它時(shí)間段更大一些,某個(gè)時(shí)間段會(huì)有大量用戶登錄。當(dāng)天系統(tǒng)開(kāi)始有用戶報(bào)障,發(fā)布系統(tǒng)線上無(wú)法構(gòu)建發(fā)布,然后后續(xù)有用戶不能登錄系統(tǒng),系統(tǒng)發(fā)生假死,當(dāng)然系統(tǒng)不是真的宕機(jī),而是所有和數(shù)據(jù)庫(kù)有關(guān)的連接都被阻塞,隨后查看日志發(fā)現(xiàn)有大量報(bào)錯(cuò)。

[[313322]]

和數(shù)據(jù)庫(kù)連接池相關(guān):

 

  1. Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30002ms. 

可以看出上面的報(bào)錯(cuò)和數(shù)據(jù)庫(kù)連接有關(guān),大量超時(shí)。通過(guò)對(duì)線上debug日志的分析,也驗(yàn)證了數(shù)據(jù)庫(kù)連接池被大量消耗。

 

  1. [DEBUG] c.z.h.p.HikariPool: HikariPool-1 - Timeout failure stats (total=20, active=20, idle=0, waiting=13) 

這是開(kāi)始大量報(bào)錯(cuò)前的日志。我們可以看到此時(shí)HikariPool連接池已經(jīng)無(wú)法獲取連接了,active=20表示被獲取正在被使用的數(shù)據(jù)庫(kù)連接。waiting表示當(dāng)前正在排隊(duì)獲取連接的請(qǐng)求數(shù)量??梢钥闯?,已經(jīng)有相當(dāng)多的請(qǐng)求處于掛起狀態(tài)。

所以當(dāng)時(shí)我們的解決辦法是調(diào)整數(shù)據(jù)庫(kù)連接池大小,開(kāi)始初步認(rèn)為是,高峰時(shí)期,我們?cè)O(shè)置的連接池?cái)?shù)量大小,不足以支撐早高峰的連接數(shù)量導(dǎo)致的。

 

  1. jdbc.connection.timeout=30000 
  2. jdbc.max.lifetime=1800000 
  3. jdbc.maximum.poolsize=200 
  4. jdbc.minimum.idle=10 
  5. jdbc.idle.timeout=60000 
  6. jdbc.readonly=false 

 

我們將將數(shù)據(jù)庫(kù)連接池的數(shù)量調(diào)整到了200。

二:事務(wù)

2.1事務(wù)濫用的后果

及時(shí)將配置調(diào)整成了200,服務(wù)重啟也恢復(fù)了正常,但是我仍然認(rèn)為系統(tǒng)存在連接泄露的風(fēng)險(xiǎn),我試圖從日志表現(xiàn)出的行為里尋找蛛絲馬跡。我在訪問(wèn)日志看到每次在系統(tǒng)崩潰前,其實(shí)都有人在做構(gòu)建,而且構(gòu)建經(jīng)常點(diǎn)擊沒(méi)反應(yīng),我當(dāng)時(shí)添加的構(gòu)建debug日志也顯示了這一點(diǎn)。我開(kāi)始懷疑是構(gòu)建造成的連接泄露。

在這里我簡(jiǎn)單說(shuō)下構(gòu)建代碼處的邏輯

  • 用戶觸發(fā)構(gòu)建
  • 將job加入增量job緩存,用于更新job狀態(tài)
  • jenkinsClient調(diào)用jenkins的api,開(kāi)始構(gòu)建
  • 將構(gòu)建信息寫(xiě)入數(shù)據(jù)庫(kù)(jobname,version)

我開(kāi)始觀察自己寫(xiě)的代碼,可是看了多遍,我也發(fā)現(xiàn)不了這段代碼和數(shù)據(jù)庫(kù)連接有啥關(guān)系,大多數(shù)人包括當(dāng)時(shí)自己來(lái)說(shuō),數(shù)據(jù)庫(kù)連接的泄露,大多數(shù)情況應(yīng)該是服務(wù)和數(shù)據(jù)庫(kù)連接的過(guò)程中發(fā)生了阻塞,導(dǎo)致連接泄露。但是現(xiàn)在來(lái)看,很容易能發(fā)現(xiàn)問(wèn)題所在,看當(dāng)時(shí)的代碼:

 

  1. @Transactional(rollbackFor = Exception.class) 
  2.    public void build(BuildHistoryReq buildHistoryReq) { 
  3.        //1.封裝操作 
  4.        //2.調(diào)用jenkins Api 
  5.        //3.數(shù)據(jù)庫(kù)更新寫(xiě)入 
  6.    } 

 

這就是當(dāng)時(shí)的代碼入口,當(dāng)然代碼處沒(méi)有這么簡(jiǎn)單。可以看到我在方法入口就加上了Transactional注解,這里的意思其實(shí)就是發(fā)生錯(cuò)誤,拋出異常時(shí),數(shù)據(jù)庫(kù)回滾。

問(wèn)題就出現(xiàn)在了這里,當(dāng)有用戶點(diǎn)擊構(gòu)建時(shí),請(qǐng)求剛進(jìn)入build方法時(shí),就會(huì)從數(shù)據(jù)庫(kù)連接獲取一個(gè)連接??墒谴藭r(shí),程序并沒(méi)有和數(shù)據(jù)庫(kù)相關(guān)的操作,如果此時(shí)代碼在步驟1或者2處出現(xiàn)io或者網(wǎng)絡(luò)阻塞,就會(huì)導(dǎo)致,事務(wù)無(wú)法提交,連接也就會(huì)一直被該請(qǐng)求占用。而再大的連接池也會(huì)被耗費(fèi)殆盡。從而造成系統(tǒng)崩潰。

2.2事務(wù)注解的正確用法

通常情況下作為非業(yè)務(wù)部門(mén),沒(méi)有涉及到核心的業(yè)務(wù),像支付,訂單,庫(kù)存相關(guān)的操作時(shí),事務(wù)在可讀層面并沒(méi)有特別高的要求。通常也只涉及到,多表操作同時(shí)更新時(shí),保證數(shù)據(jù)一致性,要么同時(shí)成功要么同時(shí)失敗。而使用

 

  1. @Transactional(rollbackFor = Exception.class) 

足以。

而上述代碼該如何改進(jìn)呢??

首先分析有沒(méi)有需要使用事務(wù)的必要。在步驟3中,數(shù)據(jù)操作,看代碼后發(fā)現(xiàn)只有對(duì)一張表的操作,同時(shí)和其它操作沒(méi)有相關(guān)性。而且本身屬于最后一個(gè)步驟。所以在此代碼中完全沒(méi)有必要使用,刪除注解即可。

當(dāng)然如果步驟3操作數(shù)據(jù)庫(kù)是多表操作,具有強(qiáng)相關(guān)性,數(shù)據(jù)一致,我們可以這樣做。將和步驟3無(wú)關(guān)的步驟分開(kāi),變成兩個(gè)方法,那么在1,2處發(fā)生阻塞也不會(huì)影響到數(shù)據(jù)庫(kù)連接。

 

  1. public void build(BuildHistoryReq buildHistoryReq) { 
  2.       //1.封裝操作 
  3.       //2.調(diào)用jenkins Api 
  4.       update**(XX); 
  5.   } 
  6.  
  7.   @Transactional(rollbackFor = Exception.class) 
  8.   public void update**(XX xx) { 
  9.       //3.數(shù)據(jù)庫(kù)更新寫(xiě)入 
  10.   }    

 

這里需要注意,注解事務(wù)的用法,方法必須是公開(kāi)調(diào)用的。

三:HttpClient 4.x連接池

當(dāng)時(shí)找到數(shù)據(jù)連接池泄露的原因后,我第一步就是去掉了事務(wù),然后加上了一些日志,這時(shí)我已經(jīng)能確定代碼在jenkinsclient處出現(xiàn)了問(wèn)題,但是仍然不確定問(wèn)題出在了哪,我只能加上一些日志,同時(shí)通過(guò)監(jiān)控繼續(xù)觀察。

果然在hotfix的第二天還是出現(xiàn)了我預(yù)料中的事情,構(gòu)建發(fā)布仍然有問(wèn)題,當(dāng)然此時(shí)其它功能是不受影響了。我觀察日志發(fā)現(xiàn)構(gòu)建開(kāi)始并在該處阻塞

 

  1. jenkinsClient.startBuild(jobName, params); 

隨后我觀察了項(xiàng)目監(jiān)控。觀察線程情況,發(fā)現(xiàn)大量http-nio的線程阻塞了,而這個(gè)線程和httpclient相關(guān)。

 

  1. java.lang.Thread.State: WAITING (parking) 
  2.     at sun.misc.Unsafe.park(Native Method) 
  3.     - parking to wait for  <0x00000007067027e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) 
  4.     at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) 
  5.     at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) 
  6.     at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:379) 
  7.     at org.apache.http.pool.AbstractConnPool.access$200(AbstractConnPool.java:69) 
  8.     at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:245) 
  9.     - locked <0x00000007824713a0> (a org.apache.http.pool.AbstractConnPool$2) 
  10.     at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:193) 

 

隨后我跟進(jìn)源碼查看了AbstractConnPool類的379行

 

 

可以看到線程走到379行執(zhí)行了this.condition.await()后進(jìn)入無(wú)限期的等待,所以此時(shí)如果沒(méi)有線程執(zhí)行this.condition.signal()就會(huì)導(dǎo)致該線程一直處于waiting狀態(tài),而前端也會(huì)遲遲收不到相應(yīng),導(dǎo)致請(qǐng)求timeout。

我們?cè)俜治鱿略创a,看看什么情況下會(huì)導(dǎo)致線程跑到該處:

 

  1. /** 
  2. * 獲取http連接,從名稱也能看出該方法會(huì)造成阻塞 
  3. */ 
  4.  private E getPoolEntryBlocking( 
  5.             final T route, final Object state, 
  6.             final long timeout, final TimeUnit timeUnit, 
  7.             final Future<E> future) throws IOException, InterruptedException, TimeoutException { 
  8.  
  9.         Date deadline = null
  10.         if (timeout > 0) { 
  11.             deadline = new Date (System.currentTimeMillis() + timeUnit.toMillis(timeout)); 
  12.         } 
  13.         this.lock.lock(); 
  14.         try { 
  15.             final RouteSpecificPool<T, C, E> pool = getPool(route); 
  16.             E entry; 
  17.             for (;;) { 
  18.                 Asserts.check(!this.isShutDown, "Connection pool shut down"); 
  19.                 for (;;) { 
  20.                     entry = pool.getFree(state); 
  21.                     if (entry == null) { 
  22.                         break; 
  23.                     } 
  24.                     if (entry.isExpired(System.currentTimeMillis())) { 
  25.                         entry.close(); 
  26.                     } 
  27.                     if (entry.isClosed()) { 
  28.                         this.available.remove(entry); 
  29.                         pool.free(entry, false); 
  30.                     } else { 
  31.                         break; 
  32.                     } 
  33.                 } 
  34.                 if (entry != null) { 
  35.                     this.available.remove(entry); 
  36.                     this.leased.add(entry); 
  37.                     onReuse(entry); 
  38.                     return entry; 
  39.                 } 
  40.  
  41.                 // New connection is needed 
  42.                 final int maxPerRoute = getMax(route); 
  43.                 // Shrink the pool prior to allocating a new connection 
  44.                 final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute); 
  45.                 if (excess > 0) { 
  46.                     for (int i = 0; i < excess; i++) { 
  47.                         final E lastUsed = pool.getLastUsed(); 
  48.                         if (lastUsed == null) { 
  49.                             break; 
  50.                         } 
  51.                         lastUsed.close(); 
  52.                         this.available.remove(lastUsed); 
  53.                         pool.remove(lastUsed); 
  54.                     } 
  55.                 } 
  56.  
  57.                 if (pool.getAllocatedCount() < maxPerRoute) { 
  58.                     final int totalUsed = this.leased.size(); 
  59.                     final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0); 
  60.                     if (freeCapacity > 0) { 
  61.                         final int totalAvailable = this.available.size(); 
  62.                         if (totalAvailable > freeCapacity - 1) { 
  63.                             if (!this.available.isEmpty()) { 
  64.                                 final E lastUsed = this.available.removeLast(); 
  65.                                 lastUsed.close(); 
  66.                                 final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute()); 
  67.                                 otherpool.remove(lastUsed); 
  68.                             } 
  69.                         } 
  70.                         final C conn = this.connFactory.create(route); 
  71.                         entry = pool.add(conn); 
  72.                         this.leased.add(entry); 
  73.                         return entry; 
  74.                     } 
  75.                 } 
  76.  
  77.                 boolean success = false
  78.                 try { 
  79.                     if (future.isCancelled()) { 
  80.                         throw new InterruptedException("Operation interrupted"); 
  81.                     } 
  82.                     pool.queue(future); 
  83.                     this.pending.add(future); 
  84.                     if (deadline != null) { 
  85.                         success = this.condition.awaitUntil(deadline); 
  86.                     } else { 
  87.                         this.condition.await(); 
  88.                         success = true
  89.                     } 
  90.                     if (future.isCancelled()) { 
  91.                         throw new InterruptedException("Operation interrupted"); 
  92.                     } 
  93.                 } finally { 
  94.                     // In case of 'success', we were woken up by the 
  95.                     // connection pool and should now have a connection 
  96.                     // waiting for us, or else we're shutting down. 
  97.                     // Just continue in the loop, both cases are checked. 
  98.                     pool.unqueue(future); 
  99.                     this.pending.remove(future); 
  100.                 } 
  101.                 // check for spurious wakeup vs. timeout 
  102.                 if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) { 
  103.                     break; 
  104.                 } 
  105.             } 
  106.             throw new TimeoutException("Timeout waiting for connection"); 
  107.         } finally { 
  108.             this.lock.unlock(); 
  109.         } 
  110.     } 

 

從源碼我們可以看出有幾處必要條件才會(huì)導(dǎo)致線程會(huì)無(wú)限期等待:

  • timeout=0 也就是說(shuō)沒(méi)有給默認(rèn)值,導(dǎo)致: deadline = null
  • pool.getAllocatedCount() < maxPerRoute 判斷是否已經(jīng)到達(dá)了該路由(host地址)的最大連接數(shù)。

其實(shí)整體邏輯就是,從池里獲取連接,如果有就直接返回,沒(méi)有,判斷當(dāng)前請(qǐng)求出去的路由有沒(méi)有到達(dá)該路由的最大值,如果達(dá)到了,就進(jìn)行等待。如果timeout為0就會(huì)進(jìn)行無(wú)限期等待。

而這些值我本身也沒(méi)有做任何設(shè)置,我當(dāng)時(shí)的第一想法就是,給http請(qǐng)求設(shè)置超時(shí)時(shí)間。也就是給每個(gè)client設(shè)置必要的參數(shù)

解決

1.jenkinsClient分配超時(shí)時(shí)間

 

  1. public HttpClientBuilder clientBuilder() { 
  2.         HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); 
  3.         RequestConfig.Builder builder = RequestConfig.custom(); 
  4.         //該參數(shù)對(duì)應(yīng)AbstractConnecPool getPoolEntryBlocking方法的timeout 
  5.         builder.setConnectionRequestTimeout(5 * 1000); 
  6.         //數(shù)據(jù)傳輸?shù)某瑫r(shí)時(shí)間 
  7.         builder.setSocketTimeout(20 * 1000); 
  8.         //該參數(shù)為,服務(wù)和jenkins連接的時(shí)間(通常連接的時(shí)間都很短,可以設(shè)置小點(diǎn)) 
  9.         builder.setConnectTimeout(5 * 1000); 
  10.         httpClientBuilder.setDefaultRequestConfig(builder.build()); 
  11.         return httpClientBuilder; 
  12.  } 

 

2.構(gòu)建JenkinsClient和更新使用的JenkinsClient分離

其實(shí)我已經(jīng)嘗試用池化的思想來(lái)解決該問(wèn)題了。

詭異bug(同一個(gè)JenkinsClient,調(diào)用不同的api,有的api會(huì)阻塞,有的調(diào)用仍然正常)

但hotfix的第二天,又出現(xiàn)了一個(gè)詭異的bug:

構(gòu)建可以,但是無(wú)法同步j(luò)ob的狀態(tài)。這里出現(xiàn)這個(gè)問(wèn)題的原因在于我將構(gòu)建和更新兩個(gè)過(guò)程使用的jenkinsClient分離成兩個(gè),所以這個(gè)過(guò)程相互獨(dú)立,互不影響,所以,更新的client出了問(wèn)題但是構(gòu)建的client仍然能正常使用。

但是更新過(guò)程的JenkinsClient出現(xiàn)的問(wèn)題讓我百思不得其解。我們先看看更新?tīng)顟B(tài)過(guò)程會(huì)使用到的api(接口)

 

  1. //獲取對(duì)應(yīng)的job 
  2. 1 JobWithDetails job = client.get(UrlUtils.toJobBaseUrl(folder, jobName), JobWithDetails.class); 
  3.  
  4. //獲取job構(gòu)建的pipeline流水 
  5. 2 client.get("/job/" + EncodingUtils.encode(jobName) + "/" + version + "/wfapi/describe", PipelineStep.class); 
  6.  
  7. //獲取對(duì)應(yīng)job某次build的詳情 
  8. 3  client.get(url, BuildWithDetails.class); 

 

bug問(wèn)題1:為什么全量更新job和增量更新job使用的是同一個(gè)JenkinsClient,但是全量更新仍然正常獲取值,而增量更新job狀態(tài)的線程確出現(xiàn)阻塞超時(shí)(超時(shí)是因?yàn)榍懊嫖以O(shè)置了timeout,使得請(qǐng)求不會(huì)一直阻塞下去)。

要回答這個(gè)問(wèn)題,就要回到線程的相關(guān)問(wèn)題了,

this.condition.wait()會(huì)導(dǎo)致當(dāng)前線程阻塞,并不會(huì)影響到另外線程。而更新使用了兩個(gè)線程。所以這個(gè)問(wèn)題也比較好回答。

bug問(wèn)題2:為什么同一個(gè)線程(增量更新job線程)調(diào)用不同api,有的成功,而有的會(huì)阻塞:

解決這個(gè)問(wèn)題,我們還是得回到AbstractConnPool中的方法getPoolEntryBlocking()來(lái)看:

 

  1. if (pool.getAllocatedCount() < maxPerRoute) { 
  2.                     
  3.      } 

 

當(dāng)前請(qǐng)求的路由如果已經(jīng)達(dá)到最大值了就會(huì)阻塞等待。那么同一個(gè)jenkinsclient,按理來(lái)說(shuō)不可能會(huì)出現(xiàn)不同的路由。所以同一個(gè)client要么都能訪問(wèn),要么都會(huì)阻塞,怎么會(huì)出現(xiàn)有的能訪問(wèn)有的會(huì)阻塞。為了尋求問(wèn)題的答案,我翻閱了JenkinsClient的源碼,結(jié)合日志,發(fā)現(xiàn)服務(wù)每次阻塞的方法是:

 

 

不管多少次,每次都會(huì)完美的在該地方阻塞:對(duì)應(yīng)上面的api 3:

 

  1. //獲取對(duì)應(yīng)job某次build的詳情 
  2. 3  client.get(url, BuildWithDetails.class); 

 

這個(gè)url和其它兩個(gè)api拿到的路由都有區(qū)別:可以跟隨我一起看源碼:

 

  1. public class Build extends BaseModel { 
  2.  
  3.     private int number; 
  4.     private int queueId; 
  5.     private String url; 

 

我們可以看到url是屬于Build的屬性,并非client我們?cè)O(shè)置的值,當(dāng)然有人會(huì)覺(jué)得該值可能是通過(guò)將配置的url設(shè)置過(guò)來(lái)的。我們可以接著看,哪些方法可能會(huì)給build設(shè)置url,三個(gè)構(gòu)造函數(shù),一個(gè)set方法都可以,如果我們繼續(xù)只看源碼仍然很難找到問(wèn)題所在,所以這時(shí)候我開(kāi)始啟動(dòng)服務(wù)debug;

發(fā)現(xiàn)了問(wèn)題在哪:

 

 

可以看出調(diào)用jenkins的這個(gè)api出現(xiàn)了兩個(gè)router,也可以看出這個(gè)url是jenkins返回的,查閱資料可以看到,jenkins系統(tǒng)設(shè)置時(shí)可以設(shè)置這個(gè)url。

所以這個(gè)bug也能很好的解釋了,對(duì)于httpclient來(lái)說(shuō),每個(gè)router默認(rèn)可以最多兩個(gè)連接。雖然是同一個(gè)調(diào)用api采用的是同一個(gè)jenkinsClient,但是卻維護(hù)了兩個(gè)router,一個(gè)是從配置中獲取,一個(gè)是jenkins返回的,這個(gè)是配置不一致導(dǎo)致的。

JenkinsClient分配連接數(shù):

 

  1. public HttpClientBuilder clientBuilder() { 
  2.         HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); 
  3.         RequestConfig.Builder builder = RequestConfig.custom(); 
  4.         builder.setConnectionRequestTimeout(5 * 1000); 
  5.         builder.setSocketTimeout(20 * 1000); 
  6.         builder.setConnectTimeout(5 * 1000); 
  7.         httpClientBuilder.setDefaultRequestConfig(builder.build()); 
  8.         //每個(gè)路由最多有10個(gè)連接(默認(rèn)2個(gè)) 
  9.         httpClientBuilder.setMaxConnPerRoute(10); 
  10.         //設(shè)置連接池最大連接數(shù) 
  11.         httpClientBuilder.setMaxConnTotal(20); 
  12.         return httpClientBuilder; 
  13.     } 

給JenkinsClient添加健康檢查,并手動(dòng)更新不能用的Client

 

  1. @Slf4j 
  2. public class JenkinsClientManager implements Runnable { 
  3.  
  4.     private volatile boolean flag = true
  5.     private final JenkinsClientProvider jenkinsClientProvider; 
  6.  
  7.     public JenkinsClientManager(JenkinsClientProvider jenkinsClientProvider) { 
  8.         this.jenkinsClientProvider = jenkinsClientProvider; 
  9.     } 
  10.  
  11.     @Override 
  12.     public void run() { 
  13.         while (flag) { 
  14.             try { 
  15.                 checkJenkinsHealth(); 
  16.                 //每30秒檢查一次 
  17.                 Thread.sleep(30_000); 
  18.             } catch (Exception e) { 
  19.                 log.warn("check health error:{}", e.getMessage()); 
  20.             } 
  21.         } 
  22.     } 
  23.  
  24.     public void checkJenkinsHealth() { 
  25.         log.debug("check jenkins client health start"); 
  26.         //獲取client是否可用 
  27.         available = isAvailable(..) 
  28.         if (!available || !queryAvailable) { 
  29.             //更新client 
  30.             jenkinsClientProvider.retrieveJenkinsClient(); 
  31.         } 
  32.     } 
  33.  
  34.     private boolean isAvailable(Set<Map.Entry<String, JenkinsClient>> entries) { 
  35.         boolean available = true
  36.         for (Map.Entry<String, JenkinsClient> entry : entries) { 
  37.             boolean running = entry.getValue().isRunning(); 
  38.             if (!running) { 
  39.                 log.debug("jenkins running error"); 
  40.                 available = false
  41.             } 
  42.         } 
  43.         return available; 
  44.     } 
  45.  
  46.  
  47.     @PostConstruct 
  48.     public void start() { 
  49.         TaskSchedulerConfig.getExecutor().execute(this); 
  50.     } 

 

四:JenkinsClient連接池

采用池化技術(shù)解決client高可用和重復(fù)利用問(wèn)題

雖然我手動(dòng)寫(xiě)了一個(gè)JenkinsClientManager每30秒來(lái)維護(hù)一次client,但是這種手工的方式并不好:

  • 每30秒維護(hù)一次,若是在期間發(fā)生問(wèn)題,那么只能干等
  • 無(wú)法動(dòng)態(tài)的根據(jù)系統(tǒng)需要,動(dòng)態(tài)構(gòu)建新的client,也就是無(wú)法滿足高并發(fā)下的使用問(wèn)題
  • 無(wú)法配置

目前我們都知道各種池化技術(shù):線程池、數(shù)據(jù)庫(kù)連接池、redis連接池。

筆者在實(shí)現(xiàn)jenkinsClient pool之前,參考了線程池、數(shù)據(jù)庫(kù)連接池的實(shí)現(xiàn)、發(fā)現(xiàn)其底層實(shí)現(xiàn)較為復(fù)雜、redis的連接池技術(shù)相對(duì)來(lái)說(shuō)容易看懂和學(xué)習(xí)、所以采用了和jedis一樣的實(shí)現(xiàn)方式來(lái)實(shí)現(xiàn)JenkinsClient的連接池

 

 

這是jedis的類結(jié)構(gòu)目錄,其實(shí)重點(diǎn)在我標(biāo)記的這5個(gè)類。

jedis本身也是采用的commons-pool2提供的池技術(shù)實(shí)現(xiàn)的,接下來(lái)我會(huì)簡(jiǎn)單介紹一下該工具提供的池化技術(shù)。

JenkinsClient連接池應(yīng)該要具備哪些功能??

  • 動(dòng)態(tài)創(chuàng)建JenkinsClient
  • 使用完的Client放回池中
  • 回收長(zhǎng)期不用和不可用的Client
  • 能夠根據(jù)需要配置一定數(shù)量的Client

對(duì)于提到的這些功能,我將通過(guò)commons-pool2包來(lái)實(shí)現(xiàn)

PooledObjectFactory:該接口管理著bean的生命周期(An interface defining life-cycle methods for instances to be served by an)

  • makeObject 方法創(chuàng)建一個(gè)可以入池的實(shí)例,也就是我們需要用的Client由該方法創(chuàng)建
  • destroyObject 方法可以銷毀不可用或者過(guò)期的對(duì)象
  • validateObject 方法對(duì)實(shí)例進(jìn)行驗(yàn)證,在每次創(chuàng)建完實(shí)例后,都會(huì)調(diào)用該方法,同時(shí)也會(huì)以一定的頻率進(jìn)行健康檢查(頻率timeBetweenEvictionRunsMillis)

GenericObjectPool:實(shí)例都會(huì)放入該池中進(jìn)行管理:

 

  1. //所有的可用連接 
  2. private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects = new ConcurrentHashMap<>(); 
  3.  
  4. //空閑的可用連接 
  5. private final LinkedBlockingDeque<PooledObject<T>> idleObjects; 
  6.  
  7. //獲取可用連接 
  8. T borrowObject() throws Exception, NoSuchElementException, 
  9.             IllegalStateException; 
  10.  
  11. //資源釋放(將連接放回連接池) 
  12. void returnObject(T obj) throws Exception; 

 

配置(BaseObjectPoolConfig,但是我們繼承GenericObjectPoolConfig,該類給出了大量的默認(rèn)值)

 

  1. 鏈接池中最大連接數(shù),默認(rèn)為8 
  2. maxTotal 
  3. #鏈接池中最大空閑的連接數(shù),默認(rèn)也為8 
  4. maxIdle 
  5. #連接池中最少空閑的連接數(shù),默認(rèn)為0 
  6. minIdle 
  7. #連接空閑的最小時(shí)間,達(dá)到此值后空閑連接將可能會(huì)被移除。默認(rèn)為1000L*60L*30L 
  8. minEvictableIdleTimeMillis 
  9. #連接空閑的最小時(shí)間,達(dá)到此值后空閑鏈接將會(huì)被移除,且保留minIdle個(gè)空閑連接數(shù)。默認(rèn)為-1 
  10. softMinEvictableIdleTimeMillis 
  11. #當(dāng)連接池資源耗盡時(shí),等待時(shí)間,超出則拋異常,默認(rèn)為-1即永不超時(shí) 
  12. maxWaitMillis 
  13. #當(dāng)這個(gè)值為true的時(shí)候,maxWaitMillis參數(shù)才能生效。為false的時(shí)候,當(dāng)連接池沒(méi)資源,則立馬拋異常。默認(rèn)為true 
  14. blockWhenExhausted 
  15. #空閑鏈接檢測(cè)線程檢測(cè)的周期,毫秒數(shù)。如果為負(fù)值,表示不運(yùn)行檢測(cè)線程。默認(rèn)為-1. 
  16. timeBetweenEvictionRunsMillis 
  17. #在每次空閑連接回收器線程(如果有)運(yùn)行時(shí)檢查的連接數(shù)量,默認(rèn)為3 
  18. numTestsPerEvictionRun 
  19. #默認(rèn)falsecreate的時(shí)候檢測(cè)是有有效,如果無(wú)效則從連接池中移除,并嘗試獲取繼續(xù)獲取 
  20. testOnCreate 
  21. #默認(rèn)false,borrow的時(shí)候檢測(cè)是有有效,如果無(wú)效則從連接池中移除,并嘗試獲取繼續(xù)獲取 
  22. testOnBorrow 
  23. #默認(rèn)falsereturn的時(shí)候檢測(cè)是有有效,如果無(wú)效則從連接池中移除,并嘗試獲取繼續(xù)獲取 
  24. testOnReturn 
  25. #默認(rèn)false,在evictor線程里頭,當(dāng)evictionPolicy.evict方法返回false時(shí),而且testWhileIdle為true的時(shí)候則檢測(cè)是否有效,如果無(wú)效則移除 
  26. testWhileIdle 

 

了解了這些我們對(duì)于需要開(kāi)發(fā)的連接池就很輕松了:

  • 實(shí)現(xiàn)PooledObjectFactory(JenkinsFactory)該工廠類就是負(fù)責(zé)JenkinsClient的生命周期
  • 自定義連接池Pool,通過(guò)組合的方式引入框架的連接池GenericObjectPool,當(dāng)然我們也可以用繼承的方式來(lái)實(shí)現(xiàn)(組合優(yōu)先于繼承)

五:反思

連接池寫(xiě)完,目前也只是在測(cè)試環(huán)境運(yùn)行,還在觀察階段

有個(gè)特別的問(wèn)題也需要指出來(lái),該問(wèn)題是筆者在開(kāi)發(fā)時(shí)沒(méi)有注意的問(wèn)題,也是此次線上產(chǎn)生問(wèn)題的原因

筆者將原來(lái)更新頻率從15s調(diào)整到了10s,問(wèn)題就暴露出來(lái)了,對(duì)于1個(gè)job,可能會(huì)拉出上百個(gè)build,每次會(huì)調(diào)用3個(gè)api接口,如果每次有十個(gè)job,每次更新會(huì)在10秒內(nèi)完成,隨著job增加,和構(gòu)建歷史增加(雖然有設(shè)置保留多少版本,但是api還是會(huì)拉出很奇怪的歷史build),會(huì)超量發(fā)出大量http請(qǐng)求。所以我在代碼層面也做了改動(dòng),每次只更新每個(gè)job的前5個(gè)最新的build,這樣下來(lái),請(qǐng)求量會(huì)降低很多

 

  1. List<Build> buildList = builds.stream().sorted(Comparator.comparing(Build::getNumber).reversed()).limit(5).collect(toList()); 

by陳朗:

本次事故整體來(lái)講,還是筆者技術(shù)有限,解決問(wèn)題時(shí)繞了很多彎,花了大量時(shí)間研究源碼。我也總結(jié)了以下幾點(diǎn)

 

  • 對(duì)于連接、鎖等這些可能會(huì)阻塞的場(chǎng)景,都需要給出超時(shí)設(shè)置
  • 資源消耗型,需要有池化的思想,提高資源利用率,保證系統(tǒng)穩(wěn)定
  • 基礎(chǔ)很重要,需要持續(xù)不斷的學(xué)習(xí),這樣解決問(wèn)題才能深入底層,找出問(wèn)題所在,而不是浮于表面

 

責(zé)任編輯:華軒 來(lái)源: 博客園
相關(guān)推薦

2009-06-24 07:53:47

Hibernate數(shù)據(jù)

2010-03-18 15:09:15

python數(shù)據(jù)庫(kù)連接

2019-11-27 10:31:51

數(shù)據(jù)庫(kù)連接池內(nèi)存

2025-01-16 10:30:49

2018-10-10 14:27:34

數(shù)據(jù)庫(kù)連接池MySQL

2017-06-22 14:13:07

PythonMySQLpymysqlpool

2023-01-04 18:32:31

線上服務(wù)代碼

2009-06-16 09:25:31

JBoss配置

2018-01-03 14:32:32

2009-07-17 13:32:49

JDBC數(shù)據(jù)庫(kù)

2011-05-19 09:53:33

數(shù)據(jù)庫(kù)連接池

2021-08-12 06:52:01

.NET數(shù)據(jù)庫(kù)連接池

2020-04-30 14:38:51

數(shù)據(jù)庫(kù)連接池線程

2019-04-15 13:15:12

數(shù)據(jù)庫(kù)MySQL死鎖

2020-11-16 12:35:25

線程池Java代碼

2023-12-04 19:15:00

連接池case

2009-07-03 17:37:54

JSP數(shù)據(jù)庫(kù)

2009-01-15 09:02:27

JMXJBossJMX監(jiān)控

2011-08-09 15:25:14

線程池數(shù)據(jù)庫(kù)連接池

2009-07-29 09:33:14

ASP.NET數(shù)據(jù)庫(kù)連
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)