我常常會被問到這樣一個問題:個人主機內存只有 100G,如今要對一個 200G 的大表作全表掃描,會不會把數據庫主機的內存用光了?mysql
這個問題確實值得擔憂,被系統 OOM(out of memory)可不是鬧着玩的。可是,反過來想一想,邏輯備份的時候,可不就是作整庫掃描嗎?若是這樣就會把內存吃光,邏輯備份
不是早就掛了?算法
因此說,對大表作全表掃描,看來應該是沒問題的。可是,這個流程究竟是怎麼樣的呢?sql
假設,咱們如今要對一個 200G 的 InnoDB 表 db1. t,執行一個全表掃描。固然,你要把掃描結果保存在客戶端,會使用相似這樣的命令:數據庫
mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file
你已經知道了,InnoDB 的數據是保存在主鍵索引上的,因此全表掃描其實是直接掃描表 t 的主鍵索引。這條查詢語句因爲沒有其餘的判斷條件,因此查到的每一行均可以直接
放到結果集裏面,而後返回給客戶端。bash
那麼,這個「結果集」存在哪裏呢?服務器
實際上,服務端並不須要保存一個完整的結果集。取數據和發數據的流程是這樣的:網絡
1. 獲取一行,寫到 net_buffer 中。這塊內存的大小是由參數 net_buffer_length 定義的,默認是 16k。
2. 重複獲取行,直到 net_buffer 寫滿,調用網絡接口發出去。
3. 若是發送成功,就清空 net_buffer,而後繼續取下一行,並寫入 net_buffer。
4. 若是發送函數返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地網絡棧(socketsend buffer)寫滿了,進入等待。直到網絡棧從新可寫,再繼續發送。session
這個過程對應的流程圖以下所示。socket
圖 1 查詢結果發送流程函數
從這個流程中,你能夠看到:
1. 一個查詢在發送過程當中,佔用的 MySQL 內部的內存最大就是 net_buffer_length 這麼大,並不會達到 200G;
2. socket send buffer 也不可能達到 200G(默認定義/proc/sys/net/core/wmem_default),若是 socket send buffer 被寫滿,就會暫停讀數據的流程。
也就是說,MySQL 是「邊讀邊發的」,這個概念很重要。這就意味着,若是客戶端接收得慢,會致使 MySQL 服務端因爲結果發不出去,這個事務的執行時間變長。
好比下面這個狀態,就是我故意讓客戶端不去讀 socket receive buffer 中的內容,而後在服務端 show processlist 看到的結果。
圖 2 服務端發送阻塞
若是你看到 State 的值一直處於「Sending to client」,就表示服務器端的網絡棧寫滿了。
我在上一篇文章中曾提到,若是客戶端使用–quick 參數,會使用 mysql_use_result 方法。這個方法是讀一行處理一行。你能夠想象一下,假設有一個業務的邏輯比較複雜,每
讀一行數據之後要處理的邏輯若是很慢,就會致使客戶端要過好久纔會去取下一行數據,可能就會出現如圖 2 所示的這種狀況。
所以,對於正常的線上業務來講,若是一個查詢的返回結果不會不少的話,我都建議你使用 mysql_store_result 這個接口,直接把查詢結果保存到本地內存。
固然前提是查詢返回結果很少。在第 30 篇文章評論區,有同窗說到本身由於執行了一個大查詢致使客戶端佔用內存近 20G,這種狀況下就須要改用 mysql_use_result 接口了。
另外一方面,若是你在本身負責維護的 MySQL 裏看到不少個線程都處於「Sending toclient」這個狀態,就意味着你要讓業務開發同窗優化查詢結果,
並評估這麼多的返回結果是否合理。
而若是要快速減小處於這個狀態的線程的話,將 net_buffer_length 參數設置爲一個更大的值是一個可選方案。
與「Sending to client」長相很相似的一個狀態是「Sending data」,這是一個常常被誤會的問題。有同窗問我說,在本身維護的實例上看到不少查詢語句的狀態是「Sending
data」,但查看網絡也沒什麼問題啊,爲何 Sending data 要這麼久?
實際上,一個查詢語句的狀態變化是這樣的(注意:這裏,我略去了其餘無關的狀態):
也就是說,「Sending data」並不必定是指「正在發送數據」,而多是處於執行器過程當中的任意階段。好比,你能夠構造一個鎖等待的場景,就能看到 Sending data 狀態。
圖 3 讀全表被鎖
圖 4 Sending data 狀態
能夠看到,session B 明顯是在等鎖,狀態顯示爲 Sending data。也就是說,僅當一個線程處於「等待客戶端接收結果」的狀態,纔會顯示"Sending toclient";而若是顯示成「Sending data」,它的意思只是「正在執行」。
如今你知道了,查詢的結果是分段發給客戶端的,所以掃描全表,查詢返回大量的數據,並不會把內存打爆。
在 server 層的處理邏輯咱們都清楚了,在 InnoDB 引擎裏面又是怎麼處理的呢? 掃描全表會不會對引擎系統形成影響呢?
在第 2和第 15 篇文章中,我介紹 WAL 機制的時候,和你分析了 InnoDB 內存的一個做用,是保存更新的結果,再配合 redo log,就避免了隨機寫盤。
內存的數據頁是在 Buffer Pool (BP) 中管理的,在 WAL 裏 Buffer Pool 起到了加速更新的做用。而實際上,Buffer Pool 還有一個更重要的做用,就是加速查詢。
在第 2 篇文章的評論區有同窗問道,因爲有 WAL 機制,當事務提交的時候,磁盤上的數據頁是舊的,那若是這時候立刻有一個查詢要來讀這個數據頁,是否是要立刻把 redo log
應用到數據頁呢?
答案是不須要。
由於這時候內存數據頁的結果是最新的,直接讀內存頁就能夠了。你看,這時候查詢根本不須要讀磁盤,直接從內存拿結果,速度是很快的。因此說,Buffer Pool
還有加速查詢的做用。
而 Buffer Pool 對查詢的加速效果,依賴於一個重要的指標,即:內存命中率。
而 Buffer Pool 對查詢的加速效果,依賴於一個重要的指標,即:內存命中率。
你能夠在 show engine innodb status 結果中,查看一個系統當前的 BP 命中率。通常狀況下,一個穩定服務的線上系統,要保證響應時間符合要求的話,內存命中率要在 99%以上。
執行 show engine innodb status ,能夠看到「Buffer pool hit rate」字樣,顯示的就是當前的命中率。好比圖 5 這個命中率,就是 99.0%。
圖 5 show engine innodb status 顯示內存命中率
若是全部查詢須要的數據頁都可以直接從內存獲得,那是最好的,對應的命中率就是100%。但,這在實際生產上是很難作到的。
InnoDB Buffer Pool 的大小是由參數 innodb_buffer_pool_size 肯定的,通常建議設置成可用物理內存的 60%~80%。
在大約十年前,單機的數據量是上百個 G,而物理內存是幾個 G;如今雖然不少服務器都能有 128G 甚至更高的內存,可是單機的數據量卻達到了 T 級別。
因此,innodb_buffer_pool_size 小於磁盤的數據量是很常見的。若是一個 Buffer Pool滿了,而又要從磁盤讀入一個數據頁,那確定是要淘汰一箇舊數據頁的。
圖 6 基本 LRU 算法
InnoDB 管理 Buffer Pool 的 LRU 算法,是用鏈表來實現的。
1. 在圖 6 的狀態 1 裏,鏈表頭部是 P1,表示 P1 是最近剛剛被訪問過的數據頁;假設內存裏只能放下這麼多數據頁;
2. 這時候有一個讀請求訪問 P3,所以變成狀態 2,P3 被移到最前面;
3. 狀態 3 表示,此次訪問的數據頁是不存在於鏈表中的,因此須要在 Buffer Pool 中新申請一個數據頁 Px,加到鏈表頭部。可是因爲內存已經滿了,不能申請新的內存。因而,會清空鏈表末尾
Pm 這個數據頁的內存,存入 Px 的內容,而後放到鏈表頭部。
4. 從效果上看,就是最久沒有被訪問的數據頁 Pm,被淘汰了。
這個算法乍一看上去沒什麼問題,可是若是考慮到要作一個全表掃描,會不會有問題呢?
假設按照這個算法,咱們要掃描一個 200G 的表,而這個表是一個歷史數據表,平時沒有業務訪問它。
那麼,按照這個算法掃描的話,就會把當前的 Buffer Pool 裏的數據所有淘汰掉,存入掃描過程當中訪問到的數據頁的內容。也就是說 Buffer Pool 裏面主要放的是這個歷史數據表的數據。
對於一個正在作業務服務的庫,這可不妙。你會看到,Buffer Pool 的內存命中率急劇降低,磁盤壓力增長,SQL 語句響應變慢。
因此,InnoDB 不能直接使用這個 LRU 算法。實際上,InnoDB 對 LRU 算法作了改進。
圖 7 改進的 LRU 算法
在 InnoDB 實現上,按照 5:3 的比例把整個 LRU 鏈表分紅了 young 區域和 old 區域。圖中 LRU_old 指向的就是 old 區域的第一個位置,是整個鏈表的 5/8 處。也就是說,靠近
鏈表頭部的 5/8 是 young 區域,靠近鏈表尾部的 3/8 是 old 區域。
改進後的 LRU 算法執行流程變成了下面這樣。
1. 圖 7 中狀態 1,要訪問數據頁 P3,因爲 P3 在 young 區域,所以和優化前的 LRU 算法同樣,將其移到鏈表頭部,變成狀態 2。
2. 以後要訪問一個新的不存在於當前鏈表的數據頁,這時候依然是淘汰掉數據頁 Pm,可是新插入的數據頁 Px,是放在 LRU_old 處。
3. 處於 old 區域的數據頁,每次被訪問的時候都要作下面這個判斷:
若這個數據頁在 LRU 鏈表中存在的時間超過了 1 秒,就把它移動到鏈表頭部;
若是這個數據頁在 LRU 鏈表中存在的時間短於 1 秒,位置保持不變。1 秒這個時間,是由參數 innodb_old_blocks_time 控制的。其默認值是 1000,單位毫秒。
這個策略,就是爲了處理相似全表掃描的操做量身定製的。仍是以剛剛的掃描 200G 的歷史數據表爲例,咱們看看改進後的 LRU 算法的操做邏輯:
1. 掃描過程當中,須要新插入的數據頁,都被放到 old 區域 ;
2. 一個數據頁裏面有多條記錄,這個數據頁會被屢次訪問到,但因爲是順序掃描,這個數據頁第一次被訪問和最後一次被訪問的時間間隔不會超過 1 秒,所以仍是會被保留在old 區域;
3. 再繼續掃描後續的數據,以前的這個數據頁以後也不會再被訪問到,因而始終沒有機會移到鏈表頭部(也就是 young 區域),很快就會被淘汰出去。
能夠看到,這個策略最大的收益,就是在掃描這個大表的過程當中,雖然也用到了 BufferPool,可是對 young 區域徹底沒有影響,從而保證了 Buffer Pool 響應正常業務的查詢命中率。
今天,我用「大查詢會不會把內存用光」這個問題,和你介紹了 MySQL 的查詢結果,發送給客戶端的過程。
因爲 MySQL 採用的是邊算邊發的邏輯,所以對於數據量很大的查詢結果來講,不會在server 端保存完整的結果集。因此,若是客戶端讀結果不及時,會堵住 MySQL 的查詢過
程,可是不會把內存打爆。
而對於 InnoDB 引擎內部,因爲有淘汰策略,大查詢也不會致使內存暴漲。而且,因爲InnoDB 對 LRU 算法作了改進,冷數據的全表掃描,對 Buffer Pool 的影響也能作到可控。
固然,咱們前面文章有說過,全表掃描仍是比較耗費 IO 資源的,因此業務高峯期仍是不能直接在線上主庫執行全表掃描的。
最後,我給你留一個思考題吧。
我在文章中說到,若是因爲客戶端壓力太大,遲遲不能接收結果,會致使 MySQL 沒法發送結果而影響語句執行。但,這還不是最糟糕的狀況。
你能夠設想出因爲客戶端的性能問題,對數據庫影響更嚴重的例子嗎?或者你是否經歷過這樣的場景?你又是怎麼優化的?
你能夠把你的經驗和分析寫在留言區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上期的問題是,若是一個事務被 kill 以後,持續處於回滾狀態,從恢復速度的角度看,你是應該重啓等它執行結束,仍是應該強行重啓整個 MySQL 進程。
由於重啓以後該作的回滾動做仍是不能少的,因此從恢復速度的角度來講,應該讓它本身結束。
固然,若是這個語句可能會佔用別的鎖,或者因爲佔用 IO 資源過多,從而影響到了別的語句執行的話,就須要先作主備切換,切到新主庫提供服務。
切換以後別的線程都斷開了鏈接,自動中止執行。接下來仍是等它本身執行完成。這個操做屬於咱們在文章中說到的,減小系統壓力,加速終止邏輯。
@HuaMax 的回答中提到了對其餘線程的影響;@夾心麪包 @Ryoma @曾劍 同窗提到了重啓後依然繼續作回滾操做的邏