29 | 如何判斷一個數據庫是否是出問題了?

我在第2527篇文章中,和你介紹了主備切換流程。經過這些內容的講解,你應該已經很清楚了:在一主一備的雙M架構裏,主備切換隻須要把客戶端流量切到備庫;而在一主多從架構裏,主備切換除了要把客戶端流量切到備庫外,還須要把從庫接到新主庫上。mysql

主備切換有兩種場景,一種是主動切換,一種是被動切換。而其中被動切換,每每是由於主庫出問題了,由HA系統發起的。sql

這也就引出了咱們今天要討論的問題:怎麼判斷一個主庫出問題了?數據庫

你必定會說,這很簡單啊,連上MySQL,執行個select 1就行了。可是select 1成功返回了,就表示主庫沒問題嗎?服務器

select 1判斷

實際上,select 1成功返回,只能說明這個庫的進程還在,並不能說明主庫沒問題。如今,咱們來看一下這個場景。session

set global innodb_thread_concurrency=3;

CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

insert into t values(1,1)

圖1 查詢blocked

咱們設置innodb_thread_concurrency參數的目的是,控制InnoDB的併發線程上限。也就是說,一旦併發線程數達到這個值,InnoDB在接收到新請求的時候,就會進入等待狀態,直到有線程退出。架構

這裏,我把innodb_thread_concurrency設置成3,表示InnoDB只容許3個線程並行執行。而在咱們的例子中,前三個session 中的sleep(100),使得這三個語句都處於「執行」狀態,以此來模擬大查詢。併發

你看到了, session D裏面,select 1是能執行成功的,可是查詢表t的語句會被堵住。也就是說,若是這時候咱們用select 1來檢測實例是否正常的話,是檢測不出問題的。性能

在InnoDB中,innodb_thread_concurrency這個參數的默認值是0,表示不限制併發線程數量。可是,不限制併發線程數確定是不行的。由於,一個機器的CPU核數有限,線程全衝進來,上下文切換的成本就會過高。測試

因此,一般狀況下,咱們建議把innodb_thread_concurrency設置爲64~128之間的值。這時,你必定會有疑問,併發線程上限數設置爲128夠幹啥,線上的併發鏈接數動不動就上千了。spa

產生這個疑問的緣由,是搞混了併發鏈接和併發查詢。

併發鏈接和併發查詢,並非同一個概念。你在show processlist的結果裏,看到的幾千個鏈接,指的就是併發鏈接。而「當前正在執行」的語句,纔是咱們所說的併發查詢。

併發鏈接數達到幾千個影響並不大,就是多佔一些內存而已。咱們應該關注的是併發查詢,由於併發查詢過高才是CPU殺手。這也是爲何咱們須要設置innodb_thread_concurrency參數的緣由。

而後,你可能還會想起咱們在第7篇文章中講到的熱點更新和死鎖檢測的時候,若是把innodb_thread_concurrency設置爲128的話,那麼出現同一行熱點更新的問題時,是否是很快就把128消耗完了,這樣整個系統是否是就掛了呢?

實際上,在線程進入鎖等待之後,併發線程的計數會減一,也就是說等行鎖(也包括間隙鎖)的線程是不算在128裏面的。

MySQL這樣設計是很是有意義的。由於,進入鎖等待的線程已經不吃CPU了;更重要的是,必須這麼設計,才能避免整個系統鎖死。

爲何呢?假設處於鎖等待的線程也佔併發線程的計數,你能夠設想一下這個場景:

  1. 線程1執行begin; update t set c=c+1 where id=1, 啓動了事務trx1, 而後保持這個狀態。這時候,線程處於空閒狀態,不算在併發線程裏面。

  2. 線程2到線程129都執行 update t set c=c+1 where id=1; 因爲等行鎖,進入等待狀態。這樣就有128個線程處於等待狀態;

  3. 若是處於鎖等待狀態的線程計數不減一,InnoDB就會認爲線程數用滿了,會阻止其餘語句進入引擎執行,這樣線程1不能提交事務。而另外的128個線程又處於鎖等待狀態,整個系統就堵住了。

下圖2顯示的就是這個狀態。

圖2 系統鎖死狀態(假設等行鎖的語句佔用併發計數)

這時候InnoDB不能響應任何請求,整個系統被鎖死。並且,因爲全部線程都處於等待狀態,此時佔用的CPU倒是0,而這明顯不合理。因此,咱們說InnoDB在設計時,遇到進程進入鎖等待的狀況時,將併發線程的計數減1的設計,是合理並且是必要的。

雖說等鎖的線程不算在併發線程計數裏,但若是它在真正地執行查詢,就好比咱們上面例子中前三個事務中的select sleep(100) from t,仍是要算進併發線程的計數的。

在這個例子中,同時在執行的語句超過了設置的innodb_thread_concurrency的值,這時候系統其實已經不行了,可是經過select 1來檢測系統,會認爲系統仍是正常的。

所以,咱們使用select 1的判斷邏輯要修改一下。

查表判斷

爲了可以檢測InnoDB併發線程數過多致使的系統不可用狀況,咱們須要找一個訪問InnoDB的場景。通常的作法是,在系統庫(mysql庫)裏建立一個表,好比命名爲health_check,裏面只放一行數據,而後按期執行:

mysql> select * from mysql.health_check;

使用這個方法,咱們能夠檢測出因爲併發線程過多致使的數據庫不可用的狀況。

可是,咱們立刻還會碰到下一個問題,即:空間滿了之後,這種方法又會變得很差使。

咱們知道,更新事務要寫binlog,而一旦binlog所在磁盤的空間佔用率達到100%,那麼全部的更新語句和事務提交的commit語句就都會被堵住。可是,系統這時候仍是能夠正常讀數據的。

所以,咱們仍是把這條監控語句再改進一下。接下來,咱們就看看把查詢語句改爲更新語句後的效果。

更新判斷

既然要更新,就要放個有意義的字段,常見作法是放一個timestamp字段,用來表示最後一次執行檢測的時間。這條更新語句相似於:

mysql> update mysql.health_check set t_modified=now();

節點可用性的檢測都應該包含主庫和備庫。若是用更新來檢測主庫的話,那麼備庫也要進行更新檢測。

但,備庫的檢測也是要寫binlog的。因爲咱們通常會把數據庫A和B的主備關係設計爲雙M結構,因此在備庫B上執行的檢測命令,也要發回給主庫A。

可是,若是主庫A和備庫B都用相同的更新命令,就可能出現行衝突,也就是可能會致使主備同步中止。因此,如今看來mysql.health_check 這個表就不能只有一行數據了。

爲了讓主備之間的更新不產生衝突,咱們能夠在mysql.health_check表上存入多行數據,並用A、B的server_id作主鍵。

mysql> CREATE TABLE `health_check` (
`id` int(11) NOT NULL,
`t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

/* 檢測命令 */
insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();

因爲MySQL規定了主庫和備庫的server_id必須不一樣(不然建立主備關係的時候就會報錯),這樣就能夠保證主、備庫各自的檢測命令不會發生衝突。

更新判斷是一個相對比較經常使用的方案了,不過依然存在一些問題。其中,「斷定慢」一直是讓DBA頭疼的問題。

你必定會疑惑,更新語句,若是失敗或者超時,就能夠發起主備切換了,爲何還會有斷定慢的問題呢?

其實,這裏涉及到的是服務器IO資源分配的問題。

首先,全部的檢測邏輯都須要一個超時時間N。執行一條update語句,超過N秒後還不返回,就認爲系統不可用。

你能夠設想一個日誌盤的IO利用率已是100%的場景。這時候,整個系統響應很是慢,已經須要作主備切換了。

可是你要知道,IO利用率100%表示系統的IO是在工做的,每一個請求都有機會得到IO資源,執行本身的任務。而咱們的檢測使用的update命令,須要的資源不多,因此可能在拿到IO資源的時候就能夠提交成功,而且在超時時間N秒未到達以前就返回給了檢測系統。

檢測系統一看,update命令沒有超時,因而就獲得了「系統正常」的結論。

也就是說,這時候在業務系統上正常的SQL語句已經執行得很慢了,可是DBA上去一看,HA系統還在正常工做,而且認爲主庫如今處於可用狀態。

之因此會出現這個現象,根本緣由是咱們上面說的全部方法,都是基於外部檢測的。外部檢測自然有一個問題,就是隨機性。

由於,外部檢測都須要定時輪詢,因此係統可能已經出問題了,可是卻須要等到下一個檢測發起執行語句的時候,咱們纔有可能發現問題。並且,若是你的運氣不夠好的話,可能第一次輪詢還不能發現,這就會致使切換慢的問題。

因此,接下來我要再和你介紹一種在MySQL內部發現數據庫問題的方法。

內部統計

針對磁盤利用率這個問題,若是MySQL能夠告訴咱們,內部每一次IO請求的時間,那咱們判斷數據庫是否出問題的方法就可靠得多了。

其實,MySQL 5.6版本之後提供的performance_schema庫,就在file_summary_by_event_name表裏統計了每次IO請求的時間。

file_summary_by_event_name表裏有不少行數據,咱們先來看看event_name='wait/io/file/innodb/innodb_log_file’這一行。

圖3 performance_schema.file_summary_by_event_name的一行

圖中這一行表示統計的是redo log的寫入時間,第一列EVENT_NAME 表示統計的類型。

接下來的三組數據,顯示的是redo log操做的時間統計。

第一組五列,是全部IO類型的統計。其中,COUNT_STAR是全部IO的總次數,接下來四列是具體的統計項, 單位是皮秒;前綴SUM、MIN、AVG、MAX,顧名思義指的就是總和、最小值、平均值和最大值。

第二組六列,是讀操做的統計。最後一列SUM_NUMBER_OF_BYTES_READ統計的是,總共從redo log裏讀了多少個字節。

第三組六列,統計的是寫操做。

最後的第四組數據,是對其餘類型數據的統計。在redo log裏,你能夠認爲它們就是對fsync的統計。

在performance_schema庫的file_summary_by_event_name表裏,binlog對應的是event_name = "wait/io/file/sql/binlog"這一行。各個字段的統計邏輯,與redo log的各個字段徹底相同。這裏,我就再也不贅述了。

由於咱們每一次操做數據庫,performance_schema都須要額外地統計這些信息,因此咱們打開這個統計功能是有性能損耗的。

個人測試結果是,若是打開全部的performance_schema項,性能大概會降低10%左右。因此,我建議你只打開本身須要的項進行統計。你能夠經過下面的方法打開或者關閉某個具體項的統計。

若是要打開redo log的時間監控,你能夠執行這個語句:

mysql> update setup_instruments set ENABLED='YES', Timed='YES' where name like '%wait/io/file/innodb/innodb_log_file%';

假設,如今你已經開啓了redo log和binlog這兩個統計信息,那要怎麼把這個信息用在實例狀態診斷上呢?

很簡單,你能夠經過MAX_TIMER的值來判斷數據庫是否出問題了。好比,你能夠設定閾值,單次IO請求時間超過200毫秒屬於異常,而後使用相似下面這條語句做爲檢測邏輯。

mysql> select event_name,MAX_TIMER_WAIT FROM performance_schema.file_summary_by_event_name where event_name in ('wait/io/file/innodb/innodb_log_file','wait/io/file/sql/binlog') and MAX_TIMER_WAIT>200*1000000000;

發現異常後,取到你須要的信息,再經過下面這條語句:

mysql> truncate table performance_schema.file_summary_by_event_name;

把以前的統計信息清空。這樣若是後面的監控中,再次出現這個異常,就能夠加入監控累積值了。

小結

今天,我和你介紹了檢測一個MySQL實例健康狀態的幾種方法,以及各類方法存在的問題和演進的邏輯。

你看完後可能會以爲,select 1這樣的方法是否是已經被淘汰了呢,但實際上使用很是普遍的MHA(Master High Availability),默認使用的就是這個方法。

MHA中的另外一個可選方法是隻作鏈接,就是 「若是鏈接成功就認爲主庫沒問題」。不過據我所知,選擇這個方法的不多。

其實,每一個改進的方案,都會增長額外損耗,並不能用「對錯」作直接判斷,須要你根據業務實際狀況去作權衡。

我我的比較傾向的方案,是優先考慮update系統表,而後再配合增長檢測performance_schema的信息。

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

今天,我想問你的是:業務系統通常也有高可用的需求,在你開發和維護過的服務中,你是怎麼判斷服務有沒有出問題的呢?

你能夠把你用到的方法和分析寫在留言區,我會在下一篇文章中選取有趣的方案一塊兒來分享和分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

上期問題時間

上期的問題是,若是使用GTID等位點的方案作讀寫分離,在對大表作DDL的時候會怎麼樣。

假設,這條語句在主庫上要執行10分鐘,提交後傳到備庫就要10分鐘(典型的大事務)。那麼,在主庫DDL以後再提交的事務的GTID,去備庫查的時候,就會等10分鐘纔出現。

這樣,這個讀寫分離機制在這10分鐘以內都會超時,而後走主庫。

這種預期內的操做,應該在業務低峯期的時候,確保主庫可以支持全部業務查詢,而後把讀請求都切到主庫,再在主庫上作DDL。等備庫延遲追上之後,再把讀請求切回備庫。

經過這個思考題,我主要想讓關注的是,大事務對等位點方案的影響。

固然了,使用gh-ost方案來解決這個問題也是不錯的選擇。

相關文章
相關標籤/搜索