# 多副本

## 概述

Alluxio Worker 节点上缓存的一份文件数据和元数据称为该文件的一个副本。\
文件多副本功能通过在不同 Worker 上创建多个副本，来使客户端可以从多个Worker 节点访问同一个文件。\
因此，当缓存的数据集被大量客户端并发访问时，多副本可提高 I/O 性能。

如果未启用多副本功能，则同一时刻一个文件在集群中只有一个副本。\
这意味着客户端对文件元数据和数据的所有访问都将由存储这个副本的单个 Worker 提供服务。\
在大量客户端需要同时访问该文件的情况下，该 Worker 的服务能力可能会成为瓶颈。

请注意，对于同一个文件，每个工作节点最多只会拥有一个副本。工作节点不会拥有多个相同文件的副本。

## 配置副本级别

在创建副本之前，你需要先配置文件的副本级别，也就是在集群中希望保留多少份文件副本。副本级别可以通过协调器（coordinator）上的一个 RESTful API 接口进行管理：

```console
http://<coordinator_host>:<coordinator_api_port>/api/v1/replica-rule
```

## 列出现有的副本规则

要列出当前生效的所有副本规则，可以向以下端点发送一个 GET 请求：

```console
$ curl -G http://<coordinator_host>:<coordinator_api_port>/api/v1/replica-rule
```

返回结果是一个包含所有副本规则的 JSON 对象，例如：

```json
{
  "/": {
    "pathPrefix": "/",
    "clusterReplicaMap": {
      "DefaultAlluxioCluster": 1
    },
    "totalReplicas": 1,
    "type":"UNIFORM"
  }
}
```

这个示例展示了一个设置在根路径上的副本规则，表示所有文件都有 1 个副本。

### 创建一个新的副本规则

要添加一个新的副本规则，可以使用以下命令发送一个 POST 请求：

```console
$ curl -X POST -H 'Content-Type: application/json' -d '{"path": "/", "numReplicasPerCluster": 2}' http://<coordinator_host>:<coordinator_api_port>/api/v1/replica-rule
```

请求的 POST 数据是一个 JSON 对象：

```json
{
  "path": "/",
  "numReplicasPerCluster": 2
}
```

`path` 字段用于指定副本规则生效的路径前缀。`numReplicasPerCluster` 字段用于指定该路径前缀下文件的副本数量。

可以为不同的路径前缀设置多个副本规则。例如，下面的示例设置 `/s3/vip_data` 路径下的文件有 3 个副本，而 `/s3/temp_data` 路径下的文件只有 1 个副本：

```console
$ curl -X POST -H 'Content-Type: application/json' -d '{"path": "/s3/vip_data", "numReplicasPerCluster": 3}' http://<coordinator_host>:<coordinator_api_port>/api/v1/replica-rule
$ curl -X POST -H 'Content-Type: application/json' -d '{"path": "/s3/temp_data", "numReplicasPerCluster": 1}' http://<coordinator_host>:<coordinator_api_port>/api/v1/replica-rule
```

{% hint style="warning" %}
请注意，目前如果创建了多个副本规则，这些规则的路径不能存在嵌套关系。例如，不能同时存在设置在 `/parent` 和 `/parent/child` 上的规则。因此，设置在根路径 `/` 上的规则也不能与设置在其他任何路径上的规则共存。

这一限制将在未来的版本中被取消。
{% endhint %}

### 更新现有的副本规则

更新已有规则的方式与创建新规则相同，只需在已有规则对应的路径上进行操作即可：

```console
$ curl -X POST -H 'Content-Type: application/json' -d '{"path": "/s3/vip_data", "numReplicasPerCluster": 1}' http://<coordinator_host>:<coordinator_api_port>/api/v1/replica-rule
```

上述示例将 `/s3/vip_data` 路径上的副本数量从 3 个更新为 1 个。

## 删除副本规则

要删除某个副本规则，可以向对应的端点发送一个 DELETE 请求，请求体中包含要删除规则的路径前缀：

```console
$ curl -X DELETE -H 'Content-Type: application/json' -d '{"path": "/s3/vip_data"}' http://<coordinator_host>:<coordinator_api_port>/api/v1/replica-rule
```

### 默认副本级别

如果没有创建任何副本规则，系统的默认行为等同于在根路径 上设置了一个拥有 1 个副本的规则：

```json
{
  "path": "/",
  "numReplicasPerCluster": 1
}
```

因此，整个 Alluxio 集群中的所有文件默认只会有 1 个副本。

已经废弃的配置属性 `alluxio.user.file.replication.min` 仍可用于修改默认的副本数量，例如设置为 2：

```properties
alluxio.user.file.replication.min=2
```

我们建议直接在根路径上显式设置副本规则，而不要依赖这个已废弃的配置项。

## 创建副本

文件的副本可以因为缓存未命中而被动创建，也可以通过分布式加载功能手动创建。

### 手动创建副本

Alluxio 可以将文件 [分布式加载（Distributed Load）](https://documentation.alluxio.io/ee-ai-cn/ai-3.6/reference/user-cli#job-load) 进 Alluxio 的缓存.

```console
bin/alluxio job load --path s3://bucket/file --submit
```

如果在路径 `/s3` 上设置了一个副本规则，且副本数量为 3，那么当加载操作完成后，位于 `/s3/file` 的文件将会以 3 个副本的形式加载到 Alluxio 中，并分别存储在 3 个不同的工作节点上。

当一个具有多个副本的加载任务结束时，即使某些副本未能成功加载，任务状态也会被标记为成功。对于那些加载失败的副本，在有访问请求时会被被动加载（参见“[被动创建副本](#被动创建副本)”部分）。

### 被动创建副本

当客户端从 Alluxio 读取文件时，若该文件未被任何工作节点缓存，则会被加载到 Alluxio 中。\
若启用了多副本功能，客户端会尝试从不同工作节点加载文件，从而被动创建多个副本。**请注意**，被动创建的副本数量取决于读取该文件的客户端数量。由于单个客户端一次仅从一个工作节点读取文件，若文件仅被单个客户端请求，只会在一个工作节点上创建一个副本。

若客户端数量足够多，被动创建的副本数可能达到副本规则中设置的指定复制级别。\
这与通过分布式负载手动创建的副本不同 —— 手动创建的副本会主动加载副本规则中指定数量的副本。

## 并发读取多个副本

如果根据生效的副本规则，某个文件拥有多个副本，则客户端可以从多个副本中读取数据。

对于一个多副本文件，客户端选择哪个副本进行读取由**副本选择策略**控制。副本选择策略会在所有候选工作节点中决定客户端应该连接哪一个工作节点来读取文件。

Alluxio 提供了三种副本选择策略：

1. Global fixed（全局固定）：所有客户端按照候选工作节点列表中的相同顺序选择工作节点。
2. Client fixed（客户端固定）：每个客户端看到的候选工作节点集合相同，但顺序可能不同，因此不同客户端可能会选择不同的工作节点读取文件。
3. Random（随机）：每次读取文件时，客户端都会从候选工作节点中随机选择一个副本。

要设置副本选择策略，可以将配置项 `alluxio.user.replica.selection.policy` 设置为 `GLOBAL_FIXED`、`CLIENT_FIXED` 或 `RANDOM`。

如果由于临时故障导致副本选择策略选中的工作节点不可用，客户端会按候选列表的顺序尝试下一个工作节点，直到所有选项都失败，最终抛出错误。

请注意，默认情况下，客户端在尝试从某个工作节点读取文件之前，不会检查该节点是否实际缓存了该文件。如果所选工作节点未缓存该文件，它将从底层存储（UFS）中获取并缓存该文件，然后再发送给客户端。除非当前所选的工作节点不可用，客户端不会切换到另一个副本节点。

若希望通过优先使用已缓存的副本来提升 I/O 性能，请参见下一节。

### 多副本文件的优化I/O

如果一个文件在多个工作节点上有副本，Alluxio 支持优化的 I/O 机制，通过优先从已完全缓存该文件的数据节点读取，从而加快访问速度。

当 Alluxio 客户端读取一个多副本文件时，它会首先检查由副本选择策略选中的那些工作节点中，是否有任何一个已经完整缓存了该文件。如果存在这样的节点，客户端将优先从第一个已完整缓存该文件的节点读取数据。

如果所有工作节点都未完整缓存该文件，客户端将按前一节所述的方式进行冷读取（从底层存储中拉取并缓存）。

请注意，部分缓存文件的工作节点不会比完全未缓存该文件的节点具有优先权。

若要为多可用区（multi-AZ）副本启用优化 I/O，请设置以下配置项：

```properties
alluxio.user.replica.prefer.cached.replicas=true
```

### 用于自动创建副本的被动缓存

当启用了优化 I/O 功能后，客户端在初始检查过程中发现其尝试读取的文件在首选工作节点上未被缓存或仅被部分缓存时，客户端会转而从一个优先级较低的节点读取该文件，而不是进行冷读取（即从底层存储拉取数据）。

出现首选工作节点未完整缓存文件而较低优先级节点已缓存的情况，可能有以下几种原因：

* 首选工作节点上的文件缓存已被驱逐，以为最近访问的文件腾出空间；
* 集群发生了扩容，首选工作节点是新加入的节点，尚未缓存该文件。

由于客户端不会在首选节点上主动发起冷读取，该节点将不会缓存该文件，除非用户显式运行加载任务将文件加载进来。这样每次读取请求就会产生一个额外的、固定的 I/O 延迟，用于探测首选工作节点是否已缓存该文件。

为了消除这部分额外延迟，用户可以启用“被动缓存”功能。当缓存未命中时，被动缓存会自动并异步地加载文件。当客户端下次访问相同文件时，首选工作节点就已经完整缓存了该文件，客户端无需再切换到低优先级节点，从而优化 I/O 延迟。

要启用被动缓存功能，请添加以下配置项：

```properties
# multi-replica optimized IO must be enabled for passive cache to work
alluxio.user.replica.prefer.cached.replicas=true
alluxio.user.file.passive.cache.enabled=true
```

## 通过指标监控副本读取情况

当启用 `alluxio.user.replica.prefer.cached.replicas` 配置项后，指标 `alluxio_multi_replica_read_from_workers_bytes_total` 会用于监控跨集群的数据读取模式。该指标有助于了解在高可用配置下多副本缓存的效果以及读取流量的分布情况。

* **类型**：`counter`
* **组件**：`client`
* **描述**：追踪客户端在读取多副本文件时，从 Alluxio 工作节点读取的总字节数。该指标仅在 `alluxio.user.replica.prefer.cached.replicas` 设置为 `true` 时生效。它可以帮助分析客户端从本地集群还是远程集群读取数据的频率，以及读取的数据是否来自已完整缓存的副本。
* **标签（Labels）**：
  * `cluster_name`：提供读取服务的工作节点所属集群名称
  * `local_cluster`：若读取节点与客户端属于同一集群，则为 `true`，否则为 `false`
  * `hot_read`：若该工作节点已完整缓存读取的文件，则为 `true`；若未缓存或仅部分缓存，则为 `false`

当启用了被动缓存（passive cache）功能后，指标 `alluxio_passive_cache_async_loaded_files` 用于监控通过被动缓存机制加载的文件数量。

* **类型**：`counter`
* **组件**：`worker`
* **描述**：追踪由被动缓存机制触发、由工作节点加载的文件总数。该指标仅在 `alluxio.user.file.passive.cache.enabled` 设置为 `true` 时生效。它可以帮助分析有多少文件在首选工作节点上遇到缓存未命中，从而需要通过被动缓存机制加载。
* **标签（Labels）**：
  * `result`：文件加载的结果，可为 `"submitted"`（提交加载）、`"success"`（加载成功）或 `"failure"`（加载失败）。当文件被异步提交加载时，`"submitted"` 对应的计数器会增加；加载完成后，按结果分别增加 `"success"` 或 `"failure"` 对应的计数器。

## 多副本的局限

### 可变数据的一致性问题

多副本在数据不会发生变化的场景中最有效，因为副本之间不会出现不一致。\
如果 UFS 中的数据会随着时间的推移而变化，那么在不同时间从 UFS 加载的副本可能对应着文件的不同版本。\
因此，如果对于同一文件，两个客户端选择了两个不同副本，就可能出现数据不一致的问题。

要避免文件可变时可能出现的不一致，可以采取以下措施：

* 手动刷新副本的文件元数据，并使现有缓存副本失效。

这可以通过在调用 [分布式加载（Distributed Load）](https://documentation.alluxio.io/ee-ai-cn/ai-3.6/reference/user-cli#job-load) 工具时，加上 `-metadataOnly` 选项来实现：

```console
bin/alluxio job load --path s3://bucket/file --replicas 3 --metadataOnly --submit
```

以上的命令将启动一个分布式作业，加载文件 `s3://bucket/file` 的元数据。\
如果该文件有任何现存副本，且 Alluxio 检测到 UFS 中的文件已更改，则现有副本将失效。\
随后访问该文件时，Alluxio 将从 UFS 中加载最新数据。

* 为文件设置一个具有适当缓存控制策略的 [Cache Filter](https://documentation.alluxio.io/ee-ai-cn/ai-3.6/cache/cache-filter-policy)规则。

例如，如果一个文件预计平均每 10 分钟更新一次，则可使用 10 分钟的 `maxAge` 策略：

```json
{
  "apiVersion": 1,
  "data": {
	"defaultType": "maxAge",
	"defaultMaxAge": "10min"
  },
  "metadata": {
	"defaultType": "maxAge",
	"defaultMaxAge": "10min"
  }
}
```

当副本的最长有效期为 10 分钟时，它将在 10 分钟后失效并被 Worker 从 UFS 重新加载。\
每次 UFS 中的文件发生变化后，至少要等待 10 分钟，以便使任何现有的副本失效。

### 副本数量不是主动维护的级别

副本规则中指定的目标副本数量仅作为副本选择算法的输入参数。集群中某个时间点实际存在的副本数量可能小于、等于或大于配置中设定的目标值。Alluxio 并不会主动监控副本数量，也不会主动维护目标副本数量。

实际副本数量小于目标值的原因可能有以下几种：

* 文件曾经在多个工作节点上被缓存，但由于缓存容量不足，被部分工作节点驱逐，导致副本数量减少；
* 在缓存未命中时，副本可能是被动创建的。如果某客户端发现某个文件已被某个工作节点缓存，就会直接从该节点读取，不会创建新的副本。除非后续客户端访问一个未缓存该文件的节点，否则文件的副本数量不会增加。

而在以下情况下，实际副本数量可能会大于目标值：

* 当集群的工作节点发生变动（如加入或退出）时，副本选择算法的结果可能会改变；
* 某个原本包含副本的工作节点不再被选择用于提供副本服务，这会导致该副本不可访问；
* 此类“多余”副本会继续存在于工作节点的缓存中，直到因缓存不足被驱逐，或被手动执行“[清除过期缓存](https://documentation.alluxio.io/ee-ai-cn/ai-3.6/cache/stale-cache-cleaning)”操作清除为止。
