导言
摘要:本文通过 tcpdump 抓包,完整拆解了从执行
mysql命令到出现mysql>提示符的整个过程。内容涵盖 MySQL 的 TCP/IP 与 Unix Socket 两种通信方式、mysqld的启动监听、TCP 三次握手、MySQL 握手初始化包与认证包的字段解析、协议分包格式,以及caching_sha2_password认证插件的原理与线程模型,帮助读者从底层理解 MySQL 连接建立的每一步。
当你在终端执行
mysql -h <数据库ip> -P 3306 -uroot -p时,输入密码,从客户端敲下回车,到最终出现mysql>提示符,这中间发生了什么?
MySQL 是典型的客户端/服务端(CS)架构,因此本质还是客户端发请求 -> 服务器处理 -> 返回结果集,类似于浏览器访问网站的流程。 不同的是MySQL走的是一套私有二进制协议,不是HTTP协议,也不是纯文本)。
一、MySQL 通信传输方式
| 传输方式 | 说明 | 典型场景 |
|---|---|---|
| TCP/IP | 默认3306端口,跨主机访问 | mysql -h <MySQL主机ip> -P 3306 |
| Unix Socket | 本机通信通常比 TCP/IP 更省网络协议栈开销 | mysql -uroot -p(默认连本机 socket) |
二、mysqld 启动与监听
mysqld是 MySQL Server进程。启动之后,它需要告诉操作系统。
“如果有人想通过 TCP 连接 MySQL 进程监听的端口(默认3306),请把连接交给我。”
于是mysqld通过socket()系统调用让操作系统内核创建一个 Socket,然后通过bind()将其绑定到指定的监听地址和端口(默认通常为 3306),然后调用listen()将其设置为监听状态。这个 Socket 称为listening socket,它主要负责接收新的客户端连接请求,而不负责某个客户端建立连接后的实际数据传输。
三、MySQL 连接流程
整个流程可以用tcpdump抓包来窥见
#本机连远程 MySQL 时,抓本机发往 3306 端口的包 sudo tcpdump -i any -s 0 -A 'tcp port 3306' -w mysql_handshake.pcap# 另一窗口执行 mysql -h <mysql服务器ip> -P 3306 -u你的用户 -ptcpdump -nn -r mysql_handshake.pcap 用来读取抓包内容。 tcpdump -nn -X -r mysql_handshake.pcap 用于看 TCP Payload tcpdump -nn -A -r mysql_handshake.pcap 以 ASCII 形式查看 Payload1. TCP 三次握手
客户端执行:
mysql -h <mysql服务器ip> -P 3306 -uroot -p整个连接流程就开始了。
客户端发起连接后,TCP 三次握手实际由操作系统内核的 TCP/IP 协议栈负责处理。
当第一个 SYN 包到达时,内核会创建用于记录和管理该连接请求的request_sock对象(这里需要注意,request_sock并不是最终用于传输数据的 connected socket,而是内核在连接建立过程中用于管理连接请求的请求对象),这个对象主要用来保存客户端IP、客户端端口、服务器 IP、服务器端口、初始序列号、TCP Options等信息,可以简单理解成:
有人想和我建立 TCP 连接,我先把这个人的信息记下来
然后将其放入 SYN Queue(半连接队列)。然后服务器不会傻等客户端,会回复SYN+ACK,大概意思就是:
我收到了你的 SYN,我也同意建立连接,这是我的初始序列号
等客户端回复ACK后,这个ACK会触发内核把该连接从 SYN Queue(半连接队列)中移除,并创建对应的connected socket,然后将该连接放入 Accept Queue(已完成连接队列)。这里需要注意与负责接收新连接的listening socket不同,connected socket专门负责与某一个客户端进行双向数据传输,例如接收 MySQL 协议数据、SQL 请求,以及向客户端发送结果集、错误信息等。
当mysqld调用accept()时,内核会从 Accept Queue 中取出一个已经建立的连接,并为mysqld创建一个新的文件描述符FD,这个 FD 指向内核中对应的connected socket。
当客户端连接关闭并完成相关资源回收后,对应的connected socket和文件描述符等资源会被释放,而listening socket仍然保持监听状态,可以继续通过accept()接收新的客户端连接。TCP 连接建立之后,MySQL 协议才会在这个连接之上继续进行 Initial Handshake、身份认证以及后续的 SQL 通信。
三次握手是操作系统网络协议栈层要做的事,MySQL自己不管。客户端connect()时内核自动完成 TCP握手过程。
此外我们还可以通过tcpdump抓包看到tcp三次握手的完整过程。
22:34:55.736318 IP 192.168.255.10.58537 > 203.0.113.5.3306: Flags [S], seq 940091726, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 510555173 ecr 0,sackOK,eol], length 0 22:34:55.736637 IP 203.0.113.5.3306 > 192.168.255.10.58537: Flags [S.], seq 965481546, ack 940091727, win 65535, options [mss 16344,nop,nop,TS val 971023692 ecr 510555173,nop,wscale 5], length 0 22:34:55.736730 IP 192.168.255.10.58537 > 203.0.113.5.3306: Flags [.], ack 1, win 6380, options [nop,nop,TS val 510555173 ecr 971023692], length 022:34:55.736318 客户端 58537 > 服务器 3306: [S] ← SYN,我要连你 22:34:55.736637 服务器 3306 > 客户端 58537: [S.] ← SYN+ACK,我同意 22:34:55.736730 客户端 58537 > 服务器 3306: [.] ← ACK,那我开始发数据length=0 表明 tcp payload = 0 字节、 Flags表示标志(syn、ack等),seq表示序列号,win表明自己接受窗口大小
2. MySQL 握手流程
TCP 建立之后,才进入MySQL协议。接下来mysqld会给客户端发送MySQL Initial Handshake Packet
2.1 服务器 -> 客户端:握手初始化包
首先我们通过tcpdump工具可以查看到底层的字节信息
服务器发来第一个数据包length 78,这是 MySQL 的握手初始化包
22:34:56.058254 IP 203.0.113.5.3306 > 192.168.255.10.58537: Flags [P.], seq 1:79, ack 1, win 32768, options [nop,nop,TS val 971024014 ecr 510555173], length 78 0x0000: 4500 0082 3e22 0000 ff06 bfda 0986 f43f E...>".........? 0x0010: c0a8 ff0a 0cea e4a9 398c 144b 3808 a94f ........9..K8..O 0x0020: 8018 8000 b565 0000 0101 080a 39e0 a68e .....e......9... 0x0030: 1e6e 7425 4a00 0000 0a32 362e 372e 3000 .nt%J....26.7.0. 0x0040: ca2a 0000 3201 586b 7c1d 0242 00ff ffff .*..2.Xk|..B.... 0x0050: 0200 ffdf 1500 0000 0000 0000 0000 000a ................ 0x0060: 2558 5112 5a5e 4a1a 4a70 3a00 6361 6368 %XQ.Z^J.Jp:.cach 0x0070: 696e 675f 7368 6132 5f70 6173 7377 6f72 ing_sha2_passwor 0x0080: 6400这是mysql向客户端打招呼的包,首先看4a 00 00这是MySQL包头:长度 0x4a 序号0,
后面紧跟74字节的payload,内容就是在告诉客户端 “这是我的协议版本、服务器版本和随机盐,插件认证请用caching_sha2_password认证”。
我们通过tcpdump解读MySQL Initial Handshake Packet,可以获得它的结构。
| 字段 | 长度 | 含义 |
|---|---|---|
| protocol_version | 1 | 固定0x0A(协议版本 10) |
| server_version | 变长(NULL 结尾) | 如8.0.38 |
| connection_id | 4 | 线程 ID,即SHOW PROCESSLIST里的 Id |
| auth-plugin-data-part-1 | 8 | salt 前半(随机数) |
| filler | 1 | 固定0x00 |
| capability_flags(低 16 位) | 2 | 服务器能力标志低位 |
| character_set | 1 | 默认字符集编号 |
| status_flags | 2 | 状态标志 |
| capability_flags(高 16 位) | 2 | 能力标志高位 |
| auth_plugin_data_len | 1 | salt 总长度 |
| reserved | 10 | 保留,全 0 |
| auth-plugin-data-part-2 | 变长 | salt 后半 |
| auth_plugin_name | 变长(NUL 结尾) | 认证插件名,如caching_sha2_password |
22:34:56.058767 IP 192.168.255.10.58537 > 203.0.113.5.3306: Flags [P.], seq 1:37, ack 79, win 6379, options [nop,nop,TS val 510555495 ecr 971024014], length 36 0x0000: 4500 0058 0000 4000 4006 7d27 c0a8 ff0a E..X..@.@.}'.... 0x0010: 0986 f43f e4a9 0cea 3808 a94f 398c 1499 ...?....8..O9... 0x0020: 8018 18eb 87cd 0000 0101 080a 1e6e 7567 .............nug 0x0030: 39e0 a68e 2000 0001 a5ae be19 0000 0040 9..............@ 0x0040: ff00 0000 0000 0000 0000 0000 0000 0000 ................ 0x0050: 0000 0000 0000 0000 ........然后比较有意思的就是 客户端并没有直接回复,而是发起36字节的SSLRequest,这是属于MySQL协议层,相当于客户端在说“我要用SSL加密”(可以看见包里带MySQL包头)。
之后进入TLS握手阶段,客户端发起ClientHello,这是纯TLS记录,里面包含客户端支持的加密套件编号。接着到了最值得注意的地方了,服务器一口气回了2227B的大包,里面包含了服务器选定的加密套件、服务器证书,以及通知客户端我要开始加密了。
2.2 客户端 -> 服务器: 认证包
紧接着,客户端回复,宣布我也切加密了,以及加密的应用数据,其实就是客户端回复MySQL的认证包HandshakeResponse41 ,也就是用户名、加密后的密码。但因为TLS已经建立,这些内容被加密了,所以在抓包里你看到的是一堆随机字节(56 29 71 6a 17 c2 95 …),完全看不到明文用户名和密码。
包里的字段如下
| 字段 | 含义 |
|---|---|
| capability_flags | 客户端能力标志(与服务器取交集) |
| max_packet_size | 客户端能收的最大包 |
| character_set | 客户端字符集 |
| username | 用户名 |
| auth_response | 密码加密结果(用 salt 算出的) |
| database | 要连接的默认库(可选) |
| auth_plugin_name | 客户端确认使用的插件 |
客户端获取到认证插件后,用它和salt进行计算得到auth_response,用于MySQL服务端加密计算后得到的结果进行比对,如果相等,说明验证正确
除此之外,capability_flags是比较重要的字段,它告诉服务端我支持哪些 MySQL 协议能力。而且之前初始化握手包,也有capability_flags,那么双方连接能够使用的能力就是二者的交集。主要是用于这样的场景:服务器可能支持很多新功能,但老客户端并不认识,所以服务器不能直接说:
“我所有功能都打开,你自己看着办。”
所以通过capability_flags协调双方都支持的功能。
2.3 服务器 -> 客户端:OK 包 或 ERR 包
接下来比较简单,就是服务器对于客户端身份认证的回复。
认证成功 ->OK包(首字节0x00),随后进入命令循环。
失败 ->ERR包(首字节0xFF),带错误码和消息。
四、MySQL 协议分包格式
刚才分析字节流就注意到MySQL协议包,每个包都是4 字节包头、payload:
- 前 3 字节:payload 长度(受
max_allowed_packet限制) - 第 4 字节:序号(sequence id,保证有序)
- 后面:命令类型 + 参数
在命令阶段,客户端发送的 MySQL Command Packet 的 Payload 第一个字节是命令类型,例如0x03表示COM_QUERY,0x16表示COM_STMT_PREPARE,0x17表示COM_STMT_EXECUTE,0x01表示COM_QUIT。SHOW PROCESSLIST中的Command则是服务器对当前连接所处命令/状态的展示,不能简单等同于这些操作码。
还有一点需要注意就是当payload超过16MB时,MySQL 协议会自动拆分为多个包,每个包的序号递增
至于为什么是16MB?那是 payload 长度是由3个字节决定的,所以2的24 次方=16777216,约等于16MB
五、caching_sha2_password的补充:
如果没走 SSL,客户端没拿到 RSA 公钥,服务器会发一个AuthMoreData包要求客户端拿公钥加密密码再发一次
1. 为什么要换掉 mysql_native_password
mysql_native_password用的是SHA1。SHA1 早在 2017 年就被 Google 和 CWI 用碰撞攻击实际攻破了(SHAttered 攻击),虽然那针对的是文件碰撞、不直接影响密码,但安全界共识是SHA1 该退役了。所以 8.0 全面换成SHA256系列,默认就是caching_sha2_password。
2. 服务器端到底存的是什么
看mysql.user表
SELECT user, host, plugin, authentication_string FROM mysql.user;authentication_string里存的不是明文,也不是单纯 SHA256(密码),而是:SHA256+盐+5000轮迭代
而caching_sha2_password的"caching"指的是另一个东西,Server 内存里维护一份auth cache,第一次完整认证(走 SSL 或 RSA)通过后,把(user, host, hash)三元组记下来;下次同一用户再连,客户端只需发scramble,服务端在内存缓存里直接比对,跳过昂贵的 RSA 交换。
六、连接管理与线程模型
TCP 通了,服务器接下来要解决的问题是:这条连接由谁来伺候?(认证还需要线程来帮忙处理)
MySQL的答案很朴素,每个客户端连接分配一个专属线程,从此这个线程就和这条连接绑定,直到连接断开。这就是经典的"一连接一线程"(thread-per-connection)模型,也是社区版MySQL的模型。连接断开后,线程退出或被线程缓存复用。
相关的重要命令有下面这些
SHOW VARIABLES LIKE 'thread_cache_size'; -- 缓存最多留多少个空闲线程 SHOW STATUS LIKE 'Threads_%'; -- Threads_cached 缓存里现在有几个空线程 -- Threads_connected 当前活跃连接数 -- Threads_created 自启动以来一共创建过多少线程(重点看这个) -- Threads_running 正在跑 SQL 的线程数如果Threads_created增长很快(比如每分钟涨几百),说明thread_cache_size偏小、连接复用没吃到红利,需要调大或者上层用连接池。
从客户端 SYN 到进入 MySQL 认证阶段,有三道容量闸门,任何一道爆了都会导致"连不上":
- 一个就是Accept Queue上限,队列满时新 SYN 会被丢弃
- max_connections,全服务器允许的最大并发连接,超过就报
Too many connections - max_user_connections 单个账号最多能同时开几条连接
查看当前情况:
SHOW VARIABLES LIKE 'max_connections'; SHOW STATUS LIKE 'Max_used_connections'; -- 启动以来的峰值 SHOW STATUS LIKE 'Connection_errors_%'; -- 各类连接错误计数还有最后一点是关于权限的东西要讲——关于权限
权限是在认证时一次性从mysql.user,mysql.db等表读入内存,并附着在这条连接的线程上下文里。
GRANT、REVOKE后,已存在的连接仍然持有认证时获得的权限快照,只有新连接或 KILL 后重连才会读取最新权限。FLUSH PRIVILEGES 只影响新建立的连接,对现有连接无效。
结尾
以上就是客户端发起连接到MySQL服务端连接的全流程