聊聊 MySQL 網路緩衝區、net_buffer_length、max_allowed_packet 那些事

green_hand發表於2022-10-10

本文主要介紹了連線緩衝區、結果集緩衝區,它們的自動增長邏輯和上下限。以及超過 16M 的大資料怎麼傳送和接收,net_buffer_length、max_allowed_packet 兩個系統變數的配

本文由作者公眾號一樹一溪首發,歡迎關注。

本文是 MySQL 簡單查詢語句執行過程分析 6 篇中的第 6 篇,第 1 ~ 5 篇請看這裡:
1. 詞法分析 & 語法分析
2. 查詢準備階段
3. 從 InnoDB 讀資料
4. WHERE 條件
5. 傳送資料給客戶端

網路緩衝區,顧名思義,就是從網路連線讀取資料,或者向網路連線傳送資料時使用的緩衝區。

說到 MySQL 的網路緩衝區,不得不先說一個 MySQL 系統變數 net_buffer_length,從它的名字可以看到,這個系統變數是用來控制網路緩衝區大小的。

由於對 net_buffer_length 這個系統變數的印象根深蒂固,我一直認為 MySQL 只有一個網路緩衝區,直到最近研究原始碼的時候又看了一下官方文件中關於 net_buffer_length 的描述,才發現在內部實現中,實際上用了 2 個緩衝區。

官方文件描述的意思是這樣的:

每個客戶端執行緒都關聯了 1 個連線緩衝區(connection buffer)和 1 個結果集緩衝區(result buffer),這 2 個緩衝區的初始大小都由 net_buffer_length 控制,需要時最大可以自動增長到不超過 max_allowed_packet。每條 SQL 語句執行完成後,結果集緩衝區都會自動恢復到 net_buffer_length 指定的大小。

一般情況下,不應該修改 net_buffer_length 的值,如果這個值設定得非常小,你可以把它修改為客戶端傳送的 SQL 語句預期的長度。如果 SQL 語句長度超過這個值,連線緩衝區還會自動增長。net_buffer_length 的最大值為 1048576(1M)。

看到上面兩段描述,可能有的小夥伴會奇怪,為什麼第一段中說連線緩衝區的小大可以自動增長到不超過 max_allowed_packet,第二段中卻說 net_buffer_length 最大隻能設定為 1048576,這不是矛盾嗎?這個邏輯是沒問題的,net_buffer_length 只用來控制連線緩衝區的初始大小,一旦連線緩衝區初始化完成,它就不受 net_buffer_length 控制了,而是受 max_allowed_packet 控制,也就是說:net_buffer_length 控制連線緩衝區的下限,max_allowed_packet 控制連線緩衝區的上限

以下是官方文件原文:

Each client thread is associated with a connection buffer and result buffer. Both begin with a size given by net_buffer_length but are dynamically enlarged up to max_allowed_packet bytes as needed. The result buffer shrinks to net_buffer_length after each SQL statement. This variable should not normally be changed, but if you have very little memory, you can set it to the expected length of statements sent by clients. If statements exceed this length, the connection buffer is automatically enlarged. The maximum value to which net_buffer_length can be set is 1MB.

從官方的描述可以看到幾個關鍵點:

  • 緩衝區有 2 個:連線緩衝區、結果集緩衝區。

  • 緩衝區初始大小都由 net_buffer_length 控制。

  • net_buffer_length 的大小不能超過 1M。

  • 連線緩衝區可以自動增長,但是其大小必須小於等於 max_allowed_packet。


資料包是 MySQL 傳送資料的基本單元,接下來我們從資料包開始,分為三個部分來聊聊網路緩衝區那些事。

  1. 資料包(packet)

MySQL 中,客戶端傳送資料給服務端、服務端傳送執行結果給客戶端,都是以資料包(packet)為單元傳送的。

每個資料包,都由包頭、包體兩部分組成,包頭由 3 位元組的包體長度、1 位元組的包編號組成。3 位元組最多能夠表示 2 ^ 24 = 16777216 位元組(16 M),也就是說,一個資料包的包體長度必須小於等於 16M。

如果要傳送超過 16M 的資料怎麼辦?

分而治之在解決各種問題的時候經常會用到,MySQL 就是用這種思路解決資料包超過 16M 的問題的。

當要傳送大於 16M 的資料時,會把資料拆分成多個 16M 的資料包,除最後一個資料包之外,其它資料包大小都為 16M。

  1. 連線緩衝區

連線緩衝區,有 2 個應用場景:

  • 從客戶端接收資料,把資料暫存到連線緩衝區。

  • 傳送執行結果給客戶端,可能會先把執行結果暫存到連線緩衝區,待緩衝區滿再一次性傳送。

後面會解釋為什麼是可能,而不是一定把執行結果暫存到連線緩衝區。

雖然接收、傳送兩個場景都使用了連線緩衝區,並且是共用同一個連線緩衝區,但它們之間還是有一些區別,請繼續往下看。

2.1 從客戶端接收資料

每次服務端接收客戶端發來的資料,寫入資料到連線緩衝區之前,都會判斷連線緩衝區中的剩餘空間,是否足夠寫入新資料。

如果剩餘空間不夠寫入新資料,會重新分配更大的記憶體空間,這就是前面提到的連線緩衝區可以自動增長

重新分配的記憶體空間大小是寫入新資料之後的資料總長度,並且以 4096 位元組對齊(也就是 4096 的整數倍)。

舉個例子: 假設寫入新資料之前,連線緩衝區的大小為 4096 位元組,緩衝區中已經有 1688 位元組資料,而即將要寫入的新資料為 5000 位元組,寫入新資料後資料總長度為 1688(已有資料)+ 5000(新資料) = 6688 位元組,連線緩衝區空間不夠,需要分配更大的空間,因為要按 4096 位元組對齊,所以新分配的空間大小為 8192 位元組。

一般情況下,客戶端傳送資料給 MySQL 服務端,服務端把資料暫存到連線緩衝區,就可以進行後續的操作了。

就這麼簡單嗎?是的,一般情況下,連線緩衝區的使用就是這麼簡單,但凡事都可能有例外,連線緩衝區也不能免俗,當客戶端傳送的資料長度比較小的時候,這就是個簡單的你發我收的過程(頂多是空間不夠了,就加大點),而當客戶端傳送的資料很長的時候(如 insert 批次插入、update … case … when 批次更新),這個過程就會複雜一些了。

SQL 語句很長的時候,不也就是發和收嗎,為什麼就會更復雜?

這就像我們寫程式碼的時候一樣,如果不考慮高併發,寫起來很簡單,一旦要考慮高併發,就需要快取、分散式、微服務等各路神仙通通上馬了。

服務端讀取客戶端發來的資料包的包頭資訊時,如果發現包體長度等於 16M,它就知道本次接收的資料由多個資料包組成,會先把當前資料包的內容寫入連線緩衝區,然後接著讀取下一個資料包,並把下一個資料包的內容追加到連線緩衝區,直到讀到結束資料包,就接收到客戶端傳送的完整資料了。

2.2 傳送執行結果給客戶端

服務端傳送執行結果給客戶端時,會有 2 種方式:

  • 如果執行結果資料大於連線緩衝區大小,資料不會寫入連線緩衝區,而是直接傳送給客戶端。
    這裡的連線緩衝區,就是服務端接收客戶端發來的資料時使用的那個連線緩衝區。

  • 如果執行結果資料小於等於連線緩衝區大小,會先寫入連線緩衝區,等連線緩衝區滿之後傳送給客戶端。

實際上原始碼中,處理的邏輯要更復雜一些,我這裡捨棄了小部分細枝末節,不然就變成了描述程式碼流程,那看起來就很費勁了。

為什麼執行結果資料大於**連線緩衝區**大小時就不使用連線緩衝區了?

這就要說到連線緩衝區的作用了,連線緩衝區本來就是為了把多個小的資料包(packet)攢起來一起傳送,如果執行結果資料超過了連線緩衝區的大小,那就不需要攢著一起發了,服務端直接把資料包傳送給客戶端,還能節省複製資料到連線緩衝區的時間。

這就解釋了前面所說的,執行結果資料是可能,而不是一定會暫存到連線緩衝區。

還有一點需要說明的是,如果執行結果資料超過 16M,也一樣會分為多個資料包傳送,客戶端接收資料時,會把多個資料包合併起來,得到完整的執行結果資料。

  1. 結果集衝區

為什麼有了連線緩衝區之後,還要再弄個結果集緩衝區呢?

server 層從儲存引擎讀取到一條符合 where 條件的記錄之後,一條記錄需要作為一個整體傳送給客戶端,這就需要個臨時空間把各欄位內容攢到一起,這個臨時空間就是結果集緩衝區

結果集緩衝區的初始值,也是由 net_buffer_length 控制的,預設為 16K,當結果集緩衝區剩餘空間不夠容納新的資料時,會重新分配更大的記憶體空間,重新分配空間時,是按 8 位元組對齊的,例如:如果需要 7509 位元組來儲存資料,會分配 7512 位元組的空間。

欄位內容寫入結果集緩衝區的過程是這樣的: 遍歷要傳送給客戶端的欄位列表,讀取欄位內容到結果集緩衝區,如果緩衝區剩餘空間不夠儲存即將要寫入的欄位內容,重新分配更大的記憶體空間,然後把欄位內容追加到緩衝區,並更緩衝區內容總長度。

當一條記錄中所有欄位的內容都已經寫入結果集緩衝區之後,組裝也就完成了,然後把結果集緩衝區中的資料寫入連線緩衝區或者直接傳送給客戶端。

  1. net_buffer_length

net_buffer_length 初始值為 16384 位元組(16K),最小可設定為 1024 位元組(1K)最大可設定為 1048576 位元組(1M),並且必須小於等於 max_allowed_packet。

官方文件中描述 net_buffer_length 時,有個不起眼的小東西:Block Size,它的值為 1024,表示 net_buffer_length 必須是 1024 的整數倍,並且是向下取整數倍的,它的計算邏輯為:(net_buffer_length / 1024) * 1024。

舉例說明: 假設 my.cnf 中配置 net_buffer_length = 2047,那麼計算邏輯為:(2047 / 1024) * 1024 = 1024,因為在 c 語言中,兩個整數相除得到的結果也是整數,並且是直接捨棄小數,而不是四捨五入,所以,2047 / 1024 = 1。

  1. max_allowed_packet

max_allowed_packet 初始值為 4194304 位元組(4M),最小值為 1024 位元組(1K),最大值為 1073741824(1G)。

max_allowed_packet 既會限制客戶端傳送給服務端的資料大小,也會限制服務端傳送給客戶端的資料大小,max_allowed_packet 最大值只能設定為 1G,因此,客戶端和服務端之間,不管誰給誰傳送資料,一次傳送的資料最多隻能有 1G,這是個硬限制。

官方文件中描述 max_allowed_packet 時,也有一個 Block size,它的值也是 1024,和 net_buffer_length 的計算邏輯是一樣的。

  1. 總結

本文主要介紹了 MySQL net_buffer_length 背後的兩個緩衝區:連線緩衝區、結果集緩衝區,並介紹了這兩個緩衝區的自動增長邏輯,以及它們的上下限。還介紹了 MySQL 客戶端和服務端資料互動的基本單元是資料包(packet),以及超過 16M 的大資料怎麼傳送和接收。 最後介紹了 net_buffer_length、max_allowed_packet 兩個系統變數的配置,以及一個不起眼的小東西 Block Size。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章