璧說:從資料庫連線池說起

homerhuang發表於2015-12-08

該文章來自於阿里巴巴技術協會(ATA)精選文章。

這次我們來聊聊資料庫的連線, 因為我覺得這是蠻有內容且蠻重要的一部分內容。首先會從單個的連線池講起,重點考察下單連線池和資料庫的互動情況, 然後探討下大規模叢集下資料庫連線會遇到的問題,以及對應的解法。

首先什麼是連線池,出現的原因是啥?我們可以從一個標準SQL的生命週期說起, 如果一個SQL要到DB上去執行, 那麼首先要建立應用伺服器和資料庫的一個連線狀態,連線建立後資料庫會分配一個執行緒或者程式來排程,完成解析並生成執行計劃,然後才進入執行階段,讀取必要的資料到記憶體並邏輯處理, 最後才通過之前建立的連線傳送結果集給到客戶端,關掉連線並釋放資源,所以連線可以說是應用和DB互動的橋樑和管道,可惜這個橋樑的構建和銷燬,對與資料庫來說是資源消耗很大的操作,這裡會涉及到CPU的運算, 資源的爭用,記憶體的分配, socket的建立等,頻繁的建立連線和銷燬連線,對資料庫來說是不可接受的,所以長連線顯然比短連線更適合資料庫,這時候就出現了連線池,來對SQL生命週期中連線的建立和銷燬這個環節進行優化,有了連線池,就能做到連線複用,維護連線物件,做分配,管理和釋放,也就能減少平均連線時間,有了連線池,並加以合理的配置,同時能避免應用建立大量的連線到DB而引發的各種問題,通過請求排隊,來緩衝應用對DB的衝擊,所以從這個角度看,連線池其實就是排隊。

我們可以想象一下對連線池的基本動作,無非就是申請連線,從連線池中獲取連線,和業務處理完後,把連線釋放回連線池這些動作。在通常情況下一個連線池在啟動時會初始化MIN連線數,這時候通往資料庫的一部分管道已經建立起來了,你可以通過這些管道,對資料庫進行查詢和增刪改查,如果一個請求申請管道的時候發現有空閒的管道, 那麼直接可以拿來用了, 如果所有的管道都在忙,但管道的數量沒有達到MAX連線數, 那麼不需要等待,直接申請建立一個新的連線,用完了再把他放回去,當發現沒有空閒的管道, 並且活躍的管道已經到達MAX連線數了, 那麼這時候你只能選擇暫時等待, 等待的時間取決於block-timeout, 在這等待期間如果有管道空閒下來, 那麼恭喜你,你有機會拿到這個連線, 如果超出等待時間還沒有拿到連線,那麼就丟擲個拿不到連線的異常,連線池基本的邏輯就是這樣了,另外的功能無非就是對連線池使用狀態的監控,比如一個連線如果空閒下來了,多久沒有使用需要被關閉,比如哪些錯誤情況下需要重新建立一下連線再放入池子,比如如何定時來驗證連線是否有效,等等。

剛才提到了連線池的MIN和MAX連線,需要大家的關注,因為連線池是無法感知資料庫的執行情況以及負載的,通過經驗值或者計算模型,合理的加以設定, 對於應用伺服器和資料庫來說,都非常的重要,即要能發揮出應用伺服器的最大能力,也要能有效利用資料庫的連線資源和處理能力, 換句話說不想在有能力處理時讓請求在佇列中等待,也不想讓執行的請求超出DB的處理能力。

我們具體來看一下,如果連線池MIN設定過小的話,在應用業務量突增或者啟動時,就可能短時間內產生連線風暴,這對於資料庫是不小的衝擊,但是如果MIN值設定過大,就會出現資料庫連線過剩的情況, 連線一方面超出空閒時間被銷燬,而銷燬後發現又小於MIN連線數, 又開始建立, 結果就發生迴圈, 浪費資源浪費電。那如果連線池MAX值設定過大,在極端情況下,當應用發生異常時,會導致連線數被撐到MAX值,有可能導致資料庫的連線數被耗盡,或者超出資料庫的處理能力,進而導致業務受到影響。並且當連線數被撐到MAX值,在獲取連線等待超時的時候,應用的執行緒池也有可能受到影響,會形成一系列的連鎖反應,乃至雪崩。

所以平時有開發同學抱怨連線池的配置不夠,讓我們加大MAX值, 我都會解釋下,能不能加連線要看DB是否還有餘量,如果DB還有餘量,加連線也許是一種臨時的解決辦法, 如果DB已經容量不足, 加大MAX會放進更多的請求倒DB,只會讓效能變得更差,我們換個角度來做一個數學題,按照連線池預設的配置MAX為6,一百臺應用伺服器連線一個MySQL ,所以會有600個連線落到資料庫,按照一個請求的處理時間1ms的話, 那麼一秒鐘就能處理1000個請求, 600個連線的話可以處理60w的qps/tps請求了,這時候就已經遠遠超出單個DB的容量極限了。

也有的同學會說, 那把我block-timeout的時間改長一點, 儘可能的提高拿到連線的概率,豈不是挺好? 不好意思,這個同樣不太靠譜,當應用併發很高,大大超過連線池最大值,block-timeout也不能起到緩衝作用,返而會阻塞應用執行緒,大量的積壓執行緒會導致應用直接掛了。所以這個等待的時間也不是越長越好,而需要從應用的維度去評估一下,並建立好容錯機制。

強調了以上兩點,細心的同學可能已經發現了,這裡面的關鍵不在別的地方, 而是在於怎麼提高響應時間,就是怎麼做SQL優化,讓事務儘可能的短,怎麼進一步做連線複用,提高管道的效率,進而縮短請求的DB服務時間。前面提到過,連線池就是排隊論的思想, 我們可以進一步根據little`s law 來闡述一下這裡面的關係,比如說每秒訪問頻率是1000 (W), 平均服務的時間2ms(λ), 那麼佇列的長度 L = λW =0.002*1000 =2, 也就是說佇列的長度為2,只要兩個連線就能搞定這些需求了,如果我們平均服務的時間縮短到1ms,那麼連線池就只需要1個連線就夠了,根據little`s law ,我們拿到SQL的響應時間,以及請求到達率, 就可以比較簡單直觀的評估出連線池的大小, 而blocking的時間,也會決定最大等待佇列的限制,都可以根據排隊論理論做進一步的評估。

這時候連線已經能複用了,連線池的設定也比較合理了,假設SQL的優化上已經沒有空間了,這時候應用和DB就應該開始比較流暢的工作了,我們是不是可以高枕無憂, 矇頭睡覺了? 很遺憾,優化是一種毒藥,會讓人慾罷不能,整個SQL生命週期中有無數的點可以優化(今天主要是跟連線相關的,跟資料庫相關的優化以後會單獨拎出來扯)。 當我們發現很多情況下執行的都是相同的SQL, 管道雖然已經可以複用了, 但是每一次都把SQL發到資料庫上去執行, 都要進行網路互動,資料庫還是要重新解析,一遍遍的生成執行計劃再執行,代價還是非常高, 同樣的SQL是否能預編譯掉,省去資料庫硬解析的成本呢,或者能否減少網路的互動時間呢?這時候引入了PreparedStatement的概念,只在第一次傳送SQL到資料庫進行解析,然後就會將有關這個SQL的資訊儲存到PreparedStatement裡面, 這樣就可以被同樣的SQL語句反覆使用了。

對於ORACLE和OB來說,繫結變數下的SQL,使用PreparedStatement能夠顯著的提高系統的效能,這裡面要注意PreparedStatement的物件佔用JVM的記憶體大小,特別是拆分資料來源中,曾經發生過JVM記憶體被撐爆的情況。(JVM記憶體佔用情況=連線總數*PreparedStatementCache設定大小*每個PreparedStatement佔用的平均記憶體) ,在MYSQL資料庫中,因為沒有繫結變數這個概念,客戶端雖然可以設定PreparedStatement,但是在Server端只能在session級別共享一些資訊,每個SQL都還是需要進行解析的,所以效能不會有太大的影響, 我們實際的測試也驗證了這一論斷,目前MySQL官方也在做Server端全域性的PreparedStatement,不知道何時能夠出來。

再進一步看連線的優化點,資料庫的連線都是附帶狀態的,事務的狀態也是維持在連線上的,而一個連線在單位時間內只能處理一個事務請求, 所以需要多個連線來保證併發度,同時資料庫(MySQL)也需要建立相應多的執行緒來繫結這個關係, 那麼這個利用率是否足夠高呢? 一個連線+一個事務狀態+一個執行緒繫結在一起的狀態是否能被打破呢? 比如單連線一次傳送多個請求是否可行? 比如連線和(事務狀態+執行緒)的繫結是否能打破, 甚至全部打破?

接下來我們來講講大規模叢集下的連線問題, 我們拿ICDB叢集來舉列子,順便解答下剛才這個問題。記得13年的雙十一前夕,ICDB發生效能抖動的問題,把我們驚出了一身冷汗,現在看起來最主要的原因還是大量併發的請求導致MySQL出現抖動。

我們前面講到過資料庫的連線數和實際執行的執行緒數是兩個不同的概念,一個MySQL例項能支撐的連線數可以有很多,受MAX_connections控制,真正的天花板可能在核心的檔案控制程式碼,按照一個連線2M來算(預設一個thread建立連線需要分配stack,connect buffer,result buffer,應用層面的連線會更輕量一點),即使有一萬個連線所佔用的記憶體也只有20G,Server端能支撐得住。但是要注意的是,這些連線並不都是活躍的,也即在不會同時在執行的,如果DB上執行的活躍連線數過高,執行緒上下文切換的成本就會很高,DB的響應時間就往往就滿足不了業務的需求了,還有即使觀察看每秒DB的併發執行執行緒可能在200左右,但1秒之內請求不是平均分佈。在大連線下,很容易出現瞬間執行執行緒量巨大的情況。問題在於,在瞬間大量併發請求時,也就是活躍的連線數非常大的時候,MySQL對於併發處理的不夠好,容易產生效能波動,並持續惡化,進而影響應用響應時間。

所以大併發和多連線,其實是兩個問題,可以分開來看,但是這兩問題又不能孤立的來解決,多連線的情況下更有可能出現大併發 ,而解了多連線很大程度上也就緩解了高併發的問題,而如果完美的解決了高併發, 也許可能就不需要解多連線了。

為啥需要這麼多連線?我們分析下就可以得到, 一個例項的連線數由三個因素決定, 例項的DB數,連線池配置的MAX,以及連資料庫的應用機器數量。 假設一個例項有兩個DB,有500個應用伺服器會去連DB, 連線池的MAX配置是6, 那麼這個例項的連線總數就為 2* 500*6 =6000,而資料庫連線不斷增加很大程度上是受第三個因素的影響,其本質原因還是應用叢集規模增大了。

圍繞這三個因素做解法,第一個是通過拆分和降低連線池,降低單例項MySQL的連線數,比如原來一個例項上面有兩個DB, 通過拆分一個例項只有一個DB, 那麼在應用伺服器不變的情況下, 連線數就變成1*500*6=3000。

第二個就是提高DB響應時間,這樣在系統同樣處理能力的情況, 連線池的最大連線可以減少一半,前面little`s law 也提到過,響應時間縮短一倍, 同樣的處理能力,連線池只要三個連線,這樣進一步把連線數減少到 1*500*3 =1500,比如線上的tcbuyer叢集的MAX的設定就是2, 肯定比你想象的要小吧。

但是前面兩個改進的紅利, 很快就會被應用伺服器數量的增加給吃掉了,第三個解決辦法,也是徹底的解決辦法,就是減小應用叢集規模,比如採用應用邏輯分組, 甚至單元化部署來解決。單元化並不是為了減少MySQL連線數而做的,但是單元化之後確實可以有效降低連線數 。

前面的三個辦法能夠有效的解決大連線的問題,但是沒有解決高併發的問題,還是可能出現高併發把資料庫打垮的問題, 所以我們還是需要第四種方式, 來解決多連線的同時,進一步解決高併發的問題, 這個解法就是文章中間提到的,將(事務狀態+執行緒) 和連線解綁, 方案也比較多,比如增加一層Proxy (這個Proxy位置可以比較靈活), 但是鏈路複用需要對使用者SQL的上下文有依賴, 而且proxy的引入對穩定性和效能有一點的影響,所以不是很推薦。或者第二種辦法使用MySQL執行緒池,就是類似於Oracle的MTS模式,這種方案在我們線上高併發,短事務居多的情況下,是比較合適的,而且直接做到MySQL這一層是最合理的。

所以問題其實就是在高併發時,MySQL需要一個更好的排隊策略而已。圍繞這個思路,13年的雙十一我們採用的是MySQL高低水位限流版,如果出現大量併發請求,通過低水位來排隊, 同時通過高水位來削峰限流,即拒絕請求的方式,保證MySQL的響應時間,高水位限流這其實是一種損過載保護, 確保輸入不會大於DB的處理能力。到了14年的雙十一,我們徹底採用了執行緒池版本的MySQL,執行緒跟連線解綁開來,演化成更加合理的等待制排隊系統了。

用大家都熟悉的餐廳故事來解釋下,假設一家餐廳同時來了100個客人,但餐廳的產能不足,只能同時服務10位客人,MySQL原先的做法是找了100個服務員來接待這100個客人,然後這一百個服務員各種爭搶和廚師溝通的機會, 容易亂成一鍋粥, 高水位水位限流就是我只最多能讓50個客人進來 對後面50個客人說你回去吧, 我伺候不了你們了, 而執行緒池的做法,就是隻有10個服務員, 100個客人都乖乖的排隊, 等待分配服務員,保證分配了服務員的客人能夠享受餐廳的服務, 這樣廚師只要和這十個服務員打交道就可以了, 這樣能夠減少溝通, 切換, 資源爭用的成本。

講到這裡, 相信大家對資料庫的連線都有所瞭解了,還有一塊沒有涉及到,但是非常關鍵,就是資料庫的RT變化對應用服務的影響, 之前在雲化的過程中,直接拿RT的變化推導機器數是有問題的,應用達到瓶頸的時候其實處理執行緒池通常都還沒滿,所以有可能是DB RT增加了一點是完全沒影響的, 這個事情由大神圭多在牽頭,會建立有效的壓測模型,這對於提高資料庫的水位,探索DB和APP伺服器的最佳配比,最終降低成本是非常有意義的,等理清楚了再跟大家一起探討下。


相關文章