天天看點

池化技術:如何減少頻繁建立資料庫連接配接的性能損耗?

導語

  想想一下,如何快速研發出一套面向某個垂直領域的電商系統。在人手緊張的情況下,時間不足,為了能夠完成任務可能就是使用JSP模闆引擎等技術快速的開發出一套系統來。背景使用一台資料庫伺服器存儲業務資料。如下圖

池化技術:如何減少頻繁建立資料庫連接配接的性能損耗?

  這個架構圖是我們每個人比較熟悉的,也是最簡單的架構原型圖,很多的系統在最開始的時候,都是這樣子的,随着業務複雜度的提高,架構做了疊加,然後就看上去複雜起來了。

  在說回上面提到的電商系統,系統一開始上線之後,雖然使用者量不大,也運作平穩。很有成就感的一件事情。有一天營運的同學搞了一波活動進行了全網的推廣。這個時候系統的通路速度就開始變慢了。

  分析程式日志之後,發現了問題,通路慢的主要問題就集中在與資料庫的頻繁互動上,商品資訊,商戶資訊,使用者資訊等等,都是從資料庫中進行查詢的,并且查詢的速度非常慢,每一次的查詢都與資料庫建立的連接配接,操作完成之後關閉釋放連接配接,這樣的調用方式就會導緻,每次操作SQL就要建立連接配接。那麼就出現問題了,是不是因為頻繁的建立資料庫連接配接導緻系統性能的損耗呢?

來看一個小例子

  使用 tcpdump -I bond0 -nn -tttt port 4490 指令抓取了線上MySql建立連接配接的網絡包,從抓取的結果來看,整個的MySQL的連接配接過程可以分成兩個部分

  • 第一部分是前三個資料包。第一個資料包是用戶端向服務端發送一個SYN的資料包,第二個包是服務端回給用戶端的ACK包以及一個SYN包,第三個包是用戶端回給服務端的ACK包。其實這個就是TCP連接配接的三次握手機制。
  • 第二個部分是MySQL服務端校驗用戶端密碼的過程 其中第一個包是服務端發送給用戶端要求認證的封包,第二個和第三個包是用戶端将加密後的密碼發送給服務端的包,最後兩個包是服務端回給用戶端認證OK的封包。從圖中可以看到整個的過程大概消耗了4ms。整個抓包的過程是使用WireShack進行操作的。
池化技術:如何減少頻繁建立資料庫連接配接的性能損耗?

  那麼單條SQL執行的之間是多少呢?每條SQL在執行的過程中平均時間可以消耗在1ms上,也就是說相比于SQL的執行,MySQL資料庫連接配接的建立其實是比較耗時的一個過程。在請求量小的情況下影響并不是太大,但是一旦通路量增加的時候,無論是建立資料庫連接配接還是執行SQL都是比較耗時的操作,但是相比而言,在一個1s的操作中建立連接配接占據了4/5,而執行SQL隻占用的1/5。這樣看其實,還是建立連接配接更耗時。

如何進行優化?

  這就可以參考池化技術。将所有的資料庫連接配接先通過資料庫連接配接池來預先建立好,當一個SQL需要執行的時候就可以直接使用已經建立好的連接配接。由于是已經建立好的連接配接,是以說從連接配接的建立到銷毀都是由資料庫連接配接池來完成的,比較一個操作自己建立連接配接管理連接配接真的在性能上有了很大的提升。

如何用連接配接池預先建立連接配接

  雖然池化技術在短時間内解決了具體出現的問題,那麼它是如何管理整個的連接配接操作呢?

  在平時的開發中會遇到很多池化技術解決的問題,例如資料庫連接配接池、線程池、HTTP連接配接池。Redis連接配接池等等。這些連接配接池的管理核心其實就是連接配接池如何設計。以資料庫連接配接池為例來說明一下。

  首先資料庫連接配接池有兩個關鍵的配置,最大連接配接數和最小連接配接數,它們控制的是從連接配接池中擷取連接配接的流程。

  • 1、如果目前連接配接數小于最小連接配接數,則表示連接配接池中還有位置,可以建立新的連接配接請求資料庫。
  • 2、如果連接配接池中有空閑的連接配接,則就複用空閑連接配接
  • 3、如果空閑池中沒有連接配接并且連接配接數小于最大連接配接數,則建立新的連接配接處理請求;
  • 4、如果目前連接配接數已經大于等于最大連接配接數,按照配置中設定的時間,等待舊連接配接的釋放
  • 5、如果等待超過了設定的時間,則需要向使用者抛出錯誤。

  主要是要了解這個流程中的設計思路,在後續的架構設計中會經常用到。

  舉個例子,加入在火車站有一家電動按摩椅小店,店裡一共是10個椅子,這個類似于最大連接配接數,為了節省成本,平時的時候會保持有4個按摩椅是開着的,最小連接配接數,其他的6台都是關閉的。

  有遊客進來的時候,如果平時開啟的4台椅子都是空的,那麼直接去使用就可以了,但如果4台椅子已經都占滿了,那麼就需要新啟動一台,直到10台按摩椅都被用完方停止。

  那麼當10台椅子都用完了,這個時候還有其他遊客進來,那就要告訴遊客在一段時間内會有椅子空出來,然後這個時候第11位遊客就在等待,這個時候就會有兩種結果,如果在一段時間内有椅子空出來那麼這個顧客有可以直接過去使用,但是如果一段時間之後,沒有空出來就需要讓遊客到其他地方試試了。

  對于資料庫連接配接池,一般線上上建議最小連接配接數控制在10左右,最大連接配接數控制在20~30左右就可以了。

&emsp 這個時候就需要對連接配接池中的連接配接進行維護的問題,就像是椅子一樣,雖然是能用的但是保證不了會有故障發生,一般“按摩椅故障”原因有如下的幾種

  • 1、資料庫的域名對應的IP發生的變化,池子中的連接配接還是用的舊的IP,當舊的IP下的資料服務關閉之後,再使用這個連接配接查詢就會發生錯誤;
  • 2、MySQL有個參數是wait_timeout ,控制這當資料庫連接配接閑置多長時間後,資料庫會主動關閉這條連接配接。這機制對于資料庫使用方是無感覺的,是以當我們使用這個被關閉的連接配接時就會發生錯誤。

  但是作為老闆,怎麼保證你啟動狀态的椅子一定是可用的呢?

  • 1、啟動一個線程來定期的檢測連接配接池中的連接配接是否可用,例如使用連接配接發送 “select 1” 的指令給資料庫看是否會抛出異常,如果抛出異常則将這個連接配接從連接配接池中移除,并且嘗試關閉,目前C3P0連接配接池可以采用這種方式來檢測連接配接是否可用。
  • 在擷取到連接配接之後,先校驗連接配接是否可用,如果可用才會執行SQL語句,例如DBCP連接配接池的testOnBorrow配置項,就是控制是否開啟這個驗證。這種方式在擷取連接配接時會引入多餘開銷。線上系統盡量不要開啟。

  到這裡看上去一切都沒有問題,但是在上面需求中有個問題,在一個非常重要的接口中,需要通路3次資料庫,在高并發的場景下資料庫通路次數增加,會增加系統開銷,順着這個思路。就又要對線程池進行優化了。

用線程池預先建立線程

  在JDK1.5 中引入的ThreadPoolExecutor 就是這種線程池實作,它由兩個重要參數:coreThreadCount 和maxTHreadCount ,這兩個參數控制這線程池的執行過程。它的執行原理與上述的按摩椅原理相同。

  • 如果線程池中的線程數少于coreThreadCount的時候,處理新的任務的時候會建立新的線程;
  • 如果線程數大于coreThreadCount則把任務丢到一個隊列中,由目前空閑的線程執行;
  • 當隊列中的任務堆積滿的時候,則繼續建立線程,直到達到maxThreadCount;
  • 當線程數達到maxThreadCount的時候還有新的任務送出那麼就不得不将其丢棄了;
    池化技術:如何減少頻繁建立資料庫連接配接的性能損耗?

  首先JDK實作的這個線程池優先把任務放入隊列暫存起來,而不是建立更多的線程,它比較适用于執行CPU密集型的任務,也就是需要執行大量CPU運算的任務,這是為什麼呢?因為執行CPU密集型的任務時CPU比較繁忙,是以隻需要建立和CPU核數相當的線程就好了,多個反而會造成線程上下文切換帶來的效率低下的問題。是以當目前線程數超過核心線程數的時候,線程池不會增加線程,而是放在隊列中等待核心線程空閑下來執行。

  但,平時的開發的Web系統通常都有大量的IO操作,例如查詢資料庫、查詢緩存等等。任務在執行IO操作的時候CPU就空閑下來了,這個時候如果增加執行任務的線程數而不是把任務暫存到隊列中,就可以在機關時間内執行更多的任務,大大提高了任務執行的吞吐量,是以在Tomcat中的線程池就不是JDK原生的線程池,而是做了一些改造,當線程數超過coreThreadCount之後會優先建立線程,直到線程數達到maxThreadCount,這樣就比較适合于Web系統大量IO操作的場景了,在實際使用的過程中可以進行參考。

  其次,線程池中使用的隊列的堆積量也是需要進行監控的重要名額,對于實時性要求比較高的任務來說,這個名額尤為關鍵。

  在實際項目中曾經遇到過任務被丢給線程池中,長時間都沒有執行的問題,這個問題是怎麼出現的呢?就是coreThreadCount 和 maxThreadCount設定的比較小,導緻任務線上程池裡面大量的堆積,在調大了這兩個參數之後問題得以解決。是以要将線程堆積量作為監控名額進行監控。

  最後,如果使用線程池一定不要使用無界隊列,開始認為使用無界隊列可能會讓任務永遠都不會丢失,但是實際上,大量的資料還是會占用很多的記憶體空間,一旦空間被占滿了就會頻繁的觸發Full GC,進而導緻Stop-The-World ,然後就是服務不可用。

總結

  池化技術有一個共同點:就是它們所管理的對象,無論是連接配接池還是線程池,它們在建立過程中都是比較耗時的操作,同樣也消耗的很多的系統資源,是以要利用池化技術盡心管理,進而達到資源複用的目的。這是一種比較常見的軟體設計思想。它的核心就是使用空間換取時間,使用預先建立好的内容來減少頻繁建立帶來的系統性能方面的開銷,同時還可以對對象進行統一的管理,降低了對象的使用建立成本。

  同樣一個技術有好的方面,就會有瓶頸的地方。例如存儲池子的對象肯定需要消耗多餘的記憶體,如果對象沒有被頻繁使用到情況下,池子本身就是記憶體上的浪費,在如,池中的對象在系統啟動的時候就建立好,這一個層面上其實增加了系統啟動的時間。例如Spring 容器的裝載對象,就是需要一個過程,這個過程就是一個比較消耗啟動時間的過程。

  可能這些缺陷對于優點來說,看上去消耗是微不足道的,是以說池化技術還是在實際開發中比較優化的一種方案。

繼續閱讀