早些时候在给一个客户做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/64CPU核数紧张时不要盲目加大先看实际利用率TASK_THREADS任务调度和部分异步处理线程数有默认值无需频繁调整调优空间有限重点还是WORKER_THREADSMAX_SESSIONS最大会话数通常几百到上千和工作线程数配套调整不是越大越好MTP多线程模式相关配置控制线程模型运行模式默认开启一般保持默认这里重点展开两个参数。第一个是WORKER_THREADS这是最常被误调的参数。它并不是越多越好。工作线程切换是有代价的如果服务器是32核你却把工作线程配到128大量时间会耗在上下文切换上业务响应反而更差。一般来说工作线程数设置为CPU核数的1到2倍是相对合理的区间。OLTP场景偏短查询可以适当接近2倍OLAP场景大查询较多建议保守一些靠近1倍甚至略低避免大查询把线程池全占住。第二个是MAX_OS_THREADS。这个参数容易被忽略但一旦触顶现象是数据库报错无法创建新线程应用侧全是连接失败。曾见过有人把MAX_OS_THREADS调得太低又想同时跑大量备份和外部调度工具结果线程耗尽数据库看起来还在运行新连接却全都进不来。还有一点要注意连接数MAX_SESSIONS和工作线程数是两个维度的配置但调整时得放在一起考虑。如果MAX_SESSIONS设5000WORKER_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调到25632核机器结果CPU整体繁忙但有效吞吐反而下降回退到64后恢复。调这个参数前先想清楚你的瓶颈到底是线程不足还是SQL本身慢。第二从应用侧控制并发这把钥匙更有效。达梦工作线程池的设计决定了它能扛住比Oracle更大量的空闲连接但扛不住同时真干活的请求数量。假如同一个Tomcat连接池里跑了100个线程每个线程都同时发起SQL那工作线程池预取的SQL任务就会排起长队体验是数据库不干活。解决方案不是拼命加工作线程而是严格控制应用并发度把连接池最大连接数收紧到工作线程数的1.5到2倍以内让真正在执行的请求数匹配数据库处理能力。第三大查询尽量拆分到业务低峰期或者从资源组层面限制单个查询消耗。一条大查询拖垮一个库在达梦的单进程多线程架构下特别明显因为它占的是全局公共线程资源。能走分区裁剪的走分区裁剪能用物化视图的用物化视图不要让几个报表权限把交易系统的线程池占光了。第四关注锁等待这是线程池耗尽的隐藏元凶。工作线程被等锁占着不放表面上线程是忙的实际上什么都没干。这类问题单纯调线程数没用得从应用事务设计下手事务最小化、提交及时化、避免一个大事务里做太多事情。第五有条件的话做连接分层隔离。让核心交易应用和报表分析应用使用不同的连接配置或不同的中间件实例避免分析类大查询和交易短事务抢工作线程。如果SQL级别想控制可以研究一下达梦的资源管理功能对指定用户或会话做并发和资源限制。再补充一个日常容易忽略的点备份和批量任务也会消耗工作线程。达梦备份通常在后台线程执行但部分批量操作、大事务的提交和回滚、统计信息更新都会占用工作线程或相关任务线程。如果业务高峰时段恰好跑批量任务工作线程被批量任务占掉一截线上用户体验就会明显下降。排程这类任务时尽量和交易高峰错开。从我个人实践看达梦工作线程的核心是公共资源池思维。它不像某些数据库那样能为每个VIP用户预留专属进程所有请求都要去抢有限的执行线程所以决定系统上限的不是CPU也不是连接数而是工作线程的利用效率。日常运维只要盯住三件事线程是否长期被慢查询占住、会话是否堆积、SQL执行效率是否明显退化工作线程这块基本不会出大乱子。