「筆記」MySQL 實戰 45 講 - 實踐篇(四)

檢測 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 參數的原因
  • 查表判斷

    • 爲了檢測 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 秒未到達之前就返回給了檢測系統
      • 根本原因是我們上面說的所有方法,都是基於外部檢測(存在天然問題 - 隨機性
        • 外部檢測都需要定時輪詢,可能得多次輪詢後才能發現問題,導致切換慢
  • 內部統計

    • MySQL 5.6 版本以後提供了 performance_schema 庫,可以藉助此庫檢測健康狀態
      • file_summary_by_event_name 表裏統計了每次 IO 請求的時間
    • 如果打開所有的 performance_schema 項,性能大概會下降 10% 左右
      • 建議只打開自己需要的項進行統計

刪庫不跑路

  • 強調的核心:預防遠比處理的意義來得大

    • 數據和服務的可靠性不止是運維團隊的工作,最終是各個環節一起保障的結果
  • 誤刪行

    • 可以用 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 審計
  • 誤刪庫 / 表

    • delete 全部很慢,建議優先考慮使用 truncate /drop table 和 drop database 命令刪除的數據
      • 即使我們配置了 binlog_format=row,以上三個命令記錄的 binlog 均是 statement 格式
      • 導致無法使用 Flashback 工具恢復數據
    • 全量備份,加增量日誌的方式才能恢復數據(要求線上有定期的全量備份,並且實時備份 binlog
      • 數據恢復流程 -mysqlbinlog 方法

        img

        • 取最近一次全量備份,假設這個庫是一天一備,上次備份是當天 0 點

        • 用備份恢復出一個臨時庫

        • 從日誌備份裏面,取出凌晨 0 點之後的日誌

        • 把這些日誌,除了誤刪除數據的語句外,全部應用到臨時庫

        • 恢復流程特別說明

          • 若臨時庫上有多個數據庫,可使用 mysqlbinlog 命令時,加上一個–database 參數
            • 用來指定誤刪表所在的庫,避免了在恢復數據時還要應用其他庫日誌的情況
          • 在應用日誌的時候,需要跳過 12 點誤操作的那個語句的 binlog
            • 若未使用 GTID 模式,則通過 –stop-position / –start-position 跳過誤操作語句
            • 若使用 GTID 模式,則通過 set gtid_next=gtid1;begin;commit; 跳過誤操作語句
        • 此方法恢復不夠快的主要兩個原因

          • 如果是誤刪表,最好就只恢復出這張表,但 mysqlbinlog 工具並不能支持表維度解析
          • 用 mysqlbinlog 解析出日誌應用,應用日誌的過程就只能是單線程(無法並行複製
      • 數據恢復流程 -master-slave 方法

        img

        • 虛線:若備庫上已刪除需要的 binlog,則從 binlog 備份系統中找到並放回備庫

        • 在用備份恢復出臨時實例之後,將這個臨時實例設置成線上備庫的從庫

          • 在 start slave 之前執行change replication filter replicate_do_table = (tbl_name) 命令
            • 目的:讓臨時庫只同步誤操作的表
          • 這樣做也可以用上並行複製技術,來加速整個數據恢復過程
      • 兩套方法共同點:誤刪庫表後恢復數據的思路主要就是通過備份,再加上應用 binlog 的方式

        • 均要求要求備份系統定期備份全量日誌,確保 binlog 在被從本地刪除之前已經做了備份
      • 建議將數據恢復功能做成自動化工具並經常拿出來演練

        • 萬一出現了誤刪事件,能夠快速恢復數據,將損失降到最小,也應該不用跑路了
        • 避免出現手忙腳亂的操作,對業務造成二次傷害
  • 延遲複製備庫

    • 如果有非常核心的業務,不允許太長的恢復時間,可以考慮搭建延遲複製的備庫(MySQL 5.6 起
      • 延遲複製的備庫是一種特殊的備庫
      • 命令 CHANGE MASTER TO MASTER_DELAY = N
      • 可以指定這個備庫持續保持跟主庫有 N 秒的延遲(例如 N 設置成3600,則1小時後同步
    • 在N秒內發現誤操作命令後,只要在未執行前在備庫設置跳過誤操作命令,就可以恢復需要的數據
  • 預防誤刪庫 / 表的方法

    • 賬號分離(避免寫錯命令
      • 只給業務開發同學 DML 權限,而不給 truncate/drop 權限(若業務需要則通過管理系統支持
      • 即使是 DBA 團隊成員,日常也都規定只使用只讀賬號,必要的時候才使用有更新權限的賬號
    • 制定操作規範(避免寫錯要刪除的表名
      • 在刪除數據表之前,必須先對錶做改名操作(觀察一段時間,確保無影響後再徹底刪除
      • 改表名的時候,要求給表名加固定的後綴,刪除必須通過管理系統且只能刪除固定後綴的表
  • rm 刪除數據

    • 只要不是惡意刪除整個集羣,而只是刪除某一個節點數據的話,HA系統會自動選出一個新的主庫
      • 高可用機制的 MySQL 集羣不會因此造成影響(刪除節點數據恢復後可再接入集羣
    • SA(系統管理員)的自動化系統若誤操作批量下線機器操作,可能導致MySQL集羣所有節點掛
      • 建議你的備份跨機房,或者最好是跨城市保存

kill 命令

  • MySQL 中有兩個 kill 命令

    • 一個是 kill query + 線程 id,表示終止這個線程中正在執行的語句
    • 一個是 kill connection + 線程 id,這裏 connection 可缺省,表示斷開這個線程的連接
    • 如果這個線程有語句正在執行,也是要先停止正在執行的語句的
  • kill query/connection 命令有效的場景(大多數情況

    • 執行一個查詢的過程中,發現執行時間太久,通過 kill query 命令終止這條查詢語句

    • 處於鎖等待的時候,直接使用 kill 命令也是有效的

      img

      • kill 並不是馬上停止的意思,而是告訴執行線程說,這條語句需要開始 “執行停止的邏輯了”
        • 類似於 Linux kill -N pid(並不是讓進程直接停止,而是給進程發一個信號,進入終止邏輯
      • 用戶執行 kill query 時,處理 kill 命令的線程做了兩件事
        • 把 session B 的運行狀態改成 THD::KILL_QUERY (即將變量 killed 賦值
        • 給 session B 的執行線程發一個信號(讓線程退出等待,來處理 THD::KILL_QUERY 狀態
      • 以上分析包含的三層意思
        • 一個語句執行過程中有多處 “埋點”,在這些 “埋點” 的地方判斷線程狀態(處理對應邏輯
        • 如果處於等待狀態,必須是一個可以被喚醒的等待,否則根本不會執行到 “埋點” 處
        • 語句從開始進入終止邏輯,到終止邏輯完全完成,是有一個過程的
  • kill 不掉的場景

    • 場景一:innodb_thread_concurrency 不夠用的例子

      • 首先執行 set global innodb_thread_concurrency=2,將 InnoDB 的併發線程上限數設置爲 2

        img

      • 此時執行 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
        • 不會把執行命令記錄到本地的命令歷史文件

全表掃描的影響

  • 對 server 層的影響

    • 採用的是邊算邊發的邏輯,不會保留完整的結果集,但如果客戶端讀取結果不及時,會堵住查詢過程

    • 栗子:mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file

      • 查到的每一行都可以直接放到結果集裏面,然後返回給客戶端(服務端並不需要保存一個完整的結果集
    • 查詢結果發送流程

      img

      • 獲取一行,寫到 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 區域

          img

          • 圖中狀態 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 響應正常業務的查詢命中率
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章