ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MySQL 8.0线程全景解析:连接线程与InnoDB后台线程调优实战

MySQL 8.0线程全景解析:连接线程与InnoDB后台线程调优实战 1. 连接线程的生命周期从握手到释放1.1 一条连接进来后MySQL到底做了什么MySQL 8.0里的线程这个概念其实可以分成两大阵营一类是用户能感知到的连接线程也叫前台线程另一类是用户一般感知不到但一直在默默干活的InnoDB后台线程。很多人调优时只盯着max_connections和慢查询结果线上出问题时用SHOW PROCESSLIST一看全是Sleep再用performance_schema.threads一查后台线程的数目和状态也是一头雾水——这就是对全景图没概念。先说连接线程。客户端发起连接请求时主线程在Unix系统上通常就是mysqld主进程里那个accept循环收到TCP握手包然后分配一个线程专门处理这条连接。这个线程做完MySQL协议层的握手、认证、初始化会话变量之后就进入等SQL状态。你看到的SHOW PROCESSLIST里的Id列实际上和performance_schema.threads里的PROCESSLIST_ID是对应的但注意这个PROCESSLIST_ID不完全等于线程内部ID两张表通过THREAD_ID才能准确关联。这里有个很多人忽略的细节MySQL 8.0默认的认证插件改成了caching_sha2_password第一次连接时如果走的是不安全的通道服务端会要求客户端先请求RSA公钥或走TLS加密导致首次握手多一个来回。如果连接频繁建立比如短连接场景这个额外开销会被放大。8.0客户端默认支持但老版本驱动比如5.1.x的JDBC驱动可能握手直接失败——这是连接线程生命周期第一个容易踩的坑。再往下SQL执行阶段并不完全是一个连接线程干完所有事。执行计划、排序、Join缓冲这些工作大部分就在连接线程的栈里完成只有某些场景比如INSERT DELAYED在8.0已经移除、全文索引查询、或者NDB引擎才可能起额外的辅助线程。对InnoDB来说一个前台查询的最主要开销其实不是线程调度而是行锁等待、InnoDB数据页从磁盘到buffer pool的加载。搞清楚这一点你就知道为什么线程数调优不能只看max_connections。1.2 连接线程的回收与缓存机制MySQL处理完一条连接后连接线程并不一定马上销毁。如果thread_cache_size设置合理线程会被挂起到线程缓存里等下一个新连接来了直接复用这样就省掉了反复创建/销毁线程的开销。8.0里thread_cache_size的默认值是-1也就是自动调整系统会根据max_connections和内存情况算一个值出来。自动调整在大多数场景下够用但如果你用短连接为主的架构比如PHP-FPM、无连接池的Java老代码建议还是手动观察命中率。命中率可以这么算SHOW GLOBAL STATUS LIKE Threads_created; SHOW GLOBAL STATUS LIKE Connections;Threads_created除以Connections就是线程创建率理想情况下应该是个位数百分比。如果这个值很高说明线程频繁创建可以适当调大thread_cache_size。不过注意8.0里thread_cache_size的上限是16384单条连接占用的内存并不止thread_stack默认1MB是栈大小还有会话级别的sort_buffer_size、join_buffer_size、read_buffer_size等这些不是一开始全部分配而是在需要时才分配。所以别把thread_cache_size调到几千就以为万事大吉真并发上来时内存会很难看。2. InnoDB后台线程全景谁在背后干活2.1 后台线程分类与职责说完前台线程InnoDB的后台线程才是这个引擎的隐形生产线。很多人只知道MySQL有后台线程但不知道每个线程是干嘛的遇到脏页刷不下去、undo膨胀等问题时就只能靠重启。其实后台线程大致可以分为以下几类IO线程负责读写数据文件和日志文件。8.0里innodb_read_io_threads和innodb_write_io_threads默认都是4注意这两个参数在8.0.30之前可以手动调大最大64但8.0.30之后被标记为废弃系统会自动根据innodb_io_capacity和实际负载来调整。对于机械盘环境多几个读线程确实能提高扫描吞吐但对SSD来说线程数量太多反而容易增加锁竞争默认4个通常够用。Page Cleaner线程这个线程是InnoDB的清道夫负责把buffer pool里的脏页刷到磁盘。8.0里innodb_page_cleaners默认值和innodb_buffer_pool_instances一致上限64。如果脏页比例高、checkpoint跟不上大概率是Page Cleaner数量不够或者innodb_io_capacity设置得太保守。Purge线程负责清理已提交事务留下的undo记录和标记删除的旧版本数据。8.0里innodb_purge_threads默认4最大32。注意history list length如果持续上涨基本可以确定是Purge线程跟不上通常和长事务有关而不只是线程数量问题。Redo Log刷盘线程在8.0里redo log的写入有了专门的log writer和log flusher线程不是简单地复用write_io_threads。8.0.30之后还对redo log的并发写做了优化引入了innodb_log_writer_threads参数。这一块在8.0里演进得比较快如果你还在用老版本的调优思路很容易漏掉这些新线程的存在。除了这些主要的还有负责死锁检测的线程INNODB_DEADLOCK_DETECT、负责统计信息采样的线程、负责change buffer合并的线程以及8.0才有的数据字典线程因为8.0的字典表全部换成了InnoDB存储。2.2 各后台线程的参数联动关系后台线程的参数不是孤立的它们之间有很强的联动关系。我来画个简单的对应关系线程类型核心参数默认值上限主要职责读IO线程innodb_read_io_threads4648.0.30后废弃自动从磁盘读数据页到buffer pool写IO线程innodb_write_io_threads4648.0.30后废弃自动把脏页写回磁盘Page Cleanerinnodb_page_cleaners等于buffer pool instances64脏页刷新Purge线程innodb_purge_threads432清理undo和旧版本数据Redo刷盘线程innodb_log_writer_threads开启-redo log写入与刷盘字典线程8.0新增自动管理--数据字典缓存与访问这里要特别提醒innodb_page_cleaners如果设置得比innodb_buffer_pool_instances大MySQL会自动降级。实际生产里最常见的配置误区是buffer pool设置128GBinnodb_buffer_pool_instances默认8但Page Cleaner还是默认8结果到了写入高峰比如大批量UPDATE脏页比例飙到90%以上性能断崖式下跌。这时候你需要的不是盲目加Page Cleaner而是先看innodb_io_capacity和innodb_io_capacity_max——默认200和2000适用于传统SATA盘对SSD来说太保守了可以分别调到1000和5000左右再配合innodb_flush_neighbors0SSD不需要顺序化相邻脏页来提速。3. 线程全景图实操用 Performance Schema 看清每一根线程3.1 从performance_schema.threads看全貌performance_schema.threads表是观察MySQL 8.0线程全貌最直接的地方。我一般在排查问题时先跑这条SQLSELECT THREAD_ID, NAME, TYPE, PROCESSLIST_ID, PROCESSLIST_USER, PROCESSLIST_HOST, PROCESSLIST_DB, PROCESSLIST_COMMAND, PROCESSLIST_STATE, PROCESSLIST_TIME, THREAD_OS_ID FROM performance_schema.threads ORDER BY TYPE DESC, PROCESSLIST_TIME DESC;这里有两个关键字段需要理解TYPE只有两个值FOREGROUND表示连接线程对应一条用户会话BACKGROUND表示InnoDB后台线程或系统线程。THREAD_OS_ID对应操作系统的线程号你可以直接拿它去Linux上排查top -H -p $(pgrep mysqld)按线程号找到对应的THREAD_OS_ID能看出该线程的CPU使用率、内存状态。有一次我定位一个CPU飙高的问题就是从threads表里发现一个FOREGROUND线程在statistics状态THREAD_OS_ID对应的系统线程CPU占满最后定位到是一个统计信息采样线程和前台查询互相放大导致的。这种情况用SHOW PROCESSLIST完全看不出来因为进程列表里全是正常的SELECT。3.2 透视一条SQL执行时的线程流转下面我们做一个实际测试。我准备用一个Python脚本模拟10个并发连接去执行同一条SELECT COUNT(*) FROM big_table WHERE status1同时每0.1秒抓一次performance_schema.threads的快照看线程怎么变化。import mysql.connector import threading import time import random config { user: root, password: your_password, host: 127.0.0.1, port: 3306, database: test, connect_timeout: 5, pool_size: 10 } def run_query(): conn mysql.connector.connect(**config) cur conn.cursor() cur.execute(SELECT COUNT(*) FROM big_table WHERE status1) cur.fetchone() cur.close() conn.close() threads [] for i in range(10): t threading.Thread(targetrun_query) threads.append(t) t.start() # 抓取threads表 import subprocess snapshot_sql SELECT THREAD_ID, TYPE, PROCESSLIST_COMMAND, PROCESSLIST_STATE, THREAD_OS_ID FROM performance_schema.threads os.system(fmysql -uroot -ppassword -e \{snapshot_sql}\ /tmp/thread_snapshot.txt) for t in threads: t.join()跑完之后看/tmp/thread_snapshot.txt你会发现几个现象FOREGROUND类型的线程会有10条左右但每条线程在SQL执行期间的状态可能从Sending data变成executing最后变成Sleep如果连接没关闭。BACKGROUND线程的数量是固定的不管你怎么并发InnoDB后台线程不会因为前台压力增大而多开额外线程只会在内部增加工作量。每条连接线程的THREAD_OS_ID在Linu x上对应一个系统线程可以用top -H看到它们的CPU累加情况。这个实验能直观验证一个结论MySQL 8.0的连接线程是一对一模型每个连接固定一条前台线程不会像线程池那样复用执行线程。所以连接数一旦暴涨线程数量就会线性增长上下文切换开销跟着涨——这是MySQL connection-per-thread模型的天花板。3.3 区分Sleeping连接和Active连接很多DBA看SHOW PROCESSLIST时看到几十条Sleep就慌。其实Sleep只是连接线程在等客户端发下一条SQL不代表占着CPU。真正要看的是PROCESSLIST_TIME特别大且STATE不是Sleep的线程那才叫卡住的查询。一个有用的排查技巧是用sys.session视图SELECT thd_id, conn_id, user, db, command, state, time, current_statement FROM sys.session WHERE command ! Sleep ORDER BY time DESC;如果有一两条time很大但command显示Query、state一直卡在Waiting for table metadata lock那多半是有DDL在等MDL锁而不是线程本身有问题。这类的定位思路先查performance_schema.metadata_locks看谁持有锁再决定要不要KILL阻塞源。4. 线程相关常见问题与排查实战4.1 Too many connections的真相与应对错误信息Too many connections本质是连接线程数达到了max_connections上限。但在实际生产里触发这个错误通常不是用户真的需要那么多并发而是连接没被及时释放。最常见的情况应用层的数据库连接池配置过大比如Java的HikariCP设置了maximumPoolSize50但服务开了20个实例总共1000个连接直接打满默认151。慢查询把连接线程占住不放。这个要结合performance_schema.events_statements_current看而不只是看连接数。wait_timeout和interactive_timeout设置太长默认28800秒大量空闲连接一直堆着不释放。应对思路分几层先临时用SET GLOBAL max_connections 500救急再查是什么连接占着最后从应用侧优化连接池配置和SQL。注意max_connections调大之后内存会跟着涨因为每条连接线程都要分配栈空间和可能用到各种buffer。在8.0中连接线程默认的thread_stack是1MB500条连接就多出500MB的虚拟内存虽说不一定全用上但至少不能忽视。我踩过的一个真实坑某个业务用了MyBatis但没配连接池每次请求都新建连接每分钟请求量只有2000但由于接口响应慢连接并发叠到300多直接打满max_connections。最后不是靠调大参数解决的而是先把wait_timeout从8小时改成60秒再给应用加上连接池彻底解决。调max_connections永远只是治标。4.2 Page Cleaner线程忙不过来脏页刷不下去你可能会遇到这种场景UPDATE或INSERT的写入量很大但buffer pool的脏页比例一直居高不下磁盘IO没跑满可写入性能就是上不去。这时候大概率是Page Cleaner线程的刷新策略出了问题而不是磁盘本身的瓶颈。先看脏页比例SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_pages_dirty; SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_pages_total;如果比例长期超过75%甚至到了90%以上就要关注SHOW ENGINE INNODB STATUS里面LOG部分的Modified db pages。接下来查Page Cleaner线程的工作情况SHOW ENGINE INNODB STATUS;看到类似Pending writes: 64, 512, 512对应LRU刷、flush list刷、single page刷说明有大量页在排队。其实Page Cleaner线程数量多少不是关键关键是innodb_io_capacity设得太低导致它不敢快速刷脏页。SSD环境建议把innodb_io_capacity从200调到1000以上innodb_io_capacity_max调到5000以上然后重启实例生效这两个参数是静态的8.0里也一样要重启。另外提一句innodb_flush_neighbors在8.0默认是0关闭相邻页刷盘如果你的环境是SSD保持默认就行如果机械盘可以考虑开成1或2利用磁盘顺序读优势减少随机IO。4.3 Purge线程跟不上undo膨胀长事务是InnoDB的天敌它不仅会阻塞Purge线程还会让undo log不断膨胀。判断标准是看SHOW ENGINE INNODB STATUS里的History list length。正常情况下这个值应该很小几十到几百。如果持续涨到几万甚至几十万说明Purge线程处理不过来或者有事务一直持有着旧版本。Purge线程默认4条大多数情况下够用但如果你有大量批量写事务比如持续跑一天的UPDATE循环可能需要临时调大到8或16条。还有一种情况很隐蔽有个连接打开了事务但一直不提交performance_schema.threads里显示这条连接的状态是SleepINFORMATION_SCHEMA.INNODB_TRX里能看到trx_started时间很早。这种长事务会阻塞Purge的推进导致undo膨胀即使所有SQL都很快也无济于事。排查时把INNODB_TRX和threads按trx_mysql_thread_id关联一下找出持有事务最久的那条连接确认是业务bug还是人为漏提交。4.4 连接线程调度与线程池的话题MySQL传统的connection-per-thread模型在连接数几千上万时性能会下降因为线程上下文切换太频繁。Oracle的InnoDB集群或商业版Thread Pool能缓解但开源版默认不支持线程池。8.0社区版同样没有官方线程池你需要用Percona分支或MariaDB的线程池或者通过中间件ProxySQL、MyCat在应用层做连接复用。这里引入一个很多做Java开发的朋友熟悉的话题Java线程池有七大参数核心线程数、最大线程数、阻塞队列、拒绝策略等。如果你用Java写数据库访问层可以类比来理解MySQL的连接管理corePoolSize≈ 连接池的最小连接数HikariCP的minimumIdle。maximumPoolSize≈ 连接池最大连接数对应maximumPoolSize。阻塞队列 ≈ 等待获取连接的排队请求连接池打满时请求会排队等超时抛异常。拒绝策略 ≈ 连接获取超时的策略是抛异常还是快速失败。实际上MySQL内部并没有用线程池来调度SQL执行所以你在Java层做了再精细的线程池配置最终打到MySQL上的还是连接数线程数这么简单粗暴。理解了这一点你就知道为什么很多高并发系统都喜欢用ProxySQL在中间层做连接多路复用把上游的几千个连接收敛成下游几十个MySQL连接。5. 线程参数调优速查表与实测体会参数8.0默认值建议调整场景注意事项max_connections151高并发OLTP、连接池多实例不是越高越好受内存限制thread_cache_size-1自动短连接场景需要手动观察命中率上限16384innodb_read_io_threads4老版本机械盘可调大8.0.30后自动管理不建议手动innodb_write_io_threads4老版本机械盘可调大同上innodb_page_cleaners等于buffer pool instances大buffer pool且脏页刷不动时调大最大64innodb_purge_threads4大量写事务导致history list过长时调大最大32innodb_io_capacity200SSD必须调高静态参数需重启innodb_io_capacity_max2000SSD可调5000以上静态参数需重启innodb_buffer_pool_instances8大内存时调大减少锁竞争8.0.30后随buffer pool大小自动调整最后再分享一个实用技巧排查线程问题时把以下四条SQL配成监控脚本每5分钟跑一次问题出现时能快速回溯-- 1. 连接线程情况 SELECT COUNT(*) AS connection_threads, SUM(commandSleep) AS sleeping, SUM(commandQuery) AS querying FROM performance_schema.threads WHERE TYPEFOREGROUND; -- 2. 后台线程数量 SELECT NAME, COUNT(*) AS cnt FROM performance_schema.threads WHERE TYPEBACKGROUND GROUP BY NAME; -- 3. 每个连接线程的实时状态 SELECT t.PROCESSLIST_ID, t.PROCESSLIST_USER, t.PROCESSLIST_COMMAND, t.PROCESSLIST_STATE, t.PROCESSLIST_TIME, t.THREAD_OS_ID FROM performance_schema.threads t WHERE t.TYPEFOREGROUND ORDER BY t.PROCESSLIST_TIME DESC; -- 4. InnoDB核心状态 SHOW ENGINE INNODB STATUS;我个人在MySQL 8.0的运维实战中最深刻的体会是线程问题的本质往往是连接管理问题和事务生命周期问题而不是线程本身的问题。performance_schema.threads这张表能让你把从连接到线程从线程到后台任务的链路打通但知道了全景之后真正要做的不是去调各种线程参数而是优化应用的连接使用方式、控制事务长度、监控脏页和undo的积压情况。把这几点做扎实了大部分线程问题都不会找上门。
返回列表