# etcd v3.7 学习手册 · 03 运维运营篇
> 基于 https://etcd.io/docs/v3.7/ 整理
> 文档版本:v3.7.0
> 整理日期:2026-09-18
> 本册定位:运维运营篇(部署、配置、监控、故障、升级)。兄弟分册:《01-核心概念篇》《02-开发实战篇》。
> 范围声明:按用户指定「只做 v3.7 主线」,本手册未覆盖 v3.0–v3.5 的历史升级/降级步骤页与 v2.x benchmark 页(共 11 页),清单见《etcd-v3.7-抓取清单.md》。
---
## 目录
- [1. 本册导读](#1-本册导读)
- [2. 集群部署:三种引导方式与容器 / Kubernetes](#2-集群部署三种引导方式与容器--kubernetes)
- [3. 配置全参(flag / 环境变量 / 默认值)](#3-配置全参flag--环境变量--默认值)
- [4. 硬件、支持平台与系统调优](#4-硬件支持平台与系统调优)
- [5. 成员与运行时管理](#5-成员与运行时管理)
- [6. 日常维护:压缩、碎片整理、配额、快照](#6-日常维护压缩碎片整理配额快照)
- [7. 灾难恢复](#7-灾难恢复)
- [8. 故障模式](#8-故障模式)
- [9. 数据损坏](#9-数据损坏)
- [10. 安全:TLS、认证与 RBAC](#10-安全tls认证与-rbac)
- [11. 监控与告警](#11-监控与告警)
- [12. 指标全表(123 条)](#12-指标全表123-条)
- [13. 代理层:etcd gateway 与 gRPC proxy](#13-代理层etcd-gateway-与-grpc-proxy)
- [14. 升级与降级](#14-升级与降级)
- [15. 性能与容量:三个 benchmark](#15-性能与容量三个-benchmark)
- [16. 生态与社区](#16-生态与社区)
- [17. FAQ 全量](#17-faq-全量)
- [18. 附录 A:官方页面 → 本册章节映射表](#18-附录-a官方页面--本册章节映射表)
- [19. 附录 B:本次未覆盖页面清单](#19-附录-b本次未覆盖页面清单)
---
## 1. 本册导读
### 1.1 本册覆盖的官方页面
本册由 8 个素材批次整理而成,共覆盖 etcd v3.7 官方文档 **50 个页面**(不含索引页重复计数):
| 批次 | 主题 | 覆盖页面数 | 对应本册章节 |
|---|---|---|---|
| batch-04 | 集群引导 / 配置项全表 / 硬件 / 支持平台 / feature gates / 调优 | 6 | 第 2、3、4 章 |
| batch-05 | 运行时重配置 / 维护 / 灾难恢复 / 故障模式 / 数据损坏 | 5 | 第 5、6、7、8、9 章 |
| batch-06 | TLS 安全模型 / 认证 / RBAC / 版本策略 / 监控 / metrics 概览 | 6 | 第 10、11 章 |
| batch-07 | 容器部署 / K8s StatefulSet / gateway / gRPC proxy / 命名与发现 / gRPC gateway / 嵌入 / modules | 8 | 第 2、13 章 |
| batch-09 | 运维任务(搭集群 / 查状态 / 增删成员 / 快照 / 选主)+ 两版 discovery 协议 | 8 | 第 2、5、6 章 |
| batch-10 | 升级总纲 / 3.5→3.6 / 3.6→3.7 / 降级总纲 / 3.7→3.6 / 3.6→3.5 | 6 | 第 14 章 |
| batch-11 | 生态工具与客户端库 / 日志约定 / 报 bug / issue 与 PR 分诊 / FAQ | 7 | 第 16、17 章 |
| batch-13 | 3 个 benchmark + metrics-latest 指标全表 | 4 | 第 12、15 章 |
| **合计** | | **50** | |
> 逐页映射见 [附录 A](#18-附录-a官方页面--本册章节映射表);未纳入的 11 页见 [附录 B](#19-附录-b本次未覆盖页面清单)。
### 1.2 读者对象
- **SRE / 运维工程师**:负责 etcd 集群的部署、扩缩容、备份恢复、升级降级与值班处置。重点读第 2、5、6、7、8、14 章。
- **平台 / 中间件工程师**:负责把 etcd 作为底座提供给上层业务(如 Kubernetes)。重点读第 3、4、13 章。
- **安全合规同学**:重点读第 10 章(TLS、认证、RBAC)。
- **可观测性 / 稳定性同学**:重点读第 11、12 章(监控、指标全表)与第 15 章(容量推算)。
- **正在排查线上问题的值班同学**:直接跳到第 8 章「故障模式」与第 9 章「数据损坏」的「现象 → 原因 → 处置」三段式表格。
### 1.3 运维全景图
```text
┌──────────────────────────────────────────────┐
│ 规划阶段 (Plan) │
│ 硬件选型(Ch4) · 平台矩阵(Ch4) · 容量推演(Ch15) │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ 交付阶段 (Deploy) │
│ static / etcd discovery / DNS discovery(Ch2) │
│ 容器化 docker run(Ch2) · K8s StatefulSet(Ch2) │
│ 配置全参 flag / env / yaml(Ch3) │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────────────────────────┼───────────────────────────────────────────┐
│ │ │
┌─────▼──────────┐ ┌───────────▼───────────┐ ┌──────────▼─────────┐
│ 安全 (Secure) │ │ 运行 (Operate) │ │ 观测 (Observe) │
│ TLS 双向(Ch10) │ │ 成员增删/learner(Ch5) │ │ /metrics /health(Ch11)│
│ auth enable │ │ 压缩/整理/配额(Ch6) │ │ 123 条指标(Ch12) │
│ RBAC(Ch10) │ │ 快照备份(Ch6) │ │ Prometheus/Grafana │
└─────┬──────────┘ └───────────┬───────────┘ └──────────┬─────────┘
│ │ │
│ ┌────────────▼────────────┐ │
│ │ 故障 (Failure) │◀─────────────────────────────┘
│ │ 少数派/多数派/分区(Ch8) │
│ │ 数据损坏检测与修复(Ch9) │
│ │ 灾难恢复 restore(Ch7) │
│ └────────────┬────────────┘
│ │
└───────────────────────────────────────────┼───────────────────────────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ 演进 (Evolve) │
│ 一个 minor 一跳(Ch14) · 滚动替换 · health 校验 │
│ downgrade validate / enable / cancel(Ch14) │
│ 代理层 gateway / gRPC proxy(Ch13) │
└──────────────────────────────────────────────┘
```
### 1.4 使用约定
- 中文正文叙述;命令、flag、环境变量、指标名、YAML 字段名一律保留英文原文。
- 引用官方文档原文处标注 `> 引自:[页面标题](URL)`;非官方推导、经验性补充标注 `> 💡 补充:`。
- 所有数值(默认值、benchmark 数字、配额、超时)均原样保留,不做四舍五入或单位换算。
- 素材中标注「源文未提供」的,本册如实说明,不编造。
---
## 2. 集群部署:三种引导方式与容器 / Kubernetes
> 引自:[Clustering Guide](https://etcd.io/docs/v3.7/op-guide/clustering/)、[Run etcd clusters inside containers](https://etcd.io/docs/v3.7/op-guide/container/)、[Run etcd clusters as a Kubernetes StatefulSet](https://etcd.io/docs/v3.7/op-guide/kubernetes/)
etcd 集群的**引导(bootstrap)**有三种机制:**static**、**etcd discovery**、**DNS discovery**。集群运行起来之后的成员增删走 runtime reconfiguration(第 5 章),不再依赖引导机制。
静态方式要求每个成员都事先知道其他成员;若成员 IP 事先未知(云环境、DHCP),可借助 discovery 服务引导。
### 2.1 示例集群(全章通用)
| Name | Address | Hostname |
|---|---|---|
| infra0 | 10.0.1.10 | infra0.example.com |
| infra1 | 10.0.1.11 | infra1.example.com |
| infra2 | 10.0.1.12 | infra2.example.com |
### 2.2 方式一:Static(静态引导)
#### 适用场景与参数说明
- **适用场景**:成员 IP / 主机名**事先已知**、节点数量固定的环境(自建机房、固定 IP 的云主机)。
- **核心参数**:`--initial-cluster` 提供离线引导配置,`--initial-cluster-state=new` 表示新建集群。
环境变量形式:
```bash
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380"
ETCD_INITIAL_CLUSTER_STATE=new
```
等价命令行:
```bash
--initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
--initial-cluster-state new
```
#### 启动命令(infra0)
```bash
$ etcd --name infra0 --initial-advertise-peer-urls http://10.0.1.10:2380 \
--listen-peer-urls http://10.0.1.10:2380 \
--listen-client-urls http://10.0.1.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.10:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
--initial-cluster-state new
```
infra1(`--name infra1`,地址 `10.0.1.11`)、infra2(`--name infra2`,地址 `10.0.1.12`)命令同构,仅 `--name`、`--initial-advertise-peer-urls`、`--listen-peer-urls`、`--listen-client-urls`、`--advertise-client-urls` 中的 IP 替换为各自地址,其余 flag 与上面完全一致。
#### 三个必知的坑
1. `initial-cluster` 中的 URL 是 **advertised peer URLs**,必须与对应节点上的 `--initial-advertise-peer-urls` 一致。
2. 强烈建议为测试用途反复创建的集群设置**唯一的 `--initial-cluster-token`**,以便 etcd 生成唯一的 cluster ID 与 member ID,避免跨集群交互导致数据损坏。
3. etcd 在 `--listen-client-urls` 上接受客户端流量,通过 `--advertise-client-urls` 向其他成员、代理、客户端通告;**常见错误是把它设为 localhost 或保留默认值,导致远程客户端无法访问**。
> 补充:`--initial-cluster` 开头的命令行参数在 etcd **后续运行中会被忽略**,引导完成后可移除这些环境变量/flag。后续变更成员请走 runtime configuration。
#### TLS:自签名证书启动示例
每个成员需要由共享集群 CA(`ca.crt`)签发的唯一密钥对(`member.crt`、`member.key`),peer 与 client 连接各一套。
```bash
$ etcd --name infra0 --initial-advertise-peer-urls https://10.0.1.10:2380 \
--listen-peer-urls https://10.0.1.10:2380 \
--listen-client-urls https://10.0.1.10:2379,https://127.0.0.1:2379 \
--advertise-client-urls https://10.0.1.10:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra0=https://10.0.1.10:2380,infra1=https://10.0.1.11:2380,infra2=https://10.0.1.12:2380 \
--initial-cluster-state new \
--client-cert-auth --trusted-ca-file=/path/to/ca-client.crt \
--cert-file=/path/to/infra0-client.crt --key-file=/path/to/infra0-client.key \
--peer-client-cert-auth --peer-trusted-ca-file=ca-peer.crt \
--peer-cert-file=/path/to/infra0-peer.crt --peer-key-file=/path/to/infra0-peer.key
```
infra1 / infra2 将 `infra0` 与 `10.0.1.10` 分别替换为 `infra1`/`10.0.1.11`、`infra2`/`10.0.1.12`(含证书路径 `infra1-client.crt`、`infra1-peer.crt` 等)。
#### TLS:自动证书
只需加密、不需认证连接时,etcd 可自动生成密钥;初始化时每个成员基于自己的 advertised IP 与主机名生成密钥集。
```bash
$ etcd --name infra0 --initial-advertise-peer-urls https://10.0.1.10:2380 \
--listen-peer-urls https://10.0.1.10:2380 \
--listen-client-urls https://10.0.1.10:2379,https://127.0.0.1:2379 \
--advertise-client-urls https://10.0.1.10:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra0=https://10.0.1.10:2380,infra1=https://10.0.1.11:2380,infra2=https://10.0.1.12:2380 \
--initial-cluster-state new \
--auto-tls \
--peer-auto-tls
```
#### 常见错误(现象 → 原因 → 处置)
| 现象(日志 / 退出码) | 原因 | 处置 |
|---|---|---|
| `etcd: infra1 not listed in the initial cluster config` → `exit 1` | 新主机未列入枚举节点列表 | 新集群必须把该 host 加入 `--initial-cluster` 成员列表 |
| `etcd: error setting up initial cluster: infra0 has different advertised URLs in the cluster and advertised peer URLs list` → `exit 1` | 节点的 advertised 地址与列表中枚举地址不一致(如 advertised `http://127.0.0.1:2380` 而列表为 `http://10.0.1.10:2380`) | 让 `--initial-advertise-peer-urls` 与 `--initial-cluster` 中该成员的 URL 完全一致 |
| `etcd: conflicting cluster ID to the target cluster (c6ab534d07e8fcc4 != bc25ea2a74fb18b0). Exiting.` → `exit 1` | 使用不同配置参数加入集群导致 cluster ID 不匹配 | 清空 data-dir 重新引导,或修正 `--initial-cluster-token` / `--initial-cluster-state` |
### 2.3 方式二:etcd Discovery
#### 适用场景
用于 **peer IP 事先未知**的场景(云厂商、DHCP)。两种方式:etcd discovery service 与 DNS SRV records(后者见 2.4)。
**discovery URL 生命周期**:一个 discovery URL 标识唯一 etcd 集群,**每个新集群都应使用新的 URL,不要复用**;且 discovery URL **仅用于初始引导**,集群运行后变更成员走 runtime reconfiguration。
#### 自建 discovery 服务
```bash
$ curl -X PUT https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=3
```
设置 size key 后即创建期望集群大小为 3 的 discovery URL,实际使用的 URL 为 `https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83`。
**每个成员必须指定不同的 `--name` flag,`Hostname` 或 `machine-id` 是好选择,否则会因重名导致 discovery 失败。**
```bash
$ etcd --name infra0 --initial-advertise-peer-urls http://10.0.1.10:2380 \
--listen-peer-urls http://10.0.1.10:2380 \
--listen-client-urls http://10.0.1.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.10:2379 \
--discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
```
infra1、infra2 同构(替换 IP 与 name)。所有成员注册完成后集群即启动。
#### 公共 discovery 服务
无现成集群时可用 `discovery.etcd.io`:
```bash
$ curl https://discovery.etcd.io/new?size=3
https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
```
不指定 size 时默认为 3。对应环境变量 `ETCD_DISCOVERY=https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de`,命令行 `--discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de`。各成员启动命令同上(把 `--discovery` 换成该 URL,name 各不相同)。可用环境变量 `ETCD_DISCOVERY_PROXY` 让 etcd 通过 HTTP 代理连接 discovery 服务。
#### 错误与警告
| 类型 | 输出 | 说明 |
|---|---|---|
| 服务端错误 | `etcd: error: the cluster doesn’t have a size configuration value in https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de/_config` → `exit 1` | discovery URL 未设置 size |
| 无害警告 | `etcdserver: discovery token ignored since a cluster has already been initialized. Valid log found at /var/lib/etcd` | 本机已初始化过,discovery URL 被忽略 |
> 💡 补充:v3.7 中 v2 store 已被完全移除,「自建 discovery 服务」依赖 v2 keys API 的写法在 v3.7 集群上**不再可用**;v3.7 提供的 discovery 相关 flag 是另一套(`--discovery-token` / `--discovery-endpoints` 等,见第 5.8 节 dev-internal 版协议)。本页为官方 clustering 文档原文,按原文保留。
### 2.4 方式三:DNS Discovery
#### 适用场景与查询顺序
`--discovery-srv` 指定 discovery SRV 记录所在的 DNS 域名。设置 `--discovery-srv example.com` 后按以下顺序查询:
1. `_etcd-server-ssl._tcp.example.com`
2. `_etcd-server._tcp.example.com`
若找到 `_etcd-server-ssl._tcp.example.com`,etcd 将尝试通过 **TLS** 引导。
帮助客户端发现集群的 SRV 记录查询顺序:
1. `_etcd-client._tcp.example.com`
2. `_etcd-client-ssl._tcp.example.com`
若找到 `_etcd-client-ssl._tcp.example.com`,客户端将用 SSL/TLS 通信。
#### TLS 注意事项(重要)
- 使用 TLS 时,discovery SRV 记录域名(如 `example.com`)必须包含在证书的 **DNS SAN** 中(连同主机名),否则集群化失败,日志形如:
`[...] rejected connection from "10.0.1.11:53162" (error "remote error: tls: bad certificate", ServerName "example.com")`
- 若使用 TLS 而没有自定义 CA,discovery 域名(如 `example.com`)**必须匹配 SRV 记录域名**(如 `infra1.example.com`),以缓解伪造 SRV 记录指向他域的攻击。
- `--discovery-srv-name` 可为查询的 SRV 名追加后缀,用于在同一域下区分多个 etcd 集群。例如 `discovery-srv=example.com` + `-discovery-srv-name=foo` 时查询:
- `_etcd-server-ssl-foo._tcp.example.com`
- `_etcd-server-foo._tcp.example.com`
#### 创建 DNS SRV 记录
```bash
$ dig +noall +answer SRV _etcd-server._tcp.example.com
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra0.example.com.
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra1.example.com.
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra2.example.com.
```
```bash
$ dig +noall +answer SRV _etcd-client._tcp.example.com
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra0.example.com.
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra1.example.com.
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra2.example.com.
```
```bash
$ dig +noall +answer infra0.example.com infra1.example.com infra2.example.com
infra0.example.com. 300 IN A 10.0.1.10
infra1.example.com. 300 IN A 10.0.1.11
infra2.example.com. 300 IN A 10.0.1.12
```
#### 用 DNS 引导集群(域名形式)
成员可通告域名或 IP,引导过程会解析 DNS A 记录。**自 3.2 起(3.1 仅打印警告)`--listen-peer-urls` 与 `--listen-client-urls` 会拒绝接受域名**作为网络接口绑定地址。`--initial-advertise-peer-urls` 解析后的地址**必须匹配** SRV 目标解析出的地址之一。
```bash
$ etcd --name infra0 \
--discovery-srv example.com \
--initial-advertise-peer-urls http://infra0.example.com:2380 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster-state new \
--advertise-client-urls http://infra0.example.com:2379 \
--listen-client-urls http://0.0.0.0:2379 \
--listen-peer-urls http://0.0.0.0:2380
```
infra1、infra2 同构(域名分别换为 `infra1.example.com`、`infra2.example.com`)。
#### 用 DNS 引导集群(IP 形式)
```bash
$ etcd --name infra0 \
--discovery-srv example.com \
--initial-advertise-peer-urls http://10.0.1.10:2380 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster-state new \
--advertise-client-urls http://10.0.1.10:2379 \
--listen-client-urls http://10.0.1.10:2379 \
--listen-peer-urls http://10.0.1.10:2380
```
infra1(10.0.1.11)、infra2(10.0.1.12)同构。
自 v3.1.0 起(v3.2.9 除外),当 `etcd --discovery-srv=example.com` 配置 TLS 时,服务端仅在提供的证书 SAN 字段包含根域 `example.com` 条目时才认证 peer/client。
#### 三种引导方式对比
| 维度 | Static | etcd Discovery | DNS Discovery |
|---|---|---|---|
| 前置条件 | 成员 IP 事先已知 | 有可用的 etcd 集群或公共 discovery 服务 | 可控的 DNS SRV 记录 |
| 核心 flag | `--initial-cluster` | `--discovery` | `--discovery-srv` |
| 集群规模约定 | 写在 `--initial-cluster` 里 | 写在 discovery URL 的 `_config/size` | 由 SRV 记录条数决定 |
| 典型适用 | 自建机房 / 固定 IP | 云厂商 DHCP、临时集群 | 已有 DNS 基础设施、需自动化 |
| 主要坑 | advertised 地址必须与列表一致 | name 不能重复;URL 不能复用 | 证书 SAN 必须含 SRV 域名 |
### 2.5 容器部署(Docker)
> 引自:[Run etcd clusters inside containers](https://etcd.io/docs/v3.7/op-guide/container/)
本页演示用 Docker 走 **static bootstrap** 方式启动 etcd。
**网络暴露前提**:要把 etcd API 暴露给 Docker 主机之外的客户端,必须使用容器的 host IP 地址(可用 `docker inspect` 获取);也可以在 `docker run` 上加 `--net=host` 跳过容器独立网络栈。
#### 镜像仓库与版本
镜像仓库有两处:`quay.io/coreos/etcd`;自 v3.2.5 起可用 `gcr.io/etcd-development/etcd`(脚本中后者覆盖前者)。版本变量 `ETCD_VERSION` 取当前文档版本(本册为 v3.7.0)。
#### 单节点 etcd
```bash
export NODE1=192.168.1.21
docker volume create --name etcd-data
export DATA_DIR="etcd-data"
ETCD_VERSION=v3.7.0
REGISTRY=quay.io/coreos/etcd
# available from v3.2.5
REGISTRY=gcr.io/etcd-development/etcd
docker run \
-p 2379:2379 \
-p 2380:2380 \
--volume=${DATA_DIR}:/etcd-data \
--name etcd ${REGISTRY}:${ETCD_VERSION} \
/usr/local/bin/etcd \
--data-dir=/etcd-data --name node1 \
--initial-advertise-peer-urls http://${NODE1}:2380 --listen-peer-urls http://0.0.0.0:2380 \
--advertise-client-urls http://${NODE1}:2379 --listen-client-urls http://0.0.0.0:2379 \
--initial-cluster node1=http://${NODE1}:2380
```
要素说明:
| 要素 | 值 |
|---|---|
| 端口映射 | 2379 客户端、2380 peer |
| 容器内数据目录 | `/etcd-data` |
| 容器名 | `etcd` |
| 入口命令 | `/usr/local/bin/etcd` |
| 镜像 | `quay.io/coreos/etcd` 或 `gcr.io/etcd-development/etcd` |
查看成员:
```bash
etcdctl --endpoints=http://${NODE1}:2379 member list
```
#### 3 节点 etcd 集群
```bash
REGISTRY=quay.io/coreos/etcd
# available from v3.2.5
REGISTRY=gcr.io/etcd-development/etcd
# For each machine
ETCD_VERSION=v3.7.0
TOKEN=my-etcd-token
CLUSTER_STATE=new
NAME_1=etcd-node-0
NAME_2=etcd-node-1
NAME_3=etcd-node-2
HOST_1=10.20.30.1
HOST_2=10.20.30.2
HOST_3=10.20.30.3
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2380,${NAME_3}=http://${HOST_3}:2380
DATA_DIR=/var/lib/etcd
# For node 1
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
docker run \
-p 2379:2379 \
-p 2380:2380 \
--volume=${DATA_DIR}:/etcd-data \
--name etcd ${REGISTRY}:${ETCD_VERSION} \
/usr/local/bin/etcd \
--data-dir=/etcd-data --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://0.0.0.0:2380 \
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://0.0.0.0:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
```
node 2、node 3 结构完全相同,仅把 `THIS_NAME=${NAME_2}`/`THIS_IP=${HOST_2}`、`THIS_NAME=${NAME_3}`/`THIS_IP=${HOST_3}` 替换即可(`docker run` 与全部 flag 一字不差复用)。三节点共用同一个 `CLUSTER` 字符串与同一个 `--initial-cluster-token`,`--initial-cluster-state=new`。
使用 API version 3 的 etcdctl:
```bash
docker exec etcd /usr/local/bin/etcdctl put foo bar
```
#### Bare Metal
裸机 3 节点部署可参考 coreos-baremetal 仓库的 examples(https://github.com/coreos/coreos-baremetal/tree/master/examples)。
#### 挂载证书 volume(重要坑)
etcd release 容器**不含默认根证书**。若要使用被根 CA 信任的 HTTPS(例如 discovery),需把证书目录挂进容器。
rkt 示例(`--insecure-options=image`,REGISTRY 写作 `docker://gcr.io/etcd-development/etcd`):
```bash
rkt run \
--insecure-options=image \
--volume etcd-ssl-certs-bundle,kind=host,source=/etc/ssl/certs/ca-certificates.crt \
--mount volume=etcd-ssl-certs-bundle,target=/etc/ssl/certs/ca-certificates.crt \
${REGISTRY}:${ETCD_VERSION} -- --name my-name \
--initial-advertise-peer-urls http://localhost:2380 --listen-peer-urls http://localhost:2380 \
--advertise-client-urls http://localhost:2379 --listen-client-urls http://localhost:2379 \
--discovery https://discovery.etcd.io/c11fbcdc16972e45253491a24fcf45e1
```
Docker 等价写法:
```bash
docker run \
-p 2379:2379 \
-p 2380:2380 \
--volume=/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt \
${REGISTRY}:${ETCD_VERSION} \
/usr/local/bin/etcd --name my-name \
--initial-advertise-peer-urls http://localhost:2380 --listen-peer-urls http://localhost:2380 \
--advertise-client-urls http://localhost:2379 --listen-client-urls http://localhost:2379 \
--discovery https://discovery.etcd.io/86a9ff6c8cb8b4c4544c1a2f88f8b801
```
### 2.6 Kubernetes StatefulSet 部署
> 引自:[Run etcd clusters as a Kubernetes StatefulSet](https://etcd.io/docs/v3.7/op-guide/kubernetes/)
用 StatefulSet 完成 static bootstrap。
#### 应用与验证
把清单存为 `etcd.yaml` 后:
```bash
$ kubectl apply --filename etcd.yaml
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
etcd-0 1/1 Running 0 24m
etcd-1 1/1 Running 0 24m
etcd-2 1/1 Running 0 24m
```
示例镜像内含 etcdctl,可在 pod 内直接调用:
```bash
$ kubectl exec -it etcd-0 -- etcdctl member list -wtable
```
输出中 peer/client 地址为 `http://etcd-0.etcd:2380` / `http://etcd-0.etcd:2379`(etcd-1、etcd-2 同理)。
#### Headless Service
```yaml
apiVersion: v1
kind: Service
metadata:
name: etcd
namespace: default
spec:
type: ClusterIP
clusterIP: None
selector:
app: etcd
publishNotReadyAddresses: true
ports:
- name: etcd-client
port: 2379
- name: etcd-server
port: 2380
- name: etcd-metrics
port: 8080
```
要点:
- `clusterIP: None` 即 headless service;
- `publishNotReadyAddresses: true` 是为了让 etcd pod 在 Ready 之前就能互相解析(注释说明:理想情况应通过 SRV 记录做 peer discovery,但容器内缺少等待记录填充的逻辑,故以此规避);
- 端口命名 `etcd-client`/`etcd-server`/`etcd-metrics` 与 etcd 用 SRV 做 discovery 时的命名方案一致。
#### StatefulSet 关键字段
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: default
name: etcd
spec:
serviceName: etcd
replicas: 3
podManagementPolicy: Parallel
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
app: etcd
template:
metadata:
labels:
app: etcd
annotations:
serviceName: etcd
```
| 字段 | 含义 / 注意 |
|---|---|
| `serviceName: etcd` | 用于以 headless 方式使用该 service |
| `replicas: 3` | **若对已有集群扩容副本数,必须把 `--initial-cluster-state` 改为 `existing`** |
| `podManagementPolicy: Parallel` | 初始化时 etcd pod 必须在 Ready 之前彼此可见 |
| `updateStrategy.type: RollingUpdate` | 为保证可用性,任意时刻至少 51% 的 etcd 节点在线 |
| pod 模板 annotation `serviceName: etcd` | 会被容器配置引用为 DNS 名的一部分,必须与为 etcd 集群创建的 service 名一致(放 annotation 而非 env,是因为每个 etcd 集群只应有 1 个 service) |
| 标签 `app: etcd` | 只适用于「一个 namespace 一个 etcd 集群」;同 namespace 多集群需改成每集群唯一 |
#### 反亲和(podAntiAffinity)
```yaml
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- etcd
topologyKey: "kubernetes.io/hostname"
```
含义:若某节点上已有 `app=etcd` 的 pod,调度器不得再往该节点放一个,防止多个 etcd server 落在同一硬件上。
#### 容器定义与探针
```yaml
containers:
- name: etcd
image: quay.io/coreos/etcd:v3.7.0
imagePullPolicy: IfNotPresent
ports:
- name: etcd-client
containerPort: 2379
- name: etcd-server
containerPort: 2380
- name: etcd-metrics
containerPort: 8080
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 30
livenessProbe:
httpGet:
path: /livez
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
```
探针走 **8080 端口**而非 TLS 客户端端口(自签证书下走 TLS 会失败);`/readyz` 和 `/livez` 自 **v3.5.12** 起提供,此前需要在容器内额外装工具;探针参数仅为示例,生产需自行校验。
#### 环境变量
```yaml
env:
- name: K8S_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: HOSTNAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: SERVICE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.annotations['serviceName']
- name: ETCDCTL_ENDPOINTS
value: $(HOSTNAME).$(SERVICE_NAME):2379
- name: URI_SCHEME
value: "http"
```
这些 env 由 Kubernetes 解析(不是在容器环境里解析)。`ETCDCTL_ENDPOINTS` 让容器内的 etcdctl 默认连自己这个节点,便于排障。TLS 场景下 `URI_SCHEME` 改为 `"https"`,并启用 `ETCDCTL_KEY=/etc/etcd/certs/client/tls.key`、`ETCDCTL_CERT=/etc/etcd/certs/client/tls.crt`、`ETCDCTL_CACERT=/etc/etcd/certs/client/ca.crt`。
#### 启动命令与参数
```yaml
command:
- /usr/local/bin/etcd
args:
- --name=$(HOSTNAME)
- --data-dir=/data
- --wal-dir=/data/wal
- --listen-peer-urls=$(URI_SCHEME)://0.0.0.0:2380
- --listen-client-urls=$(URI_SCHEME)://0.0.0.0:2379
- --advertise-client-urls=$(URI_SCHEME)://$(HOSTNAME).$(SERVICE_NAME):2379
- --initial-cluster-state=new
- --initial-cluster-token=etcd-$(K8S_NAMESPACE)
- --initial-cluster=etcd-0=$(URI_SCHEME)://etcd-0.$(SERVICE_NAME):2380,etcd-1=$(URI_SCHEME)://etcd-1.$(SERVICE_NAME):2380,etcd-2=$(URI_SCHEME)://etcd-2.$(SERVICE_NAME):2380
- --initial-advertise-peer-urls=$(URI_SCHEME)://$(HOSTNAME).$(SERVICE_NAME):2380
- --listen-metrics-urls=http://0.0.0.0:8080
```
**DNS 域名规则**:`$(HOSTNAME).$(SERVICE_NAME)`,展开后为 `etcd-0.etcd`、`etcd-1.etcd`、`etcd-2.etcd`;peer 与 client 均走该域名(2380 / 2379)。
参数注意事项:
| 参数 | 说明 |
|---|---|
| `--initial-cluster-state=new` | 必须为 `new` 才能完成初始 bootstrap;**扩容副本时改为 `existing`**,设置错误时 etcd 会尝试启动但安全失败 |
| `--initial-cluster-token=etcd-$(K8S_NAMESPACE)` | 建议每集群唯一;同 namespace 多集群需额外保证唯一性 |
| `--initial-cluster` | 必须与 `replicas` 数量匹配,单个 peer 的参数化形式为 `etcd-0=$(URI_SCHEME)://etcd-0.$(SERVICE_NAME):2380` |
| `--wal-dir=/data/wal` | 把 WAL 与 data 目录放在一起,示例明确说明**生产不理想,应单独挂卷** |
| `--listen-metrics-urls=http://0.0.0.0:8080` | 用于避免启用 TLS 后探针失败 |
可选 / 注释项:
- 自动压缩:`--auto-compaction-mode=periodic`、`--auto-compaction-retention=10m`
- 客户端 TLS:`--client-cert-auth --trusted-ca-file=$(ETCDCTL_CACERT) --cert-file=$(ETCDCTL_CERT) --key-file=$(ETCDCTL_KEY)`
- peer TLS:`--peer-client-cert-auth --peer-trusted-ca-file=/etc/etcd/certs/server/ca.crt --peer-cert-file=/etc/etcd/certs/server/tls.crt --peer-key-file=/etc/etcd/certs/server/tls.key`
#### 存储
容器挂载:`volumeMounts: - name: etcd-data, mountPath: /data`;TLS 时另挂 `/etc/etcd/certs/client` 与 `/etc/etcd/certs/server`(readOnly),对应 secret `etcd-client-tls` / `etcd-server-tls`。
```yaml
volumeClaimTemplates:
- metadata:
name: etcd-data
spec:
accessModes: ["ReadWriteOnce"]
# storageClassName: ""
resources:
requests:
storage: 1Gi
```
注意:由 `volumeClaimTemplate` 为每个副本创建的 PVC **后续不易扩容**;部分集群需显式设置 storageClassName,本例使用默认 storage class。
#### 证书生成(cert-manager)
```bash
$ helm upgrade --install --create-namespace --namespace cert-manager cert-manager cert-manager --repo https://charts.jetstack.io --set crds.enabled=true
```
ClusterIssuer(`issuer.yaml`):
```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}
```
Certificate(`certificates.yaml`)分别生成 `etcd-server`(secretName `etcd-server-tls`)与 `etcd-client`(secretName `etcd-client-tls`),均 `issuerRef: {name: selfsigned, kind: ClusterIssuer}`、`commonName: etcd`。
**`dnsNames` 必须是证书所有合法主机名的穷举列表**,含:
- `etcd`、`etcd.default`、`etcd.default.svc.cluster.local`
- `etcd-0`、`etcd-0.etcd`、`etcd-0.etcd.default`、`etcd-0.etcd.default.svc`、`etcd-0.etcd.default.svc.cluster.local`
- (etcd-1、etcd-2 同构)
> 💡 补充:这份 `dnsNames` 清单是 K8s 场景下最容易漏配的一项——漏掉 `etcd-0.etcd` 这类短域名会直接导致 peer TLS 握手失败。上线前建议用 `openssl x509 -in tls.crt -noout -text` 核对 SAN 列表。
### 2.7 演示集群快速拉起(tasks/operator 版)
官方 `tasks/operator/how-to-setup-cluster` 页给了一套「3 节点演示集群」的最短路径,与 2.2/2.3 同源但变量命名更简洁,适合本地验证或给新人做第一次上手。
**方式一:静态配置**
先在每台 etcd 节点上定义集群成员变量:
```shell
TOKEN=token-01
CLUSTER_STATE=new
NAME_1=machine-1
NAME_2=machine-2
NAME_3=machine-3
HOST_1=10.240.0.17
HOST_2=10.240.0.18
HOST_3=10.240.0.19
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2380,${NAME_3}=http://${HOST_3}:2380
```
然后在每台机器上执行(只改 `THIS_NAME` / `THIS_IP`,其余完全一致)。以 machine 1 为例:
```shell
# For machine 1
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
etcd --data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
```
关键参数作用(与 2.2 一致,此处补齐逐项说明):
| 参数 | 作用 |
|---|---|
| `--data-dir=data.etcd` | 数据落盘目录,相对路径 |
| `--name ${THIS_NAME}` | 本节点成员名,**必须与 `--initial-cluster` 列表中的名字一致** |
| `--initial-advertise-peer-urls http://${THIS_IP}:2380` | 告知集群内其他成员本节点被访问的 peer 地址(2380 为 peer 端口) |
| `--listen-peer-urls http://${THIS_IP}:2380` | 本节点实际监听的 peer 地址 |
| `--advertise-client-urls http://${THIS_IP}:2379` | 对外公告的客户端地址(2379 为 client 端口) |
| `--listen-client-urls http://${THIS_IP}:2379` | 本节点实际监听的客户端地址 |
| `--initial-cluster ${CLUSTER}` | 初始成员列表,格式 `name=peerURL`,逗号分隔,**所有节点必须给完全相同的值** |
| `--initial-cluster-state ${CLUSTER_STATE}` | 取 `new` 表示新建集群 |
| `--initial-cluster-token ${TOKEN}` | 集群唯一标识 token(`token-01`),同一 token 下的成员才属同一集群,可防止误跨集群加入 |
**方式二:使用公共发现服务(`--discovery`)**
先申请一个 discovery URL:
```shell
curl https://discovery.etcd.io/new?size=3
https://discovery.etcd.io/a81b5818e67a6ea83e9d4daea5ecbc92
```
返回的 URL 即 discovery token(示例中为 `a81b5818e67a6ea83e9d4daea5ecbc92`)。把它赋给 `DISCOVERY`,启动命令把 `--initial-cluster ${CLUSTER}` 替换为 `--discovery ${DISCOVERY}`:
```shell
DISCOVERY=https://discovery.etcd.io/a81b5818e67a6ea83e9d4daea5ecbc92
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
etcd --data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
--discovery ${DISCOVERY} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
```
machine 2、machine 3 同样只改 `THIS_NAME` / `THIS_IP`。**注意:此时仍需保留 `--initial-cluster-state` 与 `--initial-cluster-token`。**
**验证连接**:
```shell
export ETCDCTL_API=3
HOST_1=10.240.0.17
HOST_2=10.240.0.18
HOST_3=10.240.0.19
ENDPOINTS=$HOST_1:2379,$HOST_2:2379,$HOST_3:2379
etcdctl --endpoints=$ENDPOINTS member list
```
**注意事项**:
- 必须先 `export ETCDCTL_API=3` 才能使用 v3 API 的 etcdctl;
- `--initial-cluster` 在所有节点上必须**逐字一致**;
- peer 端口 2380 与 client 端口 2379 不要混淆;
- 发现服务方式下 `--initial-cluster` 与 `--discovery` 二选一,**不可同时用作互斥的初始化来源**。
---
## 3. 配置全参(flag / 环境变量 / 默认值)
etcd 有三种配置途径:**命令行 flag**、**环境变量**、**配置文件**。
### 3.1 配置途径与优先级(有坑)
- 环境变量命名规则:flag 名前加 `ETCD_` 前缀,全大写、snake_case。例如 `--some-flag` → `ETCD_SOME_FLAG`。
- 优先级:
- 命令行 flag **优先于**环境变量;
- **一旦提供配置文件,所有命令行 flag 与环境变量都会被忽略**(互斥,不是叠加)。
- 配置文件为 YAML,key 就是命令行 flag 名,通过 `--config-file` 或环境变量 `ETCD_CONFIG_FILE` 指定路径。
- **持续时间字段的坑**:命令行里可写人类可读串(如 `10m`、`5s`),但在配置文件中**只能填纳秒整数**(Go `time.Duration` 反序列化限制)。
受影响 flag 清单:`--grpc-keepalive-min-time`、`--grpc-keepalive-interval`、`--grpc-keepalive-timeout`、`--backend-batch-interval`、`--corrupt-check-time`、`--compact-hash-check-time`、`--compaction-sleep-interval`、`--watch-progress-notify-interval`、`--warning-apply-duration`、`--warning-unary-request-duration`、`--downgrade-check-time`。
```yaml
# 正确写法(纳秒)
watch-progress-notify-interval: 600000000000
# 错误写法(配置文件里不认人类可读串)
# watch-progress-notify-interval: '10m'
```
> 💡 补充:这条限制是实战中最容易踩的坑之一——从命令行迁移到 `--config-file` 时,把 `'10m'` 原样搬进 YAML 会直接导致启动失败或值被解析为 0。建议迁移时把上述 11 个 flag 单独列一份换算表(1s = 1000000000)。
### 3.2 Member flags(24 条)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--name` | `ETCD_NAME` | 本成员的可读名称 | `'default'` |
| `--data-dir` | `ETCD_DATA_DIR` | 数据目录路径 | `'${name}.etcd'` |
| `--wal-dir` | `ETCD_WAL_DIR` | 专用 wal 目录路径 | `''` |
| `--snapshot-count` | `ETCD_SNAPSHOT_COUNT` | 触发落盘快照的已提交事务数 | `'10000'` |
| `--heartbeat-interval` | `ETCD_HEARTBEAT_INTERVAL` | 心跳间隔,单位毫秒 | `'100'` |
| `--election-timeout` | `ETCD_ELECTION_TIMEOUT` | 选举超时,单位毫秒 | `'1000'` |
| `--initial-election-tick-advance` | `ETCD_INITIAL_ELECTION_TICK_ADVANCE` | 启动时快进初始选举 tick 以加速选举 | `'true'` |
| `--listen-peer-urls` | `ETCD_LISTEN_PEER_URLS` | 监听 peer 流量的 URL 列表 | `'http://localhost:2380'` |
| `--listen-client-urls` | `ETCD_LISTEN_CLIENT_URLS` | 监听客户端 grpc 流量的 URL 列表;若未指定 `--listen-client-http-urls` 则同时承载 http | `'http://localhost:2379'` |
| `--listen-client-http-urls` | `ETCD_LISTEN_CLIENT_HTTP_URLS` | 仅监听 http 客户端流量的 URL 列表;启用后会从 `--listen-client-urls` 移除 http 服务 | `''` |
| `--max-snapshots` | `ETCD_MAX_SNAPSHOTS` | 保留的快照文件最大数(0 为不限) | `'5'` |
| `--max-wals` | `ETCD_MAX_WALS` | 保留的 wal 文件最大数(0 为不限) | `'5'` |
| `--memory-mlock` | `ETCD_MEMORY_MLOCK` | 强制 etcd 页面(尤其 bbolt)常驻 RAM | 无值(布尔开关) |
| `--quota-backend-bytes` | `ETCD_QUOTA_BACKEND_BYTES` | backend 大小超配额时告警(0 表示使用低空间配额) | `'0'` |
| `--backend-bbolt-freelist-type` | `ETCD_BACKEND_BBOLT_FREELIST_TYPE` | boltdb backend 使用的 freelist 类型,支持 `array` 和 `map` | `'map'` |
| `--backend-batch-interval` | `ETCD_BACKEND_BATCH_INTERVAL` | 提交 backend 事务前的最长等待时间 | `''` |
| `--backend-batch-limit` | `ETCD_BACKEND_BATCH_LIMIT` | 提交 backend 事务前允许的最大操作数 | `'0'` |
| `--max-txn-ops` | `ETCD_MAX_TXN_OPS` | 单个事务允许的最大操作数 | `'128'` |
| `--max-request-bytes` | `ETCD_MAX_REQUEST_BYTES` | 服务端接受的最大客户端请求字节数 | `'1572864'` |
| `--grpc-keepalive-min-time` | `ETCD_GRPC_KEEPALIVE_MIN_TIME` | 客户端 ping 服务端的最小间隔 | `'5s'` |
| `--grpc-keepalive-interval` | `ETCD_GRPC_KEEPALIVE_INTERVAL` | 服务端到客户端 ping 的频率(0 关闭) | `'2h'` |
| `--grpc-keepalive-timeout` | `ETCD_GRPC_KEEPALIVE_TIMEOUT` | 关闭无响应连接前的额外等待时长(0 关闭) | `'20s'` |
| `--socket-reuse-port` | `ETCD_SOCKET_REUSE_PORT` | 在 listener 上启用 `SO_REUSEPORT`,允许重绑已占用端口 | `'false'` |
| `--socket-reuse-address` | `ETCD_SOCKET_REUSE_ADDRESS` | 在 listener 上启用 `SO_REUSEADDR`,允许绑定 `TIME_WAIT` 状态的地址 | `'false'` |
> 💡 补充:`--snapshot-count` 在本册素材中**存在两处不同记载**:本表(Configuration options 页)为 `'10000'`;而 batch-05 的 maintenance 文档称「自 v3.2 起默认值从 10,000 改为 100,000」。同时 upgrade_3_6 页把「`--snapshot-count` 从 `100000` 变为 `10000`」列为 v3.6 的默认值变更。三处并列呈现,请以实际版本的 `etcd -h` 输出为准,不要擅自统一。
### 3.3 Clustering flags(16 条)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--initial-advertise-peer-urls` | `ETCD_INITIAL_ADVERTISE_PEER_URLS` | 向集群其余成员通告的本成员 peer URL 列表 | `'http://localhost:2380'` |
| `--initial-cluster` | `ETCD_INITIAL_CLUSTER` | 引导用的初始集群配置 | `'default=http://localhost:2380'` |
| `--initial-cluster-state` | `ETCD_INITIAL_CLUSTER_STATE` | 初始集群状态(`new` 或 `existing`) | `'new'` |
| `--initial-cluster-token` | `ETCD_INITIAL_CLUSTER_TOKEN` | 引导期集群 token,防止多集群间意外交叉交互 | `'etcd-cluster'` |
| `--advertise-client-urls` | `ETCD_ADVERTISE_CLIENT_URLS` | 向外部通告的客户端 URL 列表,客户端库据此连接集群 | `'http://localhost:2379'` |
| `--discovery` | `ETCD_DISCOVERY` | 用于引导集群的 discovery URL | `''` |
| `--discovery-fallback` | `ETCD_DISCOVERY_FALLBACK` | discovery 服务失败时的行为(`exit` 或 `proxy`);`proxy` 仅支持 v2 API | `'proxy'` |
| `--discovery-proxy` | `ETCD_DISCOVERY_PROXY` | 访问 discovery 服务使用的 HTTP 代理 | `''` |
| `--discovery-srv` | `ETCD_DISCOVERY_SRV` | 用于引导集群的 DNS srv 域名 | `''` |
| `--discovery-srv-name` | `ETCD_DISCOVERY_SRV_NAME` | 引导时查询的 dns srv 名称后缀 | `''` |
| `--strict-reconfig-check` | `ETCD_STRICT_RECONFIG_CHECK` | 拒绝会导致丢 quorum 的重配置请求 | `'true'` |
| `--pre-vote` | `ETCD_PRE_VOTE` | 启用 raft Pre-Vote 算法,防止被分区节点重新加入时造成扰动 | `'true'` |
| `--auto-compaction-retention` | `ETCD_AUTO_COMPACTION_RETENTION` | 自动压缩保留时长,0 表示禁用自动压缩 | `'0'` |
| `--auto-compaction-mode` | `ETCD_AUTO_COMPACTION_MODE` | 解释 `--auto-compaction-retention` 的方式:`periodic`(按时间,无单位时默认小时,如 `'5m'`)或 `revision`(按 revision 数) | `'periodic'` |
| `--enable-v2` | `ETCD_ENABLE_V2` | 接受 etcd V2 客户端请求。已废弃,计划在 v3.6 下线 | `'false'` |
| `--v2-deprecation` | `ETCD_V2_DEPRECATION` | v2store 下线阶段,可 opt-in 更高兼容模式 | `'not-yet'` |
`--v2-deprecation` 取值说明:
| 取值 | 含义 | 备注 |
|---|---|---|
| `'not-yet'` | 若 v2store 有实质内容则告警 | v3.5 默认 |
| `'write-only'` | 不允许自定义 v2 state | **v3.6 与 v3.7 默认** |
| `'write-only-skip-check'` | 不支持自定义 v2 state,存在则忽略 | v3.5.32+、v3.6.13+、v3.7.0+ 可用,风险自负 |
| `'write-only-drop-data'` | 自定义 v2 state 会被**删除** | 计划成为 v3.8 默认 |
| `'gone'` | v2store 不再维护 | — |
> 💡 补充:Configuration 表中 `--enable-v2` 与 `--v2-deprecation` 的默认值分别写作 `'false'` 与 `'not-yet'`(v3.5 口径),而 v3.7 中 v2 store 已被完全移除、`--enable-v2` 也不复存在(见第 14 章 v3.6→v3.7 的 Flags removed)。本表按配置页原文保留。
### 3.4 Security flags / TLS(21 条)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--cert-file` | `ETCD_CERT_FILE` | 客户端服务端 TLS 证书文件路径 | `''` |
| `--key-file` | `ETCD_KEY_FILE` | 客户端服务端 TLS 私钥文件路径 | `''` |
| `--client-cert-auth` | `ETCD_CLIENT_CERT_AUTH` | 启用客户端证书认证。官方建议开启以防未认证客户端攻击(如 CVE-2023-44487),尤其是 etcd 作为公共服务运行时 | `'false'` |
| `--client-crl-file` | `ETCD_CLIENT_CRL_FILE` | 客户端证书吊销列表文件路径 | `''` |
| `--client-cert-allowed-hostname` | `ETCD_CLIENT_CERT_ALLOWED_HOSTNAME` | 客户端证书认证允许的 SAN 主机名,逗号分隔 | `''` |
| `--trusted-ca-file` | `ETCD_TRUSTED_CA_FILE` | 客户端服务端 TLS 受信 CA 证书路径。**注意:设置此参数会自动启用客户端证书认证**,无论 `--client-cert-auth` 为何值 | `''` |
| `--auto-tls` | `ETCD_AUTO_TLS` | 客户端 TLS 使用自动生成证书 | `'false'` |
| `--peer-cert-file` | `ETCD_PEER_CERT_FILE` | peer 服务端 TLS 证书文件路径 | `''` |
| `--peer-key-file` | `ETCD_PEER_KEY_FILE` | peer 服务端 TLS 私钥文件路径 | `''` |
| `--peer-client-cert-auth` | `ETCD_PEER_CLIENT_CERT_AUTH` | 启用 peer 客户端证书认证。建议开启以防伪造 peer 攻击(如 CVE-2023-44487) | `'false'` |
| `--peer-trusted-ca-file` | `ETCD_PEER_TRUSTED_CA_FILE` | peer 服务端 TLS 受信 CA 文件路径 | `''` |
| `--peer-cert-allowed-cn` | `ETCD_PEER_CERT_ALLOWED_CN` | peer 间 TLS 认证允许的 CN,逗号分隔 | `''` |
| `--peer-cert-allowed-hostname` | `ETCD_PEER_CERT_ALLOWED_HOSTNAME` | peer 间 TLS 认证允许的 SAN 主机名,逗号分隔 | `''` |
| `--peer-auto-tls` | `ETCD_PEER_AUTO_TLS` | 未提供 `--peer-key-file` 与 `--peer-cert-file` 时,peer TLS 使用自生成证书 | `'false'` |
| `--self-signed-cert-validity` | `ETCD_SELF_SIGNED_CERT_VALIDITY` | 指定 ClientAutoTLS / PeerAutoTLS 时自动生成证书的有效期,单位年 | `'1'` |
| `--peer-crl-file` | `ETCD_PEER_CRL_FILE` | peer 证书吊销列表文件路径 | `''` |
| `--cipher-suites` | `ETCD_CIPHER_SUITES` | client/server 与 peer 间支持的 TLS 加密套件列表,逗号分隔(留空由 Go 自动填充) | `''` |
| `--cors` | `ETCD_CORS` | CORS 允许的源白名单,逗号分隔(空或 `*` 表示允许全部) | `'*'` |
| `--host-whitelist` | `ETCD_HOST_WHITELIST` | 服务端非安全模式下可接受的 HTTP 客户端请求主机名(空或 `*` 表示允许全部) | `'*'` |
| `--tls-min-version` | `ETCD_TLS_MIN_VERSION` | 支持的最低 TLS 版本 | `'TLS1.2'` |
| `--tls-max-version` | `ETCD_TLS_MAX_VERSION` | 支持的最高 TLS 版本(留空由 Go 自动填充) | `''` |
### 3.5 Auth flags(3 条)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--auth-token` | `ETCD_AUTH_TOKEN` | 指定 v3 认证 token 类型及选项(`simple` 或 `jwt`) | `'simple'` |
| `--bcrypt-cost` | `ETCD_BCRYPT_COST` | 认证密码 bcrypt 哈希的 cost/强度,合法取值 4–31 | `10` |
| `--auth-token-ttl` | `ETCD_AUTH_TOKEN_TTL` | auth token TTL,单位秒 | `300` |
### 3.6 Profiling and monitoring flags(3 条)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--enable-pprof` | `ETCD_ENABLE_PPROF` | 通过 HTTP 启用运行时 profiling 数据,地址为 client URL + `/debug/pprof/` | `'false'` |
| `--metrics` | `ETCD_METRICS` | 导出指标的详尽程度,指定 `extensive` 会包含服务端 grpc histogram 指标 | `'basic'` |
| `--listen-metrics-urls` | `ETCD_LISTEN_METRICS_URLS` | metrics 与 health 端点监听的 URL 列表 | `''` |
### 3.7 Logging flags(7 条)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--logger` | `ETCD_LOGGER` | 当前仅支持 `zap` 结构化日志 | `'zap'` |
| `--log-outputs` | `ETCD_LOG_OUTPUTS` | 指定 `stdout` 或 `stderr`(即使在 systemd 下也跳过 journald),或逗号分隔的输出目标列表 | `'default'` |
| `--log-level` | `ETCD_LOG_LEVEL` | 日志级别,仅支持 `debug`/`info`/`warn`/`error`/`panic`/`fatal` | `'info'` |
| `--log-format` | `ETCD_LOG_FORMAT` | 日志格式,仅支持 `json`、`console` | `'json'` |
| `--enable-log-rotation` | `ETCD_ENABLE_LOG_ROTATION` | 对单个 `log-outputs` 文件目标启用日志轮转 | `'false'` |
| `--log-rotation-config-json` | `ETCD_LOG_ROTATION_CONFIG_JSON` | 启用日志轮转时的 JSON 配置:MaxSize(MB)、MaxAge(天,0 不限)、MaxBackups(0 不限)、LocalTime(使用本机时区)、Compress(gzip) | `'{"maxsize": 100, "maxage": 0, "maxbackups": 0, "localtime": false, "compress": false}'` |
| `--warning-unary-request-duration` | `ETCD_WARNING_UNARY_REQUEST_DURATION` | 一元请求超过该时长则记录警告日志 | `'300ms'` |
### 3.8 Distributed tracing flags(5 条)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--enable-distributed-tracing` | `ETCD_ENABLE_DISTRIBUTED_TRACING` | 启用分布式追踪 | `'false'` |
| `--distributed-tracing-address` | `ETCD_DISTRIBUTED_TRACING_ADDRESS` | 分布式追踪 collector 地址 | `'localhost:4317'` |
| `--distributed-tracing-service-name` | `ETCD_DISTRIBUTED_TRACING_SERVICE_NAME` | 分布式追踪服务名,所有 etcd 实例必须一致 | `'etcd'` |
| `--distributed-tracing-instance-id` | `ETCD_DISTRIBUTED_TRACING_INSTANCE_ID` | 分布式追踪实例 ID,每个 etcd 实例必须唯一 | `''` |
| `--distributed-tracing-sampling-rate` | `ETCD_DISTRIBUTED_TRACING_SAMPLING_RATE` | 每百万 span 采集的采样数 | `'0'` |
> v3.7 提示:若干 `--experimental-*` flag 已在 v3.7 中转正或改名,需替换为稳定版本(映射见第 14.4 节)。
### 3.9 v2 Proxy flags(6 条,文档标注将在 v3.6 废弃)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--proxy` | `ETCD_PROXY` | 代理模式设置(`off`、`readonly`、`on`) | `'off'` |
| `--proxy-failure-wait` | `ETCD_PROXY_FAILURE_WAIT` | endpoint 保持失败状态的时长,毫秒 | `5000` |
| `--proxy-refresh-interval` | `ETCD_PROXY_REFRESH_INTERVAL` | endpoint 刷新间隔,毫秒 | `30000` |
| `--proxy-dial-timeout` | `ETCD_PROXY_DIAL_TIMEOUT` | 拨号超时,毫秒 | `1000` |
| `--proxy-write-timeout` | `ETCD_PROXY_WRITE_TIMEOUT` | 写超时,毫秒 | `5000` |
| `--proxy-read-timeout` | `ETCD_PROXY_READ_TIMEOUT` | 读超时,毫秒 | `0` |
> 💡 提示:这一组 flag 在 v3.6 中已被移除(见第 14.4 节 Flags removed 清单),v3.7 同样不存在。保留本表是为了让老配置迁移时有对照。
### 3.10 Features flags(11 条)
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--corrupt-check-time` | `ETCD_CORRUPT_CHECK_TIME` | 集群 corruption 检查的时间间隔 | `'0s'` |
| `--compact-hash-check-time` | `ETCD_COMPACT_HASH_CHECK_TIME` | leader 检查 follower compaction hash 的时间间隔 | `'1m'` |
| `--compaction-batch-limit` | `ETCD_COMPACTION_BATCH_LIMIT` | 每个 compaction 批次删除的最大 revision 数 | `1000` |
| `--peer-skip-client-san-verification` | `ETCD_PEER_SKIP_CLIENT_SAN_VERIFICATION` | peer 连接时跳过客户端证书 SAN 字段校验 | `'false'` |
| `--watch-progress-notify-interval` | `ETCD_WATCH_PROGRESS_NOTIFY_INTERVAL` | 周期性 watch 进度通知的时间间隔 | `'10m'` |
| `--warning-apply-duration` | `ETCD_WARNING_APPLY_DURATION` | 请求处理超过该时长则产生告警 | `'100ms'` |
| `--bootstrap-defrag-threshold-megabytes` | `ETCD_BOOTSTRAP_DEFRAG_THRESHOLD_MEGABYTES` | 启动时若碎片整理能释放至少该阈值(MB)磁盘空间则执行;需非 0 值才生效 | 无默认值标注 |
| `--max-learners` | `ETCD_MAX_LEARNERS` | 集群中允许的 learner 成员最大数 | `'1'` |
| `--compaction-sleep-interval` | `ETCD_COMPACTION_SLEEP_INTERVAL` | 每个 compaction 批次之间的 sleep 间隔 | 无默认值标注 |
| `--downgrade-check-time` | `ETCD_DOWNGRADE_CHECK_TIME` | 两次 downgrade 状态检查之间的时间间隔 | 无默认值标注 |
| `--snapshot-catchup-entries` | `ETCD_SNAPSHOT_CATCHUP_ENTRIES` | follower 在 raft 存储条目压缩后追赶所需的条目数 | 无默认值标注 |
> 素材说明:`--bootstrap-defrag-threshold-megabytes`、`--compaction-sleep-interval`、`--downgrade-check-time`、`--snapshot-catchup-entries` 四个 flag 在官方配置表中**未标注默认值**,本册如实保留「无默认值标注」,不做推测。
### 3.11 Unsafe features flags(2 条)
> 警告:使用不安全特性会破坏共识协议给出的保证。
| 命令行 flag | 环境变量 | 说明 | 默认值 |
|---|---|---|---|
| `--force-new-cluster` | `ETCD_FORCE_NEW_CLUSTER` | 强制创建新的单成员集群 | `'false'` |
| `--unsafe-no-fsync` | `ETCD_UNSAFE_NO_FSYNC` | 关闭 fsync,不安全,会导致数据丢失 | `'false'` |
### 3.12 Feature Gates
#### 命令行写法(9 条)
| 命令行 flag | 说明 | 默认值 |
|---|---|---|
| `--feature-gates=AllAlpha=true\|false` | 启用/禁用所有 alpha 特性 | false |
| `--feature-gates=AllBeta=true\|false` | 启用/禁用所有 beta 特性 | false |
| `--feature-gates=CompactHashCheck=true` | 启用 leader 周期性检查 follower compaction hashes | 取代 `--experimental-compact-hash-check-enabled` |
| `--feature-gates=InitialCorruptCheck=true` | 在提供 client/peer 流量前做 corruption 检查 | 取代 `--experimental-initial-corrupt-check` |
| `--feature-gates=LeaseCheckpoint=true` | 启用 primary lessor 持久化 lease remainingTTL,防止长生命周期 lease 无限自动续期 | 取代 `--experimental-enable-lease-checkpoint` |
| `--feature-gates=LeaseCheckpointPersist=true` | 持久化 remainingTTL 防止长生命周期 lease 无限自动续期。v3.6 中始终启用;用于从启用了该特性的 v3.5 集群平滑升级 | 取代 `--experimental-enable-lease-checkpoint-persist` |
| `--feature-gates=SetMemberLocalAddr=true` | 允许设置成员的本地地址 | — |
| `--feature-gates=StopGRPCServiceOnDefrag=true` | 碎片整理期间让 gRPC 服务停止响应客户端请求 | 取代 `--experimental-stop-grpc-service-on-defrag` |
| `--feature-gates=TxnModeWriteWithSharedBuffer=true` | 写事务在只读检查操作中使用共享 buffer | 取代 `--experimental-txn-mode-write-with-shared-buffer` |
命令行与 YAML 配置文件两种写法:
```shell
# 命令行
--feature-gates=...,StopGRPCServiceOnDefrag=true
```
```shell
# YAML 配置文件
feature-gates: ...,StopGRPCServiceOnDefrag=true
```
#### 特性门清单(7 条)
> 引自:[Feature Gates](https://etcd.io/docs/v3.7/feature-gates/)
| Feature | Default | Stage | Details |
|---|---|---|---|
| CompactHashCheck | false | Alpha | 在提供任何 client/peer 流量前检查数据损坏 |
| InitialCorruptCheck | false | Alpha | 让 leader 周期性检查 follower 的 compaction hashes |
| LeaseCheckpoint | false | Alpha | 让 leader 定期向其他成员发送 checkpoint,防止 leader 变更时 remaining TTL 被重置 |
| LeaseCheckpointPersist | false | Alpha | 持久化 remainingTTL,防止长生命周期 lease 无限自动续期 |
| SetMemberLocalAddr | false | Alpha | 使用 `initial-advertise-peer-urls` 中第一个指定且非 loopback 的本地地址作为与 peer 通信的本地地址 |
| StopGRPCServiceOnDefrag | false | Alpha | 碎片整理期间让 etcd gRPC 服务停止响应客户端请求 |
| TxnModeWriteWithSharedBuffer | true | Beta | 让写事务在其只读检查操作中使用共享 buffer |
另可通过 `AllAlpha=true|false`(默认 false)与 `AllBeta=true|false`(默认 false)批量开关所有 alpha / beta 特性。用 `etcd -h` 可查看完整列表。
> 💡 补充:注意官方两处对 `CompactHashCheck` 与 `InitialCorruptCheck` 的 Details 描述在「Configuration options」页与「Feature Gates」页**顺序相反**(配置页把 InitialCorruptCheck 描述为「在提供流量前做检查」,Feature Gates 页把 CompactHashCheck 描述为「在提供流量前检查数据损坏」)。本册按各自页面原文保留,未做统一。
#### 特性阶段说明
| 阶段 | 含义 |
|---|---|
| **Alpha** | 默认关闭;可能有 bug,开启可能暴露问题;支持可能随时无通知地移除;API 可能在后续版本中以不兼容方式变更;因 bug 风险高且缺乏长期支持,建议仅在短生命周期测试集群中使用 |
| **Beta** | 默认开启;经过充分测试,开启被认为是安全的;特性整体支持不会取消,但细节可能变更;因广泛采用可能暴露新的隐蔽 bug,建议仅用于非业务关键场景(官方鼓励试用 Beta 特性并反馈) |
| **GA(stable)** | 特性始终启用,无法关闭;对应的 feature gate 不再需要;稳定特性会在后续多个版本中持续存在 |
| **Deprecated** | feature gate 不再使用;特性已升为 GA 或被移除 |
#### `embed.EtcdServer` 结构体变更(3.6)
3.6 中 `embed.Config` 新增字段 `ServerFeatureGate`(类型 `featuregate.FeatureGate`),用于取代下列已废弃的 experimental 字段(标注将于 v3.7 下线):
| 废弃字段(json 名) | 替代 Feature Gate |
|---|---|
| `ExperimentalCompactHashCheckEnabled`(`experimental-compact-hash-check-enabled`) | CompactHashCheck |
| `ExperimentalInitialCorruptCheck`(`experimental-initial-corrupt-check`) | InitialCorruptCheck |
| `ExperimentalTxnModeWriteWithSharedBuffer`(`experimental-txn-mode-write-with-shared-buffer`) | TxnModeWriteWithSharedBuffer |
| `ExperimentalStopGRPCServiceOnDefrag`(`experimental-stop-grpc-service-on-defrag`) | StopGRPCServiceOnDefrag |
| `ExperimentalEnableLeaseCheckpoint`(`experimental-enable-lease-checkpoint`) | LeaseCheckpoint |
| `ExperimentalEnableLeaseCheckpointPersist`(`experimental-enable-lease-checkpoint-persist`) | LeaseCheckpointPersist |
### 3.13 配置参数计数核对
| 分组 | 条数 |
|---|---|
| Member flags | 24 |
| Clustering flags | 16 |
| Security flags(TLS) | 21 |
| Auth flags | 3 |
| Profiling and monitoring flags | 3 |
| Logging flags | 7 |
| Distributed tracing flags | 5 |
| v2 Proxy flags | 6 |
| Features flags | 11 |
| Feature Gates(命令行写法) | 9 |
| Unsafe features flags | 2 |
| **命令行 flag 合计** | **107** |
| Feature Gates 特性清单(Alpha/Beta 表) | 7 |
> 💡 补充:107 条命令行 flag 中包含 9 条 `--feature-gates=...` 写法(其中 AllAlpha / AllBeta 为聚合开关,与 7 条具体特性不重复计数)。Feature Gates 特性清单 7 条与之对应。
---
## 4. 硬件、支持平台与系统调优
> 引自:[Hardware recommendations](https://etcd.io/docs/v3.7/op-guide/hardware/)、[Supported platforms](https://etcd.io/docs/v3.7/op-guide/supported-platform/)、[Tuning](https://etcd.io/docs/v3.7/tuning/)
开发与测试环境下 etcd 资源需求很低(笔记本或廉价云机即可)。以下为**生产环境**建议,属起点而非硬性规则,上线前务必用模拟负载测试。
### 4.1 硬件建议
#### CPU
- 大多数部署不需要很多 CPU:典型集群 **2–4 核**即可平稳运行。
- 重负载部署(服务数千客户端、每秒数万请求)通常受 CPU 限制(etcd 可从内存直接响应),一般需要 **8–16 个专用核**。
#### 内存
- etcd 内存占用较小,但性能依赖充足内存:服务端会积极缓存 key-value 数据,其余内存主要用于跟踪 watcher。
- 通常 **8GB** 足够;重负载(数千 watcher、数百万 key)按需分配 **16GB 到 64GB**。
#### 磁盘(最关键因素)
- 快速磁盘是 etcd 性能与稳定性的**最关键因素**。
- 慢磁盘会拉高请求延迟并可能损害集群稳定性:多数派成员必须将每个请求落盘,etcd 还会增量 checkpoint 状态以截断日志;写入过慢会导致心跳超时并触发选举。
- 可用 [fio](https://github.com/axboe/fio) 等基准工具判断磁盘是否够快。
- **顺序 IOPS**:通常需要 **50 sequential IOPS**(如 7200 RPM 盘);重负载集群建议 **500 sequential IOPS**(如典型本地 SSD 或高性能虚拟化块设备)。
- 注意多数云厂商公布的是**并发 IOPS**,可能是顺序 IOPS 的 10 倍;实测顺序 IOPS 请用 [diskbench](https://github.com/ongardie/diskbenchmark) 或 [fio](https://github.com/axboe/fio)。
- **磁盘带宽**:需求不高,但带宽越大故障成员追赶越快。通常 **10MB/s** 可在 15 秒内恢复 100MB 数据;大型集群建议 **100MB/s 或更高**,以便 15 秒内恢复 1GB 数据。
- 尽量使用 SSD 作为 etcd 存储后端:写入延迟更低、抖动更小。若必须用机械盘则选最快规格(**15,000 RPM**)。RAID 0 对机械盘和 SSD 都是有效的提速手段。至少有 3 个成员时,RAID 的镜像/奇偶校验变体没有必要——etcd 的一致性复制已提供高可用。
#### 网络
- 多成员部署受益于快速可靠的网络;不可靠、频繁分区的网络会损害可用性。低延迟保证成员通信迅速,高带宽缩短故障成员恢复时间。
- 常见部署 **1GbE** 足够;大型 etcd 集群用 **10GbE** 可降低平均恢复时间(MTTR)。
- 尽量把成员部署在同一数据中心以避免延迟开销、降低分区概率;若需跨数据中心容灾,选择离现有机房更近的机房,并参阅 tuning 文档了解跨数据中心部署。
#### 示例硬件配置(假设机器完全专用于 etcd)
> 混跑其他应用会造成资源争抢、导致集群不稳定。
**Small cluster**(少于 100 客户端、少于 200 请求/秒、数据不超过 100MB;示例负载:50 节点 Kubernetes 集群)
| Provider | Type | vCPUs | Memory (GB) | Max concurrent IOPS | Disk bandwidth (MB/s) |
|---|---|---|---|---|---|
| AWS | m4.large | 2 | 8 | 3600 | 56.25 |
| GCE | n1-standard-2 + 50GB PD SSD | 2 | 7.5 | 1500 | 25 |
**Medium cluster**(少于 500 客户端、少于 1,000 请求/秒、数据不超过 500MB;示例负载:250 节点 Kubernetes 集群)
| Provider | Type | vCPUs | Memory (GB) | Max concurrent IOPS | Disk bandwidth (MB/s) |
|---|---|---|---|---|---|
| AWS | m4.xlarge | 4 | 16 | 6000 | 93.75 |
| GCE | n1-standard-4 + 150GB PD SSD | 4 | 15 | 4500 | 75 |
**Large cluster**(少于 1,500 客户端、少于 10,000 请求/秒、数据不超过 1GB;示例负载:1,000 节点 Kubernetes 集群)
| Provider | Type | vCPUs | Memory (GB) | Max concurrent IOPS | Disk bandwidth (MB/s) |
|---|---|---|---|---|---|
| AWS | m4.2xlarge | 8 | 32 | 8000 | 125 |
| GCE | n1-standard-8 + 250GB PD SSD | 8 | 30 | 7500 | 125 |
**xLarge cluster**(超过 1,500 客户端、超过 10,000 请求/秒、数据超过 1GB;示例负载:3,000 节点 Kubernetes 集群)
| Provider | Type | vCPUs | Memory (GB) | Max concurrent IOPS | Disk bandwidth (MB/s) |
|---|---|---|---|---|---|
| AWS | m4.4xlarge | 16 | 64 | 16,000 | 250 |
| GCE | n1-standard-16 + 500GB PD SSD | 16 | 60 | 15,000 | 250 |
> 💡 补充:这张表把「客户端数 / QPS / 数据量」与「机型」直接挂钩,是做容量规划时最实用的一张表。Kubernetes 场景可直接按节点数对号入座(50 / 250 / 1000 / 3000 节点)。
### 4.2 支持的平台与架构
#### 支持层级定义
| 层级 | 定义 |
|---|---|
| **Tier 1** | 由 etcd maintainers 完全支持;保证通过所有测试,包括 functional 与 robustness 测试 |
| **Tier 2** | 保证通过 integration 与 end-to-end 测试,但不保证 functional 或 robustness 测试 |
| **Tier 3** | 仅保证能构建,可能只做轻度测试(或完全不测),应视为**不稳定** |
#### 当前支持矩阵
| Architecture | Operating system | Support tier | Maintainers |
|---|---|---|---|
| AMD64 | Linux | 1 | etcd maintainers |
| ARM64 | Linux | 1 | etcd maintainers |
| AMD64 | Darwin | 3 | — |
| ARM64 | Darwin | 3 | — |
| AMD64 | Windows | 3 | — |
| ppc64le | Linux | 3 | — |
| s390x | Linux | 3 | — |
未列出的平台不受支持。
#### 支持新平台的要求
成为新平台的「官方」maintainer,除承诺支持该平台外,还需搭建满足对应层级要求的 CI:
| etcd continuous integration | Tier 1 | Tier 2 | Tier 3 |
|---|---|---|---|
| Build passes | ✓ | ✓ | ✓ |
| Unit tests pass | ✓ | ✓ | — |
| Integration and end-to-end tests pass | ✓ | ✓ | — |
| Robustness tests pass | ✓ | — | — |
ARM64 的 tier-2 CI 搭建示例见 etcd PR #12928。
#### 不受支持平台的处理
为避免误在不受支持平台运行 etcd,etcd 会打印警告信息并**立即退出**,**除非**将环境变量 `ETCD_UNSUPPORTED_ARCH` 设为目标架构。
32 位系统警告:etcd 在 32 位系统上存在**已知问题**(源于 Go runtime bug),详见 Go issue #599 与 atomic package bug note。
> 💡 补充:生产环境请只用 **AMD64 + Linux** 或 **ARM64 + Linux**。Darwin / Windows 只应作为本地开发调试用途(Tier 3)。
### 4.3 系统调优(tuning)
默认设置适用于平均网络延迟较低的本地网络。跨多数据中心或高延迟网络时,心跳间隔与选举超时可能需要调优。注意**网络不是延迟的唯一来源**:leader 与 follower 上的慢磁盘也会影响每个请求/响应;这些超时代表**从发出请求到对端成功响应的总时长**。
#### 4.3.1 时间参数:Heartbeat Interval 与 Election Timeout
分布式共识协议依赖两个时间参数:
| 参数 | 含义 | 默认值 |
|---|---|---|
| **Heartbeat Interval**(`--heartbeat-interval`) | leader 通知 follower 自己仍是 leader 的频率。最佳实践应设为成员间往返时间(RTT)附近 | `100ms` |
| **Election Timeout**(`--election-timeout`) | follower 多久收不到心跳就尝试自己成为 leader | `1000ms` |
调优取权衡:
- 心跳间隔建议约为成员间平均 RTT 的最大值,通常为 **0.5–1.5 倍 RTT**。过低会让 etcd 发送不必要的消息,增加 CPU 与网络资源消耗;过高会导致选举超时变长,进而延长 leader 故障的检测时间。测量 RTT 最简单的方式是用 PING 工具。
- 选举超时应基于心跳间隔与成员间平均 RTT 设定,**必须至少为 RTT 的 10 倍**以应对网络波动。例如成员间 RTT 为 10ms,则选举超时至少应为 100ms。
- 选举超时**上限为 50000ms(50s)**,仅应在部署全球分布式 etcd 集群时使用。参考:美国大陆内 RTT 约 **130ms**,美日之间约 **350–400ms**;网络性能不均或有规律丢包延迟时可能需多次重试,因此 **5s** 是全球 RTT 的安全上限。由于选举超时应比广播时间大一个数量级,全球分布式集群在 ~5s 广播时间下,**50 秒**成为合理的最大值。
- 心跳间隔与选举超时的值在**同一集群的所有成员上必须一致**,不同取值可能破坏集群稳定性。
覆盖默认值(单位为毫秒):
```sh
# Command line arguments:
$ etcd --heartbeat-interval=100 --election-timeout=500
# Environment variables:
$ ETCD_HEARTBEAT_INTERVAL=100 ETCD_ELECTION_TIMEOUT=500 etcd
```
> 💡 补充:推导逻辑一句话版——先测出成员间 RTT(ping 或 `etcd_network_peer_round_trip_time_seconds`,见第 12 章),`heartbeat-interval ≈ 1 × RTT`,`election-timeout ≥ 10 × RTT` 且 `≥ 5 × heartbeat-interval`(后者来自 FAQ 对 `failed to send out heartbeat on time` 的处置建议,见第 17 章)。同集群所有 member 必须一致。
#### 4.3.2 快照(Snapshots)与 `--snapshot-count`
etcd 把所有 key 变更追加到日志文件,该日志会无限增长、构成完整的线性变更历史。轻负载集群适合完整历史,重负载集群则会拖着巨大的日志。为避免日志过大,etcd 会周期性做快照:保存当前系统状态并删除旧日志,从而压缩日志。
**快照调优**:V2 backend 下创建快照代价较高,因此仅在 etcd 发生一定数量变更后才创建,默认每 **10,000** 次变更后做一次快照。若 etcd 的内存与磁盘占用过高,可下调快照阈值:
```sh
# Command line arguments:
$ etcd --snapshot-count=5000
# Environment variables:
$ ETCD_SNAPSHOT_COUNT=5000 etcd
```
> 💡 补充:`--snapshot-count` 越小 → 快照越频繁 → WAL 更短、恢复更快,但落盘开销更大;越大 → 快照更少、写放大更小,但 WAL 更长、重启恢复更慢。调整前最好结合 `etcd_debugging_snap_save_total_duration_seconds`(第 12 章)观察单次 save 耗时。
#### 4.3.3 磁盘优先级
etcd 集群对磁盘延迟极为敏感。由于 etcd 必须把 proposal 持久化到日志,其他进程的磁盘活动可能造成很长的 `fsync` 延迟,导致 etcd 错过心跳、请求超时与临时性 leader 丢失。给 etcd 更高的磁盘优先级有时可以让它与这些进程稳定共存。
Linux 下用 `ionice` 配置磁盘优先级:
```sh
# best effort, highest priority
$ sudo ionice -c2 -n0 -p `pgrep etcd`
```
#### 4.3.4 网络优先级(peer 流量高于 client 流量)
若 etcd leader 服务大量并发客户端请求,可能因网络拥塞延迟处理 follower 的 peer 请求,表现为 follower 节点上的发送缓冲区错误:
```text
dropped MsgProp to 247ae21ff9436b2d since streamMsg's sending buffer is full
dropped MsgAppResp to 247ae21ff9436b2d since streamMsg's sending buffer is full
```
解决办法是把 etcd 的 peer 流量优先级调到客户端流量之上。Linux 下用流量控制机制(示例网卡 `eth0`):
```bash
tc qdisc add dev eth0 root handle 1: prio bands 3
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 2379 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 2379 0xffff flowid 1:1
```
peer 端口 **2380** 用 prio 1,客户端端口 **2379** 用 prio 2。取消 `tc` 配置:
```bash
tc qdisc del dev eth0 root
```
#### 4.3.5 CPU governor
etcd 对延迟非常敏感,Linux 系统可把 CPU governor 设为 performance 或 conservative 模式进一步优化性能:
```bash
echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
```
#### 4.3.6 调优项速查表
| 调优对象 | 手段 | 关键值 / 命令 |
|---|---|---|
| 心跳间隔 | `--heartbeat-interval` | 默认 `100ms`;取 0.5–1.5 倍 RTT |
| 选举超时 | `--election-timeout` | 默认 `1000ms`;≥ 10 倍 RTT,上限 `50000ms` |
| 快照频率 | `--snapshot-count` | 默认 `10000`;内存/磁盘占用高时下调(如 `5000`) |
| 磁盘 IO 优先级 | `ionice` | `sudo ionice -c2 -n0 -p \`pgrep etcd\`` |
| 网络优先级 | `tc qdisc` | peer 2380 → prio 1;client 2379 → prio 2 |
| CPU | scaling_governor | `performance` 或 `conservative` |
| 磁盘介质 | SSD / 15,000 RPM / RAID 0 | 顺序 IOPS ≥ 50(重负载 ≥ 500) |
> 💡 补充:关于「并发 fd / ulimit」的具体建议值,官方 tuning 页**未提供**;本册不在该条目上编造数值。相关可观测项可用 `process_open_fds` 与 `process_max_fds` / `os_fd_used` 与 `os_fd_limit`(第 12 章)自行建立水位告警。
---
## 5. 成员与运行时管理
本章回答运维最常问的四件事:**集群现在什么状态**、**怎么加/减机器**、**成员变更做砸了怎么办**、**bootstrap 阶段成员是怎么互相找到的**。
主要依据:
> 引自:[Runtime reconfiguration](https://etcd.io/docs/v3.7/op-guide/runtime-configuration/)
> 引自:[How to check Cluster status](https://etcd.io/docs/v3.7/tasks/operator/how-to-check-cluster-status/)
> 引自:[How to Add and Remove Members](https://etcd.io/docs/v3.7/tasks/operator/how-to-deal-with-membership/)
> 引自:[How to conduct leader election in etcd cluster](https://etcd.io/docs/v3.7/tasks/operator/how-to-conduct-elections/)
> 引自:[Discovery service protocol](https://etcd.io/docs/v3.7/dev-guide/discovery_protocol/)(v2 版)
> 引自:[Discovery service protocol](https://etcd.io/docs/v3.7/dev-internal/discovery_protocol/)(v3 版)
### 5.1 查看集群状态:endpoint status / health / hashkv
`etcdctl` 提供三个 `endpoint` 子命令,分别看**整体状态**、**健康度**、**KV 历史哈希**。三者都支持 `--endpoints` 与 `--cluster` 两种取地址方式(二选一)。
#### 5.1.1 命令形式
```bash
etcdctl endpoint status (--endpoints=$ENDPOINTS|--cluster)
etcdctl endpoint health (--endpoints=$ENDPOINTS|--cluster)
etcdctl endpoint hashkv (--endpoints=$ENDPOINTS|--cluster) [rev=$REV]
```
各命令的专有选项:
| 子命令 | 选项 | 默认值 | 含义 |
|---|---|---|---|
| `endpoint status` | `--cluster` | `false` | use all endpoints from the cluster member list,从集群成员列表自动取全部 endpoint |
| `endpoint health` | `--cluster` | `false` | 同上 |
| `endpoint hashkv` | `--cluster` | `false` | 同上 |
| `endpoint hashkv` | `--rev` | `0` | maximum revision to hash(default: latest revision) |
从父命令继承的全局选项:
```bash
--endpoints="127.0.0.1:2379": gRPC endpoints
-w, --write-out="simple": set the output format (fields, json, protobuf, simple, table)
```
> 💡 补充:`--endpoints` 默认是 `127.0.0.1:2379`,不指定就只查本地节点——这是巡检脚本里最常见的「我以为查了整个集群」事故来源。巡检务必显式写 `--cluster` 或完整 `--endpoints`。
#### 5.1.2 `endpoint status` 输出字段全解
```shell
etcdctl --write-out=table --endpoints=$ENDPOINTS endpoint status
+------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| 10.240.0.17:2379 | 4917a7ab173fabe7 | 3.5.0 | 45 kB | true | false | 4 | 16726 | 16726 | |
| 10.240.0.18:2379 | 59796ba9cd1bcd72 | 3.5.0 | 45 kB | false | false | 4 | 16726 | 16726 | |
| 10.240.0.19:2379 | 94df724b66343e6c | 3.5.0 | 45 kB | false | false | 4 | 16726 | 16726 | |
+------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------|
```
| 字段 | 含义 | 巡检要点 |
|---|---|---|
| `ENDPOINT` | 被查询的节点 client 地址(示例 `10.240.0.17:2379` 等三个节点) | 确认覆盖了全部节点 |
| `ID` | 该成员的十六进制 member ID(如 `4917a7ab173fabe7`),与 `member list` 中的 ID 对应 | 与 `member list` 交叉核对,发现「幽灵成员」 |
| `VERSION` | 节点 etcd 版本(示例为 `3.5.0`) | 滚动升级中会出现版本不一致,属正常 |
| `DB SIZE` | 该节点后端数据库大小(示例均为 `45 kB`) | 三节点应基本一致;偏差过大说明有节点落后或存在碎片 |
| `IS LEADER` | 是否为当前 raft leader(示例中仅第一个节点为 `true`) | 正常集群应**恰好一个**为 `true` |
| `IS LEARNER` | 是否为 learner(非投票成员),示例全部为 `false` | learner 不参与投票,长期 `true` 说明 promote 忘了做 |
| `RAFT TERM` | 当前 raft 任期号(示例均为 `4`) | 同集群各节点任期应一致;任期跳变说明发生过重新选举 |
| `RAFT INDEX` | raft 日志索引(示例均为 `16726`) | — |
| `RAFT APPLIED INDEX` | 已应用到状态机的日志索引(示例均为 `16726`) | 与 `RAFT INDEX` 的差值反映应用滞后,正常应相等或极接近 |
| `ERRORS` | 节点级错误信息,示例为空即无异常 | 非空即为告警信号 |
> ⚠️ 源文说明:上表 `VERSION` 示例为 `3.5.0`(官方文档示例尚未更新到 v3.7),字段含义与列顺序按 v3.7 文档原文保留。
#### 5.1.3 `endpoint health`
```shell
etcdctl --endpoints=$ENDPOINTS endpoint health
10.240.0.17:2379 is healthy: successfully committed proposal: took = 3.345431ms
10.240.0.19:2379 is healthy: successfully committed proposal: took = 3.767967ms
10.240.0.18:2379 is healthy: successfully committed proposal: took = 4.025451ms
```
要点:
- 健康检查会向集群**提交一个 proposal**,成功提交即判定 healthy,并打印耗时 `took`。
- 输出顺序**不保证**与 `--endpoints` 输入顺序一致(示例中 `10.240.0.19` 排在 `10.240.0.18` 之前)。
- `took` 反映一次共识往返的时延,可用于判断网络/磁盘抖动。
#### 5.1.4 `endpoint hashkv`
```shell
etcdctl --cluster endpoint hashkv --write-out=table
+------------------+------------+---------------+
| ENDPOINT | HASH | HASH REVISION |
+------------------+------------+---------------+
| 10.240.0.17:2379 | 3892279174 | 3 |
| 10.240.0.18:2379 | 3892279174 | 3 |
| 10.240.0.19:2379 | 3892279174 | 3 |
+------------------+------------+---------------+
```
| 字段 | 含义 |
|---|---|
| `ENDPOINT` | 被查询的节点 client 地址 |
| `HASH` | 该 endpoint 在指定 revision 内的 KV 历史哈希(示例 `3892279174`) |
| `HASH REVISION` | 哈希对应的 revision(示例 `3`) |
各节点 `HASH` 与 `HASH REVISION` 一致说明数据一致;不一致则意味着节点间数据出现分叉或落后。
#### 5.1.5 三个命令怎么选
| 场景 | 用哪个 | 理由 |
|---|---|---|
| 日常巡检看版本/DB 大小/leader | `endpoint status -w table` | 信息最全、字段可读 |
| 探活、判断能否写入、看共识时延 | `endpoint health` | 真的提交了一次 proposal,最接近真实可写性 |
| 校验各节点数据是否一致 | `endpoint hashkv --cluster` | 直接比较 KV 历史哈希 |
| 只想看某一历史 revision 的一致性 | `endpoint hashkv --rev=N` | `--rev=0` 默认哈希到最新 revision |
### 5.2 运行时重配置:设计约束
> 引自:[Runtime reconfiguration](https://etcd.io/docs/v3.7/op-guide/runtime-configuration/)
- etcd 支持 **incremental runtime reconfiguration(增量运行时重配置)**,可在运行时更新集群成员关系。
- 重配置请求只有在**多数派成员正常工作**时才能被处理。
- 生产环境**强烈建议**集群规模大于 2。从 2 成员集群中移除成员是**不安全**的:2 成员集群的多数派同样是 2,移除过程中若发生故障,集群可能无法继续推进,需要从多数派故障中重新启动(restart from majority failure)。
#### 5.2.1 四类典型场景
| 场景 | 官方做法要点 |
|---|---|
| **轮换/升级多台机器**(计划性维护:硬件升级、网络中断) | **一次只改动一个成员**。移除 leader 是安全的,但选举期间会有短暂不可用。若集群持有超过 **50MB** 的 v2 数据,建议先做 member migration(迁移该成员的数据目录) |
| **改变集群规模** | 扩容提升失败容忍度与读性能(客户端可从任意成员读,成员越多整体串行读吞吐越高);缩容提升写性能(写入需复制到多数派才算提交,多数派变小则提交更快),代价是韧性下降 |
| **替换故障机器** | 硬件故障、数据目录损坏等致命情况要尽快替换;已故障但未移除的成员会**损害 quorum** 并降低对额外故障的容忍度。步骤 = 先 remove 再 add。若集群超过 50MB 且故障成员数据目录仍可访问,建议迁移它 |
| **从多数派故障重启集群** | 多数派丢失或所有节点 IP 都变了时需人工介入:用旧数据创建新集群 → 强制单个成员充当 leader → 用运行时配置逐个 add 新成员 |
| **从少数派故障恢复** | 等同于「替换故障机器」 |
#### 5.2.2 顺序约束(硬性)
- 任何变更前必须有简单多数(quorum)可用,与任何写入 etcd 的要求一致。
- **所有集群变更必须串行执行**:
| 目标 | 需要的操作序列 |
|---|---|
| 更新单个成员 peerURLs | 一次 `update` 操作 |
| 替换一个健康成员 | 先 `remove` 旧成员,再 `add` 新成员 |
| 3 扩到 5 | 两次 `add` |
| 5 缩到 3 | 两次 `remove` |
- 除 `etcdctl` 外,也可用 v2 HTTP members API 或 v3 gRPC members API 变更成员。
### 5.3 更新成员:client URL 与 peer URL 的区别(易踩坑)
| 更新对象 | 做法 | 是否需要显式 member update |
|---|---|---|
| **advertise client URLs** | 直接以更新后的客户端 URL **重启该成员**即可,用 flag `--advertise-client-urls` 或环境变量 `ETCD_ADVERTISE_CLIENT_URLS`。重启后成员会自行发布新 URL | **不需要** |
| **advertise peer URLs** | 必须**先通过 member 命令显式更新,再重启成员** | **需要** |
原因:peer URL 变更属于**集群级**配置改动,会影响集群健康;而客户端 URL 配错**不会**影响集群健康。
先列出成员拿到 ID:
```sh
$ etcdctl member list
6e3bd23ae5f1eae0: name=node2 peerURLs=http://localhost:23802 clientURLs=http://127.0.0.1:23792
924e2e83e93f2560: name=node3 peerURLs=http://localhost:23803 clientURLs=http://127.0.0.1:23793
a8266ecf031671f3: name=node1 peerURLs=http://localhost:23801 clientURLs=http://127.0.0.1:23791
```
更新 ID 为 `a8266ecf031671f3` 的成员,把 peerURLs 改为 `http://10.0.1.10:2380`:
```sh
$ etcdctl member update a8266ecf031671f3 --peer-urls=http://10.0.1.10:2380
Updated member with ID a8266ecf031671f3 in cluster
```
### 5.4 移除成员
```sh
$ etcdctl member remove a8266ecf031671f3
Removed member a8266ecf031671f3 from cluster
```
目标成员会自行停止并在日志中打印:
```text
etcd: this member has been permanently removed from the cluster. Exiting.
```
移除 leader 是安全的,但新 leader 选举期间集群处于 inactive 状态,时长通常为**选举超时加上投票过程**的时间。
### 5.5 添加新成员:完整命令序列(两步)
**第一步**:通过 HTTP members API、gRPC members API 或 `etcdctl member add` 把新成员加入集群。
**第二步**:用新的集群配置启动新成员(已有成员 + 新成员的完整列表)。
#### 5.5.1 op-guide 版示例
```sh
$ etcdctl member add infra3 --peer-urls=http://10.0.1.13:2380
added member 9bf1b35fc7761a23 to cluster
ETCD_NAME="infra3"
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra3=http://10.0.1.13:2380"
ETCD_INITIAL_CLUSTER_STATE=existing
```
`etcdctl` 会告知集群新成员并打印启动所需的环境变量,然后据此启动:
```sh
$ export ETCD_NAME="infra3"
$ export ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra3=http://10.0.1.13:2380"
$ export ETCD_INITIAL_CLUSTER_STATE=existing
$ etcd --listen-client-urls http://10.0.1.13:2379 --advertise-client-urls http://10.0.1.13:2379 --listen-peer-urls http://10.0.1.13:2380 --initial-advertise-peer-urls http://10.0.1.13:2380 --data-dir %data_dir%
```
新成员会立即开始追赶集群其余成员。
- 最佳实践:**一次只配置一个成员**,确认启动正确后再加下一个。
- 向 **1 节点集群**添加成员时,在新成员启动前集群无法推进(需要 2 个成员构成多数派达成一致)。该状态只存在于 `etcdctl member add` 通知集群之后、新成员成功建立连接之前这段时间。
#### 5.5.2 tasks/operator 版完整序列(含替换成员)
**第 0 步:静态方式拉起 3 节点集群**。`TOKEN=my-etcd-token-1`、`CLUSTER_STATE=new`,成员 `etcd-node-1`/`etcd-node-2`/`etcd-node-3`,主机 `10.240.0.13`/`10.240.0.14`/`10.240.0.15`:
```shell
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2380,${NAME_3}=http://${HOST_3}:2380
```
每个节点启动命令(以 node 1 为例,node 2/node 3 同构,只改 `THIS_NAME`/`THIS_IP`):
```shell
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
etcd --data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:2380 \
--listen-peer-urls http://${THIS_IP}:2380 \
--advertise-client-urls http://${THIS_IP}:2379 \
--listen-client-urls http://${THIS_IP}:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} \
--initial-cluster-token ${TOKEN}
```
**第 1 步:取 member ID → 移除 → 加入新成员**
```shell
# get member ID
export ETCDCTL_API=3
HOST_1=10.240.0.13
HOST_2=10.240.0.14
HOST_3=10.240.0.15
etcdctl --endpoints=${HOST_1}:2379,${HOST_2}:2379,${HOST_3}:2379 member list
# remove the member
MEMBER_ID=278c654c9a6dfd3b
etcdctl --endpoints=${HOST_1}:2379,${HOST_2}:2379,${HOST_3}:2379 \
member remove ${MEMBER_ID}
# add a new member (node 4)
export ETCDCTL_API=3
NAME_1=etcd-node-1
NAME_2=etcd-node-2
NAME_4=etcd-node-4
HOST_1=10.240.0.13
HOST_2=10.240.0.14
HOST_4=10.240.0.16 # new member
etcdctl --endpoints=${HOST_1}:2379,${HOST_2}:2379 \
member add ${NAME_4} \
--peer-urls=http://${HOST_4}:2380
```
要点:先用 `member list` 取得待删成员的 member ID(示例 `278c654c9a6dfd3b`),再用 `member remove ${MEMBER_ID}` 移除;`member add ${NAME_4} --peer-urls=http://${HOST_4}:2380` 中 `--peer-urls` 指定新成员的 peer 地址(2380)。注意 `member add` **只写两个存活节点** `HOST_1:2379,${HOST_2}:2379`,不需要把还没启动的新成员列进去。
**第 2 步:以 `existing` 状态启动新成员**
```shell
# [WARNING] If the new member starts from the same disk space,
# make sure to remove the data directory of the old member
#
# restart with 'existing' flag
TOKEN=my-etcd-token-1
CLUSTER_STATE=existing
NAME_1=etcd-node-1
NAME_2=etcd-node-2
NAME_4=etcd-node-4
HOST_1=10.240.0.13
HOST_2=10.240.0.14
HOST_4=10.240.0.16 # new member
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2380,${NAME_4}=http://${HOST_4}:2380
THIS_NAME=${NAME_4}
THIS_IP=${HOST_4}
etcd --data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:2380 \
--listen-peer-urls http://${THIS_IP}:2380 \
--advertise-client-urls http://${THIS_IP}:2379 \
--listen-client-urls http://${THIS_IP}:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} \
--initial-cluster-token ${TOKEN}
```
**关键注意事项(5 条)**:
1. 新成员必须用 `--initial-cluster-state existing`(而非 `new`),表示加入一个已存在的集群。
2. 源文 `[WARNING]`:新成员若复用同一块磁盘空间,**务必先删除旧成员的 data directory**(即 `--data-dir=data.etcd` 对应目录),否则旧数据会导致加入失败或集群损坏。
3. `--initial-cluster` 列表要更新为新的成员组合(node-1、node-2、node-4),且 `--initial-cluster-token` 必须与现有集群一致(`my-etcd-token-1`),否则会被视为另一个集群。
4. 每个 member 操作前都要 `export ETCDCTL_API=3`。
5. 本页未出现 `--learner` 与 `--member-id` 两个 flag——`--learner` 只在 `op-guide/runtime-configuration.md` 与 `learning/design-learner.md` 中出现,不属于本页内容。
### 5.6 Learner:先追赶、再投票(自 v3.4 起)
推荐新成员**先以 learner 加入、追赶完成后再提升**,这样更安全并减少集群停机时间,共三步:
1. 通过 gRPC members API 或 `etcdctl member add --learner` 添加 learner。
2. 用新的集群配置启动新成员(与前述完全相同)。
3. 通过 gRPC members API 或 `etcdctl member promote` 将 learner 提升为投票成员。
etcd server 会校验 promote 请求以确保操作安全性:**只有 raft log 追上 leader 之后** learner 才能被提升;未追上时 promote 会失败,应等待后重试。
v3.4 中 etcd server 限制**集群最多只能有 1 个 learner**,主要考虑是限制 leader 向 learner 传播数据带来的额外负载。
```sh
$ etcdctl member add infra3 --peer-urls=http://10.0.1.13:2380 --learner
Member 9bf1b35fc7761a23 added to cluster a7ef944b95711739
ETCD_NAME="infra3"
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra3=http://10.0.1.13:2380"
ETCD_INITIAL_CLUSTER_STATE=existing
```
```sh
$ etcdctl member promote 9bf1b35fc7761a23
Member 9e29bbaa45d74461 promoted in cluster a7ef944b95711739
```
> 💡 补充:learner 的价值在于「加入集群的瞬间不会立刻改变 quorum 计算」。直接 add 一个投票成员,若它启动慢或 peer URL 配错,新成员会被计入 quorum 却无法真正参与,等于凭空把可用性门槛抬高——这正是 5.7 节 `--strict-reconfig-check` 试图兜底的问题,也是 5.6 节推荐用 learner 的原因。
### 5.7 `--strict-reconfig-check`:重配置的护栏
| 项目 | 内容 |
|---|---|
| 背景 | 新成员若配置不正确(例如 peer URLs 写错),即便该成员对现有成员**不可达**,仍会被计入 quorum,从而**导致 quorum 丢失**;连接问题或运维问题同样可能引发 |
| 开启后的行为 | 若重配置后**已启动成员数**将少于重配置集群的 quorum,etcd 会**拒绝**该重配置请求 |
| 默认值 | **默认启用**(`true`) |
> 💡 补充:除非你非常清楚自己在做什么(例如从多数派故障中人工重建集群),不要关掉它。关掉之后的「成功 add」很可能是一个已经失去 quorum 的集群。
### 5.8 成员变更失败场景处置(现象 → 原因 → 处置)
#### 5.8.1 添加成员
| 现象(报错) | 原因 | 处置 |
|---|---|---|
| ```text
etcdserver: assign ids error: the member count is unequal
exit 1
``` | 新主机**未包含在枚举节点列表中**;若是新集群,该节点必须加入 initial cluster 成员列表 | 把新成员补进 `--initial-cluster` 再启动 |
对应命令:
```sh
$ etcd --name infra3 \
--initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
--initial-cluster-state existing
etcdserver: assign ids error: the member count is unequal
exit 1
```
| 现象(报错) | 原因 | 处置 |
|---|---|---|
| ```text
etcdserver: assign ids error: unmatched member while checking PeerURLs
exit 1
``` | 使用了与加入集群时(`10.0.1.13:2380`)**不同**的地址(`10.0.1.14:2380`) | 启动时的 peer URL 必须与 `member add` 时登记的完全一致;不一致就先 `member remove` 再重新 `member add` |
对应命令:
```sh
$ etcd --name infra4 \
--initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380,infra4=http://10.0.1.14:2380 \
--initial-cluster-state existing
etcdserver: assign ids error: unmatched member while checking PeerURLs
exit 1
```
| 现象(报错) | 原因 | 处置 |
|---|---|---|
| ```text
etcd: this member has been permanently removed from the cluster. Exiting.
exit 1
``` | etcd 使用了**已被移除成员的数据目录**启动,一旦连上集群中任何活跃成员就会自动退出 | 清空/删除该 `--data-dir` 后重新走 add 流程 |
对应命令:
```sh
$ etcd
etcd: this member has been permanently removed from the cluster. Exiting.
exit 1
```
#### 5.8.2 添加 learner
| 现象(报错) | 原因 | 处置 |
|---|---|---|
| `Error: etcdserver: too many learner members in cluster` | 集群已有 1 个 learner 时不能再添加(v3.4 限制最多 1 个 learner) | 先把已有 learner promote 或 remove,再添加新的 |
```sh
$ etcdctl member add infra4 --peer-urls=http://10.0.1.14:2380 --learner
Error: etcdserver: too many learner members in cluster
```
#### 5.8.3 promote learner
| 现象(报错) | 原因 | 处置 |
|---|---|---|
| `Error: etcdserver: can only promote a learner member which is in sync with leader` | learner **未与 leader 同步**(raft log 还没追上) | 等待追赶完成后重试 promote |
| `Error: etcdserver: can only promote a learner member` | 被提升的对象**不是 learner**(已是投票成员) | 无需 promote |
| `Error: etcdserver: member not found` | 被提升的成员在集群中**不存在**(ID 写错) | 用 `member list` 重新确认 ID |
```sh
$ etcdctl member promote 9bf1b35fc7761a23
Error: etcdserver: can only promote a learner member which is in sync with leader
```
```sh
$ etcdctl member promote 9bf1b35fc7761a23
Error: etcdserver: can only promote a learner member
```
```sh
$ etcdctl member promote 12345abcde
Error: etcdserver: member not found
```
### 5.9 选举:用 etcdctl 做一次 leader election
> 引自:[How to conduct leader election in etcd cluster](https://etcd.io/docs/v3.7/tasks/operator/how-to-conduct-elections/)
文档明确说明:该命令保证同一时刻**只有一个**客户端能成为 leader。
```text
etcdctl --endpoints=$ENDPOINTS elect [proposal]
```
```shell
etcdctl --endpoints=$ENDPOINTS elect election-name p1
```
| 参数 | 说明 |
|---|---|
| `--endpoints` : `$ENDPOINTS` | etcd 集群各成员的地址(Address of each etcd cluster members) |
| `election-name`(string) | 选举的标识符。**所有参与同一领导竞争的参与者必须使用相同的选举名** |
| `leader-name`(string,即位置参数 `[proposal]` 的文档描述名) | 新 leader 的提议值(Proposal value of the new leader) |
```shell
./etcdctl elect my-election proposal1
my-election/694d99fafea88404
proposal1
another election:
./etcdctl elect new-election proposal1
new-election/694d99fafea8840f
proposal1
```
输出解读:第一行 `my-election/694d99fafea88404` 是本次选举生成的 key(格式 `/<生成的编号>`),第二行回显赢得选举的 proposal 值 `proposal1`。换一个选举名(`new-election`)会得到另一个独立 key(`new-election/694d99fafea8840f`),说明不同选举名之间是互相隔离的竞选域。
注意事项:
- 示例中的 `./etcdctl elect` 未带 `--endpoints`,走的是默认 `127.0.0.1:2379`。
- `elect` 是**阻塞式**的——客户端成为 leader 后命令保持持有,退出即释放领导权。
- ⚠️ 源文不一致:文档参数列表里写的是 `leader-name`,而命令示例里位置参数写作 `p1` / `proposal1`。本册按原文并列保留,不擅自统一。
### 5.10 两种 discovery 服务协议(dev-guide 版 vs dev-internal 版)
etcd 官方文档里有**两页同名**的 `Discovery service protocol`,标题与 front matter(`title`、`weight: 1500`、`description`)完全相同,但**内容不是同一份文档**:一页讲 v2 discovery,另一页讲 v3 discovery。
#### 5.10.1 共同约束(两版一致)
1. Discovery service protocol **仅用于集群 bootstrap 阶段**,不能用于运行时重配置(runtime reconfiguration)或集群监控(cluster monitoring)。
2. 该协议用一个**新的 discovery token** 引导出一个**唯一的** etcd 集群;一个 discovery token 只能代表一个 etcd 集群。
3. 一旦基于某个 token 的 discovery 协议开始,**即使中途失败,也绝不能再用该 token 去引导另一个 etcd 集群**。
协议思路也相同:用一个内部 etcd 集群协调新集群的 bootstrap——先由所有新成员与 discovery service 交互、共同生成期望的成员列表,然后每个新成员用这个列表启动自己的 server,效果等同于 `-initial-cluster` flag。两版都采用 `UUID=$(uuidgen)` 生成 token,size 通常取 **3、5 或 7**(文档链到 optimal cluster size);注册数大于期望大小 N 时取**最先注册的 N 个**作为成员列表,自己在列表中则成功并拉取 peers,不在列表中则以「cluster has been full」失败;成员可能在注册自己之前就先检查集群状态,因此集群已满时可快速失败。
#### 5.10.2 差异对比(9 个维度)
| 维度 | dev-guide 版(v2) | dev-internal 版(v3) |
|---|---|---|
| 线上 URL | https://etcd.io/docs/v3.7/dev-guide/discovery_protocol/ | https://etcd.io/docs/v3.7/dev-internal/discovery_protocol/ |
| 源文件 | `dev-guide/discovery_protocol.md` | `dev-internal/discovery_protocol.md` |
| 协议版本 | v2 discovery(使用 `/v2/keys/` HTTP 端点) | 明确声明 **only for v3 discovery** |
| 发现依据 | 共享的 discovery **URL** | 共享的 discovery **token + endpoint 列表** |
| 示例工具 | `curl` | `etcdctl` |
| key 前缀 | `_etcd/registry`(无前导斜杠),完整 URL `http://example.com/v2/keys/_etcd/registry` | `/_etcd/registry`(有前导斜杠) |
| 注册路径 | `/_etcd/registry/${UUID}/${member_id}`,带 `?prevExist=false` | `/_etcd/registry/${UUID}/members/${member_id}`,无 prevExist 参数 |
| 状态检查路径 | `GET /v2/keys/_etcd/registry/${UUID}`(整棵子树) | `get /_etcd/registry/${UUID}/members`(members 子前缀) |
| 等待机制 | v2 long polling:`?wait=true&waitIndex=${current_etcd_index}`,链到 v2.3 API 文档 | v3 watch:`watch /_etcd/registry/${UUID}/members --prefix` |
| 启动 flag | `-discovery`(单 flag,指向 URL) | `--discovery-token` + `--discovery-endpoints`(两个 flag),并新增 TLS/认证/超时三组 flag |
| 公共服务章节 | 有完整「Public discovery service」章节 | **无**公共服务章节;改为「Note: only for v3 discovery」并回链 v2 文档 |
| 示例主机 | `http://example.com`(v2 keys API,默认 2379 隐含在 `/v2/keys`) | `http://example.com:2379`(显式端口) |
#### 5.10.3 v2 版(dev-guide)完整步骤
**步骤 1:创建 discovery token**
```bash
UUID=$(uuidgen)
```
**步骤 2:指定期望的集群大小**
```bash
curl -X PUT http://example.com/v2/keys/_etcd/registry/${UUID}/_config/size -d value=${cluster_size}
```
size 用于让 discovery service 判断何时已找齐所有初始成员,通常取 3、5 或 7。
**步骤 3:拉起 etcd 进程**
拿到 discovery URL 后,把它作为 `-discovery` flag 传给 etcd;给定 `-discovery` 后每个 etcd 进程会在内部自动执行后续几步。
**步骤 4:注册自己**
```bash
curl -X PUT http://example.com/v2/keys/_etcd/registry/${UUID}/${member_id}?prevExist=false -d value="${member_name}=${member_peer_url_1}&${member_name}=${member_peer_url_2}"
```
以 member ID 为 key 写入 discovery URL;`?prevExist=false` 保证该 key 此前不存在(避免重复注册覆盖);value 形如 `member_name=peer_url_1&member_name=peer_url_2`。
**步骤 5:检查状态**
```bash
curl -X GET http://example.com/v2/keys/_etcd/registry/${UUID}/_config/size
curl -X GET http://example.com/v2/keys/_etcd/registry/${UUID}
```
已注册成员不足则继续等待;若已注册成员数**大于**期望大小 N,取**最先注册的 N 个**作为成员列表:若自己在列表中,discovery 成功并通过该列表拉取所有 peers;若不在列表中,discovery 以「cluster has been full」失败结束。
**步骤 6:等待所有成员**
```bash
curl -X GET http://example.com/v2/keys/_etcd/registry/${UUID}?wait=true&waitIndex=${current_etcd_index}
```
基于 etcd v2 API 的 long polling(`wait=true` + `waitIndex`),持续等待直到找齐所有成员。
**Public discovery service**(CoreOS Inc. 托管于 https://discovery.etcd.io/):
- **Mask key prefix**:公共服务会把 `https://discovery.etcd.io/${UUID}` 重定向到后端 etcd 集群中位于 `/v2/keys/_etcd/registry` 的 key,从而遮蔽注册 key 前缀,得到简短可读的 discovery URL。
- **Get new token**:
```text
GET /new
Sent query:
size=${cluster_size}
Possible status codes:
200 OK
400 Bad Request
200 Body:
generated discovery url
```
生成流程遵循前文「Creating a New Discovery Token」到「Specifying the Expected Cluster Size」两步。
- **Check discovery status**:`GET /${UUID}`,请求该 UUID 的值即可查看该 token 的注册状态,包括已注册的机器。
- 开源仓库:https://github.com/coreos/discovery.etcd.io,可用于自建 discovery service。
适用范围说明:本文示例对应自托管的 discovery 集群;公共服务 `discovery.etcd.io` 工作方式相同,只是额外做了包装(隐藏丑陋 URL、自动生成 UUID、对过量请求提供一定保护),其核心仍是使用 etcd 集群作为数据存储。
#### 5.10.4 v3 版(dev-internal)完整步骤
**步骤 1:创建 token**
```bash
UUID=$(uuidgen)
```
**步骤 2:指定期望集群大小**
```bash
etcdctl --endpoints=http://example.com:2379 put /_etcd/registry/${UUID}/_config/size ${cluster_size}
```
**步骤 3:拉起 etcd 进程(v3 特有的一批 flag)**
把 token `${UUID}` 设到 `--discovery-token`,把承载 discovery service 的 etcd 集群 endpoints 设到 `--discovery-endpoints`,即可启用 v3 discovery 引导集群。给定这两个 flag 后每个 etcd 进程会执行后续内部步骤。
配套 flag 分三组:
| 场景 | flag |
|---|---|
| discovery 服务启用客户端证书认证(用法与 etcdctl 访问集群完全一致) | `--discovery-insecure-transport`、`--discovery-insecure-skip-tls-verify`、`--discovery-cert`、`--discovery-key`、`--discovery-cacert` |
| 启用基于角色的认证 | `--discovery-user`、`--discovery-password` |
| 调整默认时间与超时值(用法同样与 etcdctl 一致) | `--discovery-dial-timeout`、`--discovery-request-timeout`、`--discovery-keepalive-time`、`--discovery-keepalive-timeout` |
**步骤 4:注册自己**
```bash
etcdctl --endpoints=http://example.com:2379 put /_etcd/registry/${UUID}/members/${member_id} ${member_name}=${member_peer_url_1}&${member_name}=${member_peer_url_2}
```
注意 v3 的 key 路径多了一层 `/members/`。
**步骤 5:检查状态**
```bash
etcdctl --endpoints=http://example.com:2379 get /_etcd/registry/${UUID}/_config/size
etcdctl --endpoints=http://example.com:2379 get /_etcd/registry/${UUID}/members
```
**步骤 6:等待所有成员**
v3 用 **watch** 而非 v2 的 long polling,持续 watch key 前缀 `/_etcd/registry/${UUID}/members` 直到找齐所有成员:
```bash
etcdctl --endpoints=http://example.com:2379 watch /_etcd/registry/${UUID}/members --prefix
```
> ⚠️ 源文说明:v3 版文档明确写着「Note that this document is only for v3 discovery」,并链回 v2 discovery 文档(源文链接写为 `/docs/v3.5/dev-internal/discovery_protocol`)查看细节——该链接指向 v3.5,疑为官方文档自身笔误,本册按原文保留。
### 5.11 成员管理速查卡
| 想做的事 | 命令 |
|---|---|
| 看成员列表与 ID | `etcdctl member list` |
| 看各节点状态(table) | `etcdctl -w table --cluster endpoint status` |
| 探活 | `etcdctl --cluster endpoint health` |
| 校验数据一致性 | `etcdctl -w table --cluster endpoint hashkv` |
| 改 peer URL | `etcdctl member update --peer-urls=` **然后**重启该成员 |
| 改 client URL | 直接以新 `--advertise-client-urls` 重启,**无需** member update |
| 移除成员 | `etcdctl member remove ` |
| 加投票成员 | `etcdctl member add --peer-urls=` → 以 `existing` 启动 |
| 加 learner | `etcdctl member add --peer-urls= --learner` → 以 `existing` 启动 |
| 提升 learner | `etcdctl member promote `(需已与 leader 同步) |
| 客户端侧选举 | `etcdctl --endpoints=$ENDPOINTS elect [proposal]`(阻塞式) |
> 💡 补充:`tasks/operator/_index.md` 这一页**仅含 front matter,无任何正文**(共 5 行,字段为 `title: Operator Tasks`、`weight: 95`、`description: Operational guides for deploying, configuring, and maintaining an etcd cluster.`),是纯导航用的 list page,因此没有可提炼的内容;真正内容在本章引用的各子页中。
---
## 6. 日常维护:压缩、碎片整理、配额、快照
> 引自:[Maintenance](https://etcd.io/docs/v3.7/op-guide/maintenance/)
etcd 集群需要周期性维护以保持可靠;通常可自动化、无需停机或显著性能下降即可完成。所有维护动作都在管理 keyspace 消耗的**存储资源**:
- keyspace 大小失控 → 由 **storage space quotas**(空间配额)兜底;成员空间不足会触发**集群级 alarm**,使系统进入 limited-operation maintenance mode。
- 为避免写不进去,必须做**历史压缩**(history compaction)。
- 空间通过**碎片整理**(defragmentation)归还。
- 周期性**快照备份**可恢复运维误操作导致的逻辑数据丢失或损坏。
### 6.1 Raft log retention 与 `--snapshot-count`
- `etcd --snapshot-count` 配置在压缩前**在内存中保留的已应用 Raft entry 数量**。达到阈值后,server 先把快照数据落盘,再截断旧 entry。慢 follower 请求已被压缩索引之前的日志时,leader 会发送快照强制 follower 覆盖自身状态。
- 更高的 `--snapshot-count`:内存中保留更多 Raft entry,导致**周期性的更高内存占用**;同时 leader 保留最新 entry 更久,慢 follower 有更多时间追赶。是「更高内存占用」与「慢 follower 更好可用性」之间的权衡。
- **自 v3.2 起,默认值从 10,000 改为 100,000。**
- 性能上 `--snapshot-count` 大于 **100,000** 可能影响写吞吐:内存中对象数量多会拖慢 Go GC mark phase `runtime.scanobject`,内存回收不频繁也会让分配变慢。压缩过频繁影响集群可用性与写吞吐,过稀疏则给 Go GC 太大压力。
> 💡 补充:`--snapshot-count` 的取值存在三处不一致记载:配置表 `'10000'`、maintenance 页「自 v3.2 起改为 100,000」、upgrade_3_6 页「v3.6 默认值由 100000 改为 10000」。本册并列保留,请以实际版本 `etcd -h` 为准(见 3.2 节注)。
### 6.2 历史压缩(History Compaction)
etcd 保存 keyspace 的完整历史,必须定期压缩,否则性能退化并最终耗尽存储空间。压缩会丢弃给定 keyspace revision 之前被取代的 key 的全部信息,其占用空间变为可复用。
可**自动压缩**(`--auto-compaction-mode` + `--auto-compaction-retention`),也可用 `etcdctl` **手动压缩**(细粒度控制):
```bash
# compact up to revision 3
$ etcdctl compact 3
```
压缩点之前的 revision 将不可访问:
```bash
$ etcdctl get --rev=2 somekey
Error: rpc error: code = 11 desc = etcdserver: mvcc: required revision has been compacted
```
### 6.3 Auto Compaction(两种模式)
| 模式 | 含义 | 启用方式 | 检查频率 |
|---|---|---|---|
| `periodic`(默认) | 保留基于时间窗口的 keyspace 历史 | `--auto-compaction-mode=periodic --auto-compaction-retention=<时长>` | 保留期 > 1h:每小时一次;≤ 1h:按保留期间隔 |
| `revision` | 保留固定数量的 revision | `--auto-compaction-mode=revision --auto-compaction-retention=<条数>` | 每 5 分钟检查一次 |
#### Periodic compaction
```bash
# keep one hour of history
$ etcd --auto-compaction-retention=1h
```
- retention 值指定保留多少历史;一条记录在创建后约该时长内不会被压缩,确保慢 watcher 能在窗口内追上。
- 保留期 **大于 1 小时**:etcd 每小时压缩一次,同时维持完整保留窗口;保留期 **≤ 1 小时**:按保留期间隔压缩。
- 例:`--auto-compaction-retention=10h`,先等 10 小时做第一次压缩,之后每小时压缩一次:
```text
0hr (rev = 1)
1hr (rev = 10)
...
8hr (rev = 80)
9hr (rev = 90)
10hr (rev = 100, Compact(1))
11hr (rev = 110, Compact(10))
...
```
**推荐值**:
| 场景 | 建议值 |
|---|---|
| 同一 key 频繁更新 | `1h` 或 `30m` |
| 更新不频繁 | `24h`、`48h`、`72h` |
| 通用默认 | `10h` |
#### Revision compaction
```bash
# keep 1000 revisions
$ etcd --auto-compaction-mode=revision --auto-compaction-retention=1000
```
etcd 每 **5 分钟**检查一次,压缩到 `"latest revision" - 1000`;例如最新 revision 为 30000 时压缩到 29000。
### 6.4 碎片整理(Defragmentation)
#### 为什么需要 defrag
- 压缩后后端数据库会产生**内部碎片**:这些空间对后端可用,但仍占用存储;压缩旧 revision 会在后端数据库中留下 gap。碎片空间对 etcd 可用,但**对宿主机文件系统不可用**——即删除应用数据并不会回收磁盘空间。
- 碎片整理把该空间释放回文件系统,**按成员逐个执行**,以避免集群范围的延迟尖刺。
#### 在线整理(etcdctl defrag)
```bash
$ etcdctl defrag
Finished defragmenting etcd member[127.0.0.1:2379]
```
- **警告**:对**在线成员**做碎片整理会在重建状态期间**阻塞系统读写**。
- **警告**:碎片整理请求**不会在集群内复制**,只作用于本地节点。需在 `--endpoints` 中指定所有成员,或用 `--cluster` 自动发现所有集群成员。
```bash
$ etcdctl defrag --cluster
Finished defragmenting etcd member[http://127.0.0.1:2379]
Finished defragmenting etcd member[http://127.0.0.1:22379]
Finished defragmenting etcd member[http://127.0.0.1:32379]
```
#### 离线整理(etcdutl defrag)
在 etcd **未运行**时直接整理数据目录:
```bash
etcdutl defrag --data-dir
```
#### 何时需要 defrag(判据)
> 💡 补充:官方未给出「何时必须 defrag」的阈值,以下判据基于官方给出的两个监控指标推导。
| 判据 | 说明 |
|---|---|
| `etcd_mvcc_db_total_size_in_bytes`(物理)远大于 `etcd_mvcc_db_total_size_in_use_in_bytes`(逻辑) | 差值即碎片空间,差值占比高时应 defrag |
| 两者都接近 `--quota-backend-bytes` | 应先做历史压缩,再做 defrag |
| 刚做过大批量删除 / 大版本压缩 | 压缩后必然产生碎片 |
### 6.5 空间配额(Space quota)
- 没有配额时,keyspace 过大可能导致性能变差或耗尽存储,引发不可预测的集群行为。
- 任一成员的 keyspace 后端数据库超过配额 → etcd 触发**集群级 alarm**,集群进入 maintenance mode,**只接受 key 读和删除**。只有「释放足够空间 + 碎片整理 + 清除配额 alarm」后才能恢复正常。
- 默认已有适合大多数应用的保守配额,也可按字节在命令行配置:
```bash
# set a very small 16 MiB quota
$ etcd --quota-backend-bytes=$((16*1024*1024))
```
#### 触发配额的现象与确认
```bash
# fill keyspace
$ while [ 1 ]; do dd if=/dev/urandom bs=1024 count=1024 | ETCDCTL_API=3 etcdctl put key || break; done
...
Error: rpc error: code = 8 desc = etcdserver: mvcc: database space exceeded
# confirm quota space is exceeded
$ ETCDCTL_API=3 etcdctl --write-out=table endpoint status
+----------------+------------------+-----------+---------+-----------+-----------+------------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
+----------------+------------------+-----------+---------+-----------+-----------+------------+
| 127.0.0.1:2379 | bf9071f4639c75cc | 2.3.0+git | 18 MB | true | 2 | 3332 |
+----------------+------------------+-----------+---------+-----------+-----------+------------+
# confirm alarm is raised
$ ETCDCTL_API=3 etcdctl alarm list
memberID:13803658152347727308 alarm:NOSPACE
```
#### 完整恢复序列(NOSPACE 处置五步)
```bash
# 1. get current revision
$ rev=$(ETCDCTL_API=3 etcdctl --endpoints=:2379 endpoint status --write-out="json" | egrep -o '"revision":[0-9]*' | egrep -o '[0-9].*')
# 2. compact away all old revisions
$ ETCDCTL_API=3 etcdctl compact $rev
compacted revision 1516
# 3. defragment away excessive space
$ ETCDCTL_API=3 etcdctl defrag
Finished defragmenting etcd member[127.0.0.1:2379]
# 4. disarm alarm
$ ETCDCTL_API=3 etcdctl alarm disarm
memberID:13803658152347727308 alarm:NOSPACE
# 5. test puts are allowed again
$ ETCDCTL_API=3 etcdctl put newkey 123
OK
```
#### 配额相关监控指标
| 指标 | 含义 |
|---|---|
| `etcd_mvcc_db_total_size_in_use_in_bytes` | 历史压缩后的**实际**数据库用量 |
| `etcd_debugging_mvcc_db_total_size_in_bytes` | 包含待碎片整理空闲空间的数据库大小。该值仅在前者接近它时才增长,因此两者都接近配额时就需要做历史压缩以避免触发配额 |
| `etcd_mvcc_db_total_size_in_bytes` | 自 v3.4 起由 `etcd_debugging_mvcc_db_total_size_in_bytes` 更名而来 |
| `etcd_server_quota_backend_bytes` | 当前 backend 存储配额大小(字节) |
**警告**:Put/Txn/LeaseGrant 请求可能返回 `ErrGRPCNoSpace` 错误,但**写入在后端仍然成功**——因为 etcd 在 API 层和内部 Apply 层都检查配额,而 Apply 层只会触发 `NOSPACE` alarm,不会阻断事务继续。
> 💡 补充:正因为「报错但写入成功了」这一行为,处理 NOSPACE 时**不要**假设失败请求可以重放——应先确认数据实际状态再决定是否重试,否则可能产生重复写入。
### 6.6 快照备份(Snapshot backup)
定期快照是 etcd keyspace 的持久化备份,可把集群恢复到某个已知良好状态的时间点。
```bash
$ etcdctl snapshot save backup.db
$ etcdutl --write-out=table snapshot status backup.db
+----------+----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| fe01cf57 | 10 | 7 | 2.1 MB |
+----------+----------+------------+------------+
```
文件命名:官方示例使用 `backup.db`(也有 `snapshot.db` 的写法)。文件名本身无强制约定,建议带上**集群名 + 日期 + revision**,例如 `--r.db`,便于回溯。
> 💡 补充:这一条命名建议是本册补充,官方页面只给了 `backup.db` / `snapshot.db` 两种示例名。
### 6.7 维护动作速查
| 动作 | 命令 | 是否阻塞 | 是否复制到集群 |
|---|---|---|---|
| 手动压缩 | `etcdctl compact ` | 否 | 是(集群级 revision) |
| 在线碎片整理 | `etcdctl defrag` / `etcdctl defrag --cluster` | **是(阻塞读写)** | **否**(只作用于本地节点) |
| 离线碎片整理 | `etcdutl defrag --data-dir ` | etcd 未运行 | 否 |
| 查配额告警 | `etcdctl alarm list` | 否 | — |
| 清除配额告警 | `etcdctl alarm disarm` | 否 | 是 |
| 快照备份 | `etcdctl snapshot save ` | 否 | — |
| 查快照状态 | `etcdutl snapshot status -w table` | 否 | — |
---
## 7. 灾难恢复
> 引自:[Disaster recovery](https://etcd.io/docs/v3.7/op-guide/recovery/)
### 7.1 前提与设计
- etcd 集群能自动从临时故障(如机器重启)中恢复,**N 成员集群可容忍最多 (N-1)/2 个永久性故障**。
- 成员永久故障(硬件故障或磁盘损坏)会失去集群访问权;永久丢失超过 **(N-1)/2** 个成员则灾难性失败,**不可逆转地丢失 quorum**,集群无法达成共识、不能再接受更新。
- 恢复手段:etcd v3 提供 snapshot and restore 设施,可在**不丢失 v3 key 数据**的前提下重建集群。恢复 v2 key 需参考 v2 admin guide(v3.7 中 v2 已移除)。
### 7.2 快照 keyspace(snapshot save)
```bash
$ ETCDCTL_API=3 etcdctl --endpoints $ENDPOINT snapshot save snapshot.db
```
快照来源有两种:
| 来源 | 说明 | 风险 |
|---|---|---|
| 对活跃成员执行 `etcdctl snapshot save` | 推荐方式 | 无 |
| **从 etcd 数据目录复制 `member/snap/db` 文件** | 离线拷贝 | **可能丢失尚未写入、但已包含在 wal(write-ahead-log)目录中的数据** |
### 7.3 查看快照状态(snapshot status)
```bash
$ etcdutl snapshot status snapshot.db -w table
+---------+----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+---------+----------+------------+------------+
| 7ef846e | 485261 | 11642 | 94 MB |
+---------+----------+------------+------------+
```
### 7.4 Revision Difference(revision 回退问题)
- 恢复集群时,现有客户端可能感知到 revision 回退数百或数千——因为快照只包含截止到快照时刻的数据沿革,而当前状态可能已经超前。
- 运行 Kubernetes 时这尤其成问题:controller 和 operator 使用 `informers`(本地缓存,通过 watch 接收更新),恢复到旧 revision 可能**无法正确刷新缓存**,导致 controller 行为不可预测且不一致。
- **只要存在 watch API 的已知消费者、etcd 数据的本地缓存副本,或使用 Kubernetes,强烈建议使用 revision bumps 方式恢复。**
### 7.5 从快照恢复(snapshot restore)
- 恢复集群只需要**一个快照 "db" 文件**;`etcdutl snapshot restore` 会创建新的 etcd 数据目录,**所有成员都应使用同一个快照**恢复。
- 恢复会**覆写部分快照元数据**(具体是 member ID 和 cluster ID),成员失去原有身份,防止其无意中加入已有集群。因此从快照启动集群必须启动一个**新的逻辑集群**。
```bash
$ etcdutl snapshot restore snapshot.db --data-dir output-dir
```
### 7.6 完整性检查(Integrity Checks)
- 恢复时可选择性校验快照完整性。
- 用 `etcdctl snapshot save` 生成的快照带有 **integrity hash**,`etcdutl snapshot restore` 会校验它。
- 从数据目录复制出来的快照**没有** integrity hash,只能加 `--skip-hash-check` 才能恢复。
### 7.7 带 revision bump 的恢复
- `--bump-revision` 接受一个 64 位整数,表示在当前快照 revision 上增加多少 revision,确保恢复后 revision **永不回退**。
- 由于每次写入 revision +1,假设 etcd 每秒写入少于 1500 次,则 bump `1'000'000'000` 足以覆盖一周前的快照。
- 在 Kubernetes controller 场景下,还应用 `--mark-compacted` 把所有 revision(含 bump 部分)标记为已压缩,确保所有 watch 被终止、etcd 不响应快照之后 revision 的请求,从而**有效作废 informer 缓存**。
```bash
$ etcdutl snapshot restore snapshot.db --bump-revision 1000000000 --mark-compacted --data-dir output-dir
```
### 7.8 带更新成员关系的恢复
- 成员关系存储在 etcd 自身中并通过 raft 共识维护。quorum 完全丢失时,可以重新规划新集群的构成(例如全新的一套成员)。
- 恢复时可直接把新的成员关系写入 datastore:
```bash
$ etcdutl snapshot restore snapshot.db \
--name m1 \
--data-dir m1.etcd \
--initial-cluster m1=http://host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
--initial-cluster-token etcd-cluster-1 \
--initial-advertise-peer-urls http://host1:2380
```
这确保新建集群只会与给定 token 的其他已恢复成员连接,而不会连到可能仍存活并尝试连接的旧成员。
- **替代方案**:启动 etcd 时用 `--force-new-cluster` 覆写成员关系并保留现有应用数据。文档明确**强烈不建议**这样做,因为若旧集群的其他成员仍存活会 panic。务必周期性保存快照。
### 7.9 逐节点恢复完整流程(End-2-End Example)
**Step 1:从活跃集群取快照**
```bash
$ etcdctl snapshot save snapshot.db
```
**Step 2:为三成员集群创建新的数据目录**
每个成员用**同一快照**、不同 `--name` / `--data-dir` / `--initial-advertise-peer-urls`:
```bash
$ etcdutl snapshot restore snapshot.db \
--name m1 \
--data-dir m1_data_dir.etcd \
--initial-cluster m1=http://host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
--initial-cluster-token etcd-cluster-1 \
--initial-advertise-peer-urls http://host1:2380
$ etcdutl snapshot restore snapshot.db \
--name m2 \
--data-dir m2_data_dir.etcd \
--initial-cluster m1=http://host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
--initial-cluster-token etcd-cluster-1 \
--initial-advertise-peer-urls http://host2:2380
$ etcdutl snapshot restore snapshot.db \
--name m3 \
--data-dir m3_data_dir.etcd \
--initial-cluster m1=http://host1:2380,m2=http://host2:2380,m3=http://host3:2380 \
--initial-cluster-token etcd-cluster-1 \
--initial-advertise-peer-urls http://host3:2380
```
**Step 3:用新数据目录启动 etcd**
注意 `--name` 与 `--data-dir` 一一对应,`--listen-client-urls` / `--advertise-client-urls` 用 2379,`--listen-peer-urls` 用 2380:
```bash
$ etcd \
--name m1 \
--data-dir m1_data_dir.etcd \
--listen-client-urls http://host1:2379 \
--advertise-client-urls http://host1:2379 \
--listen-peer-urls http://host1:2380 &
$ etcd \
--name m2 \
--data-dir m2_data_dir.etcd \
--listen-client-urls http://host2:2379 \
--advertise-client-urls http://host2:2379 \
--listen-peer-urls http://host2:2380 &
$ etcd \
--name m3 \
--data-dir m3_data_dir.etcd \
--listen-client-urls http://host3:2379 \
--advertise-client-urls http://host3:2379 \
--listen-peer-urls http://host3:2380 &
```
**Step 4:恢复后的 etcd 集群即可对外提供快照中的 keyspace。**
### 7.10 恢复后校验
> 💡 补充:官方 recovery 页未列出「恢复后校验」清单,以下为本册基于本页与维护页信息整理的校验动作。
| 校验项 | 命令 | 期望 |
|---|---|---|
| 集群健康 | `etcdctl endpoint health --endpoints=...` | 全部 `is healthy` |
| 成员关系 | `etcdctl member list -w table` | 三个成员、peerURLs / clientURLs 正确 |
| 版本 | `curl http://host:2379/version` | `etcdcluster` 与 `storage` 均为目标版本 |
| 数据完整性 | `etcdctl get "" --prefix --keys-only \| wc -l` | 与快照 `TOTAL KEYS` 一致 |
| revision 单调 | `etcdctl endpoint status -w table` | RAFT INDEX / revision 不小于恢复前(若用了 `--bump-revision`) |
| 告警状态 | `etcdctl alarm list` | 无输出(无 NOSPACE / CORRUPT) |
### 7.11 注意事项汇总
| 项 | 说明 |
|---|---|
| 工具分工 | **自 etcd v3.6 起**:只能用 `etcdctl` 做快照(`snapshot save`),必须用 `etcdutl` 从快照恢复(`snapshot restore`) |
| `--data-dir` 默认值 | 若未指定,默认值为 `.etcd`(`` 取自 `--name`)。例如成员名为 `m1`、`m2`、`m3` 且未指定 `--data-dir`,则数据目录为 `m1.etcd`、`m2.etcd`、`m3.etcd` |
| 必须用同一快照 | 所有成员都应使用同一个快照恢复 |
| 新逻辑集群 | 恢复会覆写 member ID 与 cluster ID,必须启动一个新的逻辑集群 |
| `--force-new-cluster` | **强烈不建议**;若旧集群其他成员仍存活会 panic |
| integrity hash | 从数据目录复制的快照没有 hash,需 `--skip-hash-check` |
| revision 回退 | Kubernetes / 有 watch 消费者的场景必须用 `--bump-revision` + `--mark-compacted` |
---
## 8. 故障模式
> 引自:[Failure modes](https://etcd.io/docs/v3.7/op-guide/failures/)
大规模部署中机器故障很常见:单台因软硬件故障失效;多台因断电或网络问题同时失效;多种故障也可能并发,几乎不可能穷举所有情况。本节对故障种类做编目并说明 etcd 的设计容忍方式。大多数用户都能把具体故障映射到其中一类。为应对罕见的**不可恢复故障**,务必**备份** etcd 集群。
### 8.1 故障模式总表(现象 → 原因 → 处置)
| # | 故障模式 | 现象 | 原因 | 影响面 | 处置 / 恢复路径 |
|---|---|---|---|---|---|
| 1 | **Minor followers failure**(少数派 follower 故障) | 少于半数的 follower 失效;客户端与故障成员的连接断开 | 单台机器软硬件故障 | 集群**仍可接受请求并正常推进**,无重大中断(5 成员集群中 2 个 follower 故障不影响运行) | 客户端库应对读请求**自动重连到其他成员**;运维应预期其他成员因重连导致**系统负载上升** |
| 2 | **Leader failure**(leader 故障) | 集群自动选举新 leader,但**不是瞬时**完成 | leader 节点失效;故障检测基于超时模型 | 约需一个 **election timeout** 才能选出新 leader;选举期间**不能处理任何写**,写请求被**排队**;已发给旧 leader 但未提交的写**可能丢失**(已提交的写永不丢失) | 等待自动选举完成;新 leader 会**自动延长所有 lease 的超时**,确保 lease 不会在授予的 TTL 之前过期 |
| 3 | **Majority failure**(多数派故障) | 集群多数成员失效 | 断电、机房故障等导致多台同时失效 | 集群失败,**不能接受更多写入** | 只有多数成员重新可用后集群才能恢复;多数恢复后**自动选举新 leader** 并回到健康状态,新 leader 同样自动延长所有 lease 超时;若多数成员无法重新上线,必须执行 **disaster recovery**(第 7 章) |
| 4 | **Network partition**(网络分区) | 集群被切成多数派一侧与少数派一侧 | 网络设备 / 链路故障 | 多数派一侧成为可用集群,少数派一侧不可用。**etcd 不存在 split-brain** | 分区恢复后,少数派一侧会**自动识别多数派一侧的 leader** 并恢复自身状态。leader 在多数派侧=视作少数派 follower 故障;leader 在少数派侧=视作 leader 故障,该 leader **主动退位(step down)**,多数派侧选出新 leader |
| 5 | **Failure during bootstrapping**(启动引导期故障) | 集群 bootstrap 未成功 | 只要有必需成员未成功启动,bootstrap 就不算成功 | 集群不可用 | **删除所有成员上的数据目录**,用**新的 cluster-token 或新的 discovery token** 重新 bootstrap(几乎总比恢复更快更省资源) |
### 8.2 各类故障详解
#### 8.2.1 少数派 follower 故障
- **现象**:少于半数的 follower 失效。
- **影响**:集群仍可接受请求并正常推进,无重大中断。例如 5 成员集群中 2 个 follower 故障不影响运行。但客户端会失去与故障成员的连接。
- **恢复/应对**:客户端库应对读请求自动重连到其他成员,对用户隐藏中断;运维方应预期其他成员因重连导致**系统负载上升**。
#### 8.2.2 Leader 故障
- **现象**:leader 失效。
- **影响**:集群自动选举新 leader,但**不是瞬时**完成的——故障检测基于超时模型,因此大约需要一个 **election timeout** 才能选出新 leader。
- 选举期间集群**不能处理任何写**;此期间发来的写请求会被**排队**,直到新 leader 选出后再处理。
- 已发给旧 leader 但尚未提交的写**可能丢失**;新 leader 有权重写旧 leader 的任何未提交 entry。从用户视角看,部分写请求可能在新 leader 选举后超时,但**已提交的写永远不会丢失**。
- **恢复/应对**:新 leader 会**自动延长所有 lease 的超时**,确保 lease 不会在授予的 TTL 之前过期(即使该 lease 由旧 leader 授予)。
#### 8.2.3 多数派故障
- **现象**:集群多数成员失效。
- **影响**:集群失败,**不能接受更多写入**。
- **恢复**:只有多数成员重新可用后集群才能恢复;若多数成员无法重新上线,运维必须执行 **disaster recovery** 来恢复集群。一旦多数成员恢复工作,集群会**自动选举新 leader** 并回到健康状态;新 leader 同样会自动延长所有 lease 超时,确保没有 lease 因服务端不可用而过期。
#### 8.2.4 网络分区
- **现象**:与少数派 follower 故障或 leader 故障类似,网络分区把集群切成两半:多数派一侧与少数派一侧。
- **影响**:多数派一侧成为可用集群,少数派一侧不可用。**etcd 不存在 "split-brain"**,因为集群成员的增删都是显式的,且每次变更都由当前多数派成员批准。
- leader 在多数派一侧 → 从多数派视角看就是少数派 follower 故障。
- leader 在少数派一侧 → 属于 leader 故障,少数派侧的 leader 会**主动退位(step down)**,多数派一侧选出新 leader。
- **恢复**:分区恢复后,少数派一侧会**自动识别多数派一侧的 leader** 并恢复自身状态。
#### 8.2.5 启动引导期故障
- **现象**:集群 bootstrap 只有在所有必需成员都成功启动时才算成功。
- **影响/恢复**:引导期发生任何故障,应**删除所有成员上的数据目录**,并用**新的 cluster-token 或新的 discovery token** 重新 bootstrap。
- **说明**:也可以像恢复运行中集群那样去恢复一个 bootstrap 失败的集群,但由于没有数据要恢复,几乎总是比重新 bootstrap 更耗时耗资源。
### 8.3 与故障相关的补充场景(映射到上述五类)
> 💡 补充:官方 failures 页只编目了上述 5 类。实践中常见的两个场景可映射如下(映射关系为本册推导):
| 实践场景 | 归入类别 | 处置要点 |
|---|---|---|
| **磁盘慢**(fsync 延迟高导致心跳超时、频繁切主) | 归入「Leader failure」的诱因 | 用 `etcd_disk_wal_fsync_duration_seconds` 确认;`ionice` 提优先级;必要时换 SSD(见第 4.3 节) |
| **Quorum 丢失**(永久丢失超过 (N-1)/2 成员) | 归入「Majority failure」的终局 | 必须走第 7 章 disaster recovery,不能靠重启自愈 |
### 8.4 容错能力速查
| 集群规模 N | 可容忍永久故障数 (N-1)/2 | 写入所需多数派 |
|---|---|---|
| 1 | 0 | 1 |
| 3 | 1 | 2 |
| 5 | 2 | 3 |
| 7 | 3 | 4 |
> 💡 补充:上表由 failures 页给出的公式 *(N-1)/2* 推导,具体 quorum 表见第 17 章 FAQ。
---
## 9. 数据损坏
> 引自:[Data Corruption](https://etcd.io/docs/v3.7/op-guide/data_corruption/)
etcd 内置了**自动化数据损坏检测**,用于防止成员状态发生偏离(diverging)。
### 9.1 检测方式与启用 flag
检测方式分两类:
| 类别 | 检测对象 | 启用 flag |
|---|---|---|
| **Initial check**(初始检查) | 成员 bootstrap 期间的持久化状态 | `--experimental-initial-corrupt-check` |
| **Periodic check**(周期性检查)· Compacted revision hash | 已压缩 revision 的 hash | `--experimental-compact-hash-check-enabled` |
| **Periodic check**(周期性检查)· Latest revision hash | 最新 revision 的 hash | `--experimental-corrupt-check-time` |
> 💡 补充:这三个 flag 均带 `--experimental-` 前缀,在 v3.7 中**已被移除**并改为 feature gate / 非 experimental flag(见第 3.12 与 14.4 节):
| 旧写法(data_corruption 页) | v3.7 等价写法 |
|---|---|
| `--experimental-initial-corrupt-check` | `--feature-gates=InitialCorruptCheck=true` |
| `--experimental-compact-hash-check-enabled` | `--feature-gates=CompactHashCheck=true` |
| `--experimental-compact-hash-check-time` | `--compact-hash-check-time` |
| `--experimental-corrupt-check-time` | `--corrupt-check-time` |
### 9.2 两类检查的行为差异
| 维度 | Initial check | Periodic check |
|---|---|---|
| 执行时机 | etcd 成员 **bootstrap 期间** | 已运行集群中,由**集群 leader** 执行 |
| 执行者 | 该成员自己 | 集群 leader |
| 比对对象 | 自己的持久化状态 vs 其他成员 | 自己的持久化状态 vs 其他成员 |
| 不匹配后果 | **退出(exit)** | 触发 **CORRUPT ALARM** |
两种周期检查目的相同,但建议**都启用**,以在性能开销与检测时延之间取得平衡:
| 周期检查 | 性能开销 | 能否处理慢 follower | 前置要求 |
|---|---|---|---|
| Compacted revision hash check | 极小(不造成额外数据库扫描) | **能** | 要求集群有定期 compaction |
| Latest revision hash check | **高**(需在给定 revision 上扫描整个 etcd 内容计算 checksum) | **不能**(也不适用于频繁 compaction) | 无 |
### 9.3 Compacted revision hash check
- 用 `--experimental-compact-hash-check-enabled` 启用后,**每分钟执行一次**。
- 可用 `--experimental-compact-hash-check-time` 调整周期,格式为 `1m`(每分钟)、`1h`(每小时)。
- 该检查扩展了 compaction 流程,额外计算可供成员间比对的 checksum;**不会造成额外的数据库扫描**,因此非常廉价,但**要求集群有定期 compaction**。
### 9.4 Latest revision hash check
- 用 `--experimental-corrupt-check-time` 启用,需要提供执行周期,格式同为 `1m` / `1h`。
- 由于**性能开销高**,推荐周期为**数小时(a couple of hours)**。
- 执行一次检查需要在给定 revision 上**扫描整个 etcd 内容**来计算 checksum。
### 9.5 数据损坏成因
> 💡 补充:官方 data_corruption 页**没有单独列出「成因」小节**,仅说明检测机制的目的是「防止成员状态发生偏离(diverging)」。本册不编造成因清单。可归入「成员状态偏离」的实际诱因(如非正常关机导致 WAL 未落盘、跨集群误配置、磁盘静默错误等)请以官方 CHANGELOG 与 issue 为准。
### 9.6 修复损坏的成员(三种方式)
修复完成后,CORRUPT ALARM 可以被移除。三种方式:
| 方式 | 适用场景 | 数据目录是否需要保留 |
|---|---|---|
| **Purge member persistent state** | 只想让该成员重新从 leader 拉取最新快照 | 保留(移走 `snap` 子目录) |
| **Replace member** | 该成员本地状态已不可信 | 删除 |
| **Restore whole cluster** | 损坏范围广,或多个成员受损 | 全部重建 |
#### 方式一:Purge member persistent state
步骤:
1. 停止 etcd 实例。
2. 备份 etcd 数据目录。
3. 把 etcd 数据目录中的 `snap` 子目录移出去。
4. 以 `--initial-cluster-state=existing` 启动 `etcd`,并在 `--initial-cluster` 中列出集群成员。
预期结果:该 etcd 成员会从 leader **下载最新的快照**。
> 注:原文步骤编号为 1、2、3、6,本册按顺序整理为 1–4。
#### 方式二:Replace member
步骤:
1. 停止 etcd 实例。
2. 备份 etcd 数据目录。
3. 删除数据目录。
4. 执行 `etcdctl member remove` 把该成员从集群中移除。
5. 执行 `etcdctl member add` 把它重新加回。
6. 以 `--initial-cluster-state=existing` 启动 `etcd`,并在 `--initial-cluster` 中列出集群成员。
#### 方式三:Restore whole cluster
- 通过从**当前 leader** 保存快照并恢复到所有成员来恢复整个集群。
- 对 leader 执行 `etcdctl snapshot save`,然后按 restoring a cluster 流程操作(即第 7 章的恢复流程)。
### 9.7 三种修复方式对比
> 💡 补充:下表对比为本册基于三种方式的步骤推导。
| 维度 | Purge | Replace | Restore whole cluster |
|---|---|---|---|
| 影响的成员数 | 1 | 1 | 全部 |
| 是否需要 remove/add | 否 | **是** | 是(重建新逻辑集群) |
| 数据来源 | 从 leader 下载最新快照 | 从 leader 追平 | 指定快照文件 |
| 是否改变 member ID | 否 | **是**(remove+add 后 ID 变化) | **是**(restore 覆写 member/cluster ID) |
| 停机范围 | 单成员 | 单成员 | 全集群 |
| 典型场景 | 快照文件损坏但 WAL/成员身份可用 | 单成员数据目录整体损坏 | quorum 丢失或多成员不一致 |
---
## 10. 安全:TLS、认证与 RBAC
本章讲三件事:**链路上怎么加密**(TLS)、**身份怎么认证**(auth)、**权限怎么划分**(RBAC),最后给出版本策略与兼容范围。
> 引自:[Transport security model](https://etcd.io/docs/v3.7/op-guide/security/)
> 引自:[Authentication](https://etcd.io/docs/v3.7/op-guide/authentication/authentication.md)
> 引自:[Role-based access control](https://etcd.io/docs/v3.7/op-guide/authentication/rbac/)
> 引自:[Versioning](https://etcd.io/docs/v3.7/op-guide/versioning/)
### 10.1 基本前提与默认风险(必须先读)
etcd 同时支持**自动 TLS** 与**客户端证书认证**,覆盖 client-to-server 与 peer(server-to-server / 集群)两种通信。
> **重要默认行为**:etcd **默认不启用**基于 RBAC 的认证,也不启用传输层认证,目的是降低新用户上手摩擦;且该默认自 2013 年确立,改动属于 breaking change。因此**未启用安全特性的 etcd 集群会把数据暴露给任意客户端**。
起步要求:
- 先准备一个 CA 证书(`ca.crt`)和为该成员签名的密钥对;
- **建议为集群中每个成员单独创建并签名新的密钥对**;
- 文档推荐 [cfssl](https://github.com/cloudflare/cfssl) 工具(示例见 `hack/tls-setup`),也提供自签名密钥对生成指南。
> ⚠️ 源文提示:下方 flag 列表可能因开发变更而过期,最新 flag 以 `etcd --help` 为准。
### 10.2 client-to-server(客户端到服务器)TLS flag
| flag | 说明 |
|---|---|
| `--cert-file=` | 用于到 etcd 的 SSL/TLS 连接的证书。设置后 `--advertise-client-urls` 可使用 HTTPS schema |
| `--key-file=` | 证书对应的私钥,**必须未加密** |
| `--client-cert-auth` | 设置后 etcd 检查所有入站 HTTPS 请求是否携带由受信任 CA 签名的客户端证书,未提供有效证书的请求失败。若同时启用了 authentication,证书提供凭据,**用户名取证书 Common Name 字段** |
| `--trusted-ca-file=` | 受信任的证书颁发机构 |
| `--auto-tls` | 使用自动生成自签名证书与客户端建立 TLS |
### 10.3 Peer(server-to-server / 集群)TLS flag
| flag | 说明 |
|---|---|
| `--peer-cert-file=` | peer 间 SSL/TLS 证书,既用于监听 peer 地址,也用于向其他 peer 发请求 |
| `--peer-key-file=` | 私钥,**必须未加密** |
| `--peer-client-cert-auth` | 检查来自集群的入站 peer 请求是否持有由给定 CA 签发的有效客户端证书 |
| `--peer-trusted-ca-file=` | peer 侧受信任 CA |
| `--peer-auto-tls` | peer 间使用自动生成自签名证书 |
**规则:只要提供了 client-to-server 或 peer 任一证书,就必须同时设置对应的 key。** 全部配置项均可用环境变量表达,如 `ETCD_CA_FILE`、`ETCD_PEER_CA_FILE` 等。
### 10.4 通用 TLS 选项
| flag | 说明 |
|---|---|
| `--cipher-suites` | server/client 与 peer 之间支持的 TLS cipher suite 逗号分隔列表;**留空则由 Go 自动填充** |
| `--tls-min-version=` | 设置 etcd 支持的最低 TLS 版本 |
| `--tls-max-version=` | 设置 etcd 支持的最高 TLS 版本;**未设置则使用 Go 支持的最高版本** |
### 10.5 证书 keyUsage 与 extendedKeyUsage(必配)
etcd 依赖 Go 的 `crypto/tls` 与 `crypto/x509` 做证书校验,握手期间会强制这些用途。推荐配置:
| 证书角色 | keyUsage | extendedKeyUsage |
|---|---|---|
| Server(client-to-server) | digitalSignature, keyEncipherment | serverAuth |
| Client | digitalSignature, keyEncipherment | clientAuth |
| Peer(server-to-server) | digitalSignature, keyEncipherment | serverAuth, clientAuth |
补充:启用 `--peer-cert-allowed-cn` 之外的 `--peer-client-cert-auth` 时,peer 证书用于成员间 mTLS,**必须同时含 `serverAuth` 与 `clientAuth`**;配合 `--client-cert-auth` 的客户端证书应含 `clientAuth`。
### 10.6 四个标准示例
#### 10.6.1 Example 1:client-to-server HTTPS 传输安全
```sh
$ etcd --name infra0 --data-dir infra0 \
--cert-file=/path/to/server.crt --key-file=/path/to/server.key \
--advertise-client-urls=https://127.0.0.1:2379 --listen-client-urls=https://127.0.0.1:2379
```
验证(自签名 CA 必须用 `--cacert` 传给 curl;也可把 CA 加入系统受信目录 `/etc/pki/tls/certs` 或 `/etc/ssl/certs`):
```sh
$ curl --cacert /path/to/ca.crt https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v
```
**OSX 10.9+ 注意**:该版本 curl 7.30.0 不理解命令行传入的证书,需把 `ca.crt` 导入 keychain,或加 `-k` 忽略错误;可运行 `open ./tests/fixtures/ca/ca.crt` 后按提示操作,测试完请删除该证书。
#### 10.6.2 Example 2:HTTPS 客户端证书认证
```sh
$ etcd --name infra0 --data-dir infra0 \
--client-cert-auth --trusted-ca-file=/path/to/ca.crt --cert-file=/path/to/server.crt --key-file=/path/to/server.key \
--advertise-client-urls https://127.0.0.1:2379 --listen-client-urls https://127.0.0.1:2379
```
此时未带客户端证书的请求被服务端拒绝:`routines:SSL3_READ_BYTES:sslv3 alert bad certificate`。携带证书后成功(握手输出含 `SSLv3, TLS handshake, CERT verify (15)` 与 `TLS handshake, Finished (20)`,返回 `{"action": "set", "node": {...}}`):
```sh
$ curl --cacert /path/to/ca.crt --cert /path/to/client.crt --key /path/to/client.key \
-L https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v
```
**指定 cipher suites 屏蔽弱套件**(参考 issue#8320),握手时 client hello 若使用非法套件会失败:
```bash
$ etcd \
--cert-file ./server.crt \
--key-file ./server.key \
--trusted-ca-file ./ca.crt \
--cipher-suites TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
```
```bash
# 合法套件,请求成功,返回 etcd_server_version{server_version="3.2.22"} 1
$ curl --cacert /path/to/ca.crt --cert /path/to/client.crt --key /path/to/client.key \
-L [CLIENT-URL]/metrics --ciphers ECDHE-RSA-AES128-GCM-SHA256
# 非法套件,失败:(35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure
$ curl --cacert /path/to/ca.crt --cert /path/to/client.crt --key /path/to/client.key \
-L [CLIENT-URL]/metrics --ciphers ECDHE-RSA-DES-CBC3-SHA
```
#### 10.6.3 Example 3:集群内传输安全 + 客户端证书(peer 双向 TLS)
```sh
DISCOVERY_URL=... # from https://discovery.etcd.io/new
# member1
$ etcd --name infra1 --data-dir infra1 \
--peer-client-cert-auth --peer-trusted-ca-file=/path/to/ca.crt --peer-cert-file=/path/to/member1.crt --peer-key-file=/path/to/member1.key \
--initial-advertise-peer-urls=https://10.0.1.10:2380 --listen-peer-urls=https://10.0.1.10:2380 \
--discovery ${DISCOVERY_URL}
# member2
$ etcd --name infra2 --data-dir infra2 \
--peer-client-cert-auth --peer-trusted-ca-file=/path/to/ca.crt --peer-cert-file=/path/to/member2.crt --peer-key-file=/path/to/member2.key \
--initial-advertise-peer-urls=https://10.0.1.11:2380 --listen-peer-urls=https://10.0.1.11:2380 \
--discovery ${DISCOVERY_URL}
```
成员组成集群后,成员间通信全部加密并用客户端证书认证,日志中连接地址显示为 HTTPS。
#### 10.6.4 Example 4:自动自签名传输安全
仅需加密、不需认证时用 `--auto-tls` 与 `--peer-auto-tls`:
```sh
# member1
$ etcd --name infra1 --data-dir infra1 \
--auto-tls --peer-auto-tls \
--initial-advertise-peer-urls=https://10.0.1.10:2380 --listen-peer-urls=https://10.0.1.10:2380 \
--discovery ${DISCOVERY_URL}
# member2 同构,IP 为 10.0.1.11
```
**限制一**:自签名证书不认证身份,curl 报 `curl: (60) SSL certificate problem: Invalid certificate chain`,需用 `-k`:
```sh
curl -k https://127.0.0.1:2379/v2/keys/foo -Xput -d value=bar -v
```
**限制二**:指定 ClientAutoTLS 与 PeerAutoTLS 时,etcd 自动生成的 client / peer 证书**有效期仅 1 年**;可用 `--self-signed-cert-validity` 以「年」为单位设置有效期。
### 10.7 DNS SRV 注意事项
自 **v3.1.0** 起(**v3.2.9 除外**),discovery SRV 引导会用 `--discovery-srv` 的根域名认证 `ServerName`,以防御中间人证书攻击:要求证书的 **Subject Alternative Name(SAN)** 含匹配的根域名。
例如 `etcd --discovery-srv=etcd.local` 只接受 SAN 中含 `etcd.local` 的证书。
### 10.8 etcd proxy 注意事项
proxy **终止**来自其客户端的 TLS,并使用自身 `--peer-key-file` / `--peer-cert-file` 指定的 key/cert 与 etcd 成员通信。proxy 通过成员的 `--advertise-client-urls` 和 `--advertise-peer-urls` 两者通信:
| 用途 | 走哪个 URL |
|---|---|
| 转发客户端请求 | advertise client urls |
| 同步初始集群配置 | advertise peer urls |
管理员必须确保:
- 成员启用 client 认证时,proxy 的 `--peer-cert-file` 证书对该认证**有效**;
- 启用 peer 认证时该证书也**必须对 peer 认证有效**。
### 10.9 TLS 认证的版本演进细节(逐条)
| 版本 | 变更 | 细节 |
|---|---|---|
| **v3.2.0** | TLS 证书在**每次客户端连接时重载**(PR#7829) | 可在不停机情况下直接用新证书覆盖旧证书完成轮换;每连接刷新开销不大,未来可加缓存层优化 |
| **v3.2.0** | 服务端拒绝 IP SAN 错误的入站 peer 证书(PR#7687) | 若 peer 证书 SAN 含 IP 地址,只有远端 IP 命中其中之一才通过。例:CSR hosts 含 `10.138.0.27` 而实际 IP 为 `10.138.0.2` 时被拒,错误 `x509: certificate is valid for 10.138.0.27, not 10.138.0.2` |
| **v3.2.0** | 服务端在检查 SAN 时解析 TLS `DNSNames`(PR#7767) | 若证书 SAN 只有 DNS 名无 IP,则对该 DNS 名做正向解析(如 `dig b.com`),解析结果含远端 IP 才通过,否则报错 `tls: 10.138.0.2 does not match any of DNSNames ["b.com"]` |
| **v3.2.2** | 若 IP 匹配则直接接受,不再检查 DNS 条目(PR#8223) | 例:hosts 为 `["invalid.domain", "10.138.0.2"]`,远端 IP 为 `10.138.0.2` 时即使 `invalid.domain` 无效也认证通过(issue#8206) |
| **v3.2.5** | 支持通配符 DNS SAN 的反向查找(PR#8281) | 先对远端 IP 做反向解析(`nslookup IPADDR`)得到主机名列表,与证书 DNS 名做精确或通配匹配;都不匹配时再对每个 DNS 条目做正向解析(条目 `*.example.default.svc` 则查 `example.default.svc`),命中远端 IP 才接受;否则返回 `"tls: "10.138.0.2" does not match any of DNSNames ["*.example.default.svc","*.example.default.svc.cluster.local"]`(issue#8268) |
| **v3.3.0** | 新增 `etcd --peer-cert-allowed-cn`(PR#8616) | 支持基于 CN 的 inter-peer 连接认证(issue#8262) |
| **v3.2.19 与 v3.3.4** | 修复「证书 SAN 只含 IP、不含域名」时的 TLS reload(issue#9541) | 见下方展开说明 |
#### 10.9.1 `--peer-cert-allowed-cn` 的工作方式
Kubernetes TLS bootstrapping 会为 etcd 成员与 API server、kubelet 等动态生成证书,为每个组件维护独立 CA 虽更严格但繁琐;指定该 flag 后,**即使共享 CA,也只有 CN 匹配的节点才能加入**。
- CN 匹配是**精确字符串比较**,**不支持通配符或前缀匹配**。
- 基于主机名的过滤 `--peer-cert-allowed-hostname` / `--client-cert-allowed-hostname` 使用 Go 的 `x509.Certificate.VerifyHostname()`,**支持精确主机名与通配符条目**(如 `*.example.com`)。
三节点示例:CSR 均为 `"CN": "etcd.local"`,hosts 分别为 `m1.etcd.local` / `m2.etcd.local` / `m3.etcd.local` 加 `127.0.0.1`、`localhost`;`--peer-cert-allowed-cn etcd.local` 时仅 CN 匹配者可加入,否则日志显示:
```bash
$ etcd --peer-cert-allowed-cn m1.etcd.local
I | embed: rejected connection from "127.0.0.1:48044" (error "CommonName authentication failed", ServerName "m1.etcd.local")
I | embed: rejected connection from "127.0.0.1:55702" (error "remote error: tls: bad certificate", ServerName "m3.etcd.local")
```
正确配置时每个进程以 `etcd --peer-cert-allowed-cn etcd.local` 启动,日志显示:
```text
pkg/netutil: resolving m3.etcd.local:32380 to 127.0.0.1:32380
etcdserver: published {Name:m1 ClientURLs:[https://m1.etcd.local:2379]} to cluster 9db03f09b20de32b
embed: ready to serve client requests
```
#### 10.9.2 SAN 只含 IP 时的 TLS reload 修复(v3.2.19 / v3.3.4)
Go 中仅当 `(*tls.Config).Certificates` 非空,或 `(*tls.ClientHelloInfo).ServerName` 非空且 SNI 有效时,服务端才调用 `(*tls.Config).GetCertificate` 重载证书。
- 修复前:etcd 在初始握手时总把 `Certificates` 填为非空,导致客户端必须提供匹配 SNI 才能通过校验并触发重载;而 SAN 仅含 IP 的证书会产生空 `ServerName`,无法触发重载,过期证书在线替换因此失败。
- 修复后:初始握手时 `Certificates` 创建为空以触发 `GetCertificate`,随后在每个新 TLS 连接上填充其余证书,**即使客户端 SNI 为空(证书只含 IP)也能重载**。
### 10.10 Host Whitelist(防御 DNS Rebinding)
`etcd --host-whitelist` 指定 HTTP 客户端请求可接受的主机名,用于防御 ["DNS Rebinding"](https://en.wikipedia.org/wiki/DNS_rebinding) 攻击(参考 CVE-2018-5702):任意网站可创建授权 DNS 名并指向 `"localhost"`,从而使监听 localhost 的 etcd HTTP 端点全部暴露。
规则:
1. 客户端连接为 **HTTPS 安全连接**时,允许任意主机名;
2. 连接**不安全**且 `"HostWhitelist"` 非空时,只允许 Host 字段在白名单内的请求。**无论是否启用认证,该策略都强制生效**。
默认值与坑:
- 默认 `etcd --host-whitelist` 与 `embed.Config.HostWhitelist` 为**空**(允许所有主机名);
- 指定主机名时**不会自动加入 loopback 地址**,需手动添加 `"localhost"`、`"127.0.0.1"` 等。
### 10.11 TLS 常见问题(FAQ)
**Q1:TLS 客户端认证出现 SSLv3 alert handshake failure**
Go 的 `crypto/tls` 使用证书公钥前会检查 key usage;做 client auth 必须在创建证书公钥时把 `clientAuth` 加入 Extended Key Usage。在 `openssl.cnf` 增加段并签名时引用:
```text
[ ssl_client ]
...
extendedKeyUsage = clientAuth
...
```
```bash
$ openssl ca -config openssl.cnf -policy policy_anything -extensions ssl_client -out certs/machine.crt -infiles machine.csr
```
**Q2:peer 证书认证报 "certificate is valid for 127.0.0.1, not $MY_IP"**
必须用成员**公网 IP** 作为 Subject Name 签发证书;`etcd-ca` 的 `new-cert` 提供 `--ip=` 选项。证书还需为成员的 FQDN 签名,用 **Subject Alternative Names(简称 IP SANs)** 补充 IP 地址;`etcd-ca` 提供 `--domain=` 选项,openssl 也可用 `subjectAltName`。
**Q3:etcd 是否加密磁盘上的数据?**
**不加密。** 若需加密:让客户端应用自行加解密,或使用底层存储系统的加密能力(如 [dm-crypt])。
**Q4:日志告警 "directory X exist without recommended permission -rwx------"**
etcd 新建某些目录时把权限设为 700 以防越权访问;若用户已按自己的偏好创建该目录,etcd 沿用现有目录并在权限非 700 时打印该告警。
### 10.12 开启认证:完整步骤与命令
> 源文件说明:`authentication.md` 仅 1254 字节,正文只有一段 shell 示例加一句 "This is just a stub which needs to be filled and updated with more information on authentication. The text above is just a code example."。同目录 `_index.md` **只有 Hugo front matter**(`title: Authentication Guides` / `weight: 4000` / description),**无任何正文内容**。因此本册仅能输出源文实际存在的命令序列,权限模型细节见 10.13 节。
源文完整命令序列(原样保留):
```shell
export ETCDCTL_API=3
ENDPOINTS=localhost:2379
etcdctl --endpoints=${ENDPOINTS} role add root
etcdctl --endpoints=${ENDPOINTS} role get root
etcdctl --endpoints=${ENDPOINTS} user add root
etcdctl --endpoints=${ENDPOINTS} user grant-role root root
etcdctl --endpoints=${ENDPOINTS} user get root
etcdctl --endpoints=${ENDPOINTS} role add role0
etcdctl --endpoints=${ENDPOINTS} role grant-permission role0 readwrite foo
etcdctl --endpoints=${ENDPOINTS} user add user0
etcdctl --endpoints=${ENDPOINTS} user grant-role user0 role0
etcdctl --endpoints=${ENDPOINTS} auth enable
# now all client requests go through auth
etcdctl --endpoints=${ENDPOINTS} --user=user0:123 put foo bar
etcdctl --endpoints=${ENDPOINTS} get foo
# permission denied, user name is empty because the request does not issue an authentication request
etcdctl --endpoints=${ENDPOINTS} --user=user0:123 get foo
# user0 can read the key foo
etcdctl --endpoints=${ENDPOINTS} --user=user0:123 get foo1
```
要点解读(严格基于上述命令的注释):
1. 第一步固定为 `export ETCDCTL_API=3`,随后所有操作通过 `etcdctl --endpoints=${ENDPOINTS}` 发起。
2. 建 `root` 角色 → `role get root` 查看 → 建 `root` 用户 → `user grant-role root root`(把 root 角色授予 root 用户)→ `user get root` 查看。
3. 再建普通角色 `role0`,用 `role grant-permission role0 readwrite foo` 授予对 key `foo` 的 READWRITE 权限;建普通用户 `user0` 并用 `user grant-role user0 role0` 授予角色。
4. `auth enable` 之后**所有客户端请求都走认证**。
5. 未带 `--user` 的 `get foo` 被拒绝(permission denied),源文注释明确原因:**请求未发起认证请求,因此 user name 为空**。
6. `--user=user0:123 get foo` 成功(user0 可读 foo);`get foo1` 则不在授权范围内。
最小启用步骤(管理员可在启用认证之前或之后配置用户与角色,取决于偏好):
```bash
$ etcdctl user add root
Password of root:
$ etcdctl auth enable
```
禁用认证(需要 root 身份):
```bash
$ etcdctl --user root:rootpw auth disable
```
### 10.13 RBAC 权限模型
认证功能自 etcd 2.1 加入;etcd v3 API 略微修改了认证特性的 API 与用户界面以适配新数据模型。
#### 10.13.1 特殊用户与特殊角色
存在一个特殊用户 `root` 与一个特殊角色 `root`。
| 主体 | 说明 |
|---|---|
| **User `root`** | 拥有对 etcd 的完全访问权,**必须在激活认证之前创建**。设计用途是管理目的——管理角色与普通用户。root 用户**必须拥有 root 角色**,并被允许修改 etcd 内任何内容 |
| **Role `root`** | 除 root 用户外,也可授予任何其他用户。持有 root 角色的用户拥有**全局读写访问权**以及**更新集群认证配置的权限**。此外 root 角色还授予通用集群维护特权,包括**修改集群成员、对 store 做碎片整理(defragment)、以及打快照** |
> ⚠️ 源文说明:源文只列出 root 用户与 root 角色两个特殊主体,**未提及 guest 角色**。(任务书中提到的「默认角色 root/guest」,`guest` 一项 v3.7 源文未提供。)
#### 10.13.2 用户管理(`etcdctl user` 子命令)
```bash
$ etcdctl user list # 列出用户
$ etcdctl user add myusername # 创建用户,会提示输入新密码
$ etcdctl user add myusername --no-password # 创建不能用密码认证的用户,只能通过 TLS Common Name 认证
$ etcdctl user grant-role myusername foo # 授予角色
$ etcdctl user revoke-role myusername bar# 撤销角色
$ etcdctl user get myusername # 查看用户设置
$ etcdctl user passwd myusername # 修改密码,会再次提示输入
$ etcdctl user delete myusername # 删除账号
```
- 创建/改密时可用 `--interactive=false` 从标准输入提供密码;也可用 `--new-user-password` 提供密码。
- **空密码限制**:etcd 不支持用空密码认证(形如 `--user username:`)。例如以空密码创建的用户 `etcdctl user add anonymous:''` 无法通过用户名/密码请求认证,`etcdctl --user anonymous: get foo` 会失败并报 `user name is empty`。
- 角色本身**没有密码**,仅定义一组访问权限。
#### 10.13.3 角色管理(`etcdctl role` 子命令)
```bash
$ etcdctl role list # 列出角色
$ etcdctl role add myrolename # 新建角色
$ etcdctl role get myrolename # 查看已授予的权限
$ etcdctl role delete myrolename # 删除角色
```
#### 10.13.4 权限(permission)模型
角色被授予对**单个 key 或一段 key range** 的访问权。range 以区间 **[start-key, end-key)** 表示,要求 start-key 在字典序上小于 end-key。
权限类型三种:**read(READ)、write(WRITE)、readwrite(READWRITE)**。
```bash
# 授予对 key /foo 的读权限
$ etcdctl role grant-permission myrolename read /foo
# 授予对前缀 /foo/ 下 key 的读权限;前缀等价于区间 [/foo/, /foo0)
$ etcdctl role grant-permission myrolename --prefix=true read /foo/
# 授予 /foo/bar 的只写权限
$ etcdctl role grant-permission myrolename write /foo/bar
# 授予区间 [key1, key5) 的完全权限
$ etcdctl role grant-permission myrolename readwrite key1 key5
# 授予前缀 /pub/ 下 key 的完全权限
$ etcdctl role grant-permission myrolename --prefix=true readwrite /pub/
```
撤销权限与授权逻辑对称:
```bash
$ etcdctl role revoke-permission myrolename /foo/bar
```
### 10.14 认证的安全作用域(重要边界)
用 `etcdctl auth enable` 启用认证后,它保护 **V3 gRPC API 操作(get、put、delete、watch 等)**。
> **`/metrics` 与 `/health` HTTP 端点运行在独立的 handler 上,不受 V3 RBAC 认证保护。**
该设计让 Prometheus 与负载均衡器无需 gRPC 认证即可抓取指标,同时仍然保护 key-value 数据。要保护这些可观测性端点,可选方案:
| 方案 | 具体做法 |
|---|---|
| 启用 mTLS | `--cert-file`、`--key-file`、`--client-cert-auth` |
| 绑定内网 | 用 `--listen-metrics-urls` 把 metrics 绑定到私有网络接口 |
| 网络层限制 | 使用网络策略 / 防火墙规则限制访问 |
### 10.15 etcdctl 认证用法
```bash
$ etcdctl --user user:password get foo # 类似 curl 的认证 flag
$ etcdctl --user user get foo # 密码从提示符读取
$ etcdctl --user user --password password get foo # 密码由 --password 命令行 flag 提供
```
其余 etcdctl 命令保持不变;用户与角色仍可创建和修改,但**必须由具有 root 角色的用户通过认证后执行**。
### 10.16 使用 TLS Common Name 做认证
自 **v3.2** 起,若 etcd server 以 `--client-cert-auth=true` 启动,客户端 TLS 证书的 **Common Name(CN)字段会被当作 etcd 用户名**;此时 CN 即完成用户认证,客户端**不需要密码**。
**优先级**:若同时满足 1)传入 `--client-cert-auth=true` 且客户端提供了 CN,2)客户端提供了用户名和密码,则**优先使用基于用户名与密码的认证**。
**限制**:该特性**不能与 gRPC-proxy 和 gRPC-gateway 一起使用**。
- gRPC-proxy 会终止来自其客户端的 TLS,所有客户端共享 proxy 的证书;
- gRPC-gateway 内部也用 TLS 连接把 HTTP 请求转换为 gRPC 请求,限制相同——客户端无法正确向服务端提供自己的 CN;
- 若给定证书的 CN 非空,gRPC-proxy 会报错并停止,返回的错误表明客户端证书含非空 CN。
### 10.17 密码强度说明
`etcdctl` 与 etcd API 在**创建用户或更新用户密码时不强制任何密码长度要求**,是否强制由管理员负责。为规避密码强度相关的安全风险,可使用 TLS Common Name 认证以及用 `--no-password` 创建的用户。
### 10.18 版本策略与兼容范围
> 引自:[Versioning](https://etcd.io/docs/v3.7/op-guide/versioning/)
#### 10.18.1 版本号语义
etcd 版本表示为 **x.y.z**,其中 **x = major(主版本)**,**y = minor(次版本)**,**z = patch(补丁版本)**,遵循 [Semantic Versioning](https://semver.org/) 术语。**新的次版本可能向 API 添加额外特性。**
#### 10.18.2 支持的发布分支
- etcd 项目为**当前版本与上一个发布版本**维护 release branch。
- 例如:v3.5 为当前版本时,v3.4 受支持;**当 v3.6 发布时,v3.4 不再受支持**。
- 适用的修复(包括安全修复)**可能被 backport 到这两个发布分支**,取决于严重性与可行性;需要时从这些分支切出 patch 版本。该决策由项目 Maintainers 负责。
> 💡 补充:按上述规则推演,v3.7 为当前版本时,受支持的分支是 v3.7 与 v3.6;v3.5 及更早分支不再受支持(源文未明示 v3.7 场景,此处为规则推演,非原文)。
#### 10.18.3 查看运行中集群版本
```sh
etcdctl --endpoints=127.0.0.1:2379 endpoint status
```
#### 10.18.4 API 版本化
`v3` API 的响应自 **3.0.0** 发布起**不应发生变化**,但新特性会随时间增加。
> ⚠️ 源文说明:源文**未给出** API 版本兼容矩阵或跨版本升级/降级的具体兼容范围表格,本页不含此类内容。实际升级/降级的版本要求见第 14 章。
### 10.19 安全配置检查清单
| 项 | 建议 | 依据章节 |
|---|---|---|
| client-to-server 加密 | `--cert-file` + `--key-file`(key 必须未加密) | 10.2 |
| client 强制证书认证 | `--client-cert-auth` + `--trusted-ca-file` | 10.2 |
| peer 双向 TLS | `--peer-client-cert-auth` + `--peer-trusted-ca-file` + `--peer-cert-file`/`--peer-key-file` | 10.3 |
| peer 证书 EKU | 必须同时含 `serverAuth` 与 `clientAuth` | 10.5 |
| 自动 TLS 有效期 | 仅 1 年,用 `--self-signed-cert-validity` 调整 | 10.6.4 |
| DNS SRV 引导 | 证书 SAN 需含 `--discovery-srv` 根域名 | 10.7 |
| DNS Rebinding 防护 | `--host-whitelist` 手动补 `localhost`/`127.0.0.1` | 10.10 |
| 启用认证 | 先建 root 用户与 root 角色,再 `auth enable` | 10.12 |
| 可观测端点 | `/metrics`、`/health` 不受 RBAC 保护,需 mTLS 或内网绑定 | 10.14 |
| 磁盘加密 | etcd 不加密磁盘数据,需应用层或 dm-crypt | 10.11 Q3 |
---
## 11. 监控与告警
本章回答:**etcd 暴露了哪些可观测端点**、**Prometheus 怎么配**、**哪些指标必须告警**。指标全量 123 条见第 12 章。
> 引自:[Monitoring etcd](https://etcd.io/docs/v3.7/op-guide/monitoring/)
> 引自:[Metrics](https://etcd.io/docs/v3.7/metrics/)
每个 etcd server 通过 HTTP 端点在其 **client port** 上提供本地监控信息,数据可用于系统健康检查与集群调试。
### 11.1 可观测端点全景
| 端点 | 路径 | 启用条件 / 版本 | 用途 |
|---|---|---|---|
| Debug | `/debug/pprof`、`/debug/requests` | 需 `--log-level=debug` | Go runtime profiling、gRPC 调用轨迹 |
| Metrics | `/metrics` | 默认在 client port;也可用 `--listen-metrics-urls` 指定 | Prometheus 指标 |
| Health | `/health` | **v3.3.0 起**,`--listen-metrics-urls` 指定的位置也会响应 | 健康检查(供 LB / 监控用) |
| Liveness | `/livez` | **v3.4 起** | 进程是否存活、是否需要重启 |
| Readiness | `/readyz` | **v3.4 起** | 进程是否就绪可对外提供服务 |
### 11.2 Debug 端点(有代价)
设置 `--log-level=debug` 时,etcd server 在 client port 的 `/debug` 路径下导出调试信息。**注意该设置会导致性能下降与日志冗长。**
#### 11.2.1 `/debug/pprof`
标准 Go runtime profiling 端点,可用于 CPU、heap、mutex、goroutine 使用率分析。
```sh
$ go tool pprof http://localhost:2379/debug/pprof/profile
Fetching profile from http://localhost:2379/debug/pprof/profile
Please wait... (30s)
Saved profile in /home/etcd/pprof/pprof.etcd.localhost:2379.samples.cpu.001.pb.gz
Entering interactive mode (type "help" for commands)
(pprof) top10
310ms of 480ms total (64.58%)
Showing top 10 nodes out of 157 (cum >= 10ms)
flat flat% sum% cum cum%
130ms 27.08% 27.08% 130ms 27.08% runtime.futex
70ms 14.58% 41.67% 70ms 14.58% syscall.Syscall
20ms 4.17% 45.83% 20ms 4.17% github.com/coreos/etcd/vendor/golang.org/x/net/http2/hpack.huffmanDecode
20ms 4.17% 50.00% 30ms 6.25% runtime.pcvalue
20ms 4.17% 54.17% 50ms 10.42% runtime.schedule
10ms 2.08% 56.25% 10ms 2.08% github.com/coreos/etcd/vendor/github.com/coreos/etcd/etcdserver.(*EtcdServer).AuthInfoFromCtx
10ms 2.08% 58.33% 10ms 2.08% github.com/coreos/etcd/vendor/github.com/coreos/etcd/etcdserver.(*EtcdServer).Lead
10ms 2.08% 60.42% 10ms 2.08% github.com/coreos/etcd/vendor/github.com/coreos/etcd/pkg/wait.(*timeList).Trigger
10ms 2.08% 62.50% 10ms 2.08% github.com/coreos/etcd/vendor/github.com/prometheus/client_golang/prometheus.(*MetricVec).hashLabelValues
10ms 2.08% 64.58% 10ms 2.08% github.com/coreos/etcd/vendor/golang.org/x/net/http2.(*Framer).WriteHeaders
```
#### 11.2.2 `/debug/requests`
通过浏览器给出 gRPC 调用轨迹与性能统计。示例(对 key `abc` 的 `Range` 请求):
```text
When Elapsed (s)
2017/08/18 17:34:51.999317 0.000244 /etcdserverpb.KV/Range
17:34:51.999382 . 65 ... RPC: from 127.0.0.1:47204 deadline:4.999377747s
17:34:51.999395 . 13 ... recv: key:"abc"
17:34:51.999499 . 104 ... OK
17:34:51.999535 . 36 ... sent: header: kvs: count:1
```
### 11.3 Metrics 端点
每个 etcd server 在 client port 的 `/metrics` 路径导出指标,也**可选地在 `--listen-metrics-urls` 指定的位置导出**。用 curl 抓取(源文示例用 `grep -v debugging` 过滤不稳定的 debugging 指标):
```sh
$ curl -L http://localhost:2379/metrics | grep -v debugging # ignore unstable debugging metrics
# HELP etcd_disk_backend_commit_duration_seconds The latency distributions of commit called by backend.
# TYPE etcd_disk_backend_commit_duration_seconds histogram
etcd_disk_backend_commit_duration_seconds_bucket{le="0.002"} 72756
etcd_disk_backend_commit_duration_seconds_bucket{le="0.004"} 401587
etcd_disk_backend_commit_duration_seconds_bucket{le="0.008"} 405979
etcd_disk_backend_commit_duration_seconds_bucket{le="0.016"} 406464
...
```
### 11.4 健康检查端点
| 端点 | 引入版本 | 语义 |
|---|---|---|
| `/health` | v3.3.0 起,`--listen-metrics-urls` 指定的位置**也会响应** | 标准健康检查。当标准端点配置了双向(客户端)TLS 认证,而负载均衡器或监控服务仍需访问健康检查时,这一设计很有用 |
| `/livez` | **v3.4 起** | 反映进程是否存活、或是否需要重启 |
| `/readyz` | **v3.4 起** | 反映进程是否就绪可对外提供服务 |
设计细节记录在 Kubernetes KEP(sig-etcd/4331-livez-readyz)中。
每个端点包含若干**独立的健康检查项**,可用 `verbose` 参数打印各检查项及其状态:
```bash
curl -k http://localhost:2379/readyz?verbose
```
响应类似:
```text
[+]data_corruption ok
[+]serializable_read ok
[+]linearizable_read ok
ok
```
HTTP API 也支持排除特定检查项:
```bash
curl -k http://localhost:2379/readyz?exclude=data_corruption
```
源文中出现的检查项名称共三个:
| 检查项 | 含义(按名称) |
|---|---|
| `data_corruption` | 数据损坏检查 |
| `serializable_read` | 可串行化读检查 |
| `linearizable_read` | 线性一致读检查 |
> 💡 补充:把 `/livez` 与 `/readyz` 分开用——存活探针用 `/livez`(避免 etcd 短暂不可读时被 K8s 误杀重启),就绪探针用 `/readyz`。排障时先打 `?verbose` 看是哪一项不通过,比反复看日志快得多。
### 11.5 Prometheus 抓取配置
运行 Prometheus 是摄取与记录 etcd 指标的最简单方式。安装(源文示例版本 `2.0.0`):
```sh
PROMETHEUS_VERSION="2.0.0"
wget https://github.com/prometheus/prometheus/releases/download/v$PROMETHEUS_VERSION/prometheus-$PROMETHEUS_VERSION.linux-amd64.tar.gz -O /tmp/prometheus-$PROMETHEUS_VERSION.linux-amd64.tar.gz
tar -xvzf /tmp/prometheus-$PROMETHEUS_VERSION.linux-amd64.tar.gz --directory /tmp/ --strip-components=1
/tmp/prometheus -version
```
scrape 配置(`scrape_interval: 10s`):
```yaml
global:
scrape_interval: 10s
scrape_configs:
- job_name: test-etcd
static_configs:
- targets: ['10.240.0.32:2379','10.240.0.33:2379','10.240.0.34:2379']
```
写入配置文件并启动:
```sh
cat > /tmp/test-etcd.yaml <> /tmp/test-etcd.log 2>&1 &
```
配置完成后 Prometheus 每 **10 秒**抓取一次 etcd 指标。
> ⚠️ 源文说明:以上为源文示例命令,Prometheus `2.0.0` 版本较旧,实际生产请使用当前版本并参考其官方文档的 flag 命名(如 `--config.file`)。本册不对源文命令做改写。
### 11.6 Alerting(告警)
etcd v3 集群有一组 [default alerts](https://github.com/etcd-io/etcd/tree/main/contrib/mixin)(Prometheus 用)。
**注意**:`job` label 可能需要按实际需求调整;这些规则是针对单一集群编写的,因此建议选择**集群唯一的 label**。
> ⚠️ 源文说明:本页**未在正文中给出任何具体 PromQL 告警表达式与阈值**,仅链接到 `contrib/mixin` 的外部规则集,故本册不列举表达式。
### 11.7 Grafana
Grafana 内置 Prometheus 支持,只需添加 Prometheus data source:
```text
Name: test-etcd
Type: Prometheus
Url: http://localhost:9090
Access: proxy
```
然后导入默认的 etcd dashboard 模板(`../grafana.json`)并按需定制。例如若 Prometheus data source 名为 `my-etcd`,则 JSON 中的 `datasource` 字段值也需改为 `my-etcd`。页面含一张示例 dashboard 截图 `../etcd-sample-grafana.png`。
### 11.8 分布式追踪(Distributed tracing)
v3.5 起 etcd 支持使用 [OpenTelemetry](https://github.com/open-telemetry) 做分布式追踪。**该特性仍为实验性,可能随时变更。**
启用方式:给 etcd server 传 `--experimental-enable-distributed-tracing=true`,并用 `--experimental-distributed-tracing-sampling-rate=` 选择**每百万 span 采集多少样本**,**默认采样率为 `0`**。
| flag | 可选性 | 默认值 | 说明 |
|---|---|---|---|
| `--experimental-distributed-tracing-address` | (可选) | `"localhost:4317"` | tracing collector 地址 |
| `--experimental-distributed-tracing-service-name` | (可选) | `"etcd"` | 分布式追踪服务名,**所有 etcd 实例必须相同** |
| `--experimental-distributed-tracing-instance-id` | (可选) | — | 实例 ID,虽可选但**强烈建议设置**,且**每个 etcd 实例必须唯一** |
启用前请确保已有 OpenTelemetry 端点;若地址与默认值不同,用 `--experimental-distributed-tracing-address` 覆盖。
**开销提示**:任何可观测性信号都有资源开销,据初始测量,该特性的 CPU 开销约在 **2% - 4%** 之间。
### 11.9 关键指标清单(来自 metrics/_index.md)
> 引自:[Metrics](https://etcd.io/docs/v3.7/metrics/)
#### 11.9.1 总览与命名空间规则
etcd 使用 [Prometheus](https://prometheus.io/) 上报指标,指标可用于实时监控与调试。**etcd 不持久化其指标:成员重启后指标会被重置。**
**命名规则**:遵循 Prometheus 建议的最佳实践。**指标名以 `etcd` 或 `etcd_debugging` 前缀作为命名空间(namespace),后跟子系统(subsystem)前缀**(例如 `wal`、`etcdserver`)。
- **`etcd` 命名空间下的指标用于监控与告警**,是稳定的高层指标;若这些指标发生任何变更,**会包含在 release notes 中**。
- etcd2 相关指标另见 v2 metrics guide。
#### 11.9.2 Server 指标(前缀 `etcd_server_`)
描述 etcd server 状态。**为了发现故障或排障,每个生产 etcd 集群都应密切监控这些 server 指标。**
| Name | Description | Type |
|---|---|---|
| has_leader | Whether or not a leader exists. 1 is existence, 0 is not. | Gauge |
| leader_changes_seen_total | The number of leader changes seen. | Counter |
| proposals_committed_total | The total number of consensus proposals committed. | Gauge |
| proposals_applied_total | The total number of consensus proposals applied. | Gauge |
| proposals_pending | The current number of pending proposals. | Gauge |
| proposals_failed_total | The total number of failed proposals seen. | Counter |
逐条含义:
| 指标 | 含义与告警解读 |
|---|---|
| `etcd_server_has_leader` | 指示该成员是否有 leader。**某成员没有 leader 则完全不可用;若集群所有成员都没有 leader,则整个集群完全不可用。** |
| `etcd_server_leader_changes_seen_total` | 自启动以来该成员看到的 leader 变更次数。**频繁的 leader 变更会显著影响 etcd 性能**,也说明 leader 不稳定,可能源于网络连通性问题或集群负载过重 |
| `etcd_server_proposals_committed_total` | 已提交的共识提案总数。集群健康时该 gauge 应随时间增长。**健康的多个成员在同一时刻的总提交数可能不同**,原因可能是启动后从 peer 恢复、落后于 leader,或本身是 leader 因而提交最多。**必须跨所有成员监控此指标;若单个成员与 leader 持续存在较大滞后,说明该成员缓慢或不健康。** |
| `etcd_server_proposals_applied_total` | 已应用的共识提案总数。etcd server **异步应用每个已提交提案**。`proposals_committed_total` 与 `proposals_applied_total` 的差值通常应很小(即使高负载下也在几千以内)。**若差值持续增大,说明 etcd server 过载**,可能发生在执行昂贵查询(如大范围 range 查询或大型 txn 操作)时 |
| `etcd_server_proposals_pending` | 排队等待提交的提案数。**pending 上升说明客户端负载高,或该成员无法提交提案。** |
| `etcd_server_proposals_failed_total` | 通常与两类问题相关:与 leader 选举相关的临时性失败,或因集群丧失 quorum 造成的较长停机 |
> 💡 补充:「committed 与 applied 差值即使高负载下也在几千以内」是源文给出的经验量级,可作为告警阈值的起点——超过这个量级就要怀疑 server 过载或存在大 range/txn 查询。
#### 11.9.3 Disk 指标(前缀 `etcd_disk_`)
| Name | Description | Type |
|---|---|---|
| wal_fsync_duration_seconds | The latency distributions of fsync called by wal | Histogram |
| backend_commit_duration_seconds | The latency distributions of commit called by backend. | Histogram |
- `etcd_disk_wal_fsync_duration_seconds`:etcd 在应用日志条目**之前**将其持久化到磁盘时调用 `wal_fsync`。
- `etcd_disk_backend_commit_duration_seconds`:etcd 把最近变更的增量快照提交到磁盘时调用 `backend_commit`。
- **高磁盘操作延迟(上述任一指标)通常指示磁盘问题**,可能导致高请求延迟或使集群不稳定。
#### 11.9.4 Network 指标(前缀 `etcd_network_`)
| Name | Description | Type |
|---|---|---|
| peer_sent_bytes_total | The total number of bytes sent to the peer with ID `To`. | Counter(To) |
| peer_received_bytes_total | The total number of bytes received from the peer with ID `From`. | Counter(From) |
| peer_sent_failures_total | The total number of send failures from the peer with ID `To`. | Counter(To) |
| peer_received_failures_total | The total number of receive failures from the peer with ID `From`. | Counter(From) |
| peer_round_trip_time_seconds | Round-Trip-Time histogram between peers. | Histogram(To) |
| client_grpc_sent_bytes_total | The total number of bytes sent to grpc clients. | Counter |
| client_grpc_received_bytes_total | The total number of bytes received to grpc clients. | Counter |
- `etcd_network_peer_sent_bytes_total`:发往特定 peer 的总字节数。**通常 leader 成员发送的数据多于其他成员**,因为它负责传输复制数据。
- `etcd_network_peer_received_bytes_total`:从特定 peer 接收的总字节数。**通常 follower 只从 leader 成员接收数据。**
#### 11.9.5 gRPC requests
这类指标通过 [go-grpc-prometheus](https://github.com/grpc-ecosystem/go-grpc-prometheus) 暴露。
> ⚠️ 源文说明:**源文在该小节下未列出具体指标名称与表格。** 实际可观测的 gRPC 指标(如 `grpc_server_*` 四条)见第 12 章指标全表。
#### 11.9.6 `etcd_debugging` 命名空间指标
`etcd_debugging` 前缀下的指标**用于调试**,高度依赖具体实现且**易变**,**可能在新 etcd 版本中不做任何警告地被修改或移除**。其中部分指标在变得更稳定后**可能被移到 `etcd` 前缀下**。
**Snapshot 类**:
| Name | Description | Type |
|---|---|---|
| snapshot_save_total_duration_seconds | The total latency distributions of save called by snapshot | Histogram |
即 `etcd_debugging_snapshot_save_total_duration_seconds`:**异常偏高的快照耗时指示磁盘问题,并可能导致集群不稳定。**
#### 11.9.7 Prometheus 供给的指标(Prometheus supplied metrics)
Prometheus client library 在 `go` 与 `process` 命名空间下提供若干指标,其中两项尤受关注:
| Name | Description | Type |
|---|---|---|
| process_open_fds | Number of open file descriptors. | Gauge |
| process_max_fds | Maximum number of open file descriptors. | Gauge |
**注意**:`process_open_fds`、`process_max_fds` 这类 process 指标**目前在 Darwin(macOS)系统上不受支持**。
**文件描述符(`process_open_fds`)占用过高**(即接近进程的 fd 上限 `process_max_fds`)意味着存在 fd 耗尽风险;**一旦 fd 耗尽,etcd 可能因无法创建新的 WAL 文件而 panic。**
#### 11.9.8 指标生成清单
源文末尾仅有 "Generated list of metrics" 小节标题,**正文未提供生成的指标清单内容**(同目录下存在 `etcd-metrics-latest.txt`)。
> 💡 补充:本册第 12 章正是补全了这一缺口——按 `etcd-metrics-latest` 页面给出全部 123 条指标。
### 11.10 监控落地建议(值班视角)
| 优先级 | 监控对象 | 指标 | 说明 |
|---|---|---|---|
| P0 | 是否有 leader | `etcd_server_has_leader` | 任何一个成员为 0 即需立即处理 |
| P0 | 磁盘 fsync/commit 延迟 | `etcd_disk_wal_fsync_duration_seconds`、`etcd_disk_backend_commit_duration_seconds` | 延迟高 = 磁盘问题,会引起请求延迟与集群不稳 |
| P0 | fd 水位 | `process_open_fds` / `process_max_fds` | 接近上限会导致无法创建 WAL 而 panic |
| P1 | 提交/应用落差 | `etcd_server_proposals_committed_total` - `etcd_server_proposals_applied_total` | 差值持续增大说明 server 过载 |
| P1 | leader 抖动 | `etcd_server_leader_changes_seen_total` | 频繁变更显著影响性能 |
| P1 | pending 堆积 | `etcd_server_proposals_pending` | 客户端负载高或成员无法提交 |
| P2 | peer 往返时延 | `etcd_network_peer_round_trip_time_seconds` | 跨机房/网络质量问题定位 |
| P2 | 快照耗时 | `etcd_debugging_snapshot_save_total_duration_seconds` | 偏高指示磁盘问题 |
> 💡 补充:以上优先级与阈值口径为本册基于源文「含义描述」整理的建议,**源文 monitoring 页未给出具体 PromQL 表达式与阈值**(见 11.6)。落地时请以 `contrib/mixin` 的默认告警规则为准。
### 11.11 日志约定(zap 级别)
etcd 使用 **zap** 库(https://github.com/uber-go/zap)输出应用日志,日志按 *levels*(级别)分类。日志级别决定你在告警里能看到什么,因此放进监控章节:
| 级别 | 语义 | 源文示例 | 告警含义 |
|---|---|---|---|
| **DebugLevel** | 日志量通常很大,生产环境一般禁用 | 向远端 peer 发送一条普通消息;向磁盘写一条日志条目 | 只在排障时临时开启(`--log-level=debug` 会**导致性能下降与日志冗长**) |
| **InfoLevel** | **默认**的日志优先级 | 启动配置;开始做快照;向集群加入新节点;向 auth 子系统加入新用户 | 正常信息,无需告警 |
| **WarnLevel** | 比 Info 更重要,但不需要人工逐条介入 review | 向远端 peer 发送 Raft 消息失败;在配置的 election timeout 内未收到心跳消息 | **值得告警**:往往是抖动/网络问题的前兆 |
| **ErrorLevel** | 高优先级。若应用运行平稳,**不应该**产生任何 error 级日志 | 为 WAL 分配磁盘空间失败 | **必须告警** |
| **PanicLevel** | 记录一条消息后 panic | Raft 消息编码失败 | **必须告警**(进程将退出) |
| **FatalLevel** | 记录一条消息后调用 `os.Exit(1)` | Raft 快照保存失败 | **必须告警**(进程将退出) |
> 💡 补充:把告警阈值定在 Warn 及以上,并把 Error/Panic/Fatal 设为立即告警——源文对 Error 的描述是「应用运行平稳时**不应该**产生任何 error 级日志」,这本身就是最好的告警基线。完整约定见 16.2 节。
---
## 12. 指标全表(123 条)
> 引自:[etcd Metrics(latest)](https://etcd.io/docs/v3.7/metrics/etcd-metrics-latest/)(线上生成页,源文件名 `metrics-latest.txt`)
本章按命名空间分组,完整收录 **123 条**指标。列含义:
- **指标名**:Prometheus 暴露的原始名称,原样保留;
- **类型**:`gauge` / `counter` / `histogram` / `summary`;
- **中文说明**:官方描述的中译;
- **标签**:维度标签,**`-` 表示该指标无维度标签**;标签值沿用页面给出的示例值。
### 12.0 分组计数汇总(自检用)
| 命名空间 | 条数 |
|---|---|
| `etcd_debugging_*` | 35 |
| `go_*` | 28 |
| `etcd_server_*` | 21 |
| `etcd_mvcc_*` | 9 |
| `process_*` | 7 |
| `etcd_network_*` | 6 |
| `etcd_disk_*` | 5 |
| `grpc_server_*` | 4 |
| `etcd_snap_*` | 3 |
| `os_*` | 2 |
| `promhttp_*` | 2 |
| `etcd_cluster_*` | 1 |
| **合计** | **123** |
> 💡 补充(三点使用提醒):
> 1. 任务描述称 `metrics-latest.txt` 为「124 行指标表」,但实际有效指标行为 **123** 行,本册按实际内容全部收录,未补写任何不存在的指标。
> 2. **不存在 `etcd_raft_*` 命名空间**——v3.7 latest 指标表中没有该组,raft 相关状态请用 `etcd_server_has_leader`、`etcd_server_leader_changes_seen_total`、`etcd_server_proposals_*` 等代替。
> 3. `etcd_debugging_*` 占比最大(35/123),且与 `etcd_mvcc_*`、`etcd_disk_*`、`etcd_snap_*` 存在**语义重叠**(如 `etcd_debugging_mvcc_db_total_size_in_bytes` 与 `etcd_mvcc_db_total_size_in_bytes`)。做告警时优先用非 debugging 命名空间,debugging 组留给排障下钻。
### 12.1 `etcd_cluster_*`(1 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| etcd_cluster_version | gauge | 正在运行的版本。对带有当前集群版本的 `cluster_version` 标签取值为 1 | cluster_version="3.5" |
### 12.2 `etcd_debugging_*`(35 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| etcd_debugging_auth_revision | gauge | auth store 当前的 revision | - |
| etcd_debugging_disk_backend_commit_rebalance_duration_seconds | histogram | bboltdb backend 调用 commit.rebalance 的延迟分布 | le="0.001" |
| etcd_debugging_disk_backend_commit_spill_duration_seconds | histogram | bboltdb backend 调用 commit.spill 的延迟分布 | le="0.001" |
| etcd_debugging_disk_backend_commit_write_duration_seconds | histogram | bboltdb backend 调用 commit.write 的延迟分布 | le="0.001" |
| etcd_debugging_lease_granted_total | counter | 已授予的 lease 总数 | - |
| etcd_debugging_lease_renewed_total | counter | leader 观测到的续约 lease 数量 | - |
| etcd_debugging_lease_revoked_total | counter | 已撤销的 lease 总数 | - |
| etcd_debugging_lease_ttl_total | histogram | lease TTL 的分桶直方图 | le="1" |
| etcd_debugging_mvcc_compact_revision | gauge | store 中最近一次压缩的 revision | - |
| etcd_debugging_mvcc_current_revision | gauge | store 当前的 revision | - |
| etcd_debugging_mvcc_db_compaction_keys_total | counter | 已压缩的 db key 总数 | - |
| etcd_debugging_mvcc_db_compaction_last | gauge | 上一次 db 压缩的 unix 时间,启动时重置为 0 | - |
| etcd_debugging_mvcc_db_compaction_pause_duration_milliseconds | histogram | db 压缩暂停时长的分桶直方图 | le="1" |
| etcd_debugging_mvcc_db_compaction_total_duration_milliseconds | histogram | db 压缩总时长的分桶直方图 | le="100" |
| etcd_debugging_mvcc_db_total_size_in_bytes | gauge | 底层数据库物理分配的总大小(字节) | - |
| etcd_debugging_mvcc_delete_total | counter | 本成员观测到的 delete 总数 | - |
| etcd_debugging_mvcc_events_total | counter | 本成员发送的事件总数 | - |
| etcd_debugging_mvcc_index_compaction_pause_duration_milliseconds | histogram | 索引压缩暂停时长的分桶直方图 | le="0.5" |
| etcd_debugging_mvcc_keys_total | gauge | key 总数 | - |
| etcd_debugging_mvcc_pending_events_total | gauge | 待发送的事件总数 | - |
| etcd_debugging_mvcc_put_total | counter | 本成员观测到的 put 总数 | - |
| etcd_debugging_mvcc_range_total | counter | 本成员观测到的 range 总数 | - |
| etcd_debugging_mvcc_slow_watcher_total | gauge | 未同步的慢 watcher 总数 | - |
| etcd_debugging_mvcc_total_put_size_in_bytes | gauge | 本成员观测到的 put kv 对总大小 | - |
| etcd_debugging_mvcc_txn_total | counter | 本成员观测到的 txn 总数 | - |
| etcd_debugging_mvcc_watch_stream_total | gauge | watch stream 总数 | - |
| etcd_debugging_mvcc_watcher_total | gauge | watcher 总数 | - |
| etcd_debugging_server_lease_expired_total | counter | 已过期的 lease 总数 | - |
| etcd_debugging_snap_save_marshalling_duration_seconds | histogram | snapshot 调用 save 时的 marshalling 开销分布 | le="0.001" |
| etcd_debugging_snap_save_total_duration_seconds | histogram | snapshot 调用 save 的总延迟分布 | le="0.001" |
| etcd_debugging_store_expires_total | counter | 过期的 key 总数 | - |
| etcd_debugging_store_reads_total | counter | 本成员本地读操作(get/getRecursive)总数,local to this member | action="getRecursive" |
| etcd_debugging_store_watch_requests_total | counter | 进入的 watch 请求总数(新建或重连) | - |
| etcd_debugging_store_watchers | gauge | 当前活跃的 watcher 数 | - |
| etcd_debugging_store_writes_total | counter | 本成员观测到的写操作(如 set/compareAndDelete)总数 | action="create" |
### 12.3 `etcd_disk_*`(5 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| etcd_disk_backend_commit_duration_seconds | histogram | backend 调用 commit 的延迟分布 | le="0.001" |
| etcd_disk_backend_defrag_duration_seconds | histogram | backend 碎片整理(defragmentation)的延迟分布 | le="0.1" |
| etcd_disk_backend_snapshot_duration_seconds | histogram | backend 快照的延迟分布 | le="0.01" |
| etcd_disk_wal_fsync_duration_seconds | histogram | WAL 调用 fsync 的延迟分布 | le="0.001" |
| etcd_disk_wal_write_bytes_total | gauge | 写入 WAL 的总字节数 | - |
> 💡 补充:这一组是排查「慢磁盘」类故障的第一现场——`etcd_disk_wal_fsync_duration_seconds` 的 p99 是 FAQ 中明确给出的阈值对象(< 10ms,见第 17 章)。
### 12.4 `etcd_mvcc_*`(9 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| etcd_mvcc_db_open_read_transactions | gauge | 当前打开的读事务数 | - |
| etcd_mvcc_db_total_size_in_bytes | gauge | 底层数据库物理分配的总大小(字节) | - |
| etcd_mvcc_db_total_size_in_use_in_bytes | gauge | 底层数据库逻辑上正在使用的总大小(字节) | - |
| etcd_mvcc_delete_total | counter | 本成员观测到的 delete 总数 | - |
| etcd_mvcc_hash_duration_seconds | histogram | storage hash 操作的延迟分布 | le="0.01" |
| etcd_mvcc_hash_rev_duration_seconds | histogram | storage 按 revision 做 hash 操作的延迟分布 | le="0.01" |
| etcd_mvcc_put_total | counter | 本成员观测到的 put 总数 | - |
| etcd_mvcc_range_total | counter | 本成员观测到的 range 总数 | - |
| etcd_mvcc_txn_total | counter | 本成员观测到的 txn 总数 | - |
### 12.5 `etcd_network_*`(6 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| etcd_network_active_peers | gauge | 当前活跃的 peer 连接数 | Local="LOCAL_NODE_ID",Remote="REMOTE_PEER_NODE_ID" |
| etcd_network_client_grpc_received_bytes_total | counter | 从 grpc 客户端接收的总字节数 | - |
| etcd_network_client_grpc_sent_bytes_total | counter | 发送给 grpc 客户端的总字节数 | - |
| etcd_network_peer_received_bytes_total | counter | 从 peer 接收的总字节数 | From="REMOTE_PEER_NODE_ID" |
| etcd_network_peer_round_trip_time_seconds | histogram | peer 之间的往返时间(RTT)直方图 | To="REMOTE_PEER_NODE_ID",le="+Inf" |
| etcd_network_peer_sent_bytes_total | counter | 发送给 peer 的总字节数 | To="REMOTE_PEER_NODE_ID" |
### 12.6 `etcd_server_*`(21 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| etcd_server_client_requests_total | counter | 按客户端版本统计的客户端请求总数 | client_api_version="3.5",type="stream" |
| etcd_server_go_version | gauge | 服务端运行所用的 Go 版本。对带当前版本的 `server_go_version` 标签取值为 1 | server_go_version="go1.14.3" |
| etcd_server_has_leader | gauge | 是否存在 leader。1 表示存在,0 表示不存在 | - |
| etcd_server_health_failures | counter | 健康检查失败的总次数 | - |
| etcd_server_health_success | counter | 健康检查成功的总次数 | - |
| etcd_server_heartbeat_send_failures_total | counter | leader 心跳发送失败总次数(通常是慢磁盘导致过载) | - |
| etcd_server_id | gauge | 服务/成员 ID(十六进制格式)。对带当前 ID 的 `server_id` 标签取值为 1 | server_id="451af08594378132" |
| etcd_server_is_leader | gauge | 本成员是否为 leader。1 是,0 否 | - |
| etcd_server_is_learner | gauge | 本成员是否为 learner。1 是,0 否 | - |
| etcd_server_leader_changes_seen_total | counter | 观测到的 leader 变更次数 | - |
| etcd_server_learner_promote_successes | counter | 本成员为 leader 时,learner 提升成功的总次数 | - |
| etcd_server_proposals_applied_total | gauge | 已应用的共识提案总数 | - |
| etcd_server_proposals_committed_total | gauge | 已提交的共识提案总数 | - |
| etcd_server_proposals_failed_total | counter | 观测到的失败提案总数 | - |
| etcd_server_proposals_pending | gauge | 当前待提交的提案数 | - |
| etcd_server_quota_backend_bytes | gauge | 当前 backend 存储配额大小(字节) | - |
| etcd_server_read_indexes_failed_total | counter | 失败的 read index 总数 | - |
| etcd_server_slow_apply_total | counter | 慢 apply 请求总数(通常是慢磁盘导致过载) | - |
| etcd_server_slow_read_indexes_total | counter | 与 leader 不同步或已超时的 pending read index 请求总数 | - |
| etcd_server_snapshot_apply_in_progress_total | gauge | 服务端正在应用 incoming snapshot 时为 1,否则为 0 | - |
| etcd_server_version | gauge | 正在运行的版本。对带当前版本的 `server_version` 标签取值为 1 | server_version="3.5.0-pre" |
### 12.7 `etcd_snap_*`(3 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| etcd_snap_db_fsync_duration_seconds | histogram | fsync `.snap.db` 文件的延迟分布 | le="0.001" |
| etcd_snap_db_save_total_duration_seconds | histogram | v3 snapshot save 的总延迟分布 | le="0.1" |
| etcd_snap_fsync_duration_seconds | histogram | snap 调用 fsync 的延迟分布 | le="0.001" |
### 12.8 `grpc_server_*`(4 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| grpc_server_handled_total | counter | 服务端已完成的 RPC 总数,无论成功或失败 | grpc_code="CODE",grpc_method="Alarm",grpc_service="etcdserverpb.Maintenance",grpc_type="unary" |
| grpc_server_msg_received_total | counter | 服务端接收的 RPC 流消息总数 | grpc_method="Alarm",grpc_service="etcdserverpb.Maintenance",grpc_type="unary" |
| grpc_server_msg_sent_total | counter | 服务端发送的 gRPC 流消息总数 | grpc_method="Alarm",grpc_service="etcdserverpb.Maintenance",grpc_type="unary" |
| grpc_server_started_total | counter | 服务端启动的 RPC 总数 | grpc_method="Alarm",grpc_service="etcdserverpb.Maintenance",grpc_type="unary" |
### 12.9 `go_*`(28 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| go_gc_duration_seconds | summary | GC 调用耗时的摘要 | quantile="0" |
| go_goroutines | gauge | 当前存在的 goroutine 数 | - |
| go_info | gauge | Go 环境信息 | version="go1.14.3" |
| go_memstats_alloc_bytes | gauge | 已分配且仍在使用的字节数 | - |
| go_memstats_alloc_bytes_total | counter | 已分配的字节总数,即使已被释放 | - |
| go_memstats_buck_hash_sys_bytes | gauge | profiling bucket hash 表占用的字节数 | - |
| go_memstats_frees_total | counter | free 总次数 | - |
| go_memstats_gc_cpu_fraction | gauge | 程序启动以来 GC 占用可用 CPU 时间的比例 | - |
| go_memstats_gc_sys_bytes | gauge | GC 系统元数据占用的字节数 | - |
| go_memstats_heap_alloc_bytes | gauge | 已分配且仍在使用的堆字节数 | - |
| go_memstats_heap_idle_bytes | gauge | 等待被使用的堆字节数 | - |
| go_memstats_heap_inuse_bytes | gauge | 正在使用的堆字节数 | - |
| go_memstats_heap_objects | gauge | 已分配的对象数 | - |
| go_memstats_heap_released_bytes | gauge | 已归还给操作系统的堆字节数 | - |
| go_memstats_heap_sys_bytes | gauge | 从系统获取的堆字节数 | - |
| go_memstats_last_gc_time_seconds | gauge | 上次 GC 距 1970 年的秒数 | - |
| go_memstats_lookups_total | counter | 指针查找总次数 | - |
| go_memstats_mallocs_total | counter | malloc 总次数 | - |
| go_memstats_mcache_inuse_bytes | gauge | mcache 结构正在使用的字节数 | - |
| go_memstats_mcache_sys_bytes | gauge | 从系统获取的用于 mcache 结构的字节数 | - |
| go_memstats_mspan_inuse_bytes | gauge | mspan 结构正在使用的字节数 | - |
| go_memstats_mspan_sys_bytes | gauge | 从系统获取的用于 mspan 结构的字节数 | - |
| go_memstats_next_gc_bytes | gauge | 下次 GC 触发时的堆字节数 | - |
| go_memstats_other_sys_bytes | gauge | 其他系统分配占用的字节数 | - |
| go_memstats_stack_inuse_bytes | gauge | 栈分配器正在使用的字节数 | - |
| go_memstats_stack_sys_bytes | gauge | 从系统获取的用于栈分配器的字节数 | - |
| go_memstats_sys_bytes | gauge | 从系统获取的字节数 | - |
| go_threads | gauge | 已创建的 OS 线程数 | - |
### 12.10 `os_*`(2 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| os_fd_limit | gauge | 文件描述符上限 | - |
| os_fd_used | gauge | 已使用的文件描述符数 | - |
### 12.11 `process_*`(7 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| process_cpu_seconds_total | counter | 用户态与内核态 CPU 耗时总秒数 | - |
| process_max_fds | gauge | 最大可打开文件描述符数 | - |
| process_open_fds | gauge | 已打开的文件描述符数 | - |
| process_resident_memory_bytes | gauge | 常驻内存大小(字节) | - |
| process_start_time_seconds | gauge | 进程启动时间,unix epoch 秒 | - |
| process_virtual_memory_bytes | gauge | 虚拟内存大小(字节) | - |
| process_virtual_memory_max_bytes | gauge | 可用虚拟内存上限(字节) | - |
### 12.12 `promhttp_*`(2 条)
| 指标名 | 类型 | 中文说明 | 标签 |
|---|---|---|---|
| promhttp_metric_handler_requests_in_flight | gauge | 当前正在处理的 scrape 数 | - |
| promhttp_metric_handler_requests_total | counter | 按 HTTP 状态码统计的 scrape 总数 | code="200" |
### 12.13 值班常用指标速查(非官方,基于上表的组合建议)
> 💡 补充:以下组合为本册基于 123 条指标给出的排障入口,非官方文档原文。
| 症状 | 优先看的指标 |
|---|---|
| 集群没有 leader / 频繁切主 | `etcd_server_has_leader`、`etcd_server_leader_changes_seen_total`、`etcd_server_is_leader` |
| 写入变慢 | `etcd_disk_wal_fsync_duration_seconds`、`etcd_disk_backend_commit_duration_seconds`、`etcd_server_slow_apply_total` |
| 心跳超时告警 | `etcd_server_heartbeat_send_failures_total`、`etcd_network_peer_round_trip_time_seconds` |
| 空间将满 | `etcd_mvcc_db_total_size_in_bytes` vs `etcd_server_quota_backend_bytes` |
| 碎片严重 | `etcd_mvcc_db_total_size_in_bytes`(物理)与 `etcd_mvcc_db_total_size_in_use_in_bytes`(逻辑)差值 |
| watcher 积压 | `etcd_debugging_mvcc_slow_watcher_total`、`etcd_debugging_mvcc_pending_events_total` |
| 压缩是否推进 | `etcd_debugging_mvcc_compact_revision` vs `etcd_debugging_mvcc_current_revision` |
| fd 是否将耗尽 | `os_fd_used` / `os_fd_limit`、`process_open_fds` / `process_max_fds` |
| 版本盘点 | `etcd_server_version`、`etcd_cluster_version`、`etcd_server_go_version` |
| learner 状态 | `etcd_server_is_learner`、`etcd_server_learner_promote_successes` |
---
## 13. 代理层:etcd gateway 与 gRPC proxy
> 引自:[etcd gateway](https://etcd.io/docs/v3.7/op-guide/gateway/)、[gRPC proxy](https://etcd.io/docs/v3.7/op-guide/grpc_proxy/)
### 13.1 定位差异总览
| 维度 | etcd gateway | gRPC proxy |
|---|---|---|
| 工作层次 | **TCP 层** | **gRPC 层(L7)** |
| 角色 | 简单的 TCP 代理,把网络数据转发到 etcd 集群 | 无状态 etcd 反向代理 |
| 状态 | 无状态且**透明**:不检查客户端请求、不干预集群响应 | 无状态,但会理解并改写请求语义 |
| TLS | **不终止 TLS**、不代客户端做 TLS 握手、不校验连接是否安全 | **可终止 TLS**,对外提供未加密本地 endpoint |
| endpoint 选择 | 支持多个 endpoint,**简单轮询(round-robin)** | 启动时**随机挑选**一个 endpoint,故障时才切换 |
| 故障处理 | 只路由到可用 endpoint,对客户端隐藏故障 | 检测到 endpoint 故障则切到其他可用 endpoint |
| 缓存 / 合并 | 无缓存、无 watch 合并、无批处理 | 合并 watch 与 lease、缓存 key range 请求 |
| 主要目的 | 提供**稳定的本地 endpoint**,让集群 endpoint 变化自动传播 | **降低核心集群负载**、提升可扩展性 |
| 未来演进 | 可能支持加权轮询等重试策略 | 可能支持加权轮询等策略 |
#### etcd gateway 的「何时用 / 何时不用」
**何时使用**:每个访问 etcd 的应用都必须先知道 etcd 集群的 client endpoint 地址;同机多个应用访问同一集群时都要各自维护该列表,集群重新配置后还要逐一更新,既繁琐又易错。gateway 作为**稳定的本地 endpoint** 解决这个问题:典型配置是每台机器跑一个 gateway 监听本地地址,本机所有 etcd 应用连本地 gateway,集群变更时只需更新 gateway 的 endpoints。
总结:**为了让集群 endpoint 变化自动传播,应在每台运行多个访问同一 etcd 集群应用的机器上部署 etcd gateway。**
**何时不要用**:
- **提升性能**:gateway 不是为提升集群性能设计的,不提供缓存、watch 合并(coalescing)或批处理;etcd 团队另有面向集群可扩展性的缓存代理(即 gRPC proxy)在开发中。
- **运行在集群管理系统上**:Kubernetes 这类高级集群管理系统原生支持服务发现,应用可用 DNS 名或系统托管的虚拟 IP 访问 etcd;**kube-proxy 就等价于 etcd gateway**。
### 13.2 etcd gateway
#### 启动命令(静态 endpoint)
集群静态 endpoint 示例:infra0 = 10.0.1.10 / infra0.example.com:2379,infra1 = 10.0.1.11 / infra1.example.com:2379,infra2 = 10.0.1.12 / infra2.example.com:2379。
```bash
$ etcd gateway start --endpoints=infra0.example.com:2379,infra1.example.com:2379,infra2.example.com:2379
2016-08-16 11:21:18.867350 I | tcpproxy: ready to proxy client requests to [...]
```
#### 启动命令(DNS SRV 发现)
用 DNS SRV 做服务发现时,先有 SRV 记录:
```bash
$ dig +noall +answer SRV _etcd-client._tcp.example.com
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra0.example.com.
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra1.example.com.
_etcd-client._tcp.example.com. 300 IN SRV 0 0 2379 infra2.example.com.
$ dig +noall +answer infra0.example.com infra1.example.com infra2.example.com
infra0.example.com. 300 IN A 10.0.1.10
infra1.example.com. 300 IN A 10.0.1.11
infra2.example.com. 300 IN A 10.0.1.12
```
再从 SRV 记录拉取 endpoints:
```bash
$ etcd gateway start --discovery-srv=example.com
2016-08-16 11:21:18.867350 I | tcpproxy: ready to proxy client requests to [...]
```
#### 全部 flag
**etcd cluster 类**
| flag | 说明 | 默认值 / 注意 |
|---|---|---|
| `--endpoints` | 转发客户端连接的 etcd server 目标,逗号分隔列表 | 默认 `127.0.0.1:2379`;**必须带端口**;非法示例 `https://127.0.0.1:2379`(gateway 不终止 TLS)。gateway 不校验 HTTP scheme 也不检查请求,只做转发 |
| `--discovery-srv` | 通过 SRV 记录引导集群 endpoint 的 DNS 域名 | 默认未设置 |
**Network 类**
| flag | 说明 | 默认值 / 注意 |
|---|---|---|
| `--listen-addr` | 接受客户端请求所绑定的接口与端口 | 默认 `127.0.0.1:23790` |
| `--retry-delay` | 重连失败 endpoint 前的延迟时长 | 默认 `1m0s`;非法示例 `"123"`(需要带时间单位) |
**Security 类**
| flag | 说明 | 默认值 / 注意 |
|---|---|---|
| `--insecure-discovery` | 接受不安全或易受中间人攻击的 SRV 记录 | 默认 `false` |
| `--trusted-ca-file` | 客户端 TLS CA 文件路径,用于校验 SRV 发现返回的 endpoint。**仅**用于认证被发现的 endpoint,不用于创建数据传输连接;gateway 永不终止 TLS,也不代客户端建立 TLS 连接 | 默认未设置 |
### 13.3 gRPC proxy
#### 概述与 endpoint 选择
gRPC proxy 是运行在 gRPC 层(L7)的**无状态 etcd 反向代理**,用于降低核心 etcd 集群的总处理负载:为水平扩展合并 watch 与 lease API 请求;为防止滥用客户端拖垮集群,缓存 key range 请求。
支持多个 etcd server endpoint:启动时**随机挑选**一个 endpoint,该 endpoint 服务所有请求,直到 proxy 检测到 endpoint 故障;此时若还有其他 endpoint 可用则切换过去,对客户端隐藏故障。
#### 可扩展 watch API(watch 聚合)
proxy 把同一 key 或 range 上的多个客户端 watcher(`c-watcher`)合并为连接到 etcd server 的单个 watcher(`s-watcher`),再把 `s-watcher` 的所有事件广播给各个 `c-watcher`。
**N 个客户端 watch 同一 key 时,一个 gRPC proxy 可把 etcd server 上的 watch 负载从 N 降到 1**;可部署多个 proxy 进一步分摊。
**限制(一致性注意)**:为合并 watcher,proxy 会尽可能把新 `c-watcher` 并入已有的 `s-watcher`,而这个 `s-watcher` 可能因网络延迟或未投递的缓冲事件而与 etcd server 不同步。当 **watch revision 未指定**时,proxy 不保证 `c-watcher` 从最新的 store revision 开始 watch:例如客户端直连 etcd server 从 revision 1000 开始,而**经 proxy 可能从 revision 990 开始**。取消操作有类似限制:watcher 被取消时,etcd server 的 revision 可能大于取消响应的 revision。文档指出这两点对多数场景无碍,将来可能提供让 watcher 绕过 proxy 以获得更精确 revision 的选项。
#### 可扩展 lease API(lease 合并)
客户端要维持 lease 必须至少建立一条到 etcd server 的 gRPC 流用于周期性心跳;大量客户端的重度 lease 活动会造成过多流与 CPU 消耗。proxy 支持 **lease stream 合并**:N 个客户端更新 lease 时,单个 proxy 把 server 上的流负载从 N 降到 1,可部署多个 proxy 进一步分摊。proxy 把各 `c-stream` 的客户端 lease 心跳转发到 `s-stream`,再把响应回给对应的 `c-stream`。
#### 防滥用客户端
proxy 会在**不破坏一致性要求**的前提下缓存请求响应,从而保护 etcd server 免受紧循环(tight for loop)客户端的冲击。
#### 启动与基本使用
静态 endpoint 示例:infra0 = 10.0.1.10 / infra0.example.com,infra1 = 10.0.1.11 / infra1.example.com,infra2 = 10.0.1.12 / infra2.example.com。
```bash
$ etcd grpc-proxy start --endpoints=infra0.example.com,infra1.example.com,infra2.example.com --listen-addr=127.0.0.1:2379
```
proxy 启动后监听 2379,把请求转发到上述三个 endpoint 之一。
```bash
$ ETCDCTL_API=3 etcdctl --endpoints=127.0.0.1:2379 put foo bar
OK
$ ETCDCTL_API=3 etcdctl --endpoints=127.0.0.1:2379 get foo
foo
bar
```
#### Client endpoint 同步与名字解析
proxy 支持把自己的 endpoint 注册到用户定义的 endpoint 上供发现,用途有二:一是让客户端在一组 proxy endpoint 之间同步自己的 endpoint 以实现高可用;二是作为 etcd gRPC naming 的 endpoint provider。
```bash
$ etcd grpc-proxy start --endpoints=localhost:2379 \
--listen-addr=127.0.0.1:23790 \
--advertise-client-url=127.0.0.1:23790 \
--resolver-prefix="___grpc_proxy_endpoint" \
--resolver-ttl=60
$ etcd grpc-proxy start --endpoints=localhost:2379 \
--listen-addr=127.0.0.1:23791 \
--advertise-client-url=127.0.0.1:23791 \
--resolver-prefix="___grpc_proxy_endpoint" \
--resolver-ttl=60
```
带 resolver prefix 时,`member list` 会列出所有成员(示例中两条记录的 CLIENT ADDRS 分别为 `127.0.0.1:23791` 与 `127.0.0.1:23790`,PEER ADDRS 为空,ID 均为 0):
```bash
ETCDCTL_API=3 etcdctl --endpoints=http://localhost:23790 member list --write-out table
```
客户端可通过 `Sync` 自动发现 proxy endpoint:
```go
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"http://localhost:23790"},
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// fetch registered grpc-proxy endpoints
if err := cli.Sync(context.Background()); err != nil {
log.Fatal(err)
}
```
若启动时**不带** resolver prefix:
```bash
$ etcd grpc-proxy start --endpoints=localhost:2379 \
--listen-addr=127.0.0.1:23792 \
--advertise-client-url=127.0.0.1:23792
```
则对 grpc-proxy 调用 member list API 只返回它自己的 `advertise-client-url`(示例为 `127.0.0.1:23792`)。
#### Namespacing(keyspace 隔离)
当应用期望对**整个 key space** 拥有完全控制权、而 etcd 集群又被其他应用共享时,可用 `--namespace` 让 proxy 对 keyspace 分区,使客户端看起来仍拥有完整 keyspace:所有进入 proxy 的客户端请求被翻译成带用户定义前缀的 key,访问 etcd 集群时挂上该前缀,proxy 返回响应时剥掉前缀,**客户端感知不到前缀**。
```bash
$ etcd grpc-proxy start --endpoints=localhost:2379 \
--listen-addr=127.0.0.1:23790 \
--namespace=my-prefix/
```
```bash
$ ETCDCTL_API=3 etcdctl --endpoints=localhost:23790 put my-key abc
# OK
$ ETCDCTL_API=3 etcdctl --endpoints=localhost:23790 get my-key
# my-key
# abc
$ ETCDCTL_API=3 etcdctl --endpoints=localhost:2379 get my-prefix/my-key
# my-prefix/my-key
# abc
```
> 💡 补充:namespace 是**多租户共享同一 etcd 集群**时成本最低的隔离手段,但它只做 key 前缀重写,**不提供鉴权隔离**——能直连后端集群的客户端仍可读到 `my-prefix/my-key`。真正的访问控制要靠 RBAC(第 10 章)。
#### TLS 终止
可由 gRPC proxy 终止来自安全 etcd 集群的 TLS,对外提供未加密的本地 endpoint。先启动一个 https 的单成员集群:
```bash
$ etcd --listen-client-urls https://localhost:2379 --advertise-client-urls https://localhost:2379 --cert-file=peer.crt --key-file=peer.key --trusted-ca-file=ca.crt --client-cert-auth
```
确认客户端端口是 https:
```bash
# fails
$ ETCDCTL_API=3 etcdctl --endpoints=http://localhost:2379 endpoint status
# works
$ ETCDCTL_API=3 etcdctl --endpoints=https://localhost:2379 --cert=client.crt --key=client.key --cacert=ca.crt endpoint status
```
用客户端证书连接 `https://localhost:2379`,在 `localhost:12379` 启动 proxy:
```bash
$ etcd grpc-proxy start --endpoints=https://localhost:2379 --listen-addr localhost:12379 --cert client.crt --key client.key --cacert=ca.crt --insecure-skip-tls-verify &
```
测试 TLS 终止(经 http 写入 proxy):
```bash
$ ETCDCTL_API=3 etcdctl --endpoints=http://localhost:12379 put abc def
# OK
```
#### Metrics 与健康检查
gRPC proxy 为 `--endpoints` 定义的 etcd 成员暴露 `/health` 与 Prometheus `/metrics`。也可以用 `--metrics-addr` 额外定义一个同时响应 `/metrics` 与 `/health` 的 URL:
```bash
$ etcd grpc-proxy start \
--endpoints https://localhost:2379 \
--metrics-addr https://0.0.0.0:4443 \
--listen-addr 127.0.0.1:23790 \
--key client.key \
--key-file proxy-server.key \
--cert client.crt \
--cert-file proxy-server.crt \
--cacert ca.pem \
--trusted-ca-file proxy-ca.pem
```
**已知问题**:proxy 主接口同时服务 HTTP2 与 HTTP/1.1。若按上例配置了 TLS,用 cURL 之类的客户端访问主接口取 `/metrics` 或 `/health` 时,必须显式指定 HTTP/1.1;使用 `--metrics-addr` 的次要接口则无此要求:
```bash
$ curl --cacert proxy-ca.pem --key proxy-client.key --cert proxy-client.crt https://127.0.0.1:23790/metrics --http1.1
```
#### gRPC proxy 全部 flag 汇总
`--endpoints`、`--listen-addr`、`--advertise-client-url`、`--resolver-prefix`、`--resolver-ttl`、`--namespace`、`--metrics-addr`、`--cert`、`--cert-file`、`--key`、`--key-file`、`--cacert`、`--trusted-ca-file`、`--insecure-skip-tls-verify`。
| flag | 作用 |
|---|---|
| `--endpoints` | 后端 etcd 集群 endpoint 列表 |
| `--listen-addr` | proxy 监听地址 |
| `--advertise-client-url` | 对外通告的 proxy 地址(用于注册与发现) |
| `--resolver-prefix` | 把 proxy endpoint 注册到该前缀下,供 gRPC naming 发现 |
| `--resolver-ttl` | 注册记录的 TTL(秒) |
| `--namespace` | keyspace 前缀隔离 |
| `--metrics-addr` | 独立的 `/metrics` 与 `/health` 监听地址 |
| `--cert` / `--key` / `--cacert` | 作为**客户端**连接后端 etcd 的证书 |
| `--cert-file` / `--key-file` / `--trusted-ca-file` | 作为**服务端**对外提供 TLS 的证书 |
| `--insecure-skip-tls-verify` | 跳过后端证书校验 |
### 13.4 选型建议
> 💡 补充(非官方原文,基于两页定位推导):
| 场景 | 建议 |
|---|---|
| 同机多应用访问同一 etcd,想少维护 endpoint 列表 | **etcd gateway**(本机 127.0.0.1:23790 + 轮询) |
| 已在 Kubernetes 内 | 两者都**不需要**,用 Service DNS 即可(kube-proxy 已等价 gateway) |
| watcher / lease 客户端极多,核心集群压力大 | **gRPC proxy**(watch N→1、lease stream N→1) |
| 需要给多租户做 keyspace 隔离 | **gRPC proxy** + `--namespace`(注意不替代 RBAC) |
| 需要把 https 集群暴露成明文本地端口 | **gRPC proxy**(TLS 终止);gateway 不终止 TLS |
| 对 watch 起始 revision 有强一致要求 | 慎用 gRPC proxy,或显式指定 watch revision;gateway 是透明转发不受影响 |
---
## 14. 升级与降级
> 引自:[Upgrading etcd clusters and applications](https://etcd.io/docs/v3.7/upgrades/upgrading-etcd/)、[Upgrade etcd from v3.5 to v3.6](https://etcd.io/docs/v3.7/upgrades/upgrade_3_6/)、[Upgrade etcd from v3.6 to v3.7](https://etcd.io/docs/v3.7/upgrades/upgrade_3_7/)、[Downgrading etcd clusters and applications](https://etcd.io/docs/v3.7/downgrades/downgrading-etcd/)、[Downgrade etcd from v3.7 to v3.6](https://etcd.io/docs/v3.7/downgrades/downgrade_3_7/)、[Downgrade etcd from v3.6 to v3.5](https://etcd.io/docs/v3.7/downgrades/downgrade_3_6/)
### 14.1 升级总纲与硬性规则
etcd 只支持以下两种升级场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| **Patch upgrade(补丁升级)** | 同一 minor 版本内的小版本升级 | `3.7.0 → 3.7.1` |
| **Minor upgrade(小版本升级)** | **一次只升一个 minor 版本**;跳 minor 版本的升级不被支持,且很可能失败 | `3.6 → 3.7` |
**在升到下一个 minor 版本之前,必须先更新到当前 minor 版本的最新 patch 版本。**
官方升级文档链接清单(v3.7 文档站):
| 页面 | 链接 |
|---|---|
| Upgrade etcd from 3.0 to 3.1 | `../upgrade_3_1/` |
| Upgrade etcd from 3.1 to 3.2 | `../upgrade_3_2/` |
| Upgrade etcd from 3.2 to 3.3 | `../upgrade_3_3/` |
| Upgrade etcd from 3.3 to 3.4 | `../upgrade_3_4/` |
| Upgrade etcd from 3.4 to 3.5 | `../upgrade_3_5/` |
| Upgrade etcd from 3.5 to v3.6 | `../upgrade_3_6/` |
| Upgrade etcd from v3.6 to v3.7 | `../upgrade_3_7/` |
| Upgrade a v2.3 cluster to v3.0 | `../upgrade_3_0/` |
> 范围声明:上表中 3.0–3.5 的五页属于历史版本,本册按用户指定「只做 v3.7 主线」不展开,见附录 B。
### 14.2 通用的服务端升级检查清单(适用于 3.5→3.6 与 3.6→3.7)
| 检查项 | 要求 |
|---|---|
| **Upgrade requirements** | 集群必须是健康的,升级前用 `etcdctl endpoint health` 检查;且需满足目标版本要求的最低版本(3.6 要求 ≥ v3.5;**3.7 要求 ≥ v3.6.11**)。**etcd 只支持一次升一个 minor 版本** |
| **Preparation** | 先在 staging 环境测试依赖 etcd 的服务,再推向生产;开始前[下载 snapshot 备份](https://etcd.io/docs/v3.7/op-guide/maintenance/#snapshot-backup)。注意 `snapshot` 命令只备份 v3 数据 |
| **Mixed versions** | 升级过程中支持 member 混合版本,并以**最低公共版本**的协议运行;只有**全部 member 都升到新版本**时集群才算完成升级;member 之间会互相协商确定整体集群版本,该版本决定对外报告的版本与可用特性 |
| **Rollback(回滚原则)** | 见下方要点 |
**Rollback 要点(升级场景)**:
1. 升级前先创建并下载 snapshot 备份,可用于把集群恢复到升级前状态。
2. 若升级中出问题,应先定位并解决根因。
3. 若集群仍处于**混合版本状态**(至少还有一个 member 是旧版本),可以把二进制/镜像换回旧版本,或直接用 snapshot 恢复集群。此时集群仍按旧版本集群运行,**可以回滚而无需走正式的降级流程**。
4. **一旦所有 member 都升到新版本,集群视为完全升级,用二进制回滚已不再可能**。此时唯一恢复手段是用升级前拍的 snapshot 恢复;若仍想回到原版本,应走官方降级指南以保证一致性、避免数据损坏。
### 14.3 v3.5 → v3.6 完整步骤
#### 总体说明
一般情况下,从 etcd v3.5 升到 v3.6 是一次**零停机滚动升级(zero-downtime, rolling upgrade)**:
- 逐个停止 etcd v3.5 进程,替换为 etcd v3.6 进程;
- 全部替换为 v3.6 进程后,v3.6 的新特性才对集群可用。
#### 升级前检查清单
**1)Update 3.5(重要 / warning)**
升级到 3.6 之前,必须确保所有 3.5 member 都已升级到 **3.5.32 或更高版本**。patch 版本 `3.5.24` 到 `3.5.26` 修复了若干潜在的升级阻塞问题;`3.5.32` 新增了 `--v2-deprecation=write-only-skip-check`,并把 `etcdutl check v2store` 扩展为可同时检查 WAL 记录和 v2 snapshot。
**2)V2 Store(info)**
- 若未配置 `--enable-v2` 或该 flag 为 false,**无需任何额外操作**。
- 若**已配置** `--enable-v2`,运行命令 `etcdutl check v2store`,确认 v2store 中是否含有非 membership 的自定义数据。没有自定义数据则可以安全移除该 flag;否则参考 [v2 migration guide](https://etcd.io/docs/v3.4/op-guide/v2-migration/)。
#### 升级流程(3 member v3.5 集群示例)
**Step 1:check upgrade requirements**
确认集群健康且运行 v3.5.x:
```bash
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 endpoint health
< Note:以下 diff 基于 v3.7.0-rc.0 与 v3.6.13。实际差异取决于你的 patch 版本,请先用
> `diff <(etcd-3.7/bin/etcd -h | grep \-\-) <(etcd-3.6/bin/etcd -h | grep \-\-)` 检查。
在 v3.7 中被移除的已废弃 `--experimental-*` flag 在 v3.6 中仍然存在,但**降级后不要再加回来**;应使用它们的非 experimental 等价 flag 或 `--feature-gates` 条目,这些写法在两个版本上都可用。
**Difference in Prometheus metrics(v3.6 中不存在的指标)**
```diff
# metrics not available in v3.6
-etcd_server_request_duration_seconds
-etcd_debugging_server_watch_send_loop_control_stream_duration_seconds
-etcd_debugging_server_watch_send_loop_progress_duration_seconds
-etcd_debugging_server_watch_send_loop_watch_stream_duration_seconds
-etcd_debugging_server_watch_send_loop_watch_stream_duration_per_event_seconds
```
#### 服务端降级检查清单
| 检查项 | 要求 |
|---|---|
| **Downgrade requirements** | 运行中的集群必须健康,降级前用 `etcdctl endpoint health` 检查 |
| **Preparation** | 先在 staging 环境测试依赖 etcd 的服务;开始前下载 snapshot 备份;并**下载最新的 etcd v3.6 发布版** |
| **Mixed versions** | 降级期间支持混合版本,按最低公共版本协议运行;**一旦通过 `etcdctl downgrade enable 3.6` 开启降级,集群即视为已降级**。内部把整体集群版本设为降级目标版本,该版本决定报告版本与可用特性 |
| **Rollback** | 见下方要点 |
**Rollback 要点(降级场景)**:
1. 降级前创建并下载 snapshot 备份,可恢复到降级前状态;出问题先定位并解决根因。
2. 若已执行 `etcdctl downgrade enable`、集群仍处混合版本状态(至少一个 member 还是 v3.7),可以运行 **`etcdctl downgrade cancel`** 取消进行中的降级,并用原来的 v3.7 二进制重启所有已降级的 member。
3. 一旦所有 member 都降到 v3.6,集群视为完全降级;此时若要回到原版本,应按官方 [upgrade guide](https://etcd.io/docs/v3.7/upgrades/upgrade_3_7/) 走升级流程,以保证一致性、避免数据损坏。
#### 降级流程(3 member v3.7 集群;输出取自 etcd v3.7.0-rc.0 与 etcd v3.6.13 的真实运行)
**Step 1:check downgrade requirements**
```bash
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 endpoint health
< Note:一旦开启降级,**即使所有 server 仍运行 v3.7 二进制,集群也会保持以 v3.6 协议运行**,除非用 `etcdctl downgrade cancel` 取消降级。
**Step 5:stop one existing etcd server**
停止 server 前先检查它是否是 leader,**建议最后再降级 leader**。若要停止的 server 是 leader,可以在停止前用 `move-leader` 把 leadership 移走,避免一些停机时间:
```bash
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 move-leader 729934363faa4a24
< Note:与 v3.5 不同,**v3.6 的 status endpoint 会报告降级信息**,因此已降级的 member 会继续显示 `DOWNGRADE ENABLED` 为 true,并保留其 storage version,直到降级完成。
**Step 7:repeat step 5 and step 6 for rest of the members**
全部 member 降级后,**降级自动完成,`DOWNGRADE ENABLED` 复位为 false**。检查集群健康与状态,确认所有 member 的 minor 版本与 storage version 均为 v3.6:
```bash
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 endpoint status -w=table
< Note:diff 基于 v3.6.0 与 v3.5.18;实际差异取决于 patch 版本,请先用
> `diff <(etcd-3.6/bin/etcd -h | grep \-\-) <(etcd-3.5/bin/etcd -h | grep \-\-)` 检查。
**(1)v3.5 中不存在的 flag(降级前必须删除):**
```diff
# flags not available in v3.5
-etcd --discovery-token ''
-etcd --discovery-endpoints ''
-etcd --discovery-dial-timeout '2s'
-etcd --discovery-request-timeout '5s'
-etcd --discovery-keepalive-time '2s'
-etcd --discovery-keepalive-timeout '6s'
-etcd --discovery-insecure-transport 'true'
-etcd --discovery-insecure-skip-tls-verify 'false'
-etcd --discovery-cert ''
-etcd --discovery-key ''
-etcd --discovery-cacert ''
-etcd --discovery-user ''
-etcd --discovery-password ''
-etcd --feature-gates
-etcd --log-format
```
**(2)同名但改名的 flag(改回 `--experimental-` 前缀):**
```diff
-etcd --bootstrap-defrag-threshold-megabytes +etcd --experimental-bootstrap-defrag-threshold-megabytes
-etcd --compaction-batch-limit +etcd --experimental-compaction-batch-limit
-etcd --compact-hash-check-time +etcd --experimental-compact-hash-check-time
-etcd --compaction-sleep-interval +etcd --experimental-compaction-sleep-interval
-etcd --corrupt-check-time +etcd --experimental-corrupt-check-time
-etcd --enable-distributed-tracing +etcd --experimental-enable-distributed-tracing
-etcd --distributed-tracing-address +etcd --experimental-distributed-tracing-address
-etcd --distributed-tracing-instance-id +etcd --experimental-distributed-tracing-instance-id
-etcd --distributed-tracing-sampling-rate +etcd --experimental-distributed-tracing-sampling-rate
-etcd --distributed-tracing-service-name +etcd --experimental-distributed-tracing-service-name
-etcd --downgrade-check-time +etcd --experimental-downgrade-check-time
-etcd --max-learners +etcd --experimental-max-learners
-etcd --memory-mlock +etcd --experimental-memory-mlock
-etcd --peer-skip-client-san-verification +etcd --experimental-peer-skip-client-san-verification
-etcd --snapshot-catchup-entries +etcd --experimental-snapshot-catchup-entries
-etcd --warning-apply-duration +etcd --experimental-warning-apply-duration
-etcd --warning-unary-request-duration +etcd --experimental-warning-unary-request-duration
-etcd --watch-progress-notify-interval +etcd --experimental-watch-progress-notify-interval
```
**(3)v3.6 feature gates 的等价 flag(改回 v3.5 写法):**
```diff
-etcd --feature-gates=CompactHashCheck=true +etcd --experimental-compact-hash-check-enabled=true
-etcd --feature-gates=InitialCorruptCheck=true +etcd --experimental-enable-initial-corrupt-check=true
-etcd --feature-gates=LeaseCheckpoint=true +etcd --experimental-enable-lease-checkpoint=true
-etcd --feature-gates=LeaseCheckpointPersist=true +etcd --experimental-enable-lease-checkpoint-persist=true
-etcd --feature-gates=StopGRPCServiceOnDefrag=true +etcd --experimental-stop-grpc-service-on-defrag=true
-etcd --feature-gates=TxnModeWriteWithSharedBuffer=false +etcd --experimental-txn-mode-write-with-shared-buffer=false
```
**(4)同一 flag 默认值不同(需改回 v3.5 默认值):**
```diff
-etcd --snapshot-count=10000 +etcd --snapshot-count=100000
-etcd --v2-deprecation='write-only' +etcd --v2-deprecation='not-yet'
-etcd --discovery-fallback='exit' +etcd --discovery-fallback='proxy'
```
**Difference in Prometheus metrics**
```diff
# metrics not available in v3.5
-etcd_network_known_peers
-etcd_server_feature_enabled
```
#### 服务端降级检查清单
| 检查项 | 要求 |
|---|---|
| **Downgrade requirements** | 运行中的集群必须健康,降级前用 `etcdctl endpoint health` 检查 |
| **Preparation** | 先在 staging 测试依赖 etcd 的服务;开始前下载 snapshot 备份以便出问题时回滚;并**下载最新的 etcd v3.5 发布版** |
| **Mixed versions** | 降级期间支持混合版本,按最低公共版本协议运行;**通过 `etcdctl downgrade enable 3.5` 开启降级后,集群即视为已降级**;内部把整体集群版本设为降级目标版本,决定报告版本与可用特性 |
| **Rollback** | 降级前创建并下载 snapshot 备份;出问题先定位并解决根因。若已执行 `etcdctl downgrade enable`、集群仍处混合版本状态(至少一个 member 还是 v3.6),可运行 **`etcdctl downgrade cancel`** 取消进行中的降级,并用原来的 v3.6 二进制重启所有已降级 member。**一旦所有 member 降到 v3.5,集群视为完全降级**;此时若要回到原版本,应按官方 upgrade guide 走升级流程,保证一致性、避免数据损坏 |
#### 降级流程(3 member v3.6 集群)
**Step 1:check downgrade requirements**
```bash
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 endpoint health
< Note:一旦开启降级,**即使所有 server 仍运行 v3.6 二进制,集群也会保持以 v3.5 协议运行**,除非用 `etcdctl downgrade cancel` 取消。
**Step 5:stop one existing etcd server**
停止 server 前先检查它是否是 leader,**建议最后再降级 leader**。先用 `endpoint status -w=table` 看 `IS LEADER` 列;若要停止的 server 是 leader,可先 `move-leader`:
```bash
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 move-leader 91bc3c398fb3c146
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 endpoint status -w=table
< 素材疑点:`downgrade_3_6.md` Step 6 的 diff 中旧行写作 `-etcd-3.6/bin --name s1 \`,**疑漏 `/etcd`**(应为 `etcd-3.6/bin/etcd`)。本册按原文保留。
验证 member 与集群在 v3.5 二进制下健康:
```bash
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 endpoint status -w=table
< Note:你会看到 v3.5 server 的 `DOWNGRADE ENABLED` 为 false,**因为 v3.5 的 status endpoint 未实现降级信息**;此时集群层面的降级仍然是开启状态。
**Step 7:repeat step 5 and step 6 for rest of the members**
全部 member 降级后,检查健康与状态,确认所有 member 的 minor 版本为 v3.5,且 **storage version 为空**:
```bash
etcdctl --endpoints=localhost:2379,localhost:22379,localhost:32379 endpoint status -w=table
< 素材注明「共 16 个」,但表格实际列出 18 行。本册按表格原文全部保留,未删行。
**v3.5 feature gate 的等价 flag 映射**
| v3.5 写法 | v3.6 等价写法 |
|---|---|
| `--experimental-compact-hash-check-enabled=true` | `--feature-gates=CompactHashCheck=true` |
| `--experimental-initial-corrupt-check=true` | `--feature-gates=InitialCorruptCheck=true` |
| `--experimental-enable-lease-checkpoint=true` | `--feature-gates=LeaseCheckpoint=true` |
| `--experimental-enable-lease-checkpoint-persist=true` | `--feature-gates=LeaseCheckpointPersist=true` |
| `--experimental-stop-grpc-service-on-defrag=true` | `--feature-gates=StopGRPCServiceOnDefrag=true` |
| `--experimental-txn-mode-write-with-shared-buffer=false` | `--feature-gates=TxnModeWriteWithSharedBuffer=false` |
**默认值发生变化的 flag(v3.5 → v3.6)**
| 原默认值 | v3.6 新默认值 |
|---|---|
| `--snapshot-count=100000` | `--snapshot-count=10000` |
| `--v2-deprecation='not-yet'` | `--v2-deprecation='write-only'` |
| `--discovery-fallback='proxy'` | `--discovery-fallback='exit'` |
**Prometheus metrics 差异(v3.6 新增)**
```diff
# metrics added in v3.6
+etcd_network_known_peers
+etcd_server_feature_enabled
```
#### v3.7 相对 v3.6 的变化
| 类别 | 内容 |
|---|---|
| Flags removed | 所有 23 个已废弃 `--experimental-*` flag(见 14.4 节 diff 清单,PR #19959) |
| Flags added | **None** |
| Flags with new defaults | **None** |
| 破坏性变更 | v2 store 完全移除(v2 HTTP API、v2-on-v3 模拟、v2 discovery service、`client/v2` 包、v2 snapshot 加载) |
| Go 重构 | `gogo/protobuf` → `google.golang.org/protobuf`(#14533);`go-grpc-middleware` v1 → v2 interceptors(#20420);`otelgrpc` v0.61.0 用 `NewServerHandler` 取代废弃 interceptor(#20017) |
#### 降级方向的指标差异汇总
| 降级方向 | 目标版本不存在的指标 |
|---|---|
| v3.7 → v3.6 | `etcd_server_request_duration_seconds`、`etcd_debugging_server_watch_send_loop_control_stream_duration_seconds`、`etcd_debugging_server_watch_send_loop_progress_duration_seconds`、`etcd_debugging_server_watch_send_loop_watch_stream_duration_seconds`、`etcd_debugging_server_watch_send_loop_watch_stream_duration_per_event_seconds` |
| v3.6 → v3.5 | `etcd_network_known_peers`、`etcd_server_feature_enabled` |
> 💡 补充:降级前若 Grafana 面板或告警规则引用了上表中的指标,降级后这些面板会出现「No data」。建议降级前先把相关面板/规则标注或临时下线。
### 14.9 升级 / 降级操作要点速查
| 项目 | 升级 | 降级 |
|---|---|---|
| 版本跨度 | 一次一个 minor,先到最新 patch | 一次一个 minor |
| 最低前置版本 | 3.6 要求 ≥ v3.5;**3.7 要求 ≥ v3.6.11** | — |
| 健康校验 | `etcdctl endpoint health` | `etcdctl endpoint health` |
| 版本校验 | `curl http://host:2379/version` | `curl http://host:2379/version` + `endpoint status -w=table` |
| 前置动作 | 从 **leader** 拍 snapshot 备份 | 从 **leader** 拍 snapshot 备份 + 下载目标版本发布包 |
| 关键命令 | 逐台替换二进制 | `etcdctl downgrade validate ` → `etcdctl downgrade enable ` → 逐台替换 → 完成 |
| 中途反悔 | 混合版本状态下可换回旧二进制 | 混合版本状态下 `etcdctl downgrade cancel` + 用原二进制重启已降级 member |
| 完成后反悔 | 只能用 snapshot 恢复,或走正式降级流程 | 只能走正式升级流程 |
| leader 处理 | leader 会在退出前自动转移 leadership | **建议最后降级 leader**,用 `etcdctl move-leader` 先移走 |
| 配置是否需要改 | 相同配置即可 | v3.7→v3.6 **无需改配置**;v3.6→v3.5 必须删除/改名/改默认值 |
### 14.10 文档内部疑点(素材已注明,本册未做修正)
1. `upgrade_3_6.md` Step 5 的命令写作 `endpoint health --endpoints=...`,疑似漏写 `etcdctl` 前缀(Step 4 中为 `etcdctl endpoint health --endpoints=...`)。
2. `upgrade_3_6.md` 的 "Upgrade requirements" 要求「低于 v3.5 请先升级到 v3.5」,但链接指向 `../upgrade_3_4/`(3.4→3.5 指南),文字与链接版本存在表述不一致。
3. `upgrade_3_6.md` 要求 3.5 member 至少为 `3.5.32` 或更高,但同页示例输出中版本为 `3.5.18`,**示例与要求不自洽**。
4. `upgrade_3_6.md` 与 `downgrade_3_6.md` 中 InitialCorruptCheck 的旧写法不一致:升级页为 `--experimental-initial-corrupt-check=true`,降级页为 `--experimental-enable-initial-corrupt-check=true`。
5. `downgrade_3_6.md` 的 Step 2 仅有说明文字,**未给出 snapshot 备份命令**(对比 `downgrade_3_7.md` Step 2 给出了 `etcdctl --endpoints=localhost:2379 snapshot save backup.db`)。
6. `downgrade_3_6.md` Step 6 的 diff 中旧行写作 `-etcd-3.6/bin --name s1 \`,疑漏 `/etcd`。
7. 「先 learner 升级」「只有所有 member 都运行 v3.x 才能降级」「降级后 revision 变化」等说法,在这 6 个源文件中**均未出现**;升级流程未规定 learner 顺序,降级前提条件仅写明「一次只降一个 minor 版本 + `etcdctl downgrade validate` 校验通过 + 集群健康」。
---
## 15. 性能与容量:三个 benchmark
本章原样保留三个官方 benchmark 页的全部数字,不做换算、不补数据。三个页面在素材中**均未给出具体的命令行调用与参数**,本册如实说明。
### 15.1 Benchmarking etcd v3(读性能)
> 引自:[Benchmarking etcd v3](https://etcd.io/docs/v3.7/benchmarks/etcd-3-demo-benchmarks/)
#### 测试目的
衡量 etcd v3(demo 模式)在单成员部署下的读性能基线,分别观测「空 store 上读单个 key」与「先执行一次 put 之后再读单个 key」两种场景,并以「空 server handler」的基准作为对照,判断读路径的性能损耗到底来自 gRPC/服务端框架,还是来自 storage 包。
#### 环境 / 硬件配置
物理机:GCE `n1-highcpu-2` machine type
- 1x 专用本地 SSD,挂载于 `/var/lib/etcd`
- 1x 专用慢速磁盘用于操作系统
- 1.8 GB 内存
- 2x CPUs
- etcd version 2.2.0
etcd 集群:1 个 etcd member,以 v3 demo mode 运行。
#### 命令
- 使用 [etcd v3 benchmark tool](https://github.com/etcd-io/etcd/tree/main/tools/benchmark)(工具名为 `benchmark`,位于 `tools/benchmark`)。
- **本页未给出具体的命令行调用与参数**(如 `benchmark put / range` 的具体 flags),仅指向上述工具仓库路径。
#### 结果数据表
**1)reading one single key(读单个 key)**
| key size in bytes | number of clients | read QPS | 90th Percentile Latency (ms) |
|---|---|---|---|
| 256 | 1 | 2716 | 0.4 |
| 256 | 64 | 16623 | 6.1 |
| 256 | 256 | 16622 | 21.7 |
**2)reading one single key after putting(put 之后读单个 key)**
| key size in bytes | number of clients | read QPS | 90th Percentile Latency (ms) |
|---|---|---|---|
| 256 | 1 | 2269 | 0.5 |
| 256 | 64 | 13582 | 8.6 |
| 256 | 256 | 13262 | 47.5 |
#### 结论
- 纯读场景下的性能与「空 server handler」几乎一致,说明 gRPC/服务端框架本身不是瓶颈。
- 客户端数从 1 提升到 64 时,read QPS 从 2716 提升到 16623,90th 延迟从 0.4 ms 上升到 6.1 ms;继续提升到 256 客户端时 QPS 基本不再增长(16622),而 90th 延迟进一步恶化到 21.7 ms,说明此时已达到饱和。
- put 之后读的场景整体下降:1/64/256 客户端下 QPS 分别为 2269 / 13582 / 13262,90th 延迟分别为 0.5 / 8.6 / 47.5 ms。
- 由于「空 server handler」的性能不受单次 put 影响,因此 put 后读的性能下降应归因于 storage 包,而非服务端框架。
> 💡 补充(容量规划可推出的结论):
> 1. **读的并发饱和点约在 64 客户端**:从 64 → 256 客户端,QPS 不再增长(16623 → 16622),但 p90 延迟从 6.1ms 恶化到 21.7ms(约 3.6 倍)。据此,客户端连接池不是开得越大越好,超过饱和点只会换延迟。
> 2. **单次 put 之后读吞吐下降约 16%–20%**(16623 → 13582;16622 → 13262),且高并发下延迟恶化更明显(21.7ms → 47.5ms,约 2.2 倍)。写多读少的集群要按「put 后读」这一档做容量基线。
> 3. 该页环境标注为 `etcd version 2.2.0` 而标题为 v3,**疑为文档沿用了早期版本的环境描述**(素材已注明),因此这组数字宜作为量级参考,不宜作为 v3.7 的 SLA 基线。
### 15.2 Watch Memory Usage Benchmark
> 引自:[Watch Memory Usage Benchmark](https://etcd.io/docs/v3.7/benchmarks/etcd-3-watch-memory-benchmark/)
#### 测试目的
量化 etcd watch 功能的物理内存占用,找到 watch 内存消耗模型中三个系数 `c1`(每个连接)、`c2`(每个 stream)、`c3`(每个 watching)的经验值,从而估算单台 etcd server 能承载多少 watcher。
etcd 的设计目标:支持 **O(10k) 个客户端**、**O(100K) 条 watch stream**(每客户端 O(10) 条 stream)、**O(10M) 个总 watching**(每 stream O(100) 个 watching)。
#### 内存消耗模型
etcd watch 有三个消耗物理内存的组件:
1. `grpc.Conn`:维护真实的 TCP 连接与 gRPC 连接状态,每个消耗 **O(10kb)**,其上可挂载多条 watch stream。
2. watch stream:独立的 HTTP2 连接,每个再消耗 **O(10kb)**,多条 watching 可共享同一条 stream。
3. watching:真正跟踪 key-value store 变更的结构体,每个应仅消耗 **< O(1kb)**。
理论估算公式:
```text
memory = c1 * number_of_conn + c2 * avg_number_of_stream_per_conn + c3 * avg_number_of_watch_stream
```
结构关系示意:`conn` → 多条 `stream`,每条 `stream` → 多个 `watch`(如 `watch foo`、`watch bar`)。
#### 环境 / 硬件配置
- etcd version:git head https://github.com/etcd-io/etcd/commit/185097ffaa627b909007e772c175e8fefac17af3
- GCE `n1-standard-2` machine type
- 7.5 GB 内存
- 2x CPUs
#### 命令 / 测量方法
- 总体内存占用以 [RSS](https://en.wikipedia.org/wiki/Resident_set_size) 衡量,结果可能有约 **10% 的波动**,但足以反映大致用量与分配模式。
- **本页未给出** `benchmark`、`etcdctl check perf` 或 `go test -bench` 等具体命令行,仅说明通过实际压测得到上表后反推系数。
#### 结果数据表(Overall memory usage)
| clients | streams per client | watchings per stream | total watching | memory usage |
|---|---|---|---|---|
| 1k | 1 | 1 | 1k | 50MB |
| 2k | 1 | 1 | 2k | 90MB |
| 5k | 1 | 1 | 5k | 200MB |
| 1k | 10 | 1 | 10k | 217MB |
| 2k | 10 | 1 | 20k | 417MB |
| 5k | 10 | 1 | 50k | 980MB |
| 1k | 50 | 1 | 50k | 1001MB |
| 2k | 50 | 1 | 100k | 1960MB |
| 5k | 50 | 1 | 250k | 4700MB |
| 1k | 50 | 10 | 500k | 1171MB |
| 2k | 50 | 10 | 1M | 2371MB |
| 5k | 50 | 10 | 2.5M | 5710MB |
| 1k | 50 | 100 | 5M | 2380MB |
| 2k | 50 | 100 | 10M | 4672MB |
| 5k | 50 | 100 | 25M | *OOM* |
#### 结论
- 由基准结果反推:**`c1 = 17kb`、`c2 = 18kb`、`c3 = 350bytes`**。即每增加一个客户端连接约消耗 17kb 内存,每增加一条 stream 约消耗 18kb 内存,而每增加一个 watching 仅约 350bytes。
- 单个 watching 的成本远低于连接和 stream,因此优化重点在于连接与 stream 的复用。
- 正常情况下单台 etcd server 可用几 GB 内存维持数百万(millions)个 watching。
- 数据也显示瓶颈主要来自连接数与 stream 数:5k clients × 50 streams × 1 watching 就已到 4700MB,而 5k × 50 × 10(总 2.5M watching)也只有 5710MB,说明 watching 数量本身对内存影响相对小。
- 极端规模(5k clients × 50 streams × 100 watchings,共 25M watching)在 7.5 GB 内存机器上会触发 **OOM**。
- 页面提示:watch 特性仍在活跃开发中,内存占用可能随之变化,但预期不会显著超过上表数字。
> 💡 补充(容量规划可直接套用的估算式):
> 用反推系数做粗算:`内存(字节) ≈ 17KB × 连接数 + 18KB × (连接数 × 每连接 stream 数) + 350B × (总 watching 数)`。
> 例:2k 客户端 × 10 stream/客户端 × 每 stream 100 watching(总 2M watching)≈ 34MB + 360MB + 700MB ≈ **1.09GB**,与表中 2k×50×10(2.37GB,stream 数更多)量级一致。
> **优化优先级由此非常明确**:① 减少 gRPC 连接数(复用 client);② 减少每连接的 stream 数(合并 watch);③ watching 数量本身反而是最便宜的。
> 表中 7.5GB 机器在 25M watching 时 OOM,因此**单 member 的 watching 上限建议控制在 10M 量级以内**并留出余量。
### 15.3 Storage Memory Usage Benchmark
> 引自:[Storage Memory Usage Benchmark](https://etcd.io/docs/v3.7/benchmarks/etcd-storage-memory-benchmark/)
#### 测试目的
量化 etcd storage 的内存占用,分为两部分:
1. **In-memory index**(进程内 B-tree 索引)的内存开销,目标是求出公式中的 `c1`(key 元数据开销)与 `c2`(版本元数据开销),并理解 store 的内存硬上限。
2. **Overall memory usage**(RSS 总体占用),验证 value size 对总体内存的影响是否显著。
etcd storage 有两个消耗物理内存的组件:etcd 进程分配的 *in-memory index*(加速 key 查找),以及由操作系统管理的 *page cache*(缓存最近访问的磁盘数据)。in-memory index 在 [B-tree](https://en.wikipedia.org/wiki/B-tree) 中保存全部 key 及指向磁盘 value 的指针,每个 key 可能有多个指向不同版本的指针。
理论公式:
```text
N * (c1 + avg_key_size) + N * (avg_versions_of_key) * (c2 + size_of_pointer)
```
其中 `c1` 为 key 元数据开销,`c2` 为版本元数据开销。Page cache 由 OS 管理,本文档不展开。
#### 环境 / 硬件配置
- etcd version:git head https://github.com/etcd-io/etcd/commit/776e9fb7be7eee5e6b58ab977c8887b4fe4d48db
- GCE `n1-standard-2` machine type
- 7.5 GB 内存
- 2x CPUs
#### 命令 / 测量方法
- 通过 Go `runtime.ReadMemStats` 计算内存用量:取创建索引前与创建索引后的 total allocated bytes 差值。
- 该方法不能完美反映 in-memory index 自身的精确占用,但能显示大致的消耗模式。
- **本页未出现** `benchmark`、`etcdctl check perf`、`go test -bench` 等具体命令行。
#### 结果数据表
**1)In-memory index memory usage**
| N | versions | key size | memory usage |
|---|---|---|---|
| 100K | 1 | 64bytes | 22MB |
| 100K | 5 | 64bytes | 39MB |
| 1M | 1 | 64bytes | 218MB |
| 1M | 5 | 64bytes | 432MB |
| 100K | 1 | 256bytes | 41MB |
| 100K | 5 | 256bytes | 65MB |
| 1M | 1 | 256bytes | 409MB |
| 1M | 5 | 256bytes | 506MB |
**2)Overall memory usage(RSS,含 value size 变量)**
| N | versions | key size | value size | memory usage |
|---|---|---|---|---|
| 100K | 1 | 64bytes | 256bytes | 40MB |
| 100K | 5 | 64bytes | 256bytes | 89MB |
| 1M | 1 | 64bytes | 256bytes | 470MB |
| 1M | 5 | 64bytes | 256bytes | 880MB |
| 100K | 1 | 64bytes | 1KB | 102MB |
| 100K | 5 | 64bytes | 1KB | 164MB |
| 1M | 1 | 64bytes | 1KB | 587MB |
| 1M | 5 | 64bytes | 1KB | 836MB |
#### 结论
- 由结果反推 **`c1 = 120bytes`、`c2 = 30bytes`**。由于 `c1`、`c2` 是公式中仅有的未知量,只需两组数据即可求解,表中的 `c1`/`c2` 是 4 组计算结果的平均值。
- 对小 key-value 对而言,key 元数据开销仍相对较高(约 50%),但相比旧 store 至少 1000% 的开销已是显著改进。
- key size 从 64bytes 增大到 256bytes 会明显推高索引内存:1M × 1 version 从 218MB 增至 409MB。
- 版本数从 1 增至 5 同样显著推高内存:1M × 64bytes 从 218MB 增至 432MB;1M × 256bytes 从 409MB 增至 506MB。
- value size 对总体内存消耗影响不显著,因为 value 保存在磁盘上,仅热数据由 OS page cache 留在内存中;表中有小幅上升(如 100K × 1 version 从 40MB 到 102MB)主要是 page cache 中驻留数据增多所致。
- Page cache 由操作系统管理,本文不做详细分析。
> 💡 补充(容量规划可推出的结论):
> 1. **索引内存 ≈ N × (120B + key_size) + N × 版本数 × (30B + 指针大小)**。以 1M key、64B key、5 个版本估算:1M × 184B + 1M × 5 × 38B ≈ 184MB + 190MB ≈ 374MB,与实测 432MB 同量级——**可用此式做上机前的内存预估**。
> 2. **key 的长度比 value 的长度更「贵」**:key 从 64B → 256B(+192B)使 1M×1version 索引内存从 218MB → 409MB(+191MB,几乎 1:1 线性);而 value 从 256B → 1KB 主要只推高 page cache,进程内影响小。**设计 key 命名时应尽量短**。
> 3. **历史版本数是隐形内存杀手**:版本数 1 → 5 让 1M×64B 从 218MB 涨到 432MB(约 2 倍)。这正是要配好 auto-compaction(第 6 章)的原因——压缩后旧版本被回收,索引内存随之下降。
> 4. 综合 watch 与 storage 两组数据:**内存规划应同时算「连接/stream 开销」与「key 数 × 版本数 开销」两项**,只算一项会低估。
### 15.4 三个 benchmark 的环境与来源对照
| 页面 | 机型 | CPU / 内存 | etcd 版本 | 测量方式 | 是否给出命令行 |
|---|---|---|---|---|---|
| Benchmarking etcd v3 | GCE `n1-highcpu-2` + 本地 SSD | 2x CPUs / 1.8 GB | 2.2.0(页面标注,疑沿用旧描述) | benchmark 工具(QPS / p90 延迟) | 否(仅给工具链接) |
| Watch Memory Usage Benchmark | GCE `n1-standard-2` | 2x CPUs / 7.5 GB | git head `185097ffaa627b909007e772c175e8fefac17af3` | RSS | 否 |
| Storage Memory Usage Benchmark | GCE `n1-standard-2` | 2x CPUs / 7.5 GB | git head `776e9fb7be7eee5e6b58ab977c8887b4fe4d48db` | Go `runtime.ReadMemStats` | 否 |
> 💡 补充:三页均为**单机 2 核小机型**下的测量,且都未给出可复现的命令行。因此它们适合用于「量级判断与系数推导」,不适合直接当作生产 SLA。真要做容量验收,建议用 `tools/benchmark` 在自己的机型上跑一遍(工具仓库:https://github.com/etcd-io/etcd/tree/main/tools/benchmark)。
---
## 16. 生态与社区
本章介绍 etcd 周边的**工具与客户端库**、**日志约定**、**报 bug 的模板要素**,以及**社区 issue / PR 的分诊规则与时效**。
> 引自:[Libraries and tools](https://etcd.io/docs/v3.7/integrations/)
> 引自:[Logging conventions](https://etcd.io/docs/v3.7/dev-internal/logging/)
> 引自:[Reporting bugs](https://etcd.io/docs/v3.7/reporting_bugs/)
> 引自:[Issue triage guidelines](https://etcd.io/docs/v3.7/triage/issues/)
> 引自:[PR management](https://etcd.io/docs/v3.7/triage/PRs/)
### 16.1 Libraries and tools
**开篇声明**:下文提到的第三方库与工具(非托管在 https://github.com/etcd-io 的)etcd 团队**既不测试也不维护**。使用前建议用户自行阅读与调研。
#### 16.1.1 Tools(共 19 个)
| # | 名称 | 链接 | 定位 |
|---|---|---|---|
| 1 | etcdctl | https://github.com/etcd-io/etcd/tree/main/etcdctl | etcd 命令行客户端(官方) |
| 2 | etcd-dump | https://npmjs.org/package/etcd-dump | 命令行工具,用于 dump / 恢复 etcd |
| 3 | etcd-fs | https://github.com/xetorthio/etcd-fs | 面向 etcd 的 FUSE 文件系统 |
| 4 | etcddir | https://github.com/rekby/etcddir | 实时同步 etcd 与本地目录,支持 Windows 与 Linux |
| 5 | etcd-browser | https://github.com/henszey/etcd-browser | 基于 AngularJS 的 Web 版 etcd key/value 编辑器 |
| 6 | etcd-lock | https://github.com/datawisesystems/etcd-lock | 基于 etcd 的 master 选举与分布式读写锁实现,支持 v2 |
| 7 | etcd-console | https://github.com/matishsiao/etcd-console | 基于 PHP 的 Web 版 etcd key/value 编辑器 |
| 8 | etcd-viewer | https://github.com/nikfoundas/etcd-viewer | Java 编写的 etcd KV 编辑器 / 查看器 |
| 9 | etcdtool | https://github.com/mickep76/etcdtool | 以 JSON/YAML/TOML 导出/导入/编辑 etcd 目录,并用 JSON schema 校验 |
| 10 | etcdloadtest | https://github.com/sinsharat/etcdloadtest | 命令行压测客户端,适用于 etcd 3.0 及以上 |
| 11 | etcd-tui | https://github.com/olamilekan000/etcd-tui | 现代化终端 UI(TUI),可在终端内浏览 key、查看 value、过滤数据、管理集群 |
| 12 | etcdfinder | https://etcdfinder.github.io | 极速现代 Web UI,带即时搜索,同时支持 v2 与 v3 |
| 13 | lucas | https://github.com/ringtail/lucas | 面向 Kubernetes etcd3.0+ 集群的 Web 版 KV 查看器 |
| 14 | etcd-manager | https://etcdmanager.io | 现代化、高效、跨平台且免费的 etcd 3.x GUI 与客户端工具,支持 Windows/Linux/Mac |
| 15 | etcd-backup-restore | https://github.com/gardener/etcd-backup-restore | 周期性、增量式备份与恢复 etcd 的实用工具 |
| 16 | etcd-druid | https://github.com/gardener/etcd-druid | Kubernetes operator,用于部署 etcd 集群并管理 day-2 运维 |
| 17 | etcdadm | https://github.com/kubernetes-sigs/etcdadm | 操作 etcd 集群的命令行工具 |
| 18 | etcd-defrag | https://github.com/ahrtr/etcd-defrag | 更易用、更智能的 etcd 碎片整理工具 |
| 19 | etcdhelper | https://github.com/tsonglew/intellij-etcdhelper | IntelliJ 平台的 etcd 插件 |
> 💡 补充:运维视角可优先关注 4 类——备份恢复(`etcd-backup-restore`)、K8s 编排(`etcd-druid`)、集群操作 CLI(`etcdadm`)、碎片整理(`etcd-defrag`)。其余多为 KV 浏览/编辑类 GUI,排障时好用但不建议作为日常运维入口。
#### 16.1.2 Libraries(按语言分类)
**Go**
| 库 | 链接 | 说明 |
|---|---|---|
| `etcd/client/v3` | https://github.com/etcd-io/etcd/tree/main/client/v3 | v3 官方维护的 Go 客户端 |
| `go-etcd` | https://github.com/coreos/go-etcd | 已废弃的官方客户端,对更老的(<2.0.0)etcd 版本可能有用 |
| `encWrapper` | https://github.com/lumjjb/etcd/tree/enc_wrapper/clientwrap/encwrapper | 针对 etcd client Keys API/KV 的加密封装 |
**Java**
| 库 | 链接 | 说明 |
|---|---|---|
| `coreos/jetcd` | https://github.com/etcd-io/jetcd | 支持 v3 |
| `justinsb/jetcd` | https://github.com/justinsb/jetcd | — |
| `cdancy/etcd-rest` | https://github.com/cdancy/etcd-rest | 基于 jclouds 提供完整的 v2 API 实现 |
| `IBM/etcd-java` | https://github.com/IBM/etcd-java | — |
**Scala**
| 库 | 链接 | 说明 |
|---|---|---|
| `maciej/etcd-client` | https://github.com/maciej/etcd-client | 支持 v2,基于 Akka HTTP 的全异步客户端 |
| `eiipii/etcdhttpclient` | https://bitbucket.org/eiipii/etcdhttpclient | 支持 v2,基于 Netty 与 Scala Futures 的异步 HTTP 客户端 |
| `mingchuno/etcd4s` | https://github.com/mingchuno/etcd4s | 支持 v3,使用 gRPC,可选 Akka Stream 支持 |
**Perl**
| 库 | 链接 | 说明 |
|---|---|---|
| `hexfusion/perl-net-etcd` | https://github.com/hexfusion/perl-net-etcd | 支持 v3 grpc gateway HTTP API |
| `robn/p5-etcd` | https://github.com/robn/p5-etcd | 支持 v2 |
**Python**
| 库 | 链接 | 说明 |
|---|---|---|
| `kragniz/python-etcd3` | https://github.com/kragniz/python-etcd3 | v3 客户端 |
| `jplana/python-etcd` | https://github.com/jplana/python-etcd | 支持 v2 |
| `russellhaering/txetcd` | https://github.com/russellhaering/txetcd | 一个 Twisted Python 库 |
| `cholcombe973/autodock` | https://github.com/cholcombe973/autodock | Docker 部署自动化工具 |
| `lisael/aioetcd` | https://github.com/lisael/aioetcd | (Python 3.4+)asyncio 协程客户端,支持 v2 |
| `txaio-etcd` | https://github.com/crossbario/txaio-etcd | 异步 etcd v3-only 客户端库,当前支持 Twisted,未来支持 asyncio |
| `dims/etcd3-gateway` | https://github.com/dims/etcd3-gateway | 使用 HTTP grpc gateway 的 etcd v3 API 库 |
| `aioetcd3` | https://github.com/gaopeiliang/aioetcd3 | (Python 3.6+)面向 asyncio 的 etcd v3 API |
| `Revolution1/etcd3-py` | https://github.com/Revolution1/etcd3-py | (python2.7 与 python3.5+)etcd v3 Python 客户端,使用 gRPC-JSON-Gateway |
**Node**
| 库 | 链接 | 说明 |
|---|---|---|
| `mixer/etcd3` | https://github.com/mixer/etcd3 | 支持 v3 |
| `stianeikeland/node-etcd` | https://github.com/stianeikeland/node-etcd | 支持 v2(含 Coffeescript) |
| `lavagetto/nodejs-etcd` | https://github.com/lavagetto/nodejs-etcd | 支持 v2 |
| `deedubs/node-etcd-config` | https://github.com/deedubs/node-etcd-config | 支持 v2 |
**Ruby**
| 库 | 链接 | 说明 |
|---|---|---|
| `iconara/etcd-rb` | https://github.com/iconara/etcd-rb | — |
| `jpfuentes2/etcd-ruby` | https://github.com/jpfuentes2/etcd-ruby | — |
| `ranjib/etcd-ruby` | https://github.com/ranjib/etcd-ruby | 支持 v2 |
| `davissp14/etcdv3-ruby` | https://github.com/davissp14/etcdv3-ruby | 支持 v3 |
**C**
| 库 | 链接 | 说明 |
|---|---|---|
| `apache/celix/etcdlib` | https://github.com/apache/celix/tree/master/libs/etcdlib | 支持 v2 |
| `jdarcy/etcd-api` | https://github.com/jdarcy/etcd-api | 支持 v2 |
| `shafreeck/cetcd` | https://github.com/shafreeck/cetcd | 支持 v2 |
**C++**
| 库 | 链接 | 说明 |
|---|---|---|
| `edwardcapriolo/etcdcpp` | https://github.com/edwardcapriolo/etcdcpp | 支持 v2 |
| `suryanathan/etcdcpp` | https://github.com/suryanathan/etcdcpp | 支持 v2(带 waits) |
| `nokia/etcd-cpp-api` | https://github.com/nokia/etcd-cpp-api | 支持 v2 |
| `etcd-cpp-apiv3/etcd-cpp-apiv3` | https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3 | 支持 v3 |
**Clojure**
| 库 | 链接 | 说明 |
|---|---|---|
| `aterreno/etcd-clojure` | https://github.com/aterreno/etcd-clojure | — |
| `dwwoelfel/cetcd` | https://github.com/dwwoelfel/cetcd | 支持 v2 |
| `rthomas/clj-etcd` | https://github.com/rthomas/clj-etcd | 支持 v2 |
**Erlang**
| 库 | 链接 | 说明 |
|---|---|---|
| `marshall-lee/etcd.erl` | https://github.com/marshall-lee/etcd.erl | 支持 v2 |
| `zhongwencool/eetcd` | https://github.com/zhongwencool/eetcd | 支持 v3+(仅 GRPC) |
**Elixir**
| 库 | 链接 | 说明 |
|---|---|---|
| `team-telnyx/etcdex` | https://github.com/team-telnyx/etcdex | 支持 v3+(仅 GRPC) |
**.NET**
| 库 | 链接 | 说明 |
|---|---|---|
| `wangjia184/etcdnet` | https://github.com/wangjia184/etcdnet | 支持 v2 |
| `drusellers/etcetera` | https://github.com/drusellers/etcetera | — |
| `shubhamranjan/dotnet-etcd` | https://github.com/shubhamranjan/dotnet-etcd | 支持 v3+(仅 GRPC) |
| `SimplifyNet/Etcd.Microsoft.Extensions.Configuration` | https://github.com/SimplifyNet/Etcd.Microsoft.Extensions.Configuration | — |
**PHP**
| 库 | 链接 | 说明 |
|---|---|---|
| `linkorb/etcd-php` | https://github.com/linkorb/etcd-php | — |
| `activecollab/etcd` | https://github.com/activecollab/etcd | — |
| `ouqiang/etcd-php` | https://github.com/ouqiang/etcd-php | v3 gRPC gateway 客户端 |
**其他语言**
| 语言 | 库 | 链接 | 说明 |
|---|---|---|---|
| Haskell | `wereHamster/etcd-hs` | https://github.com/wereHamster/etcd-hs | — |
| R | `ropensci/etseed` | https://github.com/ropensci/etseed | — |
| Nim | `etcd_client` | https://github.com/FedericoCeratto/nim-etcd-client | — |
| Tcl | `efrecon/etcd-tcl` | https://github.com/efrecon/etcd-tcl | 支持 v2,但不支持 wait |
| Rust | `jimmycuadra/rust-etcd` | https://github.com/jimmycuadra/rust-etcd | 支持 v2 |
| Gradle | `gradle-etcd-rest-plugin` | https://github.com/cdancy/gradle-etcd-rest-plugin | 支持 v2 |
| Lua | `api7/lua-resty-etcd` | https://github.com/api7/lua-resty-etcd | 支持 v2 与 v3(grpc gateway HTTP API) |
#### 16.1.3 Deployment tools
| 类别 | 项目 | 链接 |
|---|---|---|
| Chef integrations | `coderanger/etcd-chef` | https://github.com/coderanger/etcd-chef |
| Chef cookbooks | `spheromak/etcd-cookbook` | https://github.com/spheromak/etcd-cookbook |
| BOSH releases | `cloudfoundry-community/etcd-boshrelease` | https://github.com/cloudfoundry-community/etcd-boshrelease |
| BOSH releases | `cloudfoundry/cf-release`(jobs/etcd) | https://github.com/cloudfoundry/cf-release/tree/master/jobs/etcd |
#### 16.1.4 Projects using etcd(使用 etcd 的项目)
| 项目 | 链接 | 说明 |
|---|---|---|
| etcd Raft users | https://github.com/etcd-io/raft/blob/main/README.md#notable-users | 使用 etcd raft 库实现的项目列表 |
| Apache APISIX | https://github.com/apache/apisix | 使用 etcd 作为配置存储的 API 网关 |
| apache/celix | https://github.com/apache/celix | 适配 C/C++ 的 OSGi 规范实现 |
| binocarlos/yoda | https://github.com/binocarlos/yoda | etcd + ZeroMQ |
| blox/blox | https://github.com/blox/blox | 面向 AWS ECS 的容器管理与编排开源项目集合 |
| calavera/active-proxy | https://github.com/calavera/active-proxy | 用 etcd 配置的 HTTP 代理 |
| chain/chain | https://github.com/chain/chain | 用于运维与连接高可扩展许可链网络的软件 |
| derekchiang/etcdplus | https://github.com/derekchiang/etcdplus | 基于 etcd 构建的一组分布式同步原语 |
| go-discover | https://github.com/flynn/go-discover | Go 编写的服务发现 |
| gleicon/goreman | https://github.com/gleicon/goreman/tree/etcd | Go 版 Foreman 克隆的分支,支持 etcd |
| garethr/hiera-etcd | https://github.com/garethr/hiera-etcd | 使用 etcd 的 Puppet hiera 后端 |
| mattn/etcd-vim | https://github.com/mattn/etcd-vim | 在 vim 内 SET/GET key |
| mattn/etcdenv | https://github.com/mattn/etcdenv | 集成 etcd 的 "env" shebang |
| kelseyhightower/confd | https://github.com/kelseyhightower/confd | 用模板 + etcd 数据管理本地应用配置文件 |
| configdb | https://git.autistici.org/ai/configdb/tree/master | 构建在任意数据库后端之上的 REST 关系型抽象,用于存储配置与资产清单 |
| kubernetes/kubernetes | https://github.com/kubernetes/kubernetes | Google 发起的容器集群管理器 |
| mailgun/vulcand | https://github.com/mailgun/vulcand | 以 etcd 为配置后端的 HTTP 代理 |
| duedil-ltd/discodns | https://github.com/duedil-ltd/discodns | 使用 etcd 作为名称与记录数据库的简易 DNS 服务器 |
| skynetservices/skydns | https://github.com/skynetservices/skydns | 符合 RFC 的 DNS 服务器 |
| xordataexchange/crypt | https://github.com/xordataexchange/crypt | 用 GPG 加密把值安全存入 etcd |
| spf13/viper | https://github.com/spf13/viper | Go 配置库,可从 ENV、pflags、文件与 etcd 读取(可选加密) |
| lytics/metafora | https://github.com/lytics/metafora | Go 分布式任务库 |
| ryandoyle/nss-etcd | https://github.com/ryandoyle/nss-etcd | 用于从 etcd 解析名称的 GNU libc NSS 模块 |
| Gru | https://github.com/dnaeon/gru | 用 Go 实现的编排工具 |
| Vitess | http://vitess.io/ | 用于 MySQL 水平扩展的数据库集群系统 |
| lclarkmichalek/etcdhcp | https://github.com/lclarkmichalek/etcdhcp | 使用 etcd 做持久化与协调的 DHCP 服务器 |
| openstack/networking-vpp | https://github.com/openstack/networking-vpp | 编程 FD.io VPP 数据面以提供 OpenStack 云虚拟网络的网络驱动 |
| OpenStack | https://github.com/openstack/governance/blob/master/reference/base-services.rst | OpenStack 服务可将 etcd 作为基础服务依赖 |
| CoreDNS | https://github.com/coredns/coredns/tree/master/plugin/etcd | 链式插件的 DNS 服务器,CNCF 与 Kubernetes 生态项目 |
| Uber M3 | https://github.com/m3db/m3 | Uber 开源的、面向 Prometheus 的大规模指标平台 |
| Rook | https://github.com/rook/rook | Kubernetes 的存储编排 |
| Patroni | https://github.com/zalando/patroni | 使用 ZooKeeper、etcd 或 Consul 的 PostgreSQL 高可用模板 |
| Trillian | https://github.com/google/trillian | 实现 Merkle tree,内容由数据存储层提供以支撑超大规模树 |
| purpleidea/mgmt | https://github.com/purpleidea/mgmt | 下一代分布式、事件驱动、并行配置管理 |
| Portworx/kvdb | https://docs.portworx.com/concepts/internal-kvdb/ | 存储 Portworx 集群配置的内部 kvdb |
| Apache Pulsar | https://pulsar.apache.org/ | 为云构建的开源分布式消息与流平台 |
### 16.2 日志约定(Logging conventions)
etcd 使用 **zap** 库(https://github.com/uber-go/zap)输出应用日志,日志按 *levels*(级别)分类。日志级别按以下约定判定:
| 级别 | 语义 | 源文示例 |
|---|---|---|
| **DebugLevel** | 日志量通常很大,生产环境一般禁用 | 向远端 peer 发送一条普通消息(Send a normal message to a remote peer);向磁盘写一条日志条目(Write a log entry to disk) |
| **InfoLevel** | **默认**的日志优先级 | 启动配置(Startup configuration);开始做快照(Start to do snapshot);向集群加入新节点(Add a new node into the cluster);向 auth 子系统加入新用户(Add a new user into auth subsystem) |
| **WarnLevel** | 比 Info 更重要,但不需要人工逐条介入 review | 向远端 peer 发送 Raft 消息失败(Failure to send Raft message to a remote peer);在配置的 election timeout 内未收到心跳消息(Failure to receive heartbeat message within the configured election timeout) |
| **ErrorLevel** | 高优先级。若应用运行平稳,**不应该**产生任何 error 级日志 | 为 WAL 分配磁盘空间失败(Failure to allocate disk space for WAL) |
| **PanicLevel** | 记录一条消息后 panic | Raft 消息编码失败(Failure to encode Raft messages) |
| **FatalLevel** | 记录一条消息后调用 `os.Exit(1)` | Raft 快照保存失败(Failure to save Raft snapshot) |
> ⚠️ 源文说明(如实记录):该页面原文**仅覆盖「日志级别分类约定」**这一部分内容,**未包含**结构化日志字段规范(如 `level`、`caller`、`msg`、`stacktrace` 等字段名约定)、全局 logger 的获取/注入用法(如 `zap.L()`、`GetLogger()`)以及代码位置约定等章节——本批抓取的 v3.7 源文件中不存在这些内容,故不展开,避免编造。
### 16.3 报 bug(Reporting bugs)
**提交渠道**:etcd 项目任何部分存在 bug 或文档错误,都请通过 [opening an issue](https://github.com/etcd-io/etcd/issues/new) 告知。etcd 团队严肃对待 bug 与错误,认为**没有问题是小到不值得提的**(no issue is too small)。创建报告前,请先确认是否已经存在报告同一问题的 issue。
#### 16.3.1 好的 bug 报告应满足的 5 条标准(模板要素)
| # | 标准 | 要点 |
|---|---|---|
| 1 | **Specific(具体)** | 尽可能包含细节——哪个版本、什么环境、什么配置等。若 bug 与运行 etcd server 相关,请**附上 etcd 日志**(**带 etcd 配置的启动日志尤其重要**) |
| 2 | **Reproducible(可复现)** | 包含复现步骤;某些问题难以复现时,请给出可能导致该问题的步骤。若可能,请**附上受影响的 etcd data dir 与 stack strace** |
| 3 | **Isolated(隔离)** | 尽量用**最小依赖**隔离并复现该 bug;报告中牵涉过多依赖会显著拖慢修复速度。**调试依赖 etcd 的外部系统不在范围内**,但很乐意提供方向指引或帮助使用 etcd 本身 |
| 4 | **Unique(唯一)** | 不要重复已有的 bug 报告 |
| 5 | **Scoped(范围限定)** | **一个报告只报一个 bug**,不要在一份报告里追报另一个 bug |
> ⚠️ 源文笔误保留:第 2 条中的 "stack strace" 为源文原文写法(应为 stack trace),本册不擅自修改。
**其他约定**:建议先阅读 Elika Etemad 关于如何提交好 bug 报告的文章(http://fantasai.inkedblade.net/style/talks/filing-good-bugs/)。团队可能会要求补充信息以定位 bug;**重复的 bug 报告将被关闭**。
#### 16.3.2 三条取信息的命令
- 如何获取 stack trace:
```bash
$ kill -QUIT $PID
```
- 如何获取 etcd 版本:
```bash
$ etcd --version
```
- 当 etcd 以 systemd service `etcd2.service` 运行时,如何获取配置与日志:
```bash
$ sudo systemctl cat etcd2
$ sudo journalctl -u etcd2
```
注意:**由于上游 systemd 的 bug,journald 可能在进程退出时丢失最后几行日志**。若 `journalctl` 显示 etcd 已停止却没有 fatal 或 panic 消息,请改用 `sudo journalctl -f -t etcd2` 获取完整日志。
### 16.4 Triage 章节索引页
`triage/_index.md` **仅包含 Hugo front matter**,无任何正文内容。front matter 为:`title: Triage`、`weight: 8000`、`description: Managing changes in etcd`。它是 triage 章节(含 `issues.md`、`PRs.md` 两个子页面)的索引/导航页,本身不含实质性流程说明。
### 16.5 Issue 分诊:标签与时效规则
**Purpose(目的)**:加速 issue 管理(Speed up issue management)。
**标签体系**:etcd 的 issue 列在 https://github.com/etcd-io/etcd/issues,通过 label 标识。例如被确认为 bug 的 issue 最终会被打上标签 **`area/bug`**。新建 issue 起初**没有任何 label**,通常由 etcd maintainer 与活跃贡献者根据判断补充。完整标签清单位于 https://github.com/kubernetes/kubernetes/labels。
**预定检索入口**(方便分诊):
| 入口 | 链接 |
|---|---|
| Bugs | https://github.com/etcd-io/etcd/labels/area%2Fbug |
| Help Wanted | https://github.com/etcd-io/etcd/labels/Help%20Wanted |
| Longest untriaged issues | `https://github.com/etcd-io/etcd/issues?utf8=%E2%9C%93&q=is%3Aopen+sort%3Aupdated-asc` |
| Issue Triage Meeting | https://etcd.io/community/#community-meetings |
**Scope(适用范围)**:本文档是 etcd issue 分诊的主要文档。欢迎所有人参与管理 issue 与 PR,但文中所述工作与责任是**以 etcd maintainer 与活跃贡献者为对象**编写的。
**处理流程与时限约定**:
| # | 环节 | 规则 | 时效 |
|---|---|---|---|
| 1 | **Validate if an issue is a bug** | 确认 issue 确实是 bug。若不是,附上发现并关闭 trivial 的 issue;对非 trivial 的 issue,等待报告者回应、看是否有异议 | **报告者 30 天内未回复 → 关闭** |
| 2 | **Inactive issues** | 缺乏报告者足够信息的 issue | **报告者 60 天内未提供信息 → 应关闭** |
| 3 | **Duplicate issues** | 若是重复问题,留言说明并**引用原始 issue**,然后关闭 | — |
| 4 | **Issues that don't belong to etcd** | 有些 issue 实际属于 etcd 所依赖的其他项目(例如 `grpc` 或 `golang`)。应要求报告者到对应的其他项目开 issue | **除 maintainer 与报告者认为有必要保留以做跟踪外,一律关闭** |
| 5 | **Verify important labels are in place** | 确保 issue 带有其所属 area 的标签、**已添加合适的 assignees**、**已确定 milestone**;缺失则补上。若因权限不足无法打标签或无法确定正确标签,也无需强求,必要时联系 maintainer | — |
| 6 | **Poke issue owner if needed** | 若某 issue 已被某开发者认领但未创建 PR | **30 天内没有创建 PR → 联系 owner**,要求其提交 PR 或(如需)释放认领 |
### 16.6 PR 管理:标签与时效规则
**Purpose(目的)**:加速 PR 管理(Speed up PR management)。
**PR 列表与标签**:etcd 的 PR 列在 https://github.com/etcd-io/etcd/pulls。一个 PR 可以带有各种 label、milestone、reviewer 等。完整标签清单位于 https://github.com/kubernetes/kubernetes/labels。
**示例检索**:
| 入口 | 链接 |
|---|---|
| Open PRs for milestone etcd-v3.4 | `https://github.com/etcd-io/etcd/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aopen+milestone%3Aetcd-v3.4` |
| PRs under investigation | https://github.com/etcd-io/etcd/labels/Investigating |
**生命周期与时限约定**:
| # | 环节 | 规则 | 时效 |
|---|---|---|---|
| 1 | **Handle inactive PRs** | review 意见未被处理 → 催 PR owner;PR owner 未回复 → 尽可能用一个新的 commit 更新该 PR;若做不到则关闭 | **review 意见 15 天内未处理 → 催**;**owner 90 天内未回复 → 自行更新**;**不活跃 PR 180 天后关闭** |
| 2 | **Poke reviewer if needed** | reviewer 一般会及时响应,但大家都很忙,请求 review 后若未快速响应请给些时间 | **10 天内没有回应** → 可通过在 PR 中留言、发邮件或在 **Slack** 上发消息联系 |
| 3 | **Verify important labels are in place** | 确保 PR **已添加合适的 reviewer**、**已确定 milestone**;若缺失请补充。若无法确定正确的标签,**留言让 maintainer 来处理** | — |
### 16.7 时效规则速查
| 对象 | 动作 | 天数 |
|---|---|---|
| Issue | 报告者未回复 → 关闭 | 30 |
| Issue | 报告者未提供信息 → 关闭(inactive) | 60 |
| Issue | 已认领但未出 PR → 催 owner | 30 |
| PR | review 意见未处理 → 催 owner | 15 |
| PR | owner 未回复 → 尝试自行更新 | 90 |
| PR | 不活跃 → 关闭 | 180 |
| PR | reviewer 无回应 → 可主动联系 | 10 |
---
## 17. FAQ 全量
> 引自:[FAQ](https://etcd.io/docs/v3.7/faq/)(源文件 `faq.md`,约 18KB)
本章逐条收录官方 FAQ 全部问答。**英文问题原文保留**,中文为答案要点;涉及数字、命令、默认值的均按原文保留。
### 17.1 etcd, general
---
**Q: What is etcd?**
etcd 是一个一致的分布式键值存储。主要用作分布式系统中独立的协调服务;被设计用来存放**可以完全放入内存的小量数据**。
---
**Q: How do you pronounce etcd?**
读作 **/ˈɛtsiːdiː/**,含义是 "distributed `etc` directory"(分布式的 `etc` 目录)。
---
**Q: Do clients have to send requests to the etcd leader?**
不需要。[Raft][raft] 是基于 leader 的:leader 处理所有需要集群共识的客户端请求。但**客户端不需要知道哪个节点是 leader**——任何需要共识的请求若发给 follower,会被**自动转发给 leader**。不需要共识的请求(例如序列化读,serialized reads)**可以由任意集群成员处理**。
---
### 17.2 Configuration
---
**Q: What is the difference between listen--urls, advertise-client-urls or initial-advertise-peer-urls?**
- `listen-client-urls` 与 `listen-peer-urls`:指定 etcd server 本地**绑定**并接受 incoming 连接的地址。要在某端口上监听所有网卡,把监听 IP 指定为 `0.0.0.0`。
- `advertise-client-urls` 与 `initial-advertise-peer-urls`:指定 etcd 客户端或其他 etcd 成员**联系**该 server 时应使用的地址。advertise 地址**必须能被远端机器访问到**。生产环境**不要 advertise `localhost` 或 `0.0.0.0`** 这类地址,因为它们从远端机器不可达。
---
**Q: Why doesn't changing `--listen-peer-urls` or `--initial-advertise-peer-urls` update the advertised peer URLs in `etcdctl member list`?**
成员的 advertised peer URLs 源自**集群初次启动时的 `--initial-advertise-peer-urls`**。启动后再改监听 peer URLs 或初始 advertise peers **不会影响**已导出的 advertise peer URLs,因为这类变更**必须经过 quorum**,以避免成员配置出现 split brain。要更新成员的 peer URLs,请使用 `etcdctl member update`。
---
### 17.3 Deployment
---
**Q: System requirements**
- 由于 etcd 会写盘,性能**强烈依赖磁盘性能**,因此**强烈推荐 SSD**。评估磁盘是否够快可用 [fio][fio] 等压测工具(示例见 [fio-blog-post])。
- 为防止性能下降或无意中压垮 KV 存储,etcd 强制一个**可配置的存储配额,默认 2GB**。为避免 swapping 或内存耗尽,机器至少应有能覆盖该配额的 RAM。
- **8GB 是常规环境建议的最大配额**;配置值超过它时 etcd 会在**启动时告警**。
- 在 CoreOS,etcd 集群通常部署在**专用 CoreOS Container Linux 机器**上,配置为**双核处理器、2GB RAM、80GB SSD(*至少*)**。
- **注意:性能本质上取决于工作负载,生产部署前请务必测试**。更多建议见 [hardware][hardware-setup]。
- 最稳定的生产环境是 **Linux 操作系统 + amd64 架构**,详见 [supported platform][supported-platform]。
---
**Q: Why an odd number of cluster members?**
etcd 集群需要多数派(quorum)才能就集群状态更新达成一致。n 个成员的集群,quorum = **(n/2)+1**。对**任意奇数规模集群,加一个节点总会增加达成 quorum 所需的节点数**。虽然往奇数规模集群加节点看起来机器更多了,但**容错性反而变差**:能失败而不丢 quorum 的节点数完全相同,却多出了更多可能失败的节点。若集群已处于无法再容忍任何故障的状态,**先加后删节点是危险的**——因为一旦新节点注册进集群失败(例如地址配置错误),**quorum 将永久丢失**。
---
**Q: What is maximum cluster size?**
理论上**没有硬限制**。但 etcd 集群**大概不应超过 7 个节点**。与 etcd 类似、在 Google 内部广泛部署多年的 [Google Chubby lock service][chubby] 建议运行 **5 个节点**。一个 **5 成员的 etcd 集群可容忍 2 个成员故障**,多数场景已足够。更大的集群容错更好,但**写性能会受损**,因为数据必须复制到更多机器上。
---
**Q: What is failure tolerance?**
只要能形成成员 quorum,etcd 集群就能运行。
- **瞬时网络故障**(如分区)导致 quorum 丢失:网络恢复、quorum 重建后,etcd 会**自动且安全地**恢复;Raft 强制集群一致性。
- **掉电**:etcd 将 Raft log 持久化到磁盘,etcd 会**重放日志到故障点**并恢复参与集群。
- **永久性硬件故障**:可通过 [runtime reconfiguration][runtime reconfiguration] 将该节点移出集群。
建议集群成员数为**奇数**。奇数规模集群与偶数规模集群**容忍的故障数相同但节点更少**。对照表(Cluster Size / Majority / Failure Tolerance):
| Cluster Size | Majority | Failure Tolerance |
|:-:|:-:|:-:|
| 1 | 1 | 0 |
| 2 | 2 | 0 |
| 3 | 2 | 1 |
| 4 | 3 | 1 |
| 5 | 3 | 2 |
| 6 | 4 | 2 |
| 7 | 4 | 3 |
| 8 | 5 | 3 |
| 9 | 5 | 4 |
把成员数加到偶数**不会带来额外容错**;同理,网络分区期间,**奇数成员能保证始终存在一个多数派分区**可以继续运行,并在分区结束后成为事实来源(source of truth)。
---
**Q: Does etcd work in cross-region or cross data center deployments?**
跨区域部署**提升容错性**(成员处于不同的故障域),代价是**跨数据中心边界带来更高的共识请求延迟**。由于 etcd 依赖成员 quorum 达成共识,且**至少多数派成员必须响应共识请求**,跨数据中心的延迟会比较明显;此外**集群数据必须复制到所有 peer**,因此还有**带宽成本**。延迟变长时,**默认 etcd 配置可能导致频繁的选举或心跳超时**,高延迟部署请参考 [tuning] 调整超时参数。
---
### 17.4 Operation
---
**Q: How to backup a etcd cluster?**
`etcdctl` 提供 `snapshot` 命令用于创建备份,详见 [backup][backup]。
---
**Q: Should I add a member before removing an unhealthy member?**
替换 etcd 节点时,**务必先移除成员、再添加其替代者**。
- etcd 采用基于 quorum 模型的分布式共识:**(n/2)+1 个成员(多数派)同意**后提案才能提交到集群。提案既包括键值更新,**也包括成员变更**。该模型**完全避免了 split brain 不一致的可能**,缺点是**永久性 quorum 丢失是灾难性的**。
- 对成员变更而言:3 成员集群挂了 1 个,quorum 为 2、仍有 2 个存活,可继续推进。但**往 3 成员集群加一个新成员会把 quorum 提升到 3**(4 个成员需要 3 票才算多数)。由于 quorum 上升,这个额外成员**在容错上什么都没换来**——集群距离不可恢复仍然只差一个节点故障。
- 此外新成员本身有风险:它可能配置错误或无法加入。那种情况下**无法恢复 quorum**——集群 2 个成员宕机、2 个成员存活,但需要 3 票才能变更成员来撤销这次失败的添加。**etcd 默认会拒绝**可能导致集群以这种方式宕掉的 member add 尝试。
- 反过来,**先从成员列表移除宕掉的节点**:成员数变为 2,quorum 保持 2;随后添加新成员,quorum 仍稳定在 2。这样即使新节点起不来,也仍可通过剩余存活成员上的 quorum 把新成员移除。
---
**Q: Why won't etcd accept my membership changes?**
etcd 设置了 **`strict-reconfig-check`**,用于**拒绝会导致 quorum 丢失的重配置请求**。放弃 quorum 风险极高(尤其在集群已经不健康时)。虽然在已丢 quorum 的情况下很想关掉这项检查来加新成员,但这**可能导致彻底的集群不一致**;对很多应用来说问题会更糟("disk geometry corruption" 是最可怕的候选之一)。
---
**Q: Why does etcd lose its leader from disk latency spikes?**
这是**有意设计**的:**磁盘延迟是 leader 存活(liveness)的一部分**。假设 leader 花了一分钟才把一条 raft log 更新 fsync 到磁盘,而集群的 election timeout 是 1 秒:即使 leader 能在选举间隔内处理网络消息(如发送心跳),它**实际上是不可用的**,因为它无法提交任何新提案——它在等慢磁盘。若集群**因磁盘延迟频繁丢 leader**,尝试 [tuning][tuning] 磁盘设置或 etcd 时间参数。
---
**Q: What does the etcd warning "request ignored (cluster ID mismatch)" mean?**
每个新 etcd 集群都会**基于初始集群配置和用户提供的唯一 `initial-cluster-token` 值生成新的 cluster ID**。通过唯一的 cluster ID,etcd 得以**免受跨集群交互的破坏**(这种交互可能损坏集群)。该告警通常发生在:拆掉旧集群后,**新集群复用了部分 peer 地址**。若旧集群仍有 etcd 进程在运行,它会尝试联系新集群;新集群识别出 cluster ID 不匹配,于是**忽略该请求并输出此告警**。通常**确保不同集群之间的 peer 地址互不相交**即可消除该告警。
---
**Q: What does "mvcc: database space exceeded" mean and how do I fix it?**
etcd 的 [multi-version concurrency control][api-mvcc](MVCC)数据模型会**保留 keyspace 的完整历史**。若不周期性地压缩该历史(例如设置 **`--auto-compaction`**),etcd 最终会**耗尽存储空间**。空间不足时,etcd 会**触发一个 space quota alarm** 以保护集群不再接受写入;**只要告警处于触发状态,etcd 就会以错误 `mvcc: database space exceeded` 响应写请求**。
恢复步骤:
1. [Compact][maintenance-compact] 压缩 etcd 的历史;
2. [Defragment][maintenance-defragment] 对**每一个** etcd endpoint 做碎片整理;
3. [Disarm][maintenance-disarm] 解除告警。
> 💡 补充:这与第 6.5 节的「NOSPACE 恢复五步」是同一件事的两种表述——本页为三步版(compact → defrag → disarm),maintenance 页给出含取 revision 与验证的五步版。生产执行建议按第 6.5 节的五步走。
---
**Q: What does the etcd warning "etcdserver/api/v3rpc: transport: http2Server.HandleStreams failed to read frame: read tcp 127.0.0.1:2379->127.0.0.1:43020: read: connection reset by peer" mean?**
这是 **gRPC 侧的告警**:服务端在**客户端流被提前关闭**的情况下收到了 **TCP RST 标志**。例如客户端关闭了连接,而 gRPC server 还没处理完 TCP 队列中的所有 HTTP/2 帧。服务端侧可能丢了一些数据,但**只要客户端连接已经关闭就没问题**。只有 [old versions of gRPC](https://github.com/grpc/grpc-go/issues/1362) 会记录该日志;etcd [>=v3.2.13 默认以 DEBUG 级别记录](https://github.com/etcd-io/etcd/pull/9080),因此**只有开启 `--log-level=debug` 标志时可见**。
---
### 17.5 Performance
---
**Q: How should I benchmark etcd?**
使用 [benchmark] 工具(https://github.com/etcd-io/etcd/tree/main/tools/benchmark)。当前的 [benchmark results][benchmark-result] 可供对比。
---
**Q: What does the etcd warning "apply entries took too long" mean?**
多数派成员同意提交请求后,每个 etcd server 会把请求 apply 到自己的数据存储并把结果落盘。即便使用慢速机械盘或虚拟化网络盘(如 Amazon 的 EBS、Google 的 PD),apply 一个请求**通常也应少于 50 毫秒**。若**平均 apply 耗时超过 100 毫秒**,etcd 就会告警 entries 应用过慢。
常见原因与排查:
- **最常见是慢磁盘**。磁盘可能与 etcd 之外的其他应用争用,或磁盘本身太慢(如共享的虚拟化磁盘)。为排除慢盘,监控 [backend_commit_duration_seconds][backend_commit_metrics](**p99 应小于 25ms**)以确认磁盘够快。磁盘太慢时,为 etcd 分配独占磁盘或换更快的盘通常可解决。
- **第二常见是 CPU 饥饿**。若机器 CPU 使用率监控显示负载很高,可能没有足够的算力给 etcd。把 etcd 迁到专用机器、用 **cgroups** 增强进程资源隔离、或把 etcd server 进程 **renice 到更高优先级**,通常可解决。
- **昂贵的用户请求**(访问过多 key,例如拉取整个 keyspace)也会导致长 apply 延迟。但**每次请求访问少于几百个 key 应当始终是高性能的**。
- 若上述建议都不能消除告警,请 [open an issue][new_issue] 并附上详细的日志、监控、指标以及可选的负载信息。
---
**Q: What does the etcd warning "failed to send out heartbeat on time" mean?**
etcd 使用**基于 leader 的共识协议**做一致的数据复制与日志执行。集群成员选举出唯一 leader,其余为 follower。**被选出的 leader 必须周期性向 follower 发送心跳**以维持领导权;follower 若在**一个选举间隔内没收到心跳**就推断 leader 失效并触发选举。若 leader 没按时发出心跳但仍在运行,这次选举就是**假(spurious)选举**,很可能由**资源不足**引起。为捕捉这类软故障,**leader 跳过两个心跳间隔时,etcd 会告警 failed to send a heartbeat on time**。
常见原因与排查:
- **最常见是慢磁盘**。leader 在发送附带元数据的心跳前,可能需要**先把元数据持久化到磁盘**。同样可能是磁盘争用或磁盘本身太慢(如共享虚拟化磁盘)。监控 [wal_fsync_duration_seconds][wal_fsync_duration_seconds](**p99 应小于 10ms**)确认磁盘是否够快。磁盘太慢时,分配独占磁盘或换更快的盘通常可解决。也可用 [fio][fio] 判断磁盘是否够快(示例见 [fio-blog-post])。
- **第二常见是 CPU 饥饿**。处理方式同上:迁到专用机器、**用 cgroups 增强进程资源隔离**、或把 etcd server 进程 renice 到更高优先级。
- **慢网络**也会导致该问题。若 etcd 机器间的网络指标显示**长延迟或高丢包率**,可能网络容量不足,把 etcd 成员迁到不那么拥塞的网络通常可解决。但若 etcd 集群**跨数据中心部署**,成员间长延迟是**预期内**的:此时应把 **`heartbeat-interval`** 调整为大致匹配机器间的往返时间(RTT),并把 **`election-timeout`** 配置为**至少 5 * `heartbeat-interval`**。详见 [tuning documentation][tuning]。
- 若上述建议都不能消除告警,请 [open an issue][new_issue] 并附上详细的日志、监控、指标以及可选的负载信息。
---
**Q: What does the etcd warning "snapshotting is taking more than x seconds to finish ..." mean?**
etcd 会发送其**完整键值存储的快照**,用于**刷新慢 follower**以及用于 [backups][backup]。**快照传输过慢会拉高 MTTR**;若集群正在以高吞吐摄入数据,慢 follower 可能因「还没收完一个快照就又需要新快照」而**发生活锁(livelock)**。为捕捉快照性能问题,**当发送快照耗时超过 30 秒(thirty seconds)且超过 1Gbps 连接的预期传输时间时,etcd 会告警**。
---
### 17.6 本页引用链接(原样保留)
```text
[api-mvcc]: ../learning/api/#revisions
[backend_commit_metrics]: ../metrics/#disk
[backup]: ../op-guide/recovery/#snapshotting-the-keyspace
[benchmark]: https://github.com/etcd-io/etcd/tree/main/tools/benchmark
[benchmark-result]: ../op-guide/performance/
[chubby]: http://static.googleusercontent.com/media/research.google.com/en//archive/chubby-osdi06.pdf
[fio]: https://github.com/axboe/fio
[fio-blog-post]: https://web.archive.org/web/20240726111518/https://prog.world/is-storage-speed-suitable-for-etcd-ask-fio/
[hardware-setup]: ../op-guide/hardware/
[maintenance-compact]: ../op-guide/maintenance/#history-compaction-v3-api-key-value-database
[maintenance-defragment]: ../op-guide/maintenance/#defragmentation
[maintenance-disarm]: https://github.com/etcd-io/etcd/blob/main/etcdctl/README.md#alarm-disarm
[new_issue]: https://github.com/etcd-io/etcd/issues/new
[raft]: https://raft.github.io/raft.pdf
[runtime reconfiguration]: ../op-guide/runtime-configuration/
[supported-platform]: ../op-guide/supported-platform/
[tuning]: ../tuning/
[wal_fsync_duration_seconds]: ../metrics/#disk
```
### 17.7 FAQ 关键数字速查
| 数字 | 含义 | 出处 |
|---|---|---|
| **2GB** | 默认存储配额(`--quota-backend-bytes`) | System requirements |
| **8GB** | 常规环境建议的最大配额,超过会在启动时告警 | System requirements |
| 双核 / 2GB RAM / 80GB SSD(至少) | CoreOS 上常见的 etcd 专用机配置 | System requirements |
| **(n/2)+1** | quorum 计算公式 | Why an odd number of cluster members |
| **7** | 集群规模的经验上限(「大概不应超过 7 个节点」) | What is maximum cluster size |
| **5** | Chubby 建议的节点数;5 成员可容忍 2 个故障 | What is maximum cluster size |
| **50 毫秒** | apply 一个请求的通常耗时上限(含慢盘场景) | apply entries took too long |
| **100 毫秒** | 平均 apply 耗时超过该值即告警 | apply entries took too long |
| **25ms** | `backend_commit_duration_seconds` 的 p99 建议上限 | apply entries took too long |
| **10ms** | `wal_fsync_duration_seconds` 的 p99 建议上限 | failed to send out heartbeat on time |
| **5 \* heartbeat-interval** | `election-timeout` 的最小建议值 | failed to send out heartbeat on time |
| **30 秒 / 1Gbps** | 快照耗时告警的两个触发条件 | snapshotting is taking more than x seconds |
---
## 18. 附录 A:官方页面 → 本册章节映射表
本附录把《etcd-v3.7-抓取清单.md》纳入范围的 **79 个官方页面**逐条映射到本册章节,便于按图索骥回查原文。
**状态说明**:
| 状态 | 含义 |
|---|---|
| **已覆盖** | 该页内容已被本册对应章节采用(并标注了引用来源) |
| **未覆盖 · 兄弟分册** | 该页属《01-核心概念篇》或《02-开发实战篇》范围,本册按定位不展开 |
| **未覆盖 · 本册范围外** | 该页属开发/内部主题,与运维运营无关 |
| **未覆盖 · 素材未提供** | 该页所在批次未纳入本册素材,本册未获取其内容 |
### 18.1 映射表(79 页)
| # | 源文件 | 页面标题 | 分类 | 批次 | 本册状态 |
|---|---|---|---|---|---|
| 1 | `_index.md` | v3.7 docs | 概览/入门 | batch-01 | 未覆盖 · 素材未提供(文档站导航页,属《01-核心概念篇》) |
| 2 | `install.md` | Install | 概览/入门 | batch-01 | 未覆盖 · 素材未提供(安装页,建议并入《01-核心概念篇》) |
| 3 | `quickstart.md` | Quickstart | 概览/入门 | batch-01 | 未覆盖 · 素材未提供(属《01-核心概念篇》) |
| 4 | `demo.md` | Demo | 概览/入门 | batch-01 | 未覆盖 · 素材未提供(属《01-核心概念篇》) |
| 5 | `dev-guide/local_cluster.md` | Set up a local cluster | 开发指南 | batch-01 | 未覆盖 · 素材未提供(本地集群搭建;本册第 2 章以静态/discovery/DNS 三种引导方式覆盖同类需求) |
| 6 | `learning/why.md` | etcd versus other key-value stores | 核心概念/设计 | batch-01 | 未覆盖 · 兄弟分册(《01-核心概念篇》) |
| 7 | `learning/glossary.md` | Glossary | 核心概念/设计 | batch-01 | 未覆盖 · 兄弟分册(《01-核心概念篇》) |
| 8 | `learning/data_model.md` | Data model | 核心概念/设计 | batch-02 | 未覆盖 · 兄弟分册(《01-核心概念篇》) |
| 9 | `learning/api.md` | etcd API | 核心概念/设计 | batch-02 | 未覆盖 · 兄弟分册(《01-核心概念篇》/《02-开发实战篇》) |
| 10 | `learning/api_guarantees.md` | etcd API guarantees | 核心概念/设计 | batch-02 | 未覆盖 · 兄弟分册(《01-核心概念篇》) |
| 11 | `learning/persistent-storage-files.md` | etcd persistent storage files | 核心概念/设计 | batch-02 | 未覆盖 · 兄弟分册(《01-核心概念篇》;本册第 6、7 章涉及 WAL/snap/db 文件但未展开该页) |
| 12 | `dev-guide/limit.md` | System limits | 开发指南 | batch-02 | 未覆盖 · 素材未提供(系统限制,可与第 4 章硬件建议互为补充) |
| 13 | `dev-guide/features.md` | etcd features | 开发指南 | batch-02 | 未覆盖 · 素材未提供 |
| 14 | `learning/design-client.md` | etcd client design | 核心概念/设计 | batch-03 | 未覆盖 · 兄弟分册(《01-核心概念篇》) |
| 15 | `learning/design-learner.md` | etcd learner design | 核心概念/设计 | batch-03 | 未覆盖 · 素材未提供(learner 设计;本册第 5.6 节给出 learner 的运维操作) |
| 16 | `learning/design-auth-v3.md` | learning/design-auth-v3.md | 核心概念/设计 | batch-03 | 未覆盖 · 兄弟分册(v3 认证设计;本册第 10 章覆盖认证与 RBAC 的使用层) |
| 17 | `op-guide/runtime-reconf-design.md` | Design of runtime reconfiguration | 运维 | batch-03 | 未覆盖 · 素材未提供(重配置设计原理;本册第 5 章覆盖操作层) |
| 18 | `dev-guide/interacting_v3.md` | Interacting with etcd | 开发指南 | batch-03 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 19 | `op-guide/clustering.md` | Clustering Guide | 运维 | batch-04 | **已覆盖 · 第 2.2 / 2.3 / 2.4 节** |
| 20 | `op-guide/configuration.md` | Configuration options | 运维 | batch-04 | **已覆盖 · 第 3 章(107 条命令行 flag 全表)** |
| 21 | `op-guide/hardware.md` | Hardware recommendations | 运维 | batch-04 | **已覆盖 · 第 4.1 节** |
| 22 | `op-guide/supported-platform.md` | Supported platforms | 运维 | batch-04 | **已覆盖 · 第 4.2 节** |
| 23 | `feature-gates/index.md` | Feature Gates | 概览/入门 | batch-04 | **已覆盖 · 第 3.12 节** |
| 24 | `tuning.md` | Tuning | 概览/入门 | batch-04 | **已覆盖 · 第 4.3 节** |
| 25 | `op-guide/runtime-configuration.md` | Runtime reconfiguration | 运维 | batch-05 | **已覆盖 · 第 5.2 – 5.8 节** |
| 26 | `op-guide/maintenance.md` | Maintenance | 运维 | batch-05 | **已覆盖 · 第 6 章** |
| 27 | `op-guide/recovery.md` | Disaster recovery | 运维 | batch-05 | **已覆盖 · 第 7 章** |
| 28 | `op-guide/failures.md` | Failure modes | 运维 | batch-05 | **已覆盖 · 第 8 章** |
| 29 | `op-guide/data_corruption.md` | Data Corruption | 运维 | batch-05 | **已覆盖 · 第 9 章** |
| 30 | `op-guide/security.md` | Transport security model | 运维 | batch-06 | **已覆盖 · 第 10.1 – 10.11 节** |
| 31 | `op-guide/authentication/authentication.md` | Authentication | 运维 | batch-06 | **已覆盖 · 第 10.12 节**(源文为 stub,已如实说明) |
| 32 | `op-guide/authentication/rbac.md` | Role-based access control | 运维 | batch-06 | **已覆盖 · 第 10.13 – 10.17 节** |
| 33 | `op-guide/versioning.md` | Versioning | 运维 | batch-06 | **已覆盖 · 第 10.18 节** |
| 34 | `op-guide/monitoring.md` | Monitoring etcd | 运维 | batch-06 | **已覆盖 · 第 11.1 – 11.8 节** |
| 35 | `metrics/_index.md` | Metrics | 概览/入门 | batch-06 | **已覆盖 · 第 11.9 节** |
| 36 | `op-guide/container.md` | Run etcd clusters inside containers | 运维 | batch-07 | **已覆盖 · 第 2.5 节** |
| 37 | `op-guide/kubernetes.md` | Run etcd clusters as a Kubernetes StatefulSet | 运维 | batch-07 | **已覆盖 · 第 2.6 节** |
| 38 | `op-guide/gateway.md` | etcd gateway | 运维 | batch-07 | **已覆盖 · 第 13 章** |
| 39 | `op-guide/grpc_proxy.md` | gRPC proxy | 运维 | batch-07 | **已覆盖 · 第 13 章** |
| 40 | `dev-guide/grpc_naming.md` | gRPC naming and discovery | 开发指南 | batch-07 | 未覆盖 · 本册范围外(客户端命名与发现,属《02-开发实战篇》) |
| 41 | `dev-guide/api_grpc_gateway.md` | Why gRPC gateway | 开发指南 | batch-07 | 未覆盖 · 本册范围外(本册第 10.16 节仅引用其「不能与 TLS CN 认证共用」的限制) |
| 42 | `dev-guide/golang_embed_pkg.md` | Embedding etcd in a Go Application | 开发指南 | batch-07 | 未覆盖 · 本册范围外(Go 嵌入式用法,属《02-开发实战篇》) |
| 43 | `dev-internal/modules.md` | Golang modules | 开发者内部 | batch-07 | 未覆盖 · 本册范围外(模块依赖管理) |
| 44 | `tasks/developer/_index.md` | tasks/developer/_index.md | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》导航页) |
| 45 | `tasks/developer/reading-from-etcd.md` | Reading from etcd | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 46 | `tasks/developer/writing-to-etcd.md` | Writing to etcd | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 47 | `tasks/developer/how-to-get-key-by-prefix.md` | How to get keys by prefix | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 48 | `tasks/developer/how-to-delete-keys.md` | How to delete keys | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 49 | `tasks/developer/how-to-transactional-write.md` | How to make multiple writes in a transaction | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 50 | `tasks/developer/how-to-watch-keys.md` | How to watch keys | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 51 | `tasks/developer/how-to-create-lease.md` | How to create lease | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 52 | `tasks/developer/how-to-create-locks.md` | How to create locks | 任务 | batch-08 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 53 | `tasks/operator/_index.md` | tasks/operator/_index.md | 任务 | batch-09 | **已覆盖 · 第 5.11 节**(源文仅 front matter,已如实说明) |
| 54 | `tasks/operator/how-to-setup-cluster.md` | How to Set Up a Demo etcd Cluster | 任务 | batch-09 | **已覆盖 · 第 2.7 节** |
| 55 | `tasks/operator/how-to-check-cluster-status.md` | How to check Cluster status | 任务 | batch-09 | **已覆盖 · 第 5.1 节** |
| 56 | `tasks/operator/how-to-deal-with-membership.md` | How to Add and Remove Members | 任务 | batch-09 | **已覆盖 · 第 5.5.2 节** |
| 57 | `tasks/operator/how-to-save-database.md` | How to save the database | 任务 | batch-09 | **已覆盖 · 第 6.6 节**(`--endpoints` 只能含一个 endpoint 的约束) |
| 58 | `tasks/operator/how-to-conduct-elections.md` | How to conduct leader election in etcd cluster | 任务 | batch-09 | **已覆盖 · 第 5.9 节** |
| 59 | `dev-guide/discovery_protocol.md` | Discovery service protocol | 开发指南 | batch-09 | **已覆盖 · 第 5.10.3 节**(v2 版) |
| 60 | `dev-internal/discovery_protocol.md` | Discovery service protocol | 开发者内部 | batch-09 | **已覆盖 · 第 5.10.4 节**(v3 版) |
| 61 | `upgrades/upgrading-etcd.md` | Upgrading etcd clusters and applications | 升级 | batch-10 | **已覆盖 · 第 14.1 – 14.3 节** |
| 62 | `upgrades/upgrade_3_6.md` | Upgrade etcd from v3.5 to v3.6 | 升级 | batch-10 | **已覆盖 · 第 14.5 节** |
| 63 | `upgrades/upgrade_3_7.md` | Upgrade etcd from v3.6 to v3.7 | 升级 | batch-10 | **已覆盖 · 第 14.4 节** |
| 64 | `downgrades/downgrading-etcd.md` | Downgrading etcd clusters and applications | 降级 | batch-10 | **已覆盖 · 第 14.6 节** |
| 65 | `downgrades/downgrade_3_7.md` | Downgrade etcd from v3.7 to v3.6 | 降级 | batch-10 | **已覆盖 · 第 14.7 节** |
| 66 | `downgrades/downgrade_3_6.md` | Downgrade etcd from v3.6 to v3.5 | 降级 | batch-10 | **已覆盖 · 第 14.8 节** |
| 67 | `integrations.md` | Libraries and tools | 概览/入门 | batch-11 | **已覆盖 · 第 16.1 节** |
| 68 | `dev-internal/logging.md` | Logging conventions | 开发者内部 | batch-11 | **已覆盖 · 第 11.11 / 16.2 节** |
| 69 | `reporting_bugs.md` | Reporting bugs | 概览/入门 | batch-11 | **已覆盖 · 第 16.3 节** |
| 70 | `triage/_index.md` | Triage | 社区流程 | batch-11 | **已覆盖 · 第 16.4 节**(源文仅 front matter,已如实说明) |
| 71 | `triage/issues.md` | Issue triage guidelines | 社区流程 | batch-11 | **已覆盖 · 第 16.5 节** |
| 72 | `triage/PRs.md` | PR management | 社区流程 | batch-11 | **已覆盖 · 第 16.6 节** |
| 73 | `faq.md` | FAQ | 概览/入门 | batch-11 | **已覆盖 · 第 17 章(全量问答)** |
| 74 | `dev-guide/api_reference_v3.md` | API reference | 开发指南 | batch-12 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 75 | `dev-guide/api_concurrency_reference_v3.md` | API reference: concurrency | 开发指南 | batch-12 | 未覆盖 · 兄弟分册(《02-开发实战篇》) |
| 76 | `benchmarks/etcd-3-demo-benchmarks.md` | Benchmarking etcd v3 | 基准测试 | batch-13 | **已覆盖 · 第 15.1 节** |
| 77 | `benchmarks/etcd-3-watch-memory-benchmark.md` | Watch Memory Usage Benchmark | 基准测试 | batch-13 | **已覆盖 · 第 15.2 节** |
| 78 | `benchmarks/etcd-storage-memory-benchmark.md` | Storage Memory Usage Benchmark | 基准测试 | batch-13 | **已覆盖 · 第 15.3 节** |
| 79 | `__metrics_latest__` | etcd Metrics (latest) | 监控 | batch-13 | **已覆盖 · 第 12 章(123 条指标全表)** |
### 18.2 覆盖统计
| 统计项 | 数量 | 页号 |
|---|---|---|
| 纳入范围页面总数 | 79 | 1–79 |
| **本册已覆盖** | **46** | 19–39、53–73、76–79 |
| 未覆盖 · 兄弟分册(核心概念篇 / 开发实战篇) | 20 | 6–11、14、16、18、44–52、74、75 |
| 未覆盖 · 本册范围外(开发/内部主题) | 4 | 40–43 |
| 未覆盖 · 素材未提供(batch-01/02/03 未纳入本册素材) | 9 | 1–5、12、13、15、17 |
| 合计校验 | 46 + 20 + 4 + 9 = **79** | ✓ |
**已覆盖的 47 页清单**(按章节聚合):
| 章节 | 覆盖页号 |
|---|---|
| 第 2 章(部署) | 19、36、37、54 |
| 第 3 章(配置全参) | 20、23 |
| 第 4 章(硬件与平台) | 21、22、24 |
| 第 5 章(成员与运行时管理) | 25、53、55、56、58、59、60 |
| 第 6 章(日常维护) | 26、57 |
| 第 7 章(灾难恢复) | 27 |
| 第 8 章(故障模式) | 28 |
| 第 9 章(数据损坏) | 29 |
| 第 10 章(安全) | 30、31、32、33 |
| 第 11 章(监控与告警) | 34、35、68 |
| 第 12 章(指标全表) | 79 |
| 第 13 章(代理层) | 38、39 |
| 第 14 章(升级与降级) | 61、62、63、64、65、66 |
| 第 15 章(性能与容量) | 76、77、78 |
| 第 16 章(生态与社区) | 67、68、69、70、71、72 |
| 第 17 章(FAQ) | 73 |
> 说明:第 68 页(Logging conventions)同时服务于第 11.11 节与第 16.2 节,故在上表两个章节中均列出,但页数统计只计一次。
---
## 19. 附录 B:本次未覆盖页面清单
本附录说明**按用户指定「只做 v3.7 主线」而未纳入**的 11 个官方页面,以及本册内部未展开的页面类别。
### 19.1 范围声明(复述)
按用户指定「只做 v3.7 主线」,本手册未覆盖 **v3.0–v3.5 的历史升级/降级步骤页**与 **v2.x benchmark 页**(共 11 页),清单见《etcd-v3.7-抓取清单.md》。
### 19.2 v2.x benchmark 页(4 页)
| # | 源文件 | 页面标题 | 未覆盖原因 |
|---|---|---|---|
| 1 | `benchmarks/etcd-2-1-0-alpha-benchmarks.md` | Benchmarking etcd v2.1.0 | 历史版本内容,非 v3.7 主线;v2 存储引擎已于 v3.x 时代弃用,对 v3.7 容量规划无参考价值 |
| 2 | `benchmarks/etcd-2-2-0-benchmarks.md` | Benchmarking etcd v2.2.0 | 同上 |
| 3 | `benchmarks/etcd-2-2-0-rc-benchmarks.md` | Benchmarking etcd v2.2.0-rc | 同上 |
| 4 | `benchmarks/etcd-2-2-0-rc-memory-benchmarks.md` | Benchmarking etcd v2.2.0-rc-memory | 同上 |
> 💡 补充:本册第 15 章覆盖的是 **v3** 的三份 benchmark(读吞吐、watch 内存、storage 内存)。若确需 v2 时代的对照数据,请回官方站查阅上述 4 页——本册不转述、不估算其数字。
### 19.3 v3.0–v3.5 升级/降级步骤页(7 页)
| # | 源文件 | 页面标题 | 未覆盖原因 |
|---|---|---|---|
| 5 | `upgrades/upgrade_3_0.md` | Upgrade etcd from 2.3 to 3.0 | 历史版本内容,非 v3.7 主线;v3.7 集群无法直接执行该路径的升级 |
| 6 | `upgrades/upgrade_3_1.md` | Upgrade etcd from 3.0 to 3.1 | 同上 |
| 7 | `upgrades/upgrade_3_2.md` | Upgrade etcd from 3.1 to 3.2 | 同上 |
| 8 | `upgrades/upgrade_3_3.md` | Upgrade etcd from 3.2 to 3.3 | 同上 |
| 9 | `upgrades/upgrade_3_4.md` | Upgrade etcd from 3.3 to 3.4 | 同上 |
| 10 | `upgrades/upgrade_3_5.md` | Upgrade etcd from 3.4 to 3.5 | 同上;本册第 14 章只保留 **v3.5 → v3.6** 与 **v3.6 → v3.7** 两步 |
| 11 | `downgrades/downgrade_3_5.md` | Downgrade etcd from 3.5 to 3.4 | 同上;本册第 14 章只保留 **v3.7 → v3.6** 与 **v3.6 → v3.5** 两步 |
> ⚠️ 提示:若现网仍在运行 v3.4 或更早版本,**不能**按本册第 14 章直接跳到 v3.7。按第 14.1 节的总纲原则——**一次只跨一个 minor,且先升到该 minor 的最新 patch**——需先依次完成 `upgrade_3_5`、`upgrade_3_6`,再执行本册第 14.4 节的 v3.6 → v3.7。这些中间步骤请回官方站查阅上表对应页面。
### 19.4 本册内部「未展开」的三类页面
除上述 11 页外,纳入 79 页清单中还有 33 页未被本册采用,原因已在附录 A 逐条标注,此处汇总:
| 类别 | 数量 | 说明 |
|---|---|---|
| 属兄弟分册 | 20 | 《01-核心概念篇》(数据模型、API、设计、glossary 等)与《02-开发实战篇》(读写删 watch lease lock、API reference 等) |
| 本册范围外(开发/内部主题) | 4 | `grpc_naming`、`api_grpc_gateway`、`golang_embed_pkg`、`modules`——均为客户端/嵌入式开发主题 |
| 素材未提供 | 9 | batch-01/02/03 未纳入本册素材(install、quickstart、demo、local_cluster、limit、features、design-learner、runtime-reconf-design 等) |
### 19.5 源文本身「内容缺失」的页面(如实记录)
以下页面已抓取,但源文本身几乎没有正文或有明显未完成的痕迹,本册已在对应章节如实标注,**未做任何补全或推测**:
| 页面 | 情况 | 本册处理位置 |
|---|---|---|
| `tasks/operator/_index.md` | 仅 front matter(5 行),无正文,纯导航 list page | 第 5.11 节 |
| `triage/_index.md` | 仅 front matter,无正文 | 第 16.4 节 |
| `op-guide/authentication/authentication.md` | 仅 1254 字节,源文自称 "This is just a stub which needs to be filled…" | 第 10.12 节 |
| `op-guide/authentication/_index.md` | 仅 front matter,无正文 | 第 10.12 节 |
| `metrics/_index.md` 末尾 | 仅有 "Generated list of metrics" 标题,**正文未提供指标清单** | 第 11.9.8 节(并由第 12 章补全 123 条) |
| `dev-internal/logging.md` | 只覆盖日志级别分类,无结构化字段规范 / logger 用法 / 代码位置约定 | 第 11.11 / 16.2 节 |
### 19.6 全文数据一致性声明
本册所有数字(benchmark 结果、flag 默认值、配额、时间阈值、指标条数)均**原样取自素材**,未做任何换算或估算。素材内部存在冲突或笔误处,本册采用「并列保留 + 标注」而非擅自统一,共 6 处:
| # | 位置 | 冲突内容 | 本册处理 |
|---|---|---|---|
| 1 | 第 3.2 / 6.1 / 14 章 | `--snapshot-count` 默认值:配置表 `10000` vs maintenance「自 v3.2 改为 100000」vs upgrade_3_6「v3.6 由 100000 改为 10000」 | 并列呈现,注明以实际版本 `etcd -h` 为准 |
| 2 | 第 3.12 节 | `CompactHashCheck` 与 `InitialCorruptCheck` 在 Configuration 页与 Feature Gates 页 Details 顺序相反 | 按各自页面原文保留 |
| 3 | 第 14 章 | `upgrade_3_6.md` Step 5 漏 `etcdctl` 前缀 | 按原文保留并标注 |
| 4 | 第 14 章 | `downgrade_3_6.md` Step 6 diff 疑漏 `/etcd` | 按原文保留并标注 |
| 5 | 第 14 章 | deprecated flag 称「共 16 个」但表列 18 行 | 按原文保留并标注 |
| 6 | 第 5.9 节 | `elect` 参数 `leader-name` 与示例 `[proposal]` 命名不一致 | 并列保留,不擅自统一 |