MYSQL CLENT/SERVER數據包傳輸及net packet buffer作用解析
原創:轉載請說明出處
水平有限再加上源碼的復雜性,難免出現錯誤,請共同研究予以糾正
本文參考源碼:
Net_serv.cc(主要參考)
Mysql.h.pp
Mysql_socket.h
Violite.h
Viosocket.c
Vio.c
參考書籍:
深入理解MYSQL核心技術
MYSQL核心內幕
internals-en
MYSQL官方手冊
LINUX系統編程手冊
注意:
1、本文將主要解析非壓縮MYSQL NET包,而盡量不考慮壓縮的MYSQL NET包來減小難度
2、本文主要以TCP->IP->以太網為藍本進行描述,不考慮其他協議如(UDP)
3、本文主要以Net_serv.cc的my_net_write來描述寫入socket階段,而沒有考慮net_write_command
實際上net_write_command函數是client傳遞命令包的主要函數入口,調用的下層函數一致
4、寫入階段可以達到net buffer滿寫入也可以調用net_flush()寫入,但是這里無力研究net_flush()只研究滿寫入的情況
一、基本概念
在這之前我們必須明白如下的一些基本概念,否則不利于閱讀
1、socket:是一種進程間通信的方式,可以用于多態計算機和本地兩個進程進行通信,類似管道是雙向
通信的一種方式,在網絡上主要通過綁定IP和端口和識別唯一的網絡服務端,在本地通過綁
定一個本地文件進行通信,它工作在LINUX 內核態。
2、通信協議:協議也就是客戶端和服務端事先商量好的一種格式,如果格式不對則解包失敗,比如TCP
協議格式如下,MYSQL有自己的通信協議。
3、MYSQL協議:MYSQL作為大型數據庫系統,他有著自己的協議,至少包含如下一些數據包。
1、握手階段
--服務端到客戶端 初始化握手包
--客戶端到服務端 客戶端認證包
--服務端到客戶端 OK包、ERROR包
2、連接建立階段
--客戶端到服務端 命令(command)包
--服務端到客戶端 OK包、ERROR包、結果集包
其中結果集包包含:
1、包頭包
2、FILED屬性包
3、EOF包
4、行數據包
FILED屬性包:為列屬性每個列都會有一個
行數據包:為返回數據每行一個包
如果一個SELECT 返回 2行3列數據
會包含3(列)+2(行)+1(包頭包)+2(EOF包)個數據包
由于MYSQL數據包的復雜性本文并不準備解析MYSQL協議各種包,可以參考:
MYSQL核心內幕
internals-en
下圖是展示了MYSQL 服務端和客戶端之間如何握手成功,并且進行數據傳輸
我們約定它叫做
MYSQL數據包
4、MYSQL NET包:它是實際的傳輸包的大小,大小最大為16M-1,這是在源碼中定義死了的,每個MYSQL NET包
包含一個包頭,至少包含4個字節(非壓縮包,如果壓縮包會多3個字節),如下:
3 bytes:(壓縮后)payload長度
1 bytes:序號
(壓縮)3 bytes:壓縮前payload長度
其中payload就是實際數據
比如這樣一個MYSQL NET包:
為什么有一個序號呢?因為為了保證每個命令發送的包是有序的,比如一個結果
集合包會包含多個包,而其中的行數據包(SERVER->CLIENT的時候每一行數據是一個MYSQL數據包)
包很可能大于16M-1,那么我們就需要將整個結果集包分為多個MYSQL NET包進行傳輸,當到達
client的時候保證他順序。當然并不是每個MYSQL NET包都很大,比如一些MYSQL數據包如OK包,就很
小,我們知道在以太網傳輸的最大幀為MTU 1500字節,那么可能出現一個以太網幀包含多個MYSQL NET
包(如OK包),也可能一個MYSQL NET包在多個以太網幀中,同時可能出現一個MYSQL數據包在多個MYSQL
NET包中,但是最小一個MYSQL NET包至少包含一個MYSQL數據包(如OK包),當然TCP
注意當一個MYSQL數據包分為多個MYSQL NET包的時候其最后會緊跟一個長度為0作為結束的標識,源碼中
/* End of big multi-packet. */
if (!pkt_len)
goto end;
我們約定它叫做
MYSQL NET包
5、NET結構體說明
下面先來看幾個截圖說明:
可以看到NET結構中封裝了一個BUFFER,而這個BUFFER正是由參數net-buffer-length控制
其大小不能超過參數max-allowed-packet大小的這個buffer,本文約定將
它叫做net buffer。
net-buffer-length 默認16K最大為1M
max-allowed-packet 默認4M最大1G
結構體還封裝了2個unsigned int的變量write_timeout,read_timeout. 他們正是
net-wirte-timeout,net-read-timeout參數指定,用來表示在返回一個ETIMEDOUT錯誤前能夠
KEEPLIVE最大的時間。
設置超時的底層調用很有可能是
ret= mysql_socket_setsockopt(vio->mysql_socket, SOL_SOCKET, optname,optval, sizeof(timeout));
之類的調用
另外結構體還封裝了retry_count這是在遇到EINTR錯誤的時候重試的次數由參數net-retry-count
控制,在后面將會講述
6、LINUX ETIMEDOUT、EINTR、EWOULDBLOCK、EAGAIN
#define ETIMEDOUT 110 /* Connection timed out */
#define EINTR 4 /* Interrupted system call */
#define EAGAIN 11 /* Try again */
#define EWOULDBLOCK EAGAIN /* Operation would block *
7、LINUX平臺下MYSQL讀取和寫入scoket函數
位于Mysql_socket.h中
send(mysql_socket.fd, buf, IF_WIN((int),) n, flags);
recv(mysql_socket.fd, buf, IF_WIN((int),) n, flags);
當然如果是WIN_32平臺send和recv函數有底層封裝
8、包封裝流程
如下圖:
本文研究是應用層MYSQL通過自己的協議進行數據包封裝后如何進行傳輸的
二、MYSQL數據包的寫入scoket階段
1、將可能大的MYSQL數據包進行拆分
函數原型
my_bool my_net_write(NET *net, const uchar *packet, size_t len)
net:NET結構體指針
packet:MYSQL數據包指針,MYSQL數據包由MYSQL協議棧準備好
len:MYSQL數據包長度
這個過程會將大的MYSQL數據包進行拆分打包為多個MYSQL NET包,如果是小的MYSQL數據包(如OK包)就進行
打包為MYSQL NET包調用net_write_buff下面我將我寫的中文注釋加上源碼部分一同放出如下:
-
my_bool my_net_write(NET *net, const uchar *packet, size_t len) //將長度為packet的數據寫入到net->buffer
-
{
-
uchar buff[NET_HEADER_SIZE]; // lenth 3 seq 1 4bytes
-
int rc;
-
-
if (unlikely(!net->vio)) /* nowhere to write */
-
return 0;
-
-
MYSQL_NET_WRITE_START(len);
-
-
DBUG_EXECUTE_IF("simulate_net_write_failure", {
-
my_error(ER_NET_ERROR_ON_WRITE, MYF(0));
-
return 1;
-
};
-
);
-
-
/*
-
Big packets are handled by splitting them in packets of MAX_PACKET_LENGTH
-
length. The last packet is always a packet that is < MAX_PACKET_LENGTH.
-
(The last packet may even have a length of 0)
-
*/
-
while (len >= MAX_PACKET_LENGTH) //如果寫入MYSQL 協議包的長度大于了最大mysq NET包 就分為多個MYSQL NET包
-
{
-
const ulong z_size = MAX_PACKET_LENGTH; // 16M-1 計為包的長度
-
int3store(buff, z_size); //將長度寫入到棧 buff中
-
buff[3]= (uchar) net->pkt_nr++; //將buffer中的 seq+1 當然 pkt_nr 序列也+1
-
if (net_write_buff(net, buff, NET_HEADER_SIZE) || //寫入MYSQL NET包頭部
-
net_write_buff(net, packet, z_size)) //將長度為z_size的進行拆分的MYSQL 協議包一分部寫入到net buffer中
-
{
-
MYSQL_NET_WRITE_DONE(1);
-
return 1;
-
}
-
packet += z_size; //將packet的指針 加上z_size的大小 其實也就是16M-1
-
len-= z_size; //當然len 也就相應的減少z_size 其實也就是16M-1
-
}
-
//如果不是大的MYSQL 協議包或者是MYSQL協議包的最后一部分則執行下面代碼
-
/* Write last packet */
-
int3store(buff,len); //將最后的長度計入buffer 頭3字節
-
buff[3]= (uchar) net->pkt_nr++; //當然序號繼續+1
-
if (net_write_buff(net, buff, NET_HEADER_SIZE)) //寫入MYSQL NET包頭部
-
{
-
MYSQL_NET_WRITE_DONE(1);
-
return 1;
-
}
-
#ifndef DEBUG_DATA_PACKETS
-
DBUG_DUMP("packet_header", buff, NET_HEADER_SIZE);
-
#endif
-
rc= MY_TEST(net_write_buff(net,packet,len));//寫入 MYSQL 協議包 的最后數據寫入到net buffer中
-
MYSQL_NET_WRITE_DONE(rc);
-
return rc;
-
}
2、寫入緩存階段
函數原型
static my_bool net_write_buff(NET *net, const uchar *packet, ulong len)
net:NET結構體指針
packet:MYSQL數據包指針,注意這個指針和上面不同,由于my_net_write分包后這個指針
也會每次相應的增加到上次寫入后的位置
len:如果是拆分的大包就是16M-1,如果是小包(如OK包)就是其相應的長度,還可能是MYSQL NET包頭長度
這個過程分為如下情況:
--如果MYSQL NET包大于net buffer的剩余空間
--將MYSQL NET包一部分調用memcpy寫入到剩余空間,完成后調用net_write_packet來進行一次傳輸,清空net buffer
--如果MYSQL NET包的剩余部分任然大于net buffer(net-buffer-length)則直接調用net_write_packet進行傳輸
--如果MYSQL NET包能夠存儲在net buffer中
--直接調用memcpy寫入到net buffer即可
這里有幾個重點
one、MYSQL這樣處理實際上講大的MYSQL NET包和小的MYSQL NET進行區分開,使用net buffer來減小傳輸的次數,提高
性能
two、這里也揭示了寫入階段不會出現超過net buffer大小的情況,這和read不同,在寫入階段net buffer只是一個提高
性能的緩存,如果大于他可以直接調用net_write_packet寫入,而read階段不同net buffer還承載了另外一個重要
的功能將多個MYSQL NET包合并為一個MYSQL 數據包的功能,所以net buffer的大小小于一個MYSQL數據包的大小會
直接導致報錯如:Got a packet bigger than 'max_allowed_packet' bytes
three、net buffer的設置也就是net-buffer-length參數設置會直接影響到這里,同時這里并不會進行擴充到max_allowed_packet
的操作,擴充到max_allowed_packet是在read 階段才會出現,后面會描述
下面我將我寫的中文注釋加上源碼部分一同放出如下:
-
static my_bool
-
net_write_buff(NET *net, const uchar *packet, ulong len)
-
{
-
ulong left_length;
-
//下面計算buffer->max_packet的剩余空間
-
if (net->compress && net->max_packet > MAX_PACKET_LENGTH)
-
left_length= (ulong) (MAX_PACKET_LENGTH - (net->write_pos - net->buff));
-
else
-
left_length= (ulong) (net->buff_end - net->write_pos);
-
-
#ifdef DEBUG_DATA_PACKETS
-
DBUG_DUMP("data", packet, len);
-
#endif
-
if (len > left_length) //如果長度大于剩余空間
-
{
-
if (net->write_pos != net->buff)
-
{
-
/* Fill up already used packet and write it */
-
memcpy(net->write_pos, packet, left_length); //這里使用指針packet后left_lengeh長度來填滿整個net buffer
-
if (net_write_packet(net, net->buff,
-
(size_t) (net->write_pos - net->buff) + left_length))//寫滿后,然后調用net_write_packet寫到scoket
-
//(size_t) (net->write_pos - net->buff) + left_length 為整個buffer長度
-
return 1;
-
net->write_pos= net->buff; //這里wirte_pos指針 應該也是移動了到了wirte_pos+left_lengeh
-
packet+= left_length; //packet 指針增加
-
len-= left_length; //長度相應減少
-
}
-
if (net->compress)//壓縮屬性先不考慮,實際是壓縮開啟使用Zlib進行壓縮位于Zlib/compress中
-
{
-
..................
-
}
-
if (len > net->max_packet) //如果填滿 net->max_packet 后剩余的數據 還是大于整個net buffer 大小,則跳過緩沖區直接寫scoket (重要)
-
//實際上這里len 最大為16M-1, 如果為16M-1的MYSQL NET包始終會使用直接寫入的方法,這點
-
//和read階段不同,read階段會有一個合并mysql net包為MYSQL協議包過程,net buffer有著額外
-
//的使命
-
return net_write_packet(net, packet, len); //直接調用net_write_packet寫入
-
/* Send out rest of the blocks as full sized blocks */
-
}
-
memcpy(net->write_pos, packet, len); //如果長度小于 net buffer剩余的空間,只是寫入net buffer 即可
-
net->write_pos+= len; //這里wirte_pos指針也移動相應的長度
-
return 0;
-
}
3、進行壓縮階段
函數原型
my_bool net_write_packet(NET *net, const uchar *packet, size_t length)
return TRUE on error, FALSE on success.
net:NET結構體指針
packet:這里的packet有2個可能的來源
--來自net buffer
--原始的MYSQL 數據包指針偏移后的位置如16M-1的大MYSQL NET包
lenth:寫入長度
這一步實際上是進行一個壓縮功能,并沒有進行真正的傳輸,所以我們不進行過多的討論
下面我將我寫的中文注釋加上源碼部分一同放出如下
-
my_bool
-
net_write_packet(NET *net, const uchar *packet, size_t length) //函數并沒有真正傳輸只是做了一層封裝將數據壓縮封裝在內
-
//注意這里的數據可能來自net->buffer 可能來自net_flush
-
{
-
my_bool res;
-
DBUG_ENTER("net_write_packet");
-
-
#if defined(MYSQL_SERVER) && defined(USE_QUERY_CACHE)
-
query_cache_insert((char*) packet, length, net->pkt_nr);
-
#endif
-
-
/* Socket can't be used */
-
if (net->error == 2)
-
DBUG_RETURN(TRUE);
-
-
net->reading_or_writing= 2; //設置標示表示開始寫入
-
-
#ifdef HAVE_COMPRESS //參數是否開啟
-
const bool do_compress= net->compress;
-
if (do_compress) //MYSQL自己決定是否開啟壓縮
-
{
-
if ((packet= compress_packet(net, packet, &length)) == NULL) //壓縮數據 如果內存不足報錯
-
//{ "ER_OUT_OF_RESOURCES", 1041, "Out of memory; check if mysqld or some other process uses all available memory; if not, you may have to use \'ulimit\' to allow mysqld to use more memory or you can add more swap space" },
-
//壓縮完成后返回一個malloc的內存空間(壓縮后數據的內存首地址)給packet,這個時候packet已經不是形參的packet了,需要釋放
-
{
-
net->error= 2;
-
net->last_errno= ER_OUT_OF_RESOURCES;
-
/* In the server, allocation failure raises a error. */
-
net->reading_or_writing= 0;
-
DBUG_RETURN(TRUE);
-
}
-
}
-
#endif /* HAVE_COMPRESS */
-
-
#ifdef DEBUG_DATA_PACKETS
-
DBUG_DUMP("data", packet, length);
-
#endif
-
-
res= net_write_raw_loop(net, packet, length); //進行真正的底層傳輸工作
-
-
#ifdef HAVE_COMPRESS//參數是否開啟
-
if (do_compress)//mysql自己決定
-
my_free((void *) packet);//如前所述這里需要釋放壓縮后數據的內存避免泄露
-
#endif
-
-
net->reading_or_writing= 0;//關閉標示
-
-
DBUG_RETURN(res);
-
}
4、調用vio虛擬I/O接口進行寫入階段
函數原型
static my_bool net_write_raw_loop(NET *net, const uchar *buf, size_t count)
net:NET結構體指針
packet:這里的buffer有3個可能的來源
--來自net buffer
--原始的MYSQL 數據包指針偏移后的位置如16M-1的大MYSQL NET包
--經過壓縮后的上面兩種包
lenth:寫入長度
這個函數調用真正的底層vio_write虛擬IO接口函數進行寫入,同時如果遇到EINTR錯誤會進行如下的操作:
--線程安全客戶端如果是EINTR總是重試
--非線程安全客戶端或者
服務器端如果是EINTR并且達到net->retry_count就跳出循環
服務端MYSQLD肯定是線程安全的但是為了服務端的性能不可能在EINTR錯誤下面無限重試
非線程安全的客戶端可能全局區數據已經混亂造成I/O錯誤
此外如果數據沒有發送完成或者剩余了一部分會根據錯誤碼判斷拋錯
--ETIMEOUT錯誤,如果是則報錯Got timeout writing communication packets
--否則Got an error writing communication packets
注意這里的ETIMEOUT就是根據參數net-wirte-timeout設置的SOCKET超時設置
下面我將我寫的中文注釋加上源碼部分一同放出如下
-
static my_bool
-
net_write_raw_loop(NET *net, const uchar *buf, size_t count)
-
{
-
unsigned int retry_count= 0;
-
-
while (count)
-
{
-
size_t sentcnt= vio_write(net->vio, buf, count);//成功放回寫入字節數量 失敗返回-1 這里真正寫max_packet buffer包/mysql NET包>max_packet buffer的數據到socket
-
-
/* VIO_SOCKET_ERROR (-1) indicates an error. */
-
if (sentcnt == VIO_SOCKET_ERROR) //如果寫操作遇到錯誤下面是異常處理 總體來說就是暈倒的是EINTR就做重試,否則直接退出發送數據循環進入異常處理if語句
-
{
-
/* A recoverable I/O error occurred? */
-
if (net_should_retry(net, &retry_count))
-
//1、線程安全客戶端如果是EINTR總是重試
-
//2、非線程安全客戶端或者服務器端如果是EINTR并且達到net->retry_count就跳出循環
-
//服務端MYSQLD肯定是線程安全的但是為了服務端的性能不可能在EINTR錯誤下面無線重試
-
//非線程安全的客戶端可能全局區數據已經混亂造成I/O錯誤
-
continue;
-
else
-
break;
-
}
-
//下面是正常情況下
-
count-= sentcnt; //總數-發送的
-
buf+= sentcnt; //指針當然也就相應增加
-
update_statistics(thd_increment_bytes_sent(sentcnt));
-
}
-
-
/* On failure, propagate the error code. */
-
if (count) //如果count>0 也就是還有未發送的數據
-
{
-
/* Socket should be closed. */
-
net->error= 2;
-
-
/* Interrupted by a timeout? */
-
if (vio_was_timeout(net->vio)) //是否為ETIMEOUT錯誤,如果是則報錯Got timeout writing communication packets
-
net->last_errno= ER_NET_WRITE_INTERRUPTED;
-
else //否則報錯Got an error writing communication packets
-
net->last_errno= ER_NET_ERROR_ON_WRITE;
-
#ifdef MYSQL_SERVER
-
my_error(net->last_errno, MYF(0));
-
#endif
-
}
到這里MYSQL層次對MYSQL數據包到MYSQL NET包的轉換和傳輸準備已經完成接下來就是通過
底層TCP/IP、以太網等協議進行封裝然后通過socket傳輸了。下面一張圖對上面的說明
進行一個匯總,但是圖中有些細節并沒有表示出來還是最好通過源碼備注了解
三、MYSQL數據包的讀取scoket階段
1、合并多個MYSQL NET包為一個MYSQL 數據包
函數原型
ulong my_net_read(NET *net)
net:NET結構體指針,一個MYSQL 數據包存儲在一個NET結構體的buffer所指向的內存
空間中
返回值為讀取到的實際一個MYSQL 數據包的長度,不包MYSQL NET包的包頭字節數
這個函數調用net_read_packet來讀取一個MYSQL 數據包,如果為大的MYSQL 數據包完成解壓
合并操作源碼注釋中將大的MYSQL 數據包分為多個MYSQL NET包叫做packet of a multi-packet
下面我將我寫的中文注釋加上源碼部分一同放出如下,注意我忽略了解壓操作來降低學習的難度
-
ulong
-
my_net_read(NET *net) //
-
{
-
size_t len, complen;
-
-
MYSQL_NET_READ_START();
-
-
#ifdef HAVE_COMPRESS
-
if (!net->compress)//如果沒有壓縮
-
{
-
#endif
-
len= net_read_packet(net, &complen); //讀取一個MYSQL NET包返回實際長度在len變量中,如果有壓縮
-
//壓縮前長度保存在complen變量中 這個函數還有一個重要
-
//功能就是擴張max_packet buffer的長度直到max_packet_size
-
//限制,如果不能擴張就報錯,這里也指出了一個現實每個MYSQL
-
//協議包必須放到一個max_packet buffer中,這也是很多packet
-
//buffer 不夠報錯的根源
-
if (len == MAX_PACKET_LENGTH) //是否為一個滿包及大小為16M-1大小
-
{
-
/* First packet of a multi-packet. Concatenate the packets */
-
ulong save_pos = net->where_b;
-
size_t total_length= 0;
-
do //這里這個循環完成多個mysql net包合并為一個MYSQL 協議包的動作
-
{
-
net->where_b += len; //讀取偏移量不斷增加
-
total_length += len; //總長度不斷增加
-
len= net_read_packet(net, &complen); //讀取動作
-
} while (len == MAX_PACKET_LENGTH);
-
if (len != packet_error) //packet_err被定義為 ~((unsigned long)(0))
-
len+= total_length; //這里要注意MYSQL協議包分為多個mysql net包后結束包的長度是0,所以也不會增加len
-
net->where_b = save_pos;
-
}
-
net->read_pos = net->buff + net->where_b;
-
if (len != packet_error)
-
net->read_pos[len]=0; /* Safeguard for mysql_use_result */
-
MYSQL_NET_READ_DONE(0, len);
-
return len; //返回讀取的總長度
-
#ifdef HAVE_COMPRESS
-
}
-
else //不考慮壓縮
-
{.....
2、獲得MYSQL NET包長度階段
函數原型
static ulong net_read_packet(NET *net, size_t *complen)
net:NET結構體指針,一個MYSQL 數據包存儲在一個NET結構體的buffer所指向的內存
空間中
complen:為輸出形參,輸出的是可能的壓縮前的數據長度
返回值為實際讀取的MYSQL NET包的長度大小( Read the packet data (payload))
失敗返回packet_error
本函數主要是為了獲得MYSQL NET包的長度而封裝的一層函數,net_read_packet_header為獲得MYSQL
NET包長度函數,并且本函數計算多個MYSQL NET包為一個MYSQL 數據包后需要的內存空間是否夠用
如果不夠用分為如下操作
1、如果擴充后NET BUFFER的大小不會超過參數max_packet_size設置的大小,則調用net_realloc()擴充成功
2、如果擴充后NET BUFFER的大小超過參數max_packet_size設置的大小,則調用net_realloc擴充失敗報錯
{ "ER_NET_PACKET_TOO_LARGE", 1153, "Got a packet bigger than \'max_allowed_packet\' bytes" }
這也是非常常見的一個錯誤
當然如果內存不足都會引起如下錯誤
{ "ER_OUT_OF_RESOURCES", 1041, "Out of memory; check if mysqld or some other process uses all
available memory; if not, you may have to use \'ulimit\' to allow mysqld to use more memory
or you can add more swap space" }
這里不對net_realloc函數和net_read_packet_header函數進行詳細分析,如果有興趣自行研究
下面我將我寫的中文注釋加上源碼部分一同放出如下
-
static ulong net_read_packet(NET *net, size_t *complen)
-
{
-
size_t pkt_len, pkt_data_len;
-
-
*complen= 0;
-
-
net->reading_or_writing= 1; //將讀寫標示設置為1,表示讀取開始
-
-
/* Retrieve packet length and number. */
-
if (net_read_packet_header(net)) //讀取一個MYSQL net包的長度和MYSQL NET sequence
-
goto error;
-
-
net->compress_pkt_nr= net->pkt_nr;
-
-
#ifdef HAVE_COMPRESS
-
if (net->compress)//先不考慮壓縮
-
{
-
.......
-
}
-
#endif
-
-
/* The length of the packet that follows. */
-
pkt_len= uint3korr(net->buff+net->where_b);//獲得本MYSQL NET包的長度
-
-
/* End of big multi-packet. */
-
if (!pkt_len) //判斷是否為mysql數據包分包后的結束包
-
goto end;
-
-
pkt_data_len = max(pkt_len, *complen) + net->where_b; //獲得讀取此MYSQL NET包后需要的內存空間,也就是整個NET BUFFER需要多大,需要判斷如果是
-
//是經過壓縮的需要的空間是數據壓縮前的長度
-
-
/* Expand packet buffer if necessary. */
-
if ((pkt_data_len >= net->max_packet) && net_realloc(net, pkt_data_len)) //這里實際的判斷net buffer是否夠用,如果不夠用調用realloc進行內存擴充,
-
//在realloc中判斷是否超過max_packet_size的設置
-
goto error;
-
-
/* Read the packet data (payload). */
-
if (net_read_raw_loop(net, pkt_len)) //開始進行實際的讀取操作
-
goto error;
-
-
end:
-
net->reading_or_writing= 0; //將讀寫標示設置為0,表示讀取結束
-
return pkt_len; //函數返回本次讀取
-
-
error: //出錯返回值
-
net->reading_or_writing= 0;
-
return packet_error;
-
}
3、調用vio虛擬I/O接口進行讀取階段
函數原型
static my_bool net_read_raw_loop(NET *net, size_t count)
net:NET結構體指針,一個MYSQL 數據包存儲在一個NET結構體的buffer所指向的內存
空間中
count:本次讀取的MYSQL NET包有多大,如果是壓縮過的MYSQL NET包不是壓縮前的數據而是壓縮后的MYSQL NET包長度
(@return TRUE on error, FALSE on success.)
成功返回FALSE、失敗返回TURE
-
static my_bool net_read_raw_loop(NET *net, size_t count)
-
{
-
bool eof= false;
-
unsigned int retry_count= 0;
-
uchar *buf= net->buff + net->where_b;
-
-
while (count)
-
{
-
size_t recvcnt= vio_read(net->vio, buf, count); //如果寫操作遇到錯誤下面是異常處理 總體來說就是暈倒的是EINTR就做重試,否則直接退出發送數據循環進入異常處理if語句
-
-
/* VIO_SOCKET_ERROR (-1) indicates an error. */
-
if (recvcnt == VIO_SOCKET_ERROR) //
-
{
-
/* A recoverable I/O error occurred? */
-
if (net_should_retry(net, &retry_count))
-
//1、線程安全客戶端如果是EINTR總是重試
-
//2、非線程安全客戶端或者服務器端如果是EINTR并且達到net->retry_count就跳出循環
-
//服務端MYSQLD肯定是線程安全的但是為了服務端的性能不可能在EINTR錯誤下面無線重試
-
//非線程安全的客戶端可能全局區數據已經混亂造成I/O錯誤
-
-
continue;
-
else
-
break;
-
}
-
/* Zero indicates end of file. */
-
else if (!recvcnt) //recv半連接狀態? LINUX man recv:The return values will be 0 when the peer has performed an orderly shutdown
-
{
-
eof= true;
-
break;
-
}
-
-
count-= recvcnt;
-
buf+= recvcnt;
-
update_statistics(thd_increment_bytes_received(recvcnt));
-
}
-
-
/* On failure, propagate the error code. */
-
if (count)//如果count>0 也就是沒有讀取到預期的數據
-
{
-
/* Socket should be closed. */
-
net->error= 2;
-
-
/* Interrupted by a timeout? */
-
if (!eof && vio_was_timeout(net->vio)) //是否為ETIMEOUT錯誤,如果是則報錯Got timeout reading communication packets
-
net->last_errno= ER_NET_READ_INTERRUPTED;
-
else
-
net->last_errno= ER_NET_READ_ERROR;//否則報錯Got an error reading communication packets
-
-
#ifdef MYSQL_SERVER
-
my_error(net->last_errno, MYF(0));
-
#endif
-
}
-
-
return MY_TEST(count);
-
}
這個函數和前面寫階段的時候差不多,調用底層vio虛擬IO接口進行實際的讀取
也會出現如果數據沒有發送完成或者剩余了一部分會根據錯誤碼判斷拋錯
--ETIMEOUT錯誤,如果是則報錯Got timeout reading communication packets
--否則Got an error reading communication packets
注意這里的ETIMEOUT就是根據參數net-read-timeout設置的SOCKET超時設置
到這里MYSQL層次對從讀取到MYSQL NET包到MYSQL數據包的轉換合并過程就完成了,讀取工作
成接下來就是通過底層TCP/IP、以太網等協議進行封裝然后通過socket讀取了。下面一張圖對上面的說明
進行一個匯總,但是圖中有些細節并沒有表示出來還是最好通過源碼備注了解
四、使用TCPDUMP抓取MYSQL NET包解析實例
雖然本文不解析MYSQL 協議但是通過tcpdump抓包來進行一下簡單的客戶端和服務端的連接建立后的交互情況
使用命令
tcpdump tcp port 3307 -X >log.log
1、select 模型
客戶端:select * from test.test;(命令包)
服務端:返回查詢結果(結果集包)
id1 id2
1 1
2 2
3 3
4 4
5 5
6 6
7 7
1311 12:48:29.632228 IP bogon.61796 > testmy.opsession-prxy: Flags [P.], seq 53:82, ack 19791, win 16142, length 29
1312 0x0000: 4500 0045 0dbe 4000 4006 2f45 c0a8 be01 E..E..@.@./E....
1313 0x0010: c0a8 be5d f164 0ceb 097a 4b52 7e10 8b88 ...].d...zKR~...
1314 0x0020: 5018 3f0e 9de8 0000 1900 0000 0373 656c P.?..........sel
1315 0x0030: 6563 7420 2a20 6672 6f6d 2074 6573 742e ect.*.from.test.
1316 0x0040: 7465 7374 3b test;
1317 12:48:29.632651 IP testmy.opsession-prxy > bogon.61796: Flags [P.], seq 19791:19956, ack 82, win 131, length 165
1318 0x0000: 4500 00cd f754 4000 4006 4526 c0a8 be5d E....T@.@.E&...]
1319 0x0010: c0a8 be01 0ceb f164 7e10 8b88 097a 4b6f .......d~....zKo
1320 0x0020: 5018 0083 fe6f 0000 0100 0001 0226 0000 P....o.......&..
1321 0x0030: 0203 6465 6604 7465 7374 0474 6573 7404 ..def.test.test.
1322 0x0040: 7465 7374 0269 6402 6964 0c3f 000b 0000 test.id.id.?....
1323 0x0050: 0003 0350 0000 0028 0000 0303 6465 6604 ...P...(....def.
1324 0x0060: 7465 7374 0474 6573 7404 7465 7374 0369 test.test.test.i
1325 0x0070: 6432 0369 6432 0c3f 000b 0000 0003 0000 d2.id2.?........
1326 0x0080: 0000 0005 0000 04fe 0000 2200 0400 0005 ..........".....
1327 0x0090: 0131 0131 0400 0006 0132 0132 0400 0007 .1.1.....2.2....
1328 0x00a0: 0133 0133 0400 0008 0134 0134 0400 0009 .3.3.....4.4....
1329 0x00b0: 0135 0135 0400 000a 0136 0136 0400 000b .5.5.....6.6....
1330 0x00c0: 0137 0137 0500 000c fe00 0022 00 .7.7.......".
客戶端:
IP bogon.61796 > testmy.opsession-prxy 客戶端端口61796端口發送給3307端口的TCP包
1314 0x0020: 1900 0000 0373 656c .sel
1315 0x0030: 6563 7420 2a20 6672 6f6d 2074 6573 742e ect.*.from.test.
1316 0x0040: 7465 7374 3b test;
我們只看這一段
1900 00 MYSQL NET包長度小端顯示為0X19=25 當然數一數后面從0373開始一共25個字節
00 MYSQL NET序號
03 MYSQL 協議命令包的第一個自己0X03代表是COM_QUERY指令
后面的沒什么說的攜帶就是select * from test.test;的strings模式
服務端:
IP testmy.opsession-prxy > bogon.61796 服務端端端口3307端口發送給客戶端端口61796的TCP包
服務端我們將列屬性包的分析留給讀者這里從列屬性過后的EOF包開始
05 0000:小端顯示為0X05=05
04:這個MYSQL NET包在整個結果集包中的SEQ
fe:總是0XFE
0000:警告數量
2200:狀態標示
0400 00:小端顯示為0X05=04MYSQL NET長度
05:這個MYSQL NET包在整個結果集包中的SEQ
0131:01為返回結果集第一個列數據的長度 31就是實際數據1
0131:02為返回結果集第一個列數據的長度 31就是實際數據1
以此類推可以看到全部的結果,這里也展示這樣一個事實、因為一行記錄為一個結果集行包,那么
當這行數據很長而導致超過客戶端(如MYSQL MYSQLDUMP)max_packet_size大小的時候會報錯,這點
在源碼分析中已經分析這點也要非常注意
2、insert模型
客戶端:insert into test.test values(100,100),(101,102),(103,103),(104,104),
(105,105),(106,107),(108,109),(111,123);(命令包)
服務端:返回受影響的行數等(OK包)
[SQL] insert into test.test values(100,100),(101,102),(103,103),(104,104),
(105,105),(106,107),(108,109),(111,123);
受影響的行: 8
時間: 0.027s
13:07:39.121552 IP bogon.61796 > testmy.opsession-prxy: Flags [P.], seq 220:335, ack 39809, win 16140, length 115
0x0000: 4500 009b 100f 4000 4006 2c9e c0a8 be01 E.....@.@.,.....
0x0010: c0a8 be5d f164 0ceb 097a 4de5 7e11 54c6 ...].d...zM.~.T.
0x0020: 5018 3f0c 9890 0000 6f00 0000 0369 6e73 P.?.....o....ins
0x0030: 6572 7420 696e 746f 2074 6573 742e 7465 ert.into.test.te
0x0040: 7374 2076 616c 7565 7328 3130 302c 3130 st.values(100,10
0x0050: 3029 2c28 3130 312c 3130 3229 2c28 3130 0),(101,102),(10
0x0060: 332c 3130 3329 2c28 3130 342c 3130 3429 3,103),(104,104)
0x0070: 2c0d 0a28 3130 352c 3130 3529 2c28 3130 ,..(105,105),(10
0x0080: 362c 3130 3729 2c28 3130 382c 3130 3929 6,107),(108,109)
0x0090: 2c28 3131 312c 3132 3329 3b ,(111,123);
13:07:39.147808 IP testmy.opsession-prxy > bogon.61796: Flags [P.], seq 39809:39859, ack 335, win 140, length 50
0x0000: 4500 005a f77f 4000 4006 456e c0a8 be5d E..Z..@.@.En...]
0x0010: c0a8 be01 0ceb f164 7e11 54c6 097a 4e58 .......d~.T..zNX
0x0020: 5018 008c fdfc 0000 2e00 0001 0008 0002 P...............
0x0030: 0000 0026 5265 636f 7264 733a 2038 2020 ...&Records:.8..
0x0040: 4475 706c 6963 6174 6573 3a20 3020 2057 Duplicates:.0..W
0x0050: 6172 6e69 6e67 733a 2030 arnings:.0
客戶端:
bogon.61796 > testmy.opsession-prxy 客戶端端口61796端口發送給3307端口的TCP包
6f00 0000 0369 6e73從這里開始
6f00 00:MYSQL NET包長度小端顯示為0X6f=111 當然數一數后面從0369開始一共111個字節
00:MYSQL NET序號
03:MYSQL 協議命令包的第一個自己0X03代表是COM_QUERY指令 這里query不止代表SELECT
代表了INSERT\UPDATE\DELETE\SELECT
后面沒什么說的就是insert into test.test values(100,100),(101,102),(103,103),(104,104),
(105,105),(106,107),(108,109),(111,123);
的strings acsii編碼
這里也展示了這樣一個事實如果客戶端source導入語句的時候那么每一個INSERT語句是一個指令包
如果這個指令包大于了服務端max_packet_size的大小就會報錯、或者其他錯誤這點需要非常注意
服務端:
IP testmy.opsession-prxy > bogon.61796 服務端端端口3307端口發送給客戶端端口61796的TCP包
我們從2e00 0001 0008 0002開始解析
2e00 00 MYSQL NET包長度小端顯示為0X2e=46
01 MYSQL NET序號
00 總為0
08 影響行數
00 插入ID
02 00 服務器狀態
00 00 警告數量
最后就是消息的ASCII編碼沒什么好說的了
五、本文中提到的一些錯誤
1、{ "ER_OUT_OF_RESOURCES", 1041, "Out of memory; check if mysqld or some
other process uses all available memory; if not, you may have to use
\'ulimit\' to allow mysqld to use more memory or you can add more swap space" },
內存不足分配內存失敗
2、{ "ER_NET_PACKET_TOO_LARGE", 1153, "Got a packet bigger than \'max_allowed_packet\' bytes" }
讀取階段由于max_allowed_packet大小的限制net buffer不能進行擴充,一個MYSQL數據包必須存放到一個
net buffer中。
3、{ "ER_NET_WRITE_INTERRUPTED", 1161, "Got timeout writing communication packets" }
{ "ER_NET_READ_INTERRUPTED", 1159, "Got timeout reading communication packets" }
由于net-wirte-timeout,net-read-timeout參數指定,在返回ETIMEDOUT錯誤前保持連接活躍的
秒數
六、錯誤演示
MYSQLD服務端
MYSQL 客戶端程序官方文檔給出的
? max_allowed_packet
The maximum size of the buffer for client/server communication. The default is 16MB, the maximum is
1GB.
? net_buffer_length
The buffer size for TCP/IP and socket communication. (Default value is 16KB.)
源碼中定義 mysql.cc
{"max_allowed_packet", OPT_MAX_ALLOWED_PACKET,
"The maximum packet length to send to or receive from server.",
&opt_max_allowed_packet, &opt_max_allowed_packet, 0,
GET_ULONG, REQUIRED_ARG, 16 *1024L*1024L, 4096,
(longlong) 2*1024L*1024L*1024L, MALLOC_OVERHEAD, 1024, 0},
{"net_buffer_length", OPT_NET_BUFFER_LENGTH,
"The buffer size for TCP/IP and socket communication.",
&opt_net_buffer_length, &opt_net_buffer_length, 0, GET_ULONG,
REQUIRED_ARG, 16384, 1024, 512*1024*1024L, MALLOC_OVERHEAD, 1024, 0},
MYSQLDUMP 客戶端程序官方文檔給出的
? max_allowed_packet
The maximum size of the buffer for client/server communication. The default is 24MB, the maximum is
1GB.
? net_buffer_length
The initial size of the buffer for client/server communication. When creating multiple-row INSERT
statements (as with the --extended-insert or --opt option), mysqldump creates rows up
to net_buffer_length bytes long. If you increase this variable, ensure that the MySQL server
net_buffer_length system variable has a value at least this large.
源碼中定義mysqldump.c
{"max_allowed_packet", OPT_MAX_ALLOWED_PACKET,
"The maximum packet length to send to or receive from server.",
&opt_max_allowed_packet, &opt_max_allowed_packet, 0,
GET_ULONG, REQUIRED_ARG, 24*1024*1024, 4096,
(longlong) 2L*1024L*1024L*1024L, MALLOC_OVERHEAD, 1024, 0},
{"net_buffer_length", OPT_NET_BUFFER_LENGTH,
"The buffer size for TCP/IP and socket communication.",
&opt_net_buffer_length, &opt_net_buffer_length, 0,
GET_ULONG, REQUIRED_ARG, 1024*1024L-1025, 4096, 16*1024L*1024L,
MALLOC_OVERHEAD-1024, 1024, 0},
我們可以看不到不管是MYSQLDUMP還是MYSQL客戶端程序都有這樣兩個命令行參數,他的功能已經在上面源碼解析中
進行了說明,在MYSQLDUMP中net_buffer_length還有一個額外注意的地方,也就是當多個結果集行包進入NET BUFFER
后需要進行輸出,這里看起來他的輸出就是以NET BUFFER為單位的官方文檔也有說明,換句話說,當使用multiple-row
INSERT 方式的時候一條語句的長度由他控制,注意在導入的時候服務端的max_allowed_packet一定要大于這個值,因為
導入的時候一個INSERT語句就是客戶端到服務端的一個命令包,這個MYSQL服務端讀取這個數據命令包必須保存在一個
NET BUFFER中
我們來驗證一下MYSQLDUMP的這種說法
/mysqldata/mysql5.7/bin/mysqldump --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123 --net-buffer-length=4k test testpack2>log10.log
/mysqldata/mysql5.7/bin/mysqldump --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123 --net-buffer-length=16k test testpack2>log11.log
cat log10.log |grep INSERT |head -n 1 >test10.log
cat log11.log |grep INSERT |head -n 1 >test11.log
[root@testmy ~]# du -hs test1*
4.0K test10.log
16K test11.log
確實如我們期望了一個multiple-row 由于NET BUFFER的變動而改變了大小。
接下來我們來模擬這種服務端和客戶端一個mysql數據包大于max_allowed_packet報錯的情況
1、服務端報錯
使用
/mysqldata/mysql5.7/bin/mysqldump --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123 --net-buffer-length=5m test testpack2>log11.log
這樣會生成一個大約5m的命令包
然后在服務端進行source
默認的服務端max_allowed_packet為4M,net-buffer-length 為16k
mysql> show variables like '%max_allowed_packet%';
+--------------------------+------------+
| Variable_name | Value |
+--------------------------+------------+
| max_allowed_packet | 4194304 |
| slave_max_allowed_packet | 1073741824 |
+--------------------------+------------+
mysql> source /root/test11.log
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id: 63
Current database: test
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id: 64
Current database: test
ERROR 2006 (HY000): MySQL server has gone away
可以看到服務端報錯了
2017-05-07T07:14:15.957486Z 58 [Note] Aborted connection 58 to db: 'test' user: 'root' host: 'localhost' (Got a packet bigger than 'max_allowed_packet' bytes)
2017-05-07T07:14:16.020153Z 63 [Note] Aborted connection 63 to db: 'test' user: 'root' host: 'localhost' (Got a packet bigger than 'max_allowed_packet' bytes)
2017-05-07T07:14:16.080146Z 64 [Note] Aborted connection 64 to db: 'test' user: 'root' host: 'localhost' (Got a packet bigger than 'max_allowed_packet' bytes)
原因在前面已經做了詳細的描述,我們來修改max_allowed_packet為6M
再次source
mysql> show variables like '%max_allowed_packet%';
+--------------------------+------------+
| Variable_name | Value |
+--------------------------+------------+
| max_allowed_packet | 5999616 |
| slave_max_allowed_packet | 1073741824 |
+--------------------------+------------+
mysql> use test
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> source /root/log11.log
Query OK, 349522 rows affected (3.49 sec)
Records: 349522 Duplicates: 0 Warnings: 0
Query OK, 174766 rows affected (1.77 sec)
Records: 174766 Duplicates: 0 Warnings: 0
2、客戶端MYSQL
為了方便測試我構造可一行2M左右數據的行,我們知道一行數據就是一個MYSQL數據包
這里模擬了另外一個錯誤
mysql> insert into testpack2 values(100,repeat('a',7000000));
ERROR 1301 (HY000): Result of repeat() was larger than max_allowed_packet (5999616) - truncated
報錯明顯大于了我們服務端設置的max_allowed_packet (5999616),而這個命令行包雖然使用了repeat但是repeat
的個數超過了服務端的max_allowed_packet設置,導致報錯.
我們改為2M左右
mysql> insert into testpack2 values(100,repeat('a',2000000));
Query OK, 1 row affected (0.06 sec)
沒有問題
[root@testmy data]# /mysqldata/mysql5.7/bin/mysql --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123 --max-allowed-packet=1m -e 'select * from test.testpack2 where id=100' >log.log
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2020 (HY000) at line 1: Got packet bigger than 'max_allowed_packet' bytes
我們看到了預期的報錯
修改--max-allowed-packet為2M
[root@testmy data]# /mysqldata/mysql5.7/bin/mysql --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123 --max-allowed-packet=2m -e 'select * from test.testpack2 where id=100' >log.log
mysql: [Warning] Using a password on the command line interface can be insecure.
[root@testmy data]#
報錯消失
3、客戶端MYSQLDUMP
沿用上面的數據,這里出現一樣的結果 不需要過多描述了
[root@testmy data]# /mysqldata/mysql5.7/bin/mysqldump --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123 --max-allowed-packet=1m test testpack2>log2.log
mysqldump: [Warning] Using a password on the command line interface can be insecure.
Warning: A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database. If you don't want to restore GTIDs, pass --set-gtid-purged=OFF. To make a complete dump, pass --all-databases --triggers --routines --events.
mysqldump: Error 2020: Got packet bigger than 'max_allowed_packet' bytes when dumping table `testpack2` at row: 524288
[root@testmy data]# ^C
[root@testmy data]# /mysqldata/mysql5.7/bin/mysqldump --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123 --max-allowed-packet=2m test testpack2>log2.log
mysqldump: [Warning] Using a password on the command line interface can be insecure.
Warning: A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database. If you don't want to restore GTIDs, pass --set-gtid-purged=OFF. To make a complete dump, pass --all-databases --triggers --routines --events.
[root@testmy data]#
這3個測試分別用來證明了在讀取階段不管是客戶端還是服務端都需要將一個MYSQL數據包在NET BUFFER中進行合并,如果一個MYSQL數據包大于了--max-allowed-packet設置就會拋錯,而寫入階段當然不需要,源碼解析的時候已經做了詳細解析。
至此整個文章從預備知識到源碼解析到抓包解析到錯誤證明都進行了詳細的描述,耗費了我大約2天半的時間基本就是整個周末多一點,因為怕上班時間少有時間研究,所以加緊完成
其中肯定有一些不嚴謹或者錯誤的地方特別是源碼解析因為沒有過多的資料而且要了解設計者的思想特別困難,還有就是涉及到底層SOCKET通信的地方,因為沒有過多的去剖析所以
有的地方一筆帶過,如果日后進行詳細的分析會在以文章的方式給出。
作者微信: