MySQL连接数爆满时,运维第一时间该做什么?

admin 2026-08-14 08:09:22 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: MySQL连接数爆满时切忌盲目调大参数,应先查状态定位占用连接最多的IP与库。止血须先从网关切断问题流量,再按条件安全清理长时间空闲的连接。恢复后需排查应用连接池配置是否超限,以及代码是否遗漏行关闭或事务回滚,最后才考虑调整数据库参数。 综合评分: 93 文章分类: 应急响应,安全运营


cover_image

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 连接数爆满时,运维第一时间该做什么?》

评论:0   参与:  0