From 25485a96b7a256281f3bcaa11f67d160021c5ab0 Mon Sep 17 00:00:00 2001 From: fengzhao Date: Thu, 31 Oct 2024 00:17:22 +0800 Subject: [PATCH] update sql docs --- ...23\345\214\205\346\246\202\350\277\260.md" | 130 +++++++----------- 1 file changed, 46 insertions(+), 84 deletions(-) diff --git "a/docs/foundmental/1.MySQL\345\215\217\350\256\256\345\210\206\346\236\220\344\270\216\346\212\223\345\214\205\346\246\202\350\277\260.md" "b/docs/foundmental/1.MySQL\345\215\217\350\256\256\345\210\206\346\236\220\344\270\216\346\212\223\345\214\205\346\246\202\350\277\260.md" index 3e28511d1..b58e3c5a7 100644 --- "a/docs/foundmental/1.MySQL\345\215\217\350\256\256\345\210\206\346\236\220\344\270\216\346\212\223\345\214\205\346\246\202\350\277\260.md" +++ "b/docs/foundmental/1.MySQL\345\215\217\350\256\256\345\210\206\346\236\220\344\270\216\346\212\223\345\214\205\346\246\202\350\277\260.md" @@ -1,10 +1,7 @@ -# MySQL协议分析 - - +# MySQL 协议分析 ### 背景 - 作为 DBA ,在日常运维工作中,常常会被开发同学问到类似下面的这些问题: - 我的这条数据不知道被谁改了,能帮我查一下吗? @@ -26,18 +23,16 @@ 就是前面提到的,通过慢日志、查询日志来记录。优缺点也很明显,优点就是记录的信息准确,开启简单;缺点就是会占用系统性能,同时带来风险,这种实现方式通常有几种方案: - General log:这种方案可以记录所有访问日志,但日志内容匮乏,只包括了 Query 语句,而缺少了访问 IP ,用户名等信息,并且如果线上都打开这个日志文件,对 MySQL 性能的冲击很大,容易产生故障,所以很难实现我们的需求。 - + - Slow log:将 long_query_time 参数设置为 0 ,即可记录所有 MySQL Server 端执行的 SQL 语句,用户名、客户端 IP 、执行时间等信息均有记录,但是带来的问题与开启 General log 相同,生产环境不可能一直开启。 - - - Audit plugin:使用 MySQL 自带的审计功能,其实是可以实现部分需求的,但问题点在于 audit 本身是 MySQL 的一个插件,与版本关系比较大,功能不尽相同,而最大的一个问题是,如果开启这个插件功能,需要重启数据库,这是比较要命的,所以也不能被采用。 + - Audit plugin:使用 MySQL 自带的审计功能,其实是可以实现部分需求的,但问题点在于 audit 本身是 MySQL 的一个插件,与版本关系比较大,功能不尽相同,而最大的一个问题是,如果开启这个插件功能,需要重启数据库,这是比较要命的,所以也不能被采用。 - 从客户端记录 客户端发起请求和收到响应时,能够记录自己发出的 SQL 请求,结果集以及响应时间,但我们知道,客户端众多,汇总起来是一个问题,也不能保证每台机器执行 SQL 的时序性。更不能体现 Server 端的真实执行时间,网络开销、客户端程序处理耗时等因素没法排除。 - -- MySQL中间层记录 +- MySQL 中间层记录 通过中间层日志追加的方式,记录所有对于底层数据库的请求;但这种方式受限于中间层的特性,并且 MySQL 和中间层的关系往往是多对多,也存在一个汇总的问题,同时也会增加额外开销。更加重要的是,在我们使用的 MySQL 架构中,没有中间层这个模块,所以这种方案就已经被排除在外。 @@ -45,17 +40,11 @@ 这是一种常见的 SQL 审计手段,对服务是非入侵式的,通过网络抓包来获取对于数据库的全量访问日志。这种方式既不会对服务造成任何影响,同时也可以控制对于服务器资源的占用,准确性也有一定保障 - - ### 概述 - - - 本文不讨论语言层, 主要阐述 `MySQL Client/Server` 的通信协议, 了解了整个通信协议之后, 无论是针对什么编程语言, 哪怕是自己开发的新编程语言, 也都可以自行实现一个 MySQL 驱动, 基于 MySQL 驱动可以在上层做更多自定义的事情。例如实现 MySQL 数据到 Redis、ElasticSearch、MongoDB 等的数据同步、开发 MySQL Proxy 等。 - -MySQL客户端与服务器的交互主要分为两个阶段:握手认证阶段和命令执行阶段。 +MySQL 客户端与服务器的交互主要分为两个阶段:握手认证阶段和命令执行阶段。 握手认证阶段 @@ -71,69 +60,59 @@ MySQL客户端与服务器的交互主要分为两个阶段:握手认证阶段 客户端 -> 服务器:执行命令消息 服务器 -> 客户端:命令执行结果 +MySQL 通信协议是一个有状态的协议,主要用于 MySQL 客户端与服务器之间的通信。这个协议在 MySQL 客户端连接器(如 Connector/C、Connector/J 等)、MySQL Proxy 以及主从复制中都有实现。 -MySQL通信协议是一个有状态的协议,主要用于MySQL客户端与服务器之间的通信。这个协议在MySQL客户端连接器(如Connector/C、Connector/J等)、MySQL Proxy以及主从复制中都有实现。 +该协议的特性包括:支持 SSL、压缩和认证等功能。 -该协议的特性包括:支持SSL、压缩和认证等功能。 +MySQL 客户端和服务端的交互过程主要分为两个阶段:握手认证阶段和命令执行阶段。在握手认证阶段,服务端会生成一些服务信息,包括协议号、server 版本、线程 id、加密随机数(seeds)和服务端能力 flag 等信息。客户端则会生成认证信息,包含客户端能力 flag、用户名、加密密码和连接数据库名字等信息。然后,服务端返回认证结果,如果验证正确,则返回 OK 包,否则是异常包。 -MySQL客户端和服务端的交互过程主要分为两个阶段:握手认证阶段和命令执行阶段。在握手认证阶段,服务端会生成一些服务信息,包括协议号、server版本、线程id、加密随机数(seeds)和服务端能力flag等信息。客户端则会生成认证信息,包含客户端能力flag、用户名、加密密码和连接数据库名字等信息。然后,服务端返回认证结果,如果验证正确,则返回OK包,否则是异常包。 +此外,MySQL 通信协议还支持多种进程间通信方式,例如管道、命名管道、共享内存、TCP/IP 套接字和 UNIX 域套接字等。在实际应用中,可以使用诸如 Wireshark 的工具来抓取并分析 MySQL 协议。 -此外,MySQL通信协议还支持多种进程间通信方式,例如管道、命名管道、共享内存、TCP/IP套接字和UNIX域套接字等。在实际应用中,可以使用诸如Wireshark的工具来抓取并分析MySQL协议。 - - - -在互联网七层协议中,显然MySQL是自己实现了协议,MySQL 连接的方式有多种, 比如 TCP/IPv4、TCP/IPv6、UNIX domain sockets 等, 我们以 TCP 连接为例来讨论连接建立过程: +在互联网七层协议中,显然 MySQL 是自己实现了协议,MySQL 连接的方式有多种, 比如 TCP/IPv4、TCP/IPv6、UNIX domain sockets 等, 我们以 TCP 连接为例来讨论连接建立过程: - 客户端与服务端建立 TCP 连接, 出于性能的考虑, 客户端和服务端之间一般建立 TCP 长连接, 这样可以避免每次客户端要与服务器通信都再进行一次连接建立操作, 客户端可以将建立好的 TCP 连接缓存在连接池中, 以便需要的时候可以直接拿来用。 - - 服务端向客户端返回 Initial HandShake Packet, 客户端解析包结构并返回 HandShake Response Packet, 数据包中携带能力交换信息, 要登录的用户等, 在这个阶段, 客户端也可以向服务端返回 SSL Connection Request Packet, 请求服务端开启 SSL 连接, 之后便是 SSL 的数据交换, 此过程完成以后, 客户端再向服务端返回 Handshake Response Packet。 - 客户端在向服务端返回 HandShake Response Packet 时会告知服务端此次要登录的用户, 在 MySQL 中, 系统表 mysql.user 表的 plugin 字段标志了该用户所对应的鉴权方法, 比如 mysql_native_password 等, 服务端根据客户端返回的用户信息查询该 user 表的 plugin 字段获取该用户对应的鉴权方式, 并与客户端进行鉴权信息交换, 若鉴权成功, 则服务端最终向客户端发送 OK_Packets, 此时连接建立完成, 或发生比如鉴权未通过等其它情况时向客户端返回 ERR_Packets +mysql 客户端和服务器之间的通信怎么实现?协议特征有哪些? - -mysql客户端和服务器之间的通信怎么实现?协议特征有哪些? - -- MySQL客户端和服务器之间的传输层通信是基于TCP协议实现的,应用层是由MySQL自己的应用协议MySQL协议。以下是MySQL协议的一些特征: +- MySQL 客户端和服务器之间的传输层通信是基于 TCP 协议实现的,应用层是由 MySQL 自己的应用协议 MySQL 协议。以下是 MySQL 协议的一些特征: - 基于请求-响应模型:通信过程中,客户端发送请求到服务器,服务器进行处理后返回响应给客户端。 -- 二进制协议:MySQL协议使用二进制格式进行传输,每个数据报文都包含了特定的字段结构以及相应的数据类型。 +- 二进制协议:MySQL 协议使用二进制格式进行传输,每个数据报文都包含了特定的字段结构以及相应的数据类型。 -- 数据报文格式:数据报文由固定长度的报头和可变长度的有效载荷(payload)组成。报头包含了协议版本、连接ID、数据长度等信息。 +- 数据报文格式:数据报文由固定长度的报头和可变长度的有效载荷(payload)组成。报头包含了协议版本、连接 ID、数据长度等信息。 -- 多线程支持:MySQL协议支持多个线程在同一个连接上并发执行多个查询。服务器会为每个线程分配一个线程ID。 +- 多线程支持:MySQL 协议支持多个线程在同一个连接上并发执行多个查询。服务器会为每个线程分配一个线程 ID。 -- 长连接:MySQL协议支持长连接,即客户端和服务器之间的连接可以保持开放状态,减少了连接和断开连接的开销。 - -- SQL语句支持:MySQL协议可以发送SQL语句(如查询语句、事务语句等)到服务器执行,并接收服务器返回的结果集。 - -- 错误处理:MySQL协议支持服务器返回错误信息,包括错误代码和错误描述,以便客户端处理异常情况。 - -- 安全连接:MySQL协议支持通过SSL/TLS进行通信加密,保护会话数据的安全性。 +- 长连接:MySQL 协议支持长连接,即客户端和服务器之间的连接可以保持开放状态,减少了连接和断开连接的开销。 +- SQL 语句支持:MySQL 协议可以发送 SQL 语句(如查询语句、事务语句等)到服务器执行,并接收服务器返回的结果集。 +- 错误处理:MySQL 协议支持服务器返回错误信息,包括错误代码和错误描述,以便客户端处理异常情况。 +- 安全连接:MySQL 协议支持通过 SSL/TLS 进行通信加密,保护会话数据的安全性。 ### 协议数据类型 - 协议数据类型是与语言无关的数据类型, 可以理解为 `wire type`, 它们是网络字节流中的数据类型。 在 MySQL 协议中数据类型只有两大类, 分别是整数(Integers) 和字符串(Strings), 这两种数据类型又会分为多个更小的数据类型, 对于整数类型, 可以进一步分为定长整数(Fixed-Length Integer Types) 和长度编码型整数(Length-Encoded Integer Type)。 -对于定长整数, 顾名思义其长度是固定的, 定长整数有 int<1>、int<2>、int<3>、int<4>、int<6>、int<8> 共 6 种, 定长整数都是以小端字节序存放的, 即最低有效位在前, 举例来说, 对于 int<4> 类型的数字 2, 其字节概况为 10 00 00 00; +对于定长整数, 顾名思义其长度是固定的, 定长整数有 int<1>、int<2>、int<3>、int<4>、int<6>、int<8> 共 6 种, 定长整数都是以小端字节序存放的, 即最低有效位在前, 举例来说, 对于 int<4> 类型的数字 2, 其字节概况为 10 00 00 00; 而长度编码型整数, 它所占用的字节数不是固定的, 取决于值的大小, 记为 int, 这是一种变长编码方式, 长度编码型整数占用的字节数有 1、3、4、9 字节共 4 种情况, 数字越小, 占用的字节数越少, 当数字小于 0xfb 时, 编码后其占用一个字节, 当数字大于等于 0xfb 并且小于等于 0x7ffff 时, 其编码后将占用 3 个字节(固定的 0xfc + 2 个字节数值位), 当数字大于等于 0x10000 且小于等于 0x7fffff 时, 其编码后将占用 4 个字节(固定的 0xfd + 3 字节的数值位), 当数字大于等于 0x1000000 且小于等于 0x7ffffffff 时, 其编码后将占用 9 个字节(固定的 0xfe + 8 字节的数值位), 根据这样的编码规则, 反过来我们也能得到长度编码型整数的解码规则, 识别数据的第一个字节便可以知道其后所跟随的数值位的字节数, 进而可以获得数值本身, 也就是说长度编码型整数实际上是用开头的一字节来指示其后所跟的数据的字节长度, 而其后的数值字节仍然是以小端字节序存储, 这里实际上有点类似于 Redis 的 ziplist 结构中的结点, 在 Redis ziplist 结构中, 其结点的第一个元素 previous_entry_length 也是采用类似的结构来表示前一个结点的长度, 若前一结点的长度小于 254 个字节, 则 previous_entry_length 的本身就是前一结点的长度, 若前一结点的长度大于等于 254 个字节, 则 previous_entry_length 本身占用 5 个字节的长度, 其第一字节的值固定为 0xfe, 而后所跟的 4 个字节指示了其前一个结点的长度 +### 数据包格式 +MySQL 的客户端和服务端交互是以数据包(Packet) 为单位进行的, 每个包的大小长度有限制, 最长为 2^24 -1 个字节(即 16MB)。`max_allowed_packet` 表示 MySQL Server 或者客户端接收的 packet 的最大大小,packet 即数据包,MySQL Server 和客户端上都有这个限制。**注意,这里说的数据包大小是指 MySQL 应用层对数据的约定,在网络协议封装中,TCP 层和 IP 层各自都有各自的单个数据报文限制,即 MTU 和 MSS 等**。 -### 抓包实战 +若包长度过大, 则客户端需要自行将包分片, 使得每段的长度在 MySQL 包的最大长度之下, MySQL Packet 由 Header 和 Body 组成, Header 包含两个字段: 包长度(payload_length)、序列号(sequence_id), Body 则是包的主体部分, 它的长度由 Header 中的 payload_length 字段指示, 包长度字段固定使用 int<3> 类型, 序列号固定使用 int<1> 类型, 当客户端和服务端开始交互时, 由客户端初始化 0 序列号, 之后每次交互, 数据发出方都基于前一个 sequence_id 增一, sequence_id 不是恒定唯一的, 当交互次数足够多后, 它会重新回到 0。 -在Windows客户端抓包,抓包图如下: -![](./image.png) @@ -141,75 +120,58 @@ mysql客户端和服务器之间的通信怎么实现?协议特征有哪些? -![image-20241030174852542](MySQL协议分析与抓包.assets/image-20241030174852542.png) +### 抓包实战 +在 Windows 客户端抓包,抓包图如下: -- TCP三次握手 -- Server——>Client,Server Greeting,Sever告诉客户端自己的版本、分配的连接ID、服务器的能力(Capabilities)、认证插件,字符集,Salt等信息。可以理解为 MySQL 的握手是由 Server 端发起的。 -- Client——>Server,Login Request,Client发起的登录请求报文, +![](./image.png) +![image-20241030174852542](MySQL协议分析与抓包.assets/image-20241030174852542.png) -#### TCP三次握手 +- TCP 三次握手 +- Server——>Client,Server Greeting,Sever 告诉客户端自己的版本、分配的连接 ID、服务器的能力(Capabilities)、认证插件,字符集,Salt 等信息。可以理解为 MySQL 的握手是由 Server 端发起的。 +- Client——>Server,Login Request,Client 发起的登录请求报文, +#### TCP 三次握手 #### 挑战数据包 -MySQL采用的是挑战-响应方式来进行身份认证。由服务端生成加密用的盐给客户端,提高安全性。 - - - - - -| 名称 | 类型 | 长度 | 含义 | -| ---------------------------- | ------ | ------ | ------------------------------------------------------------ | -| Protocol | int | 1 byte | MySQL应用协议的版本号,参考MySQL源代码/include/mysql_version.h头文件定义 | -| Version | string | 不确定 | MySQL Server的版本号 | -| Thread ID | int | 4 byte | 建立连接所分配的线程id | -| Salt | | | 挑战字符串,相当于用于加密密码的盐,分为两部分,长度分别为8字节和12字节 | -| Server Capabilities | | | 服务器支持的功能,int为32位,1支持,0不支持,最多支持32种功能。这里占2个字节,为int的前16位 | -| Server Language | | | 服务器编码,通常为utf8mb4 | -| Server Status | | | 服务器状态,通常为SERVER_STATUS_AUTOCOMMIT,表示自动提交事务 | -| Extended Server Capabilities | | | 服务器支持的拓展功能,int为32位,1支持,0不支持,最多支持32种功能。这里占2个字节,为int的后16位 | -| Authentication Plugin Length | | | ?好像指的是挑战字符串的长度,没搞懂为什么是21而不是20 | -| Unused | | | 未使用的部分 | -| Authentication Plugin | string | | 密码采用的加密方法,通常为mysql_native_password | - - - +MySQL 采用的是挑战-响应方式来进行身份认证。由服务端生成加密用的盐给客户端,提高安全性。 + +| 名称 | 类型 | 长度 | 含义 | +| ---------------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------ | +| Protocol | int | 1 byte | MySQL 应用协议的版本号,参考 MySQL 源代码/include/mysql_version.h 头文件定义 | +| Version | string | 不确定 | MySQL Server 的版本号,由 MYSQL_SERVER_VERSION 宏定义决定(参考MySQL源代码/include/mysql_version.h头文件定义) | +| Thread ID | int | 4 byte | 建立连接所分配的线程 id | +| Salt | | | 挑战字符串,相当于用于加密密码的盐,分为两部分,长度分别为 8 字节和 12 字节 | +| Server Capabilities | | | 服务器支持的功能,int 为 32 位,1 支持,0 不支持,最多支持 32 种功能。这里占 2 个字节,为 int 的前 16 位(参考MySQL源代码/include/mysql_com.h中的宏定义) | +| Server Language | | | 服务器编码,通常为 utf8mb4 | +| Server Status | | | 服务器状态,通常为 SERVER_STATUS_AUTOCOMMIT,表示自动提交事务 | +| Extended Server Capabilities | | | 服务器支持的拓展功能,int 为 32 位,1 支持,0 不支持,最多支持 32 种功能。这里占 2 个字节,为 int 的后 16 位 | +| Authentication Plugin Length | | | ?好像指的是挑战字符串的长度,没搞懂为什么是 21 而不是 20 | +| Unused | | | 未使用的部分 | +| Authentication Plugin | string | | 密码采用的加密方法,通常为 mysql_native_password | #### 认证数据包 - - #### 切换加密算法 如果挑战数据包返回的`Authentication Plugin`认证插件与你的用户密码加密方式不一致(比如你的用户采用的是`mysql_native_password`加密,而服务器返回的是`caching_sha2_password`),那么校验肯定失败,所以需要更换加密方式。服务器会发送一个切换认证插件请求数据包来指示客户端重新加密密码并发送。 - - - #### 成功响应报文 - ![alt text](./response_ok.png) Response OK ,成功响应,这个没什么好说的。 - #### 密码插件算法 - MySQL 5.6/5.7 的默认密码插件一直以来都是 `mysql_native_password`。其优点是它支持 `challenge-response` 机制,这是非常快的验证机制,无需在网络中发送实际密码,并且不需要加密的连接。 然而,`mysql_native_password` 依赖于 SHA1 算法,但 NIST(美国国家标准与技术研究院)已建议停止使用 SHA1 算法,因为 SHA1 和其他哈希算法(例如 MD5)已被证明非常容易破解。 - - 此外,由于 `mysql_native_password` 在 mysql.user 表中 `authentication_string` 字段存储的是两次哈希 SHA1(SHA1(password)) 计算的值 ,也就是说如果两个用户帐户使用相同的密码,那么经过 `mysql_native_password` 转换后在 mysql.user 表得到的哈希值相同。 尽管有 hash 值也无法得到实际密码信息,但它仍然告诉这两个用户使用了相同的密码。为了避免这种情况,应该给密码加盐(salt),salt 基本上是被用作输入,用于转换用户密码的加密散列函数。由于 salt 是随机的,即使两个用户使用相同的密码,转换后的最终结果将发生较大的变化。 - 从 MySQL 5.6 开始支持 `sha256_password` 认证插件。它使用一个加盐密码(salted password)进行多轮 SHA256 哈希(数千轮哈希,暴力破解更难),以确保哈希值转换更安全。然而,它需要要么在安全连接或密码使用 RSA 秘钥对加密。所以,虽然密码的安全性更强,但安全连接和多轮 hash 转换需要在认证过程中的时间更长。 - -