[Critical bug] TiDB 并行aggregation spill 可能会导致结果出错
问题
已打开 05:32AM - 08 Aug 24 UTC
已关闭 06:46AM - 12 Aug 24 UTC
type/bug
sig/execution
severity/critical
affects-8.0
affects-8.1
impact/wrong-result
affects-8.2
## Bug Report
Please answer these questions before submitting your issue. Tha… nks!
### 1. Minimal reproduce step (Required)
for a simple query like
```
select count(*), id from t group by id
```
if parallel spill is triggered, it will return wrong result.
### 2. What did you expect to see? (Required)
### 3. What did you see instead (Required)
### 4. What is your TiDB version? (Required)
对于并行模式的 hash aggregation 算子(hash aggregation 算子默认即为并行模式),如果触发了 spil,对于特定的aggregation 函数(count 和 avg ),会返回错误的结果
根因
在 TiDB aggregtion 算子的并行 spill 实现中,存在一个 bug 导致在以下两种情况下,最终的结果会是错误的
如果 hash aggregation 算子中带有 count 函数
如果 hash aggregation 算子没有被下推到 TiKV/TiFlash,且 hash aggregation 算子中带有 avg 函数
诊断步骤
判断 hash aggregation 算子是否在 parallel 模式下触发了 spill.
这个判断主要依据是 explain analyze 的结果。在 explain analyze 结果中,对于 hash aggregation 算子,如果 partial_worker 或者 final_worker 的并发度大于 1,说明这个 aggregation 运行在 parallel 模式。如果 hash aggregation 算子的 disk usage 不是 N/A, 说明该 hash aggregation 触发了 spill。
以下面的 explain analyze 结果为例,该 query 中的 hash aggregation 触发了 parallel spill,因为:
partial_worker 和 final_worker 的并发度是 5
hash aggregation 算子的 disk usage 是 1.85 GB
mysql> explain analyze select count(), value from spill_test group by value having count( ) > 0;
±-----------------------------±------------±---------±----------±-----------------±----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------±------------------------------------------------------------------------------------------------------------------------------±--------±--------+
| id | estRows | actRows | task | access object | execution info | operator info | memory | disk |
±-----------------------------±------------±---------±----------±-----------------±----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------±------------------------------------------------------------------------------------------------------------------------------±--------±--------+
| Selection_7 | 6848921.60 | 0 | root | | time:2m33.6s, loops:1, RU:44821.700304 | gt(Column#4, 0) | 61.7 KB | N/A |
| └─HashAgg_12 | 8561152.00 | 17099136 | root | | time:2m33.3s, loops:16702, partial_worker:{wall_time:15.445530168s, concurrency:5, task_num:625, tot_wait:32.672876439s, tot_exec:44.553148301s, tot_time:1m17.22698924s, max:15.445404668s, p95:15.445404668s}, final_worker:{wall_time:2m33.608198045s, concurrency:5, task_num:5, tot_wait:2.8µs, tot_exec:0s, tot_time:12m45.060517323s, max:2m33.608064545s, p95:2m33.608064545s} | group by:test.spill_test.value, funcs:count(Column#7)->Column#4, funcs:firstrow(test.spill_test.value)->test.spill_test.value | 3.24 GB | 1.85 GB |
| └─TableReader_13 | 8561152.00 | 17099216 | root | | time:3.07s, loops:626, cop_task: {num: 625, max: 632.4ms, min: 1.04ms, avg: 135.4ms, p95: 444.5ms, max_proc_keys: 51200, p95_proc_keys: 51200, tot_proc: 1m12.3s, tot_wait: 252ms, copr_cache_hit_ratio: 0.00, build_task_duration: 48.8µs, max_distsql_concurrency: 15}, rpc_info:{Cop:{num_rpc:625, total_time:1m24.6s}} | data:HashAgg_8 | 40.3 MB | N/A |
| └─HashAgg_8 | 8561152.00 | 17099216 | cop[tikv] | | tikv_task:{proc max:620ms, min:0s, avg: 123.8ms, p80:240ms, p95:400ms, iters:16740, tasks:625}, scan_detail: {total_process_keys: 17122304, total_process_keys_size: 1338451200, total_keys: 17122929, get_snapshot_time: 68.6ms, rocksdb: {key_skipped_count: 17122304, block: {cache_hit_count: 4366, read_count: 47582, read_byte: 126.9 MB, read_time: 28.3s}}}, time_detail: {total_process_time: 1m12.3s, total_suspend_time: 7.42s, total_wait_time: 252ms, total_kv_read_wall_time: 52.1s, tikv_wall_time: 1m20.8s} | group by:test.spill_test.value, funcs:count(1)->Column#7 | N/A | N/A |
| └─TableFullScan_11 | 17122304.00 | 17122304 | cop[tikv] | table:spill_test | tikv_task:{proc max:450ms, min:0s, avg: 83.4ms, p80:160ms, p95:300ms, iters:16740, tasks:625} | keep order:false | N/A | N/A |
±-----------------------------±------------±---------±----------±-----------------±----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------±------------------------------------------------------------------------------------------------------------------------------±--------±--------+
5 rows in set (2 min 33.87 sec)
判断 hash aggregation 是否下推到 TiKV/TiFlash
这个判断主要基于 query 的 plan。如果一个 hash aggregation 被下推到了 TiKV/TiFlash,它会被转化成一个两阶段的 hash aggregation。其中一个 hash aggregation 算子的 task type 是 cop[tikv]/cop[tiflash]/batchcop[tiflash]/mpp[tiflash],另一个 hash aggregation 算子的 task type 是 root
以下面两个 query 的 plan 为例,plan 1 中 hash aggregation 被下推到了 TiKV,plan 2 中 hash aggregation 没有被下推到TiKV
plan 1
mysql> explain select count(), value from spill_test group by value having count( ) > 0;
±-----------------------------±------------±----------±-----------------±------------------------------------------------------------------------------------------------------------------------------+
| id | estRows | task | access object | operator info |
±-----------------------------±------------±----------±-----------------±------------------------------------------------------------------------------------------------------------------------------+
| Selection_7 | 6848921.60 | root | | gt(Column#4, 0) |
| └─HashAgg_12 | 8561152.00 | root | | group by:test.spill_test.value, funcs:count(Column#7)->Column#4, funcs:firstrow(test.spill_test.value)->test.spill_test.value |
| └─TableReader_13 | 8561152.00 | root | | data:HashAgg_8 |
| └─HashAgg_8 | 8561152.00 | cop[tikv] | | group by:test.spill_test.value, funcs:count(1)->Column#7 |
| └─TableFullScan_11 | 17122304.00 | cop[tikv] | table:spill_test | keep order:false |
±-----------------------------±------------±----------±-----------------±------------------------------------------------------------------------------------------------------------------------------+
plan 2
mysql> explain select count(), value from spill_test group by value having count( ) > 0;
±---------------------------±------------±----------±-----------------±-----------------------------------------------------------------------------------------------------------------------+
| id | estRows | task | access object | operator info |
±---------------------------±------------±----------±-----------------±-----------------------------------------------------------------------------------------------------------------------+
| Selection_7 | 13697843.20 | root | | gt(Column#4, 0) |
| └─HashAgg_10 | 17122304.00 | root | | group by:test.spill_test.value, funcs:count(1)->Column#4, funcs:firstrow(test.spill_test.value)->test.spill_test.value |
| └─TableReader_15 | 17122304.00 | root | | data:TableFullScan_14 |
| └─TableFullScan_14 | 17122304.00 | cop[tikv] | table:spill_test | keep order:false |
±---------------------------±------------±----------±-----------------±-----------------------------------------------------------------------------------------------------------------------+
检查 hash aggregation 算子中是否包含可能会输出错误结果的 aggregation 函数
当 hash aggregation 触发了 parallel spill 时
如果 hash aggregation 被下推到 TiKV/TiFlash: count 函数会返回错误结果
如果 hash aggregation 没有被下推到 TiKV/TiFlash: count 和 avg 函数都会返回错误的结果
解决方法
在 v8.1.0 中,除了关闭 hash aggregation 的 parallel spill 外没有其他解决方法
这个 bug 会在 v8.1.1 中修复
绕过方法
关闭 hash aggregation 的 parallel spill 功能:
set tidb_enable_parallel_hashagg_spill=0;
影响版本
v8.0.0
v8.1.0
v8.2.0
修复版本
v8.1.1
v8.3.0
补充说明
Parallel hash agg spill 在 v8.1.0 中并未 GA,所以不推荐在 v8.1.x 集群中在生产环境中默认启用 parallel hash agg spill 的特性
在 v8.1.1 中我们会将 tidb_enable_parallel_hashagg_spill 的默认值改成 off。所以对于 v8.1.x(x != 0) 的新装集群,parallel hash agg spill 功能都会默认关闭。但是对于从 v8.0.0/v8.1.0 升级到 v8.1.x 的集群,tidb_enable_parallel_hashagg_spill 会保持集群的原有值(默认是 on),可以通过以下命令来显式关闭 parallel hash agg 的 spill
set @@global.tidb_enable_parallel_hashagg_spill=0;