MySQL 8.4 LTS:会话与线程的 1:1 关系,以及如何与操作系统线程绑定排查

本文基于 MySQL 8.4.10 LTS 实例实测验证,所有数据均来自真实环境查询结果,可直接在 8.4 上复现。

前言

在 MySQL 的日常运维与性能排查中,有三个问题几乎必然遇到:

  1. 数据库里的"会话"和"线程"到底是什么关系?
  2. 每次建立一个连接,服务器会不会新建一个线程?
  3. ps -T -p <mysqld_pid> 看到的操作系统线程,怎么和数据库里的会话对上号?

本文用一张 performance_schema.threads 表,把「会话 → MySQL 线程 → OS 线程」三层关系一次讲透,并给出可直接复制执行的排查 SQL。


一、核心结论:默认 1 个会话 = 1 个前台线程

MySQL 8.4(社区版)默认的线程处理方式是 thread-per-connection,由系统变量确认:

SELECT VERSION(), @@thread_handling, @@thread_cache_size, @@max_connections;

-- 实测结果:
mysql> SELECT VERSION(), @@thread_handling, @@thread_cache_size, @@max_connections;
+-----------+---------------------------+---------------------+-------------------+
| VERSION() | @@thread_handling         | @@thread_cache_size | @@max_connections |
+-----------+---------------------------+---------------------+-------------------+
| 8.4.10    | one-thread-per-connection |                   9 |               151 |
+-----------+---------------------------+---------------------+-------------------+
1 row in set (0.00 sec)

thread_handling = one-thread-per-connection 意味着:

  • 建立连接时:客户端完成 TCP/握手 → 认证通过 → 服务器为该连接创建一个专用前台线程TYPE='FOREGROUND',也叫会话线程/连接线程)。
  • 连接存活期间:该连接上的所有 SQL(解析、优化、执行、返回)都由这一个线程全程独占执行,不会中途换线程。
  • 连接断开时:线程不一定销毁——优先进入线程缓存(thread cache) 等待下一个连接复用(见第四节)。

例外:仅 MySQL Enterprise 版 提供 Thread Pool 插件,启用后多会话共享少量工作线程,不再是 1:1。社区版 8.4 没有线程池,永远是 1:1。

实时数据对账:数量严格一一对应

用性能数据交叉验证(同一时刻抓取):

SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME LIKE 'Threads%'
ORDER BY VARIABLE_NAME;
指标 实测值 含义
Threads_connected 11 当前客户端连接数
Threads_running 2 正在执行语句的线程数
Threads_created 12 自启动以来累计创建的客户端线程数
Threads_cached 1 缓存中的空闲线程数

performance_schema.threads 里数前台客户端连接:11 个(10 个 Sleep + 1 个 Query),与 Threads_connected = 11 逐一对上

Threads_created = 12 = 11(在用)+ 1(缓存) 更是直接证明了:新会话 ≠ 必新建线程,优先复用缓存线程


二、三层 ID 体系:一张表看懂

一个会话在 MySQL 内部涉及 三个不同的 ID,全在 performance_schema.threads 表里,务必区分:

叫什么 代表什么 举例(实测)
THREAD_ID MySQL 内部线程号 服务器内部分配的线程标识 83
PROCESSLIST_ID 会话/连接 ID 等于 CONNECTION_ID(),与 information_schema.processlist.ID 一致 49
THREAD_OS_ID 操作系统线程 ID Linux 内核线程号(gettid / LWP) 4080

三者关系:一个会话在存活期间,THREAD_IDPROCESSLIST_ID 一一对应;THREAD_OS_ID 则把 MySQL 线程映射到操作系统线程——这正是第三节"绑定查看"的钥匙。

自证实验:当前会话的三层 ID

SELECT CONNECTION_ID() AS my_connection_id,
       t.THREAD_ID, t.PROCESSLIST_ID, t.TYPE,
       t.THREAD_OS_ID, t.CONNECTION_TYPE, t.PROCESSLIST_USER
FROM performance_schema.threads t
WHERE t.PROCESSLIST_ID = CONNECTION_ID();
mysql> SELECT CONNECTION_ID() AS my_connection_id,
    ->        t.THREAD_ID, t.PROCESSLIST_ID, t.TYPE,
    ->        t.THREAD_OS_ID, t.CONNECTION_TYPE, t.PROCESSLIST_USER
    -> FROM performance_schema.threads t
    -> WHERE t.PROCESSLIST_ID = CONNECTION_ID();
+------------------+-----------+----------------+------------+--------------+-----------------+------------------+
| my_connection_id | THREAD_ID | PROCESSLIST_ID | TYPE       | THREAD_OS_ID | CONNECTION_TYPE | PROCESSLIST_USER |
+------------------+-----------+----------------+------------+--------------+-----------------+------------------+
|               35 |        83 |             35 | FOREGROUND |         4080 | TCP/IP          | admin            |
+------------------+-----------+----------------+------------+--------------+-----------------+------------------+
1 row in set (0.00 sec)

→ 我发起查询的这个会话,CONNECTION_ID() 正好等于 PROCESSLIST_ID,对应唯一 THREAD_ID=83,并在 OS 层对应线程 4080


三、操作系统线程绑定查看(重点)

3.1 8.4 的重要变化:列名改了!

MySQL 8.4 把 8.0 的 OS_THREAD_ID 列重命名为 THREAD_OS_ID

用 8.0 的旧文档写 WHERE OS_THREAD_ID = ... 会直接报错:

ERROR: Unknown column 'OS_THREAD_ID' in 'field list'

在 8.4.10 上核对列清单:

SELECT COLUMN_NAME FROM information_schema.columns
WHERE TABLE_SCHEMA='performance_schema' AND TABLE_NAME='threads';
-- ... THREAD_OS_ID bigint unsigned ...

(8.4 的 threads 表还新增了 CONTROLLED_MEMORYTOTAL_MEMORYTELEMETRY_ACTIVE 等内存治理相关列,感兴趣可以自己 DESC performance_schema.threads 查看。)

3.2 绑定原理

MySQL 在 Linux 上把每个内部线程的内核线程 IDgettid() 返回的 LWP 号)写入 THREAD_OS_ID,而 ps -T -p <PID> 输出的 SPID/LWP 列恰好就是同一个值

ps -T 的 SPID 列  <══ 一一对应 ══>  performance_schema.threads.THREAD_OS_ID

3.3 完整操作步骤(在 MySQL 服务器上执行)

第 1 步:拿到 mysqld 进程 PID

pgrep -x mysqld          # 假设得到 1234

第 2 步:OS 侧列出所有线程(注意看 SPID 列)

ps -T -p 1234 -o pid,tid,pcpu,comm

# 按 CPU 排序找热点线程:
ps -eLo pid,tid,pcpu,comm --sort=-pcpu | grep mysqld
# 或实时观察:
top -H -p 1234

第 3 步:DB 侧输出绑定清单

SELECT PROCESSLIST_ID, THREAD_ID, THREAD_OS_ID, TYPE,
       PROCESSLIST_USER, PROCESSLIST_DB, PROCESSLIST_COMMAND,
       PROCESSLIST_STATE, PROCESSLIST_INFO
FROM performance_schema.threads
WHERE TYPE = 'FOREGROUND' AND PROCESSLIST_ID IS NOT NULL
ORDER BY THREAD_OS_ID;

第 4 步:对照:把第 2 步 ps -TSPID 与第 3 步的 THREAD_OS_ID 逐行对上,就得到完整链路:

OS 线程(SPID=4080) → MySQL线程(THREAD_ID=83) → 会话(CONNECTION_ID=49) → 用户admin,正在执行的SQL

3.4 实测对照样本(本实例真实数据)

THREAD_ID PROCESSLIST_ID THREAD_OS_ID TYPE 用户/命令
1 NULL 1023 BACKGROUND main 主线程
38 5 1287 FOREGROUND event_scheduler 调度线程
60 26 3493 FOREGROUND admin / Sleep
83 49 4080 FOREGROUND admin / Query
4–18 NULL 1163–1184 BACKGROUND InnoDB io/log 系列线程

小彩蛋:示例命令 ps -T -p 1023 里的 1023 是进程 PID,而本机恰好主线程 THREAD_OS_ID 也是 1023——两者数值相同纯属巧合,一个是进程号、一个是线程号,不要混为一谈。

3.5 反向排查(最常用场景)

top -H -p 1234 发现某个 TID(比如 4080)CPU 飙高,立刻反查它到底是哪个会话:

SELECT PROCESSLIST_ID, THREAD_ID, PROCESSLIST_USER, PROCESSLIST_DB,
       PROCESSLIST_COMMAND, PROCESSLIST_STATE, PROCESSLIST_INFO
FROM performance_schema.threads
WHERE THREAD_OS_ID = 4080;

同理可排查 InnoDB 后台线程的 IO 热点(THREAD_OS_ID 对应 NAME,如 thread/innodb/io_read_thread)、复制线程、主线程等问题。


四、线程的生命周期与缓存复用

  • 连接断开 → 线程不销毁:线程进入 thread cache(受 thread_cache_size 控制,8.4 默认 -1 表示自动调节,上限约为 max_connections/5)。
  • 新连接 → 优先取缓存线程:因此 Threads_created 往往远小于历史会话总数。本实例 thread_cache_size=9,实测缓存中有 1 个线程待命。
  • 复用带来的绑定陷阱:被复用的 OS 线程,其 THREAD_OS_ID 不变,但 PROCESSLIST_ID(会话号)是新分配的。所以做绑定排查时,务必用同一时刻的快照对照,不要拿旧结果对。

五、注意事项汇总

  1. 列名:8.4 用 THREAD_OS_ID,8.0 是 OS_THREAD_ID,别照旧文档抄。
  2. 平台THREAD_OS_ID 只在 Linux 上填充,Windows/macOS 为 NULL。
  3. 权限:查看其他用户的线程信息需要 PROCESS 权限(同 SHOW PROCESSLIST 权限),否则只能看到自己。
  4. 线程池例外:仅 Enterprise 版 Thread Pool 会打破 1:1;社区版 thread_handling 恒为 one-thread-per-connection
  5. 特殊的 FOREGROUNDevent_scheduler 与部分 daemon 在表中也是 FOREGROUND,但它们不是客户端连接,不计入 Threads_connected,筛选时注意用 PROCESSLIST_ID IS NOT NULL 并排除 Daemon 命令。
  6. 线程缓存:连接断开后线程进缓存复用,THREAD_OS_ID 不变而会话 ID 会变,绑定要看同时刻快照。
  7. OS 层线程名:前台会话线程在 OS 层都叫 mysqld(进程名),无法靠 ps 的线程名区分是哪个会话——必须靠 THREAD_OS_ID 反查,这正是本文方法的用武之地。

六、总结

  • 会话与线程:MySQL 8.4 社区版中,每个会话从建立到断开,始终对应且独占一个前台线程(1:1);断开后线程进入缓存复用,避免重复创建的开销。
  • 三层 IDTHREAD_ID(内部线程号)≠ PROCESSLIST_ID/CONNECTION_ID()(会话号)≠ THREAD_OS_ID(OS 内核线程号),三者同列于 performance_schema.threads
  • 绑定排查ps -T -p <PID> 的 SPID 与 THREAD_OS_ID 一一对应,正向输出清单、反向定位热点会话,一条 SQL 即可完成从「操作系统线程」到「用户与正在执行的 SQL」的全链路定位。

参考

  • MySQL 8.4 Reference Manual: The performance_schema.threads Table
  • MySQL 8.4 Reference Manual: Status Variables(Threads_connected / Threads_created / Threads_cached / Threads_running)
  • MySQL 8.4 Reference Manual: Server System Variables(thread_handling / thread_cache_size / max_connections)
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐