璧說:從數據庫連接池說起
該文章來自於阿裏巴巴技術協會(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服務器的最佳配比,最終降低成本是非常有意義的,等理清楚了再跟大家一起探討下。
最後更新:2017-04-01 13:37:09