22 | MySQL有哪些「飲鴆止渴」提升性能的方法?

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

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

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

短鏈接風暴

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

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

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

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

在機器負載比較高的時候,處理現有請求的時間變長,每一個鏈接保持的時間也更長。這時,再有新建鏈接的話,就可能會超過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語句加了哪些鎖:

  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的一個基礎設定,須要你在分析問題的時候要一直記得。

相關文章
相關標籤/搜索