日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

mysql实战33 | 我查这么多数据,会不会把数据库内存打爆?

發(fā)布時間:2023/11/30 数据库 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 mysql实战33 | 我查这么多数据,会不会把数据库内存打爆? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

我經(jīng)常會被問到這樣一個問題:我的主機內(nèi)存只有 100G,現(xiàn)在要對一個 200G 的大表做全表掃描,會不會把數(shù)據(jù)庫主機的內(nèi)存用光了?

這個問題確實值得擔心,被系統(tǒng) OOM(out of memory)可不是鬧著玩的。但是,反過來想想,邏輯備份的時候,可不就是做整庫掃描嗎?如果這樣就會把內(nèi)存吃光,邏輯備份不是早就掛了?

所以說,對大表做全表掃描,看來應該是沒問題的。但是,這個流程到底是怎么樣的呢?

全表掃描對 server 層的影響

假設,我們現(xiàn)在要對一個 200G 的 InnoDB 表 db1. t,執(zhí)行一個全表掃描。當然,你要把掃描結(jié)果保存在客戶端,會使用類似這樣的命令:

mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file 復制代碼

你已經(jīng)知道了,InnoDB 的數(shù)據(jù)是保存在主鍵索引上的,所以全表掃描實際上是直接掃描表 t 的主鍵索引。這條查詢語句由于沒有其他的判斷條件,所以查到的每一行都可以直接放到結(jié)果集里面,然后返回給客戶端。

那么,這個“結(jié)果集”存在哪里呢?

實際上,服務端并不需要保存一個完整的結(jié)果集。取數(shù)據(jù)和發(fā)數(shù)據(jù)的流程是這樣的:

  • 獲取一行,寫到 net_buffer 中。這塊內(nèi)存的大小是由參數(shù) net_buffer_length 定義的,默認是 16k。
  • 重復獲取行,直到 net_buffer 寫滿,調(diào)用網(wǎng)絡接口發(fā)出去。
  • 如果發(fā)送成功,就清空 net_buffer,然后繼續(xù)取下一行,并寫入 net_buffer。
  • 如果發(fā)送函數(shù)返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地網(wǎng)絡棧(socket send buffer)寫滿了,進入等待。直到網(wǎng)絡棧重新可寫,再繼續(xù)發(fā)送。
  • 這個過程對應的流程圖如下所示。


    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 1 查詢結(jié)果發(fā)送流程

    從這個流程中,你可以看到:

  • 一個查詢在發(fā)送過程中,占用的 MySQL 內(nèi)部的內(nèi)存最大就是 net_buffer_length 這么大,并不會達到 200G;
  • socket send buffer 也不可能達到 200G(默認定義 /proc/sys/net/core/wmem_default),如果 socket send buffer 被寫滿,就會暫停讀數(shù)據(jù)的流程。
  • 也就是說,MySQL 是“邊讀邊發(fā)的”,這個概念很重要。這就意味著,如果客戶端接收得慢,會導致 MySQL 服務端由于結(jié)果發(fā)不出去,這個事務的執(zhí)行時間變長。

    比如下面這個狀態(tài),就是我故意讓客戶端不去讀 socket receive buffer 中的內(nèi)容,然后在服務端 show processlist 看到的結(jié)果。


    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 2 服務端發(fā)送阻塞

    如果你看到 State 的值一直處于“Sending to client”,就表示服務器端的網(wǎng)絡棧寫滿了。

    我在上一篇文章中曾提到,如果客戶端使用–quick 參數(shù),會使用 mysql_use_result 方法。這個方法是讀一行處理一行。你可以想象一下,假設有一個業(yè)務的邏輯比較復雜,每讀一行數(shù)據(jù)以后要處理的邏輯如果很慢,就會導致客戶端要過很久才會去取下一行數(shù)據(jù),可能就會出現(xiàn)如圖 2 所示的這種情況。

    因此,對于正常的線上業(yè)務來說,如果一個查詢的返回結(jié)果不會很多的話,我都建議你使用 mysql_store_result 這個接口,直接把查詢結(jié)果保存到本地內(nèi)存。

    當然前提是查詢返回結(jié)果不多。在第 30 篇文章評論區(qū),有同學說到自己因為執(zhí)行了一個大查詢導致客戶端占用內(nèi)存近 20G,這種情況下就需要改用 mysql_use_result 接口了。

    另一方面,如果你在自己負責維護的 MySQL 里看到很多個線程都處于“Sending to client”這個狀態(tài),就意味著你要讓業(yè)務開發(fā)同學優(yōu)化查詢結(jié)果,并評估這么多的返回結(jié)果是否合理。

    而如果要快速減少處于這個狀態(tài)的線程的話,將 net_buffer_length 參數(shù)設置為一個更大的值是一個可選方案。

    與“Sending to client”長相很類似的一個狀態(tài)是“Sending data”,這是一個經(jīng)常被誤會的問題。有同學問我說,在自己維護的實例上看到很多查詢語句的狀態(tài)是“Sending data”,但查看網(wǎng)絡也沒什么問題啊,為什么 Sending data 要這么久?

    實際上,一個查詢語句的狀態(tài)變化是這樣的(注意:這里,我略去了其他無關(guān)的狀態(tài)):

    • MySQL 查詢語句進入執(zhí)行階段后,首先把狀態(tài)設置成“Sending data”;
    • 然后,發(fā)送執(zhí)行結(jié)果的列相關(guān)的信息(meta data) 給客戶端;
    • 再繼續(xù)執(zhí)行語句的流程;
    • 執(zhí)行完成后,把狀態(tài)設置成空字符串。

    也就是說,“Sending data”并不一定是指“正在發(fā)送數(shù)據(jù)”,而可能是處于執(zhí)行器過程中的任意階段。比如,你可以構(gòu)造一個鎖等待的場景,就能看到 Sending data 狀態(tài)。


    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 3 讀全表被鎖


    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 4 Sending data 狀態(tài)

    可以看到,session B 明顯是在等鎖,狀態(tài)顯示為 Sending data。

    也就是說,僅當一個線程處于“等待客戶端接收結(jié)果”的狀態(tài),才會顯示"Sending to client";而如果顯示成“Sending data”,它的意思只是“正在執(zhí)行”。

    現(xiàn)在你知道了,查詢的結(jié)果是分段發(fā)給客戶端的,因此掃描全表,查詢返回大量的數(shù)據(jù),并不會把內(nèi)存打爆。

    在 server 層的處理邏輯我們都清楚了,在 InnoDB 引擎里面又是怎么處理的呢? 掃描全表會不會對引擎系統(tǒng)造成影響呢?

    全表掃描對 InnoDB 的影響

    在第 2和第 15 篇文章中,我介紹 WAL 機制的時候,和你分析了 InnoDB 內(nèi)存的一個作用,是保存更新的結(jié)果,再配合 redo log,就避免了隨機寫盤。

    內(nèi)存的數(shù)據(jù)頁是在 Buffer Pool (BP) 中管理的,在 WAL 里 Buffer Pool 起到了加速更新的作用。而實際上,Buffer Pool 還有一個更重要的作用,就是加速查詢。

    在第 2 篇文章的評論區(qū)有同學問道,由于有 WAL 機制,當事務提交的時候,磁盤上的數(shù)據(jù)頁是舊的,那如果這時候馬上有一個查詢要來讀這個數(shù)據(jù)頁,是不是要馬上把 redo log 應用到數(shù)據(jù)頁呢?

    答案是不需要。因為這時候內(nèi)存數(shù)據(jù)頁的結(jié)果是最新的,直接讀內(nèi)存頁就可以了。你看,這時候查詢根本不需要讀磁盤,直接從內(nèi)存拿結(jié)果,速度是很快的。所以說,Buffer Pool 還有加速查詢的作用。

    而 Buffer Pool 對查詢的加速效果,依賴于一個重要的指標,即:內(nèi)存命中率

    你可以在 show engine innodb status 結(jié)果中,查看一個系統(tǒng)當前的 BP 命中率。一般情況下,一個穩(wěn)定服務的線上系統(tǒng),要保證響應時間符合要求的話,內(nèi)存命中率要在 99% 以上。

    執(zhí)行 show engine innodb status ,可以看到“Buffer pool hit rate”字樣,顯示的就是當前的命中率。比如圖 5 這個命中率,就是 99.0%。


    ? ? ? ? ? ? ? ? ? 圖 5 show engine innodb status 顯示內(nèi)存命中率

    如果所有查詢需要的數(shù)據(jù)頁都能夠直接從內(nèi)存得到,那是最好的,對應的命中率就是 100%。但,這在實際生產(chǎn)上是很難做到的。

    InnoDB Buffer Pool 的大小是由參數(shù) innodb_buffer_pool_size 確定的,一般建議設置成可用物理內(nèi)存的 60%~80%。

    在大約十年前,單機的數(shù)據(jù)量是上百個 G,而物理內(nèi)存是幾個 G;現(xiàn)在雖然很多服務器都能有 128G 甚至更高的內(nèi)存,但是單機的數(shù)據(jù)量卻達到了 T 級別。

    所以,innodb_buffer_pool_size 小于磁盤的數(shù)據(jù)量是很常見的。如果一個 Buffer Pool 滿了,而又要從磁盤讀入一個數(shù)據(jù)頁,那肯定是要淘汰一個舊數(shù)據(jù)頁的。

    InnoDB 內(nèi)存管理用的是最近最少使用 (Least Recently Used, LRU) 算法,這個算法的核心就是淘汰最久未使用的數(shù)據(jù)。

    下圖是一個 LRU 算法的基本模型。


    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 6 基本 LRU 算法

    InnoDB 管理 Buffer Pool 的 LRU 算法,是用鏈表來實現(xiàn)的。

  • 在圖 6 的狀態(tài) 1 里,鏈表頭部是 P1,表示 P1 是最近剛剛被訪問過的數(shù)據(jù)頁;假設內(nèi)存里只能放下這么多數(shù)據(jù)頁;
  • 這時候有一個讀請求訪問 P3,因此變成狀態(tài) 2,P3 被移到最前面;
  • 狀態(tài) 3 表示,這次訪問的數(shù)據(jù)頁是不存在于鏈表中的,所以需要在 Buffer Pool 中新申請一個數(shù)據(jù)頁 Px,加到鏈表頭部。但是由于內(nèi)存已經(jīng)滿了,不能申請新的內(nèi)存。于是,會清空鏈表末尾 Pm 這個數(shù)據(jù)頁的內(nèi)存,存入 Px 的內(nèi)容,然后放到鏈表頭部。
  • 從效果上看,就是最久沒有被訪問的數(shù)據(jù)頁 Pm,被淘汰了。
  • 這個算法乍一看上去沒什么問題,但是如果考慮到要做一個全表掃描,會不會有問題呢?

    假設按照這個算法,我們要掃描一個 200G 的表,而這個表是一個歷史數(shù)據(jù)表,平時沒有業(yè)務訪問它。

    那么,按照這個算法掃描的話,就會把當前的 Buffer Pool 里的數(shù)據(jù)全部淘汰掉,存入掃描過程中訪問到的數(shù)據(jù)頁的內(nèi)容。也就是說 Buffer Pool 里面主要放的是這個歷史數(shù)據(jù)表的數(shù)據(jù)。

    對于一個正在做業(yè)務服務的庫,這可不妙。你會看到,Buffer Pool 的內(nèi)存命中率急劇下降,磁盤壓力增加,SQL 語句響應變慢。

    所以,InnoDB 不能直接使用這個 LRU 算法。實際上,InnoDB 對 LRU 算法做了改進。


    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖 7 改進的 LRU 算法

    在 InnoDB 實現(xiàn)上,按照 5:3 的比例把整個 LRU 鏈表分成了 young 區(qū)域和 old 區(qū)域。圖中 LRU_old 指向的就是 old 區(qū)域的第一個位置,是整個鏈表的 5/8 處。也就是說,靠近鏈表頭部的 5/8 是 young 區(qū)域,靠近鏈表尾部的 3/8 是 old 區(qū)域。

    改進后的 LRU 算法執(zhí)行流程變成了下面這樣。

  • 圖 7 中狀態(tài) 1,要訪問數(shù)據(jù)頁 P3,由于 P3 在 young 區(qū)域,因此和優(yōu)化前的 LRU 算法一樣,將其移到鏈表頭部,變成狀態(tài) 2。
  • 之后要訪問一個新的不存在于當前鏈表的數(shù)據(jù)頁,這時候依然是淘汰掉數(shù)據(jù)頁 Pm,但是新插入的數(shù)據(jù)頁 Px,是放在 LRU_old 處。
  • 處于 old 區(qū)域的數(shù)據(jù)頁,每次被訪問的時候都要做下面這個判斷:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?a:若這個數(shù)據(jù)頁在 LRU 鏈表中存在的時間超過了 1 秒,就把它移動到鏈表頭部;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?b:如果這個數(shù)據(jù)頁在 LRU 鏈表中存在的時間短于 1 秒,位置保持不變。1 秒這個時間,是由參數(shù)? ? ?innodb_old_blocks_time 控制的。其默認值是 1000,單位毫秒。?

  • 這個策略,就是為了處理類似全表掃描的操作量身定制的。還是以剛剛的掃描 200G 的歷史數(shù)據(jù)表為例,我們看看改進后的 LRU 算法的操作邏輯:

  • 掃描過程中,需要新插入的數(shù)據(jù)頁,都被放到 old 區(qū)域 ;
  • 一個數(shù)據(jù)頁里面有多條記錄,這個數(shù)據(jù)頁會被多次訪問到,但由于是順序掃描,這個數(shù)據(jù)頁第一次被訪問和最后一次被訪問的時間間隔不會超過 1 秒,因此還是會被保留在 old 區(qū)域;
  • 再繼續(xù)掃描后續(xù)的數(shù)據(jù),之前的這個數(shù)據(jù)頁之后也不會再被訪問到,于是始終沒有機會移到鏈表頭部(也就是 young 區(qū)域),很快就會被淘汰出去。
  • 可以看到,這個策略最大的收益,就是在掃描這個大表的過程中,雖然也用到了 Buffer Pool,但是對 young 區(qū)域完全沒有影響,從而保證了 Buffer Pool 響應正常業(yè)務的查詢命中率。

    小結(jié)

    今天,我用“大查詢會不會把內(nèi)存用光”這個問題,和你介紹了 MySQL 的查詢結(jié)果,發(fā)送給客戶端的過程。

    由于 MySQL 采用的是邊算邊發(fā)的邏輯,因此對于數(shù)據(jù)量很大的查詢結(jié)果來說,不會在 server 端保存完整的結(jié)果集。所以,如果客戶端讀結(jié)果不及時,會堵住 MySQL 的查詢過程,但是不會把內(nèi)存打爆。

    而對于 InnoDB 引擎內(nèi)部,由于有淘汰策略,大查詢也不會導致內(nèi)存暴漲。并且,由于 InnoDB 對 LRU 算法做了改進,冷數(shù)據(jù)的全表掃描,對 Buffer Pool 的影響也能做到可控。

    當然,我們前面文章有說過,全表掃描還是比較耗費 IO 資源的,所以業(yè)務高峰期還是不能直接在線上主庫執(zhí)行全表掃描的。

    最后,我給你留一個思考題吧。

    我在文章中說到,如果由于客戶端壓力太大,遲遲不能接收結(jié)果,會導致 MySQL 無法發(fā)送結(jié)果而影響語句執(zhí)行。但,這還不是最糟糕的情況。

    你可以設想出由于客戶端的性能問題,對數(shù)據(jù)庫影響更嚴重的例子嗎?或者你是否經(jīng)歷過這樣的場景?你又是怎么優(yōu)化的?

    你可以把你的經(jīng)驗和分析寫在留言區(qū),我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。

    上期問題時間

    上期的問題是,如果一個事務被 kill 之后,持續(xù)處于回滾狀態(tài),從恢復速度的角度看,你是應該重啟等它執(zhí)行結(jié)束,還是應該強行重啟整個 MySQL 進程。

    因為重啟之后該做的回滾動作還是不能少的,所以從恢復速度的角度來說,應該讓它自己結(jié)束。

    當然,如果這個語句可能會占用別的鎖,或者由于占用 IO 資源過多,從而影響到了別的語句執(zhí)行的話,就需要先做主備切換,切到新主庫提供服務。

    切換之后別的線程都斷開了連接,自動停止執(zhí)行。接下來還是等它自己執(zhí)行完成。這個操作屬于我們在文章中說到的,減少系統(tǒng)壓力,加速終止邏輯。

    轉(zhuǎn)載于:https://juejin.im/post/5d05cd43f265da1ba9157c32

    總結(jié)

    以上是生活随笔為你收集整理的mysql实战33 | 我查这么多数据,会不会把数据库内存打爆?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。