早些时候在给一个客户做DM数据库的性能排查,现象很典型:业务侧反馈系统变慢,数据库CPU占用却只有20%出头,连接数正常,没有锁等待风暴,怎么看都像是"数据库没干活"。后来一查工作线程池状态,发现几乎所有工作线程都被几个慢查询占着,后续请求全部在队列里排队,这才定位到根因。
和Oracle、PostgreSQL这些"一进程一连接"的架构不同,DM数据库(达梦数据库)采用的是单进程多线程架构,dmserver进程内部的线程池管理直接决定了并发处理能力。这篇文章就围绕DM数据库的工作线程机制展开,把这套模型拆开讲明白,包括工作线程池如何运作、和会话是什么关系、线程被占满时怎么排查,以及一线实战里的调优经验。
1. 先搞清楚DM数据库的进程模型和工作线程的位置
很多人习惯用Oracle的思路去理解达梦,结果第一步就绕晕了。Oracle是重型多进程架构,每个用户连接对应一个专用后台进程;达梦则是一个dmserver主进程,连接处理、SQL执行、事务管理、后台刷盘这些职责全部由进程内部的线程协作完成。想理解DM的工作线程,得先接受这个"单进程多线程"的设定。
dmserver启动后,内部线程大致可以分成几类:网络监听线程负责接受客户端连接,会话管理相关线程负责维护会话状态,工作线程池负责真正执行任务,还有后台线程负责检查点、日志刷盘、统计信息收集这些周期性工作。我们今天讨论的核心是工作线程池,也就是实际承载SQL编译、执行、结果返回的那批线程。
工作线程和服务线程可以理解为同一批资源的不同称呼。在达梦的官方文档和动态视图里,v$threads中type字段标为WORKER的就是工作线程,它们从任务队列里取任务执行。一个线程同一时刻只能执行一个任务,但一个会话的操作可以拆分为多个任务,也可以由不同线程在不同时间点接手。这套机制决定了DM在面对大量短事务时能保持较高吞吐,但也埋下了一个隐患:工作线程一旦被长任务占满,系统整体响应就会急剧恶化,和CPU吃不吃得满关系不大。
连接数和线程数不是一回事,这个必须反复强调。很多人看会话数几百上千就觉得数据库应该"忙不过来",实际上工作线程池可能只有几十个线程,由这几十个线程轮流处理上千个会话的任务。数据库要应对的并发压力,最终都要折算成工作线程的占用情况来看。
2. 涉及工作线程的核心参数:改了哪些、默认是什么、怎么配合
达梦的线程池设计逻辑是分层解耦的,不会让你的每个连接都占一个操作系统线程。先看一张参数对应表,这里以DM8常规配置为例,不同版本参数名和默认值会略有差异,但底层逻辑是一致的。
| 参数 | 作用 | 典型默认值 | 调整方向 |
|---|---|---|---|
| MAX_OS_THREADS | 整个dmserver进程可创建的操作系统线程总数上限 | 根据系统配置自动计算,通常较大 | 一般不需要动,除非外部工具大量占用线程 |
| WORKER_THREADS | 工作线程池大小,决定并行执行SQL任务的工作线程数量 | 受CPU核数影响,默认与核数相关,常见16/32/64 | CPU核数紧张时不要盲目加大,先看实际利用率 |
| TASK_THREADS | 任务调度和部分异步处理线程数 | 有默认值,无需频繁调整 | 调优空间有限,重点还是WORKER_THREADS |
| MAX_SESSIONS | 最大会话数 | 通常几百到上千 | 和工作线程数配套调整,不是越大越好 |
| MTP(多线程模式)相关配置 | 控制线程模型运行模式 | 默认开启 | 一般保持默认 |
这里重点展开两个参数。
第一个是WORKER_THREADS,这是最常被误调的参数。它并不是越多越好。工作线程切换是有代价的,如果服务器是32核,你却把工作线程配到128,大量时间会耗在上下文切换上,业务响应反而更差。一般来说,工作线程数设置为CPU核数的1到2倍是相对合理的区间。OLTP场景偏短查询,可以适当接近2倍;OLAP场景大查询较多,建议保守一些,靠近1倍甚至略低,避免大查询把线程池全占住。
第二个是MAX_OS_THREADS。这个参数容易被忽略,但一旦触顶,现象是数据库报错"无法创建新线程",应用侧全是连接失败。曾见过有人把MAX_OS_THREADS调得太低,又想同时跑大量备份和外部调度工具,结果线程耗尽,数据库看起来还在运行,新连接却全都进不来。
还有一点要注意:连接数MAX_SESSIONS和工作线程数是两个维度的配置,但调整时得放在一起考虑。如果MAX_SESSIONS设5000,WORKER_THREADS只有16,那5000个连接的大部分时间都在排队。与其把会话数拉高,不如在应用层把连接池控制好,让工作线程始终有活干但又不至于被短连接风暴冲垮。
达梦的参数修改有的是静态参数,需要重启生效;有的是动态参数,可以会话级或系统级在线修改。判断方法是查v$parameter和v$dm_ini,关注type字段。改动前先确认是否要重启,否则白改。
3. 一条SQL是如何被工作线程消化掉的:完整任务流转链路
理解工作线程,不能只停留在参数层面,还得把SQL从进入到返回的全过程走一遍。我习惯把这条路拆成四个阶段。
第一阶段是连接建立和会话登记。客户端发起连接请求后,网络监听线程(DM中体现为监听和接收连接的系统线程)负责三次握手,完成用户认证、初始化会话上下文,这个会话会被挂到会话管理结构中,但这会儿还没有工作线程参与。如果应用层频繁创建和销毁连接,这里会产生明显开销,也就是为什么连接池在达梦场景下这么重要。
第二阶段是任务入队。会话产生一条SQL请求后,并不会直接被某个工作线程"领养"。这个请求会按一定规则打包成任务,放进工作线程池的任务队列。工作线程处于空闲状态时会从队列里取任务执行。队列是FIFO还是优先级分队列,不同版本有差异,但总体都是"谁来取谁干"的模型。线程完成任务后,会再次回到队列等待下一个任务。
第三阶段是真正干活。工作线程取到任务后,完成SQL解析、生成执行计划、执行计划、访问缓冲区或磁盘数据、处理事务、生成结果集。这一阶段占用的时间取决于SQL本身复杂度、数据量、索引情况、锁等待情况。这里有一个关键点:一个长事务执行期间,它占用工作线程的时间不一定等于事务持续时间,因为遇到I/O等待、锁等待时,某些版本的执行模型可能会让线程让出CPU去处理其他任务,但SQL执行上下文依然占据着执行资源。也就是说,慢SQL和阻塞会话是工作线程池被耗尽的头号杀手。
第四阶段是结果返回。执行完成后,工作线程把结果集通过会话对应的网络连接发送给客户端。如果结果集很大,或者客户端消费很慢,工作线程不会一直死等在这里,通常由异步发送机制或缓冲区配合处理,但发送缓冲如果被打满,依然会反压到执行流程上。
用一个不太严谨的类比:工作线程是呼叫中心的客服,会话是排队的来电者,任务队列是呼叫排队系统。客服不归某个来电者私有,接完一个电话就去服务下一个。但如果有一个来电者占线很长时间不挂(长事务),或者一个客服遇到超复杂的诉求处理不完(慢SQL),后面排队的全都被拖住了。
这也是达梦工作线程模型和Oracle专用进程模型最大的感官差异所在。Oracle里某个会话再慢,主要影响它自己(当然锁和资源竞争除外);DM里慢会话会占用工作线程,相当于占用了公共资源池,影响面会被放大。
4. 工作线程池被打满时,一次完整的排查链路
前面说了理论,接下来是实践。遇到系统变慢、并发上不去的情况,怎么一步步确认是不是工作线程池的问题,我从一次真实排查里总结出这套方法,目前用下来比较顺手。
第一步,先确认现象。应用侧反馈SQL超时,数据库CPU占用不高甚至偏低,IO也没有明显瓶颈,这是典型的"线程池耗尽但系统资源没打满"信号。反过来,如果CPU已经跑满,那是计算瓶颈,和线程池耗尽不是一回事,排查方向完全不同。
第二步,查v$threads动态视图,看看当前工作线程都在干什么。DM的v$threads能反映出线程的类型、ID、当前状态,以及正在执行的SQL信息。重点看有没有大量线程处于"运行中"但实际在执行同类型慢SQL的状态。如果看到十几二十个工作线程都在执行同一条SQL文本或者同一类大查询,基本就锁定方向了。
第三步,查v$sessions,关联会话信息,找到这些工作线程对应的会话来自哪个应用、哪个IP、执行了多久。注意,不同版本视图的字段名称会有差异,常见的关键字段包括会话ID、状态、SQL_ID、等待事件等。通过等待事件还能进一步区分是锁等待、IO等待还是纯CPU计算慢。
第四步,定位源头SQL。把工作线程正在执行的SQL抓出来,分析执行计划。我遇到过最常见的两类问题:
一类是报表类大查询。某个BI报表没加过滤条件,直接全表扫描加笛卡尔积关联,一条SQL跑几十分钟。因为DM是串行执行的长查询,这条SQL会长期占用一个工作线程。如果是并发调度多个报表任务,几个线程就同时被占。
另一类是应用连接池配置失控。某个应用框架设置了最小连接数20、最大连接数200,但每个连接都长期持有事务不提交,等于说几十个会话各自挂着一个未提交事务,相关行被锁住。其他请求虽然线程是空闲的,但一到SQL执行阶段就卡在锁等待上,工作线程全被"等锁"卡住,问题表现为线程池耗尽,本质是锁冲突。
第五步,根据定位结果处理。慢查询就kill会话或通过系统函数取消SQL执行,然后优化SQL、补索引。锁等待就找到持锁会话,分析事务逻辑,必要时提交或回滚。处理完观察v$threads的忙线程数量和排队情况是否回落。
5. 工作线程的观测手段:常用视图、SQL和关键指标
要管理好工作线程,日常观测比出了故障再排查更重要。达梦在不改代码的情况下,主要靠系统动态视图来观测线程情况。以下是几个我每次排查都会用的查询,以DM8为参考,字段名在更早版本里可能略有收缩,但思路通用。
查看当前系统里所有工作线程类型和状态:
SELECT ID, TYPE, STATE, CREATE_TIME, REF_SESS FROM V$THREADS ORDER BY TYPE, ID;这里面TYPE为WORKER的就是工作线程,STATE可以反映线程是空闲还是在执行任务,REF_SESS能关联到会话ID,配合V$SESSIONS能查出这个线程正在为谁服务。
如果想直接看会话和工作线程的对应关系,以及每个会话在干什么:
SELECT S.ID AS SESS_ID, S.STATE, S.CREATE_TIME, S.USER_NAME, S.APPNAME, T.ID AS THREAD_ID, T.STATE AS THREAD_STATE FROM V$SESSIONS S LEFT JOIN V$THREADS T ON S.THREAD_ID = T.ID WHERE S.STATE = 'ACTIVE' ORDER BY T.STATE DESC;注意,并非所有版本的V$SESSIONS都有THREAD_ID字段,如果没有这个字段,可以通过V$THREADS里的REF_SESS反查。实际操作时先desc一下视图结构再写查询,不丢人。
关于指标,我个人的经验阈值如下:
- 工作线程忙时占比(BUSY_RATIO或通过STATE统计):如果长期高于80%,说明工作线程基本无空闲,业务随时可能排队。这个值高本身不是问题,问题要看SQL执行效率。如果是大量短SQL,线程忙但每笔都很快,系统吞吐正常;如果线程忙且每笔执行时间都很长,说明SQL有问题或者锁冲突严重。
- 任务排队情况:DM上层应用在并发量大时,任务队列会有堆积。如果你发现数据库CPU正常、线程利用率高,但业务反馈RT(响应时间)暴涨,要往排队方向考虑。
- 工作线程数变化:正常情况下工作线程数稳定在配置值。如果发现线程频繁创建销毁或数量异常增长,往往是有外部工具或非业务代码在大量占用数据库连接资源。
另外,不要忽视系统等待事件。DM的v$wait或类似视图可以展示会话当前等待什么,是buffer等待、I/O等待还是锁等待。我曾经定位过一个诡异问题:工作线程全部是IDLE,但业务卡死。后来发现是所有会话都在等待应用层释放连接池锁,数据库本身完全没事,问题出在应用端连接池配置。如果只看数据库线程,永远找不到根因。
6. 调优工作线程的几个实战决策,以及容易踩的坑
最后聊一聊真正动参数时需要考虑的东西。
先说结论:大部分非极端场景下,工作线程不需要频繁调优,更需要调的是应用侧。
第一,别盲目调大WORKER_THREADS。在低并发、高CPU场景下,调大工作线程通常没收益甚至会降低性能。线程上下文切换是有开销的,线程数远超核数后,光切换就吃掉CPU资源。线上遇到过有人把WORKER_THREADS调到256,32核机器,结果CPU整体繁忙但有效吞吐反而下降,回退到64后恢复。调这个参数前先想清楚:你的瓶颈到底是线程不足,还是SQL本身慢。
第二,从应用侧控制并发这把钥匙更有效。达梦工作线程池的设计决定了它能扛住比Oracle更大量的空闲连接,但扛不住同时"真干活"的请求数量。假如同一个Tomcat连接池里跑了100个线程,每个线程都同时发起SQL,那工作线程池预取的SQL任务就会排起长队,体验是"数据库不干活"。解决方案不是拼命加工作线程,而是严格控制应用并发度:把连接池最大连接数收紧到工作线程数的1.5到2倍以内,让真正在执行的请求数匹配数据库处理能力。
第三,大查询尽量拆分到业务低峰期,或者从资源组层面限制单个查询消耗。"一条大查询拖垮一个库"在达梦的单进程多线程架构下特别明显,因为它占的是全局公共线程资源。能走分区裁剪的走分区裁剪,能用物化视图的用物化视图,不要让几个报表权限把交易系统的线程池占光了。
第四,关注锁等待,这是线程池耗尽的隐藏元凶。工作线程被"等锁"占着不放,表面上线程是忙的,实际上什么都没干。这类问题单纯调线程数没用,得从应用事务设计下手:事务最小化、提交及时化、避免一个大事务里做太多事情。
第五,有条件的话做连接分层隔离。让核心交易应用和报表分析应用使用不同的连接配置或不同的中间件实例,避免分析类大查询和交易短事务抢工作线程。如果SQL级别想控制,可以研究一下达梦的资源管理功能,对指定用户或会话做并发和资源限制。
再补充一个日常容易忽略的点:备份和批量任务也会消耗工作线程。达梦备份通常在后台线程执行,但部分批量操作、大事务的提交和回滚、统计信息更新都会占用工作线程或相关任务线程。如果业务高峰时段恰好跑批量任务,工作线程被批量任务占掉一截,线上用户体验就会明显下降。排程这类任务时尽量和交易高峰错开。
从我个人实践看,达梦工作线程的核心是"公共资源池"思维。它不像某些数据库那样能为每个VIP用户预留专属进程,所有请求都要去抢有限的执行线程,所以决定系统上限的不是CPU也不是连接数,而是工作线程的利用效率。日常运维只要盯住三件事:线程是否长期被慢查询占住、会话是否堆积、SQL执行效率是否明显退化,工作线程这块基本不会出大乱子。