天天看點

璧說:從資料庫連接配接池說起

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

首先什麼是連接配接池,出現的原因是啥?我們可以從一個标準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伺服器的最佳配比,最終降低成本是非常有意義的,等理清楚了再跟大家一起探讨下。