mysql实战33 | 我查这么多数据,会不会把数据库内存打爆?
我經(jīng)常會(huì)被問(wèn)到這樣一個(gè)問(wèn)題:我的主機(jī)內(nèi)存只有 100G,現(xiàn)在要對(duì)一個(gè) 200G 的大表做全表掃描,會(huì)不會(huì)把數(shù)據(jù)庫(kù)主機(jī)的內(nèi)存用光了?
這個(gè)問(wèn)題確實(shí)值得擔(dān)心,被系統(tǒng) OOM(out of memory)可不是鬧著玩的。但是,反過(guò)來(lái)想想,邏輯備份的時(shí)候,可不就是做整庫(kù)掃描嗎?如果這樣就會(huì)把內(nèi)存吃光,邏輯備份不是早就掛了?
所以說(shuō),對(duì)大表做全表掃描,看來(lái)應(yīng)該是沒(méi)問(wèn)題的。但是,這個(gè)流程到底是怎么樣的呢?
全表掃描對(duì) server 層的影響
假設(shè),我們現(xiàn)在要對(duì)一個(gè) 200G 的 InnoDB 表 db1. t,執(zhí)行一個(gè)全表掃描。當(dāng)然,你要把掃描結(jié)果保存在客戶端,會(huì)使用類似這樣的命令:
你已經(jīng)知道了,InnoDB 的數(shù)據(jù)是保存在主鍵索引上的,所以全表掃描實(shí)際上是直接掃描表 t 的主鍵索引。這條查詢語(yǔ)句由于沒(méi)有其他的判斷條件,所以查到的每一行都可以直接放到結(jié)果集里面,然后返回給客戶端。
那么,這個(gè)“結(jié)果集”存在哪里呢?
實(shí)際上,服務(wù)端并不需要保存一個(gè)完整的結(jié)果集。取數(shù)據(jù)和發(fā)數(shù)據(jù)的流程是這樣的:
這個(gè)過(guò)程對(duì)應(yīng)的流程圖如下所示。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 1 查詢結(jié)果發(fā)送流程
從這個(gè)流程中,你可以看到:
也就是說(shuō),MySQL 是“邊讀邊發(fā)的”,這個(gè)概念很重要。這就意味著,如果客戶端接收得慢,會(huì)導(dǎo)致 MySQL 服務(wù)端由于結(jié)果發(fā)不出去,這個(gè)事務(wù)的執(zhí)行時(shí)間變長(zhǎng)。
比如下面這個(gè)狀態(tài),就是我故意讓客戶端不去讀 socket receive buffer 中的內(nèi)容,然后在服務(wù)端 show processlist 看到的結(jié)果。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 2 服務(wù)端發(fā)送阻塞
如果你看到 State 的值一直處于“Sending to client”,就表示服務(wù)器端的網(wǎng)絡(luò)棧寫(xiě)滿了。
我在上一篇文章中曾提到,如果客戶端使用–quick 參數(shù),會(huì)使用 mysql_use_result 方法。這個(gè)方法是讀一行處理一行。你可以想象一下,假設(shè)有一個(gè)業(yè)務(wù)的邏輯比較復(fù)雜,每讀一行數(shù)據(jù)以后要處理的邏輯如果很慢,就會(huì)導(dǎo)致客戶端要過(guò)很久才會(huì)去取下一行數(shù)據(jù),可能就會(huì)出現(xiàn)如圖 2 所示的這種情況。
因此,對(duì)于正常的線上業(yè)務(wù)來(lái)說(shuō),如果一個(gè)查詢的返回結(jié)果不會(huì)很多的話,我都建議你使用 mysql_store_result 這個(gè)接口,直接把查詢結(jié)果保存到本地內(nèi)存。
當(dāng)然前提是查詢返回結(jié)果不多。在第 30 篇文章評(píng)論區(qū),有同學(xué)說(shuō)到自己因?yàn)閳?zhí)行了一個(gè)大查詢導(dǎo)致客戶端占用內(nèi)存近 20G,這種情況下就需要改用 mysql_use_result 接口了。
另一方面,如果你在自己負(fù)責(zé)維護(hù)的 MySQL 里看到很多個(gè)線程都處于“Sending to client”這個(gè)狀態(tài),就意味著你要讓業(yè)務(wù)開(kāi)發(fā)同學(xué)優(yōu)化查詢結(jié)果,并評(píng)估這么多的返回結(jié)果是否合理。
而如果要快速減少處于這個(gè)狀態(tài)的線程的話,將 net_buffer_length 參數(shù)設(shè)置為一個(gè)更大的值是一個(gè)可選方案。
與“Sending to client”長(zhǎng)相很類似的一個(gè)狀態(tài)是“Sending data”,這是一個(gè)經(jīng)常被誤會(huì)的問(wèn)題。有同學(xué)問(wèn)我說(shuō),在自己維護(hù)的實(shí)例上看到很多查詢語(yǔ)句的狀態(tài)是“Sending data”,但查看網(wǎng)絡(luò)也沒(méi)什么問(wèn)題啊,為什么 Sending data 要這么久?
實(shí)際上,一個(gè)查詢語(yǔ)句的狀態(tài)變化是這樣的(注意:這里,我略去了其他無(wú)關(guān)的狀態(tài)):
- MySQL 查詢語(yǔ)句進(jìn)入執(zhí)行階段后,首先把狀態(tài)設(shè)置成“Sending data”;
- 然后,發(fā)送執(zhí)行結(jié)果的列相關(guān)的信息(meta data) 給客戶端;
- 再繼續(xù)執(zhí)行語(yǔ)句的流程;
- 執(zhí)行完成后,把狀態(tài)設(shè)置成空字符串。
也就是說(shuō),“Sending data”并不一定是指“正在發(fā)送數(shù)據(jù)”,而可能是處于執(zhí)行器過(guò)程中的任意階段。比如,你可以構(gòu)造一個(gè)鎖等待的場(chǎng)景,就能看到 Sending data 狀態(tài)。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 3 讀全表被鎖
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 4 Sending data 狀態(tài)
可以看到,session B 明顯是在等鎖,狀態(tài)顯示為 Sending data。
也就是說(shuō),僅當(dāng)一個(gè)線程處于“等待客戶端接收結(jié)果”的狀態(tài),才會(huì)顯示"Sending to client";而如果顯示成“Sending data”,它的意思只是“正在執(zhí)行”。
現(xiàn)在你知道了,查詢的結(jié)果是分段發(fā)給客戶端的,因此掃描全表,查詢返回大量的數(shù)據(jù),并不會(huì)把內(nèi)存打爆。
在 server 層的處理邏輯我們都清楚了,在 InnoDB 引擎里面又是怎么處理的呢? 掃描全表會(huì)不會(huì)對(duì)引擎系統(tǒng)造成影響呢?
全表掃描對(duì) InnoDB 的影響
在第 2和第 15 篇文章中,我介紹 WAL 機(jī)制的時(shí)候,和你分析了 InnoDB 內(nèi)存的一個(gè)作用,是保存更新的結(jié)果,再配合 redo log,就避免了隨機(jī)寫(xiě)盤(pán)。
內(nèi)存的數(shù)據(jù)頁(yè)是在 Buffer Pool (BP) 中管理的,在 WAL 里 Buffer Pool 起到了加速更新的作用。而實(shí)際上,Buffer Pool 還有一個(gè)更重要的作用,就是加速查詢。
在第 2 篇文章的評(píng)論區(qū)有同學(xué)問(wèn)道,由于有 WAL 機(jī)制,當(dāng)事務(wù)提交的時(shí)候,磁盤(pán)上的數(shù)據(jù)頁(yè)是舊的,那如果這時(shí)候馬上有一個(gè)查詢要來(lái)讀這個(gè)數(shù)據(jù)頁(yè),是不是要馬上把 redo log 應(yīng)用到數(shù)據(jù)頁(yè)呢?
答案是不需要。因?yàn)檫@時(shí)候內(nèi)存數(shù)據(jù)頁(yè)的結(jié)果是最新的,直接讀內(nèi)存頁(yè)就可以了。你看,這時(shí)候查詢根本不需要讀磁盤(pán),直接從內(nèi)存拿結(jié)果,速度是很快的。所以說(shuō),Buffer Pool 還有加速查詢的作用。
而 Buffer Pool 對(duì)查詢的加速效果,依賴于一個(gè)重要的指標(biāo),即:內(nèi)存命中率
你可以在 show engine innodb status 結(jié)果中,查看一個(gè)系統(tǒng)當(dāng)前的 BP 命中率。一般情況下,一個(gè)穩(wěn)定服務(wù)的線上系統(tǒng),要保證響應(yīng)時(shí)間符合要求的話,內(nèi)存命中率要在 99% 以上。
執(zhí)行 show engine innodb status ,可以看到“Buffer pool hit rate”字樣,顯示的就是當(dāng)前的命中率。比如圖 5 這個(gè)命中率,就是 99.0%。
? ? ? ? ? ? ? ? ? 圖 5 show engine innodb status 顯示內(nèi)存命中率
如果所有查詢需要的數(shù)據(jù)頁(yè)都能夠直接從內(nèi)存得到,那是最好的,對(duì)應(yīng)的命中率就是 100%。但,這在實(shí)際生產(chǎn)上是很難做到的。
InnoDB Buffer Pool 的大小是由參數(shù) innodb_buffer_pool_size 確定的,一般建議設(shè)置成可用物理內(nèi)存的 60%~80%。
在大約十年前,單機(jī)的數(shù)據(jù)量是上百個(gè) G,而物理內(nèi)存是幾個(gè) G;現(xiàn)在雖然很多服務(wù)器都能有 128G 甚至更高的內(nèi)存,但是單機(jī)的數(shù)據(jù)量卻達(dá)到了 T 級(jí)別。
所以,innodb_buffer_pool_size 小于磁盤(pán)的數(shù)據(jù)量是很常見(jiàn)的。如果一個(gè) Buffer Pool 滿了,而又要從磁盤(pán)讀入一個(gè)數(shù)據(jù)頁(yè),那肯定是要淘汰一個(gè)舊數(shù)據(jù)頁(yè)的。
InnoDB 內(nèi)存管理用的是最近最少使用 (Least Recently Used, LRU) 算法,這個(gè)算法的核心就是淘汰最久未使用的數(shù)據(jù)。
下圖是一個(gè) LRU 算法的基本模型。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 6 基本 LRU 算法
InnoDB 管理 Buffer Pool 的 LRU 算法,是用鏈表來(lái)實(shí)現(xiàn)的。
這個(gè)算法乍一看上去沒(méi)什么問(wèn)題,但是如果考慮到要做一個(gè)全表掃描,會(huì)不會(huì)有問(wèn)題呢?
假設(shè)按照這個(gè)算法,我們要掃描一個(gè) 200G 的表,而這個(gè)表是一個(gè)歷史數(shù)據(jù)表,平時(shí)沒(méi)有業(yè)務(wù)訪問(wèn)它。
那么,按照這個(gè)算法掃描的話,就會(huì)把當(dāng)前的 Buffer Pool 里的數(shù)據(jù)全部淘汰掉,存入掃描過(guò)程中訪問(wèn)到的數(shù)據(jù)頁(yè)的內(nèi)容。也就是說(shuō) Buffer Pool 里面主要放的是這個(gè)歷史數(shù)據(jù)表的數(shù)據(jù)。
對(duì)于一個(gè)正在做業(yè)務(wù)服務(wù)的庫(kù),這可不妙。你會(huì)看到,Buffer Pool 的內(nèi)存命中率急劇下降,磁盤(pán)壓力增加,SQL 語(yǔ)句響應(yīng)變慢。
所以,InnoDB 不能直接使用這個(gè) LRU 算法。實(shí)際上,InnoDB 對(duì) LRU 算法做了改進(jìn)。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 7 改進(jìn)的 LRU 算法
在 InnoDB 實(shí)現(xiàn)上,按照 5:3 的比例把整個(gè) LRU 鏈表分成了 young 區(qū)域和 old 區(qū)域。圖中 LRU_old 指向的就是 old 區(qū)域的第一個(gè)位置,是整個(gè)鏈表的 5/8 處。也就是說(shuō),靠近鏈表頭部的 5/8 是 young 區(qū)域,靠近鏈表尾部的 3/8 是 old 區(qū)域。
改進(jìn)后的 LRU 算法執(zhí)行流程變成了下面這樣。
這個(gè)策略,就是為了處理類似全表掃描的操作量身定制的。還是以剛剛的掃描 200G 的歷史數(shù)據(jù)表為例,我們看看改進(jìn)后的 LRU 算法的操作邏輯:
可以看到,這個(gè)策略最大的收益,就是在掃描這個(gè)大表的過(guò)程中,雖然也用到了 Buffer Pool,但是對(duì) young 區(qū)域完全沒(méi)有影響,從而保證了 Buffer Pool 響應(yīng)正常業(yè)務(wù)的查詢命中率。
小結(jié)
今天,我用“大查詢會(huì)不會(huì)把內(nèi)存用光”這個(gè)問(wèn)題,和你介紹了 MySQL 的查詢結(jié)果,發(fā)送給客戶端的過(guò)程。
由于 MySQL 采用的是邊算邊發(fā)的邏輯,因此對(duì)于數(shù)據(jù)量很大的查詢結(jié)果來(lái)說(shuō),不會(huì)在 server 端保存完整的結(jié)果集。所以,如果客戶端讀結(jié)果不及時(shí),會(huì)堵住 MySQL 的查詢過(guò)程,但是不會(huì)把內(nèi)存打爆。
而對(duì)于 InnoDB 引擎內(nèi)部,由于有淘汰策略,大查詢也不會(huì)導(dǎo)致內(nèi)存暴漲。并且,由于 InnoDB 對(duì) LRU 算法做了改進(jìn),冷數(shù)據(jù)的全表掃描,對(duì) Buffer Pool 的影響也能做到可控。
當(dāng)然,我們前面文章有說(shuō)過(guò),全表掃描還是比較耗費(fèi) IO 資源的,所以業(yè)務(wù)高峰期還是不能直接在線上主庫(kù)執(zhí)行全表掃描的。
最后,我給你留一個(gè)思考題吧。
我在文章中說(shuō)到,如果由于客戶端壓力太大,遲遲不能接收結(jié)果,會(huì)導(dǎo)致 MySQL 無(wú)法發(fā)送結(jié)果而影響語(yǔ)句執(zhí)行。但,這還不是最糟糕的情況。
你可以設(shè)想出由于客戶端的性能問(wèn)題,對(duì)數(shù)據(jù)庫(kù)影響更嚴(yán)重的例子嗎?或者你是否經(jīng)歷過(guò)這樣的場(chǎng)景?你又是怎么優(yōu)化的?
你可以把你的經(jīng)驗(yàn)和分析寫(xiě)在留言區(qū),我會(huì)在下一篇文章的末尾和你討論這個(gè)問(wèn)題。感謝你的收聽(tīng),也歡迎你把這篇文章分享給更多的朋友一起閱讀。
上期問(wèn)題時(shí)間
上期的問(wèn)題是,如果一個(gè)事務(wù)被 kill 之后,持續(xù)處于回滾狀態(tài),從恢復(fù)速度的角度看,你是應(yīng)該重啟等它執(zhí)行結(jié)束,還是應(yīng)該強(qiáng)行重啟整個(gè) MySQL 進(jìn)程。
因?yàn)橹貑⒅笤撟龅幕貪L動(dòng)作還是不能少的,所以從恢復(fù)速度的角度來(lái)說(shuō),應(yīng)該讓它自己結(jié)束。
當(dāng)然,如果這個(gè)語(yǔ)句可能會(huì)占用別的鎖,或者由于占用 IO 資源過(guò)多,從而影響到了別的語(yǔ)句執(zhí)行的話,就需要先做主備切換,切到新主庫(kù)提供服務(wù)。
切換之后別的線程都斷開(kāi)了連接,自動(dòng)停止執(zhí)行。接下來(lái)還是等它自己執(zhí)行完成。這個(gè)操作屬于我們?cè)谖恼轮姓f(shuō)到的,減少系統(tǒng)壓力,加速終止邏輯。
轉(zhuǎn)載于:https://juejin.im/post/5d05cd43f265da1ba9157c32
總結(jié)
以上是生活随笔為你收集整理的mysql实战33 | 我查这么多数据,会不会把数据库内存打爆?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 09、策略模式
- 下一篇: WordPress Option API