计算倾斜排查与解决

更新时间:

云原生数据仓库AnalyticDB PostgreSQL中,计算倾斜是指由于数据分布不均或查询逻辑设计不合理,导致实例中部分计算节点负载显著高于其他节点的现象,这种不均衡会造成资源利用率降低、查询性能下降,以及高负载节点容易出现内存溢出或磁盘IO瓶颈等问题。本文为您介绍如何识别并缓解MPP数据库中的计算倾斜问题,从而提升集群整体性能和稳定性。

常见现象

通过监控或报错可观察到以下现象:

  • CPU:执行SQL时,单个或部分节点的CPU明显高于其他节点。

  • 内存:执行SQL时,单个或部分节点的内存水位明显高于其他节点,可能伴随报错ERROR: Canceling query because of high VMEM usage.

  • 磁盘:执行SQL时,单个或部分节点的落盘文件大小明显高于其他节点,可能伴随报错ERROR: workfile per segment size limit exceeded.

诊断方法

根据SQL的执行状态,采用不同的诊断方法。

SQL正常执行

SQL能够正常执行时,可以通过EXPLAIN ANALYZE来查看该SQL的具体信息。

  1. 在该SQL前添加以下语句,用于打印每个计算节点(Segment)的统计信息。

    SET gp_enable_explain_allstat TO ON;

    例如:

    SET gp_enable_explain_allstat TO ON;
    EXPLAIN ANALYZE <sql>;
  2. 在输出中定位allstat字段。

    格式为allstat: seg_firststart_total_ntuples,每个Segment输出三项指标,包含firststart(毫秒)、total(毫秒)和ntuples(处理的行数)。不同Segment用斜杠分隔,各指标以下划线分隔,末尾的 //end 表示该统计块结束。如果某Segment处理行数远高于其他节点,即存在计算倾斜。

    例如:

    allstat: .../seg1_0.618ms_3.569ms_100000/...//end  

案例

EXPLAIN ANALYZE输出结果如下:

                                                                            QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=0.00..862.00 rows=1 width=8) (actual time=54.648..54.649 rows=1 loops=1)
   allstat: seg_firststart_total_ntuples/seg-1_0.433 ms_55 ms_1//end
   ->  Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..862.00 rows=2 width=1) (actual time=40.810..54.268 rows=599999 loops=1)
         allstat: seg_firststart_total_ntuples/seg-1_0.435 ms_54 ms_599999//end
         ->  Hash Left Join  (cost=0.00..862.00 rows=1 width=1) (actual time=20.366..54.055 rows=266546 loops=1)
               Hash Cond: (skew_hashjoin1.a = skew_hashjoin2.c)
               Extra Text: (seg1)   Hash chain length 100000.0 avg, 100000 max, using 1 of 131072 buckets.
               allstat: seg_firststart_total_ntuples/seg0_0.611 ms_41 ms_166579/seg1_0.618 ms_54 ms_266546/seg2_0.658 ms_40 ms_166874//end
               ->  Redistribute Motion 3:3  (slice2; segments: 3)  (cost=0.00..431.00 rows=1 width=8) (actual time=0.023..36.498 rows=166874 loops=1)
                     Hash Key: skew_hashjoin1.a
                     allstat: seg_firststart_total_ntuples/seg0_4.405 ms_37 ms_166579/seg1_7.684 ms_5.770 ms_166547/seg2_4.415 ms_36 ms_166874//end
                     ->  Seq Scan on skew_hashjoin1  (cost=0.00..431.00 rows=1 width=8) (actual time=0.055..4.708 rows=166874 loops=1)
                           allstat: seg_firststart_total_ntuples/seg0_1.946 ms_4.398 ms_166579/seg1_1.941 ms_4.789 ms_166547/seg2_1.943 ms_4.708 ms_166874//end
               ->  Hash  (cost=431.00..431.00 rows=1 width=8) (actual time=7.064..7.064 rows=100000 loops=1)
                     Buckets: 1  Batches: 1  Memory Usage: 5305kB
                     allstat: seg_firststart_total_ntuples/seg0_0.612 ms_3.792 ms_0/seg1_0.618 ms_7.064 ms_100000/seg2_0.659 ms_3.755 ms_0//end
                     ->  Redistribute Motion 3:3  (slice3; segments: 3)  (cost=0.00..431.00 rows=1 width=8) (actual time=2.895..3.569 rows=100000 loops=1)
                           Hash Key: skew_hashjoin2.c
                           allstat: seg_firststart_total_ntuples/seg0_0.612 ms_3.790 ms_0/seg1_0.618 ms_3.569 ms_100000/seg2_0.659 ms_3.752 ms_0//end
                           ->  Seq Scan on skew_hashjoin2  (cost=0.00..431.00 rows=1 width=8) (actual time=0.050..0.915 rows=33462 loops=1)
                                 allstat: seg_firststart_total_ntuples/seg0_1.946 ms_0.915 ms_33462/seg1_1.925 ms_1.013 ms_33327/seg2_1.923 ms_0.989 ms_33211//end
 Optimizer: GPORCA
 Planning Time: 4.612 ms
   (slice0)    Executor memory: 134K bytes.
   (slice1)    Executor memory: 2691K bytes avg x 3 workers, 7300K bytes max (seg1).  Work_mem: 5305K bytes max.
   (slice2)    Executor memory: 233K bytes avg x 3 workers, 233K bytes max (seg0).
   (slice3)    Executor memory: 233K bytes avg x 3 workers, 233K bytes max (seg0).
 Memory used:  4194304kB
 Execution Time: 55.433 ms
(29 rows)

定位到allstat字段,统计信息如下:

allstat: seg_firststart_total_ntuples/seg0_0.612 ms_3.790 ms_0/seg1_0.618 ms_3.569 ms_100000/seg2_0.659 ms_3.752 ms_0//end

含义为:

  • seg0: firststart=0.612 ms,total=3.790 ms,ntuples=0

  • seg1: firststart=0.618 ms,total=3.569 ms,ntuples=100000

  • seg2: firststart=0.659 ms,total=3.752 ms,ntuples=0

数据重分布(Redistribute Motion)将实际数据集中在seg1(处理了 100000 行),其他Segment没有处理数据。可以判断出现了计算倾斜

SQL执行报错

SQL执行出现如下报错,且监控中发现部分节点内存明显高于其他节点,可通过以下步骤诊断。

ERROR:  Canceling query because of high VMEM usage. Used: 6975MB, available 25MB, red zone: 6300MB  (seg0 slice1 xxx.xxx.xxx.xxx:xxxx pid=xxx)
重要

计算倾斜需要结合监控具体判断,并非出现该报错一定为计算倾斜,同时计算倾斜并非必定出现上述报错信息。本节以上述报错为例,介绍诊断步骤。

  1. 使用EXPLAIN查看执行计划。

    EXPLAIN statement;

    上述报错的SQL计划如下:

                                                     QUERY PLAN                                                 
    ------------------------------------------------------------------------------------------------------------
     Aggregate  (cost=0.00..862.00 rows=1 width=8)
       ->  Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..862.00 rows=2 width=1)
             ->  Hash Left Join  (cost=0.00..862.00 rows=1 width=1)
                   Hash Cond: (skew_hashjoin1.a = skew_hashjoin2.c)
                   ->  Redistribute Motion 3:3  (slice2; segments: 3)  (cost=0.00..431.00 rows=1 width=8)
                         Hash Key: skew_hashjoin1.a
                         ->  Seq Scan on skew_hashjoin1  (cost=0.00..431.00 rows=1 width=8)
                   ->  Hash  (cost=431.00..431.00 rows=1 width=8)
                         ->  Redistribute Motion 3:3  (slice3; segments: 3)  (cost=0.00..431.00 rows=1 width=8)
                               Hash Key: skew_hashjoin2.c
                               ->  Seq Scan on skew_hashjoin2  (cost=0.00..431.00 rows=1 width=8)
     Optimizer: GPORCA
    (12 rows)

    使用EXPLAIN阅读查询计划:上述计划表明查询执行分为三个Slice。其中,slice1执行Hash Left Join操作,左表所在slice2按照分布键skew_hashjoin1.a重分布,右表所在slice3按照skew_hashjoin2.c键进行重分布。

  2. 验证数据分布。

    分析关联键的数据分布是否存在热点值,如果查询结果显示某个或某几个值的 cnt 远高于其他值,即可确认存在数据倾斜,从而导致了计算倾斜。

    SELECT count(*) AS cnt, 
           distribute_key_1, 
           distribute_key_2, 
           ..., 
           distribute_key_n
    FROM [ table | sub_select ]
    GROUP BY distribute_key_1, distribute_key_2, ... distribute_key_n
    ORDER BY cnt DESC
    LIMIT N;

    例如:

    -- 验证slice1下左表经过shuffle后的数据分布
    SELECT count(*) AS cnt, 
           skew_hashjoin1.a
    FROM skew_hashjoin1
    GROUP BY skew_hashjoin1.a
    ORDER BY cnt DESC
    LIMIT 10;
    
    -- 验证右表slice2经过shuffle后的数据分布
    SELECT count(*) AS cnt, 
           skew_hashjoin2.c
    FROM skew_hashjoin2
    GROUP BY skew_hashjoin2.c
    ORDER BY cnt DESC
    LIMIT 10;

    返回示例如下,从该结果可以发现skew_hashjoin2中的c值倾斜。由此断定该SQL发生计算倾斜。

    -- 验证slice1下左表经过shuffle后的数据分布
    cnt | a  
    -----+----
       1 | 10
       1 | 11
       1 | 13
       1 | 14
       1 | 17
       1 | 21
       1 | 52
       1 | 56
       1 | 58
       1 | 9
    (10 rows)
    
    -- 验证右表slice2经过shuffle后的数据分布
        cnt    | c 
    -----------+---
     100000000 | 1
    (1 row)

解决方案

计算倾斜在分布式场景下多数是由于计算发生重分布时,分布键发生值倾斜。因此,解决方案分为解决值倾斜和重分布两类。

解决值倾斜问题

  • 业务优化

    重分布的意义在于JOIN时将相同的连接键(Join key)分布在同一节点上,因此当连接键发生倾斜的值有匹配的数据时,建议从业务角度优化数据,从数据源避免键值集中。

  • NULL值倾斜处理

    • 当发生倾斜的连接键不存在匹配的数据时,例如Inner Join中的NULL值,此时可以在运行时将NULL值映射到一片不会被匹配上的值域中。例如,当连接键全为正数时,可以将NULL值映射到随机负值,使其可以在进行重分布时均匀分布至各个节点,同时也不会影响JOIN的结果,最后再将所有的负值重新转换为NULL值。

    • JOIN发生NULL值倾斜时, 建议排查对应业务SQL。例如,推荐使用EXISTS,尽量规避使用IN表达式。

解决重分布问题

如果表不存在分布倾斜,建议建表时将小表创建为复制表,避免运行时发生连接键的重分布。