> For the complete documentation index, see [llms.txt](https://documentation.alluxio.io/ee-ai-cn/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://documentation.alluxio.io/ee-ai-cn/administration/managing-etcd.md).

# etcd 管理

Alluxio 把持久化的集群状态放在 etcd 里：挂载表、各类配置实体、部署许可证，以及在 开启版本化时的哈希环成员列表。**缓存数据不在 etcd 里**——它在 worker 的磁盘上，所以 大多数 etcd 操作都不会让缓存变冷。

在 Kubernetes 上，Operator 会替你部署 etcd。本页讲的是之后的事：指向你自己运维的 etcd、调整内置 etcd、迁移到另一套 etcd，以及在无法修复时重建。

安装前的容量与 quorum 规则见[前置条件](/ee-ai-cn/start/prerequisites.md#etcd)；跨可用区的 etcd 拓扑见[多可用区](/ee-ai-cn/high-availability/multi-az.md)。

## 使用外部 etcd

要使用你自己运维的 etcd，而不是 Operator 部署的那套：

```yaml
apiVersion: k8s-operator.alluxio.com/v1
kind: AlluxioCluster
spec:
  etcd:
    enabled: false
  properties:
    alluxio.etcd.endpoints: http://external-etcd:2379
    # 若 etcd 启用了 TLS，追加：
    # alluxio.etcd.tls.enabled: "true"
```

**已经在运行、已有状态的集群不要直接这么改**——新 etcd 是空的，挂载表不会跟着过去。 这种情况请走[迁移到另一套 etcd](#qian-yi-dao-ling-yi-tao-etcd)。

连接加密见 [TLS](/ee-ai-cn/administration/security/securing-alluxio-with-tls.md#bao-hu-etcd-tong-xin)。

## 定制内置 etcd

`spec.etcd` 下的字段遵循 [Bitnami etcd Helm chart](https://github.com/bitnami/charts/blob/main/bitnami/etcd/values.yaml)。 例如用[节点亲和性](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity)把 etcd pod 固定到特定可用区：

```yaml
apiVersion: k8s-operator.alluxio.com/v1
kind: AlluxioCluster
spec:
  etcd:
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
          - matchExpressions:
            - key: topology.kubernetes.io/zone
              operator: In
              values:
              - antarctica-east1
              - antarctica-west1
```

## 迁移到另一套 etcd

把持久化的键拷到新 etcd，改指向，重启。**全程缓存不冷**——它在 worker 磁盘上，不在 etcd。

适用于：从内置 etcd 迁到托管 etcd，或更换 etcd 集群。如果旧 etcd 已经无法恢复，本流程 不适用，见[重建 etcd](#chong-jian-etcd)。

### 需要拷贝什么

只有三样是持久化的、必须搬：

| 键                          | 内容         |
| -------------------------- | ---------- |
| `/alluxio/CONF/<cluster>/` | 挂载表和全部配置实体 |
| `/LE/license/<cluster>`    | 部署许可证      |
| `/LE/version/<cluster>`    | 许可证版本      |

其余都会自行重建。`/ServiceDiscovery/` 下的 worker 注册信息，以及 `/LE/instances/` 和 `/LE/status/` 下的许可证实例记录，都挂在 etcd 租约上——进程重启后过期并重新创建。

例外是哈希环成员列表，它**不会**自愈——但只在你开启了环版本化时才需要处理， 见[若开启了环版本化](#ruo-kai-qi-le-huan-ban-ben-hua)。

{% hint style="warning" %}
etcd 的值是二进制。下面每次拷贝都经 `base64` 往返，以保证字节完全一致——直接 `get | put` 会追加一个换行符，把值弄坏。
{% endhint %}

### 若任一端 etcd 启用了 TLS

托管 etcd 基本都启用，而下面的命令默认是明文。需要证书的那一端（源端也是 TLS 就两端都要）， 每条 `etcdctl` 都得带上：

```shell
etcdctl --endpoints=$DST \
  --cacert=/path/to/etcd-ca.crt \
  --cert=/path/to/etcd-client.crt \
  --key=/path/to/etcd-client.key \
  endpoint health
```

为了让后面的命令还能读，建议起个别名：

```shell
DSTCTL="etcdctl --endpoints=$DST --cacert=/path/to/etcd-ca.crt \
  --cert=/path/to/etcd-client.crt --key=/path/to/etcd-client.key"
```

第 6 步里 Alluxio 也需要同一套信任材料，与 `alluxio.etcd.endpoints` 并列：

```yaml
    alluxio.etcd.tls.enabled: "true"
    alluxio.etcd.tls.ca.cert: /path/to/etcd-ca.crt
    alluxio.etcd.tls.client.cert: /path/to/etcd-client.crt
    alluxio.etcd.tls.client.key: /path/to/etcd-client-key-pkcs8.pem
```

端点要写成 `https://`，而且 **Alluxio 要求客户端私钥是 PKCS#8 格式**——`etcdctl` 不在乎 这个，所以同一个密钥文件不能两边通用。格式转换，以及目标端还启用了密码认证时要用的 `alluxio.etcd.username` / `alluxio.etcd.password`，见 [TLS](/ee-ai-cn/administration/security/securing-alluxio-with-tls.md#bao-hu-etcd-tong-xin)。

### 前置条件

`etcdctl` 命令在任意一台装有 `etcdctl` 且能同时访问**新旧两个**端点的主机或 Pod 上执行。 这些命令用的是 v3 API——etcdctl v3.4 起是默认值，更老的版本需要先 `export ETCDCTL_API=3`。 `alluxio` 和 `kubectl` 命令则针对 Alluxio 的 Pod 执行。

以下变量设置一次即可，请替换成你自己的命名空间、集群名和端点：

```shell
# 源端：etcd 的 Service 端点，它前置了所有 HA 成员。
# 不是单个成员——如果你的托管 etcd 只提供各成员地址，就用逗号分隔全部列出。
SRC=http://alluxio-cluster-etcd.alx-ns:2379

# 目标端：新的 etcd Service 端点
DST=http://new-etcd:2379

# 集群名 = <命名空间>-<AlluxioCluster 名>，它是每个键的前缀
CL=alx-ns-alluxio-cluster
```

不要凭猜，确认一下 `CL`：

{% tabs %}
{% tab title="Kubernetes (Operator)" %}

```shell
kubectl exec -n alx-ns alluxio-cluster-coordinator-0 -c alluxio-coordinator -- \
  alluxio conf get alluxio.cluster.name
```

{% endtab %}

{% tab title="Docker / Bare-Metal" %}

```shell
bin/alluxio conf get alluxio.cluster.name
```

{% endtab %}
{% endtabs %}

```console
alx-ns-alluxio-cluster
```

该值必须等于 `$CL`。不相等的话，下面所有键路径都是错的。

继续之前先确认 `etcdctl` 两端都通：

```shell
etcdctl --endpoints=$SRC endpoint health
etcdctl --endpoints=$DST endpoint health
```

```console
http://alluxio-cluster-etcd.alx-ns:2379 is healthy: successfully committed proposal: took = 1.9ms
http://new-etcd:2379 is healthy: successfully committed proposal: took = 2.4ms
```

### 第 1 步：记录基线

切换完成后，这两项输出必须没有变化。

{% tabs %}
{% tab title="Kubernetes (Operator)" %}

```shell
# Worker 身份
kubectl exec -n alx-ns alluxio-cluster-coordinator-0 -c alluxio-coordinator -- \
  alluxio info nodes

# 挂载 URI——事后要逐字节比对，包括协议前缀
kubectl exec -n alx-ns alluxio-cluster-coordinator-0 -c alluxio-coordinator -- \
  alluxio mount list
```

{% endtab %}

{% tab title="Docker / Bare-Metal" %}

```shell
bin/alluxio info nodes
bin/alluxio mount list
```

{% endtab %}
{% endtabs %}

把两份输出存下来，第 7 步要拿它对比。

```console
WorkerId        Address          Status
worker-1        10.0.1.11:29999  ONLINE
worker-2        10.0.1.12:29999  ONLINE
```

### 第 2 步：检查目标 etcd

目标 etcd 里可能已经有无关的键，这**没关系**——Alluxio 只读写自己的前缀。不允许存在的 是**当前这个** `cluster.name` 的状态。

```shell
for P in /alluxio/CONF/$CL/ /LE/license/$CL /LE/version/$CL /DHT/$CL /ServiceDiscovery/$CL; do
  echo -n "$P -> "; etcdctl --endpoints=$DST get "$P" --prefix --keys-only | grep -c .
done
```

```console
/alluxio/CONF/alx-ns-alluxio-cluster/ -> 0
/LE/license/alx-ns-alluxio-cluster -> 0
/LE/version/alx-ns-alluxio-cluster -> 0
/DHT/alx-ns-alluxio-cluster -> 0
/ServiceDiscovery/alx-ns-alluxio-cluster -> 0
```

出现非零，说明之前有一次安装（或中断的安装）已经用同一个 `cluster.name` 使用过这套 etcd。先清掉这些前缀，或者换一个 `cluster.name`，否则会叠加在陈旧状态之上。

### 第 3 步：拷贝持久化的键

```shell
# 幂等——重复执行只是用相同字节覆盖相同的键
{ etcdctl --endpoints=$SRC get /alluxio/CONF/$CL/ --prefix --keys-only | grep -v '^$'; \
  echo "/LE/license/$CL"; echo "/LE/version/$CL"; } \
  | while read -r K; do
      [ -z "$K" ] && continue
      etcdctl --endpoints=$SRC get "$K" -w json | sed -E 's/.*"value":"([^"]+)".*/\1/' | base64 -d \
        | etcdctl --endpoints=$DST put "$K" >/dev/null && echo "copied: $K"
    done
```

```console
copied: /alluxio/CONF/alx-ns-alluxio-cluster/mount/s3a:%2F%2Fbucket
copied: /alluxio/CONF/alx-ns-alluxio-cluster/cachefilter/default
copied: /LE/license/alx-ns-alluxio-cluster
copied: /LE/version/alx-ns-alluxio-cluster
```

每个键对应一行 `copied:`。没有输出的键就是没传过去——第 4 步会抓到。

### 第 4 步：校验拷贝结果

每一行都必须是 `MATCH`。

```shell
vfy(){ a=$(etcdctl --endpoints=$SRC get "$1" -w json | sed -E 's/.*"value":"([^"]+)".*/\1/'); \
       b=$(etcdctl --endpoints=$DST get "$1" -w json | sed -E 's/.*"value":"([^"]+)".*/\1/'); \
       [ "$a" = "$b" ] && echo "MATCH    $1" || echo "MISMATCH $1"; }

{ etcdctl --endpoints=$SRC get /alluxio/CONF/$CL/ --prefix --keys-only | grep -v '^$'; \
  echo "/LE/license/$CL"; echo "/LE/version/$CL"; } \
  | while read -r K; do [ -z "$K" ] && continue; vfy "$K"; done
```

```console
MATCH    /alluxio/CONF/alx-ns-alluxio-cluster/mount/s3a:%2F%2Fbucket
MATCH    /alluxio/CONF/alx-ns-alluxio-cluster/cachefilter/default
MATCH    /LE/license/alx-ns-alluxio-cluster
MATCH    /LE/version/alx-ns-alluxio-cluster
```

出现 `MISMATCH` 说明该键没能完整往返，重新拷贝它之后再继续。**只要还有一行是 `MISMATCH` 就不要往下走。**

### 第 5 步：导出源端做回滚兜底

```shell
etcdctl --endpoints=$SRC get "" --prefix -w json > source-keyspace-full.json
```

留好这个文件。旧 etcd 的存储卷一旦被删，[回滚](#hui-gun)就只能靠它。

### 第 6 步：改指向并重启

在 `AlluxioCluster` CR 中设置 `etcd.enabled: false` 并加上 `alluxio.etcd.endpoints`。 **不要**改 `metadata.name`、挂载配置或 `alluxio.cluster.name`——集群名正是所有已拷贝 键的前缀。

```shell
# 幂等。Operator 会删除内置 etcd 但保留其 PVC，然后重启 coordinator 和 worker。
kubectl apply -f alluxio-cluster.yaml -n alx-ns

# 用 wait 而不是 watch
kubectl wait --for=condition=Ready pod -n alx-ns \
  -l app.kubernetes.io/component=coordinator --timeout=300s
kubectl wait --for=condition=Ready pod -n alx-ns \
  -l app.kubernetes.io/component=worker --timeout=300s
```

```console
pod/alluxio-cluster-coordinator-0 condition met
pod/alluxio-cluster-worker-0 condition met
pod/alluxio-cluster-worker-1 condition met
```

FUSE pod 也必须滚动。DaemonSet 方式的 FUSE pod 会随集群一起重启；CSI 方式的不会， 需要手动重启它们，然后再重启挂载它们的应用 pod。

```shell
kubectl get pods -n alx-ns -l 'app.kubernetes.io/component in (fuse, csi-fuse)'
# 预期：所有 pod 均为 Running，且 AGE 晚于上面那次 apply
```

Docker 和裸机上没有 CR：在每个节点的 `alluxio-site.properties` 里设置 `alluxio.etcd.endpoints`，然后重启 coordinator、worker 和 FUSE 进程。

### 第 7 步：验证

{% tabs %}
{% tab title="Kubernetes (Operator)" %}

```shell
kubectl exec -n alx-ns alluxio-cluster-coordinator-0 -c alluxio-coordinator -- \
  alluxio info nodes
# 预期：WorkerId 与第 1 步相同。地址可以变，身份不能变。

kubectl exec -n alx-ns alluxio-cluster-coordinator-0 -c alluxio-coordinator -- \
  alluxio mount list
# 预期：与第 1 步逐字节相同，含协议前缀
```

{% endtab %}

{% tab title="Docker / Bare-Metal" %}

```shell
bin/alluxio info nodes
bin/alluxio mount list
```

{% endtab %}
{% endtabs %}

然后确认缓存还在。从 worker 上读出这两个计数器，重新读取一份迁移前已缓存的数据集， 再读一次——端点见[监控](/ee-ai-cn/administration/monitoring-alluxio.md)：

```shell
kubectl -n alx-ns exec alluxio-cluster-worker-0 -- \
  curl -s http://localhost:30000/metrics/ \
  | grep -E 'alluxio_cache_(hit|miss)_calls_total'
# 预期：重读之后 hit 大约每文件加 1，miss 不动。
# miss 有任何上升，都说明那部分数据又回去读 UFS 了——缓存没保住。
```

### 若开启了环版本化

哈希环成员列表不会自行重建，所以也要拷。这只在 `alluxio.user.consistent.hash.ring.versioning.enabled` 为 `true` 时适用；它默认是 `false`，默认情况下跳过本节。

```shell
# 输出为空说明版本化未开启——跳过
etcdctl --endpoints=$SRC get /DHT/$CL/currentMembers

# 否则把三个列表都拷过去
for M in currentMembers stagingMembers historicMembers; do
  etcdctl --endpoints=$SRC get /DHT/$CL/$M -w json | sed -E 's/.*"value":"([^"]+)".*/\1/' | base64 -d \
    | etcdctl --endpoints=$DST put /DHT/$CL/$M >/dev/null 2>&1 && echo "copied: /DHT/$CL/$M"
done
```

这几个列表的作用见[哈希环与 Worker 生命周期](/ee-ai-cn/administration/managing-ring.md)。

### 回滚

**旧 etcd 的 PVC 还在**（正常情况，第 6 步会保留它们）：把 CR 改回去 （`etcd.enabled: true`，删掉 `alluxio.etcd.endpoints` 那行）并 apply，Operator 会按名字 重新接管原来的 PVC。

```shell
kubectl apply -f alluxio-cluster.yaml -n alx-ns
kubectl wait --for=condition=Ready pod -n alx-ns \
  -l app.kubernetes.io/component=coordinator --timeout=300s
# 预期输出 "condition met"。随后对着旧 etcd 重跑第 7 步。
```

**PVC 已经删了**：起一套空的 etcd，把第 5 步导出的持久化键回放进去，再让 CR 指向它：

```shell
python3 - source-keyspace-full.json http://fresh-etcd:2379 <<'PY'
import json, sys, base64, subprocess
d = json.load(open(sys.argv[1])); dst = sys.argv[2]
KEEP = ("/alluxio/CONF/", "/LE/license/", "/LE/version/")
for kv in d["kvs"]:
    if int(kv.get("lease", 0) or 0):
        continue                      # 带租约的键会自行重建
    k = base64.b64decode(kv["key"]).decode()
    if not k.startswith(KEEP):
        continue
    v = base64.b64decode(kv.get("value", "") or "")
    subprocess.run(["etcdctl", "--endpoints", dst, "put", k], input=v, check=True)
    print("restored", k)
PY
```

### 清理

只在第 7 步通过之后做。删掉这些卷，上面那条简单回滚路径就不可逆了。

```shell
kubectl get pvc -n alx-ns | grep etcd
kubectl delete pvc data-alluxio-cluster-etcd-0 data-alluxio-cluster-etcd-1 \
  data-alluxio-cluster-etcd-2 -n alx-ns
```

## 重建 etcd

Alluxio 能在一段宽限期内（通常 24 小时）容忍 etcd 不可用而不影响 I/O，所以能干净重启的 etcd pod 无需干预。只有在集群无法恢复、数据也拷不出来时才重建——否则应当走 [迁移](#qian-yi-dao-ling-yi-tao-etcd)，那样能保住状态。

{% hint style="danger" %}
重建会丢弃包括挂载表在内的集群状态，事后需要重新创建挂载。
{% endhint %}

1. 关停 Alluxio 集群：`kubectl delete -f alluxio-cluster.yaml`
2. 删除 etcd 的 PVC：`kubectl -n alx-ns delete pvc -l app.kubernetes.io/component=etcd`
3. 清理节点上的 etcd 数据：登录每个曾运行 etcd pod 的 Kubernetes 节点，删除 etcd PV 所用 host path 目录下的内容。
4. 重建集群：`kubectl create -f alluxio-cluster.yaml`。Operator 会创建一套全新的空 etcd。
5. 重新挂载 UFS 路径。若你用 `UnderFileSystem` CRD 管理挂载，它们会自动恢复；否则用 `alluxio mount add --path <路径> --ufs-uri <URI>` 重新添加。

三副本的 etcd 集群只有在副本分布于**不同节点**时才能容忍单节点故障。副本挤在同一节点上会一起失去 quorum。

## 相关文档

* [前置条件 — etcd](/ee-ai-cn/start/prerequisites.md#etcd) —— 安装前的节点数与 quorum 规则
* [多可用区](/ee-ai-cn/high-availability/multi-az.md) —— 跨区的独立 / 共享 / 外部 etcd 拓扑
* [TLS](/ee-ai-cn/administration/security/securing-alluxio-with-tls.md#bao-hu-etcd-tong-xin) —— 加密 etcd 连接
* [监控](/ee-ai-cn/administration/monitoring-alluxio.md) —— etcd 可用性告警
* [Job Service](/ee-ai-cn/administration/managing-job-service.md#ke-xuan-wei-zuo-ye-diao-du-shi-yong-du-li-etcd-ji-qun) —— 为作业调度单独部署 etcd
* [在 Kubernetes 上安装](/ee-ai-cn/start/installing-on-kubernetes.md) —— 安装过程中遇到的 etcd 症状
