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

admin 2026-10-23 06:44:03 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

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\
\",
   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\
\", 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,这次可能能糊过去,下次还会在凌晨把你叫起来。


MySQL连接数爆满时,运维第一时间该做什么?" 网络安全文章

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

MySQL连接数爆满时切忌盲目调大参数,应先查状态定位占用连接最多的IP与库。止血须先从网关切断问题流量,再按条件安全清理长时间空闲的连接。恢复后需排查应用连接池配置是否超限,以及代码是否遗漏行关闭或事务回滚,最后才考虑调整数据库参数。
分析一个简单的goroutine资源池" 网络安全文章

分析一个简单的goroutine资源池"

针对Go无节制创建goroutine导致线上卡死的问题,本文指出需建立并发边界。作者基于channel实现轻量级资源池,强调三处关键设计:任务提交须快速失败或设超时防阻塞,worker内须recover防脏数据打挂,关闭时须先close再Wait。建议按任务特性配置worker数,通过池化约束并发以保障系统稳定。
FuckMyShitMountain:项目能跑不算完,先让AI下屎山挖一遍" 网络安全文章

FuckMyShitMountain:项目能跑不算完,先让AI下屎山挖一遍"

本文介绍了一个名为FuckMyShitMountain的代码审计工具,该工具为编程Agent提供skill以增强审计能力。它能自动分析项目结构、依赖、CI配置并生成包含严重程度、置信度、影响、修复建议和回归测试的详细报告。工具覆盖27个审计维度,包括安全、稳定性、性能、测试真实性、供应链及LLM成本等,并支持增量审计用于PRreview。作者强调其优势在于承认证据不足,标记未评估项,且支持Markdown、HTML、JSON输出以便集成到CI流程。同时指出AI审计依赖上下文理解和项目可运行性,不能替代人工
评论:0   参与:  0