服务器数据库连接超时怎么办

结论: 服务器数据库连接超时要先 classify:能否建立连接看 max_connections 与安全组端口,查询慢看慢日志与锁等待,跑了一段突然断开看 wait_timeout 与连接池回收策略。80% 的数据库连接超时来自连接池耗尽与慢查询占满连接,而不是数据库宕机;配置 skip-name-resolve 可消除 DNS 反解析导致的首次连接超时。

引言:报错是一样的,原因可以差三条街

应用侧抛出的往往只是一行抽象异常——Java 的 java.net.SocketTimeoutException: connect timed out、Python 的 pymysql.err.OperationalError: (2003, "Can't connect to MySQL server")、Communications link failure,或经典的 Too many connections。服务器数据库连接超时怎么办这件事难就难在:同一个报错可能出现在网络层、数据库层、连接池层三个完全不同的位置,而在错误的那一层排查,可以耗掉一整天。

一个典型误区是"超时就加连接数"。把连接池从 50 提到 500,往往让数据库并发上升十倍,锁等待加剧、响应更慢,最终把所有请求拖成一锅粥。正确的顺序永远是:先看数据库侧的连接态势,再回溯是应用在"浪费"连接,还是 SQL 本身太慢。

本文以 MySQL 8.0/8.4 为主要示例,PostgreSQL 的思路一致(对应 max_connections、statement_timeout、idle_in_transaction_session_timeout),文中会给出对照。

第一步:给超时分类,避免瞎猜

结论: 先确认超时发生在 TCP 建连、首次握手认证、还是查询执行/空闲阶段——看异常里的动词即可初步判断,不同类别的排查路径完全不同。

三类超时的特征对照表

异常特征超时类别首要怀疑对象第一手证据
connect timed out / 3306 端口不通建立连接超时安全组、防火墙、mysqld 未监听公网、网络质量nc -vz ip 3306、telnet、ping
Too many connections (1040)连接池/连接数耗尽max_connections 满、连接未释放Threads_connected、show processlist
首次连接慢 3~10 秒,之后变快DNS 反解析MySQL 对客户端 IP 做反向 DNS 查询未配置 skip-name-resolve
查询几秒到几十秒后超时返回慢查询 / 锁等待缺失索引、大表扫描、行锁等待慢日志、show engine innodb status
长时间空闲后第一次请求失败空闲连接被回收wait_timeout(默认 28800 秒)、interactive_timeout、连接池未探活show variables like '%timeout%'
报 read timed out 且随机出现网络抖动 / TCP 重传跨可用区访问、安全组会话老化、keepalive 未开tcpdump、mtr、内核 tcp_retries2

三个立刻可以跑的判断命令

bash
# 1) 端口通不通(注意:这是 TCP 层,与账号密码无关)
nc -vz 10.0.0.10 3306
timeout 3 bash -c 'cat < /dev/null > /dev/tcp/10.0.0.10/3306' && echo OK || echo FAIL

# 2) 应用服务器上抓最近的连接错误量
ss -antp | grep 3306 | awk '{print $1}' | sort | uniq -c

# 3) 数据库侧看总体连接态势
mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Threads_%'; SHOW GLOBAL VARIABLES LIKE 'max_connections';"

第二步:查连接池是否真的耗尽

结论: 用 Threads_connected / max_connections 算占用率,超过 80% 基本可以判定是连接池耗尽;再用 show processlist 看这些连接处于 Sleep 还是 Query 状态,据此判断是"应用没释放"还是"SQL 太慢"。

sql
-- 连接数全局态势
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL VARIABLES LIKE 'max_connections';

-- 占用率粗算:超过 80% 应扩容连接或治理连接泄漏
SELECT
  (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Threads_connected') AS connected,
  (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME='max_connections') AS max_conn,
  ROUND((SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Threads_connected')
      / (SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME='max_connections') * 100, 2) AS pct;

-- 按状态统计:Sleep 多 = 应用端未释放;Query 多 = SQL 慢
SELECT command, time, state, count(*) AS cnt
FROM information_schema.processlist
GROUP BY command, time, state ORDER BY cnt DESC LIMIT 20;

-- 揪出睡眠最久的空闲连接
SELECT id, user, host, db, command, time, state, LEFT(info,120) AS sql_text
FROM information_schema.processlist
WHERE command='Sleep' ORDER BY time DESC LIMIT 10;

判别口诀:Sleep 多且 time 很大 → 连接池配置问题(未设 maxLifetime / idleTimeout,或事务未提交不释放);大量处于 Sending data / Locked / statistics 状态 → SQL 与锁的问题;全是同一个 host 打满 → 某个实例的连接池上限被调得过大。

临时救火手段:

sql
-- 批量杀掉超过 300 秒的空闲连接(先 SELECT 确认,再拼 KILL)
SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist
WHERE command='Sleep' AND time > 300 AND user NOT IN ('system user','event_scheduler');

需要提醒的是,KILL 是止血不是治疗。根治方向有两个:让应用把连接还回来(连接池按业务峰值合理设值、事务边界收缩、finally 里保证释放),或让数据库能撑住(提高 max_connections 前先估内存:每个连接约占用 4~10MB,max_connections=1000 意味着额外预留数 GB)。

第三步:慢查询与锁等待

结论: 连接被慢 SQL 长时间占用,比连接数本身更容易造成超时;定位方式是开慢日志并用 performance_schema/sys schema 找 Top SQL,再看是否被行锁或元数据锁(MDL)阻塞。

sql
-- 慢日志开关与阈值(MySQL 8.x)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = ON;
SHOW VARIABLES LIKE 'slow_query%';

-- 用 sys schema 直接看最耗时语句
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile LIMIT 10;
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;

-- 实时看当前正在跑且超过 2 秒的查询
SELECT id, user, host, db, time, state, LEFT(info,200) AS q
FROM information_schema.processlist
WHERE command <> 'Sleep' AND time > 2 ORDER BY time DESC;

锁等待是慢查询的孪生兄弟,MySQL 8.0 用 performance_schema 查,MySQL 5.7 用 information_schema:

sql
-- MySQL 8.0:谁在等谁
SELECT waiting_pid, waiting_query, blocking_pid, blocking_query, wait_age
FROM sys.innodb_lock_waits LIMIT 20;

-- 元数据锁(MDL)阻塞:常见于 ALTER TABLE 卡住全表业务
SELECT * FROM performance_schema.metadata_locks WHERE OWNER_THREAD_ID IS NOT NULL LIMIT 20;

-- InnoDB 事务视图
SELECT trx_id, trx_state, trx_started, trx_wait_started, trx_rows_locked, LEFT(trx_query,120) AS q
FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 20;

根治手段依次是:为 WHERE/JOIN/ORDER BY 涉及的列补复合索引并符合最左前缀;避免长事务(把非数据库操作移出事务边界);大表 DDL 使用 pt-online-schema-change 或 MySQL 8.0 的 ALGORITHM=INSTANT;必要时把大查询拆到只读实例。

第四步:网络、安全组与 DNS 反解析

结论: 能解析但连不上,先查云安全组与系统防火墙是否放行 3306/5432;首次连接特别慢则是 MySQL 在做反向 DNS 解析,加 skip-name-resolve 并用 IP 授权即可解决。

bash
# 服务端确认监听地址:127.0.0.1 表示只允许本机,外部当然连不上
ss -lntp | grep -E '3306|5432'
my_print_defaults --mysqld | grep -E 'bind-address|skip-name-resolve|max_connections'

# 防火墙 / 安全组
firewall-cmd --list-all
iptables -L -n -v | grep 3306
# 云安全组需在控制台放行:来源 IP/安全组 + 协议 TCP + 端口 3306/5432

MySQL 的 DNS 反解析是隐蔽杀手:客户端连入时 mysqld 默认会调 gethostbyaddr 反查主机名,若 DNS 慢或不可达,单次连接可卡 3~10 秒。修复方式是在 [mysqld] 段加入 skip-name-resolve,并把所有账号的 host 改成 IP 段(因为关闭后不再解析主机名,'user'@'web.example.com' 这类授权会失效):

ini
# /etc/my.cnf 或 /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
skip-name-resolve
max_connections        = 500
wait_timeout           = 600
interactive_timeout    = 600
connect_timeout        = 10
max_connect_errors     = 1000
innodb_lock_wait_timeout = 30

# 绑定监听地址:0.0.0.0 表示所有网卡;仅本机访问时应保持 127.0.0.1
[mysqld]
bind-address = 0.0.0.0

改完用 SELECT user, host FROM mysql.user; 确认没有基于域名的授权再 systemctl restart mysqld。

第五步:应用侧连接池参数怎么配

结论: 连接池大小不是越大越好,经验公式是"连接池 ≤ CPU 核数 × 2 + 磁盘数"(对 OLTP 而言 20~50 通常已足够);必须开启空闲检测与连接最大存活时间,避免被 wait_timeout 悄悄掐断。

yaml
# Spring Boot + HikariCP(application.yml)
spring:
  datasource:
    hikari:
      maximum-pool-size: 30
      minimum-idle: 5
      connection-timeout: 3000        # 等待获取连接上限,超时报 SQLTransientConnectionException
      validation-timeout: 3000
      idle-timeout: 300000            # 5 分钟空闲回收,必须 < 数据库 wait_timeout
      max-lifetime: 1500000           # 25 分钟强制重建,小于设备/数据库侧超时
      keepalive-time: 120000
      leak-detection-threshold: 60000 # 连接泄漏检测,生产排障利器
      connection-test-query: SELECT 1
properties
# Druid 连接池关键参数(druid.properties)
druid.maxActive=30
druid.minIdle=5
druid.maxWait=3000
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
druid.validationQuery=SELECT 1
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000
druid.removeAbandoned=true
druid.removeAbandonedTimeout=180
druid.logAbandoned=true

一致原则是:连接池的最大生命周期要小于数据库 wait_timeout 与中间网络设备的空闲连接老化时间,否则就会出现"半夜没人访问,早上第一笔请求失败"的经典现象。若跨云/跨可用区访问数据库,建议再打开 TCP keepalive:net.ipv4.tcp_keepalive_time=600、tcp_keepalive_probes=3、tcp_keepalive_intvl=15。

常见误区 / 排错提示

  1. 一上来就把 max_connections 调到 2000:每个连接都有内存开销,盲目提高会导致 OOM 与更严重的上下文切换。应先治理连接泄漏与慢 SQL,再按峰值评估。
  2. 把 wait_timeout 设成超大值图省事:长空闲连接会累积占用内存与句柄,且与连接池的 maxLifetime 冲突时会互相覆盖,更稳妥是两端对齐而不是拉到无穷大。
  3. 只调应用侧超时,不管服务端:应用请求超时 30 秒而数据库 innodb_lock_wait_timeout 是 50 秒,客户端早已放弃、服务端还在跑,堆积后会引发雪崩。应保证超时自下而上逐级收紧。
  4. 用 root 账号压测连接数:root 在有 SUPER 权限时额外保留一个管理连接,用它能连上不代表普通账号也能连上,压测应使用业务账号。
  5. 忽略 max_connect_errors 触发的 host blocked:网络抖动或密码错误连续超过阈值后,MySQL 会屏蔽该主机,报错为 Host 'x.x.x.x' is blocked,需 mysqladmin flush-hosts 或提高阈值。