前几天帮朋友看一台内网测试机,他在自己电脑上敲下mysql -h 192.168.172.130 -uroot -p,回车之后光标卡了十几秒,最后蹦出来一行ERROR 2002 (HY000): Can't connect to server on '192.168.172.130' (115)。他第一反应是密码错了,改了三次密码;第二反应是 MySQL 没启动,systemctl status mysqld一看活得好好的。这就是这个报错最坑的地方——它长得像认证问题,实际上是连接根本没有到达 MySQL 服务进程。MySQL 远程连接这个场景,几乎每个后端、运维、做课程作业的人都会撞上一次,尤其是一台虚拟机里装好 MySQL、宿主机或者另一台机器去连的时候。这篇内容我会把 ERROR 2002 这条报错从错误码含义讲到分层排查,再到六类典型成因的实操修复、完整的打通流程,以及我自己踩过的坑,全部铺开讲清楚。不管你是刚装完 MySQL 的新手,还是接手了一台不知道被谁改过配置的服务器,都能照着往下走。
1. 先搞懂这条报错到底在说什么
1.1 报错信息逐段拆解
ERROR 2002 (HY000): Can't connect to server on '192.168.172.130' (115)这句话的信息密度其实很高,只是大多数人被最后那个 (115) 带偏了注意力。
ERROR 2002是 MySQL 客户端层面的错误码,不是服务端返回的。这一点必须先钉死:MySQL 服务端返回的错误都是四位数带 SQLSTATE 的格式,比如ERROR 1045 (28000): Access denied,那个28000是 SQLSTATE,代表认证失败类。而 2002 属于客户端错误码体系(CR_CONN_HOST_ERROR 一脉),意味着客户端在自己的连接阶段就失败了,压根没走到"服务端说行不行"这一步。换句话说,你的用户名密码对不对,此刻根本不重要,因为双方还没握上手。
(HY000)是 SQLSTATE,HY000是个通用/未分类的兜底状态码,客户端本地错误基本都是它,所以这个字段提供不了额外信息,不用纠结。
on '192.168.172.130'是客户端尝试连接的目标地址,这个值来自你命令行里的-h参数,或者配置文件里的 host 字段。
(115)是最关键的部分,它是操作系统层面的 errno,由底层 socket 系统调用失败后由客户端打印出来。很多人看到 115 会以为是 MySQL 的错误码,其实不是,它是 glibc 的 errno 编号。在 Linux 环境下,errno 115 对应的是EINPROGRESS(Operation now in progress,操作正在进行中)。这个名字听起来很正面,好像连接在推进——但注意,MySQL 客户端是用非阻塞 socket 发起 connect 的,它会用自己的connect_timeout(默认 10 秒左右)去等待可写事件。如果等满了超时时间,socket 仍然处于"进行中"的状态,客户端就会把这个最后残留的 errno 直接打印出来。所以(115) 在实战里的真实语义就是:连接超时,对端没有任何回应。
1.2 (115) 和 (111)、(113) 的区别,决定了排查方向
把这个错误码吃透,价值在于它能直接帮你缩小排查范围。Linux 环境下这几个 errno 在 MySQL 连接场景里非常常见,含义完全不同:
| 错误码 | 含义 | 典型成因 | 排查方向 |
|---|---|---|---|
| (115) | 连接超时,无任何回应 | 防火墙 DROP 丢包、路由不通、安全组拦截 | 网络层与防火墙 |
| (111) | Connection refused | 端口没有进程监听,或明确被拒绝 | 服务是否启动、bind-address |
| (113) | No route to host | 路由不可达、目标主机不在同网段 | 网卡、路由表、VLAN |
| (110) | Connection timed out | 与 115 高度类似,多见于非 Linux 平台 | 同 115 |
关键差别在 (115) 和 (111) 之间。防火墙规则有两种写法:DROP是把包静默丢掉,发起方永远等不到任何回包,最终就是超时,报 (115);REJECT是主动回一个 ICMP 端口不可达或者 TCP RST,发起方立刻收到拒绝,报 (111)。
这个区别实战意义极大:看到 (115),八成是路上被静默拦截了;看到 (111),八成是服务端根本没在监听那个地址。很多教程上来就让人改bind-address,但如果你的报错是 (115),改 bind-address 大概率白忙一场,因为包连主机都没到,服务怎么绑定根本不参与决策。反过来,如果是 (111),那基本可以直接排除网络设备,直奔 MySQL 配置和用户授权。
1.3 为什么本地能连、远程就连不上
这是新手最困惑的点:在服务器上敲mysql -uroot -p一切正常,换成另一台机器就报 2002。原因在于 MySQL 的连接路径不止一条。
MySQL 客户端连接时,如果 host 是localhost,Linux 下会优先走Unix Socket 文件(通常是/var/lib/mysql/mysql.sock或/tmp/mysql.sock),这条路完全不经过 TCP/IP 协议栈,不需要监听端口,自然也不受bind-address和防火墙影响。而一旦你用-h 192.168.172.130或者-h 127.0.0.1,连接方式就变成了TCP,需要服务端在对应网卡上真正监听,需要网络可达,需要防火墙放行。
所以"本地能连"这个事实,证明的只是 MySQL 进程活着、socket 文件在、账号至少对本地可用。它证明不了任何关于 TCP 监听和网络通路的结论。我见过太多人在这一步绕圈:反复确认服务在跑、反复重启 MySQL、甚至重装数据库,就是没意识到自己验证的是另一条完全不同的链路。
提示:想快速判断本地走的是哪条路,连上之后执行
status,输出里有一行Connection: Localhost via UNIX socket或者Connection: 192.168.172.130 via TCP/IP,一目了然。
2. 排查思路:从网络层到应用层的五层过滤
2.1 分层定位法,别一上来就改配置
远程连不上这个问题,可能出问题的地方从下往上有五层,我在实际排查时习惯按这个顺序走,绝不跳步:
第一层,物理与链路:两台机器是否在能互相到达的网络里。虚拟机网络模式(NAT、桥接、仅主机)在这里起决定作用,NAT 模式下宿主机访问虚拟机的 IP 往往会有各种意外。
第二层,IP 可达性:ping能不能通。注意 ping 通不代表 TCP 通,很多防火墙只拦 TCP 不拦 ICMP。
第三层,端口可达性:目标主机的 3306 端口能不能建立 TCP 连接。这一层能过滤掉 90% 的问题。
第四层,服务监听:MySQL 是否在能被外部访问的地址上监听 3306。
第五层,账户授权:mysql.user表里是否存在允许该来源主机连接的账号。
这五层的顺序不能乱。道理很简单:如果包在第三层就被防火墙丢了,你去第五层调 GRANT 语句是毫无意义的动作——改完了照样连不上,还会让你误以为"授权没用",进而怀疑 MySQL 本身有 bug。我给自己定的规矩是:每一层必须有明确的、可观测的证据证明它通过,才进入下一层。没有证据的"应该没问题",在排查里等同于"未知"。
2.2 必备的排查工具箱
下面这些命令是排查这条报错的全部家当,建议先确认目标机器上装没装,没装的话提前装好,出事的时候现装很耽误事。
ping —— 验证第二层
ping -c 4 192.168.172.130不通的话,先看两端 IP 是否在同一网段、掩码对不对、路由表里有没有对应条目。虚拟机场景要额外确认网络模式:桥接模式下虚拟机会拿到和宿主机同网段的 IP,通常最省事;NAT 模式下虚拟机的 192.168.x.x 是虚拟机内部网段,宿主机默认是能访问的,但其他物理机就不行。
telnet 或 nc —— 验证第三层
# 老派但通用,几乎所有 Linux 都自带 telnet 192.168.172.130 3306 # 更推荐,能直接给结论 nc -vz -w 3 192.168.172.130 3306nc -vz的-v输出详细信息,-z表示只扫描不发送数据,-w 3是 3 秒超时。返回succeeded说明端口通,返回Connection refused说明被明确拒绝(多半是没监听),返回timed out就是典型的静默丢包——和你的 (115) 同一个病因。
ss —— 验证第四层
ss -tlnp | grep 3306输出会明确告诉你监听地址。三种可能:
127.0.0.1:3306—— 只监听回环,外部必然连不上0.0.0.0:3306或:::3306—— 监听所有地址,正常- 什么都不输出 —— 服务没启动,或者被
skip-networking关掉了 TCP
netstat -tlnp | grep 3306效果一样,但 netstat 在新发行版里逐渐被移除,建议养成用 ss 的习惯。
mysql 客户端自带参数 —— 验证第五层
mysql -h192.168.172.130 -P3306 -uapp -p --connect-timeout=5--connect-timeout是客户端参数,单位秒。默认值在有些版本里长达 10 秒甚至更久,排查时手动压到 3 到 5 秒能显著减少等待。注意这个参数只影响连接阶段,连上之后执行长查询超时用的是另一套参数。
tcpdump —— 抓包看真相
# 在客户端执行,观察发出的 SYN 有没有回音 tcpdump -i any -nn 'tcp port 3306'这是终极手段。如果只看到本机发出的SYN,一直没有SYN-ACK,那就是标准的路被堵了;如果看到SYN之后立刻回来一个RST, ACK,那就是对端明确拒绝。同一时刻在服务端也抓一份,能立刻判断包到底有没有到达目标主机——这一招在云服务器安全组问题上特别好用,能直接证明"包卡在了云平台层面"还是"卡在了主机上"。
2.3 一张速查表锁定问题层
实际排查时,把现象和这张表对一遍,基本能锁定方向:
| 现象 | 高概率原因 | 优先验证命令 |
|---|---|---|
| ping 不通 | 网络模式、路由、IP 配置 | ip addr、ip route |
| ping 通但 nc 超时 | 防火墙 DROP、云安全组 | tcpdump双向抓包 |
| nc 报 Connection refused | MySQL 未启动或未监听 TCP | ss -tlnp | grep 3306 |
| 端口通但报 1045 | 账号密码或授权 host 不匹配 | SELECT user,host FROM mysql.user; |
| 端口通但报 1130 | 授权主机段不允许 | 同上 |
| 时通时不通 | 连接数打满、网络抖动 | SHOW STATUS LIKE 'Threads_connected'; |
这张表的价值在于:它把"报错文案"翻译成了"要执行的命令",避免你在没有方向的情况下漫无目的地翻配置文件。
3. 逐个击破:六类典型成因的实操修复
3.1 bind-address 只监听回环地址
这是最经典的一条。MySQL 的bind-address参数决定它在哪块网卡上监听 TCP。很多发行版(尤其是 Debian/Ubuntu 的 apt 包)默认在配置文件里写了bind-address = 127.0.0.1,意思是"只听本机回环"。这种配置下,从任何其他机器发来的包都会被内核直接拒绝,客户端看到的是 (111) 而不是 (115)——但如果中间还叠加了防火墙 DROP,就可能表现为 (115)。
修复方式:找到配置文件,把 bind-address 改成0.0.0.0或者直接注释掉。配置文件位置因发行版而异,常见位置如下:
# CentOS / RHEL / Rocky /etc/my.cnf # 也可能在 /etc/my.cnf.d/mysql-server.cnf # Ubuntu / Debian /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/my.cnf改之前先确认哪个文件真正生效,别改了半天发现改的是个不会被加载的文件:
mysqld --verbose --help | grep -A 1 "Default options"或者连上数据库执行:
SHOW VARIABLES LIKE 'bind_address'; SHOW VARIABLES LIKE 'datadir';我一般习惯用SHOW VARIABLES来反查,因为它的输出是"实际生效值",不受配置文件层级干扰。改完配置必须重启服务:
systemctl restart mysqld # Ubuntu 上是 mysql systemctl restart mysql重启后再用ss -tlnp | grep 3306确认监听地址变了。如果还是127.0.0.1,说明你改的文件没被加载,或者同一个参数在别的文件里被后面的配置覆盖了。MySQL 的配置加载是有顺序的,后面的覆盖前面的,!includedir引入的目录会按文件名字母序加载。
注意:
bind-address在 MySQL 8.0 里还涉及 IPv6。如果你的服务器同时有 IPv4 和 IPv6,写成0.0.0.0只监听 IPv4,写成::通常能同时接受两者。有些环境里客户端解析到 IPv6 地址去连,就会出现一些很诡异的超时,排查时可以用-h强制指定 IPv4 地址来验证。
3.2 授权表里没有允许远程来源的账号
端口通了,但连上去报ERROR 1045 (28000): Access denied for user 'root'@'192.168.172.1'——注意这里的 (28000) 就不是连接层问题了,说明包已经成功送到 MySQL,是认证环节被拒。这种情况属于另一类问题,但它经常和 2002 混在一起被误判,所以放在这里一起说。
MySQL 的权限模型是用户 + 来源主机二元组。'root'@'localhost'和'root'@'%'是两个完全不同的账号,一点关系都没有。默认安装完只有'root'@'localhost',所以远程连就是没有匹配的账号,直接拒绝。
MySQL 8.0 的正确做法是先建用户再授权,不要用GRANT隐式创建用户(8.0 之后这个行为已经废弃):
-- 建一个只允许 192.168.172 网段连接的账号 CREATE USER 'app'@'192.168.172.%' IDENTIFIED BY 'YourStrongPass!2024'; -- 只给业务库权限,不要一上来就 ALL ON *.* GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'192.168.172.%'; FLUSH PRIVILEGES;几个必须注意的点:
一是'%'不匹配localhost。这是 MySQL 里著名的坑。'%'在主机匹配规则里代表"除 localhost 之外的所有主机",因为localhost被特殊对待(走 socket)。所以如果你建了'app'@'%'却没建'app'@'localhost',本机反而连不上。稳妥做法是建两个账号,或者干脆用'app'@'%'加上'app'@'localhost'。
二是网段写法要精确。'192.168.172.%'匹配 192.168.172.0/24 这个网段,比'%'安全得多。生产环境即使在内网,也不要用'%',万一哪天机器暴露到公网就是灾难。
三是不要长期用 root 远程。我见过不少教程让新手直接UPDATE mysql.user SET host='%' WHERE user='root',改完之后确实能连了,但这是把一个超级权限账号敞开在网络里。正确做法是给业务单独建账号、单独授权,root 保持只允许本地。
四是改完要FLUSH PRIVILEGES。用GRANT/CREATE USER语句操作时,MySQL 会自动刷新内存中的权限表,但如果你是直接UPDATE mysql.user表,就必须手动执行FLUSH PRIVILEGES,否则改的只是硬盘上的表,内存里还是旧权限。
验证账号是否创建成功:
SELECT user, host, plugin FROM mysql.user WHERE user = 'app';MySQL 8.0 默认认证插件是caching_sha2_password。老版本的客户端工具(比如一些用了很久的 Navicat、老版 PHP 的 mysql 扩展)不支持这个插件,会在认证阶段报错。如果你确实需要用老客户端,可以指定IDENTIFIED WITH mysql_native_password BY 'xxx',但更推荐的做法是升级客户端,别为了兼容把服务端的认证强度降下来。
3.3 防火墙与云安全组拦截
如果ss -tlnp显示监听是0.0.0.0:3306,账号也建好了,但 nc 还是超时,那就到防火墙环节了。这一层是 (115) 最常见的成因,因为它默认就是静默丢包。
主机防火墙,按发行版分:
# firewalld(CentOS 7+ / RHEL 系) firewall-cmd --permanent --add-port=3306/tcp firewall-cmd --reload # 确认结果 firewall-cmd --list-ports # ufw(Ubuntu / Debian) ufw allow 3306/tcp ufw status # 老式 iptables iptables -I INPUT -p tcp --dport 3306 -j ACCEPT # 记得持久化,否则重启就没了 service iptables save # CentOS 6 时代 # 或者用 iptables-persistent云平台安全组是另一个独立层。如果你用的是云主机,安全组规则是独立于操作系统防火墙的,两边都要放行。安全组的特点是它工作在云平台的虚拟网络层,你在主机上用 tcpdump 抓不到任何东西——因为包根本没到主机。这也是我前面强调"两侧同时抓包"的原因:客户端抓到 SYN、服务端抓不到,就是云平台层面的问题,这时候去翻操作系统防火墙纯属浪费时间。
排查安全组的时候,我一般直接上 tcpdump:
# 客户端侧 tcpdump -i any -nn 'host 192.168.172.130 and tcp port 3306' # 服务端侧 tcpdump -i any -nn 'tcp port 3306'两边对照,包到没到,一秒见分晓。
提示:有些环境的防火墙规则是按 zone 走的,
--add-port要确认加到了正确的 zone(一般是 public)。用firewall-cmd --get-active-zones先看清楚哪个网卡属于哪个 zone,加错 zone 是白加。
3.4 skip-networking 与端口占用
skip-networking这个参数一旦开启,MySQL 会完全关闭 TCP/IP 监听,只保留 socket 连接。这种配置在某些"安全加固"教程里会出现,也有的是被人误加进去的。检查方式:
SHOW VARIABLES LIKE 'skip_networking';返回ON就说明 TCP 被关了。把它改成OFF或者在配置文件里注释掉skip-networking这一行,重启服务即可。
相关联的还有一个port = 0,也是关闭 TCP 监听的另一种写法。
另一种情况是端口被别的进程占了。虽然大概率 MySQL 会因为端口冲突直接启动失败(而不是静默),但如果配置了--skip-grant-tables或者一些特殊启动参数,可能出现诡异状态。检查:
ss -tlnp | grep 3306 # 输出里最后一段就是进程名和 PID如果占着 3306 的不是 mysqld,那就要处理冲突。还有一个小细节:MySQL 8.0 除了 3306,还会开一个 33060 端口给 X Protocol 用,这个端口和主连接无关,别混淆。
另外有些云服务商或者容器环境里,mysqld被配置成监听非标准端口(比如 3307)。这种情况下你的-P参数必须跟着改:
mysql -h192.168.172.130 -P3307 -uapp -p我遇到过一位朋友,配置文件里 port 被某次"优化"改成了 3307,他一直用 3306 连,报的也是 2002,折腾了大半天才发现。所以查SHOW VARIABLES LIKE 'port';应该是排查清单里的固定动作。
3.5 SELinux 与代理协议干扰
SELinux在 CentOS/RHEL 系默认是 enforcing 状态。它和防火墙是两套独立机制:防火墙管的是包能不能进来,SELinux 管的是进来之后 mysqld 这个进程能不能绑定那个端口。如果 MySQL 想监听非标准端口(比如 3307),SELinux 会直接阻止,日志里会有 avc denied 的记录。
检查和处理:
getenforce # 返回 Enforcing / Permissive / Disabled # 查看有哪些和 mysql 相关的布尔值 getsebool -a | grep mysql # 允许 mysqld 连接任意端口(谨慎使用) setsebool -P mysql_connect_any 1 # 或者更精确地给端口打标签 semanage port -a -t mysqld_port_t -p tcp 3307我个人倾向于精确打标签而不是直接开mysql_connect_any,尤其是在合规要求比较严的环境里。如果只是临时验证问题是不是 SELinux 引起的,可以setenforce 0临时切到宽容模式测一次,确认是它干的再去做正式配置,测完记得切回去,别把setenforce 0当成最终方案。
代理协议(Proxy Protocol)是另一类比较隐蔽的干扰。如果你前面挂了负载均衡或者某些代理,代理可能开启了 Proxy Protocol 模式,它在原始 TCP 数据前面插了一段额外的头部信息。MySQL 本身不认识这个头部,会直接拒绝连接。这个坑比较小众但确实存在,特征是:直连 IP 正常,走代理就报连接层错误。确认方法是在代理侧关掉 Proxy Protocol 试一次。
顺带提醒一个概念上的混淆:MySQL 自己有一套proxy_user机制(PROXY权限),那是做用户映射用的,和网络层的代理协议完全是两回事,不要因为名字像就往一起想。
3.6 连接数打满与超时参数调优
服务端有个max_connections参数,默认常见是 151。当并发连接达到上限时,新连接会被拒绝,报的是ERROR 1040 (HY000): Too many connections,这是另一条错误。但如果配合连接池的连接慢、以及大量的半开连接,也可能表现为连接阶段长时间等待,最终超时。
排查思路:
SHOW VARIABLES LIKE 'max_connections'; SHOW VARIABLES LIKE 'connect_timeout'; SHOW STATUS LIKE 'Threads_connected'; SHOW STATUS LIKE 'Max_used_connections'; SHOW STATUS LIKE 'Aborted_connects';Max_used_connections如果接近max_connections,说明连接池配置有问题;Aborted_connects持续增长则说明有大量连不上的尝试,可能来自配置错误的客户端或者扫描。
connect_timeout是服务端参数(默认 10 秒),指的是等待握手完成的时间,和客户端的--connect-timeout是两个不同的东西。排查超时的时候,把客户端的--connect-timeout压小,能让你的验证循环快很多。
短期应急可以调大max_connections:
SET GLOBAL max_connections = 500;但请注意这只是临时生效,重启就回去了。永久生效要写进配置文件。而且调大连接数本质上是掩盖问题——真正的解法是修复连接泄漏,让连接池的上限和数据库的上限对齐。
4. 完整实操:从零打通一台远程 MySQL
4.1 服务端配置:三个参数一次搞定
假设你在一台 IP 为 192.168.172.130 的 Linux 机器上刚装好 MySQL 8.0,现在要让网段内另一台机器连上。按顺序来:
第一步,确认服务在跑且监听正确。
systemctl status mysqld ss -tlnp | grep 3306如果监听显示127.0.0.1:3306,进入第二步。
第二步,改配置文件。先定位生效的配置文件:
mysqld --verbose --help 2>/dev/null | grep -m1 "Default options"比如输出指向/etc/my.cnf,就在[mysqld]段落下加:
[mysqld] bind-address = 0.0.0.0 port = 3306 max_connections = 300 character-set-server = utf8mb4 collation-server = utf8mb4_0900_ai_ciutf8mb4这一项不是必须的,但我建议顺手加上,避免后面出现中文乱码再来回折腾。utf8mb4_0900_ai_ci是 8.0 的默认排序规则,显式写出来更清晰。
第三步,重启并验证。
systemctl restart mysqld ss -tlnp | grep 3306现在应该看到0.0.0.0:3306。
4.2 账户授权:最小权限原则
连上本地数据库,建远程账号:
CREATE USER 'app'@'192.168.172.%' IDENTIFIED BY 'Str0ng!Passw0rd'; CREATE DATABASE IF NOT EXISTS appdb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER ON appdb.* TO 'app'@'192.168.172.%'; FLUSH PRIVILEGES; SELECT user, host, plugin FROM mysql.user WHERE user = 'app';注意我这里给的是具体权限列表而不是ALL PRIVILEGES。ALL包含DROP、GRANT OPTION这些危险权限,业务账号根本用不上。同理,appdb.*限定到具体库,而不是*.*。这套最小权限写法麻烦一点,但一旦哪天账号泄露,损失范围可控。
character_set和collate我在建库时显式指定了,这样即使服务端配置被谁改过,这个库的字符集也不会漂移。
4.3 客户端验证:按层逐步确认
在另一台机器上,按顺序执行:
# 1. 网络可达 ping -c 3 192.168.172.130 # 2. 端口可达 nc -vz -w 3 192.168.172.130 3306 # 3. 认证与授权 mysql -h192.168.172.130 -P3306 -uapp -p --connect-timeout=5 appdb -e "SELECT NOW(), USER(), CURRENT_USER();"第三条命令的输出很有价值:USER()返回的是客户端声明的身份,CURRENT_USER()返回的是实际匹配到的账号。如果两者不一致,说明匹配到了另一个账号(比如你以为连的是app@192.168.172.%,实际匹配了app@%),这在排查权限问题时是决定性的证据。
我强烈建议把这三条命令写成一个脚本,以后每换一个环境就跑一遍,比凭记忆一条条敲靠谱。
4.4 事后加固:三件必须做的事
打通之后别急着收工,这三件事不做,后面一定会出问题。
第一件,别把 root 开远程。如果你之前为了图省事改过mysql.user里 root 的 host,现在改回来:
-- 先确认有没有被改过 SELECT user, host FROM mysql.user WHERE user = 'root'; -- 如果有 '%',删掉这条记录 DROP USER 'root'@'%';第二件,检查是否有匿名账号。有些老版本的初始化脚本会建空用户名的账号,这是个安全隐患:
SELECT user, host FROM mysql.user WHERE user = ''; DROP USER ''@'localhost';第三件,把改动记录下来。我吃过这个亏:半年前给别人配的机器,半年后出问题,没人记得当时改过哪些参数。现在的做法是每次动完配置,都在机器上留一个/root/mysql-notes.md,记录改了什么文件、什么参数、为什么改。成本极低,收益极高。
注意:如果确实需要从公网访问,正确做法是加一层跳板或者用带认证的隧道方案,而不是把 3306 直接暴露出去。3306 是全网扫描的重点目标,暴露在公网上的 MySQL 实例,快的话几小时内就会收到暴力破解尝试。
5. 踩坑实录与常见问题速查
5.1 那些让我熬夜的几个坑
坑一:虚拟机网络模式的坑。有次在 VMware 里用 NAT 模式装 CentOS,宿主机能 ping 通虚拟机的 192.168.x.x,但连 3306 一直超时。查了半天防火墙、bind-address 都没问题,最后发现是 VMware 的 NAT 网络设置里,DHCP 分配的网段和虚拟机静态 IP 冲突了。改成桥接模式,一切正常。教训是:虚拟机环境下,网络模式应该作为第一优先级确认,而不是最后才想起来。
坑二:改了配置文件但没重启。MySQL 里确实有一些参数支持在线修改(SET GLOBAL),但bind-address、port、skip-networking这些网络层参数是必须重启的。我见过有人改完配置直接测试,测不通就以为改错了方向,来回折腾一小时。改完先重启,再验证。
坑三:'%'和localhost的匹配混淆。这个坑我踩过不止一次。建了'app'@'%',远程连不上——因为远程那台机器解析出来的主机名恰好是localhost(在做 hosts 映射的测试环境里会发生)。或者反过来,本机连不上,因为只建了'app'@'%'。稳妥做法是建账号时把'app'@'localhost'也补一个。
坑四:客户端配置文件的隐形干扰。MySQL 客户端会读取/etc/my.cnf、~/.my.cnf等文件,如果里面写了[client]段落的host或者port,会覆盖你命令行的部分参数。有次我明明写了-h192.168.172.130,实际连的却是另一台机器,查了半天才发现是~/.my.cnf里的 host 在作祟。排查时可以用mysql --print-defaults看看客户端实际加载了哪些默认参数。
坑五:--connect-timeout的理解偏差。这个参数只作用于建连阶段。有次我把它设成 3 秒,结果连接建立之后的慢查询还是跑很久,我一度以为参数没生效。后来才想明白,建连和执行查询是两个阶段,后者要调的是max_execution_time或者客户端的读超时设置。
5.2 常见问题速查
| 报错/现象 | 直接原因 | 一行命令定位 |
|---|---|---|
| ERROR 2002 (115) | 连接超时,包被静默丢弃 | nc -vz -w 3 host 3306 |
| ERROR 2002 (111) | 端口无监听或被拒绝 | ss -tlnp | grep 3306 |
| ERROR 1045 (28000) | 账号密码错,或 host 不匹配 | SELECT user,host FROM mysql.user; |
| ERROR 1130 (HY000) | 该来源主机未被授权 | 同上,检查 host 字段 |
| ERROR 1040 (HY000) | 连接数打满 | SHOW STATUS LIKE 'Threads_connected'; |
| ERROR 1290 (HY000) | 服务端处于 --skip-grant-tables 状态 | SHOW VARIABLES LIKE 'skip_grant_tables'; |
| 连上后中文乱码 | 字符集不一致 | SHOW VARIABLES LIKE 'character_set%'; |
| 时通时断 | 连接池配置、网络抖动 | 查Aborted_connects、抓包 |
表里那条 1290 值得单独说一句:如果服务端带着--skip-grant-tables启动,所有权限检查会被跳过,此时执行任何需要写权限的操作都会报 1290。这个状态下的 MySQL 是完全不设防的,任何人不需要密码就能连上,一旦排查完必须立刻恢复正常的启动参数并重启。
5.3 日志才是最诚实的证人
所有命令都试过还是没头绪的时候,回到日志。MySQL 的错误日志里往往直接写着原因,只是大多数人不去看。
# 先找到日志位置 mysql -e "SHOW VARIABLES LIKE 'log_error';" # 然后直接看 tail -n 100 /var/log/mysqld.log # 或者 journalctl -u mysqld -n 100 --no-pagerjunctionctl这一招在 systemd 系统上特别好用,因为很多发行版把 MySQL 的日志直接导向了 journal,/var/log/mysqld.log里反而什么都没有。
日志里要重点关注几类信息:启动时有没有报"Can't start server: Bind on TCP/IP port",这说明端口冲突;有没有 "Access denied for user" 的连续记录,这说明有人在暴力尝试;有没有 "Too many connections",说明连接数问题。
最后分享一个我自己的习惯:每次远程连接出问题,我会先在客户端跑一次带超时的 nc,同时开一个 tcpdump 窗口。三秒之内,是路不通、是端口不响应、还是认证被拒,基本就定性了。比翻配置文件快得多,也比凭经验猜靠谱得多。这套动作做熟之后,从看到报错到定位到根因,通常不会超过五分钟。