1. 连不上的时候,先别急着改密码:把这条链路上的关卡数清楚
我在帮同事处理远程数据库问题时,遇到最多的一类求助就是"Navicat 连阿里云上的 MySQL,报错 10060,是不是密码错了"。十次里面有八九次跟密码没关系。数据库连接这件事,本质上不是"一个动作",而是一条跨越好几层的链路,任何一层没打通,客户端看到的报错都是同一句干巴巴的"无法连接"。你如果只盯着用户名和密码调,基本等于在黑屋子里找钥匙。
把这条链路摊开来看,从你本地的 Navicat 到云服务器上的 MySQL,中间至少要过五道关:本地网络出口、云平台的安全组(或者叫防火墙策略)、服务器操作系统自身的防火墙、MySQL 服务是否监听在对外地址上、以及这个账号本身有没有被授权从你这台机器的 IP 连进来。这五道关是串联关系,前一道不通,后面几道连表现的机会都没有。所以正确的排查姿势永远是从外往里、逐层验证,而不是跳到最后一步试密码。
这篇内容主要写给两类人:一类是第一次把数据库放到云服务器上、准备用图形化工具远程连一下的开发或运维新手;另一类是用过一段时间但每次遇到连接报错都要重新查一遍资料、没有形成固定排查套路的人。文中的操作以阿里云 ECS 上自建 MySQL 为主线,顺带说清楚云数据库实例在配置思路上有什么不同。我不会只给你一串命令,更重要的是告诉你为什么是这个顺序、每个参数改动的实际影响是什么。
1.1 一次成功的连接,服务端到底发生了什么
理解握手过程能帮你省掉大量瞎试的时间。当你在 Navicat 里点下"测试连接",客户端会做这么几件事:向目标 IP 的 3306 端口发起 TCP 三次握手;握手成功后,服务端把版本号、连接 ID、认证插件等握手包信息发回来;客户端拿这些信息决定用哪种认证方式提交凭据;服务端在mysql.user表里按"用户名 + 来源主机"匹配到一行记录,比对密码哈希;匹配通过后再检查这个账号对目标库有没有权限。
关键点在于第四步——MySQL 的账号是"用户名@来源主机"的组合,不是单纯一个用户名。app@127.0.0.1和app@%在 MySQL 眼里是两个完全不同的账号,可以有不同的密码和权限。很多人新建用户时顺手写成'app'@'localhost',然后本地死活连不上,报 1045 或 1130,问题就出在这里。这个设计在安全上很有价值,允许你精细控制"哪个来源可以连",但也确实是新手最容易踩的坑之一。
1.2 五道关卡的先后顺序,决定了你的排查效率
我一般的验证顺序是这样的,每一步都有一个明确的"通过/不通过"信号:
| 顺序 | 检查项 | 验证方式 | 典型现象 |
|---|---|---|---|
| 1 | TCP 可达性 | 本地telnet或nc测 3306 | 不通说明被安全组或防火墙挡了 |
| 2 | 云安全组 | 控制台入方向规则 | 刚建的实例常常一条 3306 规则都没有 |
| 3 | 系统防火墙 | firewall-cmd --list-all或ufw status | 安全组放行了但本机仍然拒绝 |
| 4 | 服务监听地址 | ss -lntp看 3306 绑定在哪个 IP | 只监听 127.0.0.1,外部永远连不上 |
| 5 | 账号与授权 | 查mysql.user的 host 和 plugin | 报 1045、1130、2059 |
按这个顺序走,你可以在两分钟内判断问题卡在哪一层,而不是在 Navicat 里反复改密码、反复重装客户端。下面几节就按这个顺序把每一关拆开讲。
2. 云服务器侧:安全组这道门,比你想的更"严格"
先说一个反直觉的结论:安全组不是"某台服务器的防火墙",它是绑定在实例上的虚拟防火墙,和操作系统里的 iptables、firewalld 完全独立。你在服务器里firewall-cmd --add-port=3306/tcp放行了,安全组没放,外面照样连不上;反过来安全组放行了,系统防火墙没放,也还是连不上。两者都要过,缺一不可。很多人只知道其中一个,于是陷入"我明明放行了啊"的循环。
2.1 入方向规则怎么加:三个字段的含义
在云平台控制台找到实例对应的安全组,添加入方向规则,核心是三个字段:
- 协议类型与端口范围:选自定义 TCP,端口范围填
3306/3306。不建议填1/65535图省事,那等于把整台机器所有服务都暴露了。 - 授权对象:这是安全问题的高发区。填
0.0.0.0/0表示全世界都能来敲你的 3306 端口,虽然 MySQL 还有账号密码这一层,但把数据库端口暴露在公网本身就是把风险敞口放大的做法。更合理的是填你自己的公网出口 IP,写成你的IP/32。 - 优先级:如果同一个安全组里既有拒绝规则又有允许规则,优先级数值小的先生效。默认规则通常是允许,你新增的允许规则填 100 之类的值即可。
怎么知道自己的公网出口 IP?在本地终端执行curl ifconfig.me或者直接在搜索引擎里搜"IP",拿到的那个地址就是。注意家里宽带和公司网络、手机热点的出口 IP 是不一样的,换了网络环境要重新加规则——这也是很多人"昨天还能连今天就不行"的真实原因。
2.2 用"按需开放"替代"长期裸奔"
我的习惯是把 3306 当成一个临时通道来管理:平时不开,需要连的时候加一条只允许自己 IP 的规则,用完删掉。听起来麻烦,但实际操作也就十几秒。如果确实需要长期开放,至少做到两点:一是授权对象锁定到具体 IP 段而不是全网;二是给数据库账号设置强密码并限制权限范围(后面会讲怎么建专用账号)。
这里插一句关于云数据库实例和自建 MySQL 的区别。如果你用的是云厂商托管的数据库服务(比如 RDS),它没有"安全组"这个概念,取而代之的是"白名单"或者"访问控制",配置位置在实例详情页里,填的也是 IP 段。逻辑一模一样,只是入口不同。另外托管实例通常不给你 SSH 到宿主机,所以一旦连不上,你能做的只剩改白名单和改账号,中间的排查空间比自建小得多。这也是我倾向于在开发测试阶段用 ECS 自建、把每一层都摸熟的原因。
2.3 别忘了服务器操作系统自己还有一道防火墙
安全组放行之后,进到服务器上确认系统防火墙的状态。不同发行版命令不一样:
# CentOS / RHEL 系列,firewalld sudo firewall-cmd --state sudo firewall-cmd --list-ports sudo firewall-cmd --permanent --add-port=3306/tcp sudo firewall-cmd --reload # Ubuntu / Debian 系列,ufw sudo ufw status sudo ufw allow 3306/tcp # 直接用 iptables 的情况 sudo iptables -L -n --line-numbers | grep 3306有个容易忽略的点:firewall-cmd加了--permanent之后必须执行--reload才生效,只执行前一条命令,规则写进了配置文件但没进内核,表现就是"命令成功了但端口还是不通"。我见过不止一个人卡在这个地方反复怀疑安全组。
还有一种更隐蔽的情况:某些云服务器的镜像里预装了安全加固脚本或者安装了其他防护组件,会自动下发一套 iptables 规则,你手动加的规则可能被覆盖或者顺序不对。遇到"放行了还是不通"的情况,用iptables -L -n完整看一遍规则链,特别注意有没有 REJECT 或 DROP 出现在你的 ACCEPT 之前。
3. MySQL 服务端:监听地址和账号授权是两码事
过了网络层,问题就进入数据库内部了。这里有两个独立的问题经常被混在一起:服务有没有在对外地址上监听,和账号允不允许来自外部的连接。前者决定了"端口通不通",后者决定了"能不能登录"。两者都要正确,缺一不可。
3.1 bind-address:为什么默认只能本机访问
MySQL 有一个配置项叫bind-address,决定服务监听在哪个网络接口上。如果它是127.0.0.1,那么服务只接受来自本机的连接,你的 Navicat 无论怎么配都连不上,因为 TCP 握手根本建立不起来。不同发行版和不同安装方式下这个默认值不一样,有的包默认绑 127.0.0.1,有的默认绑0.0.0.0(即所有网卡),所以不要假设,一定要去查。
配置文件位置按发行版区分:
# Ubuntu / Debian 通过包管理器安装 /etc/mysql/mysql.conf.d/mysqld.cnf # CentOS / RHEL 通过 yum 安装 /etc/my.cnf # 用 tar 包或容器方式安装的,找实例的 my.cnf找到[mysqld]段落里的这一行:
[mysqld] bind-address = 0.0.0.0改成0.0.0.0表示监听所有网卡。如果出于安全考虑不想全开,也可以指定具体的内网 IP,比如bind-address = 172.16.0.5,这样只有通过内网过来的连接能被接受。改完执行sudo systemctl restart mysqld(或者mysql服务名,视发行版而定)。
重启后用这条命令确认监听状态:
ss -lntp | grep 3306期望看到的是0.0.0.0:3306或者*:3306。如果看到的是127.0.0.1:3306,说明配置没生效——常见原因是改错了文件(存在多个 my.cnf,加载顺序不同)、配置项写在了错误的段落(比如写进了[client])、或者服务重启失败但你没注意。systemctl status mysqld看一下有没有报错。
提示:MySQL 从 8.0 开始,
bind-address如果配置成主机名而不是 IP,启动时会解析失败导致起不来,一律用 IP 地址格式。
3.2 账号的 Host 字段:'%' 不是"所有",而是"除本机外的所有"
这是 MySQL 权限体系里最反直觉的一个细节。mysql.user表里每一行是"用户名 + host"的组合,host 的取值规则是:
| Host 值 | 含义 |
|---|---|
localhost | 只允许通过 Unix socket 或 127.0.0.1 连接 |
127.0.0.1 | 只允许来自本机回环地址的 TCP 连接 |
% | 允许来自任意远程主机,但不包含 localhost |
192.168.1.% | 允许来自该网段的连接 |
203.0.113.10 | 只允许来自这一个 IP |
注意第三行那个"不包含 localhost",这不是 bug 而是设计。MySQL 在匹配账号时,会优先匹配更具体的 host 值。所以如果你只建了'app'@'%'而没建'app'@'localhost',在服务器上用mysql -u app -p通过本地 socket 登录反而可能失败,因为它匹配的是localhost这个 host,而你没有这条记录。反过来同样成立。
创建远程可用账号的正确写法:
-- 创建一个只能从指定网段连接、只能操作业务库的账号 CREATE USER 'app_user'@'203.0.113.%' IDENTIFIED BY '一个足够长的随机密码'; -- 只授业务库的增删改查权限,不给 DROP、不给 GRANT GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app_user'@'203.0.113.%'; -- 如果确实需要建表改表的能力 GRANT CREATE, ALTER, INDEX ON shop.* TO 'app_user'@'203.0.113.%'; -- 查看结果 SHOW GRANTS FOR 'app_user'@'203.0.113.%';我强烈建议不要直接用 root 账号远程连接。root 的权限太大,一旦密码泄露或者客户端被入侵,整台服务器上的所有库都会被波及。正确的做法是按应用建独立账号,只给它需要的那几个库和那几类操作。开发阶段图方便用 root,等上线了又懒得改,这种技术债最后往往以一次事故收尾。
3.3 MySQL 8 的认证插件:那个让人一头雾水的 2059 报错
如果你用的是比较老的 Navicat 版本,连接 MySQL 8.0 时可能遇到这个报错:
2059 - Authentication plugin 'caching_sha2_password' cannot be loaded原因是 MySQL 8.0 把默认认证插件从mysql_native_password换成了caching_sha2_password,安全性更高(用了 SHA-256 加盐挑战应答),但老版本客户端不认识这个插件。有两条路可选:
第一条是升级 Navicat 到较新的版本,让它原生支持caching_sha2_password。这是更推荐的做法,因为你在客户端侧解决了问题,服务端保持更高的安全标准。
第二条是把账号的认证插件改回旧的:
ALTER USER 'app_user'@'203.0.113.%' IDENTIFIED WITH mysql_native_password BY '一个足够长的随机密码'; FLUSH PRIVILEGES;改完用SELECT user, host, plugin FROM mysql.user;确认 plugin 列变成了mysql_native_password。需要提醒的是,MySQL 8.4 及之后的版本已经把mysql_native_password从默认编译选项中去掉了,可能需要在配置里显式启用或者它根本不可用。所以在较新版本上,升级客户端几乎是唯一解。这也是我常跟人说"别为了省事去装来路不明的老版本客户端"的原因——迟早会在某个新版本的服务端上卡住。
还有一个必须澄清的误区:用GRANT或者CREATE USER之后执行FLUSH PRIVILEGES,在绝大多数情况下是不必要的。因为通过 SQL 语句修改权限时,MySQL 会自动重新加载权限表。FLUSH PRIVILEGES只在一种场景下才有意义——你直接用INSERT、UPDATE去改mysql.user表这种底层操作之后。网上大量教程无条件带上这条命令,属于典型的复制传播,照着做没坏处,但如果你以为"不加就无效",那就是理解错了。
3.4 从服务器本地验证,把网络问题和账号问题切开
在动手改 Navicat 之前,先在服务器本机做两个测试,这能帮你快速缩小范围。第一个测试用回环地址连,验证账号本身是否存在、密码是否正确:
mysql -h 127.0.0.1 -P 3306 -u app_user -p如果这一步就失败了,报 1045,那问题百分之百在账号密码或者 host 匹配上,跟网络无关。注意这里特意用-h 127.0.0.1而不是省略 host 参数,因为省略时会走 socket,匹配的是localhost那条记录,测出来的结论不一样。
第二个测试用服务器的内网 IP 连:
mysql -h 172.16.0.5 -P 3306 -u app_user -p这一步能过,说明服务监听没问题、账号 host 也匹配得上,问题就只剩安全组和系统防火墙了。这两步做完,你手里就有了明确的排除结论,而不是继续猜。
4. Navicat 这一端:参数填对了,剩下的都是小选项
前面三层都通了,Navicat 这边其实很简单。但仍有几个字段值得说清楚,因为填错的表现和"连不上"是一样的。
4.1 常规选项卡:五个必填项的真实含义
新建连接选 MySQL,常规选项卡里需要填:
- 连接名:随便起,只影响你自己识别,可以写"生产库-只读"这种描述性的名字,比写"test"有用得多。
- 主机:填服务器的公网 IP。如果走了内网或者堡垒机,填对应的地址。
- 端口:默认 3306。如果你在服务端改过端口,这里要同步改。改了端口之后,安全组规则和系统防火墙规则里的端口号也要一起改,这是很多人改端口后连接失败的完整原因链。
- 用户名 / 密码:对应
mysql.user里的那一条记录,注意用户名不要带@host部分,Navicat 只需要用户名。 - 保存密码:本地个人机器可以勾,共用电脑上别勾。
点"测试连接"之前,建议先用telnet 你的IP 3306或nc -zv 你的IP 3306在命令行测一下端口。命令行通、Navicat 不通,就是账号问题;命令行不通,就别在 Navicat 里浪费时间了。
4.2 SSH 隧道:不暴露 3306 的更稳妥做法
这是我个人最推荐的远程连接方式,尤其适合数据库端口没必要对公网开放的情况。原理是 Navicat 先通过 SSH 连到服务器的 22 端口,建立一条加密通道,然后把 MySQL 流量从这条通道里转发过去。整个过程中,MySQL 的 3306 端口一个安全组规则都不用加,服务端甚至可以继续只监听 127.0.0.1。
配置方式是在连接窗口里切到"SSH"选项卡,勾选"使用 SSH 通道",然后填:
- 主机名或 IP 地址:服务器的公网 IP(和常规选项卡里填的一样)
- 端口:22
- 用户名:服务器上的系统账号,比如
root或者你自己创建的运维账号 - 验证方法:密码或公钥。生产环境建议用公钥,把自己本地生成的私钥文件选上,服务器上放好对应的公钥
配置好之后回到常规选项卡,此时主机填127.0.0.1,端口填 3306。因为对 MySQL 来说,连接是从服务器本机发起的,走的是回环地址。这个细节是新手最容易搞错的地方——他们习惯性地把公网 IP 又填一遍,结果隧道建起来了但 MySQL 连接指向了错误的目标。
用 SSH 隧道有几个实打实的好处:数据库端口不需要对公网开放,攻击面大幅缩小;传输全程加密,中间链路上看不到明文 SQL;安全组规则可以保持最小化。代价是每次连接多一次 SSH 握手,首次连接会慢个一两秒,日常使用基本无感。
4.3 高级选项里那几个值得调的参数
Navicat 的"高级"选项卡里有几个参数在长连接场景下很有用:
- 保持连接间隔:默认可能是 240 秒。这个值的意思是客户端每隔这么久发一次心跳包,防止中间的网络设备(NAT、负载均衡)把空闲连接掐掉。如果你经常遇到"连上之后放着不动,过一会儿执行语句就报连接已断开",把这个值调小到 60 到 120 秒试试。
- 连接超时:控制 TCP 连接建立的等待时间。网络质量差的时候适当放大,但别设得太离谱,否则真连不上时你要等很久才看到报错。
- 编码:确保选 UTF-8。如果服务端是 MySQL 8,字符集默认是 utf8mb4,Navicat 这边选 UTF-8 就能正常显示中文和 Emoji 之类的四字节字符。
- 使用压缩:跨地域连接、需要传输大量数据时勾上,能明显减小传输量。代价是客户端和服务端都要多做一层压缩解压,CPU 会多消耗一点。局域网内没必要开。
还有一个跟显示相关的小坑:如果你发现 Navicat 里查出来的时间比实际时间差 8 小时,那不是编码问题,是时区问题。MySQL 服务端如果用的是 UTC,而你的客户端按本地时区解析,就会出现这个偏差。解决方式有两种,一是在服务端配置default-time-zone = '+08:00',二是在 Navicat 连接的高级设置里手动指定时区。选哪种取决于你的表里存的是 UTC 还是本地时间,这个要在项目开始前定好,中途改会牵连历史数据。
5. 报错现场还原:四种典型错误码的定位路径
光看正确配置容易记不住,反过来从报错入手反而更快形成条件反射。下面这几种报错我在实际环境里都遇到过,把它们串成一条排查链路,比死记配置项有用。
5.1 2003 / 10060:连不上,问题在网络层
完整报错通常是2003 - Can't connect to MySQL server on 'x.x.x.x' (10060)。10060 是连接超时的意思,说明 TCP 握手就没成功,报错根本没走到认证环节。定位路径很清晰:
第一步,本地命令行nc -zv x.x.x.x 3306。如果超时,往下走。
第二步,去控制台看安全组入方向有没有 3306 的规则,授权对象对不对。特别注意你当前的公网 IP 是不是变了——切换到手机热点测一下,如果热点能连而公司网络不能,那基本就是公司出口 IP 和安全组里登记的 IP 不一致。
第三步,登服务器看系统防火墙。前面说过firewall-cmd --permanent之后要--reload。
第四步,ss -lntp | grep 3306看监听地址。如果是127.0.0.1:3306,回去改bind-address。
这四步走完,2003 基本必解。
5.2 1045:能连上但被拒绝,问题在账号
1045 - Access denied for user 'app_user'@'203.0.113.10' (using password: YES)注意报错里的@后面那一串——那是服务端识别到的来源 IP,不是你在 Navicat 里填的任何东西。这个信息非常有用,因为它告诉你服务端到底把你认成了谁。如果你mysql.user表里只有'app_user'@'192.168.%',而报错显示来源是203.0.113.10,那就是 host 不匹配,需要补一条对应的记录。
另外注意using password: YES/NO这个提示。如果显示 NO,说明你压根没传密码(比如 Navicat 里密码框是空的),那是另一个方向的问题。
还有一种情况是密码里有特殊字符,在 Navicat 里输入没问题,但你在命令行里用-p交互输入时因为 shell 转义出了岔子,导致你以为密码错了其实没错。排查时建议直接在 MySQL 里重新设一次密码,避免在两侧反复比对:
ALTER USER 'app_user'@'%' IDENTIFIED BY '新密码';5.3 1130:Host is not allowed to connect
1130 - Host '203.0.113.10' is not allowed to connect to this MySQL server这个比 1045 更直白,意思是这个来源 IP 连匹配的账号记录都没有。通常有两种成因:一是你建账号时 host 写成了localhost或者127.0.0.1;二是你从没建过远程账号,一直在用 root 并且 root 只允许本机。
解决方式就是补一条记录,或者在已有的记录上改 host:
-- 查看现有账号 SELECT user, host FROM mysql.user WHERE user = 'app_user'; -- 如果已有一条 'app_user'@'localhost',改 host 为 % RENAME USER 'app_user'@'localhost' TO 'app_user'@'%';用RENAME USER而不是直接UPDATE mysql.user,好处是它会同步更新所有相关的权限表,不用手动FLUSH PRIVILEGES,也更不容易出错。
5.4 连上了但慢、或者用一会儿就断
这类问题不算报错,但特别影响体验,我把它单独列出来。三种常见原因:
一是DNS 反向解析拖慢认证。MySQL 在认证阶段可能会尝试把来源 IP 反解析成域名,如果服务器配置的 DNS 不可达或响应慢,每次连接都要等好几秒。解决办法是在服务端配置里加上skip-name-resolve,跳过反解析。注意加了之后,mysql.user表里的 host 字段就必须全部写成 IP 或%,不能再写主机名,否则会匹配不上。
二是空闲连接被中间设备回收。运维同事经常抱怨"下午连上写了个查询,晚上回来执行就报连接已断开",这就是连接被 NAT 或者负载均衡超时清理了。Navicat 里把保持连接间隔调小就能缓解。
三是跨地域的网络延迟。如果服务器在华北而你人在华南,每次操作多几十毫秒是正常的。这种情况下尽量少做"一次拉几十万行数据然后全量显示"的操作,改用条件查询分批取,或者配合 SSH 隧道开启压缩。
6. 把连接这件事做成可持续的习惯,而不是每次都现查
配置一次连通不算难,难的是半年后再回来处理同类问题还能快速上手,以及让这个连接方式在安全上站得住。这部分讲讲我在长期维护中形成的几个做法。
6.1 账号要能一眼看出用途和来源
我的命名习惯是用途_环境@来源,比如shop_dev@203.0.113.10、shop_ro@%。这么做的好处是,当你一年后打开mysql.user表,能立刻判断出哪个账号是干嘛的、能不能删。我见过太多环境里躺着一堆test、user1、root2这样的账号,谁都不敢删,因为不知道删了会不会影响线上。
清理时配合这两条查询:
-- 找出所有非本地来源的账号 SELECT user, host, plugin FROM mysql.user WHERE host NOT IN ('localhost', '127.0.0.1'); -- 看某个账号到底有什么权限 SHOW GRANTS FOR 'shop_ro'@'%';定期做这件事的价值不只是安全,也能在排查问题时排除掉"是不是被某个残留账号影响"这种可能性。
6.2 能用隧道就不要暴露端口
回到前面那个建议:如果只是你个人需要偶尔连一下开发库,SSH 隧道几乎永远是最优解。它把"安全组最小化"和"传输加密"这两件事一次解决,配置成本也就是第一次填几个字段。真正需要长期开放 3306 的场景其实不多,通常是应用服务器和数据库分离部署、又不在同一个内网网段的时候——而这种情况更推荐的做法是打通两个机器的内网,让流量走内网而不是公网。
一个判断标准:如果你能接受"每次连接时先打开一个 SSH 会话",那就说明你其实不需要公网暴露 3306。
6.3 把这几个命令固化成肌肉记忆
最后分享几个我在任何一台陌生的数据库服务器上都会先跑一遍的命令,它们能让你在一分钟内摸清这台机器的连接状态:
# 1. 服务有没有起、监听在哪 ss -lntp | grep -E '3306|mysql' # 2. 防火墙放行了哪些端口 sudo firewall-cmd --list-ports 2>/dev/null || sudo ufw status # 3. 从本机测目标端口是否可达(在没有 telnet 的机器上更实用) nc -zv 47.98.x.x 3306 # 4. 走 MySQL 协议层看服务是否真的在响应 mysql -h 127.0.0.1 -u root -p -e "SELECT VERSION(), @@bind_address, @@port;"第四条特别有用,它一次就能告诉你 MySQL 的版本、实际生效的监听地址和端口。很多配置文件的书写格式在不同发行版里有细微差别,与其对着文件猜,不如直接问服务端它现在到底是什么状态。
我在实际使用中的体会是,数据库连接这类问题的难点从来不在技术本身,而在于链路太长、报错信息又太少,一个"连不上"背后可能是五层里的任意一层。一旦你养成了按层级验证的习惯,并且知道每层用什么命令去确认,这类问题就会从"玄学"变成"流程"。真正让人难受的从来不是问题难,而是不知道从哪儿下手。