MySQL實戰45講學習筆記:第二十二講

 1、引子

不知道你在實際運維過程當中有沒有碰到這樣的情景:業務高峯期,生產環境的 MySQL 壓力太大,無法正常響應,須要短時間內、臨時性地提高一些性能。html

我之前作業務護航的時候,就偶爾會碰上這種場景。用戶的開發負責人說,無論你用什麼方案,讓業務先跑起來再說。mysql

但,若是是無損方案的話,確定不須要等到這個時候才上場。今天咱們就來聊聊這些臨時方案,並着重說一說它們可能存在的風險sql

2、短鏈接風暴

正常的短鏈接模式就是鏈接到數據庫後,執行不多的 SQL 語句就斷開,下次須要的時候再重連。若是使用的是短鏈接,在業務高峯期的時候,就可能出現鏈接數忽然暴漲的狀況。數據庫

我在第 1 篇文章《基礎架構:一條 SQL 查詢語句是如何執行的?》中說過,MySQL 創建鏈接的過程,成本是很高的。除了正常的網絡鏈接三次握手外,還須要作登陸權限判斷和
得到這個鏈接的數據讀寫權限。安全

在數據庫壓力比較小的時候,這些額外的成本並不明顯。網絡

可是,短鏈接模型存在一個風險,就是一旦數據庫處理得慢一些,鏈接數就會暴漲。max_connections 參數,用來控制一個 MySQL 實例同時存在的鏈接數的上限,超過這
個值,系統就會拒絕接下來的鏈接請求,並報錯提示「Too many connections」。對於被拒絕鏈接的請求來講,從業務角度看就是數據庫不可用。session

在機器負載比較高的時候,處理現有請求的時間變長,每一個鏈接保持的時間也更長。這時,再有新建鏈接的話,就可能會超過 max_connections 的限制。架構

碰到這種狀況時,一個比較天然的想法,就是調高 max_connections 的值。但這樣作是有風險的。由於設計 max_connections 這個參數的目的是想保護 MySQL,若是咱們把
它改得太大,讓更多的鏈接均可以進來,那麼系統的負載可能會進一步加大,大量的資源耗費在權限驗證等邏輯上,結果多是拔苗助長,已經鏈接的線程拿不到 CPU 資源去執行
業務的 SQL 請求。運維

那麼這種狀況下,你還有沒有別的建議呢?我這裏還有兩種方法,但要注意,這些方法都是有損的。工具

3、短連接風暴的處理方法一:先處理掉那些佔着鏈接可是不工做的線程。

max_connections 的計算,不是看誰在 running,是隻要連着就佔用一個計數位置。對於那些不須要保持的鏈接,咱們能夠經過 kill connection 主動踢掉。這個行爲跟事先設置
wait_timeout 的效果是同樣的。設置 wait_timeout 參數表示的是,一個線程空閒wait_timeout 這麼多秒以後,就會被 MySQL 直接斷開鏈接。

可是須要注意,在 show processlist 的結果裏,踢掉顯示爲 sleep 的線程,多是有損的。咱們來看下面這個例子。

圖 1 sleep 線程的兩種狀態

在上面這個例子裏,若是斷開 session A 的鏈接,由於這時候 session A 尚未提交,因此 MySQL 只能按照回滾事務來處理;而斷開 session B 的鏈接,就沒什麼大影響。所
以,若是按照優先級來講,你應該優先斷開像 session B 這樣的事務外空閒的鏈接。

可是,怎麼判斷哪些是事務外空閒的呢?session C 在 T 時刻以後的 30 秒執行 showprocesslist,看到的結果是這樣的。

圖 2 sleep 線程的兩種狀態,show processlist 結果

圖中 id=4 和 id=5 的兩個會話都是 Sleep 狀態。而要看事務具體狀態的話,你能夠查information_schema 庫的 innodb_trx 表。

圖 3 從 information_schema.innodb_trx 查詢事務狀態

這個結果裏,trx_mysql_thread_id=4,表示 id=4 的線程還處在事務中。

所以,若是是鏈接數過多,你能夠優先斷開事務外空閒過久的鏈接;若是這樣還不夠,再考慮斷開事務內空閒過久的鏈接。

從服務端斷開鏈接使用的是 kill connection + id 的命令, 一個客戶端處於 sleep 狀態時,它的鏈接被服務端主動斷開後,這個客戶端並不會立刻知道。直到客戶端在發起下一
個請求的時候,纔會收到這樣的報錯「ERROR 2013 (HY000): Lost connection toMySQL server during query」。

從數據庫端主動斷開鏈接多是有損的,尤爲是有的應用端收到這個錯誤後,不從新鏈接,而是直接用這個已經不能用的句柄重試查詢。這會致使從應用端看上去,「MySQL
一直沒恢復」。

你可能以爲這是一個冷笑話,但實際上我碰到過不下 10 次。

因此,若是你是一個支持業務的 DBA,不要假設全部的應用代碼都會被正確地處理。即便只是一個斷開鏈接的操做,也要確保通知到業務開發團隊。

4、短連接風暴的處理方法二:減小鏈接過程的消耗。

有的業務代碼會在短期內先大量申請數據庫鏈接作備用,若是如今數據庫確認是被鏈接行爲打掛了,那麼一種可能的作法,是讓數據庫跳過權限驗證階段。

跳過權限驗證的方法是:重啓數據庫,並使用–skip-grant-tables 參數啓動。這樣,整個MySQL 會跳過全部的權限驗證階段,包括鏈接過程和語句執行過程在內。

可是,這種方法特別符合咱們標題裏說的「飲鴆止渴」,風險極高,是我特別不建議使用的方案。尤爲你的庫外網可訪問的話,就更不能這麼作了。

在 MySQL 8.0 版本里,若是你啓用–skip-grant-tables 參數,MySQL 會默認把 --skip-networking 參數打開,表示這時候數據庫只能被本地的客戶端鏈接。可見,MySQL 官方
對 skip-grant-tables 這個參數的安全問題也很重視。

除了短鏈接數暴增可能會帶來性能問題外,實際上,咱們在線上碰到更多的是查詢或者更新語句致使的性能問題。其中,查詢問題比較典型的有兩類,一類是由新出現的慢查詢導
致的,一類是由 QPS(每秒查詢數)突增致使的。而關於更新語句致使的性能問題,我會在下一篇文章和你展開說明。

5、慢查詢性能問題

在 MySQL 中,會引起性能問題的慢查詢,大致有如下三種可能:

1. 索引沒有設計好;
2. SQL 語句沒寫好;
3. MySQL 選錯了索引。

接下來,咱們就具體分析一下這三種可能,以及對應的解決方案。

一、致使慢查詢的第一種多是,索引沒有設計好。

這種場景通常就是經過緊急建立索引來解決。MySQL 5.6 版本之後,建立索引都支持Online DDL 了,對於那種高峯期數據庫已經被這個語句打掛了的狀況,最高效的作法就
是直接執行 alter table 語句。

比較理想的是可以在備庫先執行。假設你如今的服務是一主一備,主庫 A、備庫 B,這個方案的大體流程是這樣的:

1. 在備庫 B 上執行 set sql_log_bin=off,也就是不寫 binlog,而後執行 alter table 語句加上索引;

2. 執行主備切換;

3. 這時候主庫是 B,備庫是 A。在 A 上執行 set sql_log_bin=off,而後執行 alter table語句加上索引。

這是一個「古老」的 DDL 方案。平時在作變動的時候,你應該考慮相似 gh-ost 這樣的方案,更加穩妥。可是在須要緊急處理時,上面這個方案的效率是最高的。

二、致使慢查詢的第二種多是,語句沒寫好。

好比,咱們犯了在第 18 篇文章《爲何這些 SQL 語句邏輯相同,性能卻差別巨大?》中提到的那些錯誤,致使語句沒有使用上索引。

這時,咱們能夠經過改寫 SQL 語句來處理。MySQL 5.7 提供了 query_rewrite 功能,能夠把輸入的一種語句改寫成另一種模式。

好比,語句被錯誤地寫成了 select * from t where id + 1 = 10000,你能夠經過下面的方式,增長一個語句改寫規則。

這裏,call query_rewrite.flush_rewrite_rules() 這個存儲過程,是讓插入的新規則生效,也就是咱們說的「查詢重寫」。你能夠用圖 4 中的方法來確認改寫規則是否生效。

圖 4 查詢重寫效果

三、致使慢查詢的第三種可能,就是碰上了咱們在第 10 篇文章《MySQL 爲何有時候會選錯

索引?》中提到的狀況,MySQL 選錯了索引

這時候,應急方案就是給這個語句加上 force index。

一樣地,使用查詢重寫功能,給原來的語句加上 force index,也能夠解決這個問題。

上面我和你討論的由慢查詢致使性能問題的三種可能狀況,實際上出現最多的是前兩種,即:索引沒設計好和語句沒寫好。而這兩種狀況,偏偏是徹底能夠避免的。好比,經過下
面這個過程,咱們就能夠預先發現問題。

1. 上線前,在測試環境,把慢查詢日誌(slow log)打開,而且把 long_query_time 設置成 0,確保每一個語句都會被記錄入慢查詢日誌

2. 在測試表裏插入模擬線上的數據,作一遍迴歸測試;

3. 觀察慢查詢日誌裏每類語句的輸出,特別留意 Rows_examined 字段是否與預期一致。(咱們在前面文章中已經屢次用到過 Rows_examined 方法了,相信你已經動手嘗試過
了。若是還有不明白的,歡迎給我留言,咱們一塊兒討論)。

不要吝嗇這段花在上線前的「額外」時間,由於這會幫你省下不少故障覆盤的時間。

若是新增的 SQL 語句很少,手動跑一下就能夠。而若是是新項目的話,或者是修改了原有項目的 表結構設計,全量回歸測試都是必要的。這時候,你須要工具幫你檢查全部的 SQL
語句的返回結果。好比,你可使用開源工具 pt-query-digest(https://www.percona.com/doc/percona-toolkit/3.0/pt-query-digest.html)。

6、QPS 突增問題

有時候因爲業務忽然出現高峯,或者應用程序 bug,致使某個語句的 QPS 忽然暴漲,也可能致使 MySQL 壓力過大,影響服務。

我以前碰到過一類狀況,是由一個新功能的 bug 致使的。固然,最理想的狀況是讓業務把這個功能下掉,服務天然就會恢復。

而下掉一個功能,若是從數據庫端處理的話,對應於不一樣的背景,有不一樣的方法可用。我這裏再和你展開說明一下。

1. 一種是由全新業務的 bug 致使的。假設你的 DB 運維是比較規範的,也就是說白名單是一個個加的。這種狀況下,若是你可以肯定業務方會下掉這個功能,只是時間上沒那
麼快,那麼就能夠從數據庫端直接把白名單去掉。

2. 若是這個新功能使用的是單獨的數據庫用戶,能夠用管理員帳號把這個用戶刪掉,而後斷開現有鏈接。這樣,這個新功能的鏈接不成功,由它引起的 QPS 就會變成 0。

3. 若是這個新增的功能跟主體功能是部署在一塊兒的,那麼咱們只能經過處理語句來限制。這時,咱們可使用上面提到的查詢重寫功能,把壓力最大的 SQL 語句直接重寫
成"select 1"返回。

固然,這個操做的風險很高,須要你特別細緻。它可能存在兩個反作用:

1. 若是別的功能裏面也用到了這個 SQL 語句模板,會有誤傷;
2. 不少業務並非靠這一個語句就能完成邏輯的,因此若是單獨把這一個語句以 select 1的結果返回的話,可能會致使後面的業務邏輯一塊兒失敗。

因此,方案 3 是用於止血的,跟前面提到的去掉權限驗證同樣,應該是你全部選項裏優先級最低的一個方案。

同時你會發現,其實方案 1 和 2 都要依賴於規範的運維體系:虛擬化、白名單機制、業務帳號分離。因而可知,更多的準備,每每意味着更穩定的系統。

7、小結

今天這篇文章,我以業務高峯期的性能問題爲背景,和你介紹了一些緊急處理的手段。

這些處理手段中,既包括了粗暴地拒絕鏈接和斷開鏈接,也有經過重寫語句來繞過一些坑的方法;既有臨時的高危方案,也有未雨綢繆的、相對安全的預案。

在實際開發中,咱們也要儘可能避免一些低效的方法,好比避免大量地使用短鏈接。同時,若是你作業務開發的話,要知道,鏈接異常斷開是常有的事,你的代碼裏要有正確地重連
並重試的機制。

DBA 雖然能夠經過語句重寫來暫時處理問題,可是這自己是一個風險高的操做,作好SQL 審計能夠減小須要這類操做的機會。

其實,你能夠看得出來,在這篇文章中我提到的解決方法主要集中在 server 層。在下一篇文章中,我會繼續和你討論一些跟 InnoDB 有關的處理方法。

最後,又到了咱們的思考題時間了。

今天,我留給你的課後問題是,你是否碰到過,在業務高峯期須要臨時救火的場景?你又是怎麼處理的呢?

你能夠把你的經歷和經驗寫在留言區,我會在下一篇文章的末尾選取有趣的評論跟你們一塊兒分享和分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

8、上期問題時間

前兩期我給你留的問題是,下面這個圖的執行序列中,爲何 session B 的 insert 語句會被堵住。

咱們用上一篇的加鎖規則來分析一下,看看 session A 的 select 語句加了哪些鎖:

1. 因爲是 order by c desc,第一個要定位的是索引 c 上「最右邊的」c=20 的行,因此會加上間隙鎖 (20,25) 和 next-key lock (15,20]。

2. 在索引 c 上向左遍歷,要掃描到 c=10 才停下來,因此 next-key lock 會加到 (5,10],這正是阻塞 session B 的 insert 語句的緣由。

3. 在掃描過程當中,c=20、c=1五、c=10 這三行都存在值,因爲是 select *,因此會在主鍵 id 上加三個行鎖。

所以,session A 的 select 語句鎖的範圍就是:

1. 索引 c 上 (5, 25);
2. 主鍵索引上 id=1五、20 兩個行鎖。

這裏,我再囉嗦下,你會發現我在文章中,每次加鎖都會說明是加在「哪一個索引上」的。由於,鎖就是加在索引上的,這是 InnoDB 的一個基礎設定,須要你在分析問題的時候要
一直記得。

評論區留言點贊板:

@HuaMax 給出了正確的解釋。

@Justin 同窗提了個好問題,<= 究竟是間隙鎖仍是行鎖?其實,這個問題,你要跟「執行過程」配合起來分析。在 InnoDB 要去找「第一個值」的
時候,是按照等值去找的,用的是等值判斷的規則;找到第一個值之後,要在索引內找「下一個值」,對應於咱們規則中說的範圍查找。

@信信 提了一個不錯的問題,要知道最終的加鎖是根據實際執行狀況來的。因此,若是一個 select * from … for update 語句,優化器決定使用全表掃
描,那麼就會把主鍵索引上 next-key lock 全加上。

@nero 同窗的問題,提示我須要提醒你們注意,「有行」纔會加行鎖。若是查詢條件沒有命中行,那就加 next-key lock。固然,等值判斷的時候,
須要加上優化 2(即:索引上的等值查詢,向右遍歷時且最後一個值不知足等值條件的時候,next-key lock 退化爲間隙鎖。)。

@小李子、@發條橙子同窗,都提了很好的問題,這期高質量評論不少,你也均可以去看看。

最後,我要爲元旦期間還堅持學習的同窗們,點個贊 ^_^

相關文章
相關標籤/搜索