# Manticore Search 中的分片：自动分发与复制

Manticore Search 中分片是如何工作的，从在单节点上把表拆分到 CPU 核心，到自动多节点分发并自行维持复制因子。

搜索系统通常从很简单开始：一台服务器上一张表。直到发生两种情况之一才会暴露问题。要么单个查询已经无法吃满你为它付费的全部 CPU，要么单台服务器已经不够了，无论是容量、吞吐量，还是最简单的事实：服务器可能会故障并带走你的数据。

Manticore Search 内置的自动分片自 [27.1.5](/blog/manticore-search-27-1-5/) 版本起可用，它通过**把一张表拆成多个更小的物理部分（分片），这些分片可以并行检索并放在不同节点上**，解决了这两个问题：

- **在单节点上**，分片把并发写入分散到彼此独立的部分上，并让每一部分保持足够小，从而维持高性能。
- **在集群中**，分片把数据分布到多个节点上，更重要的是，**会自动复制每个分片，并在节点故障和恢复时保持该复制因子不变**。

第二部分才是大多数人使用分片的真正原因：高可用。你只需要声明想要多少个分片，以及每个分片要有多少份副本，Manticore 会负责放置、复制和重新均衡。你不需要自己写故障切换脚本。

下面会讲这两种用法、不会淹没在内部细节里的工作机制、你会用到的命令，以及当前限制。

## 简短术语表

关键术语：

术语 | 含义
--- | ---
Shard | 表的一个物理部分，也就是 Manticore 为你创建并管理的真实表。`shards='4'` 的表会有四个这样的部分。
Replica | 另一台节点上的某个分片副本。Replica 是数据在节点故障后仍能存活的方式。
Replication factor (RF) | 每个分片有多少台节点保存副本。`rf='2'` 表示每个分片都存在于两台节点上。
Distributed table | 你实际查询的表。它使用你给定的名字，并透明地把查询分发到所有分片。
Cluster | 一个 Manticore 复制集群，也就是数据在其间复制的一组节点。
Master | 当前负责协调分片操作（放置、重新均衡）的节点，由系统自动选举。
Rebalancing | 当节点集合发生变化时，自动移动或复制分片的过程。

## 如何创建分片表

Manticore 中的分片完全由 `CREATE TABLE` 上两个简单选项驱动：

```sql
CREATE TABLE products (id bigint, title text, price float) shards='4' rf='2'
```

- `shards='N'` - 把表拆成 `N` 个物理部分。
- `rf='M'` - 在集群中保留每个部分的 `M` 份副本（复制因子）。

在常见场景下，你只需要写这一条 `CREATE TABLE`。不需要单独的“把它变成分布式表”的步骤，不需要像旧式手工分片方案那样手写 `agent=` 列表，也不需要在每台节点上分别创建表。Manticore 会创建物理分片、放置它们、建立复制，并在每台节点上创建一个名为 `products` 的分布式表，因此应用可以在集群中的任意节点上使用相同的表名。

## 用例 A：单节点上的分片

先看只有一台服务器的情况，也许它是台很大、很多核的机器，而且还没有集群。在这个场景里，分片不是为了存储持久性，而是为了更高效地利用这台机器。如果所有写入都进入一张实时表，并发 `INSERT` 会争用同一组内部表锁。随着表变大，[`RAM-chunk` 合并](https://manual.manticoresearch.com/Securing_and_compacting_a_table/Compacting_a_table#Compacting-a-Table)会更重，可能拖慢数据摄取。把这张表拆成多个彼此独立的分片，可以在两个方面都受益：写入分散到各个分片上，每个部分保持更小。此时还谈不上高可用，因为那需要不止一台节点，所以这纯粹是性能优化。

最简单的形式不包含集群，并且 `rf='1'`：

```sql
CREATE TABLE logs (id bigint, message text, ts timestamp) shards='8' rf='1'
```

这会在这台节点上创建 8 个物理分片，以及一个指向它们全部的分布式表 `logs`。这在单机上如何提升效果？

- **更多并发摄取。** 每个分片都是一张独立的实时表，所以并发写入会分散到多个分片上，而不是在一张表的锁上串行化。这正是下面的[基准测试](#benchmarks-does-sharding-actually-speed-up-inserts)直接衡量到的收益。
- **更小的部分保持高性能。** 实时表会周期性合并内部 RAM chunk。拆分成分片后，每个分片的 chunk 更小，因此这些合并消耗更少资源，也更不容易拖慢插入。
- **查询并行化（在单机上通常收益不大）。** 分布式表会通过服务器的工作线程池并行搜索各个分片，因此单个查询可以用到多个核心，而不只是一个核心，受 `searchd.threads` 和物理核心数量限制。不过在单节点上，这和[伪分片](https://manual.manticoresearch.com/Server_settings/Searchd#pseudo_sharding)有重叠，收益通常不大（约 5–12%）——见下面的[读取基准测试](#do-sharding-and-replication-speed-up-reads)。

如果你以前用过 Manticore 的[伪分片](https://manual.manticoresearch.com/Server_settings/Searchd#pseudo_sharding)，目标可能很熟悉——让一个查询用上所有核心——但机制不同。伪分片会在查询时自动并行化*单个*物理表。显式分片会创建你可控的*真实*分片：你决定数量，它们是彼此独立、便于理解的表，而且更关键的是，同一张分片表以后还可以跨节点分布，而不用改变应用与它交互的方式。

> 这两者是互补的，但并不会自动叠加出免费收益。物理分片，也就是由多个本地表组成的分布式表，已经能让工作线程忙起来，所以如果你已经显式分片了，再在上面开启 `pseudo_sharding` 通常收益很小，甚至可能稍微损失一点吞吐量。用 [`manticore-load`](https://github.com/manticoresoftware/manticore-load) 两种方式都测一下：在开启和关闭 `pseudo_sharding` 的情况下分别跑你的负载，如果它在显式分片之上没有带来额外收益，就把它关掉。

在单节点上，复制因子必须是 `1`：只有一台节点，没有地方放第二份副本。这也是单节点分片的边界条件，它带来的是并行性，不是持久性。要想要持久性，你需要不止一台节点。

## 用例 B：多节点分片与自动复制

这才是分片真正要解决的问题。先从一个包含多台节点的复制集群开始（如何创建请看[设置复制](https://manual.manticoresearch.com/Creating_a_cluster/Setting_up_replication/Setting_up_replication)），然后在这个集群里用 `cluster:` 前缀并设置大于 1 的 RF 创建表：

```sql
CREATE TABLE mycluster:products (id bigint, title text, price float) shards='4' rf='2'
```

Manticore 会替你做这些事：

1. 创建四个分片。
2. 以平衡的方式把它们分布到集群节点上。
3. 为每个分片在不同节点上创建**第二份副本**，因为 `rf='2'`。
4. 为每个分片与其副本建立复制。
5. 在每台节点上创建一个分布式表 `products`，因此任意节点都可以提供读取和接受写入。

从应用的角度看，没有任何变化，你仍然可以 `INSERT INTO products …` 和 `SELECT … FROM products`。读取会被分发到各个分片并合并结果；写入会被路由到某个分片。但现在每个分片都存在于两台节点上，这才是你关心的属性：**任意一台节点都可以故障，而表仍然完全可用且不会丢数据。**

复制因子会随着你的持久性需求和节点数量而扩展：

RF | 每个分片的副本数 | 可承受 | 典型用途
--- | --- | --- | ---
1 | 1 | 什么都扛不住 - 节点丢失就会丢失它的分片 | 单节点并行、开发/测试、可重建的数据
2 | 2 | 一台节点故障 | 常见的生产选择
3+ | 3 份或更多 | 多个同时故障 | 关键业务、故障频繁的环境

限制很简单：**你不能要求比节点数更多的副本。** `rf='3'` 至少需要集群里有三台节点。Manticore 会在你创建表时检查这一点，并在集群太小时告诉你。

```sql
-- 6 shards, 3 copies each, across a 3+ node cluster
CREATE TABLE mycluster:events (id bigint, body text) shards='6' rf='3'
```

## 把这些串起来：多节点演示

假设你有一个名为 `mycluster` 的三节点复制集群（如果还没有，[设置复制](https://manual.manticoresearch.com/Creating_a_cluster/Setting_up_replication/Setting_up_replication) 会演示 `CREATE CLUSTER` 和 `JOIN CLUSTER`）。从任意节点创建一个分片且复制的表：

```sql
CREATE TABLE mycluster:products (id bigint, title text, price float) shards='4' rf='2'
```

Manticore 会创建四个分片，把每个分片的两份副本放到三台节点上，并在每台节点上创建一个分布式表 `products`。检查其放置情况：

```sql
SHOW SHARDING STATUS products;
```

```text
-- illustrative output (abbreviated columns)
+-------+-------+--------+----+-----------+
| shard | node  | status | rf | rf_status |
+-------+-------+--------+----+-----------+
|     0 | node1 | active |  2 | ok        |
|     0 | node2 | active |  2 | ok        |
|     1 | node2 | active |  2 | ok        |
|     1 | node3 | active |  2 | ok        |
|     2 | node1 | active |  2 | ok        |
|     2 | node3 | active |  2 | ok        |
|     3 | node1 | active |  2 | ok        |
|     3 | node2 | active |  2 | ok        |
+-------+-------+--------+----+-----------+
```

每个分片都出现在两台不同的节点上，这就是 `rf=2`，并且每个 `rf_status` 都是 `ok`。（完整结果还包含 `table`、`cluster` 和 `replication_cluster` 列。）现在像使用任何其他表一样使用它，任意节点都可以：

```sql
INSERT INTO products (id, title, price) VALUES (1, 'Wireless mouse', 19.99);
SELECT * FROM products WHERE MATCH('mouse');
```

写入会被路由到某个分片，并复制到该分片的另一份副本；读取会分发到全部四个分片并合并结果。你的应用从来不需要指定某个分片名。

## 维持复制因子

设置 `rf='2'` 很简单。任何分布式系统里真正困难的是随着时间维持这个条件，也就是机器故障和恢复，以及你增加容量的时候。不过你现在不用再担心这些了。Manticore Search 会自动完成这项工作。

Manticore 的做法是让集群选举出一个负责协调循环的 **master** 节点。它监控集群拓扑，也就是哪些节点存活，并对变化做出响应：

#### 节点故障

此时它的分片副本数少于 RF 要求。master 会检测到缺失的节点，并尝试重建丢失的副本。如果故障后集群仍至少有 `rf` 个活跃节点，它会把新的副本放到那些尚未持有它们的活跃节点上，从而恢复复制因子。只要每个分片至少还有一份副本可用，查询就能继续工作。

继续上面的演示 - 如果 `node3` 宕机，`SHOW SHARDING STATUS products` 会把受影响的分片显示为 `degraded`（一份副本离线，一份仍在线）：

```text
-- illustrative: node3 is down
+-------+-------+----------+----+-----------+
| shard | node  | status   | rf | rf_status |
+-------+-------+----------+----+-----------+
|     1 | node2 | active   |  2 | degraded  |
|     1 | node3 | inactive |  2 | degraded  |
|     2 | node1 | active   |  2 | degraded  |
|     2 | node3 | inactive |  2 | degraded  |
|   ... | ...   | ...      |    | ...       |
+-------+-------+----------+----+-----------+
```

此时仍有两台活跃节点（`node1`、`node2`）且 `rf=2`，所以 master 会在缺少这些分片副本的活跃节点上创建分片 1 和 2 的缺失副本。在新副本构建期间，它会显示为 `pending`；一旦复制追上进度，就会变成 `active`，`rf_status` 也会恢复为 `ok`。

一个重要的注意事项：**只有在有地方放新副本时，Manticore 才能恢复 RF。** 如果活跃节点数降到 `rf` 以下，所请求的 RF 暂时无法满足：受影响的分片会保持 `degraded`，仅保留存活的副本，直到有节点返回或你添加新节点。Manticore 不会把同一分片的另一份副本创建在同一台节点上，也不会悄悄假装 RF 已满足。如果某个分片没有任何存活副本，它的状态会变成 `broken`；下面会讲这种情况。对于 `rf='1'`，故障节点上的分片就直接丢失了，因为本来就没有第二份副本。

#### 节点加入

新容量应该被利用起来。master 会重新均衡，让新节点承担一部分负载。具体方式取决于 RF：

- **RF = 1：** 分片必须被*移动*（因为只有一份副本，不能简单复制）。Manticore 会通过一个临时的内部集群安全地移动它们：先把数据复制到新节点，只有之后才从旧节点移除，所以分片始终都有一份可用副本。
- **RF ≥ 2：** 分片会先通过集群现有复制机制*复制*到新节点，然后再重新均衡分布。不会有高风险的数据迁移，因为总有另一份副本存在。

#### 某个分片的所有副本都离线

如果持有某个分片的所有节点同时丢失，该分片的 `rf_status` 会变成 `broken`，因为已经没有可用副本来提供服务或作为复制源。表的其他部分仍然可以工作；当其某个节点返回时，损坏的分片就会恢复。RF 会降低这种情况发生的概率：在 `rf='2'` 时，需要两个“正确的”节点同时故障；在 `rf='3'` 时，需要三个。

所有这些都通过一个内部的、有顺序的、可回滚的操作队列完成，因此一次重新均衡操作要么完成，要么被干净地回滚，即使 master 节点在操作中途自身宕机，新的 master 也会清理半完成的工作。对你这个运维者来说，重点是：**你只设置一次 RF，集群会努力保持它一直成立。**

## 底层机制简述

使用分片并不需要了解这些，但知道背后在发生什么会更有帮助。

- **物理分片是真实表。** 一张有四个分片的表，底下由四张真实表支撑，这些表由 Manticore 为你创建并管理。通常你不会直接碰它们。
- **你的应用与[分布式表](https://manual.manticoresearch.com/Creating_a_table/Creating_a_distributed_table/Creating_a_distributed_table)交互。** Manticore 会在每台节点上创建一个名为 `products` 的表。在它的内部定义里，本地分片会直接列出，而其他节点上的分片则通过 `agent` 连接。这就是 `SELECT … FROM products` 能透明命中所有数据的原因。
- **协调状态存在于集群中。** Manticore 会跟踪自己的内部元数据，包括分片放置、协调状态和待处理操作队列，因此它始终知道谁持有什么，以及还有哪些工作未完成。在多节点环境里，这些状态会在集群间复制，所以每台节点看到的是同一份视图。
- **master 驱动变更。** 放置、复制建立和重新均衡都由 master 计算，并以带回滚说明的有序命令入队，然后在各节点上执行。
- **复制复用了 Manticore 的集群机制。** Manticore 已经用于集群的那套成熟复制机制，会用来保持分片副本同步。

从架构上看：

```
            CREATE TABLE ... shards='4' rf='2'
                          │
                          ▼
                  ┌────────────────┐
                  │ Manticore      │  computes placement,
                  │ elected master │  enqueues ordered ops
                  └────────┬───────┘
                          │
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
    ┌──────────┐     ┌──────────┐     ┌──────────┐
    │  node1   │     │  node2   │     │  node3   │
    │ s0 s2 s3 │◄───►│ s0 s1 s3 │◄───►│  s1 s2   │   (each shard on 2 nodes = rf 2)
    └──────────┘     └──────────┘     └──────────┘
    distributed table "products" exists on every node
    (same placement as the SHOW SHARDING STATUS output above)
```

## 运维分片表

你期望能正常工作的都能正常工作，而且还有几条专门针对分片的可见性命令。

**查看表结构。** `DESC` 和 `SHOW CREATE TABLE` 可以作用于逻辑表；Manticore 会通过底层分片解析它们：

```sql
DESC products;
SHOW CREATE TABLE products;
```

**查看每个分片在哪里，以及 RF 是否健康。** 这是故障和重新均衡期间你要重点关注的命令：

```sql
SHOW SHARDING STATUS products;
```

```text
+-------+-------+--------+----+-----------+
| shard | node  | status | rf | rf_status |
+-------+-------+--------+----+-----------+
|     0 | node1 | active |  2 | ok        |
|     0 | node2 | active |  2 | ok        |
|   ... | ...   | ...    |    | ...       |
+-------+-------+--------+----+-----------+
```

它会为每个分片副本输出一行，包含这些列：

列 | 含义
--- | ---
`table` | 逻辑表名
`shard` | 分片编号
`node` | 持有该副本的节点
`status` | `active`、`inactive`（节点离线）或 `pending`（正在创建）
`cluster` | 该表所属的复制集群
`replication_cluster` | 保持该分片副本同步的内部集群
`rf` | 该分片当前拥有的副本数
`rf_status` | `ok`（RF 满足要求）、`degraded`（部分副本离线但至少有一份在线）或 `broken`（没有副本在线）

`rf_status` 是一眼可见的健康信号：全部是 `ok` 表示集群正在满足你要求的复制因子；`degraded` 表示系统还能工作但处于暴露状态；`broken` 表示某个分片已不可用。

**找到协调者：**

```sql
SHOW SHARDING MASTER;
```

**干净地删除它。** 删除分片表的方式和删除普通表完全一样 - `DROP TABLE` 会删除该表以及它在整个集群中的所有分片：

```sql
DROP TABLE products;
```

**通过改变集群来扩容。** 由于重新均衡是自动的，扩展分片表的方式就是向集群添加节点（[添加新节点](https://manual.manticoresearch.com/Creating_a_cluster/Adding_a_new_node)）。master 会发现新节点并自动把负载重新均衡过去，而不需要对表本身做任何操作。

## 如何选择分片数和复制因子

这里有一些经验法则：

- **为了更快写入而分片：** 少量分片通常就足够了，而且应该远低于核心数——在下面的[基准测试](#benchmarks-does-sharding-actually-speed-up-inserts)里，一台 16 核 / 32 线程机器在 **4–8 个分片** 时达到峰值，而到 32 个分片时反而*比完全不分片更慢*。从小开始（4–8），先测量，只有你的实际数据证明有必要时再增加。更多分片通常没什么帮助，反而会增加每个分片的开销。
- **为了分发而分片：** 在多节点场景下，你希望分片数量足以让它们在节点间均匀分布，并且为增长留出空间，因此最好是节点数的倍数。不要过度增加：每个分片都是真实表，带着自己的开销。（Manticore 将分片数上限设为 **3000**。）
- **为了持久性而选择 RF：** `rf='2'` 是标准生产选择，它能在 2 倍存储开销下承受任意一台节点故障。只有在你确实需要承受同时故障，或有严格可用性要求时，才使用 `rf='3'`，并记住它会带来 3 倍存储和更多复制流量。
- **RF=1 只适合性能或一次性数据。** 它没有容错能力。可在单节点上用于并行，或者在集群中仅当你有外部方式重建丢失数据时使用。

## 基准测试：分片真的能加快插入吗？

只有真正有价值的功能才值得用，所以我们做了测量。我们想要诚实回答的问题是：**在相同负载下，分片能否加快数据摄取 - 如果能，什么时候有效？** 方法很简单：把每一次分片运行都和同一个基线对比，也就是不分片的普通表。

**简而言之：** 在一台 16 核机器上配 32 个并发写入者时，分片最高把插入吞吐量提升了约 **1.5 倍** - 从约 163k 提升到约 253k docs/s - 但前提是分片数量保持较小。最佳结果出现在 **4–8 个分片**；到了 32 个分片时，吞吐量已经低于未分片基线。二进制日志大约消耗了 **25%** 的写入性能，而跨两台真实机器的 `rf=2` 复制又多消耗了约 **30%**，这对于持久性来说是合理代价，但并非没有成本。

**环境。** 一台没有其他明显 CPU 负载的专用服务器 - AMD Ryzen 9 5950X（16 核 / 32 线程）、128 GB RAM。所有测试都在一个运行了包含分片特性的近期开发版的单个 Docker 容器里完成。Manticore 使用**默认设置 - 没有性能调优**：只配置了监听端口、数据目录和二进制日志路径；线程池、RT 内存限制和二进制日志行为都保持默认。负载来自 [`manticore-load`](https://github.com/manticoresoftware/manticore-load)。每次运行都会把相同文档 - `(id bigint, name text, type int)`，其中 `name` 是 10–100 个随机单词 - 以 1000 条一批写入一张实时表。不同运行之间只改变**分片数**、**复制因子**和**二进制日志**；“不分片”基线是一张普通 RT 表。我们把完整的单节点分片扫描**跑了两次 - 一次开启二进制日志（默认），一次关闭** - 每次插入 **20,000,000** 条文档，并发写入者为 **32**。

```bash
# the exact shape of every insert run (shards/rf vary)
manticore-load --batch-size=1000 --threads=32 --total=20000000 \
  --init="create table test(id bigint, name text, type int) shards='8' rf='1'" \
  --load="insert into test(id,name,type) values(<increment>,'<text/10/100>',<int/1/100>)"
```

### 单节点：吞吐量与分片数

![单节点 16 核 / 32 线程下，二进制日志开与关时的插入吞吐量与分片数关系](./sharding-in-manticore-search/inserts_throughput.svg)

每秒插入文档数，2000 万条文档，32 个写入者 - 两种二进制日志模式：

| 分片数 | 二进制日志开启（默认） | 二进制日志关闭 |
| --- | --- | --- |
| none (baseline) | 162,920 | 218,079 |
| 2 | 191,976 | 246,288 |
| 4 | **252,807** | **290,665** |
| 8 | 251,008 | 265,015 |
| 16 | 175,848 | 182,288 |
| 32 | 108,006 | 111,381 |

图表把完整扫描画了两次 - 二进制日志**开启**（蓝色，默认）和**关闭**（橙色）。有三点特别明显：

- **并发插入有真正的提升。** 在 32 个写入者下，拆分表后吞吐量在峰值时提升了 **约 1.5 倍** - 默认（二进制日志开启）曲线从 **163k** 提到 **253k docs/s**。每个分片都是独立的实时表，所以把写入分散到多个分片上能显著减少单张 RT 表在高并发下遇到的锁争用，并让插入利用更多核心。
- **最佳范围很小 - 4–8 个分片。** 收益在这里达到峰值，随后很快下降。到 16 个分片时只比基线略好，而**到 32 个分片时吞吐量已经低于未分片表（0.66×）** - 超过这个范围后，每个分片和协调开销会压过额外并行带来的收益。更多分片绝对不是更好。两条曲线的**形状相同，最佳范围也相同** - 二进制日志并不会改变这一点。
- **持久性的成本在你最快的时候最明显。** 关闭二进制日志（橙色）会抬高整条曲线，但差距在高吞吐区最大 - **4 个分片时 253k → 291k** - 而当分片数过高时几乎消失（**32 个分片时 108k vs 111k**），此时瓶颈是协调开销，而不是持久性。二进制日志的影响我们会在下面单独测量。

有一点需要明确说明：这种加速来自**并发写入**。单个严格串行的写入者无法利用并行分片，因此不会有加速，反而会多一点分布式层开销。分片真正适合的是多个客户端或消费者同时写入的情况，这也是现实中的摄取管道通常会遇到的。

### 持久性的代价：二进制日志

Manticore 的二进制日志让插入具有崩溃安全性（在非正常关闭后可以重放未刷盘的事务）。它默认开启。你已经能从上面的图里看到它的成本 - 橙色（二进制日志关闭）曲线高于蓝色曲线。只保留 `binlog_path` 变化，在普通未分片基线上隔离它的影响：

![开启与关闭二进制日志时的插入吞吐量（2000 万条文档）](./sharding-in-manticore-search/binlog_cost.svg)

关闭二进制日志后，基线吞吐量从 **162k 提升到 218k docs/s** - 所以**崩溃安全插入大约要付出 25% 的成本**。这就是在硬故障时不丢失未刷盘写入的代价；除非你能从源数据重建，并且希望在批量导入期间获得额外速度，否则生产环境应保持开启。

### 复制对写入的成本

跨机器的持久性同样不是免费的 - 而且这也是唯一一个必须在**真实、独立硬件**上做的测试。所以和上面的单节点基准不同，我们把两个复制测试跑在了**两台不同的物理机器**上：两台 4 核 / 7 GB 的云主机组成一个 Manticore 集群。这样 `rf=1` 和 `rf=2` 就不会争用同样的核心或内存 - 这是单机根本无法提供的公平比较。

`rf=1` 在一台机器上保留一份副本；`rf=2` 则在**两台机器上**都保留完整副本，因此每次插入都会在确认前通过网络同步复制。相同的 100 万条文档负载，4 个写入线程：

![两台独立机器上 rf=1 与 rf=2 的插入吞吐量](./sharding-in-manticore-search/replication_cost.svg)

复制大约消耗了**30% 的插入吞吐量**（112k → 78k docs/s） - 这就是在 ack 之前把每次写入复制到第二台机器上的代价。一个数字就能说明 `rf` 的权衡：写得稍慢一些，换来整台机器故障后仍能存活。

### 分片和复制会加速读取吗？

读取很容易测错，所以先说方法。一个**非常简单**的查询 - 取 20 行，不做排序 - 执行时间远低于 1 毫秒，因此它的成本**全是固定开销**；一个跳转到 agent 的分布式表在这里看起来会**慢约 3 倍**（这里约 7,700 vs 2,400 q/s），纯粹是因为网络往返远大于几乎为零的工作量。这不是真实查询。一个**更贴近现实**的全文检索查询（匹配并排序少量词项，约 10–30 ms）才会告诉你真相：分发开销会缩小到**几个百分点**，因为真正的工作开始主导固定成本。下面的数字都使用了 2 节点集群上的真实查询，并从单个入口节点发起读取。

```bash
# realistic read — what the numbers below use: full-text match-and-rank, ~10–30 ms each
manticore-load --threads=4 --total=5000 \
  --load="select id from <table> where match('<text/1/5> <text/1/5>')"

# trivial read — sub-ms, all fixed overhead, exaggerates distributed cost ~3x
manticore-load --threads=4 --total=20000 \
  --load="select id from <table> limit 20 option ranker=none"
```

`<table>` 是以下三者之一：普通 RT 表（**不分片**），一个跨本地分片的 `type='distributed'` 表（**单节点分片** - `local='s0' local='s1' …`），或一个 `shards='4' rf='1'`/`rf='2'` 的分片表。每次读取都只发往**一台**节点 - 我们从不把两台节点当成两个独立客户端分别查询。

![读取吞吐量：跨两台节点的分片](./sharding-in-manticore-search/reads_rf.svg)

- **跨节点分片能加快读取。** 一个跨两台机器分布的 4 分片 `rf=1` 表，吞吐量达到 **约 516 q/s，而单个未分片节点约 315** - 大约 **1.6 倍** - 因为每个查询会同时利用两台节点的核心。
- **复制不会拖慢读取。** `rf=2`（每个分片在两台节点上都有副本）大约 **410 q/s**，略低于 `rf=1`。原因不是负载粘在一台节点上（它会在副本之间分散），而是 `rf=2` 时每个分片都要通过 agent/mirror 路径访问 - 即使是恰好本地的副本也是如此 - 而这条路径会带来上面提到的几个百分点开销；相比之下，`rf=1` 会在进程内直接读取本机上的分片。无论哪种方式，读取都明显快于单节点，而且数据现在可以在机器故障下继续存活。
- **在一台机器上，分片对读取的提升很小。** 把一张表拆成 2–4 个分片，在*单节点*上只会增加约 5–12% 的收益（查询并行化），而且还得有空闲核心才行 - 更重的查询收益更明显（约 12%，而轻查询约 3%），但满载机器基本看不出差异。真正的读取扩展来自增加**机器**，而不是在一台机器上继续加分片。

> 方法说明：读取和复制测试运行在**另一对云主机上 - 每台 4 vCPU / 7 GB RAM，两台不同的物理机器** - 它们组成一个 2 节点 Manticore 集群，使用相同的近期开发版和默认设置，数据量为 **150 万条文档**。这是一套与上面用于插入扫描的 16 核 / 32 线程服务器不同、且弱得多的环境，所以**结论只能在各自测试内部比较，不要跨测试直接对比**。核心数较少也解释了为什么这里单节点分片的读取收益较小 - 可供查询并行化的空闲核心不多。

### 结论

- 在单机上，分片能带来 **约 1.5 倍的并发插入加速**（这里 20M 负载下为 163k → 253k docs/s）。
- 在这颗 16 核 / 32 线程 CPU 上，**最佳范围是 4–8 个分片**。分片太多会*变慢*：到 32 个分片时，吞吐量已经低于未分片基线。分片数应与核心数和写入并发匹配，而不是追求一个漂亮的大整数。
- **二进制日志会消耗约 25%** 的写入吞吐量（并且当分片数过高时差距会缩小到接近零），而**`rf=2` 复制会消耗约 30%** 的写入吞吐量（在两台独立机器上测得） - 这分别是为崩溃安全和节点故障存活付出的合理代价。
- 对于**真实**的全文检索查询，在 2 节点集群上分片可把读取提升到单节点的 **约 1.6 倍**，而 `rf=2` 能在保持读取高性能的同时承受节点丢失。简单的 id 查询会把分布式开销夸大约 3 倍 - 评测读取时一定要使用真实查询。
- 绝对数值取决于硬件和文档形态；能迁移到其他环境的是这些曲线的*形状* - 所以在确定分片数之前，先对自己的负载做基准测试。

## 限制与注意事项

有些边界条件需要提前知道：

- **`rf` 是必填项。** 分片版 `CREATE TABLE` 必须指定 `rf=`。如果省略，`CREATE TABLE` 会失败。
- **大于 1 的 `rf` 需要集群。** 你不能在单机上创建多副本分片表，因为没有地方放副本。多副本表必须使用 `cluster:name` 形式。
- **已经在集群中的节点上不能创建本地分片表。** 如果某个节点属于复制集群，请在该集群内部创建表（`CREATE TABLE cluster:name …`），而不是作为本地表创建，否则分片元数据不会被正确跟踪。Manticore 会检测到这一点并提醒你。
- **每张表最多 3000 个分片。**
- **RF=1 没有容错。** 丢失某个节点，它的分片就没了。这是设计权衡，不是 bug - 这就是你选择 `rf='1'` 时接受的代价。
- **至少需要 RF 台节点。** `rf='M'` 要求集群中至少有 `M` 台节点；否则创建会失败。
- **创建是同步的，直到超时为止。** `CREATE TABLE` 会等待分发完成（默认 30 秒）。如果分片数非常大，可以用 `timeout='N'`（秒）把它调高，例如 `shards='3000' rf='3' timeout='60'`。

## 这意味着什么

Manticore 中的分片用一个刻意简洁的接口覆盖了很宽的场景。在单机上使用 `shards='N' rf='1'` 时，分片会把并发写入分散到彼此独立的部分上，并让每一部分保持足够小。在集群内部使用 `shards='N' rf='M'` 时，它会给你一张分布式、可复制的表，能够在节点故障时继续存活，并在集群变化时自动重新均衡，而你不需要写一行故障切换逻辑。同一份表定义可以从一台节点扩展到多台节点，而你的应用始终用同样的方式与它交互。实际上，这意味着你可以先在单节点上提升写入吞吐量，之后再平滑迁移到一个具备容错能力的集群，而无需修改应用。

想更深入了解分片所依赖的基础构件：

- [创建分片表](https://manual.manticoresearch.com/Creating_a_table/Creating_a_sharded_table/Creating_a_sharded_table)
- [设置复制](https://manual.manticoresearch.com/Creating_a_cluster/Setting_up_replication/Setting_up_replication)
- [伪分片](https://manual.manticoresearch.com/Server_settings/Searchd#pseudo_sharding)

如果你有关于分片的问题，或者有想让我们做基准测试的负载，[告诉我们](https://manticoresearch.com/contact-us/)。
