ARTICLE DETAIL

资讯详情

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

openGauss Worker线程与CPU飙升排查:从线程池到执行计划

openGauss Worker线程与CPU飙升排查:从线程池到执行计划 1. Worker线程在openGauss里的角色很多人一开始就没完全搞明白先说一个我自己经历过的场景。某次凌晨一点左右运营反馈核心业务库的CPU使用率直接飙到90%以上业务侧报错“连接数不足”。我登录服务器一看top里冒出几十个名字类似postgres的进程每个CPU占用从5%到30%不等总和非常吓人。那个阶段第一反应是“是不是有慢SQL在疯狂扫描”但仔细查了一圈慢SQL日志里并没有特别离谱的大查询业务侧的请求量也并没有突增。后来才定位到问题并不在SQL本身而在线程池参数和会话接入的姿势上。这也是为什么我想专门写一篇关于openGauss Worker线程的文章。很多初学openGauss的朋友会把“进程”和“线程”混为一谈遇到CPU飙高第一件事就是去看慢SQL结果走了一堆弯路。真正搞明白Worker在线程池架构里的位置CPU问题排查才谈得上有章法。在openGauss的架构里线程被划分成几个不同的角色。负责执行SQL的线程大体上有三类会话线程Session Thread专门负责接收客户端的连接做权限校验、参数解析然后把任务转交出去。它本身不干重活更像是一个前台接待。Worker线程Worker Thread真正干重活的人。SQL解析、执行计划的生成、算子执行、结果返回这些动作全都要经过Worker线程来完成。在开启了线程池Thread Pool的情况下Worker线程不是“一个连接一个线程”的绑定关系而是从线程池里动态分配。后台线程Background Thread像bgwriter、checkpointer、WAL writer、autovacuum这类它们不直接为某个用户会话服务而是负责数据库内部的维护动作也和CPU、IO消耗有关。很多情况下CPU高不一定是某个Worker线程本身出问题而是要看你当时怎么开线程池、怎么配置并发、SQL的执行路径走到了哪里。接下来就顺着这个思路把Worker当前台和后台的机制逐一拆开讲。1.1 为什么openGauss要引入线程池而不沿用“一连接一进程”如果你用过原生PostgreSQL会知道它早期是“一连接一进程”模型每来一个客户端连接就fork一个进程进程间通信走共享内存进程数一多系统上下文切换的开销就非常大。openGauss在这一点上做了明显改造核心思路是把“频繁创建进程”变成“复用线程”也就有了线程池技术。线程池在数据库里解决的不只是“创建成本”还有几个更隐性的问题限制并发对资源的挤兑。如果业务连接峰值到了5000而线程池上限只有500那么剩下的请求要么排队要么直接拒绝总比5000个进程大家一起抢CPU导致整体不可用要好。线程复用带来的缓存稳定。进程频繁创建销毁无论是CPU context切换还是内存的局部性都会明显变差。线程池里的线程建立后保留在池中切换成本低CPU缓存命中率也相对稳定。行为可度量。有了线程池后你可以通过视图明确看到当前活跃的worker数量、空闲worker数量、正在排队的任务数量从而判断是业务量真的到了瓶颈还是参数配置不合理。从应用层角度理解一个业务连接过来后实际上是在openGauss内部完成了一次“任务分发”会话线程负责接待线程池调度器从池子里挑一个空闲的Worker线程去执行SQL执行完再把线程归还。这种模型下用户侧的“连接数”和数据库内核实际的“执行线程数”解耦了。1.2 Worker线程的动态分配与实际执行路径开启线程池之后你把max_connections设得很大并不意味着数据库会真的创建那么多线程。实际生效的是线程池内部管理的那组Worker它们默认是按需创建、空闲保留的。一条SQL从客户端进入到在Worker线程上完成执行大概经历这个路径客户端与数据库建立连接由监听进程接收。鉴权结束后连接被移交到线程池调度层此时关联的是一个会话上下文。线程池从空闲Worker列表里挑一个或创建新的Worker把SQL文本、参数、会话设置传过去。Worker线程执行SQL解析、生成执行计划、执行算子并返回结果。SQL完成后Worker线程不会退出而是回到空闲池等待下一个任务。这里面有个非常容易误判的地方你通过pg_stat_activity看到某个pid正在执行一条SQL就以为它是“那个连接对应的线程”其实它只是当前这个时刻被分配出来的Worker下一秒钟换了一条连接也能复用它。所以在排查CPU问题时抓住“线程”本身没有意义我们真正要找的是哪一类SQL占用这个Worker太久、或者某个动作导致大量Worker在空转和切换。1.3 给“Worker线程”划定排查边界提到Worker可能会有人把后台线程也算进去。为了避免排查混乱建议形成这样一个分类习惯线程类别作用典型示例会话/前台Worker执行用户SQL及事务逻辑连接分配、SQL执行日志与WAL线程写事务日志保证持久性WAL writer、WAL receiver检查点与清理线程内存脏页落盘、空间回收bgwriter、checkpointer统计与维护线程自动收集统计信息、清理垃圾数据autovacuum并行计算线程将一个大查询拆为多个并行分片SMP并行WorkerCPU飙高时你不但要看前台Worker还得留意autovacuum这类后台线程是不是因为vacuum参数不合理而不断在跑。两者都可能导致CPU吃满但处理方式完全不同。2. CPU飙升背后三类主要推手SQL执行压力、连接风暴、配置盲区从我自己处理过的几十个openGauss CPU告警案例来看高CPU基本都能归到三类原因SQL本身执行压力过大、短连接或连接风暴导致线程池频繁伸缩、还有参数配置导致并发失控。下面分别拆开讲结合一些可以用得上的判断依据。2.1 SQL执行压力过大扫描路径爆炸是默认的头号嫌疑这是最典型的场景某条SQL没有走索引或者索引失效导致全表扫描在数据量大的表上反复扫。OpenGauss虽然支持行存、列存等多种存储格式但优化器一旦估算错行数就可能选出一条糟糕的嵌套循环路径出来。For example一个两百万行的维表与几万行的订单表做关联如果优化器认为维表只有几百行就可能选择重复扫描维表上万次CPU立刻打满。这里有一个经验判断当CPU高且活跃SQL数量很少甚至只有一条的时候大概率是单条SQL执行路径出了问题。这时你不需要围着线程池转而是直接去抓SQL执行计划。定位手段也很直接-- 在openGauss中查看当前正在执行的SQL及其状态 SELECT pid, query_start, state, wait_event, query FROM pg_stat_activity WHERE state idle ORDER BY query_start;如果发现某一条SQL执行时间特别长就拿到它的pid然后去做执行计划分析EXPLAIN ANALYZE 原始SQL;这里要注意EXPLAIN ANALYZE会真实执行一遍SQL生产环境操作前务必先确认影响。如果SQL涉及大批量更新和删除要尽量避免在工作时间直接跑。2.2 连接风暴线程池反复伸缩的隐形杀手连接风暴是我遇到最多、也最容易被忽略的情况。业务侧配置了连接池但池的下限和上限设置得很大应用一重启或者高峰期一冲瞬间建几百上千个连接。openGauss的线程池在启动阶段会按配置创建一批线程当同时到达的连接多于空闲Worker时它会临时新建Worker线程去执行任务。但问题是如果大量连接涌来后很快又释放线程池内部就会拼命地“创建线程—销毁线程”来回折腾这种状态同样会让CPU上涨而且你很难通过慢SQL来定位问题。怎么和第一种区分看pg_stat_activity里的连接数变化。看线程池状态。看是否有很多查询是“短暂”的CPU高但活跃SQL数量也不小。openGauss提供了线程池相关的视图比如-- 查看线程池中线程组信息以及活跃线程、空闲线程数 SELECT * FROM gs_thread_pool_info;如果活跃线程数长期接近线程池上限空闲线程数基本为0同时连接数还在不断增长那大概率是连接压上来了。这里要特别提醒线程池不是越大越好。线程数超过CPU核数太多任务就会大量处于等待和切换状态。你以为自己在并行处理实际上系统的大部分时间花在“换人”上。线下压测时往往线程数从32调到64有提升但从64调到128反而性能下降这个拐点就得靠实测去找。2.3 配置盲区并行参数和资源监控没设好第三种原因往往最难察觉数据库自己把并发放大了。openGauss支持SMP并行查询也就是一条复杂SQL会被拆成多个并行Worker同时执行。假设你某条SQL原本单个Worker要跑10秒开启SMP并行后拆成8个Worker每个跑4秒整体时间变短了但瞬间的CPU消耗会从1个核变成8个核。如果业务里同时出现多个此类大查询CPU立刻被打满。几个关键参数query_dop查询并行度1表示不并行大于1表示允许并行。设置为0表示按系统资源自动选择。enable_resource_track资源监控开关。resource_track_cost超过指定代价的查询才记录资源监控。resource_track_duration超过指定时间的查询记录资源监控。实际生产环境里很多人为了省事把query_dop设为0觉得“让数据库自动选”结果大查询一多系统按CPU核数拼命开并行反而把资源耗尽。我个人建议先明确业务预期核心交易类SQLquery_dop控制在2或4以内分析类SQL可以放宽到8但要配合资源监控来控制同时并行的大查询数量。最好还要打开资源监控。不打开的时候排查每一个问题你都只能靠猜。打开了之后你可以直接看到哪些SQL占用CPU最多、执行了多少次、平均耗时多少相当于手里多了一张案发现场照片。-- 打开资源监控并设置记录阈值 ALTER SYSTEM SET enable_resource_track on; ALTER SYSTEM SET resource_track_cost 10000; ALTER SYSTEM SET resource_track_duration 30;3. CPU高排查的完整链路从系统层到数据库层再到SQL层排查讲究顺序。很多人喜欢一上来就去看数据库但我的习惯是先从系统层把“现象”描述清楚再进数据库确认“为什么”。整个链路大概分四步下面按这个顺序讲。3.1 第一步把CPU占用从“进程”定位到“线程”登录服务器第一件事跑top看整体负载top -c按P键按CPU排序找到最靠前的进程。通常你会看到一个用户名类似omm的进程列表每个对应一个线程。注意Linux的top默认看的是进程而openGauss的Worker是同一个进程内部的多线程所以你要切换到线程模式top -H -p 主进程PID这样你才能看到进程里面的各个线程各自吃掉多少CPU。你会看到一大堆类似PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 12345 omm 20 0 28.5g 8.6g 1.8g R 47.6 1.0 100:20.32 postgres 12346 omm 20 0 28.5g 8.6g 1.8g R 38.3 1.0 90:10.11 postgres这里面的COMMAND清一色都是postgres如果不用工具进一步映射根本不知道谁是谁。把线程ID转成十六进制printf %x\n 12345十六进制值在下一步里要用来和数据库视图里的线程ID对应。3.2 第二步利用线程栈与数据库视图对应在生产环境你通常没有权限直接对数据库线程做gdb但你可以用内核提供的工具捕获线程栈。例如# 抓取CPU占用最高的线程栈 cat /proc/12345/stack这个文件通常只有内核态栈信息用户态栈在多数环境下读不到。更实用的做法是使用gstack或pstack来获取线程栈但要注意这类工具会把进程短暂停住生产环境必须谨慎。数据库侧要做两件事查pg_stat_activity观察当前活跃SQL。查线程池相关视图确认线程实际分配情况。pg_stat_activity里的pid字段显示的是实际执行线程的线程号正常情况下和top里看到的tid是对应的。如果你在top里看到一个高CPU线程却在pg_stat_activity里找不到对应的活跃SQL只有两种情况要么这个线程正在跑的事务还没被统计视图刷出来要么它在执行后台任务比如autovacuum或者WAL相关动作。3.3 第三步用等待事件和资源视图确认矛盾点openGauss提供了比较丰富的等待事件视图最常用的是dbe_perf.wait_events和dbe_perf.THREAD_WAIT_STATUS。等待事件分类里的CPU time和CPU wait要特别注意区分。- CPU time线程真正在CPU上执行计算说明它在干活。 - CPU wait线程想被调度但CPU资源不够说明系统整体在排队。当你看到大量线程处于CPU wait状态说明CPU资源本身不够分了这时候应该想到是不是CPU核数太小或者线程并发开得太多。如果你是8核机器却开了64个活跃Worker那就算SQL都很简单也一样会高CPU。查看当前线程的等待事件可以这样查SELECT * FROM dbe_perf.THREAD_WAIT_STATUS WHERE tid 线程ID;再用动态视图确认SQL执行情况SELECT COUNT(*), state, wait_event FROM pg_stat_activity GROUP BY state, wait_event ORDER BY COUNT(*) DESC;如果大量线程集中在ClientRead这种等待事件说明连接数高但没活儿干不是CPU问题大爆发的核心如果集中在CPU time和DataFileRead说明SQL真的在疯狂读取数据和计算。3.4 第四步锁定SQL看执行计划是否跑偏走到这一步已经可以确定“谁在消耗CPU”了。接下来要回答的是“它为什么消耗CPU”。把活跃SQL拿出来EXPLAIN看计划。重点观察三个东西Seq Scan是不是大量出现全表扫描本身不是坏事但大表Seq ScanCPU和IO都会暴涨。嵌套循环Nest Loop的内层行数估算如果内层表被反复扫描且估算行数远低于实际行数cpu成本会成倍放大。并行度有没有异常放大观察有没有多个Worker同时在跑同一个查询。这里有个常用技巧如果统计信息过期导致行数估算不准先执行ANALYZE更新统计信息很多时候一个问题就消失了。我还遇到过一种情况某张表每天批量更新几十万行但自动analyze没触发结果统计信息里的行数还停留在几万优化器按这个老黄历去选路径最终选了嵌套循环CPU自然就上去了。4. 处置动作与长期防线不能只救火还要建机制CPU问题光靠临时kill会话是解决不了的。每处理完一次故障我都会建议业务侧和DBA侧一起把下面这些机制建起来避免同样的问题换个马甲再回来。4.1 止血手段cancel和kill的正确姿势发现某个SQL已经失控第一步是先cancel不是直接kill。Cancel意图是让数据库自己终止正在执行的查询走正常的事务回滚流程相对温和。直接kill线程虽然更暴力但可能导致事务回滚更久甚至产生不必要的锁问题。-- 找到要终止的会话 SELECT pid, query FROM pg_stat_activity WHERE query LIKE %可疑SQL特征%; -- 终止该会话正在执行的查询会话还在 SELECT pg_cancel_backend(pid); -- 如果cancel无效再考虑pg_terminate_backend SELECT pg_terminate_backend(pid);我一般在15秒内cancel不掉才考虑terminate。当然生产环境操作之前需要先知会业务方尤其是某些长事务直接终止可能引发应用侧连接中断的判断逻辑不一致。4.2 SQL层面的长期治理每个高CPU的库背后通常都有几个固定“毒SQL”在反复出现。建议按以下方式做一套过滤机制在pg_stat_statements或openGauss对应的视图里按total_exec_time加calls排序找出累计消耗CPU最狠的SQL。针对这些SQL逐个审查执行计划看有没有涉及全表扫描、不必要的类型转换、函数索引未生效等。能改写SQL的直接改写不能改写的就靠索引来兜底。对于统计信息不敏感的大表建议定期主动ANALYZE别指望自动清理线程每次都能在正确的时间点触发。4.3 连接管理与线程池参数配置连接数这块没有“标准答案”但有一个很实用的起步建议客户端连接池大小按照“活跃请求峰值/单请求平均处理耗时”来估算。假如你的业务峰值是每秒1000个请求单请求平均SQL耗时50ms那理论并发大约50个。连接池设100都嫌多设到500基本是在浪费资源。线程池总线程数我见过比较稳的配置公式是“线程池线程数不超过CPU核数的2~4倍”再往上基本就是上下文切换在表演。如果你有监控数据可以看CPU的%sys是否涨到10%以上如果sys占比高说明切换已经过重了。max_connections不要设成一个虚高的大数字它不是业务指标而是你的保护天花板。设成“最近峰值连接数30%左右”比较合理。openGauss线程池相关的核心参数大概包括这几个参数作用建议enable_thread_pool是否启用线程池生产环境建议onthread_pool_attr线程池规格控制如线程组数、组内线程数、初始线程数、最大线程数按CPU和业务并发调整max_connections最大允许连接数按业务峰值预留30%余量query_dop查询并行度事务型场景2~4分析型场景可放宽到8需要特别提醒thread_pool_attr在openGauss里的格式是类似16, 4, 8, 32这样的组合含义分别对应线程组数、组内初始线程数、组内最大线程数等修改后一般需要重启实例才能完整生效线上操作前一定要先在测试环境验证。4.4 建立CPU问题的日常监控与预警CPU排查最怕的是“事后诸葛亮”。建议架构上至少做到以下三点部署数据库层指标监控把活跃连接数、活跃Worker数量线程池状态、最长SQL执行时间、等待事件分布这四类指标以10秒粒度采集进监控系统保留至少30天。设置分级告警CPU超过70%持续5分钟时提示超过85%持续3分钟时告警超过95%持续1分钟时紧急告警。告警信息里直接带上当时的top线程快照和正在执行SQL的截图减少排查时的“案发现场重建”成本。定期做SQLReview每两周或每月把新增的慢SQL和资源消耗TOP SQL拉出来过一遍把隐患消灭在爆发之前。我个人在实际排查里还有一个小技巧就是每次出现CPU告警时除了看数据库一定要同时看一眼服务器主板上的numa拓扑和CPU绑核情况。openGauss对NUMA架构比较敏感如果线程池里的线程在跨NUMA节点频繁访问内存CPU的sys消耗和cache miss会明显偏高。你可以用numastat命令查看进程在各NUMA节点上的内存分配如果分布极不均衡可以考虑通过numactl做绑核优化。这个问题在虚拟化环境中尤其隐蔽很多人查了半天SQL最后发现是虚拟机CPU调度问题。还有一点值得分享排查过程中一定要对“CPU高”做时间维度的对比。同样一条SQL上午高峰期跑得好好的下午突然CPU飙高那问题大概率不在SQL本身而在资源竞争。你需要在告警发生前和发生后各抓一个系统的load快照看一下是不是其他业务或备份任务恰好同时打过来了。很多时候数据库是给人背锅的。处理完CPU问题后建议顺手把整个排查过程沉淀成一份检查清单包含系统load与CPU核数对比、线程粒度top输出、pg_stat_activity快照、等待事件分布、线程池状态、当前慢SQL执行计划、连接数变化趋势。下次再出现类似告警按清单走一遍十分钟内就能完成初步定性而不是从头开始翻日志。这套方法在多个项目里复用下来可靠性和效率都很高。
返回列表