mysql协议重传,MySQL · 源码分析 · 网络通信模块浅析
MySQL 網(wǎng)絡(luò)通信淺析
MySQL的網(wǎng)絡(luò)通信協(xié)議主要包含以下幾個(gè)層次,從最上層的MySQL數(shù)據(jù)包協(xié)議層到最底層的socket傳輸:
| THD
| Protocol
| NET
| VIO
| SOCKET
本文主要掃一下相關(guān)的代碼,以下分析基于MySQL5.7。
創(chuàng)建會(huì)話
在MySQL5.7中對(duì)會(huì)話協(xié)議層的代碼進(jìn)行了大量的重構(gòu)以優(yōu)化性能,并使得代碼更加可讀。以下這幅圖大概展示了幾個(gè)相關(guān)的類關(guān)系(未包含諸如windows平臺(tái)的相關(guān)類)
創(chuàng)建用戶線程堆棧是從主線程開始的,監(jiān)聽客戶端請(qǐng)求并創(chuàng)建處理線程
mysqld_main
|-->connection_event_loop
|-->listen_for_connection_event
//根據(jù)不同的監(jiān)聽模式,去監(jiān)聽新請(qǐng)求, 當(dāng)獲取到一個(gè)新的監(jiān)聽請(qǐng)求時(shí),會(huì)創(chuàng)建一個(gè)Channel_info類,用來存儲(chǔ)用戶的socket信息
|-->Connection_handler_manager::process_new_connection
|-->Per_thread_connection_handler::add_connection
//我們通常用的one thread one connection對(duì)應(yīng)的類為Per_thread_connection_handler
|-->創(chuàng)建用戶線程,線程函數(shù)為handle_connection
在MySQL5.7里一個(gè)重大的優(yōu)化,如上所述,就是關(guān)于用戶會(huì)話的thd, net, vio等信息的初始化都不是在主線程進(jìn)行的,而是創(chuàng)建用戶線程后,由用戶線程自己來完成。通過這種方式,主線程可以更高效的接受新的連接請(qǐng)求,從而優(yōu)化了在短連接場(chǎng)景下的性能。見官方博客 及相應(yīng)的worklog
下面這幅圖摘自官方博客,大家感受下5.7相比之前版本的短連接性能優(yōu)化:
創(chuàng)建用戶會(huì)話的主要函數(shù)棧包括:
handle_connection //線程入口函數(shù)
|-->init_new_thd
|-->Channel_info_local_socket::create_thd
|-->Channel_info::create_thd
|-->create_and_init_vio
|-->Protocol_classic::init_net
|-->my_net_init
|-->vio_fastsend //設(shè)置socket選項(xiàng)
* 設(shè)置IP_TOS為IPTOS_THROUGHPUT
* 設(shè)置TCP_NODELAY
|-->Global_THD_manager::add_thd
// 加入到thd鏈表上
|-->thd_prepare_connection
|-->login_connection
|--> check_connection
//檢查鏈接,設(shè)置thd的鏈接信息,
|--> vio_keepalive // 設(shè)置SO_KEEPALIVE選項(xiàng)
|--> acl_authenticate // 權(quán)限認(rèn)證
|-->prepare_new_connection_state
//如果連接打開了CLIENT_COMPRESS,設(shè)置NET::compress為true。
//如果設(shè)置了init_connect,則在這里執(zhí)行對(duì)應(yīng)的SQL語句
/* 循環(huán)接受請(qǐng)求并處理(do_command) */
|-->Protocol_classic::get_command
|-->Protocol_classic::read_packet
|-->my_net_read// 讀取command包,這里的讀超時(shí)時(shí)間由wait_timeout決定
|-->close_connection
|-->THD::disconnect
|-->THD::shutdown_active_vio
|-->vio_shutdown/* 關(guān)閉socket */
NET/VIO
my_net_write
該函數(shù)用于將數(shù)據(jù)拷貝到NET緩沖區(qū),當(dāng)長度大于MAX_PACKET_LENGTH(即4MB-1字節(jié))會(huì)對(duì)Packet進(jìn)行拆分成多個(gè)packet。每個(gè)Packet的頭部都會(huì)留4個(gè)字節(jié),其中:1~3字節(jié),存儲(chǔ)該packet的長度,第4個(gè)字節(jié)存儲(chǔ)當(dāng)前的packet的序號(hào),每存儲(chǔ)一次后遞增net->pkt_nr。
每個(gè)Net對(duì)象有一個(gè)Buff(net->buff),即將發(fā)送的數(shù)據(jù)被拷貝到這個(gè)buffer中,當(dāng)Buffer滿時(shí)需要立刻發(fā)出到客戶端。如果Buffer足夠大,則只做memcpy操作。net->write_pos被更新到寫入結(jié)束的位置偏移量 (net_write_buff)
如果一次寫入的數(shù)據(jù)被拆分成多個(gè)Packet,那么net->pkt_nr也對(duì)應(yīng)的遞增多次. pkt_nr的作用是在客戶端解析時(shí),防止包發(fā)送亂序。
net_flush
實(shí)際上在my_net_write函數(shù)中,如果net->buff不夠用,已經(jīng)會(huì)做網(wǎng)絡(luò)寫了,net_flush最終保證所有在buff中的數(shù)據(jù)被寫到網(wǎng)絡(luò)
當(dāng)客戶端啟用壓縮協(xié)議時(shí),這里會(huì)有些不同的,會(huì)給packet頭部再加3個(gè)字節(jié)(COMP_HEADER_SIZE),被壓縮的數(shù)據(jù)不包含頭部的7個(gè)字節(jié):
[3bytes:Packet的長度]
[1bytes: pkt_nr]
[3bytes:壓縮后的長度]
[1bytes: compress_pkt_nr]
同樣的,每個(gè)壓縮包都會(huì)遞增net->compress_pkt_nr
net_write_raw_loop
當(dāng)packet準(zhǔn)備好發(fā)送后,調(diào)用函數(shù)net_write_raw_loop開始進(jìn)行數(shù)據(jù)發(fā)送
發(fā)送模式受vio->write_timeout影響(通過參數(shù)net_write_timeout控制);當(dāng)該參數(shù)被設(shè)置成大于等于0時(shí),使用非阻塞模式send數(shù)據(jù)包(MSG_DONTWAIT)
若網(wǎng)絡(luò)發(fā)送被中斷(EINTR),會(huì)去嘗試重傳
使用非阻塞模式send,每次并不保證數(shù)據(jù)全部發(fā)送完畢,因此需要循環(huán)的調(diào)用直到所有的數(shù)據(jù)都發(fā)送完畢
當(dāng)輸出緩沖區(qū)滿時(shí),獲得錯(cuò)誤碼EWOULDBLOCK/EAGAIN,則阻塞等待(vio_socket_io_wait),最大等待時(shí)間為net_write_timeout,超時(shí)則返回錯(cuò)誤
my_net_read
根據(jù)NET接口先讀取數(shù)據(jù)包(net_read_packet):
先讀取packet header,一個(gè)普通的packet header包含4個(gè)字節(jié),壓縮協(xié)議下則另外再加3個(gè)字節(jié),如上述(net_read_packet_header)。其中的pkt_nr會(huì)提取出來和本地的值相比較。在讀寫兩段維持的pkt_nr自增值保證了服務(wù)器和客戶端的通信以一種有序的方式進(jìn)行,并用于校驗(yàn)包的有序性。如果不一致,則說明網(wǎng)絡(luò)包發(fā)生了亂序。直接報(bào)錯(cuò)。如果一致,本地net->pkt_nr++
從packet header中提取剩下的packet長度,繼續(xù)從socket讀取
Vio
Vio在NET的更下一層,封裝了所有對(duì)socket的操作。根據(jù)不同的連接類型(TCP/IP, Socket, Name Pipe, SSL, SHARED MEMORY),相關(guān)函數(shù)指針在vio_init函數(shù)中定義,這里不展開描述
相關(guān)參數(shù)
connect_timeout: 在連接認(rèn)證階段的網(wǎng)絡(luò)交互超時(shí)時(shí)間(ref login_connection);
wait_timeout: 等待來自客戶端的新的command請(qǐng)求;
net_read_timeout: 一般情況下的SQL通常直接從command發(fā)過來,但拿到command后,在一條語句內(nèi)可能還需要和客戶端交互,這里會(huì)用到該timeout值,例如load data local infile語句;
net_write_timeout: 就是通過網(wǎng)絡(luò)發(fā)送數(shù)據(jù)的最大超時(shí)時(shí)間;
interactive_timeout: 當(dāng)客戶端打開選項(xiàng)CLIENT_INTERACTIVE時(shí),將當(dāng)前會(huì)話的NET的wait_timeout設(shè)置為該值;
結(jié)果集
MySQL有兩種常用的數(shù)據(jù)協(xié)議,一種是用于Prepared Statement,對(duì)應(yīng)類為Protocol_binary,另外一種是普通的協(xié)議,對(duì)應(yīng)類為Protocol_classic
我們以一個(gè)簡(jiǎn)單的表為例:
mysql> create table t1 (a int, b int);
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t1 values (1,1),(2,2);
Query OK, 2 rows affected (0.00 sec)
當(dāng)執(zhí)行最后一條select操作時(shí),這里使用的類為Protocol_classic
發(fā)送metadata
ref: Protocol_classic::start_result_metadata
將列的個(gè)數(shù)寫入Net緩沖區(qū)
ref: Protocol_classic::send_field_metadata
逐列的準(zhǔn)備元數(shù)據(jù)信息,包含:
| 3bytes 標(biāo)識(shí)符:def
| db_name
| table_name
| org_table_name
| col_name
| org_col_name
| 字符集編碼
| 列長度
| 列類型
| flags
| decimals(這里為0)
| 預(yù)留
| 預(yù)留
可以看到每個(gè)列的元數(shù)據(jù)都包含了非常多的信息,使用字符串存儲(chǔ),這也意味著對(duì)于一條簡(jiǎn)單的SQL,你的網(wǎng)絡(luò)傳輸?shù)膬?nèi)容可能大多數(shù)都是元數(shù)據(jù),即時(shí)你的客戶端可能并不需要引用到。
有多個(gè)列就寫多個(gè)packet到Net buffer (Protocol_classic::end_row)
ref: Protocol_classic::end_result_metadata
write_eof_packet函數(shù)會(huì)被調(diào)用,用于標(biāo)識(shí)元數(shù)據(jù)信息到此結(jié)束。此處共寫5個(gè)字節(jié)(不含packet header)
發(fā)送數(shù)據(jù)
ref: end_send --> Protocol_classic::end_row
如上例,發(fā)送兩行數(shù)據(jù)的packet包括
1
‘1’
1
‘1’
1
‘2’
1
‘2’
結(jié)束發(fā)送
ref: THD::send_statement_status -->net_send_eof --> write_eof_packet
發(fā)送結(jié)果結(jié)束標(biāo)記,其中包含了sql執(zhí)行過程中產(chǎn)生的warning個(gè)數(shù)
元數(shù)據(jù)開銷
從上述可以看到,結(jié)果集中有很大一部分的開銷是給元數(shù)據(jù)的,這意味著類似普通的pk查詢,元數(shù)據(jù)的開銷可能會(huì)非常昂貴。
以下貼下我之前測(cè)試過的一個(gè)例子,增加了幾個(gè)選項(xiàng)來控制發(fā)送的元數(shù)據(jù)量:
0/METADATA_FULL: return all metadata, default value.
1/METADATA_REAL_COLUMN: only column name;
2/METADATA_FAKE_COLUMN: fake column name ,use 1,2...N instead of real column name
3/METADATA_NULL_COLUMN: use NULL to express the metadata information
4/METADATA_IGNORE: ignore metadata information, just for test..
測(cè)試表:
CREATE TABLE `test_meta_impact` (
`abcdefg1` int(11) NOT NULL AUTO_INCREMENT,
`abcdefg2` int(11) DEFAULT NULL,
`abcdefg3` int(11) DEFAULT NULL,
`abcdefg4` int(11) DEFAULT NULL,
……
……
`abcdefg40` int(11) DEFAULT NULL,
PRIMARY KEY (`abcdefg1`)
) ENGINE=InnoDB AUTO_INCREMENT=229361 DEFAULT CHARSET=utf8
使用mysqlslap測(cè)試并發(fā)pk查詢
mysqlslap --no-defaults -uxx --create-schema=test -h$host -P $port --number-of-queries=1000000000 --concurrency=100 --query='SELECT * FROM test.test_meta_impact where abcdefg1 = 2'
測(cè)試結(jié)果
METADATA_FULL : 3.48w TPS, Net send 113M
METADATA_REAL_COLUMN: 7.2W TPS, Net send 111M
METADATA_FAKE_COLUMN: 9.2W TPS , Net send 116M
METADATA_NULL_COLUMN: 9.6w TPS , Net send 115M
METADATA_IGNORE: 13.8w TPS, Net send 30M
很顯然無論網(wǎng)絡(luò)流量還是TPS吞吐量,在這個(gè)人為構(gòu)造的極端場(chǎng)景下,元數(shù)據(jù)的開銷都非常的顯著。
總結(jié)
以上是生活随笔為你收集整理的mysql协议重传,MySQL · 源码分析 · 网络通信模块浅析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 很有朝气的阳光女生网名139个
- 下一篇: mysql不同版本会覆盖吗,[mysql