文章总结: MySQL连接数爆满时切忌盲目调大参数,应先查状态定位占用连接最多的IP与库。止血须先从网关切断问题流量,再按条件安全清理长时间空闲的连接。恢复后需排查应用连接池配置是否超限,以及代码是否遗漏行关闭或事务回滚,最后才考虑调整数据库参数。 综合评分: 93 文章分类: 应急响应,安全运营
MySQL 连接数爆满时,运维第一时间该做什么?
原创
go go
Go语言教程
2026年7月20日 13:31 陕西
在小说阅读器读本章
去阅读
MySQL 直接甩出一句:
ERROR 1040 (HY000): Too many connections
应用日志也跟着炸:
dial tcp 10.12.8.21:3306: connect: connection refused
sql: database is closed
Error 1040: Too many connections
这种时候我最烦听到一句话:把 max_connections 调大点。
这话不能说错,但放在第一步,基本就是给事故续命。连接数爆满,第一件事不是改参数,也不是重启 MySQL,而是先确认这些连接到底是谁占着。
先保现场。
能登进去就马上看这几条:
show global status like 'Threads%';
show variables like 'max_connections';
show full processlist;
重点看两个值:
Threads_connected 798
Threads_running 12
max_connections 800
如果 Threads_connected 很高,Threads_running 不高,大概率不是 MySQL 正在拼命干活,而是一堆连接挂着没释放。
这类事故我一般先不翻业务代码。先看连接从哪里来。
select
user,
substring_index(host, ':', 1) as ip,
db,
command,
count(*) cnt
from information_schema.processlist
group by user, substring_index(host, ':', 1), db, command
order by cnt desc
limit 20;
现场经常长这样:
user ip db command cnt
order_app 10.12.3.41 mall Sleep 312
order_app 10.12.3.42 mall Sleep 287
admin_job 10.12.9.18 mall Query 64
看到这里就别急着杀。
Sleep 多,不一定全是坏连接。连接池本来就会保留空闲连接。但如果某两台机器突然占了几百个,而且业务正好刚发版,那我第一眼就不太信它是正常波动。
再补一刀,看睡了多久:
select id, user, host, db, command, time, state, left(info, 120) sql_text
from information_schema.processlist
where command = 'Sleep'
order by time desc
limit 30;
如果一堆连接 Sleep 几千秒,这时候可以先做止血。
注意,是止血,不是根治。
我一般会先从网关、K8s、Nginx 或应用层把入口流量压一下,避免新连接继续往 MySQL 上砸。比如先把有问题的实例摘掉,或者把定时任务暂停。数据库已经顶满了,还让它继续接客,就跟地上漏水你还开水龙头一样。
然后再清理明显没价值的连接。
手工杀当然可以:
kill 23811;
kill 23812;
但线上几十几百个连接,一个个敲很容易敲错。我更习惯先写一个小工具,只处理指定用户、指定库、指定空闲时间以上的连接,并且默认只打印,不真正 kill。
下面这段 Go 代码就是干这个用的。不是给你做平台的,就是事故现场临时拿来扫一眼。
package main
import (
"context"
"database/sql"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
)
type ConnRow struct {
ID int64
User string
Host string
DB sql.NullString
Command string
TimeSec int64
}
func main() {
dsn := os.Getenv("MYSQL_DSN")
if dsn == "" {
log.Fatal("missing MYSQL_DSN, example: ops:pwd@tcp(10.12.8.21:3306)/information_schema")
}
targetUser := getenv("MYSQL_KILL_USER", "order_app")
targetDB := getenv("MYSQL_KILL_DB", "mall")
minSleep := mustInt(getenv("MIN_SLEEP_SECONDS", "300"))
apply := os.Getenv("APPLY_KILL") == "1"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, `
select id, user, host, db, command, time
from information_schema.processlist
where command = 'Sleep'
and user = ?
and db = ?
and time >= ?
order by time desc
limit 200
`, targetUser, targetDB, minSleep)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var victims []ConnRow
for rows.Next() {
var r ConnRow
if err := rows.Scan(&r.ID, &r.User, &r.Host, &r.DB, &r.Command, &r.TimeSec); err != nil {
log.Fatal(err)
}
victims = append(victims, r)
}
for _, v := range victims {
ip := strings.Split(v.Host, ":")[0]
fmt.Printf("candidate id=%d user=%s db=%s host=%s sleep=%ds\n",
v.ID, v.User, v.DB.String, ip, v.TimeSec)
if apply {
if _, err := db.ExecContext(ctx, fmt.Sprintf("kill %d", v.ID)); err != nil {
log.Printf("kill id=%d failed: %v", v.ID, err)
continue
}
fmt.Printf("killed id=%d\n", v.ID)
}
}
if !apply {
fmt.Println("dry-run only, set APPLY_KILL=1 to kill")
}
}
func getenv(k, d string) string {
v := os.Getenv(k)
if v == "" {
return d
}
return v
}
func mustInt(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
log.Fatalf("bad int: %s", s)
}
return n
}
执行时我会先 dry-run:
MYSQL_DSN='ops:pwd@tcp(10.12.8.21:3306)/information_schema' \
MYSQL_KILL_USER='order_app' \
MYSQL_KILL_DB='mall' \
MIN_SLEEP_SECONDS=600 \
go run mysql_conn_clean.go
确认候选连接没问题,再开 kill:
APPLY_KILL=1 go run mysql_conn_clean.go
这里有个坑,别杀复制账号,别杀备份账号,别杀正在跑核心变更的连接。Sleep 也不是免死金牌,但至少比盲杀 Query 安全一点。
连接数缓下来以后,再看真正的源头。
我会回到应用机器看连接池配置。Go 服务里最常见的是这个:
db.SetMaxOpenConns(200)
db.SetMaxIdleConns(200)
db.SetConnMaxLifetime(0)
单个服务 200 个连接,部署 8 个实例,就是 1600 个。MySQL 配 800,迟早顶穿。
更稳一点的写法应该有边界:
func tuneDBPool(db *sql.DB) {
db.SetMaxOpenConns(40)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(25 * time.Minute)
db.SetConnMaxIdleTime(3 * time.Minute)
}
这个值不能拍脑袋。要看实例数、MySQL max_connections、后台任务、运维账号、只读实例分流情况。
比如 MySQL 允许 800 个连接,线上有 10 个应用实例,我不会让每个实例都开 80。还得给管理连接、任务连接、突发连接留空间。一般先压到一个保守值,再看 Threads_running、接口耗时和慢 SQL。
还有一种更阴的情况:连接不是池子开太大,而是代码没关。
Go 里尤其要盯这几种:
rows, err := db.QueryContext(ctx, sqlText, uid)
if err != nil {
return err
}
defer rows.Close()
少了 rows.Close(),连接可能一直还不回池子。
事务也一样:
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()
只 Begin,异常路径没 Rollback,这连接就会被事务吊住。事故现场看到一堆长事务,我基本先怀疑这块。
最后才考虑临时调大 MySQL:
set global max_connections = 1200;
这一步不是不能做,但要看机器内存、当前 SQL 压力、连接来源。连接多了以后,每个连接都要消耗资源。你把门开大,进来的可能不是救兵,是更多请求。
我的顺序一般就这样:
先压入口,保住 MySQL 不继续被打满。
再查连接分布,找出哪个用户、哪个 IP、哪个库占得最多。
再清理长时间空闲连接,动作要小,别一把梭。
然后回应用查连接池、事务、rows.Close()、慢 SQL 和定时任务。
参数最后动。
连接数爆满不是一个数据库问题,它通常是应用、连接池、发布、任务、慢 SQL 几个东西一起挤出来的。只盯 max_connections,这次可能能糊过去,下次还会在凌晨把你叫起来。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Go语言教程 go go《MySQL 连接数爆满时,运维第一时间该做什么?》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论