22講MySQL有哪些“飲鴆止渴”提高性能的方法

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

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

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

短連接風暴

正常的短連接模式就是連接到數據庫後,執行很少的SQL語句就斷開,下次需要的時候再重連。如果使用的是短連接,在業務高峯期的時候,就可能出現連接數突然暴漲的情況。

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

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

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

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

碰到這種情況時,一個比較自然的想法,就是調高max_connections的值。但這樣做是有風險的。因爲設計max_connections這個參數的目的是想保護MySQL,如果我們把它改得太大,讓更多的連接都可以進來,那麼系統的負載可能會進一步加大,大量的資源耗費在權限驗證等邏輯上,結果可能是適得其反,已經連接的線程拿不到CPU資源去執行業務的SQL請求。

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

第一種方法:先處理掉那些佔着連接但是不工作的線程。

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秒執行show processlist,看到的結果是這樣的。
圖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 to MySQL server during query”。

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

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

所以,如果你是一個支持業務的DBA,不要假設所有的應用代碼都會被正確地處理。即使只是一個斷開連接的操作,也要確保通知到業務開發團隊。

第二種方法:減少連接過程的消耗。

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

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

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

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

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

慢查詢性能問題

在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,你可以通過下面的方式,增加一個語句改寫規則。

mysql> insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select * from t where id + 1 = ?", "select * from t where id = ? - 1", "db1");

call query_rewrite.flush_rewrite_rules();

這裏,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)。

QPS突增問題

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

我之前碰到過一類情況,是由一個新功能的bug導致的。當然,最理想的情況是讓業務把這個功能下掉,服務自然就會恢復。

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

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

  2. 如果這個新功能使用的是單獨的數據庫用戶,可以用管理員賬號把這個用戶刪掉,然後斷開現有連接。這樣,這個新功能的連接不成功,由它引發的QPS就會變成0。

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

當然,這個操作的風險很高,需要你特別細緻。它可能存在兩個副作用:

  1. 如果別的功能裏面也用到了這個SQL語句模板,會有誤傷;

  2. 很多業務並不是靠這一個語句就能完成邏輯的,所以如果單獨把這一個語句以select 1的結果返回的話,可能會導致後面的業務邏輯一起失敗。

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

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

小結

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

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

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

DBA雖然可以通過語句重寫來暫時處理問題,但是這本身是一個風險高的操作,做好SQL審計可以減少需要這類操作的機會。

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

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

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

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

上期問題時間

前兩期我給你留的問題是,下面這個圖的執行序列中,爲什麼session B的insert語句會被堵住。
在這裏插入圖片描述
我們用上一篇的加鎖規則來分析一下,看看session A的select語句加了哪些鎖:

由於是order by c desc,第一個要定位的是索引c上“最右邊的”c=20的行,所以會加上間隙鎖(20,25)和next-key lock (15,20]。

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

在掃描過程中,c=20、c=15、c=10這三行都存在值,由於是select *,所以會在主鍵id上加三個行鎖。

因此,session A 的select語句鎖的範圍就是:

索引c上 (5, 25);

主鍵索引上id=10、15、20三個行鎖。

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

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

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

@nero 同學的問題,提示我需要提醒大家注意,“有行”纔會加行鎖。如果查詢條件沒有命中行,那就加next-key lock。當然,等值判斷的時候,需要加上優化2(即:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,next-key lock退化爲間隙鎖。)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章