ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入 MySQL 网络通信:客户端如何与服务端建立连接?

深入 MySQL 网络通信:客户端如何与服务端建立连接? 导言摘要本文通过 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 3306Unix 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半连接队列。然后服务器不会傻等客户端会回复SYNACK大概意思就是我收到了你的 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.] ← SYNACK我同意 22:34:55.736730 客户端 58537 服务器 3306: [.] ← ACK那我开始发数据length0 表明 tcp payload 0 字节、 Flags表示标志syn、ack等seq表示序列号win表明自己接受窗口大小2. MySQL 握手流程TCP 建立之后才进入MySQL协议。接下来mysqld会给客户端发送MySQL Initial Handshake Packet2.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_version1固定0x0A协议版本 10server_version变长NULL 结尾如8.0.38connection_id4线程 ID即SHOW PROCESSLIST里的 Idauth-plugin-data-part-18salt 前半随机数filler1固定0x00capability_flags低 16 位2服务器能力标志低位character_set1默认字符集编号status_flags2状态标志capability_flags高 16 位2能力标志高位auth_plugin_data_len1salt 总长度reserved10保留全 0auth-plugin-data-part-2变长salt 后半auth_plugin_name变长NUL 结尾认证插件名如caching_sha2_password22: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_QUERY0x16表示COM_STMT_PREPARE0x17表示COM_STMT_EXECUTE0x01表示COM_QUIT。SHOW PROCESSLIST中的Command则是服务器对当前连接所处命令/状态的展示不能简单等同于这些操作码。还有一点需要注意就是当payload超过16MB时MySQL 协议会自动拆分为多个包每个包的序号递增至于为什么是16MB那是 payload 长度是由3个字节决定的所以2的24 次方16777216约等于16MB五、caching_sha2_password的补充如果没走 SSL客户端没拿到 RSA 公钥服务器会发一个AuthMoreData包要求客户端拿公钥加密密码再发一次1. 为什么要换掉 mysql_native_passwordmysql_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 connectionsmax_user_connections 单个账号最多能同时开几条连接查看当前情况SHOW VARIABLES LIKE max_connections; SHOW STATUS LIKE Max_used_connections; -- 启动以来的峰值 SHOW STATUS LIKE Connection_errors_%; -- 各类连接错误计数还有最后一点是关于权限的东西要讲——关于权限权限是在认证时一次性从mysql.usermysql.db等表读入内存并附着在这条连接的线程上下文里。GRANT、REVOKE后已存在的连接仍然持有认证时获得的权限快照只有新连接或 KILL 后重连才会读取最新权限。FLUSH PRIVILEGES 只影响新建立的连接对现有连接无效。结尾以上就是客户端发起连接到MySQL服务端连接的全流程
返回列表