|
| 1 | +## 分布式DB中数据倾斜的原因和解法 - 阿里云HybridDB for PostgreSQL最佳实践 |
| 2 | + |
| 3 | +### 作者 |
| 4 | +digoal |
| 5 | + |
| 6 | +### 日期 |
| 7 | +2017-08-21 |
| 8 | + |
| 9 | +### 标签 |
| 10 | +PostgreSQL , Greenplum , query倾斜 , 存储倾斜 , OOM , disk full , 短板 , 数据分布 |
| 11 | + |
| 12 | +---- |
| 13 | + |
| 14 | +## 背景 |
| 15 | +对于分布式数据库来说,QUERY的运行效率取决于最慢的那个节点。 |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +当数据出现倾斜时,某些节点的运算量可能比其他节点大。除了带来运行慢的问题,还有其他的问题,例如导致OOM,或者DISK FULL等问题。 |
| 20 | + |
| 21 | +## 出现数据倾斜的原因和解决办法 |
| 22 | +1、分布键选择不正确,导致数据存储分布不均。 |
| 23 | + |
| 24 | +例如选择的字段某些值特别多,由于数据是按分布键VALUE的HASH进行分布的,导致这些值所在的SEGMENT的数据可能比而其他SEGMENT多很多。 |
| 25 | + |
| 26 | +分布键的选择详见: |
| 27 | + |
| 28 | +[《Greenplum 最佳实践 - 数据分布黄金法则 - 分布列与分区的选择》](../201607/20160719_02.md) |
| 29 | + |
| 30 | +2、查询导致的数据重分布,数据重分布后,数据不均。 |
| 31 | + |
| 32 | +例如group by的字段不是分布键,那么运算时就需要重分布数据。 |
| 33 | + |
| 34 | +解决办法1: |
| 35 | + |
| 36 | +由于查询带来的数据倾斜的可能性非常大,所以Greenplum在内核层面做了优化,做法是: |
| 37 | + |
| 38 | +先在segment本地聚合产生少量记录,将聚合结果再次重分布,重分布后再次在segment聚合,最后将结果发到master节点,有必要的话在master节点调用聚合函数的final func(已经是很少的记录数和运算量)。 |
| 39 | + |
| 40 | +例子: |
| 41 | + |
| 42 | +tbl_ao_col表是c1的分布键,但是我们group by使用了c398字段,因此看看它是怎么做的呢?请看执行计划的解释。 |
| 43 | + |
| 44 | +``` |
| 45 | +postgres=# explain analyze select c398,count(*),sum(c399),avg(c399),min(c399),max(c399) from tbl_ao_col group by c398; |
| 46 | + QUERY PLAN |
| 47 | +-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 48 | + Gather Motion 48:1 (slice2; segments: 48) (cost=123364.18..123582.28 rows=9693 width=96) |
| 49 | + // 返回结果 |
| 50 | + Rows out: 10001 rows at destination with 120 ms to end, start offset by 1.921 ms. |
| 51 | + -> HashAggregate (cost=123364.18..123582.28 rows=202 width=96) |
| 52 | + // 重分布后再次聚合。 |
| 53 | + Group By: tbl_ao_col.c398 |
| 54 | + Rows out: Avg 208.4 rows x 48 workers. Max 223 rows (seg17) with 0.001 ms to first row, 54 ms to end, start offset by 35 ms. |
| 55 | + -> Redistribute Motion 48:48 (slice1; segments: 48) (cost=122928.00..123121.86 rows=202 width=96) |
| 56 | + // 第一次聚合后,记录数以及降低到了几千行,因此重分布后即使出现倾斜,关系也不大。 |
| 57 | + Hash Key: tbl_ao_col.c398 |
| 58 | + Rows out: Avg 8762.2 rows x 48 workers at destination. Max 9422 rows (seg46) with 31 ms to end, start offset by 63 ms. |
| 59 | + -> HashAggregate (cost=122928.00..122928.00 rows=202 width=96) |
| 60 | + // 这一步是在segment节点聚合 |
| 61 | + Group By: tbl_ao_col.c398 |
| 62 | + Rows out: Avg 8762.2 rows x 48 workers. Max 8835 rows (seg2) with 0.004 ms to first row, 8.004 ms to end, start offset by 82 ms. |
| 63 | + -> Append-only Columnar Scan on tbl_ao_col (cost=0.00..107928.00 rows=20834 width=16) |
| 64 | + Rows out: 0 rows (seg0) with 28 ms to end, start offset by 64 ms. |
| 65 | + Slice statistics: |
| 66 | + (slice0) Executor memory: 377K bytes. |
| 67 | + (slice1) Executor memory: 1272K bytes avg x 48 workers, 1272K bytes max (seg0). |
| 68 | + (slice2) Executor memory: 414K bytes avg x 48 workers, 414K bytes max (seg0). |
| 69 | + Statement statistics: |
| 70 | + Memory used: 128000K bytes |
| 71 | + Settings: optimizer=off |
| 72 | + Optimizer status: legacy query optimizer |
| 73 | + Total runtime: 122.173 ms |
| 74 | +(22 rows) |
| 75 | +``` |
| 76 | + |
| 77 | +对于非分布键的分组聚合请求,Greenplum采用了多阶段聚合如下: |
| 78 | + |
| 79 | +第一阶段,在SEGMENT本地聚合。(需要扫描所有数据,这里不同存储,前面的列和后面的列的差别就体现出来了,行存储的deform开销,在对后面的列进行统计时性能影响很明显。) |
| 80 | + |
| 81 | +第二阶段,根据分组字段,将结果数据重分布。(重分布需要用到的字段,此时结果很小。) |
| 82 | + |
| 83 | +第三阶段,再次在SEGMENT本地聚合。(需要对重分布后的数据进行聚合。) |
| 84 | + |
| 85 | +第四阶段,返回结果给master,有必要的话master节点调用聚合函数的final func(已经是很少的记录数和运算量)。 |
| 86 | + |
| 87 | +3、内核只能解决一部分查询引入的数据重分布倾斜问题,还有一部分问题内核没法解决。例如窗口查询。 |
| 88 | + |
| 89 | +``` |
| 90 | +postgres=# explain select * from (select row_number() over (partition by c2 order by c3) as rn , * from tbl_ao_col) t where rn=1; |
| 91 | + QUERY PLAN |
| 92 | +------------------------------------------------------------------------------------------------------------------------- |
| 93 | + Gather Motion 48:1 (slice2; segments: 48) (cost=5294619.34..5314619.34 rows=1000 width=3208) |
| 94 | + -> Subquery Scan t (cost=5294619.34..5314619.34 rows=21 width=3208) |
| 95 | + Filter: rn = 1 |
| 96 | + -> Window (cost=5294619.34..5302119.34 rows=20834 width=3200) |
| 97 | + Partition By: tbl_ao_col.c2 |
| 98 | + Order By: tbl_ao_col.c3 |
| 99 | + -> Sort (cost=5294619.34..5297119.34 rows=20834 width=3200) |
| 100 | + Sort Key: tbl_ao_col.c2, tbl_ao_col.c3 |
| 101 | + -> Redistribute Motion 48:48 (slice1; segments: 48) (cost=0.00..127928.00 rows=20834 width=3200) |
| 102 | + 如果c2的数据倾斜很严重,会导致某个SEGMENT节点的数据过多。后面的计算截断可能造成OOM或者disk full。 |
| 103 | + Hash Key: tbl_ao_col.c2 |
| 104 | + -> Append-only Columnar Scan on tbl_ao_col (cost=0.00..107928.00 rows=20834 width=3200) |
| 105 | + Settings: optimizer=off |
| 106 | + Optimizer status: legacy query optimizer |
| 107 | +(13 rows) |
| 108 | +``` |
| 109 | + |
| 110 | +使用窗口函数时,Greenplum需要先按窗口中的分组对数据进行重分布,这一次重分布就可能导致严重的倾斜。 |
| 111 | + |
| 112 | +实际上内核层优化才是最好的解决办法,例如以上窗口函数,由于我们只需要取c2分组中c3最小的一条记录。因此也可以在每个节点先取得一条,再重分布,再算。 |
| 113 | + |
| 114 | +不通过修改内核,还有什么方法呢? |
| 115 | + |
| 116 | +3\.1 Mapreduce任务就很好解决,Greenplum的mapreduce接口调用方法如下: |
| 117 | + |
| 118 | +http://greenplum.org/docs/ref_guide/yaml_spec.html |
| 119 | + |
| 120 | +3\.2 通过写PL函数也能解决。例如 |
| 121 | + |
| 122 | +``` |
| 123 | +declare |
| 124 | + v_c2 int; |
| 125 | + v_t tbl_ao_col; |
| 126 | +begin |
| 127 | + for v_c2 in select c2 from tbl_ao_col group by c2 |
| 128 | + loop -- 引入多次扫描数据的成本,其实是不划算的,还是内核解决最棒。 |
| 129 | + select t into v_t from tbl_ao_col as t where c2=v_c2 order by c3 limit 1; |
| 130 | + return next v_t; |
| 131 | + end loop; |
| 132 | +end; |
| 133 | +``` |
| 134 | + |
| 135 | +## 小结 |
| 136 | +数据倾斜的原因可能是数据存储的倾斜,QUERY执行过程中数据重分布的倾斜。 |
| 137 | + |
| 138 | +数据倾斜可能引入以下后果: |
| 139 | + |
| 140 | +1、计算短板 |
| 141 | + |
| 142 | +2、oom |
| 143 | + |
| 144 | +3、disk full |
| 145 | + |
| 146 | +数据倾斜的解决办法: |
| 147 | + |
| 148 | +1、如果是存储的倾斜,通过调整更加均匀的分布键来解决。(也可以选择使用随机分布,或者使用多列作为分布键)。 |
| 149 | + |
| 150 | +2、如果是QUERY造成的倾斜,Greenplum内核对group by已经做了优化,即使分组字段不是分布键,通过多阶段聚合,可以消除影响。 |
| 151 | + |
| 152 | +3、如果是窗口函数QUERY造成的倾斜,目前内核没有对这部分优化,首先会对窗口函数的分组字段所有数据进行重分布,如果这个分组字段数据有严重倾斜,那么会造成重分布后的某些节点数据量过大。解决办法有mapreduce或pl函数。 |
| 153 | + |
| 154 | +## 参考 |
| 155 | +[《Greenplum 内存与负载管理最佳实践》](../201708/20170821_01.md) |
| 156 | + |
| 157 | +[《Greenplum 最佳实践 - 数据分布黄金法则 - 分布列与分区的选择》](../201607/20160719_02.md) |
0 commit comments