Oracle 并行执行(PX)——DOP 调优、Granule 分配与服务器争用治理

并行执行(Parallel Execution, PX)是 Oracle 里最让人又爱又恨的特性。爱它,是因为一条全表扫描 10 分钟的 SQL,开 16 个并行进程,2 分钟就跑完;恨它,是因为并行开多了,CPU 被打满,其他 SQL 全卡住,业务直接瘫痪。我见过一个客户,DBA 给一张报表 SQL 开了 PARALLEL 64,结果整个 RAC 两个节点的 CPU 都飙到 100%,OLTP 交易全部超时,客服电话被打爆。这篇文章把并行执行的内部机制、DOP 计算逻辑、Granule 分配、以及并行服务器争用的治理方法拆清楚。

1 并行执行不是魔法,是进程分工

并行执行的本质是:把一个大的任务拆成多个小任务,分配给多个并行服务器进程(Parallel Server Process,简称 PX Server)同时执行。比如全表扫描 10 亿行,开一个进程要扫 10 亿行;开 16 个进程,每个进程扫 6250 万行,理论上快 16 倍。但"理论上"三个字很重要——实际加速比通常只有 DOP 的 60-80%,因为并行有协调开销。

并行执行的协调者是 Query Coordinator(QC),它是用户会话的 server process。QC 不负责实际数据扫描,只负责:解析 SQL、决定 DOP(Degree of Parallelism)、向 PX Server 分配任务、收集 PX Server 的结果并返回给客户端。PX Server 是后台进程,从进程池(Parallel Server Pool)里分配。进程池的大小由 PARALLEL_MAX_SERVERS 控制,默认是 5 × CPU_COUNT × PARALLEL_THREADS_PER_CPU,比如 32 核机器,默认进程池约 160 个。如果并行请求太多,进程池耗尽,新的并行请求会被串行执行或排队。

2 DOP 是怎么算出来的:不是你想开几就开几

DOP 不是 SQL 里写 PARALLEL(16) 就一定是 16,Oracle 有一套复杂的 DOP 计算逻辑:第一步,取 SQL 的 Hint DOP(比如 /*+ PARALLEL(16) */)、表级的 PARALLEL 属性(ALTER TABLE ... PARALLEL 16)、会话级的 PARALLEL_DEGREE_POLICY(MANUAL/LIMITED/AUTO)、以及系统级的 PARALLEL_MAX_SERVERS。第二步,如果 PARALLEL_DEGREE_POLICY = AUTO,Oracle 会根据表大小、系统负载、I/O 带宽自动调整 DOP。比如表只有 100 万行,即使你写了 PARALLEL(16),Oracle 也可能只开 2 个并行进程,因为"不值得"。第三步,检查资源管理器(DBMS_RESOURCE_MANAGER)的限制。如果用户所在的 Consumer Group 限制了最大 DOP = 8,即使 SQL 要求 16,也会被压到 8。第四步,检查进程池剩余量。如果进程池只剩 4 个进程,DOP 会被降到 4。所以最终 DOP 是这四个因素的最小值。

我们那个 PARALLEL 64 搞瘫 RAC 的案例,根因就是 DOP 计算没被限制。客户的报表 SQL 写了 PARALLEL(64),表级属性也是 PARALLEL 64,会话级 PARALLEL_DEGREE_POLICY = MANUAL(不自动限制),资源管理器没配,进程池 PARALLEL_MAX_SERVERS = 200(够开 64)。结果 64 个 PX Server 同时跑,每个占一个 CPU 核心,RAC 两个节点各 32 核,全部被占满。OLTP 交易进不来,因为 CPU scheduler 里 PX Server 的优先级跟普通进程一样,而且 PX Server 的数量是 OLTP 会话的几十倍,调度器偏向 PX Server。解决办法:给报表用户配资源管理器,限制最大 DOP = 8,并且限制并行执行只能在一个节点上跑(INSTANCE_GROUP)。这样报表只影响一个节点,另一个节点留给 OLTP。

3 Granule 分配:并行执行的"任务切片"

Oracle 把并行任务拆成 Granule(颗粒),每个 Granule 是一个工作单元。对于全表扫描,Granule 通常是数据块的集合,大小由 PARALLEL_MIN_TIME_THRESHOLD 和表大小决定。比如 10 亿行的表,DOP=16,Oracle 可能把表分成 64 个 Granule(每个 Granule 约 1560 万行),16 个 PX Server 各领 4 个 Granule,做完再领下一个。这种动态分配叫"游程分配"(Round-Robin),能平衡各进程的工作量,避免某个进程拖后腿。但如果数据分布不均匀(比如某个分区特别大),Granule 的均衡性就会受影响,出现长尾。

对于索引范围扫描,Granule 的划分更复杂。Oracle 需要把索引键值的范围切成 DOP 份,每份对应一个 Granule。如果索引键值分布不均匀(比如 80% 的数据集中在 20% 的键值范围),某些 Granule 的数据量会远大于其他 Granule,并行加速比大幅下降。我们测过一个案例:按 status 列并行扫描,80% 的数据 status='DONE',20% 是其他。DOP=8 时,负责 'DONE' 范围的 PX Server 处理了 80% 的数据,其他 7 个进程只处理了 20%,实际加速比只有 2.5 倍(理论 8 倍)。解决办法是:并行键选分布均匀的列(比如 ID、GUID),别选分布倾斜的列(比如状态、类型)。

4 实验:DOP 与加速比的非线性关系

实验环境:Oracle 19c,32 核物理机,128G 内存,NVMe SSD。测试表 sales,10 亿行,100GB。测试 SQL:SELECT COUNT(*), SUM(amount) FROM sales WHERE create_time > SYSDATE - 365。对比 DOP = 1, 2, 4, 8, 16, 32 的执行时间和 CPU 占用。

-- 设置并行度
ALTER SESSION FORCE PARALLEL QUERY PARALLEL 8;

-- 执行查询并收集统计
SELECT /*+ PARALLEL(sales 8) */ COUNT(*), SUM(amount)
FROM   sales WHERE create_time > SYSDATE - 365;

-- 查看实际 DOP
SELECT sql_id, px_servers_executions, px_max_dop
FROM   v$sql WHERE sql_id = 'a1b2c3d4';
-- px_max_dop: 8(实际使用的 DOP)

-- 查看 PX Server 的等待事件
SELECT sid, serial#, qcinst_id, qcsid, server_group, server_set,
       degree, req_degree
FROM   v$px_session WHERE qcsid = (SELECT sid FROM v$mystat WHERE rownum=1);

-- 查看 CPU 使用(OS 层)
mpstat -P ALL 1 10 | grep Average

实验结果:DOP=1:时间 480 秒,CPU 占用 100%(单核跑满)。DOP=2:时间 245 秒,CPU 200%,加速比 1.96(接近线性)。DOP=4:时间 130 秒,CPU 380%,加速比 3.69(开始衰减)。DOP=8:时间 75 秒,CPU 650%,加速比 6.4(明显衰减)。DOP=16:时间 48 秒,CPU 1100%,加速比 10(衰减严重)。DOP=32:时间 38 秒,CPU 1800%,加速比 12.6(边际效应极低)。为什么 DOP 越高,加速比越差?因为 Granule 协调开销、QC 收集结果的开销、进程间通信(IPC)开销都在增加。DOP=32 时,32 个进程的结果要汇总到 QC,QC 的内存和 CPU 成为瓶颈。另外,DOP=32 时,CPU 占用 1800%(18 个核跑满),其他 SQL 基本抢不到 CPU,系统整体吞吐量反而下降。甜点区在 DOP=4-8:加速比 4-6 倍,CPU 占用 400-650%,留一半 CPU 给其他任务。

【踩坑笔记】并行执行的甜点区通常是 CPU 核心数的 1/4 到 1/2。32 核机器,DOP 设 8-16 最合理。超过 CPU 核心数,加速比不会线性增长,反而会因调度开销下降。而且并行查询的 DOP 和并行 DML 的 DOP 要分开控制:查询可以开大一点(只读,不影响数据),DML 必须开小(写操作加锁,冲突多)。

5 并行服务器争用治理:资源隔离与队列管理

并行服务器争用是生产环境的常见问题:报表 SQL 开 16 个 PX Server,ETL 也开 16 个,备份也开 8 个,进程池 160 个瞬间耗尽,新的并行请求只能串行执行或报错。治理方法有三层:第一层,资源管理器(Resource Manager)限制每个用户的最大 DOP 和并行进程数。比如报表用户最大 DOP=8,ETL 用户最大 DOP=4,确保总并行进程不超过 CPU 核心数。第二层,PARALLEL_STATEMENT_QUEUING(语句排队)。11g 引入的特性,当进程池耗尽时,新的并行 SQL 不报错,而是进入队列等待,等前面的并行 SQL 释放进程后再执行。这避免了"全挤进来全卡住"的混乱局面。但排队时间可能很长,需要监控 V$SQL_MONITOR 的 QUEUING_TIME。第三层,INSTANCE_GROUP 和 RAC 节点隔离。把并行 SQL 限制在特定节点上执行,避免跨节点通信(IPC)开销,也避免一个节点的并行影响其他节点。比如节点 1 专门跑并行报表,节点 2 专门跑 OLTP,通过 service 和 instance_group 绑定。

我们给一个混合负载客户做的架构是:RAC 三节点,节点 1 和 2 跑 OLTP(DOP 限制 2),节点 3 跑报表和 ETL(DOP 限制 16)。通过 DBMS_SERVICE 配置 service 的 preferred instance,OLTP 应用连到节点 1/2,报表工具连到节点 3。节点 3 即使 CPU 跑满,也不影响节点 1/2 的交易。这种物理隔离虽然浪费了节点 3 的 OLTP 能力,但换来了整体稳定性,客户觉得值。最后送一句话:并行执行是"兴奋剂",偶尔用可以破纪录,天天用会猝死。控制好剂量(DOP),分好赛道(资源隔离),才能既快又稳。


请使用浏览器的分享功能分享到微信等