阿里P7大佬手把手叫我如何判斷一個數據庫是否是出問題了,受教了!

最新互聯網大廠面試真題、Java程序員面試策略(面試前的準備、面試中的技巧)請移步GitHubmysql

在一主一備的雙 M 架構裏,主備切換隻須要把客戶端流量切到備庫;而在一主多從架構裏,主備切換除了要把客戶端流量切到備庫外,還須要把從庫接到新主庫上。git

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

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

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

select 1判斷

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

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 來檢測實例是否正常的話,是檢測不出問題的。session

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

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

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

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

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

而後,你可能還會想起我以前講到的熱點更新和死鎖檢測的時候,若是把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

因爲 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

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

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

mysql> select event_name,MAX_TIMER_WAIT  FROM performance_schema.file_summary_by_event_n

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

mysql> truncate table performance_schema.file_summary_by_event_name;

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

小結

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

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

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

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

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

相關文章
相關標籤/搜索