【MySQL】 如何在“海啸”下保命
作者:田杰
在數據庫的日常使用中,來自應用的高并發場景并不罕見,其標志性的表現為?高新連接創建速率(CPS,比如 PHP 短連接)、發送大量請求到 DB 數據庫層。
如同?海嘯,大量的新建連接和請求猛烈的沖擊考驗著 DB 層的處理能力,非常容易出現數據庫被沖擊 hang 住或響應極其緩慢的情況(想象下無預知無緩沖的短時間內突然工作量翻漲數倍,會不會立時被忙哭了 ^_^)。
而數據庫通常作為架構最下端的數據存取匯聚單元,其性能表現和穩定性往往決定了應用的最終表現和使用體驗,可謂業務生死之大事,不可不察。
由此,我們一起看一下 “海嘯” 場景下可以用來 “保命” 的各種解決方案。
注:
? 本文目標是總結高并發場景下的應對處理方法,而應對熱點更新(秒殺)場景的“招數”會另文介紹。
? 本文的主旨在于方便數據庫的使用者理解業務高并發請求場景下的保障 DB 可用性和穩定性的機制和方法,非機制的全面深度技術細節介紹。
1. 線程池
1.1 模型
我們舉一個生活中的例子方便大家理解?線程池(Thread Pool)。
比如有個銀行,有 10 個窗口(實例規格 CPU 數量),官方說可以容納 10 人(Client Thread)。平時呢,人也不多,一直順暢。稍微忙一點呢,大家擠擠。這個 10 人的地方,擠個 50 人也可以(不是每個人時刻都在窗口辦業務)。效率也挺高。
年底發工資、公司結算、發行紀念幣來了一大幫人,大家一起擠,誰也不讓,就把銀行擠滿了;大家接踵摩肩,動也動不了,再發生些爭搶,那這銀行誰也辦不了業務了。
好了,來個保安(Timer Thread),搞了個隊伍機制(10 個隊列 loose_thread_pool_size = 10),按規定執行,一次放 10 個人。這個效率也不錯。
當然了,如果一下子來了 1000 個人,那么門口等待的隊伍會很長,雖然不致于把銀行撐爆,但是后面的同學要等很長時間,有的會去抱怨了(應用側等待超過自身定義的超時時間后返回錯誤)。
問題來了,有的同學搞不清楚買哪種紀念幣,一直在看看停停,保安看他們也不像馬上能決定的樣子,而且窗口柜員也不是非常忙,保安就又搞了個規則,叫?“stall_limit”。
看一些同學猶豫超過?stall_limit?定義的時間,那么就算他們?stall?了,可以再放 1 個人進去(oversubscribing)。但去窗口辦業務的人數是有上限的,最多 50 個人(10 個窗口每個窗口 5 個人,?loose_thread_pool_oversubscribe = 4)。
之后,只能出一個,進一個; 如果都不出來,那也 hang 了。這個時候,至少要讓保安能進去,把這些太慢的同學趕出來幾個,讓等待的隊列動起來。
還有,有的同學在里面發現忘帶證件了,需要等送進來。他們找地方等(lock wait)。那么他們是在等待了,這個是不算?oversubscribing?數量的,所以保安也可以放人,一直放到?thread_pool_max_threads?個人。
如果證件還沒送來,那么銀行就被這些等證件的霸占了(hang 了)。另外如果一下子證件都送來,那這個銀行一下子忙起來,也爆了(熱點更新)。
當然如果這個銀行沒有大量客戶同時辦業務的場景,是可以不需要搞個保安,不需要搞個隊伍的(loose_thread_pool_enabled = OFF)。這個銀行本身最多可以 50 個人,但是保安只讓 10 個人進去,那效率就會低了。
還有,門口等待隊伍長了,這個可以有 3 種可能,
? 顧客動作慢(慢 SQL),建議考慮優化 SQL 降低執行成本。
? 銀行小, 窗口數量少(實例規格小)建議擴店(升級實例規格)。
? 窗口動作慢(物理機問題、數據庫 bug;不在本文討論范圍內)。
從上面的例子中,我們可以看到?Thread Pool?是通過隊列機制限制數據庫的?Client Thread?的并發度(控制?Running Thread?數量),避免大量的爭搶和創建 Client Thread 的開銷來提升 CPU 使用 效率,保障吞吐的(在應用給與 DB 的訪問壓力不斷增加的情況下,保持 DB 吞吐處理能力)。
1.2 適用場景
如果我們仔細品位下上面的例子,可以發現 Thread Pool 的適用場景:
? 每個要辦的業務簡短(OLTP 場景)且性能瓶頸在 CPU 資源上
? 場景中不存在 大量 需要長時間執行且無停頓(可以暫時不使用 CPU)的 SQL
? 能夠接受一定損失(錯誤/開銷)的業務(啟用 Thread Pool 后需要一定開銷,存在簡單的查詢比不啟用 Thread Pool 的情況下執行時間增加的可能,比如被分配到了 stall 的 thread group 而要花時間等待執行)
1.3 小結
| 1 | loose_thread_pool_enabled | Yes | 是否啟用 Thread Pool |
| 2 | loose_thread_pool_oversubscribe | Yes | 每個 Thread Group 在出現 Stall Thread 的情況下可以額外同時執行(active)的線程個數;線程池最多可以同時執行(active)的線程數 =(thread_pool_oversubscribe + 1)* thread_pool_size;建議 >=3 |
| 3 | tloose_thread_pool_size | Yes (RDS) | Thread Pool 中分組(Thread Group)的個數,建議設置為實例規格 CPU 個數 |
| 4 | thread_pool_max_threads | No | Thread Pool 中最大線程數量,到達這個數量后,無法再創建新的 thread |
| 5 | thread_pool_idle_timeout | No | Thread Group 中空閑的線程退出前的空閑等待(idle)時間 |
| 6 | thread_pool_stall_limit | No | Timer Thread 檢查 “Stall” 情況的間隔,避免一個 thread 長時間霸占一個 thread group |
那么面對存在長時間執行的查詢,除了優化 SQL 降低執行成本外(有時不具有可操作性,當然如果該查詢對數據時效性不敏感可以考慮轉移到只讀實例上執行),是否還有其他招數可用? 請看下一招“限流”。
2. 限流
如果“海嘯”來的異常猛烈,并且在“海嘯”中能夠定義出一批帶有同樣特征的查詢,比如 Redis 緩存被擊穿,大量相似重復查詢打到 DB 層,或者如上例 Thread Pool 中的長時間執行的查詢,那么在業務支持/允許降級的情況下我們可以通過對這批請求采取限流的方式來“保命”。
相對 thread pool 這種對“海嘯” 全方位覆蓋的應對機制,限流更像是集力量于一點的定向打擊。
2.1 Statement Concurrency Control
對于 RDS for MySQL 8.0 和 PolarDB for MySQL,我們可以通過“語句并發控制”(Statement Concurrency Control)特性來實現針對指定語句的限流。
比如發現下面的查詢在高并發的場景下拖累了整個實例的性能,和業務核實,業務可以接受該查詢被限流。
Copy
確定 SQL 語句后,可以根據語句特征來調用 dbms_ccl 工具包創建規則進行限流。
Copy
限流規則添加后,超過定義的并發度的 SQL 請求在 "Concurrency control waiting" 狀態
限流前后對比,可以看到限流后 CPU 使用率從 100% 降低到 50% 左右,有效恢復業務可用性。
2.2 DAS 限流
對于 RDS for MySQL 5.6 和 5.7 ,控制臺的 CloudDBA 功能直接集成了 SQL 限流功能。
我們來看一個真實生活中的例子,某客戶在業務高峰期出現大量的集中請求,導致高配實例 CPU 完全打滿,由于實例響應極其緩慢,能采集到的監控數據顯示當時 活動會話達到 14700+ 。
在業務層反復調整無法恢復的情況下 在 2020.3.24 21:35 通過設置 SQL 限流恢復了業務可用性。
RDS 實例會話情況
RDS 實例 CPU 使用率情況
3. 御敵于外
上面介紹的都是數據庫層面的應對之策,那么是否我們一定要被動的在數據庫層面“兵來將擋”呢?有沒有主動“御敵于外”的辦法呢?
3.1 名詞解釋
| 1 | 短連接 | 通信雙方有數據交互時,就建立一個 TCP 連接,數據發送完成后,則斷開此 TCP 連接;通常基于 PHP 語言的應用采用短連接方式訪問數據庫 |
| 2 | 長連接 | 通信雙方有數據交互時,首先嘗試復用已有空閑 TCP 連接,如果沒有空閑 TCP 連接則嘗試創建新連接;數據發送完成后,通常不斷開此 TCP 連接以便后續復用;通常基于 Java 語言的應用采用長連接方式訪問數據庫 |
| 3 | syn queue | 用于存儲接收到的 syn 請求的連接 socket 隊列,TCP 協議棧接收到 syn 后系統內核自動回復 syn,ack 同時將 syn 代表的連接放入到 syn queue 隊列中,并管理是否需要重傳 syn,ack;其長度由 tcp_max_syn_backlog (或 somaxconn)Linux 內核參數確定 |
| 4 | accept queue | 用于存儲完成 TCP 三次握手的連接 socket 隊列,當 MySQL 調用 accept() 時從該隊列取走一個 socket 處理,其長度由 應用設置的 backlog 參數和內核參數 somaxconn 的較小值決定 |
| 5 | ListenOverFlow | 由于 syn queue 已經打滿,新收到的 syn 請求不被處理而丟棄的場景發生數量 |
| 6 | ListenDrops | 由于 accept queue 已經打滿,完成 TCP 三次握手的連接不被處理而丟棄的場景發生數量 |
3.2 短連接優化
首先我們來看看一個普通的 SQL 請求是如何被從應用通過網絡發送給 DB 層進而得到處理的。
仔細看一下上述時序圖,就會發現如果應用和數據庫之間在沒有可用的網絡連接情況下,需要首先建立起一條基于 TCP/IP 協議棧的 MySQL 網絡連接才能夠將 SQL 請求發送給數據庫實例并獲取到處理的結果集。
在應用采用短連接機制(比如基于 PHP 語言開發的應用)的情況下,每個 SQL/Query 都需要和數據庫實例創建一個 TCP 網絡連接,需要消耗數據庫實例(和其所在物理機)的 CPU 資源。
在“海嘯”的場景下,采用短連接機制的應用會保持很高的新連接創建速率(CPS,大于等于 QPS),這樣在高負載(QPS) 的基礎上進一步消耗數據庫實例的 CPU 資源,拉高 CPU 使用率,降低 CPU 使用效率,進入惡性循環容易觸發數據庫雪崩式崩潰。
在 CPU 資源緊張的情況下會出現大量連接請求積壓無法處理而觸發 ListenOverFlow 和 ListenDrops 情況出現。
這里我們看一個真實世界中的例子。
客戶在 13:30 將應用從長連接模式調整為短連接模式,由于短連接模式的高并發新建連接請求速率(CPS - 每秒新建連接數),修改后實例 CPU 使用率總體上升 25+% 左右,業務側出現大量連接失敗錯誤并感知 RDS 實例響應緩慢。
部分 CPU 被完全打滿,無法滿足處理高連接請求的需求而出現 ListenOverFlow / ListenDrops。
線程池 Thread Pool 是數據庫層對該場景較好的解決方案,而啟用了數據庫獨立代理(RDS for MySQL 讀寫分離地址 和 PolarDB for MySQL 的集群地址)的實例還可以選擇啟用“短連接優化”的鏈路層解決方案。
當應用斷開連接后,數據庫獨享代理會判斷之前的連接是否為空閑(idle)連接,如果是空閑連接,代理會將代理與數據庫之間的連接保留在連接池內一段時間(僅釋放應用與代理之間的連接)。
在保留連接的這段時間內如果應用發起新連接,代理會直接從連接池里使用保留的連接,從而減少與數據庫建立連接的開銷。
官方文檔:短連接優化
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的【MySQL】 如何在“海啸”下保命的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云原生基础架构的最佳状态,就是没有架构?
- 下一篇: 一套 SQL 搞定数据仓库?Flink有