Mysql 数据库连接问题诊断分析笔记整理
99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式
写在前面
- 博文内容极客时间老师的专栏学习笔记整理:
- https://time.geekbang.org/column/article/802420
- 理解不足小伙伴帮忙指正 :),生活加油
99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式
持续分享技术干货,感兴趣小伙伴可以关注下 ^_^
数据库连接问题诊断分析
数据连接认证流程
客户端执行命令或 SQL
前,需要先创建一个到数据库服务端的连接
,并完成用户认证
。
MySQL
服务端使用插件
的方式认证客户端的用户身份。不同的插件
在验证用户密码
时,细节上会有所不同。MySQL 中,认证插件
和用户
相关,不同用户
可以使用不同的插件
进行密码验证
。
创建用户时,如果不显式指定,会使用参数 default_authentication_plugin
指定的插件。从 MySQL 8.0
开始,使用 caching_sha2_password
作为默认的认证插件,而 5.7
使用的默认插件是 mysql_native_password
。
1 | mysql> show variables like 'default_authentication_plugin'; |
也可以在创建用户
的时候指定认证插件
1 | mysql> create user 'user_01'@'%' identified by 'somepass'; |
或者通过 alter user
命令修改用户的认证插件
。
1 | mysql> create user 'user_03'@'%' identified by 'somepass'; |
从mysql.user
表可以查看每个用户
使用了哪个认证插件
。
1 | mysql> select user,host,plugin,substring(authentication_string, 1, 18) as passwd |
caching_sha2_password
完整登录流程我们以 MySQL 8.0
默认的插件 caching_sha2_password
为例,分析连接建立的过程。
- 客户端首先要和服务端建立一个 TCP 连接。TCP 连接建立成功后,才能进行后续的步骤。
- 服务端发送
握手协议包(ServerHandshake)
,握手协议包里面包含了服务器版本、使用的协议版本、服务端支持的特性(如是否支持加密连接)、服务端使用的认证插件(caching_sha2_password)、一串随机数据
。 - 客户端读取和解析服务端的协议包,发送
握手协议回包
,回包里面有用户名、通过一定规则计算得到的密码哈希值、客户端支持的特性、客户端使用的认证插件、客户端版本
等信息。不同版本的客户端可能会使用不同的默认认证插件。如MySQL 5.7
版本的客户端默认使用mysql_native_password
插件,MySQL 8.0
默认使用caching_sha2_password
插件。客户端和服务端使用的默认插件可以不一样。 - 服务端在接收到客户端发送的握手协议回包之前,并不知道客户端使用的用户名,因此也不知道用户使用的认证插件。接收到客户端的回包后,服务端从里面解析出
用户名
,到用户列表中获取到用户信息,得到用户的认证插件
。如果用户使用的认证插件
和服务器的默认插件
不一样,或者客户端和服务端使用的认证插件不一样,服务端就需要告诉客户端切换认证方式。 - 如果
客户端和服务端的认证插件不一致
,客户端需要根据服务端的要求,重新计算密码哈希后再发送到服务端。服务端接收到客户端新发送过来的认证包之后,就可以验证用户密码是否匹配了。对于caching_sha2_password
插件,这里分为两种情况。
- 用户首次登录时,服务端没有缓存用户的密码信息,此时需要进行完整的登录流程,就是上图中的 6、7、8、9 这几个步骤。
- 用户登录成功后,会在服务端缓存哈希后的密码信息。下一次用户登录时,就可以根据缓存的密码哈希来验证,不需要进行完整的登录流程。缓存的密码哈希会在执行 ALTER USER、Flush Privileges 或重启数据库后失效。
- 使用
caching_sha2_password
时,如果服务端还没有缓存用户的密码哈希,会通知客户端发送明文的密码。 - 如果客户端和服务端建立了
加密连接
,则可以直接发送明文密码
。但如果客户端和服务端之间的连接没有加密
,直接发送明文密码是不安全的,此时客户端可以向服务端请求RSA
公钥,用于加密明文的密码
。 - 服务端将
RSA
公钥发送给客户端。 - 客户端使用接收到的
RSA
公钥加密明文密码
,发送到服务端。 - 服务端得到原始密码后,根据一定的规则计算哈希值,然后再跟存储在用户表中的
authentication_string
进行对比。用户认证成功后,服务端将密码哈希缓存起来。用户再次登录时,就可以基于缓存的密码哈希
来验证用户登录信息。 - 如果服务端、客户端以及用户的
认证插件都一样
,并且用户信息已经缓存在服务端
,那么上述4~9
之间的步骤都会跳过,只需执行步骤1、2、3、10
加密连接
MySQL
支持使用 TLS
协议建立加密连接。使用 MySQL
客户端连接数据库时,默认就会开启连接加密。
使用 MySQL 客户端登录服务器后,执行\s
,查看 SSL
这一行的输出,如果显示 Cipher in use …
,则说明当前连接启用了加密。
1 | # mysql -uuser_01 -h172.16.121.234 -P3306 -psomepass |
可以使用SHOW STATUS
命令结合 Ssl_cipher
状态变量,如果 Ssl_cipher
的值不为空,那么当前的连接是加密的。如果值为空,则表示连接未使用SSL/TLS
加密。这种方法不需要具有 PROCESS
权限,适用于 MySQL 5.7
及更高版本。
1 | SHOW STATUS LIKE 'Ssl_cipher'; |
也可以在连接数据库时指定参数,不开启连接加密,此时“SSL”
这一行显示“Not in use”
。
1 | # mysql --ssl-mode=disabled -uuser_01 -h172.16.121.234 -P3306 -psomepass |
强制要求某些用户必须使用加密连接
,这可以在创建用户
时指定,或者通过 ALTER USER
命令修改。下面创建的几个用户都需要开启连接加密
1 | mysql> create user 'user_05'@'%' identified by 'somepass' require ssl; |
必须开启加密连接
,否则无法登录数据库
。
1 | # mysql -uuser_05 -h127.0.0.1 -psomepass --ssl-mode=disabled |
其中 user_06
和 user_07
在登录时还必须提供客户端的证书,user_06
对证书 subject
有要求,user_07
对证书 issuer
有要求,如果提供的证书不满足要求,也无法登录数据库。
1 | # mysql -uuser_06 -h127.0.0.1 -psomepass --ssl-key=server-key.pem --ssl-cert=server-cert.pem -e 'select 1'; |
未开启加密连接或证书不对而无法登录数据库时,服务端返回的报错也是 ERROR 1045
,跟密码不对时的报错信息是一样的。
在确认密码没有问题后,如果还是报 ERROR 1045
,需要检查用户是否有加密连接和证书相关的要求。这一项我们可以到 mysql.user
表查看。
1 | mysql> select user,host,ssl_type, cast(ssl_cipher as char) cipher, |
从服务端的错误日志中,也可以看到一些相关的错误信息(需要将参数 log_error_verbosity
设置为 3
)。
1 | ### alert.log |
数据库无法连接问题总结
检查数据库监听是否正常开启
可以在数据库服务器上通过 netstat
或 ss
命令查看数据库端口的监听是否正常开启。这里需要注意监听的 IP
,如果监听的 IP
是 127.0.0.1
,则只能在本地连接到数据库。
1 | # ss -nltp | grep 3306 |
检查客户端到服务端之间的网络是否能连通(ERROR 2003 (HY000))
1 | # telnet 172.16.121.234 3306 |
如果端口不通,使用 MySQL
客户端访问数据库的时候,也会有相应的报错信息
1 | # mysql -h 172.16.121.234 -P 3306 |
1 | # perror 111 |
这里的错误码跟操作系统有关,比如在 mac
下,错误码就变成了 61,需要在 mac
环境下使用 perror
工具查看。
1 | $ mysql -uuser_01 -h172.16.121.234 -psomepass |
是否是认证阶段出了问题(ERROR 2059,ERROR 2061,ERROR 1045)
ERROR 2059,Authentication plugin ‘caching_sha2_password’ cannot be loaded
客户端的版本太低了,不支持 caching_sha2_password
认证插件。解决方法是使用新版本的客户端
1 | /opt/mysql5.6/bin/mysql -uuser_01 -h127.0.0.1 -psomepass |
ERROR 2061,Authentication requires secure connection
1 | # mysql --ssl-mode=disabled -uuser_01 -h127.0.0.1 -psomepass |
使用 caching_sha2_password
认证插件时,用户首次登录时还没有被缓存,服务端需要获取用户的明文密码,如果客户端没有开启连接加密,发送明文密码有安全风险,就会报这个错误。开启连接加密可以解决这个问题,或者也可以在客户端指定 get-server-public-key
选项。
1 | mysql --get-server-public-key --ssl-mode=disabled -uuser_01 -h127.0.0.1 -psomepass |
使用 JDBC
时需要添加连接属性 allowPublicKeyRetrieval=true
。
MySQL
备库连接到主库时,也可能会遇到一样的问题,可以在建立复制时,指定 GET_MASTER_PUBLIC_KEY
或 GET_SOURCE_PUBLIC_KEY
选项。
1 | ## 使用change master |
使用 MySQL
组复制(MGR)如果遇到这个问题,可以通过设置参数 group_replication_recovery_get_public_key
来解决。
1 | set global group_replication_recovery_get_public_key=ON; |
ERROR 1045 (28000): Access denied for user ‘username’@‘hostname’
ERROR 1045
可能是我们平时遇到最多的一个报错,通常这是由于客户端输入的密码不正确引起的。
但是我们在这一讲前面加密连接演示过,如果强制要求用户使用加密连接,或者对客户端的证书有要求,而客户端没有满足这些条件,那么连接时也会报这个错。可以到 mysql.user
表查看用户是否有 SSL 相关要求。同时也可以到数据库的错误日志中查看是否有相应的报错信息。
1 | # mysql -uuser_07 -h172.16.121.234 -psomepassx --ssl-key=client-key.pem --ssl-cert=client-cert.pem -e 'select 1' |
数据库连接数限制
MySQL 中有几个地方限制用户的连接数
。
参数 max_connections
限制了数据库允许创建的总连接数
。
参数 max_user_connections
限制了同一个用户允许创建的最大连接数
。还可以指定某个具体的用户允许创建的最大连接数
。
1 | mysql> create user 'user_09'@'%' identified by 'somepass' |
如果连接数超过了限制,根据上面几种情况,分别会报下面这 3 个错误。
ERROR 1040 (08004)
: Too many connectionsERROR 1203 (42000)
: User user_01 already has more than ‘max_user_connections’ active connectionsERROR 1226 (42000)
: User ‘user_09’ has exceeded the ‘max_user_connections’ resource (current value: 2)
操作系统资源限制
ERROR 1135 (HY000): Can’t create a new thread (errno 11)
MySQL 创建连接时,需要消耗操作系统资源,如果操作系统资源超出了限制,也会导致客户端连接失败。下面这个例子中,MySQL 服务端无法创建新的线程
。
1 | # mysql -uroot -h127.0.0.1 -pabc123 |
如果文件句柄数超出了限制,在数据库的错误日志中还可能会出现这样的报错信息。
1 | [ERROR] [MY-010283] [Server] Error in accept: Too many open files |
连接次数阈值限制
这也是一个比较致命的问题。客户端从某台机器连接数据库时,连续出错,出错的次数超过了参数 max_connect_errors
的设置后,服务端会禁止这台机器后续的连接。
ERROR 1129 (HY000): Host '172.16.121.237' is blocked because of many connection errors; unblock with 'mysqladmin flush-hosts'
这里限制的是客户端的 IP,也就是从这个 IP 的发起所有连接都会被限制
。从 performance_schema.host_cache
表里,我们可以看到客户端的连接错误次数。
1 | mysql> select * from performance_schema.host_cache\G |
上面的报错信息中就提供了解决方法:执行 flush hosts
操作。
1 | mysql> flush hosts; |
并不是所有的连接错误都会引起客户端被禁,比如密码错误并不会导致客户端被禁。host_cache
表的 COUNT_HANDSHAKE_ERRORS
达到 max_connect_errors
,才会导致客户端被禁。
比如连续 telnet mysql
的端口会引起这个问题,或者使用了无效的 ssl
证书可能会导致这个问题。
1 | # mysql -uuser_06 -h 172.16.121.234 -psomepass --ssl-cert=client-cert.pem --ssl-key=client-key.pem --ssl-ca=ca.pem |
连接中断问题
使用 MySQL 自带的命令行客户端时,可能会遇到下面这几个报错。
比如使用 MySQL
自带的命令行客户端时,可能会遇到下面这几个报错。
ERROR 2013 (HY000)
: Lost connection to MySQL server during queryERROR 4031 (HY000)
: The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior
使用 Java
编写的应用程序,在访问 MySQL
数据库时,比较常见的报错有 2
个
CommunicationsException: The last packet successfully received from the server was 15,032 milliseconds ago. The last packet sent successfully to the server was 15,035 milliseconds ago. is longer than the server configured value of ‘wait_timeout’.
Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost.
客户端到服务器之间的连接断开了,实际的情况分析:
情况 1:连接被 Kill
如果有人使用 Kill 命令终止了某个会话,那么原先的那个客户端再执行 SQL 时,就会发现连接已经中断了。
1 | mysql> show processlist; |
情况 2:数据库重启了
如果数据库重启发生了重启,那么原先所有的连接都会断开。
有一种比较特殊的情况,由于数据库底层数据文件损坏等原因,数据库在不停地重启。表现出来的现象是可以连接到数据库,但是执行 SQL 时,连接已经断开了。
1 | mysql> show global status where variable_name in ('Uptime'); |
我们可以通过查看数据库的 Uptime
状态变量来判断数据库最近是否有重启。Uptime
记录了数据库从启动后至今经过的秒数。
情况 3:连接空闲时间超时
MySQL 中参数 interactive_timeout
和 wait_timeout
用来控制连接的空闲超时,如果一个连接在指定的时间内没有发起任何请求,就会被服务器断开。
1 | mysql> show global variables where variable_name in ('wait_timeout', 'interactive_timeout'); |
全局变量:
interactive_timeout
用来控制交互式连接的空闲超时时间,wait_timeout
用来控制非交互式连接的空闲超时时间。
wait_timeout
还是一个会话级别的参数
,每个会话可以分别设置不同的超时时间。默认情况下,服务器在创建一个连接时,根据客户端的连接类型来设置超时时间,对于交互式连接,
服务器基于 interactive_timeout
来设置连接的超时时间,对于非交互式连接,服务器根据全局变量 wait_timeout
来设置超时时间。连接还可以自己在会话级别修改超时时间。
要查看一个连接真实的超时时间,最简单的办法是通过这个连接查看会话变量 wait_timeout
的值。
1 | mysql> set wait_timeout=3600; |
情况 4:代理(Proxy)超时
有的时候,数据库连接自身的空闲超时设置得比较大,但是在数据库之前配置了代理,而代理的空闲超时时间比较短。应用程序通过代理访问数据库时,就可能会遇到连接中断的问题。
下面是 nginx
的 4 层代理的一个例子。nginx
4 层代理默认的超时时间是 10
分钟,也就是如果 10
分钟内没有任何请求,就会把连接断开。
1 | ## /etc/nginx/nginx.conf |
如果应用程序通过 nginx
来访问数据库,空闲时间超过 10
分钟后连接就会被断开。而且还有一个更严重的问题,通过代理无法执行耗时超过 10
分钟的 SQL
1 | mysql> select sleep(610); |
那么连接断开的问题应该怎么解决呢?首先可以根据业务的实际情况,将空闲超时时间设置得长一些。如果使用了代理,需要注意代理的超时设置。
对于 Java 应用程序,一般会使用数据库连接池,要正确地设置连接池的参数。
有的连接池支持空闲连接回收,有的连接池支持连接探活(Keepalive),也就是每隔一定时间就执行一个 Keepalive 的 SQL,需要注意连接池的 Keepalive 执行间隔要比数据库的 wait_timeout 或代理的空闲超时时间设置得更短。
博文部分内容参考
© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)
© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)
Mysql 数据库连接问题诊断分析笔记整理
https://liruilongs.github.io/2024/12/10/待发布/Mysql-数据库连接问题诊断分析/