# Manticore Search 中的并行块合并

Manticore Search 现在支持并行 RT 磁盘块合并和 N 路合并，在保持摄入吞吐量稳定的同时显著减少压缩时间。

从 **Manticore Search 24.4.0** 开始，RT 表压缩有了更强大的执行模型。优化现在支持两个重要改进：

- 磁盘块合并可以并行运行
- 每个合并任务可以一次合并超过两个块

- [parallel_chunk_merges](https://manual.manticoresearch.com/Server_settings/Searchd#parallel_chunk_merges): 同时可以运行多少 RT 磁盘块合并任务
- [merge_chunks_per_job](https://manual.manticoresearch.com/Server_settings/Searchd#merge_chunks_per_job): 一个任务一次可以合并多少 RT 磁盘块

压缩文档也已更新，将优化描述为由 **后台工作池** 处理的 **N 路合并**，而不是由单个串行合并线程处理。

## 为什么这很重要

对于 RT 工作负载，有趣的数字通常不仅仅是插入文档的速度，而是压缩赶上并使表返回到目标块数所需的时间。

这在以下情况下尤为明显：

- 你以持续速率摄入数据
- `optimize_cutoff` 足够低，合并会尽早触发
- 你等待压缩完成后再认为负载完全完成

这在两种常见情况下最为重要：

- 你正在将大量数据首次上传到实时表，并希望表不仅可搜索，而且在施加更多压力之前就已经压缩到其稳定状态
- 你定期摄入大批次数据，并希望每个批次在下一个批次到达之前完成

表在压缩完成前即可搜索，但“完全可搜索”和“完全优化”并不是一回事。如果关心保持表接近其目标形状、限制下一次摄入前的后台合并工作，或减少存储繁忙于加载后压缩的时间窗口，块数较高仍可能有影响。

为了展示差异，我们将 **1000 万文档** 加载到 RT 表中。每个文档包含：

- `id bigint`
- `name text` 包含 10 到 100 个单词的生成文本
- `type int`

表是这样创建的：

```sql
CREATE TABLE test(id bigint, name text, type int) optimize_cutoff='16'
```

因此目标是将表压缩回大约 16 个磁盘块。

为了基准测试，我们使用了 [manticore-load](/blog/manticore-load/)，我们的负载生成和基准测试工具。它对于重现此类场景、压力测试摄入和比较配置更改非常有用，无需每次构建自定义脚本。

数据是这样加载的：

```bash
manticore-load \
  --cache-gen-workers=5 \
  --drop \
  --batch-size=1000 \
  --threads=5 \
  --total=10000000 \
  --init="CREATE TABLE test(id bigint, name text, type int) optimize_cutoff='16'" \
  --load="INSERT INTO test(id,name,type) VALUES(<increment>,'<text/10/100>',<int/1/100>)" \
  --wait
```

## 之前：一个合并任务，每次合并两个块

强制使用旧行为时：

```bash
mysql -P9306 -h0 -e "set global parallel_chunk_merges=1; set global merge_chunks_per_job=2"
```

运行情况如下：

- 合并在 **14 秒** 时开始，此时已插入约 **180 万** 文档
- 所有 **1000 万** 文档在 **1 分 18 秒** 后加载完成
- 此时数据已完全可搜索
- 压缩在后台持续运行，直到 **3 分 23 秒**

在 `01:18` 时，表仍有超过 50 个块。加载接近尾声时状态如下：

```text
17:14:50  01:17     98%         133      128.4K   21%     5          53        1         4.22GB      9.9M
17:14:51  01:18     100%        131      310.9K   15%     1          53        1         4.27GB      10.0M
...
17:16:55  03:22     100%        0        49.4K    4%      1          17        1         4.27GB      10.0M
...
Total time:       03:23
```

这是健康摄入管道后跟一个漫长合并尾部的经典模式。

## 之后：并行合并加上更大的合并任务

使用新设置：

```bash
mysql -P9306 -h0 -e "set global parallel_chunk_merges=3; set global merge_chunks_per_job=5"
```

相同的工作负载完成得更快：

- 合并再次在约 **14 秒** 时开始
- 所有 **1000 万** 文档再次在约 **1 分 18 秒** 后加载完成
- 完全压缩仅在 **1 分 31 秒** 后完成

运行结束时状态如下：

```text
17:19:22  01:17     99%         127      127.9K   28%     6          26        1         4.22GB      9.9M
17:19:23  01:18     100%        132      1883.8K  17%     1          23        1         4.25GB      10.0M
...
17:19:36  01:31     100%        0        110.2K   3%      1          17        1         4.25GB      10.0M
...
Total time:       01:31
```

## 实践中的变化

摄入阶段本身大致相同：

- 旧设置：**1 分 18 秒** 加载所有数据
- 新设置：**1 分 18 秒** 加载所有数据

主要的增益来自摄入后的压缩：

- 旧设置：加载完成后约 **2 分 05 秒** 的额外合并时间
- 新设置：加载完成后约 **0 分 13 秒** 的额外合并时间

这大致意味着：

- **总时间降低约 55%**，从 **3 分 23 秒** 下降到 **1 分 31 秒**
- 在最后一个文档插入后，**合并尾部减少约 90%**

摄入期间的块压力也明显降低。加载接近尾声时：

- 旧设置：**53 个块**
- 新设置：**23 个块**

因此，改进不仅仅是压缩完成得更快。它在数据仍在插入时更积极地控制块数。

## 新的默认值如何？

在这个服务器上，使用新的默认设置且没有任何显式调整，相同的工作负载完成时间如下：

```text
Total time:       01:57
```

这已经大幅缩短了旧的 `03:23` 结果，同时仍为以下设置留有进一步调整的空间：

- `parallel_chunk_merges`
- `merge_chunks_per_job`

换句话说，新的默认值已经改善了开箱即用的体验，而具有足够 I/O 头部空间的系统可以通过谨慎增加这两个设置进一步推动压缩。

## 更广泛的基准结果：行式和列式存储

上面的 1000 万文档示例清楚地展示了机制，但更大的图景更加有趣。在更广泛的测试矩阵中，我们测量了行式和列式存储在多个值下的联合 **加载 + 优化** 时间：

- `parallel_chunk_merges`
- `merge_chunks_per_job`

主要结果是，在某些情况下，调整这些设置可以将总加载 + 优化时间减少：

- 行式存储最多 **4.6 倍**
- 列式存储最多 **6.8 倍**

这是该测试集中的最佳与最差对比：

| 存储方式 | 最佳设置 | 最佳时间 | 最慢设置 | 最慢时间 | 改进倍数 |
| --- | --- | --- | --- | --- | --- |
| 行式存储 | `parallel_chunk_merges=4`, `merge_chunks_per_job=5` | 14:35 | `parallel_chunk_merges=1`, `merge_chunks_per_job=2` | 67:15 | 4.61x |
| 列式存储 | `parallel_chunk_merges=4`, `merge_chunks_per_job=5` | 15:10 | `parallel_chunk_merges=1`, `merge_chunks_per_job=2` | 99:14 | 6.80x |

完整结果中还存在一个有用的调优模式：

- 两种存储模式的最佳运行都集中在 `parallel_chunk_merges=4..5` 范围内
- 最佳运行也集中在 `merge_chunks_per_job=4..5` 范围内
- 最慢的结果始终出现在 `parallel_chunk_merges=1` 且 `merge_chunks_per_job=2` 的组合

换句话说，旧的串行两块合并模式不仅仅是稍慢一些。在大型工作负载下，它可能会变得显著更慢，尤其是在列式存储中。

## 如何理解这两个参数

新文档描述了两个独立的调节杠杆：

- `parallel_chunk_merges` 增加了可以同时运行的合并任务数量
- `merge_chunks_per_job` 增加了每个任务可以处理的块数量

较低的 `merge_chunks_per_job` 值更容易安排更多并行任务，因为每个任务从可用池中消耗的块更少。如果表中有很多等待压缩的块，较小的任务会留下更多独立块供其他工作者使用，因此调度器可以同时保持多个合并任务活跃。较高的值会减少总合并步骤数，但每个任务会变得更重，占用更多可用块，这可能会减少并发任务的空间。

正确的平衡取决于您的存储方式和工作负载，但上述基准测试表明，结合这两种方法可以显著减少等待RT块压缩完成所花费的时间。

## 总结

如果您的RT工作负载在批量插入后花费太长时间等待块压缩完成，新的并行合并模型会显著改变这一情况。

在使用 `optimize_cutoff=16` 的1000万文档测试中：

| 模式 | 可搜索时间 | 完全优化时间 |
| --- | --- | --- |
| 旧设置: `parallel_chunk_merges=1`, `merge_chunks_per_job=2` | 1:18 | 3:23 |
| 新默认设置 | 1:18 | 1:57 |
| 调优后新设置: `parallel_chunk_merges=3`, `merge_chunks_per_job=5` | 1:18 | 1:31 |

- 所有数据变为可搜索的时间保持不变
- 块压缩完成时间从 **3:23** 降至 **1:31**
- 即使是新的默认设置也将总时间减少到 **1:57**

这正是对操作型RT索引改进至关重要的类型。数据在加载后立即可搜索，这一点在两种运行中基本保持一致。区别在于此之后：服务器在后台压缩块并使表恢复到目标状态所需的时间。如果您的工作流程依赖于表在下一次大量数据导入前、维护窗口关闭前或系统交还给应以更少块和更少后台合并压力运行的搜索工作负载前再次压缩，这种改进是显著的。
