檢測 MySQL 健康狀態
-
每個改進的方案,都會增加額外損耗,需要業務方根據實際情況去做權衡
- 建議優先考慮 update 系統表,然後再配合增加檢測 performance_schema 的信息
-
select 1 判斷
- 使用非常廣泛的 MHA(Master High Availability),默認使用的就是這個方法
- 另一個可選方法是隻做連接,就是 “如果連接成功就認爲主庫沒問題”
- select 1 成功返回,只能說明這個庫的進程還在,並不能說明主庫沒問題
- innodb_thread_concurrency 參數:控制 InnoDB 的併發線程上限
- 超過閾值,則進入等待狀態,直到有線程退出
- innodb_thread_concurrency 這個參數的默認值是 0,表示不限制併發線程數量
- 建議把 innodb_thread_concurrency 設置爲 64~128 之間的值
- 在線程進入鎖等待以後,併發線程的計數會減一(也就是說等行級鎖的線程不納入計算
- 真正地執行查詢導致計數加一(select sleep (100) from t
- 併發連接和併發查詢,並不是同一個概念
- show processlist 的結果裏,看到的幾千個連接,指的就是併發連接
- 併發連接數達到幾千個影響並不大,就是多佔一些內存而已
- “當前正在執行” 的語句,纔是我們所說的併發查詢
- 併發查詢太高才是 CPU 殺手(需要設置 innodb_thread_concurrency 參數的原因
- show processlist 的結果裏,看到的幾千個連接,指的就是併發連接
- 使用非常廣泛的 MHA(Master High Availability),默認使用的就是這個方法
-
查表判斷
- 爲了檢測 InnoDB 併發線程數過多導致的系統不可用情況,我們需要找一個訪問 InnoDB 的場景
- 一般的做法是,在系統庫(mysql 庫)裏創建一個表,裏面只放一行數據並定期執行
- 栗子:select * from mysql.health_check;
- 可以檢測出由於併發線程過多導致的數據庫不可用的情況
- 缺點:空間滿了以後,這種方法又會變得不好使(讀不受影響,事務更新 commit 會被堵住
-
更新判斷
- 爲了讓主備之間的更新不產生衝突,在 health_check 表上存入多行數據,並用 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();
-
更新語句,如果失敗或者超時,就可以發起主備切換了,但會存在“判斷慢”的問題
- 栗子(其實就是服務器 IO 資源分配的問題)
- 假設日誌盤的 IO 利用率已經是 100%,整個系統響應非常慢,本應該需要主備切換
- 但 IO 利用率 100% 表示系統的 IO 是在工作的,仍有機會獲得 IO 資源執行該任務
- 拿到資源後提交成功,並且在超時時間 N 秒未到達之前就返回給了檢測系統
- 根本原因是我們上面說的所有方法,都是基於外部檢測(存在天然問題 - 隨機性
- 外部檢測都需要定時輪詢,可能得多次輪詢後才能發現問題,導致切換慢
- 栗子(其實就是服務器 IO 資源分配的問題)
-
內部統計
- MySQL 5.6 版本以後提供了 performance_schema 庫,可以藉助此庫檢測健康狀態
- file_summary_by_event_name 表裏統計了每次 IO 請求的時間
- 如果打開所有的 performance_schema 項,性能大概會下降 10% 左右
- 建議只打開自己需要的項進行統計
- MySQL 5.6 版本以後提供了 performance_schema 庫,可以藉助此庫檢測健康狀態
刪庫不跑路
-
強調的核心:預防遠比處理的意義來得大
- 數據和服務的可靠性不止是運維團隊的工作,最終是各個環節一起保障的結果
-
誤刪行
- 可以用 Flashback 工具通過閃回把數據恢復回來
- 原理:修改 binlog 的內容拿回原庫重放(需 binlog_format=row 和 binlog_row_image=FULL
- 栗子:
- Delete_rows event 改爲 Write_rows event / Update_rows 對調修改前後值的位置
- 如果誤刪數據涉及到了多個事務的話,需要將事務的順序調過來再執行
- 恢復數據比較安全的做法是找一個從庫作爲臨時庫,在從庫上執行恢復操作,確認後恢復回主庫
- 數據狀態的變更往往是有關聯的,若單獨恢復了幾行數據(未經確認),可能造成二次破壞
- 如何做好事前預防,避免誤刪數據
- 把 sql_safe_updates 參數設置爲 on(delete/update 忘記帶 where 時,直接報錯
- 代碼上線前,必須經過 SQL 審計
- 可以用 Flashback 工具通過閃回把數據恢復回來
-
誤刪庫 / 表
- delete 全部很慢,建議優先考慮使用 truncate /drop table 和 drop database 命令刪除的數據
- 即使我們配置了 binlog_format=row,以上三個命令記錄的 binlog 均是 statement 格式
- 導致無法使用 Flashback 工具恢復數據
- 全量備份,加增量日誌的方式才能恢復數據(要求線上有定期的全量備份,並且實時備份 binlog
-
數據恢復流程 -mysqlbinlog 方法
-
取最近一次全量備份,假設這個庫是一天一備,上次備份是當天 0 點
-
用備份恢復出一個臨時庫
-
從日誌備份裏面,取出凌晨 0 點之後的日誌
-
把這些日誌,除了誤刪除數據的語句外,全部應用到臨時庫
-
恢復流程特別說明
- 若臨時庫上有多個數據庫,可使用 mysqlbinlog 命令時,加上一個–database 參數
- 用來指定誤刪表所在的庫,避免了在恢復數據時還要應用其他庫日誌的情況
- 在應用日誌的時候,需要跳過 12 點誤操作的那個語句的 binlog
- 若未使用 GTID 模式,則通過 –stop-position / –start-position 跳過誤操作語句
- 若使用 GTID 模式,則通過 set gtid_next=gtid1;begin;commit; 跳過誤操作語句
- 若臨時庫上有多個數據庫,可使用 mysqlbinlog 命令時,加上一個–database 參數
-
此方法恢復不夠快的主要兩個原因
- 如果是誤刪表,最好就只恢復出這張表,但 mysqlbinlog 工具並不能支持表維度解析
- 用 mysqlbinlog 解析出日誌應用,應用日誌的過程就只能是單線程(無法並行複製
-
-
數據恢復流程 -master-slave 方法
-
虛線:若備庫上已刪除需要的 binlog,則從 binlog 備份系統中找到並放回備庫
-
在用備份恢復出臨時實例之後,將這個臨時實例設置成線上備庫的從庫
- 在 start slave 之前執行change replication filter replicate_do_table = (tbl_name) 命令
- 目的:讓臨時庫只同步誤操作的表
- 這樣做也可以用上並行複製技術,來加速整個數據恢復過程
- 在 start slave 之前執行change replication filter replicate_do_table = (tbl_name) 命令
-
-
兩套方法共同點:誤刪庫表後恢復數據的思路主要就是通過備份,再加上應用 binlog 的方式
- 均要求要求備份系統定期備份全量日誌,確保 binlog 在被從本地刪除之前已經做了備份
-
建議將數據恢復功能做成自動化工具並經常拿出來演練
- 萬一出現了誤刪事件,能夠快速恢復數據,將損失降到最小,也應該不用跑路了
- 避免出現手忙腳亂的操作,對業務造成二次傷害
-
- delete 全部很慢,建議優先考慮使用 truncate /drop table 和 drop database 命令刪除的數據
-
延遲複製備庫
- 如果有非常核心的業務,不允許太長的恢復時間,可以考慮搭建延遲複製的備庫(MySQL 5.6 起
- 延遲複製的備庫是一種特殊的備庫
- 命令 CHANGE MASTER TO MASTER_DELAY = N
- 可以指定這個備庫持續保持跟主庫有 N 秒的延遲(例如 N 設置成3600,則1小時後同步
- 在N秒內發現誤操作命令後,只要在未執行前在備庫設置跳過誤操作命令,就可以恢復需要的數據
- 如果有非常核心的業務,不允許太長的恢復時間,可以考慮搭建延遲複製的備庫(MySQL 5.6 起
-
預防誤刪庫 / 表的方法
- 賬號分離(避免寫錯命令
- 只給業務開發同學 DML 權限,而不給 truncate/drop 權限(若業務需要則通過管理系統支持
- 即使是 DBA 團隊成員,日常也都規定只使用只讀賬號,必要的時候才使用有更新權限的賬號
- 制定操作規範(避免寫錯要刪除的表名
- 在刪除數據表之前,必須先對錶做改名操作(觀察一段時間,確保無影響後再徹底刪除
- 改表名的時候,要求給表名加固定的後綴,刪除必須通過管理系統且只能刪除固定後綴的表
- 賬號分離(避免寫錯命令
-
rm 刪除數據
- 只要不是惡意刪除整個集羣,而只是刪除某一個節點數據的話,HA系統會自動選出一個新的主庫
- 高可用機制的 MySQL 集羣不會因此造成影響(刪除節點數據恢復後可再接入集羣
- SA(系統管理員)的自動化系統若誤操作批量下線機器操作,可能導致MySQL集羣所有節點掛
- 建議你的備份跨機房,或者最好是跨城市保存
- 只要不是惡意刪除整個集羣,而只是刪除某一個節點數據的話,HA系統會自動選出一個新的主庫
kill 命令
-
MySQL 中有兩個 kill 命令
- 一個是 kill query + 線程 id,表示終止這個線程中正在執行的語句
- 一個是 kill connection + 線程 id,這裏 connection 可缺省,表示斷開這個線程的連接
- 如果這個線程有語句正在執行,也是要先停止正在執行的語句的
-
kill query/connection 命令有效的場景(大多數情況
-
執行一個查詢的過程中,發現執行時間太久,通過 kill query 命令終止這條查詢語句
-
處於鎖等待的時候,直接使用 kill 命令也是有效的
- kill 並不是馬上停止的意思,而是告訴執行線程說,這條語句需要開始 “執行停止的邏輯了”
- 類似於 Linux kill -N pid(並不是讓進程直接停止,而是給進程發一個信號,進入終止邏輯
- 用戶執行 kill query 時,處理 kill 命令的線程做了兩件事
- 把 session B 的運行狀態改成 THD::KILL_QUERY (即將變量 killed 賦值
- 給 session B 的執行線程發一個信號(讓線程退出等待,來處理 THD::KILL_QUERY 狀態
- 以上分析包含的三層意思
- 一個語句執行過程中有多處 “埋點”,在這些 “埋點” 的地方判斷線程狀態(處理對應邏輯
- 如果處於等待狀態,必須是一個可以被喚醒的等待,否則根本不會執行到 “埋點” 處
- 語句從開始進入終止邏輯,到終止邏輯完全完成,是有一個過程的
- kill 並不是馬上停止的意思,而是告訴執行線程說,這條語句需要開始 “執行停止的邏輯了”
-
-
kill 不掉的場景
-
場景一:innodb_thread_concurrency 不夠用的例子
-
首先執行 set global innodb_thread_concurrency=2,將 InnoDB 的併發線程上限數設置爲 2
-
此時執行 show processlist ,則會顯示被 kill 線程的 Commnad 列顯示的是 Killed
- 客戶端雖然斷開了連接,但實際上服務端上這條語句還在執行過程中
- 只有等到滿足進入 InnoDB 的條件後,session C 的查詢語句繼續執行
- 顯示爲Killed的原因(show processlist 時有一個特別的邏輯
- 如果一個線程的狀態是 KILL_CONNECTION,就把 Command 列顯示成 Killed
- 客戶端雖然斷開了連接,但實際上服務端上這條語句還在執行過程中
-
執行 kill query 命令鎖等待場景好使,而此處不好使的原因
- 等行鎖時,使用的是 pthread_cond_timedwait 函數,這個等待狀態可以被喚醒
- 此處是每10毫秒判斷一下是否可以進入 InnoDB 執行,如果不行就調用 nanosleep 函數
- 循環過程中並沒有去判斷線程的狀態,因此根本不會進入終止邏輯
-
kill connection 命令執行流程
- 把 12 號線程狀態設置爲 KILL_CONNECTION
- 關掉 12 號線程的網絡連接(客戶端就能收到斷開連接的提示
- 其實即使是客戶端退出了,這個線程的狀態仍然是在等待中
- 只有等到滿足進入 InnoDB 的條件後,纔有可能判斷到線程狀態並進入終止邏輯階段
-
-
場景二:由於 IO 壓力過大,讀寫 IO 的函數一直無法返回,導致不能及時判斷線程的狀態
-
場景三:終止邏輯耗時較長
- 常見的場景有以下幾種
- 超大事務執行期間被 kill(需進行大量回滾操作
- 大查詢回滾(清理大量臨時文件,可能需要等待IO資源等
- DDL 命令執行到最後階段(中間過程的臨時文件需清理
- 常見的場景有以下幾種
-
-
三個關於客戶端的誤解
- 直接在客戶端通過 Ctrl+C 命令,是不可以直接終止線程的
- 客戶端和服務端只能通過網絡交互,是不可能直接操作服務端線程的
- MySQL 是停等協議,在線程執行的語句沒有返回時,在往連接裏繼續發命令是沒有用的
- 命令執行後實際上是另外啓動一個連接,然後發送一個 kill query 命令
- 如果庫裏面的表特別多,連接就會很慢
- 我們感知到的連接過程慢,其實並不是連接慢,也不是服務端慢,而是客戶端慢
- 原因:客戶端會提供一個本地庫名和表名補全的功能(本地構建哈希表的操作極度耗時
- 如果在連接命令中加上 -A,就可以關掉這個自動補全的功能,客戶端就可以快速返回
- –quick 是一個更容易引起誤會的參數,也是關於客戶端常見的一個誤解
- MySQL 客戶端發送請求後,接收服務端返回結果的方式有兩種
- 一種是本地緩存,也就是在本地開一片內存,先把結果存起來(mysql_store_result 默認
- 另一種是不緩存,讀一個處理一個(mysql_use_result
- 如果本地處理得慢,就會導致服務端發送結果被阻塞,因此會讓服務端變慢
- 使用這個參數可以達到以下三點效果(–quick 參數的意思,是讓客戶端變得更快
- 跳過表名自動補全功能
- 接收服務端返回結果的方式選擇上訴第二種不緩存,讀一個處理一個(mysql_use_result
- 不會把執行命令記錄到本地的命令歷史文件
- MySQL 客戶端發送請求後,接收服務端返回結果的方式有兩種
- 直接在客戶端通過 Ctrl+C 命令,是不可以直接終止線程的
全表掃描的影響
-
對 server 層的影響
-
採用的是邊算邊發的邏輯,不會保留完整的結果集,但如果客戶端讀取結果不及時,會堵住查詢過程
-
栗子:
mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file
- 查到的每一行都可以直接放到結果集裏面,然後返回給客戶端(服務端並不需要保存一個完整的結果集
-
查詢結果發送流程
-
獲取一行,寫到 net_buffer 中(參數 net_buffer_length 定義這塊內存大小
-
重複獲取行,直到 net_buffer 寫滿,調用網絡接口發出去
-
如果發送成功,就清空 net_buffer,然後繼續取下一行,並寫入 net_buffer
-
如果發送函數返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地網絡棧(socket send buffer)寫滿了,進入等待。直到網絡棧重新可寫,再繼續發送
-
根據上訴流程可以得出
- 一個查詢在發送過程中,佔用的 MySQL 內部的內存最大就是 net_buffer_length 這麼大,不會到 200G
- socket send buffer 也不可能達到 200G(默認定義 /proc/sys/net/core/wmem_default)
- 如果 socket send buffer 被寫滿,就會暫停讀數據的流程
-
MySQL 是 “邊讀邊發的”(如果客戶端接收得慢會導致 MySQL 服務端由於結果發不出去,事務執行時間變長
- show processlist#State 爲 “Sending to client”,就表示服務器端的網絡棧寫滿了
- 關聯知識點:-quick 參數
- 除非真是大數據查詢,否則都建議 mysql_store_result 方法(緩存在客戶端本地,不影響服務端響應
- 如何大量線程處於“Sending to client”,應讓業務開發評估返回這麼多結果是否合理並進行對應優化
- 如果要快速減少處於這個狀態的線程的話,將 net_buffer_length 參數設置爲一個更大值是一個可選方案
-
-
“Sending data” 並不一定是指 “正在發送數據”,而可能是處於執行器過程中的任意階段
- 一個查詢語句的狀態變化是這樣的(已忽略其他無關狀態
- MySQL 查詢語句進入執行階段後,首先把狀態設置成 “Sending data”
- 然後,發送執行結果的列相關的信息(meta data) 給客戶端
- 再繼續執行語句的流程
- 執行完成後,把狀態設置成空字符串
- “Sending data” 與 “Sending to client” 區別概要
- 僅當一個線程處於 “等待客戶端接收結果” 的狀態,纔會顯示 “Sending to client”
- 如果顯示成 “Sending data”,它的意思只是 “正在執行”
- 一個查詢語句的狀態變化是這樣的(已忽略其他無關狀態
-
-
對 InnoDB 的影響
-
由於有淘汰策略,大查詢也不會導致內存暴漲,並且利用改進後的 LRU保證對 Buffer Pool 的影響也能做到可控
-
內存的數據頁是在 Buffer Pool (BP) 中管理的,在 WAL 裏 Buffer Pool 起到了加速更新的作用
- 實際上,Buffer Pool 還有一個更重要的作用,就是加速查詢(Buffer Pool上的數據總是最新的,可直接讀取
-
Buffer Pool 對查詢的加速效果,依賴於一個重要的指標,即:內存命中率
-
show engine innodb status 結果中,查看一個系統當前的 BP 命中率(Buffer pool hit rate
- 一般情況下,一個穩定服務的線上系統,要保證響應時間符合要求的話,內存命中率要在 99% 以上
-
BP 的大小是由參數 innodb_buffer_pool_size 確定的,一般建議設置成可用物理內存的 60%~80%
- 如果一個 Buffer Pool 滿了,而又要從磁盤讀入一個數據頁,那肯定是要淘汰一箇舊數據頁的
-
InnoDB 使用改進後的 LRU 算法
-
是按照 5:3 的比例把整個 LRU 鏈表分成了 young 區域和 old 區域(靠近鏈表頭部的 5/8 是 young 區域
- 圖中狀態 1,要訪問數據頁 P3,由於 P3 在 young 區域,因此和優化前的 LRU 算法一樣,將其移到鏈表頭部,變成狀態 2
- 之後要訪問一個新的不存在於當前鏈表的數據頁,這時候依然是淘汰掉數據頁 Pm,但是新插入的數據頁 Px,是放在 LRU_old 處
- 處於 old 區域的數據頁,每次被訪問的時候都要做下面這個判斷
- 若這個數據頁在 LRU 鏈表中存在的時間超過了 1 秒,就把它移動到鏈表頭部
- 如果這個數據頁在 LRU 鏈表中存在的時間短於 1 秒,位置保持不變
- 1 秒這個時間,是由參數 innodb_old_blocks_time 控制的(其默認值是 1000,單位毫秒
-
這個策略最大的收益是在掃描這個大表的過程中,雖然也用到了 BP,但是對 young 區域完全沒有影響
- 從而保證了 Buffer Pool 響應正常業務的查詢命中率
-
-
-