diff --git a/.DS_Store b/.DS_Store index 52daf1a..6e86923 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/AI/.DS_Store b/AI/.DS_Store new file mode 100644 index 0000000..3206c1f Binary files /dev/null and b/AI/.DS_Store differ diff --git a/AI/华为认证/.DS_Store b/AI/华为认证/.DS_Store new file mode 100644 index 0000000..6a42b47 Binary files /dev/null and b/AI/华为认证/.DS_Store differ diff --git a/AI/华为认证/HCIA/HCIA-AI Solution V1.0 培训教材.pdf b/AI/华为认证/HCIA/HCIA-AI Solution V1.0 培训教材.pdf new file mode 100644 index 0000000..43bfdd3 Binary files /dev/null and b/AI/华为认证/HCIA/HCIA-AI Solution V1.0 培训教材.pdf differ diff --git a/AI/华为认证/HCIA/HCIA-AI V4.0 培训教材.pdf b/AI/华为认证/HCIA/HCIA-AI V4.0 培训教材.pdf new file mode 100644 index 0000000..186ac12 Binary files /dev/null and b/AI/华为认证/HCIA/HCIA-AI V4.0 培训教材.pdf differ diff --git a/AI/华为认证/HCIA/HCIA-AI V4.0 实验手册.pdf b/AI/华为认证/HCIA/HCIA-AI V4.0 实验手册.pdf new file mode 100644 index 0000000..5ea56aa Binary files /dev/null and b/AI/华为认证/HCIA/HCIA-AI V4.0 实验手册.pdf differ diff --git a/AI/华为认证/HCIE/HCIE-AI Developer V1.0 培训教材.pdf b/AI/华为认证/HCIE/HCIE-AI Developer V1.0 培训教材.pdf new file mode 100644 index 0000000..dfe00b9 Binary files /dev/null and b/AI/华为认证/HCIE/HCIE-AI Developer V1.0 培训教材.pdf differ diff --git a/AI/华为认证/HCIE/HCIE-AI Developer V1.0 实验手册.pdf b/AI/华为认证/HCIE/HCIE-AI Developer V1.0 实验手册.pdf new file mode 100644 index 0000000..aca7998 Binary files /dev/null and b/AI/华为认证/HCIE/HCIE-AI Developer V1.0 实验手册.pdf differ diff --git a/AI/华为认证/HCIP/.DS_Store b/AI/华为认证/HCIP/.DS_Store new file mode 100644 index 0000000..ff0844d Binary files /dev/null and b/AI/华为认证/HCIP/.DS_Store differ diff --git a/AI/华为认证/HCIP/智能体开发/HCIP-AI-EI Developer V2.5 培训教材.pdf b/AI/华为认证/HCIP/智能体开发/HCIP-AI-EI Developer V2.5 培训教材.pdf new file mode 100644 index 0000000..4f56d35 Binary files /dev/null and b/AI/华为认证/HCIP/智能体开发/HCIP-AI-EI Developer V2.5 培训教材.pdf differ diff --git a/AI/华为认证/HCIP/智能体开发/HCIP-AI-EI Developer V2.5 实验手册.pdf b/AI/华为认证/HCIP/智能体开发/HCIP-AI-EI Developer V2.5 实验手册.pdf new file mode 100644 index 0000000..99c20b1 Binary files /dev/null and b/AI/华为认证/HCIP/智能体开发/HCIP-AI-EI Developer V2.5 实验手册.pdf differ diff --git a/AI/华为认证/HCIP/模型开发/HCIP-AI-Model Developer V2.0 培训教材.pdf b/AI/华为认证/HCIP/模型开发/HCIP-AI-Model Developer V2.0 培训教材.pdf new file mode 100644 index 0000000..a4255cf Binary files /dev/null and b/AI/华为认证/HCIP/模型开发/HCIP-AI-Model Developer V2.0 培训教材.pdf differ diff --git a/AI/华为认证/HCIP/模型开发/HCIP-AI-Model Developer V2.0 实验手册-PyTorch.pdf b/AI/华为认证/HCIP/模型开发/HCIP-AI-Model Developer V2.0 实验手册-PyTorch.pdf new file mode 100644 index 0000000..4e57119 Binary files /dev/null and b/AI/华为认证/HCIP/模型开发/HCIP-AI-Model Developer V2.0 实验手册-PyTorch.pdf differ diff --git a/CICD/.DS_Store b/CICD/.DS_Store new file mode 100644 index 0000000..9c415e1 Binary files /dev/null and b/CICD/.DS_Store differ diff --git a/代码仓库/.DS_Store b/代码仓库/.DS_Store new file mode 100644 index 0000000..37e6dd8 Binary files /dev/null and b/代码仓库/.DS_Store differ diff --git a/大数据/.DS_Store b/大数据/.DS_Store new file mode 100644 index 0000000..8beed24 Binary files /dev/null and b/大数据/.DS_Store differ diff --git a/存储/.DS_Store b/存储/.DS_Store index cbd7e01..9e1db00 100644 Binary files a/存储/.DS_Store and b/存储/.DS_Store differ diff --git a/存储/Ceph/.DS_Store b/存储/Ceph/.DS_Store index 90200bc..0265f3e 100644 Binary files a/存储/Ceph/.DS_Store and b/存储/Ceph/.DS_Store differ diff --git a/存储/华为认证/.DS_Store b/存储/华为认证/.DS_Store new file mode 100644 index 0000000..569a6be Binary files /dev/null and b/存储/华为认证/.DS_Store differ diff --git a/存储/华为认证/HCIA/HCIA-Storage V5.0 培训教材.pdf b/存储/华为认证/HCIA/HCIA-Storage V5.0 培训教材.pdf new file mode 100644 index 0000000..edaa3c7 Binary files /dev/null and b/存储/华为认证/HCIA/HCIA-Storage V5.0 培训教材.pdf differ diff --git a/存储/华为认证/HCIA/HCIA-Storage V5.0 学员手册.pdf b/存储/华为认证/HCIA/HCIA-Storage V5.0 学员手册.pdf new file mode 100644 index 0000000..5731915 Binary files /dev/null and b/存储/华为认证/HCIA/HCIA-Storage V5.0 学员手册.pdf differ diff --git a/存储/华为认证/HCIA/HCIA-Storage V5.0 实验手册.pdf b/存储/华为认证/HCIA/HCIA-Storage V5.0 实验手册.pdf new file mode 100644 index 0000000..3a3108d Binary files /dev/null and b/存储/华为认证/HCIA/HCIA-Storage V5.0 实验手册.pdf differ diff --git a/存储/华为认证/HCIP/HCIP-Storage V6.0 培训教材.pdf b/存储/华为认证/HCIP/HCIP-Storage V6.0 培训教材.pdf new file mode 100644 index 0000000..3b88a54 Binary files /dev/null and b/存储/华为认证/HCIP/HCIP-Storage V6.0 培训教材.pdf differ diff --git a/存储/华为认证/HCIP/HCIP-Storage V6.0 实验手册.pdf b/存储/华为认证/HCIP/HCIP-Storage V6.0 实验手册.pdf new file mode 100644 index 0000000..9b0c11b Binary files /dev/null and b/存储/华为认证/HCIP/HCIP-Storage V6.0 实验手册.pdf differ diff --git a/存储/华为认证/HCIP/HCIP-Storage V6.0 版本说明.pdf b/存储/华为认证/HCIP/HCIP-Storage V6.0 版本说明.pdf new file mode 100644 index 0000000..666bb6d Binary files /dev/null and b/存储/华为认证/HCIP/HCIP-Storage V6.0 版本说明.pdf differ diff --git a/安全/.DS_Store b/安全/.DS_Store new file mode 100644 index 0000000..e00c966 Binary files /dev/null and b/安全/.DS_Store differ diff --git a/安全/华为认证/.DS_Store b/安全/华为认证/.DS_Store new file mode 100644 index 0000000..551a7a4 Binary files /dev/null and b/安全/华为认证/.DS_Store differ diff --git a/安全/华为认证/HCIA/HCIA-Security V4.0 培训教材.pdf b/安全/华为认证/HCIA/HCIA-Security V4.0 培训教材.pdf new file mode 100644 index 0000000..0721ace Binary files /dev/null and b/安全/华为认证/HCIA/HCIA-Security V4.0 培训教材.pdf differ diff --git a/安全/华为认证/HCIA/HCIA-Security V4.0 实验手册.pdf b/安全/华为认证/HCIA/HCIA-Security V4.0 实验手册.pdf new file mode 100644 index 0000000..a4300ae Binary files /dev/null and b/安全/华为认证/HCIA/HCIA-Security V4.0 实验手册.pdf differ diff --git a/安全/华为认证/HCIE/HCIE-Security V3.0 培训教材.pdf b/安全/华为认证/HCIE/HCIE-Security V3.0 培训教材.pdf new file mode 100644 index 0000000..8e9356d Binary files /dev/null and b/安全/华为认证/HCIE/HCIE-Security V3.0 培训教材.pdf differ diff --git a/安全/华为认证/HCIE/HCIE-Security V3.0 实验手册.pdf b/安全/华为认证/HCIE/HCIE-Security V3.0 实验手册.pdf new file mode 100644 index 0000000..097a605 Binary files /dev/null and b/安全/华为认证/HCIE/HCIE-Security V3.0 实验手册.pdf differ diff --git a/安全/华为认证/HCIP/HCIP-Security V4.0 培训教材.pdf b/安全/华为认证/HCIP/HCIP-Security V4.0 培训教材.pdf new file mode 100644 index 0000000..fb461ec Binary files /dev/null and b/安全/华为认证/HCIP/HCIP-Security V4.0 培训教材.pdf differ diff --git a/安全/华为认证/HCIP/HCIP-Security V4.0 实验手册.pdf b/安全/华为认证/HCIP/HCIP-Security V4.0 实验手册.pdf new file mode 100644 index 0000000..fed0797 Binary files /dev/null and b/安全/华为认证/HCIP/HCIP-Security V4.0 实验手册.pdf differ diff --git a/开发/.DS_Store b/开发/.DS_Store new file mode 100644 index 0000000..de92ca2 Binary files /dev/null and b/开发/.DS_Store differ diff --git a/开发/前端/.DS_Store b/开发/前端/.DS_Store new file mode 100644 index 0000000..bd9457f Binary files /dev/null and b/开发/前端/.DS_Store differ diff --git a/开发/后端/.DS_Store b/开发/后端/.DS_Store new file mode 100644 index 0000000..e853236 Binary files /dev/null and b/开发/后端/.DS_Store differ diff --git a/操作系统/.DS_Store b/操作系统/.DS_Store new file mode 100644 index 0000000..c0df6ff Binary files /dev/null and b/操作系统/.DS_Store differ diff --git a/操作系统/Linux/.DS_Store b/操作系统/Linux/.DS_Store new file mode 100644 index 0000000..7e25b5a Binary files /dev/null and b/操作系统/Linux/.DS_Store differ diff --git a/操作系统/Linux/OpenEuler/.DS_Store b/操作系统/Linux/OpenEuler/.DS_Store new file mode 100644 index 0000000..b11b4ac Binary files /dev/null and b/操作系统/Linux/OpenEuler/.DS_Store differ diff --git a/数据中心/.DS_Store b/数据中心/.DS_Store new file mode 100644 index 0000000..6f7cf13 Binary files /dev/null and b/数据中心/.DS_Store differ diff --git a/数据中心/华为认证/.DS_Store b/数据中心/华为认证/.DS_Store new file mode 100644 index 0000000..4b381d1 Binary files /dev/null and b/数据中心/华为认证/.DS_Store differ diff --git a/数据中心/华为认证/HCIA/HCIA-Data Center Facility V2.0 培训教材.pdf b/数据中心/华为认证/HCIA/HCIA-Data Center Facility V2.0 培训教材.pdf new file mode 100644 index 0000000..f64dbfb Binary files /dev/null and b/数据中心/华为认证/HCIA/HCIA-Data Center Facility V2.0 培训教材.pdf differ diff --git a/数据中心/华为认证/HCIE/.DS_Store b/数据中心/华为认证/HCIE/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/数据中心/华为认证/HCIE/.DS_Store differ diff --git a/数据中心/华为认证/HCIE/HCIE-Data Center Facility Design V1.0 培训教材.pdf b/数据中心/华为认证/HCIE/HCIE-Data Center Facility Design V1.0 培训教材.pdf new file mode 100644 index 0000000..06c3269 Binary files /dev/null and b/数据中心/华为认证/HCIE/HCIE-Data Center Facility Design V1.0 培训教材.pdf differ diff --git a/数据中心/华为认证/HCIP/HCIP-Data Center Facility Deployment V2.0 培训教材.pdf b/数据中心/华为认证/HCIP/HCIP-Data Center Facility Deployment V2.0 培训教材.pdf new file mode 100644 index 0000000..a1a36ad Binary files /dev/null and b/数据中心/华为认证/HCIP/HCIP-Data Center Facility Deployment V2.0 培训教材.pdf differ diff --git a/数据中心/华为认证/HCIP/HCIP-Data Center Facility Deployment V2.0 实验手册.pdf b/数据中心/华为认证/HCIP/HCIP-Data Center Facility Deployment V2.0 实验手册.pdf new file mode 100644 index 0000000..e73f7d5 Binary files /dev/null and b/数据中心/华为认证/HCIP/HCIP-Data Center Facility Deployment V2.0 实验手册.pdf differ diff --git a/数据库/.DS_Store b/数据库/.DS_Store new file mode 100644 index 0000000..5ac6d3d Binary files /dev/null and b/数据库/.DS_Store differ diff --git a/数据库/ETCD/etcd-v3.7-学习手册-01-核心概念篇.md b/数据库/ETCD/etcd-v3.7-学习手册-01-核心概念篇.md new file mode 100644 index 0000000..828cb0b --- /dev/null +++ b/数据库/ETCD/etcd-v3.7-学习手册-01-核心概念篇.md @@ -0,0 +1,3872 @@ +# etcd v3.7 学习手册 · 01 核心概念篇 + +> 基于 https://etcd.io/docs/v3.7/ 整理 +> 文档版本:v3.7.0(源仓库 front matter `git_version_tag: v3.7.0`) +> 整理日期:2026-09-18 +> 本册定位:核心概念篇(认知地图)。兄弟分册:《02-开发实战篇》《03-运维运营篇》。 +> 源文获取:GitHub 源站 etcd-io/website main 分支 content/en/docs/v3.7,逐字保真;与 sitemap.xml 交叉验证,共 79 页纳入、0 页失败。 + +--- + +## 目录 + +- [1. 本册导读](#1-本册导读) + - [1.1 本册覆盖的官方页面](#11-本册覆盖的官方页面) + - [1.2 怎么读这册](#12-怎么读这册) + - [1.3 学习路径图](#13-学习路径图) + - [1.4 本册的写作约定](#14-本册的写作约定) +- [2. 概述](#2-概述) + - [2.1 etcd 是什么](#21-etcd-是什么) + - [2.2 etcd 解决什么问题](#22-etcd-解决什么问题) + - [2.3 核心特点](#23-核心特点) + - [2.4 典型用例](#24-典型用例) + - [2.5 适用场景与不适用场景](#25-适用场景与不适用场景) + - [2.6 竞品对比表](#26-竞品对比表) + - [2.7 ZooKeeper](#27-zookeeper) + - [2.8 Consul](#28-consul) + - [2.9 NewSQL](#29-newsql) + - [2.10 用 etcd 存元数据](#210-用-etcd-存元数据) + - [2.11 用 etcd 做分布式协调](#211-用-etcd-做分布式协调) + - [2.12 lock 与 lease 的使用注意](#212-lock-与-lease-的使用注意) +- [3. 术语表](#3-术语表) + - [3.1 全量术语表](#31-全量术语表) + - [3.2 容易混淆的术语辨析](#32-容易混淆的术语辨析) +- [4. 快速上手](#4-快速上手) + - [4.1 环境要求](#41-环境要求) + - [4.2 安装方式一 预编译二进制](#42-安装方式一-预编译二进制) + - [4.3 安装方式二 源码构建](#43-安装方式二-源码构建) + - [4.4 安装方式三 Docker](#44-安装方式三-docker) + - [4.5 安装方式四 操作系统包管理器](#45-安装方式四-操作系统包管理器) + - [4.6 安装检查](#46-安装检查) + - [4.7 五分钟启动单节点](#47-五分钟启动单节点) + - [4.8 启动三节点本地集群](#48-启动三节点本地集群) + - [4.9 测试容错](#49-测试容错) + - [4.10 第一个 put 与 get 示例](#410-第一个-put-与-get-示例) +- [5. 数据模型](#5-数据模型) + - [5.1 设计目标](#51-设计目标) + - [5.2 逻辑视图](#52-逻辑视图) + - [5.3 物理视图](#53-物理视图) + - [5.4 扁平二进制键空间](#54-扁平二进制键空间) + - [5.5 range 与前缀](#55-range-与前缀) + - [5.6 lease 与 key 过期](#56-lease-与-key-过期) + - [5.7 watch 机制](#57-watch-机制) + - [5.8 最小示例](#58-最小示例) + - [5.9 进阶示例](#59-进阶示例) +- [6. etcd API](#6-etcd-api) + - [6.1 gRPC services 总览](#61-grpc-services-总览) + - [6.2 统一请求响应格式](#62-统一请求响应格式) + - [6.3 KV service](#63-kv-service) + - [6.4 Watch service](#64-watch-service) + - [6.5 Lease service](#65-lease-service) + - [6.6 Cluster service](#66-cluster-service) + - [6.7 Maintenance service](#67-maintenance-service) + - [6.8 Auth service](#68-auth-service) + - [6.9 Lock service](#69-lock-service) + - [6.10 Election service](#610-election-service) + - [6.11 方法清单汇总](#611-方法清单汇总) +- [7. API 保证](#7-api-保证) + - [7.1 纳入保证范围的 API](#71-纳入保证范围的-api) + - [7.2 时延特性](#72-时延特性) + - [7.3 持久性 durability](#73-持久性-durability) + - [7.4 严格可串行化](#74-严格可串行化) + - [7.5 原子性](#75-原子性) + - [7.6 线性一致](#76-线性一致) + - [7.7 serializable 读的语义](#77-serializable-读的语义) + - [7.8 watch 的六条保证](#78-watch-的六条保证) + - [7.9 watch 不保证线性一致](#79-watch-不保证线性一致) + - [7.10 lease 语义](#710-lease-语义) + - [7.11 操作完成的定义](#711-操作完成的定义) + - [7.12 revision 语义](#712-revision-语义) +- [8. 持久化存储文件](#8-持久化存储文件) + - [8.1 目录结构与长期存在的文件](#81-目录结构与长期存在的文件) + - [8.2 临时文件](#82-临时文件) + - [8.3 bbolt db 文件](#83-bbolt-db-文件) + - [8.4 WAL 文件](#84-wal-文件) + - [8.5 snapshot 文件](#85-snapshot-文件) + - [8.6 版本间格式变化](#86-版本间格式变化) +- [9. 系统限制](#9-系统限制) + - [9.1 请求大小限制](#91-请求大小限制) + - [9.2 存储大小限制](#92-存储大小限制) + - [9.3 官方文档未提供的其他限制](#93-官方文档未提供的其他限制) +- [10. etcd 特性清单](#10-etcd-特性清单) + - [10.1 三个阶段总览](#101-三个阶段总览) + - [10.2 Experimental](#102-experimental) + - [10.3 Stable](#103-stable) + - [10.4 Unsafe](#104-unsafe) + - [10.5 Feature Deprecation 流程](#105-feature-deprecation) + - [10.6 查看特性清单的方法](#106-查看特性清单的方法) +- [11. 设计原理](#11-设计原理) + - [11.1 客户端设计](#111-客户端设计) + - [11.2 Learner 设计](#112-learner-设计) + - [11.3 认证设计 v3](#113-认证设计-v3) + - [11.4 运行时重配置设计](#114-运行时重配置设计) +- [12. etcdctl v3 交互入门](#12-etcdctl-v3-交互入门) + - [12.1 环境变量与 API 版本](#121-环境变量与-api-版本) + - [12.2 查看版本](#122-查看版本) + - [12.3 写 key put](#123-写-key-put) + - [12.4 读 key get](#124-读-key-get) + - [12.5 读历史版本](#125-读历史版本) + - [12.6 读大于等于指定 key 的 key](#126-读大于等于指定-key-的-key) + - [12.7 删除 key del](#127-删除-key-del) + - [12.8 watch key 变化](#128-watch-key-变化) + - [12.9 watch 历史变更](#129-watch-历史变更) + - [12.10 watch progress](#1210-watch-progress) + - [12.11 压缩 revision compact](#1211-压缩-revision-compact) + - [12.12 lease 三连](#1212-lease-三连) + - [12.13 flag 清单](#1213-flag-清单) + - [12.14 官方文档未提供的命令](#1214-官方文档未提供的命令) +- [13. 附录](#13-附录) + - [13.1 官方页面与章节映射表](#131-官方页面与章节映射表) + - [13.2 本册未覆盖的页面](#132-本册未覆盖的页面) + - [13.3 本册发现的三处源文疑点](#133-本册发现的三处源文疑点) + +--- + +## 1. 本册导读 + +本册是 etcd v3.7 学习手册的第一册,目标是给读者一张**认知地图**:先知道 etcd 是什么、能做什么、不能做什么,再理解它的数据模型与 API 语义,最后读懂四份核心设计文档(客户端、Learner、认证、运行时重配置)。读完本册,你应当能够回答三个问题: + +1. 一条 `put` 从客户端发出到落盘,中间经过了哪些数据结构(keyspace → revision → MVCC → bbolt / WAL)? +2. etcd 给你的**保证边界**在哪里(哪些操作是线性一致的、watch 不保证什么、lease 不保证什么)? +3. 集群成员变更为什么必须"显式两阶段"、learner 为什么存在? + +### 1.1 本册覆盖的官方页面 + +本册共纳入并展开 **20 个官方页面**(含 2 个作为方法清单补充来源的 API 参考页)。逐条清单见 [13.1 官方页面与章节映射表](#131-官方页面与章节映射表)。 + +按主题分组如下: + +| 主题 | 官方页面(源文件) | 本册章节 | +| --- | --- | --- | +| 版本入口 | `_index.md` | [第 1 章](#1-本册导读)、[第 2 章](#2-概述) | +| 定位与选型 | `learning/why.md` | [第 2 章](#2-概述) | +| 术语 | `learning/glossary.md` | [第 3 章](#3-术语表) | +| 安装与起步 | `install.md`、`quickstart.md`、`demo.md`、`dev-guide/local_cluster.md` | [第 4 章](#4-快速上手) | +| 数据模型 | `learning/data_model.md` | [第 5 章](#5-数据模型) | +| API | `learning/api.md`、`dev-guide/api_reference_v3.md`、`dev-guide/api_concurrency_reference_v3.md` | [第 6 章](#6-etcd-api) | +| API 保证 | `learning/api_guarantees.md` | [第 7 章](#7-api-保证) | +| 持久化 | `learning/persistent-storage-files.md` | [第 8 章](#8-持久化存储文件) | +| 限制 | `dev-guide/limit.md` | [第 9 章](#9-系统限制) | +| 特性阶段 | `dev-guide/features.md` | [第 10 章](#10-etcd-特性清单) | +| 设计文档 | `learning/design-client.md`、`learning/design-learner.md`、`learning/design-auth-v3.md`、`op-guide/runtime-reconf-design.md` | [第 11 章](#11-设计原理) | +| 命令行交互 | `dev-guide/interacting_v3.md` | [第 12 章](#12-etcdctl-v3-交互入门) | + +### 1.2 怎么读这册 + +| 读者类型 | 建议路径 | 说明 | +| --- | --- | --- | +| 第一次接触 etcd | 1 → 2 → 3 → 4 → 5 → 12 | 先建立概念与手感,命令能跑通再说 | +| 应用开发者 | 1 → 3 → 5 → 6 → 7 → 11.1 → 12 | 数据模型与 API 保证是写对代码的前提 | +| 运维 / SRE | 1 → 2 → 4 → 8 → 9 → 10 → 11.2 → 11.4 | 落盘文件、限制、成员变更是日常运维主战场 | +| 要选型的人 | 2(全章,尤其是 2.6–2.12) | 对比表与 lock/lease 注意事项直接决定选型 | +| 要排查故障的人 | 5 → 7 → 8 → 11.1 | revision 语义 + WAL/snapshot 结构 + 客户端 failover | + +阅读时的三条硬建议: + +1. **第 5 章(数据模型)与第 7 章(API 保证)不要跳**。etcd 绝大多数"反直觉"行为都能在这两章找到解释,例如"为什么 `get --rev=N` 报 `required revision has been compacted`"、为什么 watch 会收到 `compact_revision`。 +2. **第 11 章是设计文档,不是操作手册**。原文明确写了"是否 promote 由集群运维做最终决定""etcd 决定不支持强制移除成员"这类**立场**,读的时候要区分"etcd 保证了什么"与"etcd 建议你怎么做"。 +3. **凡是标注 `> 💡 补充:` 的内容都不是官方文档原文**,是基于工程经验的补充,请以官方文档为准做最终判断。 + +### 1.3 学习路径图 + +```text + etcd v3.7 认知地图 + ┌──────────────────────────────────────────────────────────┐ + │ 第 1 层 是什么 / 为什么 │ + │ ├─ 第 2 章 概述:定位、用例、竞品对比(why.md) │ + │ └─ 第 3 章 术语表(glossary.md) │ + └───────────────────────┬──────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────────────────┐ + │ 第 2 层 跑起来 │ + │ ├─ 第 4 章 安装(4 种方式)+ 单节点 / 三节点本地集群 │ + │ └─ 第 12 章 etcdctl v3 交互入门 │ + └───────────────────────┬──────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────────────────┐ + │ 第 3 层 数据是怎么组织的 │ + │ ├─ 第 5 章 数据模型:keyspace / revision / MVCC / │ + │ │ triple(major,sub,type) / b+tree │ + │ └─ 第 8 章 持久化文件:snap/db、WAL、*.snap │ + └───────────────────────┬──────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────────────────┐ + │ 第 4 层 对外契约 │ + │ ├─ 第 6 章 API:KV / Watch / Lease / Cluster / │ + │ │ Maintenance / Auth / Lock / Election │ + │ └─ 第 7 章 API 保证:durability、strict serializability │ + │ linearizability、watch 六条保证 │ + └───────────────────────┬──────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────────────────┐ + │ 第 5 层 边界与内功 │ + │ ├─ 第 9 章 系统限制(1.5 MiB / 2 GiB / 8 GiB) │ + │ ├─ 第 10 章 特性阶段(experimental / stable / unsafe) │ + │ └─ 第 11 章 设计原理:客户端 / Learner / 认证 / 重配置 │ + └──────────────────────────────────────────────────────────┘ +``` + +### 1.4 本册的写作约定 + +| 约定 | 说明 | +| --- | --- | +| 语言 | 正文中文;命令、参数名、字段名、proto 消息名、Go 标识符一律保留英文原文,不翻译 | +| 表格优先 | 参数说明、对比、映射一律用表格,不用段落罗列 | +| 代码块 | 每个代码块都标注语言(`bash` / `go` / `protobuf` / `json` / `text` / `console` 等) | +| 引用官方原文 | 用 `> 引自:[页面标题](URL)` 标注出处 | +| 个人补充 | 用 `> 💡 补充:` 显式标注,绝不与原文陈述混写 | +| 官方未提供 | 素材标注"源文未提供/未包含"的,如实写"官方文档未提供" | +| 数字与默认值 | 原样保留,不做换算、不做四舍五入 | + +> 💡 补充:本册所有 `etcdctl` 示例默认已执行 `export ETCDCTL_API=3`,且默认 endpoint 为 `localhost:2379`。如果你的 `etcdctl` 来自 v3.3 或更早版本,默认 API 是 v2,必须显式设置该环境变量(见 [12.1](#121-环境变量与-api-版本))。 + +--- + +## 2. 概述 + +> 引自:[v3.7 docs](https://etcd.io/docs/v3.7/)、[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) + +### 2.1 etcd 是什么 + +etcd 的名字来自两个概念:unix 的 **`/etc`** 目录和 **"d"istributed systems**。 + +> 引自:[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) +> +> `/etc` 是单机存放配置数据的地方,而 etcd 存放的是**大规模分布式系统**的配置信息,因此"分布式的 `/etc`"就是 "etcd"。 + +官方定位原话要点: + +| 维度 | 官方表述 | +| --- | --- | +| 设计目标 | 大规模分布式系统的**通用底座**(general purpose substrate) | +| 核心取舍 | 这类系统绝不容忍 **split-brain(脑裂)**,并**愿意为此牺牲可用性** | +| 存储内容 | 以一致且容错的方式存储**元数据** | +| 集群目标 | 在**稳定性(stability)、可靠性(reliability)、可扩展性(scalability)、性能(performance)** 四方面都做到 best of class 的键值存储 | +| 典型用途 | **配置管理(configuration management)、服务发现(service discovery)、协调分布式工作(coordinating distributed work)** | + +### 2.2 etcd 解决什么问题 + +一句话:**在不可靠的网络与机器之上,提供一个不会脑裂、顺序可推理、变更可订阅的元数据存储。** + +拆成四个具体问题: + +| 问题 | etcd 的解答 | +| --- | --- | +| 多个节点如何就"某个配置的值"达成一致? | 单一复制组 + **raft 共识**;每次修改分配全局唯一且递增的 **revision** | +| 如何知道配置"刚才变了"? | **watch API**:从给定 revision(当前或历史)持续流式推送事件,绝不静默丢弃事件 | +| 如何知道某个进程/机器还活着? | **lease**:短期的、可续约的契约,到期自动删除关联 key | +| 如何让多个进程互斥地干活? | **Lock / Election RPC** + 基于 **revision number / version number 校验**的互斥 | + +### 2.3 核心特点 + +以下特点均可在官方页面中找到原文依据(出处标注在"官方依据"列): + +| 特点 | 说明 | 官方依据 | +| --- | --- | --- | +| 强一致,不容忍脑裂 | 愿意为一致性牺牲可用性 | `learning/why.md` | +| MVCC 多版本数据模型 | 保留 key 的历史版本,支持 time travel queries | `learning/data_model.md` | +| 可靠的变更通知 | watch 绝不静默丢弃事件;事件有序、唯一、可靠、原子、可恢复、可打书签 | `learning/api_guarantees.md` | +| lease 原语把连接与会话解耦 | client 断连不立即丢锁,由 TTL 决定 | `learning/why.md` | +| 安全的分布式共享锁 API | Lock / Election RPC,基于版本号校验(compare and swap) | `learning/why.md` | +| 动态集群成员重配置 | 运行时重配置 API + learner 机制 | `learning/why.md`、`op-guide/runtime-reconf-design.md` | +| gRPC 协议 + HTTP/JSON | 客户端协议基于 gRPC,可序列化为 JSON over HTTP,连 `curl` 也能通信 | `learning/why.md` | +| 扁平二进制键空间 + 区间查询 | 不是目录树,range 比目录更强大 | `learning/api.md`、`learning/data_model.md` | + +### 2.4 典型用例 + +官方给出的两个真实用例: + +**用例一:Container Linux by CoreOS** + +| 项 | 内容 | +| --- | --- | +| 场景 | 运行其上的应用可获得**自动、零停机的 Linux 内核更新** | +| etcd 的角色 | Container Linux 用 **locksmith** 协调更新 | +| 机制 | locksmith 在 etcd 之上实现**分布式信号量**,确保任意时刻集群中只有一部分节点在重启 | + +**用例二:Kubernetes** + +| 项 | 内容 | +| --- | --- | +| 存什么 | 把**配置数据**存入 etcd,用于服务发现和集群管理 | +| 为什么必须一致 | etcd 的一致性对**正确调度和运行服务**至关重要 | +| API server 怎么用 | 把**集群状态**持久化到 etcd | +| 变更如何扩散 | 使用 etcd 的 **watch API** 监控集群、滚动推出关键配置变更 | + +除这两个之外,官方列出的**常见使用模式**(common patterns)包括: + +- **leader election(领导者选举)** +- **distributed locks(分布式锁)** +- **monitoring machine liveness(监控机器存活)** + +### 2.5 适用场景与不适用场景 + +下表左侧为官方原文结论,右侧标注出处;凡属"由原文选型建议推导"的行,已用 💡 标注为整理而非原文原句。 + +| 场景 | 是否适合 | 官方依据 / 说明 | +| --- | --- | --- | +| 存储**元数据**(配置、服务发现、集群状态) | ✅ 适合 | `why.md`:etcd 在单个一致复制组内复制全部数据,对"最多几 GB 且需要一致排序的数据"最高效 | +| 需要**全局强排序**的协调(如协调进程) | ✅ 适合 | `why.md`:应用主要处理元数据或元数据顺序 → 选 etcd | +| 分布式协调原语(watch、lease、election、lock) | ✅ 适合 | `why.md`:etcd **开箱即用**提供,由 etcd 开发者维护和支持 | +| 需要**跨数据中心的大型数据存储** | ❌ 不适合 | `why.md`:选 NewSQL;且 etcd 复制**无法水平扩展**(无 data sharding) | +| 数据**超过几 GB** | ❌ 不适合 | `why.md` 原句:"若数据超过几 GB,或需要完整 SQL 查询,选 NewSQL 数据库" | +| 需要**完整 SQL 查询** | ❌ 不适合 | 同上;etcd 是 KVS,不是 SQL 数据库 | +| 端到端集群**服务发现**框架(含健康检查/DNS) | ❌ 不够 | `why.md`:etcd 特性不够,应选 **Kubernetes、Consul 或 SmartStack** | +| 保护 **etcd 之外的外部资源**的互斥 | ❌ 不适合 | `why.md`:etcd 的 lock 特性**不能**用于保护外部资源,那些资源必须自己提供版本号校验与副本一致性 | + +> 💡 补充:把上表浓缩成一句工程口诀——**"etcd 存的是'关于系统的信息',不是'系统的业务数据'"**。一旦发现自己想把业务表塞进 etcd、或者想对 etcd 里的数据做 JOIN,就是选错工具的信号。 + +### 2.6 竞品对比表 + +> 引自:[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) + +**作者声明(原文保留)**:本文档由 etcd 团队撰写,作者的专长与偏见明显偏向 etcd,仅供参考。 + +表头四列:**etcd / ZooKeeper / Consul / NewSQL (Cloud Spanner, CockroachDB, TiDB)**。以下逐项照录,一个不漏: + +| 维度 | etcd | ZooKeeper | Consul | NewSQL | +| --- | --- | --- | --- | --- | +| Concurrency Primitives | Lock RPCs、Election RPCs、命令行 locks、命令行 elections、Go 的 recipes | Java 编写的外部 curator recipes | Native lock API | Rare(极少),if any | +| Linearizable Reads | Yes | No | Yes | Sometimes | +| Multi-version Concurrency Control | Yes | No | No | Sometimes | +| Transactions | Field compares, Read, Write | Version checks, Write | Field compare, Lock, Read, Write | SQL-style | +| Change Notification | Historical and current key intervals | Current keys and directories | Current keys and prefixes | Triggers (sometimes) | +| User permissions | Role based | ACLs | ACLs | Varies(per-table GRANT、per-database roles) | +| HTTP/JSON API | Yes | No | Yes | Rarely | +| Membership Reconfiguration | Yes | >3.5.0 | Yes | Yes | +| Maximum reliable database size | Several gigabytes | Hundreds of megabytes(有时 several gigabytes) | Hundreds of MBs | Terabytes+ | +| Minimum read linearization latency | Network RTT | No read linearization | RTT + fsync | Clock barriers (atomic, NTP) | + +逐行解读(解读为整理,原句见上表): + +| 维度 | 怎么读这一行 | +| --- | --- | +| Concurrency Primitives | etcd 把锁与选举做成 **RPC + 命令行 + Go recipes** 三层;ZooKeeper 依赖外部 Java 库 curator;NewSQL 基本没有 | +| Linearizable Reads | ZooKeeper **不提供**线性化读,这是它与 etcd 最实质的差异之一 | +| Multi-version Concurrency Control | 只有 etcd 明确 Yes;这是 time travel query 与 STM 的基础 | +| Transactions | etcd 是"字段比较 + 读 + 写";ZooKeeper 是"版本检查 + 写";NewSQL 是 SQL 风格 | +| Change Notification | etcd 能订阅**历史区间和当前**;ZooKeeper 只能看当前 key 与目录;Consul 是当前 key 与前缀 | +| User permissions | etcd 是 **Role based**(基于角色,见 [11.3](#113-认证设计-v3));ZooKeeper / Consul 是 ACL | +| HTTP/JSON API | etcd 有(gRPC 可序列化为 JSON over HTTP,因此 `curl` 也能用);ZooKeeper 没有 | +| Membership Reconfiguration | ZooKeeper 要 **>3.5.0** 才支持 | +| Maximum reliable database size | etcd 是 **several gigabytes**;Consul 是 **hundreds of MBs**;NewSQL 是 **Terabytes+** | +| Minimum read linearization latency | etcd = **Network RTT**;ZooKeeper 无读线性化;Consul = **RTT + fsync**;NewSQL 依赖 **Clock barriers(atomic、NTP)** | + +### 2.7 ZooKeeper + +> 引自:[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) + +**关系**:ZooKeeper 与 etcd 解决同一问题(分布式系统协调与元数据存储)。但 etcd 有后见之明,吸取了 ZooKeeper 在设计与实现上的工程和运维经验,这些教训帮助 etcd 支撑起 Kubernetes 这样的大规模系统。 + +**etcd 相对 Zookeeper 的六项改进(原文列举,逐条保留)**: + +| # | 改进项 | 原文英文 | +| --- | --- | --- | +| ① | 动态集群成员重配置 | Dynamic cluster membership reconfiguration | +| ② | 高负载下稳定的读写 | Stable read/write under high load | +| ③ | 多版本并发控制数据模型 | A multi-version concurrency control data model | +| ④ | 可靠的 key 监控,绝不静默丢弃事件 | Reliable key monitoring which never silently drop events | +| ⑤ | 把连接与会话解耦的 lease 原语 | Lease primitives decoupling connections from sessions | +| ⑥ | 安全的分布式共享锁 API | APIs for safe distributed shared locks | + +**协议与生态差异**: + +| 项 | ZooKeeper | etcd | +| --- | --- | --- | +| RPC 协议 | 自有的 **Jute** 协议(Zookeeper 独有,限制了其 language bindings) | 基于 **gRPC** | +| 语言绑定 | 受 Jute 限制 | 有 **go、C++、Java** 等语言绑定;且 gRPC 可序列化为 JSON over HTTP | +| 命令行可达性 | — | 连 `curl` 这样的通用命令行工具也能与之通信 | +| 开箱即用支持 | — | 开箱即用地支持大量语言和框架 | + +**结论(原文)**:综合考虑特性、支持与稳定性,新应用若打算用 Zookeeper 做一致性键值存储,**最好选择 etcd**。 + +### 2.8 Consul + +> 引自:[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) + +**Consul 是什么**:一个**端到端的服务发现框架**,内置**健康检查、故障检测和 DNS 服务**,另外暴露一个带 **RESTful HTTP API** 的键值存储。 + +**以 Consul 1.0 的情况为准**,官方给出的评价: + +| 评价项 | 原文结论 | +| --- | --- | +| 扩展性 | 其存储系统在键值操作上的扩展性**不如 etcd 或 Zookeeper** | +| 规模问题 | 需要**数百万 key** 的系统会遭受**高延迟和内存压力** | +| 键值 API 最明显的三项缺失 | ① **multi-version keys(多版本 key)**;② **conditional transactions(条件事务)**;③ **reliable streaming watches(可靠的流式 watch)** | + +**结论(原文,两句分开记)**: + +- 要**分布式一致键值存储** → 选 **etcd** 优于 Consul。 +- 要**端到端的集群服务发现** → etcd 特性不够,应选 **Kubernetes、Consul 或 SmartStack**。 + +### 2.9 NewSQL + +> 引自:[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) + +**代表产品(原文点名)**:**Cockroach、TiDB、Google Spanner**(表头写作 Cloud Spanner, CockroachDB, TiDB)。 + +**共同点与差异**: + +| 项 | 内容 | +| --- | --- | +| 共同点 | etcd 与 NewSQL 都提供**强数据一致性保证与高可用性** | +| 差异根源 | **系统设计参数**显著不同 → 客户端 API 与性能特征也显著不同 | +| NewSQL 的设计取向 | 面向**跨数据中心水平扩展**:把数据分区到多个一致的复制组(shards,可能相距很远),存储 **TB 级及以上**数据集 | +| 为何不适合做分布式协调 | 等待时钟带来**长延迟**;且假定更新具有 **mostly localized dependency graphs** | +| 数据组织 | 组织成 **tables**,提供**SQL 风格**查询能力,语义比 etcd 更丰富 | +| 代价 | 额外的**查询处理、规划与优化复杂度** | + +**结论(原文)**:存储元数据或协调分布式应用选 etcd;**若数据超过几 GB,或需要完整 SQL 查询,选 NewSQL 数据库**。 + +### 2.10 用 etcd 存元数据 + +> 引自:[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) — "Using etcd for metadata" + +这一段是理解 etcd 性能特征的**关键**,逐条拆开: + +| 机制 | 说明 | +| --- | --- | +| 复制范围 | etcd 在**单个一致复制组内**复制全部数据 | +| 高效场景 | 对"存储**最多几 GB** 且需要**一致排序**的数据",这是最高效的做法 | +| 全局唯一 ID | 集群状态的每次修改(可能改动多个 key)都会被赋予一个全局唯一 ID,称为 **revision**,来自一个**单调递增计数器**,用于推导顺序 | +| 提交路径 | 因为只有一个复制组,修改请求只需经过 **raft 协议**即可提交 | +| 收益 | 把共识限制在单一复制组内,etcd 用**简单协议**获得了分布式一致性,同时实现**低延迟与高吞吐** | +| 代价 | etcd 的复制**无法水平扩展,因为它没有数据分片(data sharding)** | + +**与 NewSQL 的对照**: + +| 项 | etcd | NewSQL | +| --- | --- | --- | +| 数据分片 | 无(单复制组) | 通常把数据分片到多个一致复制组 | +| 容量 | 最多几 GB | TB 级 | +| 全局 ID 分配成本 | 由单一复制组的单调递增计数器直接给出 | 每个请求都必须**额外经过复制组之间的协调协议** | +| 冲突后果 | 无 | 这一步可能在全局 ID 上**发生冲突**,迫使**有序请求重试** | +| 结果 | 简单、低延迟、高吞吐 | 在严格排序场景上方案更复杂、性能通常比 etcd 更差 | + +**选型建议(原文)**:应用主要处理元数据或元数据顺序(如协调进程)→ 选 **etcd**;需要跨多数据中心的大型数据存储、且不强依赖全局强排序属性 → 选 **NewSQL**。 + +### 2.11 用 etcd 做分布式协调 + +> 引自:[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) — "Using etcd for distributed coordination" + +**开箱即用提供的四个原语**: + +1. **event watches** +2. **leases** +3. **elections** +4. **distributed shared locks**(注意:分布式共享锁有一些**非显而易见**的性质,见 [2.12](#212-lock-与-lease-的使用注意)) + +**为什么不该把这些原语交给外部库**(原文论证链): + +| 论据 | 原文表述 | +| --- | --- | +| 责任归属 | 把这些原语交给外部库,等于**回避了开发基础分布式软件的责任**,本质上使系统不完整 | +| NewSQL 的现状 | NewSQL 数据库通常期待这些原语由**第三方**编写 | +| ZooKeeper 的现状 | ZooKeeper 著名地有一个**独立分离的**协调 recipes 库 —— **curator** | +| Consul 的现状 | Consul 虽有原生 lock API,却要为其 **"not a bulletproof method"** 道歉 | +| 算法风险 | 理论上可以在任何强一致存储之上构建这些原语,但算法往往很**微妙**,很容易写出一个**看起来能用、却因 thundering herd(惊群)和 timing skew(时序偏移)突然失效**的锁算法 | +| 不止强一致 | etcd 支持的其他原语(如 **transactional memory**)依赖 etcd 的 **MVCC** 数据模型,**仅有强一致性是不够的** | + +**结论(原文)**:分布式协调选 etcd 可避免运维麻烦并节省工程投入。 + +### 2.12 lock 与 lease 的使用注意 + +> 引自:[etcd versus other key-value stores](https://etcd.io/docs/v3.7/learning/why/) — "Notes on the usage of lock and lease" + +**这一节是全篇最容易被忽略、也最容易踩坑的一节。** + +#### lease 机制的基本思想 + +| 步骤 | 内容 | +| --- | --- | +| 1 | 服务端向请求方客户端授予一个称为 **lease** 的 token | +| 2 | 授予时关联一个 **TTL** | +| 3 | 当服务端检测到经过的时间超过 TTL,就**撤销(revoke)**该 lease | +| 4 | 客户端持有未被撤销的 lease 期间,可以宣称自己拥有与该 lease 关联的资源的访问权(在 etcd 中该资源就是 **keyspace 中的一个 key**) | + +#### 三条必须记住的限制 + +| # | 限制 | 原文依据 | +| --- | --- | --- | +| 1 | **lock APIs 本身不能当作互斥机制使用**;之所以叫 lock 只是出于**历史原因(Chubby)**;lock APIs 只能作为互斥的**优化机制** | 原文明确 | +| 2 | **lease 机制本身并不保证互斥**,持有 lease 不能保证持有资源的锁 | 原文明确;根因:**TTL 被定义为物理时间间隔**,服务端与客户端各自用自己的时钟度量时间流逝,允许出现"服务端已撤销 lease、客户端仍宣称自己持有该 lease"的情况 | +| 3 | **etcd 的 lock 特性不能用于保护外部资源** | 原文明确;若用户要保护与 etcd 无关的外部资源,那些资源必须**自己提供版本号校验机制和副本一致性** | + +#### 真正的互斥是怎么实现的 + +etcd 中控制对自身 key 的互斥访问时,互斥是基于**版本号校验(version number validation)**实现的(在 Consul 等系统中也称为 **compare and swap**): + +| 机制 | 说明 | +| --- | --- | +| 做法 | 在 `Put`、`Txn` 这类 etcd RPC 中,可以为操作指定关于 **revision number** 和 **lease ID** 的**必要条件** | +| 失败语义 | 条件不满足则操作失败 | +| 如何知道拿到锁 | 借助该机制 etcd 为客户端提供分布式锁 —— 即**客户端的请求被 etcd 集群成功完成时,它才知道自己获得了某个 key 的锁** | + +#### 与分布式锁文献中三个经典概念的对应 + +| 文献 / 概念 | 出处 | 与 etcd 的对应关系(作者观点) | +| --- | --- | --- | +| **sequencer** | Chubby 论文 | 作者认为它几乎等同于 etcd 的 **revision number + lease ID 的组合** | +| **fencing token** | Martin Kleppmann,*How to do distributed locking* | 作者认为在 etcd 场景下它就是 **revision number** | +| **Thor** | *Practical Uses of Synchronized Clocks in Distributed Systems* | 基于**版本号校验与 lease** 实现分布式锁 | + +#### 既然有版本号校验,为什么还要 lease? + +原文答案:**lease 是一种优化机制,用于减少被中止(aborted)的请求数量。** + +> 💡 补充:这一点在实践中的含义是——不要把"我持有 lease"当作"我持有锁"来写业务代码。正确的写法是:把 lease 当作"保活 + 自动清理"(进程挂了锁能自动释放),把 `Txn` 的 revision 比较当作"我是否真的拿到锁"的判据。etcd 官方 Go 客户端的 `concurrency.Session` / `concurrency.Mutex` 就是按这个思路封装的。 + +--- + +## 3. 术语表 + +> 引自:[Glossary](https://etcd.io/docs/v3.7/learning/glossary/) + +本页定义 etcd **文档、命令行和源码**中使用的术语。术语名保留英文原文,中文为解释。 + +### 3.1 全量术语表 + +下表按官方文档出现顺序逐条列出(共 **24** 条术语名): + +| # | 术语(英文原文) | 中文解释(依据官方定义) | +| --- | --- | --- | +| 1 | **Alarm** | 告警。当集群需要**运维人员介入**才能保持可靠时,etcd server 会触发一个 alarm | +| 2 | **Authentication** | 鉴权。管理用户对 etcd 资源的访问权限 | +| 3 | **Client** | 客户端。连接到 etcd 集群并发起服务请求,例如获取键值对、写入数据、或 watch 更新 | +| 4 | **Cluster** | 集群。由若干 member 组成;每个 member 中的节点遵循 **raft** 共识协议复制日志;集群接收来自 member 的 proposal,提交(commit)后应用到本地 store | +| 5 | **Compaction** | 压缩。丢弃给定 revision 之前的**全部 etcd 事件历史与被取代的 key**,用于回收 etcd 后端数据库的存储空间 | +| 6 | **Election** | 选举。etcd 集群在成员间举行选举以选出 leader,这是 raft 共识协议的一部分 | +| 7 | **Endpoint** | 一个指向 etcd 服务或资源的 **URL** | +| 8 | **Key** | 用户定义的标识符,用于在 etcd 中存储和检索用户定义的值 | +| 9 | **Key range** | key 范围。一组 key,可以是**单个 key**、一个**字典序区间**(所有满足 `a < x <= b` 的 x),或**大于某个给定 key 的所有 key** | +| 10 | **Keyspace** | 键空间。一个 etcd 集群中**所有 key 的集合** | +| 11 | **Lease** | 租约。一个**短期的、可续约的契约(short-lived renewable contract)**,在其到期时删除与之关联的 key | +| 12 | **Member** | 成员。一个参与为 etcd 集群提供服务的**逻辑 etcd server** | +| 13 | **Modification Revision** | 修改版本号。即**最后一次**写入某个 key 的**第一个** revision(the first revision to hold the last write to a given key) | +| 14 | **Peer** | 同一集群中的**另一个 member** | +| 15 | **Proposal** | 提案。一种需要**经过 raft 协议**的请求,例如写请求、配置变更请求 | +| 16 | **Quorum** | 法定人数。修改集群状态达成共识所需的**活跃 member 数量**;etcd 要求**成员多数派(member majority)**才能达成 quorum | +| 17 | **Revision** | 版本号。一个 **64 位**的**集群范围**计数器,**从 1 开始**,每次 keyspace 被修改时递增 | +| 18 | **Role** | 角色。一组 **key range 上的权限单元**,可授予一组用户用于访问控制 | +| 19 | **Snapshot** | 快照。etcd 集群状态的某个**时间点(point-in-time)**备份 | +| 20 | **Store** | 存储。支撑集群 keyspace 的**物理存储** | +| 21 | **Term** | 任期。raft 算法中与每次 leader 选举关联的一个**单调递增**整数;一个 term 内**只能有一个**被选出的 leader,且 leader 变更时 term 递增 | +| 22 | **Transaction** | 事务。原子执行的一组操作;事务中所有被修改的 key **共享同一个 modification revision** | +| 23 | **Key Version** | key 版本。某个 key **自创建以来的写入次数**,**从 1 开始**;不存在或已删除的 key 其 version 为 **0** | +| 24 | **Watcher** | 观察者。客户端打开一个 watcher 以观察**给定 key range 上的更新** | + +> 📌 计数说明:官方 `learning/glossary.md` 源文共出现 **24 个术语条目**(上表 24 行),而素材批注中写为"术语条目共 23 条"。本册以**源文实际条目数 24** 为准,全量收录,不做删减。 + +### 3.2 容易混淆的术语辨析 + +这几组术语在排查问题时最容易搞混,用表格对照: + +| 组 | 术语 | 作用域 | 从几开始 | 何时递增 / 重置 | 典型误用 | +| --- | --- | --- | --- | --- | --- | +| A | **Revision** | **集群范围**(cluster-wide),64 位 | **1** | 每次 keyspace 被修改时递增 | 把它当成"某个 key 的版本" | +| A | **Key Version** | **单个 key** | **1** | 每次写入递增;**删除重置为 0** | 以为删除后 version 保留 | +| A | **Modification Revision** | **单个 key** | — | 最后一次写入该 key 的**第一个** revision | 与 `mod_revision` 字段混淆(见下) | +| B | **Member** | 逻辑 etcd server | — | — | 与"进程/机器"画等号 | +| B | **Peer** | 同集群中的另一个 member | — | — | 用于指客户端 | +| B | **Endpoint** | 一个 URL | — | — | 与 member ID 混用 | +| C | **Quorum** | 集群 | — | 成员多数派 | 以为"半数"即可(多数派 = 超过一半) | +| C | **Term** | raft 选举周期 | — | leader 变更时递增 | 与 revision 混为一谈 | +| D | **Compaction** | 回收空间 | — | 按给定 revision 丢弃历史 | 以为压缩后还能读旧 revision | +| D | **Snapshot** | 时间点备份 | — | — | 与 bbolt `snap/db` 文件混淆 | +| E | **Proposal** | 需走 raft 的请求 | — | — | 以为读请求也是 proposal | +| E | **Transaction** | 原子操作集合 | — | 事务内所有 key **共享同一 modification revision** | 以为事务内每个操作各有 revision | + +> 💡 补充:术语表里的 **Modification Revision**("最后一次写入的**第一个** revision")与 API 字段 `mod_revision`("最后一次修改时的 revision")在语义上对应同一个量,前者是术语定义、后者是 proto 字段名。读代码时看到 `mod_revision`,就按 Modification Revision 的定义理解。 + +--- + +## 4. 快速上手 + +> 引自:[Install](https://etcd.io/docs/v3.7/install/)、[Quickstart](https://etcd.io/docs/v3.7/quickstart/)、[Demo](https://etcd.io/docs/v3.7/demo/)、[Set up a local cluster](https://etcd.io/docs/v3.7/dev-guide/local_cluster/) + +### 4.1 环境要求 + +| 要求 | 值 / 说明 | 出处 | +| --- | --- | --- | +| 前置阅读一 | **Supported platforms**(`op-guide/supported-platform`) | Install 页 Requirements | +| 前置阅读二 | **Hardware recommendations**(`op-guide/hardware`) | Install 页 Requirements | +| 源码构建所需 Go 版本 | **Go 1.21+**(front matter `minGoVers: 1.21`) | Install 页 front matter | +| 客户端端口 | **2379** | Quickstart / Install / local_cluster | +| peer 端口 | **2380** | Install / local_cluster | +| 版本标签 | **v3.7.0**(`git_version_tag`) | `_index.md` front matter | + +> 💡 补充:`minGoVers: 1.21` 只是**源码构建**的下限,用预编译二进制或 Docker 镜像时不需要本地安装 Go。 + +### 4.2 安装方式一 预编译二进制 + +共 **4 步**: + +| 步骤 | 操作 | +| --- | --- | +| 1 | 从 Releases 页面下载对应平台的压缩包,选择 release **v3.7.0** 或更高版本 | +| 2 | 解压缩,得到一个包含二进制文件的目录 | +| 3 | 把可执行二进制加入 PATH(重命名/移动到已在 PATH 中的目录,如 `/usr/local/bin`;或把上一步生成的目录加入 PATH) | +| 4 | 在 shell 中验证(见下) | + +相关链接(原文给出): + +- Releases 页面:https://github.com/etcd-io/etcd/releases/ +- tagged-release 链接:https://github.com/etcd-io/etcd/releases/tag/v3.7.0 + +验证: + +```bash +$ etcd --version +etcd Version: 3.7.0 +... +``` + +### 4.3 安装方式二 源码构建 + +需要 **Go 1.21+**,共 **5 步**: + +| 步骤 | 命令 / 操作 | +| --- | --- | +| 1 | 下载 zip 包(https://github.com/etcd-io/etcd/archive/v3.7.0.zip )并解压,**或**克隆仓库 | +| 2 | `cd etcd` | +| 3 | `./scripts/build.sh`(产物位于 `bin` 目录) | +| 4 | `export PATH="$PATH:`pwd`/bin"` | +| 5 | `etcd --version` 验证 | + +```bash +$ git clone -b v3.7.0 https://github.com/etcd-io/etcd.git +$ cd etcd +$ ./scripts/build.sh +$ export PATH="$PATH:`pwd`/bin" +$ etcd --version +etcd Version: 3.7.0 +``` + +> 原文注解:若要基于开发分支 `github_branch@HEAD` 构建,则**省略 `-b v3.7.0`** 这个 flag。 + +### 4.4 安装方式三 Docker + +主容器镜像仓库是 **`gcr.io/etcd-development/etcd`**,备用(secondary)是 **`quay.io/coreos/etcd`**。 + +官方给出的完整运行脚本(端口映射 **2379** 客户端 / **2380** peer): + +```bash +ETCD_VER=v3.7.0 + +rm -rf /tmp/etcd-data.tmp && mkdir -p /tmp/etcd-data.tmp && \ + docker rmi gcr.io/etcd-development/etcd:${ETCD_VER} || true && \ + docker run \ + -p 2379:2379 \ + -p 2380:2380 \ + --mount type=bind,source=/tmp/etcd-data.tmp,destination=/etcd-data \ + --name etcd-gcr-${ETCD_VER} \ + gcr.io/etcd-development/etcd:${ETCD_VER} \ + /usr/local/bin/etcd \ + --name s1 \ + --data-dir /etcd-data \ + --listen-client-urls http://0.0.0.0:2379 \ + --advertise-client-urls http://0.0.0.0:2379 \ + --listen-peer-urls http://0.0.0.0:2380 \ + --initial-advertise-peer-urls http://0.0.0.0:2380 \ + --initial-cluster s1=http://0.0.0.0:2380 \ + --initial-cluster-token tkn \ + --initial-cluster-state new \ + --log-level info \ + --logger zap \ + --log-outputs stderr + +docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcd --version +docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdctl version +docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdutl version +docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdctl endpoint health +docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdctl put foo bar +docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdctl get foo +``` + +脚本中出现的配置字段一览表: + +| 类别 | 字段 / 命令 | 值 | +| --- | --- | --- | +| 容器 | 容器名 | `etcd-gcr-${ETCD_VER}` | +| 镜像 | 主仓库 | `gcr.io/etcd-development/etcd:${ETCD_VER}` | +| 镜像 | 备用仓库 | `quay.io/coreos/etcd` | +| 端口 | 客户端 / peer | `2379` / `2380` | +| flag | `--name` | `s1` | +| flag | `--data-dir` | `/etcd-data` | +| flag | `--listen-client-urls` | `http://0.0.0.0:2379` | +| flag | `--advertise-client-urls` | `http://0.0.0.0:2379` | +| flag | `--listen-peer-urls` | `http://0.0.0.0:2380` | +| flag | `--initial-advertise-peer-urls` | `http://0.0.0.0:2380` | +| flag | `--initial-cluster` | `s1=http://0.0.0.0:2380` | +| flag | `--initial-cluster-token` | `tkn` | +| flag | `--initial-cluster-state` | `new` | +| flag | `--log-level` | `info` | +| flag | `--logger` | `zap` | +| flag | `--log-outputs` | `stderr` | +| 验证 | 可执行文件 | `etcd`、`etcdctl`、`etcdutl` 三个 | +| 验证 | 命令 | `etcd --version`、`etcdctl version`、`etcdutl version`、`etcdctl endpoint health`、`etcdctl put foo bar`、`etcdctl get foo` | + +### 4.5 安装方式四 操作系统包管理器 + +**官方免责声明(原文保留)**:通过 OS 包管理器安装的 etcd 可能**版本过时**,因为这类包既不由 etcd 项目自动维护、也不被官方支持,需谨慎使用。 + +| 平台 | 前置条件 | 过程 | 结果 / 结论 | +| --- | --- | --- | --- | +| **MacOS(Homebrew)** | — | `brew update` → `brew install etcd` | `etcd --version` 验证 | +| **Linux(发行版仓库)** | — | 多数主流发行版的官方仓库和包管理器都能装 | 发布的版本可能**显著过时**,因此**强烈不建议**这种方式 | +| **Linux 上的推荐做法** | — | 「预编译二进制」或「使用 Homebrew」 | — | +| **Homebrew on Linux** | `brew update` | `brew install etcd` | 执行 `etcd --version`,输出 `etcd Version: 3.7.0` | + +> 原文注解:Homebrew 可运行在 Linux 上,能提供**较新的软件版本**。 + +**第五种方式(官方指向)**:作为 **Kubernetes** 安装的一部分 —— 指向 `op-guide/kubernetes`,即 *Running etcd as a Kubernetes StatefulSet*。 + +### 4.6 安装检查 + +> 引自:[Install](https://etcd.io/docs/v3.7/install/) — "Installation check" + +想做**稍微更完整**的健全性检查,参见 Quickstart 页面(即本章 [4.7](#47-五分钟启动单节点))。 + +### 4.7 五分钟启动单节点 + +> 引自:[Quickstart](https://etcd.io/docs/v3.7/quickstart/) + +本页目标:**5 分钟**内在本地安装、运行并测试一个**单成员(single-member)** etcd 集群。 + +```text +步骤 1 安装 etcd(预编译二进制或源码),务必执行安装说明的最后一步,确认 etcd 已在 PATH 中 +步骤 2 启动:etcd +步骤 3 另一个终端写入:etcdctl put greeting "Hello, etcd" +步骤 4 同一终端读回:etcdctl get greeting +``` + +**步骤 2 启动**: + +```console +$ etcd +{"level":"info","ts":"2021-09-17T09:19:32.783-0400","caller":"etcdmain/etcd.go:72","msg":... } +⋮ +``` + +> 原文注意:`etcd` 的输出是**日志**(logs,参见 `op-guide/configuration/#logging`),**info 级别的日志可以忽略**。 + +**步骤 3 写入**(在**另一个终端**): + +```console +$ etcdctl put greeting "Hello, etcd" +OK +``` + +**步骤 4 读回**: + +```console +$ etcdctl get greeting +greeting +Hello, etcd +``` + +**What's next?**(路径中的 `{{< param version >}}` 即 `v3.7`): + +| 读者 | 后续页面 | +| --- | --- | +| 开发者 | 探索 gRPC API(`/docs/v3.7/learning/api`);查找语言绑定与工具(`/docs/v3.7/integrations`) | +| 运维 / 管理员 | 搭建多机集群(`/docs/v3.7/op-guide/clustering`);学习如何配置 etcd(`/docs/v3.7/op-guide/configuration`);用 TLS 保护 etcd 集群(`/docs/v3.7/op-guide/security`);调优 etcd(`/docs/v3.7/tuning`) | + +### 4.8 启动三节点本地集群 + +> 引自:[Set up a local cluster](https://etcd.io/docs/v3.7/dev-guide/local_cluster/) + +测试与开发部署**最快最简单**的方式是配置本地集群;生产部署请参考 clustering 章节(`op-guide/clustering/`)。 + +#### 一、Local standalone cluster(本地单节点集群) + +```bash +$ ./etcd +``` + +> 原文注解:若当前工作目录下没有 `etcd` 二进制,它可能在 **`$GOPATH/bin/etcd`** 或 **`/usr/local/bin/etcd`**,按实际位置执行命令。运行起来的 etcd member 在 **`localhost:2379`** 上监听客户端请求。 + +交互: + +```bash +$ ./etcdctl put foo bar +OK +``` + +```bash +$ ./etcdctl get foo +bar +``` + +#### 二、Local multi-member cluster(本地多成员集群) + +etcd git 仓库根目录下提供一个 **`Procfile`**,用于方便地配置本地多成员集群。操作前需先进入 **etcd 源码树根目录**。 + +```bash +$ go install github.com/mattn/goreman@latest +``` + +```bash +$ goreman -f Procfile start +``` + +成员启动后,客户端请求监听端口: + +| 成员 | client 端口 | peer 端口 | +| --- | --- | --- | +| `infra1` | **2379** | **2380** | +| `infra2` | **22379** | **22380** | +| `infra3` | **32379** | **32380** | + +打印成员列表: + +```bash +$ etcdctl --write-out=table --endpoints=localhost:2379 member list +``` + +```text ++------------------+---------+--------+------------------------+------------------------+ +| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | ++------------------+---------+--------+------------------------+------------------------+ +| 8211f1d0f64f3269 | started | infra1 | http://127.0.0.1:2380 | http://127.0.0.1:2379 | +| 91bc3c398fb3c146 | started | infra2 | http://127.0.0.1:22380 | http://127.0.0.1:22379 | +| fd422379fda50e48 | started | infra3 | http://127.0.0.1:32380 | http://127.0.0.1:32379 | ++------------------+---------+--------+------------------------+------------------------+ +``` + +表格列:`ID`、`STATUS`、`NAME`、`PEER ADDRS`、`CLIENT ADDRS`。三个成员状态均为 `started`。 + +存入示例键值对: + +```bash +$ etcdctl put foo bar +OK +``` + +### 4.9 测试容错 + +| 步骤 | 命令 | 说明 | +| --- | --- | --- | +| 1 | (查看 `Procfile` 中列出的进程名,示例取名为 **`etcd2`** 的成员) | 确定要停掉的成员的进程名 | +| 2 | `goreman run stop etcd2` | 停止该成员 | +| 3 | `etcdctl put key hello` → `OK` | 存一个 key(集群仍可写) | +| 4 | `etcdctl get key` → `hello` | 读回该 key | +| 5 | `etcdctl --endpoints=localhost:22379 get key` | 从**已停止的成员**读,应显示连接失败错误 | +| 6 | `goreman run restart etcd2` | 重启被停掉的成员 | +| 7 | `etcdctl --endpoints=localhost:22379 get key` → `hello` | 再从重启后的成员读,应成功 | + +步骤 5 的预期错误输出(原文照录): + +```text +2017/06/18 23:07:35 grpc: Conn.resetTransport failed to create client transport: connection error: desc = "transport: dial tcp 127.0.0.1:22379: getsockopt: connection refused"; Reconnecting to "localhost:22379" +Error: grpc: timed out trying to connect +``` + +> 原文注解:重启成员会重新建立连接,`etcdctl` 此时能成功读到 key。想进一步了解与 etcd 的交互,可阅读 interacting with etcd 章节(`dev-guide/interacting_v3/`),即本册 [第 12 章](#12-etcdctl-v3-交互入门)。 + +### 4.10 第一个 put 与 get 示例 + +> 引自:[Demo](https://etcd.io/docs/v3.7/demo/) + +Demo 页当前正文只有一个小节:**Auth**,演示 `auth`、`user`、`role` 三个子命令做鉴权。先设置环境变量: + +```bash +export ETCDCTL_API=3 +ENDPOINTS=localhost:2379 +``` + +完整脚本(原文照录,注释为原文自带): + +```bash +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 +``` + +流程含义拆解: + +| 阶段 | 命令 | 含义 | +| --- | --- | --- | +| 建 root 角色 | `role add root`、`role get root` | 创建并查看角色 `root` | +| 建 root 用户并授权 | `user add root` → `user grant-role root root` → `user get root` | 创建用户 `root`,把 `root` 角色授予该用户 | +| 建受限角色 | `role add role0` → `role grant-permission role0 readwrite foo` | 创建 `role0`,授予它对 key **`foo`** 的 **`readwrite`** 权限 | +| 建受限用户 | `user add user0` → `user grant-role user0 role0` | 创建 `user0`,把 `role0` 授予它 | +| 开启鉴权 | `auth enable` | 此后**所有客户端请求都要经过鉴权** | +| 验证 1 | `--user=user0:123 put foo bar` | 带用户写 `foo` **成功** | +| 验证 2 | `get foo`(不带 `--user`) | **permission denied**;原因(原文注释):请求没有发起认证,**用户名为空** | +| 验证 3 | `--user=user0:123 get foo` | 可以读到(原文注释:user0 can read the key foo) | +| 验证 4 | `--user=user0:123 get foo1` | `foo1` **不在** `role0` 被授权的 key 上,用于验证越权访问被拒 | + +> 💡 补充:这个 demo 同时演示了 [11.3](#113-认证设计-v3) 里最重要的两个概念——**role 是 key range 上的权限单元**(`role grant-permission role0 readwrite foo`),以及**开启 auth 后未认证请求一律被拒**。注意 `readwrite` 是 etcdctl 命令行层的权限名写法,对应 proto 层的 `Permission` 消息(`permType` / `key` / `range_end`,见 [6.8](#68-auth-service))。 + +--- + +## 5. 数据模型 + +> 引自:[Data model](https://etcd.io/docs/v3.7/learning/data_model/)、[etcd API](https://etcd.io/docs/v3.7/learning/api/) + +### 5.1 设计目标 + +| 设计目标 | 原文表述 | +| --- | --- | +| 存什么数据 | 可靠存储**不频繁更新**的数据 | +| 提供什么能力 | 提供可靠的 **watch 查询** | +| 对外暴露什么 | 暴露 key-value 对的**历史版本**,从而支持低成本的快照与 watch 历史事件(**"time travel queries",时间旅行查询**) | +| 模型特征 | **持久化(persistent)、多版本(multi-version)、并发控制(concurrency-control)** 的数据模型正好契合这些场景 | + +**immutable(不可变)特性**: + +| 要点 | 说明 | +| --- | --- | +| 存储形态 | 数据存放在**多版本持久化 key-value 存储**中 | +| 取代行为 | 当某个 key-value 对被新数据取代时,存储会**保留其旧版本** | +| 不可变性 | 该 key-value 存储实际上是**不可变(immutable)**的:操作**不会原地(in-place)更新**结构,而是始终生成一个**更新后的新结构** | +| 修改之后 | 所有历史版本的 key **依然可访问、可 watch** | +| 防止无限增长 | 可以对存储执行 **compact(压缩)** 来丢弃被取代数据的最旧版本 | + +### 5.2 逻辑视图 + +存储的**逻辑视图**是一个**扁平的二进制 key 空间(flat binary key space)**。key 空间在字节串 key 上维护**字典序(lexically sorted)索引**,因此 **range 查询开销很低**。 + +#### 5.2.1 revision(修订号) + +| 要点 | 说明 | +| --- | --- | +| 初始 revision | 存储创建时初始 revision 为 **1** | +| 何时新建 revision | 每一个**原子性变更操作**(例如一个事务操作,其中可能包含多个操作)都会在 key 空间上创建一个**新的 revision** | +| 旧数据 | 之前 revision 所持有的**全部数据保持不变** | +| 旧版本访问 | 旧版本的 key 仍可通过**先前的 revision** 访问 | +| 索引 | revision **也被索引了**,因此结合 watcher 对 revision 做**范围扫描**是高效的 | +| compaction 的影响 | 若存储被 compact 以节省空间,则 **compact revision 之前的 revision 会被移除** | +| 单调性 | revision 在集群的**整个生命周期内单调递增** | + +#### 5.2.2 generation(代)与 version(版本号) + +| 要点 | 说明 | +| --- | --- | +| generation 定义 | 一个 key 的生命周期横跨一个 **generation**,从**创建**到**删除** | +| 多个 generation | 每个 key 可以有**一个或多个** generation | +| 创建 | 创建 key 会**递增该 key 的 version**;若该 key 在**当前 revision 不存在**则从 **1** 开始 | +| 删除 | 删除 key 会生成一个 **key tombstone(墓碑)**,通过把 **version 重置为 0** 来终结该 key 当前的 generation | +| 修改 | 对 key 的每次修改都会**递增其 version**,因此 version 在**一个 key 的 generation 内单调递增** | + +#### 5.2.3 compaction 对逻辑视图的影响 + +| 影响 | 说明 | +| --- | --- | +| 对 generation | 一旦发生 compaction,**任何在 compaction revision 之前结束的 generation 都会被移除** | +| 对 value | 在 compaction revision 之前设置的值,**除最新一个之外都会被移除** | + +用一个具体例子理解(示例为整理,规则来自原文): + +```text +revision: 1 2 3 4 5 +key foo: [创建 bar] [改为 bar_new] +key foo1: [创建 bar1] [改为 bar1_new] + +若在 revision 5 之后执行 compact 4: + - revision 1..4 中被取代的值被移除 + - 但每个 key 的"最新一个值"保留 + - 此后 get --rev=3 会失败(required revision has been compacted) +``` + +### 5.3 物理视图 + +etcd 把物理数据作为 key-value 对存放在持久化的 **b+tree** 中。 + +| 要点 | 说明 | +| --- | --- | +| 存储结构 | 持久化的 **b+tree** | +| delta 存储 | 存储的每个 revision **只包含相对上一个 revision 的 delta(增量)**,以提高效率 | +| 一对多 | 单个 revision 可能对应树中的**多个 key** | + +#### 5.3.1 key 的 3 元组(triple) + +key-value 对的 key 是一个三元组 **(major, sub, type)**: + +| 分量 | 含义 | +| --- | --- | +| **major** | 持有该 key 的**存储 revision** | +| **sub** | 区分**同一 revision 内**的不同 key | +| **type** | 可选后缀,用于**特殊值**(例如值包含 **tombstone** 时为 **`t`**) | + +#### 5.3.2 value + +key-value 对的 **value** 包含**相对上一 revision 的修改**,即一个 **delta**(而不是完整值)。 + +#### 5.3.3 排序与范围查找 + +| 要点 | 说明 | +| --- | --- | +| 排序 | b+tree **按 key 的字节字典序排序** | +| 高效原因 | 对 revision delta 做**范围查找很快**,这使人能快速找出从一个特定 revision 到另一个 revision 之间的修改 | +| compaction | 会**移除过期的 key-value 对** | + +#### 5.3.4 二级内存 btree 索引 + +| 要点 | 说明 | +| --- | --- | +| 结构 | etcd 另外维护一个**内存中的 btree** 索引,用于加速对 key 的 **range 查询** | +| 索引 key | btree 索引中的 key **就是暴露给用户的存储 key** | +| 索引 value | 是指向持久化 b+tree 中**该次修改**的**指针** | +| compaction | 会移除**失效(dead)指针** | + +#### 5.3.5 整体读取路径 + +```text +1. etcd 先从(内存)btree 取得 revision 信息 +2. 然后用该 revision 作为 key 从(持久化)b+tree 中取 value +``` + +> 原文引用了图示 `data-model-figure-01.png` 说明该读取路径。 + +### 5.4 扁平二进制键空间 + +etcd 数据模型在**扁平二进制 key space** 上索引所有 key,**而非层级目录**。 + +| 要点 | 说明 | +| --- | --- | +| 为何扁平 | 扁平二进制 key space 上索引所有 key,而不是目录树 | +| 表达力 | key 按**区间 `[a, b)`** 列出,etcd 中称为 **"ranges"**;**区间比目录更强大** | +| 示例文件 | 原文引用 `data-model-figure-01.png` | + +### 5.5 range 与前缀 + +#### 5.5.1 range 的表示约定 + +range 由 **`key`** 与 **`range_end`** 两个字段表示: + +| 情形 | 含义 | +| --- | --- | +| `key` | 区间的**第一个 key**,**必须非空** | +| `range_end` | 区间中**最后一个 key 的后一个 key** | +| `range_end` 未给出或为空 | 区间**只包含 `key` 本身** | +| `range_end` 是 `key` 加一(如 `"aa"+1 == "ab"`、`"a\xff"+1 == "b"`) | 区间表示**所有以 `key` 为前缀的 key** | +| `key` 与 `range_end` 都是 `\0` | 区间表示**所有 key** | +| `range_end` 是 `\0` | 区间表示所有**大于等于 `key`** 的 key | + +#### 5.5.2 range 的三种典型用法 + +| 用法 | 编码方式 | 例子 | +| --- | --- | --- | +| 单 key 查找 | `[a, a+1)` | `['a', 'a\x00')` 查找 `'a'` | +| 目录查找 | 按目录深度编码 | — | +| 前缀查找 | `[key, key+1)` / 边界编码 | `['a', 'b')` 查找所有以 `'a'` 为前缀的 key | + +#### 5.5.3 示例:前缀 range + +```bash +$ etcdctl put foo bar +OK +$ etcdctl put foo1 bar1 +OK +$ etcdctl put foo2 bar2 +OK + +$ etcdctl get --prefix foo +foo +bar +foo1 +bar1 +foo2 +bar2 +``` + +```bash +$ etcdctl get foo foo2 +foo +bar +foo1 +bar1 +``` + +> 原文注解(interacting_v3):`etcdctl get foo foo3` 中 **`foo3` 被排除**,因为区间是**半开区间 `[foo, foo3)`**,不含 `foo3`。 + +### 5.6 lease 与 key 过期 + +| 要点 | 说明 | +| --- | --- | +| lease 是什么 | 检测客户端**存活(liveness)**的机制 | +| TTL | 集群授予 lease 一个 **TTL**;若在给定 TTL 周期内 etcd 集群**未收到 keepAlive**,lease 即**过期** | +| 附加限制 | 每个 key **最多只能附加一个 lease** | +| 过期/撤销时 | 附加到该 lease 的**所有 key 都会被删除** | +| 事件 | 每个过期的 key 都会在事件历史中生成一个 **delete 事件** | +| TTL 单位 | **`LeaseGrantRequest.TTL` 是建议的存活时间(秒)** | +| ID | `LeaseGrantRequest.ID` 是请求的 lease ID,**若设为 0,则由 etcd 选择 ID** | + +最小示例: + +```bash +# 授予一个 60 秒 TTL 的 lease +$ etcdctl lease grant 60 +lease 32695410dcc0ca06 granted with TTL(60s) + +# 把 key foo 挂到该 lease 上 +$ etcdctl put --lease=32695410dcc0ca06 foo bar +OK + +# 60 秒后 foo 自动消失 +$ etcdctl get foo +# empty response +``` + +> 💡 补充:lease 过期**不是**精确的"第 60.000 秒删除"。服务端按墙上时钟推进 TTL,且 `LeaseCheckpoint` 只扩展 `RemainingTTL`(见 [8.3](#83-bbolt-db-文件))。实际工程中不要依赖 lease 做秒级精确的定时删除,而应把它当作"最多存活 N 秒"的保活契约。 + +### 5.7 watch 机制 + +| 要点 | 说明 | +| --- | --- | +| 接口性质 | `Watch` API 提供**基于事件的接口**,用于**异步**监控 key 的变化 | +| 工作方式 | etcd watch 从**给定 revision(当前或历史)**持续等待变化,并把 key 更新**流式返回**给客户端 | +| 历史范围 | 整个可用事件历史可从**最后一次 compaction revision** 开始 watch | +| stream 性质 | watch 是**长连接**请求,使用 **gRPC stream** 流式传输事件数据 | +| 双向 | watch stream 是**双向**的:客户端**写流**以建立 watch,**读流**以接收事件 | +| 复用 | **单个 watch stream 可通过给事件打上 per-watch 标识符来复用多个不同的 watch**,有助于降低核心 etcd 集群的**内存占用与连接开销** | + +#### 5.7.1 Event 消息 + +```protobuf +message Event { + enum EventType { PUT = 0; DELETE = 1; } + EventType type = 1; + KeyValue kv = 2; + KeyValue prev_kv = 3; +} +``` + +| 字段 | 说明 | +| --- | --- | +| `Type` | 事件类型:**`PUT`** 表示有新数据存入 key;**`DELETE`** 表示 key 被删除 | +| `KV` | 事件关联的 KeyValue:`PUT` 事件包含**当前 kv 对**,**`kv.Version=1` 的 PUT 事件表示 key 的创建**;`DELETE` 事件包含**被删除的 key**,其 modification revision 被设为**删除时的 revision** | +| `Prev_KV` | 事件发生前**一个 revision** 的 key-value 对;为节省带宽,**仅当 watch 显式开启时才填充** | + +#### 5.7.2 示例:watch 一个 key + +```bash +# 终端 A +$ etcdctl watch foo +# 终端 B:etcdctl put foo bar +PUT +foo +bar +``` + +```bash +# 交互式会话同时 watch 多个 key +$ etcdctl watch -i +$ watch foo +$ watch zoo +# 终端 B:etcdctl put foo bar +PUT +foo +bar +# 终端 B:etcdctl put zoo val +PUT +zoo +val +``` + +### 5.8 最小示例 + +**最小闭环:写 → 读 → 删** + +```bash +export ETCDCTL_API=3 + +$ etcdctl put foo bar +OK + +$ etcdctl get foo +foo +bar + +$ etcdctl del foo +1 # one key is deleted +``` + +**最小闭环:带 revision 的历史读** + +```bash +$ etcdctl put foo bar # revision = 2 +OK +$ etcdctl put foo bar_new # revision = 4 +OK + +$ etcdctl get --rev=2 foo +foo +bar +``` + +**最小闭环:watch** + +```bash +$ etcdctl watch --rev=2 foo +PUT +foo +bar +PUT +foo +bar_new +``` + +> 💡 补充(Go 客户端最小示例,非官方文档原文): + +```go +package main + +import ( + "context" + "fmt" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +func main() { + cli, err := clientv3.New(clientv3.Config{ + Endpoints: []string{"localhost:2379"}, + DialTimeout: 5 * time.Second, + }) + if err != nil { + panic(err) + } + defer cli.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + if _, err := cli.Put(ctx, "foo", "bar"); err != nil { + panic(err) + } + + resp, err := cli.Get(ctx, "foo") + if err != nil { + panic(err) + } + for _, kv := range resp.Kvs { + fmt.Printf("key=%s value=%s create_rev=%d mod_rev=%d version=%d lease=%d\n", + kv.Key, kv.Value, kv.CreateRevision, kv.ModRevision, kv.Version, kv.Lease) + } + fmt.Printf("header.revision=%d raft_term=%d\n", resp.Header.Revision, resp.Header.RaftTerm) +} +``` + +### 5.9 进阶示例 + +#### 5.9.1 用 revision 做"一致的多 key 视图" + +```bash +# 先制造历史 +$ etcdctl put foo bar # revision = 2 +OK +$ etcdctl put foo1 bar1 # revision = 3 +OK +$ etcdctl put foo bar_new # revision = 4 +OK +$ etcdctl put foo1 bar1_new # revision = 5 +OK +``` + +```bash +$ etcdctl get --prefix foo # 最新版本 +foo +bar_new +foo1 +bar1_new + +$ etcdctl get --prefix --rev=4 foo # revision 4 时刻的视图 +foo +bar_new +foo1 +bar1 + +$ etcdctl get --prefix --rev=3 foo # revision 3 时刻的视图 +foo +bar +foo1 +bar1 + +$ etcdctl get --prefix --rev=2 foo # revision 2 时刻的视图 +foo +bar + +$ etcdctl get --prefix --rev=1 foo # revision 1 时刻(空) +``` + +> 这正是 [5.1](#51-设计目标) 里说的 **time travel queries**:因为 revision 是集群级逻辑时钟,"在某个 revision 读一批 key"天然得到一致快照,无需额外加锁。 + +#### 5.9.2 用 Txn 做 compare-and-swap(进阶:分布式互斥的基础) + +`Txn` 的语义见 [6.3](#63-kv-service)。这里给出一个最小的"仅当值等于旧值时才写入": + +```go +// 💡 补充(工程示例,非官方文档原文):基于 mod_revision 的 CAS +resp, err := cli.Txn(ctx). + If(clientv3.Compare(clientv3.ModRevision("foo"), "=", 4)). + Then(clientv3.OpPut("foo", "bar_cas")). + Else(clientv3.OpGet("foo")). + Commit() +if err != nil { + panic(err) +} +fmt.Println("succeeded:", resp.Succeeded, "header.revision:", resp.Header.Revision) +``` + +对应的 etcdctl 层等价操作(官方文档未提供 `etcdctl txn` 的示例,见 [12.14](#1214-官方文档未提供的命令)): + +> 📌 官方文档未提供:`dev-guide/interacting_v3.md` **未包含 `TXN`(事务)命令**的示例与输出。上例为本册基于 API 语义整理的工程示例。 + +#### 5.9.3 lease + keepalive 的存活检测 + +```bash +$ etcdctl lease grant 60 +lease 32695410dcc0ca06 granted with TTL(60s) + +$ etcdctl lease keep-alive 32695410dcc0ca06 +lease 32695410dcc0ca06 keepalived with TTL(60) +lease 32695410dcc0ca06 keepalived with TTL(60) +lease 32695410dcc0ca06 keepalived with TTL(60) +... +``` + +#### 5.9.4 一个"服务注册"的最小模型 + +> 💡 补充:把上面四个原语组合起来就是最经典的 etcd 用法——服务注册 + 存活检测 + 变更订阅。 + +```bash +# 服务实例注册:key = /services/order/10.0.0.1:8080,挂 30 秒 lease +$ etcdctl lease grant 30 +lease 694d5765fc71500b granted with TTL(30s) +$ etcdctl put /services/order/10.0.0.1:8080 '{"weight":10}' --lease=694d5765fc71500b +OK + +# 调用方订阅整个服务目录 +$ etcdctl watch --prefix /services/order/ + +# 调用方列举当前实例 +$ etcdctl get --prefix /services/order/ +``` + +> 进程只要周期性 `lease keep-alive` 就"在线";进程挂掉不再续约,30 秒后 key 自动删除,watcher 收到 DELETE 事件。这就是"monitoring machine liveness"的实现骨架。 + +--- + +## 6. etcd API + +> 引自:[etcd API](https://etcd.io/docs/v3.7/learning/api/)、[API reference](https://etcd.io/docs/v3.7/dev-guide/api_reference_v3/)、[API reference: concurrency](https://etcd.io/docs/v3.7/dev-guide/api_concurrency_reference_v3/) + +### 6.1 gRPC services 总览 + +所有 etcd API 都定义在 **gRPC services** 中,按功能对 etcd server 能理解的 RPC 进行分类。注意这是 **v3 API**,不要与 etcd v3.5 中已废弃的 **v2 API** 混淆。 + +**处理 key space 的服务(3 个)**: + +| service | 用途 | +| --- | --- | +| `KV` | 创建、更新、读取、删除 key-value 对 | +| `Watch` | 监控 key 的变化 | +| `Lease` | 消费客户端 keep-alive 消息的原语 | + +**管理集群自身的服务(3 个)**: + +| service | 用途 | +| --- | --- | +| `Auth` | 基于角色的认证机制,用于认证用户 | +| `Cluster` | 提供成员信息与配置能力 | +| `Maintenance` | 生成恢复用快照、整理存储碎片(defragment)、返回每个成员的状态信息 | + +**并发原语服务(2 个,来自 `api_concurrency_reference_v3`)**: + +| service | 用途 | +| --- | --- | +| `Lock` | 把客户端侧的加锁能力以 gRPC 接口形式暴露(client-side locking facilities) | +| `Election` | 把客户端侧的选举能力以 gRPC 接口形式暴露(client-side election facilities) | + +### 6.2 统一请求响应格式 + +每个 RPC 都遵循同一格式:函数 `Name` 接受 `NameRequest` 参数并返回 `NameResponse`。例如 `Range(RangeRequest) returns (RangeResponse)`。 + +#### 6.2.1 ResponseHeader(所有响应都附带) + +```protobuf +message ResponseHeader { + uint64 cluster_id = 1; + uint64 member_id = 2; + int64 revision = 3; + uint64 raft_term = 4; +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `Cluster_ID` | `uint64` | 生成该响应的**集群 ID**(应用可据此确认正在与**预期集群**通信) | +| `Member_ID` | `uint64` | 生成该响应的**成员 ID** | +| `Revision` | `int64` | 生成响应时 key-value 存储的 revision(做 **time travel query** 时可用它知道请求时刻的最新 revision) | +| `Raft_Term` | `uint64` | 生成响应时成员的 **Raft term**(可用于检测集群是否完成了一次**新的 leader 选举**) | + +> 补充定义(来自 `api_reference_v3` 的 `ResponseHeader`):`revision` 是 **请求被 apply 时** key-value store 的 revision;对于**不与 key-value store 交互**的调用,该字段**不设置(即为 0)**。对于 **watch progress 响应**,`header.revision` 表示进度:该 stream 上后续收到的所有事件,**保证 revision 大于** `header.revision`。 + +#### 6.2.2 KeyValue(key-value API 可操作的最小单位) + +```protobuf +message KeyValue { + bytes key = 1; + int64 create_revision = 2; + int64 mod_revision = 3; + int64 version = 4; + bytes value = 5; + int64 lease = 6; +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `Key` | `bytes` | 字节形式的 key,**不允许空 key** | +| `Value` | `bytes` | 字节形式的 value | +| `Version` | `int64` | key 的版本;**删除会将其重置为 0**,任何修改都会使其递增 | +| `Create_Revision` | `int64` | 该 key **最后一次创建**时的 revision | +| `Mod_Revision` | `int64` | 该 key **最后一次修改**时的 revision | +| `Lease` | `int64` | 附加到该 key 的 **lease ID**;若为 **0** 表示**没有附加 lease** | + +**revision 元数据的三个用途(原文列举)**: + +| 用途 | 使用的 revision | +| --- | --- | +| etcd 客户端的**分布式共享锁** | 使用 **create revision** 来**等待锁归属** | +| **软件事务内存(STM)** | 用 **mod revision** 检测**读集冲突** | +| **leader election** | 用 mod revision **等待更新** | + +#### 6.2.3 Revisions(修订号)语义 + +| 要点 | 说明 | +| --- | --- | +| 计数器 | etcd 维护一个 **64 位集群级计数器** —— **store revision**,每次 key space 被修改时递增 | +| 角色 | 它充当**全局逻辑时钟**,**顺序化所有更新** | +| 增量式 | 新 revision 所代表的变化是**增量式**的;与某 revision 关联的数据就是**改变了存储的那部分数据** | +| 内部实现 | 新 revision 意味着把变更**写入后端 B+tree**,并以**递增后的 revision 作为 key** | +| 历史访问 | 结合 **MVCC** 后端,可以从**过去的 revision** 观察 key-value 存储 | +| 保留策略 | 历史保留策略可由**管理员配置**,通常 etcd 按定时器丢弃 key 的旧 revision,**典型集群保留数小时** | +| 断连恢复 | 这也为长时间客户端断连提供可靠处理:**watcher 只需从最后观测到的历史 revision 恢复** | +| 带 revision 读 | 读请求也可携带 revision,以返回该 revision **提交时刻**的 key space 视图 | + +#### 6.2.4 Key ranges(key 区间) + +约定与示例见 [5.5](#55-range-与前缀)。补充 API 层的原文表述: + +| 情形 | 含义 | +| --- | --- | +| `range_end` 未给出 | 请求**只查找 key** | +| `range_end` 为 `\0` | range 为**所有 >= key** 的 key | +| `range_end` 为 `key` 加一(`"aa"+1 == "ab"`、`"a\xff"+1 == "b"`) | range 请求获取**所有以 key 为前缀**的 key | +| `key` 与 `range_end` 均为 `\0` | range 请求**返回所有 key** | + +### 6.3 KV service + +方法清单(方法 | 请求 | 响应 | 用途): + +| 方法 | 请求 | 响应 | 用途 | +| --- | --- | --- | --- | +| `Range` | `RangeRequest` | `RangeResponse` | 取区间内的 key | +| `Put` | `PutRequest` | `PutResponse` | 写入 key;**递增 revision** 并在事件历史中生成**一个事件** | +| `DeleteRange` | `DeleteRangeRequest` | `DeleteRangeResponse` | 删除区间;**递增 revision** 并为**每个被删 key** 生成一个 **delete 事件** | +| `Txn` | `TxnRequest` | `TxnResponse` | 单事务处理多个请求;**递增 revision**,并为每个完成的请求生成**相同 revision** 的事件;**不允许在一个 txn 内多次修改同一 key** | +| `Compact` | `CompactionRequest` | `CompactionResponse` | 压缩事件历史;**应定期执行**,否则历史无限增长 | + +#### 6.3.1 RangeRequest + +```protobuf +message RangeRequest { + enum SortOrder { NONE = 0; ASCEND = 1; DESCEND = 2; } + enum SortTarget { KEY = 0; VERSION = 1; CREATE = 2; MOD = 3; VALUE = 4; } + bytes key = 1; bytes range_end = 2; int64 limit = 3; int64 revision = 4; + SortOrder sort_order = 5; SortTarget sort_target = 6; bool serializable = 7; + bool keys_only = 8; bool count_only = 9; int64 min_mod_revision = 10; + int64 max_mod_revision = 11; int64 min_create_revision = 12; int64 max_create_revision = 13; +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `Key, Range_End` | `bytes` | 要取的 key 区间 | +| `Limit` | `int64` | 返回 key 的**最大数量**;**为 0 时表示无限制** | +| `Revision` | `int64` | 取值的时间点;**小于或等于 0 表示对最新的 key-value 存储取范围**;若该 revision 已被压缩则返回 **`ErrCompacted`** | +| `Sort_Order` | enum | `NONE`(默认不排序)、`ASCEND`(最低目标值优先)、`DESCEND`(最高目标值优先) | +| `Sort_Target` | enum | `KEY` / `VERSION` / `CREATE` / `MOD` / `VALUE` | +| `Serializable` | `bool` | 设为 true 时使用 **serializable 的成员本地读**;默认 Range 是 **linearizable**(反映集群当前共识);serializable **无需与其他节点达成共识**即可本地服务,以**可能的陈旧读**换取性能与可用性 | +| `Keys_Only` | `bool` | 只返回 key 不返回 value | +| `Count_Only` | `bool` | 只返回区间内 key 的**数量** | +| `Min_Mod_Revision` / `Max_Mod_Revision` | `int64` | mod revision 的**下界 / 上界**过滤(下界:更小的被过滤掉;上界:更大的被过滤掉) | +| `Min_Create_Revision` / `Max_Create_Revision` | `int64` | create revision 的**下界 / 上界**过滤 | + +#### 6.3.2 RangeResponse + +```protobuf +message RangeResponse { + ResponseHeader header = 1; + repeated mvccpb.KeyValue kvs = 2; + bool more = 3; + int64 count = 4; +} +``` + +| 字段 | 说明 | +| --- | --- | +| `Kvs` | 匹配到的 key-value 列表(设置 `Count_Only` 时为**空**) | +| `More` | 若设置了 `limit`,指示区间内**是否还有更多 key** | +| `Count` | 满足区间请求的 key **总数**;与 `Kvs` 不同,它**不受 limit 与过滤器影响**(Min/Max、Create/Modify、Revisions),反映指定范围内的**完整计数** | + +#### 6.3.3 RangeStream + +`RangeStream` 返回与 `Range` 相同的结果集,但服务端把响应**切分成一系列 chunk 流式**下发给客户端,避免两端在内存中缓冲整个大区间。它接受与 `Range` 相同的 `RangeRequest`,客户端收到的是: + +```protobuf +message RangeStreamResponse { RangeResponse range_response = 1; } +``` + +跨 chunk 的字段填充规则: + +| 字段 | 规则 | +| --- | --- | +| `Kvs` | 每个 chunk 携带结果的一个**互不相交切片**;按到达顺序拼接所有 chunk 的 `kvs`,可得到与单次 `Range` **相同**的 key 集合 | +| `Header, More, Count` | **只在最后一个 chunk 上填充**,且仅在**流无错完成**时填充;之前的 chunk 这些字段为**零值** | +| 合并方式 | 对所有 chunk 的 `range_response` 使用 **`proto.Merge`**,可得到与 `Range` **等价**的 `RangeResponse` | +| 错误时 | 若流以错误结束,则**没有任何 chunk** 携带有效的 `header`、`more` 或 `count` | +| revision 一致性 | 流中每个 chunk 都基于**同一 revision** 提供服务。若请求未设置 `Revision`,服务端在**流开始时捕获最新已提交 revision** 并在**整个流中复用** | + +限制: + +| 限制 | 说明 | +| --- | --- | +| 不支持排序与过滤器 | `RangeStream` **不支持自定义排序**与 revision 过滤器(`min_mod_revision`、`max_mod_revision`、`min_create_revision`、`max_create_revision`),使用这些的请求返回 **`Unimplemented`** | +| proxy 不支持 | etcd **gRPC proxy 也不支持** `RangeStream` | + +两种常见消费方式: + +| 方式 | 说明 | +| --- | --- | +| ① 独立处理每个 chunk | 高性能场景:客户端**边到达边解码处理**,最后从**末尾 chunk** 读取 `header`/`more`/`count` | +| ② 组装成单个响应 | 用 `proto.Merge` 合并;Go 客户端提供 **`clientv3.GetStreamToGetResponse`** 辅助函数 | + +```bash +# etcdctl 侧:--stream 走 RangeStream +$ etcdctl get --stream --prefix foo +foo +bar +foo1 +bar1 +foo2 +bar2 +foo3 +bar3 +``` + +> 原文注解:`--stream` **不支持 `--order`、`--sort-by` 以及 revision 过滤器**。 + +#### 6.3.4 PutRequest / PutResponse + +```protobuf +message PutRequest { + bytes key = 1; bytes value = 2; int64 lease = 3; + bool prev_kv = 4; bool ignore_value = 5; bool ignore_lease = 6; +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `Key` | `bytes` | 要写入的 key 名 | +| `Value` | `bytes` | 关联的字节值 | +| `Lease` | `int64` | 关联的 lease ID,**0 表示无 lease** | +| `Prev_Kv` | `bool` | 设置时返回本次 `Put` **更新前**的 key-value 数据 | +| `Ignore_Value` | `bool` | 设置时更新 key 但**不改变其当前 value**;**key 不存在时返回错误** | +| `Ignore_Lease` | `bool` | 设置时更新 key 但**不改变其当前 lease**;**key 不存在时返回错误** | + +```protobuf +message PutResponse { ResponseHeader header = 1; mvccpb.KeyValue prev_kv = 2; } +``` + +`Prev_Kv` 为被 `Put` 覆盖的 key-value 对(**仅当请求中设置了 `Prev_Kv`**)。 + +#### 6.3.5 DeleteRangeRequest / DeleteRangeResponse + +```protobuf +message DeleteRangeRequest { bytes key = 1; bytes range_end = 2; bool prev_kv = 3; } +``` + +| 字段 | 说明 | +| --- | --- | +| `Key, Range_End` | 要删除的 key 区间 | +| `Prev_Kv` | 设置时返回**被删除的 key-value 内容** | + +```protobuf +message DeleteRangeResponse { + ResponseHeader header = 1; + int64 deleted = 2; + repeated mvccpb.KeyValue prev_kvs = 3; +} +``` + +| 字段 | 说明 | +| --- | --- | +| `Deleted` | 删除的 key **数量** | +| `Prev_Kv` | 本次 `DeleteRange` 删除的**所有 key-value 对列表** | + +#### 6.3.6 Transaction(Txn) + +事务是 key-value 存储上的原子 **If/Then/Else** 构造,可用于: + +- 防止 key 被意外的**并发更新**; +- 构建 **compare-and-swap** 操作; +- 开发**更高层并发控制**。 + +| 规则 | 说明 | +| --- | --- | +| 多请求单事务 | 事务可在**单个请求**中原子处理多个请求 | +| revision 递增次数 | 对于修改操作,**存储 revision 只递增一次**,事务产生的所有事件都拥有**相同的 revision** | +| 禁止 | **同一事务内多次修改同一个 key 是被禁止的** | +| 守卫 | 所有事务由**一组比较的合取(conjunction)**守卫,类似 `If` 语句 | +| 比较对象 | 每个比较检查存储中的**单个 key**:可检查值**存在/不存在**、与**给定值比较**、或检查 key 的 **revision / version** | +| 比较范围 | 不同比较可作用于**相同或不同 key** | +| 求值 | 所有比较**原子应用**:全部为真则事务**成功**,执行 then / `success` 请求块;否则**失败**,执行 else / `failure` 块 | + +**Compare 消息**: + +| 项 | 取值 | +| --- | --- | +| `CompareResult` 枚举 | `EQUAL = 0`、`GREATER = 1`、`LESS = 2`、`NOT_EQUAL = 3` | +| `CompareTarget` 枚举 | `VERSION = 0`、`CREATE = 1`、`MOD = 2`、`VALUE = 3` | +| 字段 `result` | 逻辑比较类型 | +| 字段 `target` | 被比较的 key-value 字段 | +| 字段 `key` | 比较的目标 key | +| 字段 `target_union` | oneof:`version`、`create_revision`、`mod_revision`、`value`(用户指定的比较数据) | + +> 来自 `api_reference_v3` 的完整 `Compare` 字段:`result`、`target`、`key`、`target_union`(oneof)、`version`、`create_revision`、`mod_revision`、`value`、`lease`、`range_end`。其中 `range_end` 的语义:把给定 target 与 **`[key, range_end)` 区间内的所有 key** 比较。 + +**RequestOp(请求块)** oneof: + +| 分支 | 类型 | 约束 | +| --- | --- | --- | +| `request_range` | `RangeRequest` | — | +| `request_put` | `PutRequest` | **key 必须唯一**,不得与其他 Put 或 Delete 共享 key | +| `request_delete_range` | `DeleteRangeRequest` | 不得与任何 Put 或 Delete 共享 key | +| `request_txn` | `TxnRequest` | (来自 `api_reference_v3`,可嵌套) | + +```protobuf +message TxnRequest { + repeated Compare compare = 1; + repeated RequestOp success = 2; + repeated RequestOp failure = 3; +} +``` + +| 字段 | 说明 | +| --- | --- | +| `Compare` | 守卫事务的**谓词合取列表** | +| `Success` | 全部比较为真时执行的请求列表 | +| `Failure` | 任一比较为假时执行的请求列表 | + +```protobuf +message TxnResponse { + ResponseHeader header = 1; + bool succeeded = 2; + repeated ResponseOp responses = 3; +} +``` + +| 字段 | 说明 | +| --- | --- | +| `Succeeded` | `Compare` 求值结果 | +| `Responses` | 与所应用的 `RequestOp` 列表**一一对应**的响应列表,编码为 `ResponseOp`(oneof:`response_range`、`response_put`、`response_delete_range`、`response_txn`) | + +> **重要注意(原文)**:内层响应中的 `ResponseHeader` **不应以任何方式解读**;客户端若需获取最新 revision,应**始终检查 `TxnResponse` 中顶层的 `ResponseHeader`**。 + +#### 6.3.7 Compact + +```protobuf +message CompactionRequest { + int64 revision = 1; + bool physical = 2; +} +``` + +| 字段 | 说明 | +| --- | --- | +| `revision` | 压缩操作所针对的 key-value store revision;**所有 revision 小于该压缩 revision 的被取代 key 都会被移除** | +| `physical` | 设置后 RPC 会**等待压缩被物理应用**到本地数据库,使被压缩条目**从后端数据库中彻底移除** | + +```bash +$ etcdctl compact 5 +compacted revision 5 + +$ etcdctl get --rev=4 foo +Error: rpc error: code = 11 desc = etcdserver: mvcc: required revision has been compacted +``` + +### 6.4 Watch service + +方法清单: + +| 方法 | 请求 | 响应 | 用途 | +| --- | --- | --- | --- | +| `Watch` | `WatchRequest`(流) | `WatchResponse`(流) | 监控正在发生或已经发生的事件。**输入输出均为流**;输入流用于创建/取消 watcher,输出流发送事件;**一个 Watch RPC 可 watch 多个 key range**;整个事件历史可从**最后一次 compaction revision** 开始 watch | + +#### 6.4.1 WatchCreateRequest + +```protobuf +message WatchCreateRequest { + bytes key = 1; bytes range_end = 2; int64 start_revision = 3; bool progress_notify = 4; + enum FilterType { NOPUT = 0; NODELETE = 1; } + repeated FilterType filters = 5; + bool prev_kv = 6; +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `Key, Range_End` | `bytes` | 要 watch 的 key 区间 | +| `Start_Revision` | `int64` | 可选的**起始 revision(含)**;若不指定则从 watch 创建响应 **header 的 revision 之后**开始流式发送 | +| `Progress_Notify` | `bool` | 设置后,若近期无事件,watch 会**周期性收到一个不含事件的 `WatchResponse`**;适用于客户端希望从**最近已知 revision** 恢复断连的 watcher。发送频率由 etcd server 依据**当前服务器负载**决定 | +| `Filters` | `repeated FilterType` | 服务端要过滤掉的事件类型列表(`NOPUT` / `NODELETE`) | +| `Prev_Kv` | `bool` | 设置后 watch 会收到事件发生**前**的 key-value 数据,用于了解**被覆盖了什么数据** | + +`api_reference_v3` 中补充的两个字段: + +| 字段 | 说明 | +| --- | --- | +| `watch_id` | 若提供且**非 0**,将被赋给该 watcher。由于在 etcd 中创建 watcher **不是同步操作**,可用它保证在同一 stream 上创建多个 watcher 时的**顺序正确**。在 stream 上使用**已被占用的 ID** 创建 watcher 会**返回错误** | +| `fragment` | 启用把**大 revision 拆分成多个 watch response** | + +#### 6.4.2 WatchResponse + +```protobuf +message WatchResponse { + ResponseHeader header = 1; int64 watch_id = 2; bool created = 3; + bool canceled = 4; int64 compact_revision = 5; repeated mvccpb.Event events = 11; +} +``` + +| 字段 | 说明 | +| --- | --- | +| `Watch_ID` | 该响应对应的 **watch ID** | +| `Created` | 为 true 表示这是对 **create watch 请求**的响应;客户端应**保存该 ID**,并预期在该 stream 上收到该 watch 的事件;发往该 watcher 的**所有事件都具有相同 `watch_id`** | +| `Canceled` | 为 true 表示这是对 **cancel watch 请求**的响应(或因 **`start_revision` 已被压缩**);此后**不再**向该 watcher 发送事件 | +| `Compact_Revision` | 当 watcher 试图在被压缩的 revision 上 watch 时,设为 etcd 可用的**最小历史 revision**。发生在:① 在被压缩 revision 上创建 watcher;② watcher **无法跟上** key-value 存储的进度。此时 watcher 会被取消,且**用相同 `start_revision` 创建新 watch 会失败** | +| `Events` | 按序对应该 watch ID 的**新事件列表** | + +`api_reference_v3` 补充字段: + +| 字段 | 说明 | +| --- | --- | +| `cancel_reason` | 取消 watcher 的**原因** | +| `fragment` | 若**大的 watch response 被拆分**到多个 response,则为 true | + +#### 6.4.3 WatchRequest / WatchCancelRequest / WatchProgressRequest + +```protobuf +message WatchRequest { + oneof request_union { + WatchCreateRequest create_request = 1; + WatchCancelRequest cancel_request = 2; + WatchProgressRequest progress_request = 3; + } +} + +message WatchCancelRequest { int64 watch_id = 1; } +``` + +| 消息 | 字段 | 说明 | +| --- | --- | --- | +| `WatchRequest` | `request_union` | 一个请求,要么是**创建新 watcher**,要么是**取消已有 watcher** | +| `WatchCancelRequest` | `watch_id` | 要取消的 watch ID,取消后**不再传输事件** | +| `WatchProgressRequest` | (无字段) | 请求 **watch stream 的进度状态**尽快在 watch response stream 中发出 | + +### 6.5 Lease service + +方法清单: + +| 方法 | 请求 | 响应 | 用途 | +| --- | --- | --- | --- | +| `LeaseGrant` | `LeaseGrantRequest` | `LeaseGrantResponse` | 创建 lease;若服务端在给定 TTL 周期内**未收到 keepAlive**,lease 过期;其上所有 key **过期并被删除**;**每个过期的 key 生成一个 delete 事件** | +| `LeaseRevoke` | `LeaseRevokeRequest` | `LeaseRevokeResponse` | 撤销 lease;其上所有 key 过期并被删除 | +| `LeaseKeepAlive` | `LeaseKeepAliveRequest`(流) | `LeaseKeepAliveResponse`(流) | 通过**客户端 → 服务端 keep alive 请求流**与**服务端 → 客户端响应流**保活 | +| `LeaseTimeToLive` | `LeaseTimeToLiveRequest` | `LeaseTimeToLiveResponse` | 获取 **lease 信息** | +| `LeaseLeases` | `LeaseLeasesRequest` | `LeaseLeasesResponse` | 列出**现有全部 lease** | + +关键消息字段: + +| 消息 | 字段 | 类型 | 说明 | +| --- | --- | --- | --- | +| `LeaseGrantRequest` | `TTL` | `int64` | **建议的**存活时间(**秒**);**过期的 lease 返回 -1** | +| `LeaseGrantRequest` | `ID` | `int64` | 请求的 lease ID;**若为 0,由 lessor 选择 ID** | +| `LeaseGrantResponse` | `ID` / `TTL` / `error` | `int64`/`int64`/`string` | 授予的 lease ID / 服务端选定的存活秒数 / 错误 | +| `LeaseRevokeRequest` | `ID` | `int64` | 要撤销的 lease ID;撤销时**所有关联 key 被删除** | +| `LeaseKeepAliveRequest` | `ID` | `int64` | 要保活的 lease ID | +| `LeaseKeepAliveResponse` | `ID` / `TTL` | `int64`/`int64` | 被刷新的 lease / 该 lease **新的剩余存活秒数** | +| `LeaseTimeToLiveRequest` | `ID` / `keys` | `int64`/`bool` | lease ID / 是否查询挂在该 lease 上的**所有 key** | +| `LeaseTimeToLiveResponse` | `ID` / `TTL` / `grantedTTL` / `keys` | `int64`/`int64`/`int64`/`bytes[]` | lease ID / **剩余 TTL(秒)**,lease 将在 **TTL+1 秒内**过期 / **初始授予的秒数**(创建或续约时)/ 挂载的 key 列表 | +| `LeaseLeasesResponse` | `leases` | `LeaseStatus[]` | 现有 lease 列表(`LeaseStatus` 含 `ID`) | +| `LeaseCheckpoint` | `ID` / `remaining_TTL` | `int64`/`int64` | 要 checkpoint 的 lease ID / 到期前的**剩余时间** | + +### 6.6 Cluster service + +方法清单: + +| 方法 | 请求 | 响应 | 用途 | +| --- | --- | --- | --- | +| `MemberAdd` | `MemberAddRequest` | `MemberAddResponse` | 把成员加入集群 | +| `MemberRemove` | `MemberRemoveRequest` | `MemberRemoveResponse` | 从集群移除已有成员 | +| `MemberUpdate` | `MemberUpdateRequest` | `MemberUpdateResponse` | 更新成员配置 | +| `MemberList` | `MemberListRequest` | `MemberListResponse` | 列出集群中所有成员 | +| `MemberPromote` | `MemberPromoteRequest` | `MemberPromoteResponse` | 把成员从 **raft learner(非投票)** 提升为 **raft 投票成员** | + +关键消息字段: + +| 消息 | 字段 | 说明 | +| --- | --- | --- | +| `Member` | `ID` / `name` / `peerURLs` / `clientURLs` / `isLearner` | 成员 ID / 人类可读名称(**成员未启动时为空字符串**)/ 暴露给集群通信的 URL 列表 / 暴露给客户端的 URL 列表(**未启动时为空**)/ 是否为 **raft learner** | +| `MemberAddRequest` | `peerURLs` / `isLearner` | 新成员用于与集群通信的 URL 列表 / 是否为 raft learner | +| `MemberAddResponse` | `header` / `member` / `members` | 响应头 / 新增成员的信息 / 添加后**所有成员**列表 | +| `MemberListRequest` | `linearizable` | 是否线性化 | +| `MemberListResponse` | `members` | 集群关联的所有成员列表 | +| `MemberPromoteRequest` | `ID` | 要提升的成员 ID | +| `MemberPromoteResponse` | `members` | 提升后所有成员列表 | +| `MemberRemoveRequest` | `ID` | 要移除的成员 ID | +| `MemberUpdateRequest` | `ID` / `peerURLs` | 成员 ID / 新的 peer URL 列表 | + +### 6.7 Maintenance service + +方法清单: + +| 方法 | 请求 | 响应 | 用途 | +| --- | --- | --- | --- | +| `Alarm` | `AlarmRequest` | `AlarmResponse` | 激活(ACTIVATE)、停用(DEACTIVATE)、查询(GET)集群健康告警 | +| `Status` | `StatusRequest` | `StatusResponse` | 获取成员状态 | +| `Defragment` | `DefragmentRequest` | `DefragmentResponse` | 整理成员后端数据库碎片,回收存储空间 | +| `Hash` | `HashRequest` | `HashResponse` | 计算**整个后端 keyspace** 的 hash,包括 key、lease 等 bucket。**仅为测试设计(designed for testing ONLY)**;生产环境有进行中事务时**不可依赖**,因为 **Hash 不持有 MVCC 锁**;`key` bucket 一致性检查应改用 **`HashKV`** | +| `HashKV` | `HashKVRequest` | `HashKVResponse` | 计算**给定 revision 之前所有 MVCC key** 的 hash;**只遍历**后端存储中的 **`key` bucket** | +| `Snapshot` | `SnapshotRequest` | `SnapshotResponse` | 通过流把成员的**整个后端快照**发给客户端 | +| `MoveLeader` | `MoveLeaderRequest` | `MoveLeaderResponse` | 请求当前 leader 节点把 leadership **转移给 transferee** | +| `Downgrade` | `DowngradeRequest` | `DowngradeResponse` | 请求降级、校验可行性或取消降级(**自 etcd 3.5 起支持**) | + +关键消息字段: + +| 消息 | 字段 | 说明 | +| --- | --- | --- | +| `AlarmRequest` | `action` / `memberID` / `alarm` | 动作(`GET` / `ACTIVATE` / `DEACTIVATE`)/ 成员 ID(**为 0 时覆盖所有成员**)/ 告警类型 | +| `AlarmResponse` | `alarms` | 与该告警请求关联的 `AlarmMember` 列表 | +| `AlarmMember` | `memberID` / `alarm` | 触发告警的成员 ID / 告警类型 | +| `StatusResponse` | `version`、`dbSize`、`leader`、`raftIndex`、`raftTerm`、`raftAppliedIndex`、`errors`、`dbSizeInUse`、`isLearner`、`storageVersion`、`dbSizeQuota`、`downgradeInfo` | 见下表 | +| `HashKVRequest` / `HashKVResponse` | `revision` / `hash`、`compact_revision`、`hash_revision` | 请求 revision;响应:hash 值、**hash 开始时的 compacted revision**、**hash 计算所至的 revision** | +| `SnapshotResponse` | `header`、`remaining_bytes`、`blob`、`version` | 第一个 header 表示快照的**时间点**;后续待发送 blob 字节数;快照的下一个 chunk;创建快照的服务器**本地版本**(用于提示恢复时用哪个 etcd 版本) | +| `MoveLeaderRequest` | `targetID` | 新 leader 的节点 ID | +| `DowngradeRequest` | `action` / `version` | 动作(`VALIDATE` / `DOWNGRADE` / `CANCEL`)/ 目标版本 | +| `DowngradeResponse` | `version` | 当前集群版本 | +| `DowngradeInfo` | `enabled` / `targetVersion` | 集群是否启用降级 / 目标降级版本 | + +`StatusResponse` 字段逐一说明: + +| 字段 | 说明 | +| --- | --- | +| `version` | 响应成员使用的**集群协议版本** | +| `dbSize` | 响应成员**物理分配**的后端数据库大小(字节) | +| `leader` | 响应成员认为的当前 **leader 成员 ID** | +| `raftIndex` | 响应成员当前的 **raft committed index** | +| `raftTerm` | 响应成员当前的 **raft term** | +| `raftAppliedIndex` | 响应成员当前的 **raft applied index** | +| `errors` | 包含 alarm/health 信息与状态 | +| `dbSizeInUse` | 响应成员**逻辑使用中**的后端数据库大小(字节) | +| `isLearner` | 是否为 raft learner | +| `storageVersion` | db 文件的版本(相对目标集群版本**可能延迟更新**) | +| `dbSizeQuota` | 配置的 etcd 存储配额字节数(即传给 etcd 实例的 **`--quota-backend-bytes`** flag 值) | +| `downgradeInfo` | 是否存在降级流程 | + +### 6.8 Auth service + +方法清单(**17 个方法**): + +| 方法 | 请求 | 响应 | 用途 | +| --- | --- | --- | --- | +| `AuthEnable` | `AuthEnableRequest` | `AuthEnableResponse` | 启用认证 | +| `AuthDisable` | `AuthDisableRequest` | `AuthDisableResponse` | 停用认证 | +| `AuthStatus` | `AuthStatusRequest` | `AuthStatusResponse` | 显示认证状态 | +| `Authenticate` | `AuthenticateRequest` | `AuthenticateResponse` | 处理认证请求 | +| `UserAdd` | `AuthUserAddRequest` | `AuthUserAddResponse` | 新增用户;**用户名不能为空** | +| `UserGet` | `AuthUserGetRequest` | `AuthUserGetResponse` | 获取用户详细信息 | +| `UserList` | `AuthUserListRequest` | `AuthUserListResponse` | 获取所有用户列表 | +| `UserDelete` | `AuthUserDeleteRequest` | `AuthUserDeleteResponse` | 删除指定用户 | +| `UserChangePassword` | `AuthUserChangePasswordRequest` | `AuthUserChangePasswordResponse` | 修改指定用户密码 | +| `UserGrantRole` | `AuthUserGrantRoleRequest` | `AuthUserGrantRoleResponse` | 给指定用户授予角色 | +| `UserRevokeRole` | `AuthUserRevokeRoleRequest` | `AuthUserRevokeRoleResponse` | 撤销指定用户的角色 | +| `RoleAdd` | `AuthRoleAddRequest` | `AuthRoleAddResponse` | 新增角色;**角色名不能为空** | +| `RoleGet` | `AuthRoleGetRequest` | `AuthRoleGetResponse` | 获取角色详细信息 | +| `RoleList` | `AuthRoleListRequest` | `AuthRoleListResponse` | 列出所有角色 | +| `RoleDelete` | `AuthRoleDeleteRequest` | `AuthRoleDeleteResponse` | 删除指定角色 | +| `RoleGrantPermission` | `AuthRoleGrantPermissionRequest` | `AuthRoleGrantPermissionResponse` | 给指定角色授予**某个 key 或 range** 的权限 | +| `RoleRevokePermission` | `AuthRoleRevokePermissionRequest` | `AuthRoleRevokePermissionResponse` | 撤销指定角色在某个 key 或 range 上的权限 | + +关键消息字段: + +| 消息 | 字段 | 说明 | +| --- | --- | --- | +| `AuthenticateRequest` | `name` / `password` | 用户名 / 密码 | +| `AuthenticateResponse` | `token` | 可用于后续 RPC 的 **authorized token** | +| `AuthStatusResponse` | `enabled` / `authRevision` | 认证是否启用 / **auth store 的当前 revision** | +| `AuthUserAddRequest` | `name` / `password` / `options` / `hashedPassword` | 用户名 / 密码 / `authpb.UserAddOptions` / 已哈希密码 | +| `AuthUserChangePasswordRequest` | `name` / `password` / `hashedPassword` | 用户名 / 新密码(**该字段将在 API 层被移除**)/ 新密码(**该字段将在 API 层被初始化**) | +| `AuthUserGetResponse` | `roles` | 该用户的角色列表 | +| `AuthRoleGetResponse` | `perm` | `authpb.Permission` 列表 | +| `AuthRoleGrantPermissionRequest` | `name` / `perm` | 被授权的角色名 / 要授予的 `authpb.Permission` | +| `AuthRoleRevokePermissionRequest` | `role` / `key` / `range_end` | 角色 / key / range_end | +| `AuthUserGrantRoleRequest` | `user` / `role` | 用户 / 角色 | +| `Permission`(`api/authpb/auth.proto`) | `permType` / `key` / `range_end` | 权限类型 / key / range_end | +| `Role`(`api/authpb/auth.proto`) | `name` / `keyPermission` | 角色名 / `Permission` 列表(**bucket `authRoles` 中的单个条目**) | +| `User`(`api/authpb/auth.proto`) | `name` / `password` / `roles` / `options` | 用户名 / 密码 / 角色列表 / `UserAddOptions`(**bucket `authUsers` 中的单个条目**) | +| `UserAddOptions` | `no_password` | 是否无密码 | + +### 6.9 Lock service + +> 引自:[API reference: concurrency](https://etcd.io/docs/v3.7/dev-guide/api_concurrency_reference_v3/) — `server/etcdserver/api/v3lock/v3lockpb/v3lock.proto` + +The lock service exposes **client-side locking facilities** as a gRPC interface. + +| 方法 | 请求 | 响应 | 用途 | +| --- | --- | --- | --- | +| `Lock` | `LockRequest` | `LockResponse` | 在给定的**命名锁(named lock)**上获取**分布式共享锁**。成功时返回一个**唯一 key**,该 key 在调用者持有锁期间一直存在。该 key 可与事务配合,**安全确保对 etcd 的更新只在持有锁所有权期间发生**。锁一直持有到:对该 key 调用 `Unlock`,**或**与属主关联的 **lease 过期** | +| `Unlock` | `UnlockRequest` | `UnlockResponse` | 用 `Lock` 返回的 key **释放**对该锁的持有。下一个等待该锁的 `Lock` 调用者会被唤醒并获得锁的所有权 | + +字段说明: + +| 消息 | 字段 | 类型 | 说明 | +| --- | --- | --- | --- | +| `LockRequest` | `name` | `bytes` | 要获取的分布式共享锁的**标识符** | +| `LockRequest` | `lease` | `int64` | 将附加到**锁所有权**上的 lease ID。若该 lease 过期或被撤销、且当前持有锁,则**锁自动释放**。**用同一个 lease 调用 `Lock` 被视为单次获取**;用同一个 lease 加锁两次是 **no-op** | +| `LockResponse` | `header` | `etcdserverpb.ResponseHeader` | 响应头 | +| `LockResponse` | `key` | `bytes` | 在 `Lock` 调用者**拥有锁期间**存在于 etcd 上的 key。**用户不应修改该 key**,否则锁可能表现出**未定义行为** | +| `UnlockRequest` | `key` | `bytes` | 由 `Lock` 授予的**锁所有权 key** | +| `UnlockResponse` | `header` | `etcdserverpb.ResponseHeader` | 响应头 | + +> 📌 与 [2.12](#212-lock-与-lease-的使用注意) 呼应:`Lock` 返回的 key 之所以要"与事务配合使用",正是因为 **lock APIs 本身不能当作互斥机制**,真正的互斥靠 `Txn` 里的 revision/lease 条件校验。 + +### 6.10 Election service + +> 引自:[API reference: concurrency](https://etcd.io/docs/v3.7/dev-guide/api_concurrency_reference_v3/) — `server/etcdserver/api/v3election/v3electionpb/v3election.proto` + +The election service exposes **client-side election facilities** as a gRPC interface. + +| 方法 | 请求 | 响应 | 用途 | +| --- | --- | --- | --- | +| `Campaign` | `CampaignRequest` | `CampaignResponse` | 等待获取选举中的 leadership,成功时返回表示 leadership 的 **`LeaderKey`**。该 `LeaderKey` 可用于:① 在选举上发布新值;② **事务性地**为"仍持有 leadership"的 API 请求做守卫;③ 从选举中辞职 | +| `Proclaim` | `ProclaimRequest` | `ProclaimResponse` | 用新值**更新 leader 已发布的值** | +| `Leader` | `LeaderRequest` | `LeaderResponse` | 返回**当前选举的公告**(若有) | +| `Observe` | `LeaderRequest` | `LeaderResponse` | 按选举产生的各 leader 的公告**顺序流式推送** | +| `Resign` | `ResignRequest` | `ResignResponse` | 释放选举 leadership,使其他 campaigner 可以获取该选举的 leadership | + +字段说明: + +| 消息 | 字段 | 类型 | 说明 | +| --- | --- | --- | --- | +| `CampaignRequest` | `name` | `bytes` | 选举中该 campaign 的**标识符** | +| `CampaignRequest` | `lease` | `int64` | 附加到选举 leadership 上的 lease ID。若该 lease 在**辞职前**过期或被撤销,则 leadership **转移给下一个 campaigner**(若有) | +| `CampaignRequest` | `value` | `bytes` | campaigner **赢得选举时**设置的**初始公告值** | +| `CampaignResponse` | `header` / `leader` | `ResponseHeader` / `LeaderKey` | 响应头 / 用于持有 leadership 的资源描述 | +| `LeaderKey` | `name` | `bytes` | 与 leadership key 对应的**选举标识符** | +| `LeaderKey` | `key` | `bytes` | 表示选举所有权的**不透明 key**。**若该 key 被删除,则 leadership 丢失** | +| `LeaderKey` | `rev` | `int64` | key 的 **creation revision**。可在事务中通过**比较 key 的 creation revision 是否等于 `rev`** 来测试选举所有权 | +| `LeaderKey` | `lease` | `int64` | 选举 leader 的 **lease ID** | +| `LeaderRequest` | `name` | `bytes` | 要查询 leadership 信息的**选举标识符** | +| `LeaderResponse` | `header` / `kv` | `ResponseHeader` / `mvccpb.KeyValue` | 响应头 / 表示**最新 leader 更新**的 key-value 对 | +| `ProclaimRequest` | `leader` / `value` | `LeaderKey` / `bytes` | 在选举上的 leadership 持有 / 用于覆盖 leader 当前值的**更新值** | +| `ResignRequest` | `leader` | `LeaderKey` | 要通过辞职放弃的 leadership | + +### 6.11 方法清单汇总 + +八个 service 的方法总数: + +| service | 方法数 | 方法清单 | +| --- | --- | --- | +| `KV` | 5 | `Range`、`Put`、`DeleteRange`、`Txn`、`Compact` | +| `Watch` | 1 | `Watch`(双向流) | +| `Lease` | 5 | `LeaseGrant`、`LeaseRevoke`、`LeaseKeepAlive`、`LeaseTimeToLive`、`LeaseLeases` | +| `Cluster` | 5 | `MemberAdd`、`MemberRemove`、`MemberUpdate`、`MemberList`、`MemberPromote` | +| `Maintenance` | 8 | `Alarm`、`Status`、`Defragment`、`Hash`、`HashKV`、`Snapshot`、`MoveLeader`、`Downgrade` | +| `Auth` | 17 | `AuthEnable`、`AuthDisable`、`AuthStatus`、`Authenticate`、`UserAdd`、`UserGet`、`UserList`、`UserDelete`、`UserChangePassword`、`UserGrantRole`、`UserRevokeRole`、`RoleAdd`、`RoleGet`、`RoleList`、`RoleDelete`、`RoleGrantPermission`、`RoleRevokePermission` | +| `Lock` | 2 | `Lock`、`Unlock` | +| `Election` | 5 | `Campaign`、`Proclaim`、`Leader`、`Observe`、`Resign` | +| **合计** | **48** | — | + +> 💡 补充:`Compact` 在 `learning/api.md` 正文的 services 描述里没有单独展开,但它是 `KV` service 的方法之一(来自 `api_reference_v3`)。使用 `etcdctl compact` 时走的就是它。 + +--- + +## 7. API 保证 + +> 引自:[etcd API guarantees](https://etcd.io/docs/v3.7/learning/api_guarantees/) + +etcd 是一个一致且持久的 key-value 存储,通过 **gRPC Services** 暴露,并为分布式系统提供最强的**一致性(consistency)与持久性(durability)**保证。本规格说明枚举 etcd 作出的 API 保证。 + +本章每条保证都按三段展开:**保证内容**(原文)+ **对使用者的意义**(怎么用它写代码)+ **违反后果**(一旦不成立会看到什么现象 / 你必须自己补什么)。 + +### 7.1 纳入保证范围的 API + +| 类别 | API | +| --- | --- | +| **KV APIs** | `Range`、`RangeStream`、`Put`、`Delete`、`Transaction` | +| **Watch APIs** | `Watch` | +| **Lease APIs** | `Grant`、`Revoke`、`Keep alive` | + +| 类别 | 能力 | +| --- | --- | +| KV API | 允许**直接读取与操作** key-value 存储 | +| Watch API | 允许**订阅** key-value 存储的变化 | +| Lease API | 允许给 key 赋予**存活时间** | +| KV 与 Watch API 共同 | 都能访问 key 的**最新版本**,**也能在一个连续的历史窗口内访问先前的版本**,该窗口由 **compaction 操作**界定 | + +> 💡 补充:注意"纳入保证范围"的清单里**没有** `Cluster`、`Maintenance`、`Auth`、`Lock`、`Election`。这些 service 的行为不在这份一致性保证规格内(例如 `Hash` 官方明确说"仅为测试设计")。写业务代码时不要把集群管理类 API 当作一致性原语用。 + +### 7.2 时延特性 + +| 项 | 内容 | +| --- | --- | +| KV API | 调用 KV API 会**立即生效** | +| Watch API | 带有**无界的延迟**返回 | +| 健康集群的经验值 | 在正常工作(correctly working)的 etcd 集群中,watch 事件应在其发生后约 **10ms** 出现 | +| 上限 | **没有上限**;不健康的集群中事件**可能永远不到达** | + +**保证内容**:KV 调用立即生效;watch 事件在正常集群约 10ms 出现,但**没有上限**。 + +**对使用者的意义**:不要把 watch 当作"同步通知"来设计流程。任何依赖"我 put 完对方马上就收到"的逻辑都是错的。 + +**违反后果**:watch 事件延迟无上界,故障期间事件可能永远不到达 → 因此必须实现 [7.8](#78-watch-的六条保证) 里的 **Resumable**(用最后收到的 revision 重建 watch),并在关键路径上用 KV 读做兜底。 + +### 7.3 持久性 durability + +**保证内容**:任何**已完成**的操作都是持久的;**所有可访问的数据也就是持久的数据**;**读永远不会返回尚未被持久化的数据**。 + +**对使用者的意义**: + +| 推论 | 说明 | +| --- | --- | +| 读到即已落盘 | 只要 `get` 返回一个值,这个值一定已经被持久化 | +| 收到响应即完成 | 客户端**收到 etcd server 响应**时即可知道操作已完成(见 [7.11](#711-操作完成的定义)) | +| 不需要额外 fsync | 应用侧不需要为"数据会不会丢"再加一层持久化 | + +**违反后果**:如果出现"读到未持久化的数据",意味着发生了 **durability 破坏**(例如错误的存储配置),这是严重 bug,通常伴随重启后数据丢失。 + +### 7.4 严格可串行化 + +**保证内容**:etcd 为所有 KV api 调用确保 **durability** 与 **strict serializability(严格可串行化)**,这是**分布式事务数据库系统中最强的隔离保证**。KV Service 的操作是**原子的**,并以与这些操作的**实时顺序一致的全序(total order)**发生;**全序由 revision 隐含表达**。 + +嵌套事务的顺序规则: + +| 事务类型 | 执行顺序 | +| --- | --- | +| **不含嵌套 TXN** 的事务 | 操作的执行顺序**保证与其操作列表中的顺序相同** → 事务内的 **GET 响应是稳定的** | +| **含嵌套 TXN** 的事务 | 执行顺序**不做规定** | + +**对使用者的意义**: + +- 可以用 revision 直接推理"谁先谁后",不需要额外加锁或版本号; +- 写 `Txn` 时应**避免嵌套 TXN**,否则无法依赖操作顺序(也就无法依赖事务内 GET 的稳定性)。 + +**违反后果**:若顺序不成立,"事务内读到的值"与"事务内写的值"之间的因果链断裂,compare-and-swap 类逻辑会出现**静默的错误结果**(不报错但写错值)。 + +### 7.5 原子性 + +**保证内容**:所有 API 请求都是原子的,一个操作要么**完全完成**要么**完全不发生**。对于 watch 请求:**一次操作生成的所有事件会在同一个 watch response 中**;watch **绝不会观察到单个操作的部分事件**。 + +**对使用者的意义**: + +| 场景 | 含义 | +| --- | --- | +| 写 | 不存在"写了一半"的中间态 | +| watch | 一次 `Txn` 改了 3 个 key,你会**在同一个 response 里收到**这 3 个事件,不会先收到 1 个、再收到 2 个 | +| 消费端 | 可以按 response 为单位做**原子地更新本地缓存** | + +**违反后果**:若 watch 能观察到部分事件,消费端会看到"事务执行了一半"的状态,缓存/状态机出现**瞬时不一致**(例如订单扣了款但没创建记录)。 + +### 7.6 线性一致 + +**保证内容**(含原文引文): + +> 引自:[etcd API guarantees](https://etcd.io/docs/v3.7/learning/api_guarantees/) +> +> *Linearizability provides the illusion that each operation applied by concurrent processes takes effect instantaneously at some point between its invocation and response.* +> (线性一致提供这样一种假象:并发进程应用的每个操作都在其调用与响应之间的某个时刻瞬时生效。) + +原文举例: + +| 时间点 | 事件 | +| --- | --- | +| *t1* | 客户端 A **完成一次写** | +| *t2*(*t2* > *t1*) | 客户端 B **发起读** → 应收到**至少与 t1 完成的写一样新**的值 | +| *t3* | 该读可能直到 *t3* 才真正**完成** | + +线性一致保证该读返回**最新的值**。没有线性一致时,*t2* 时刻"当前"的返回值到 *t3* 可能已经"陈旧",因为 *t2* 与 *t3* 之间可能发生了**并发写**。 + +**补充要点**: + +| 项 | 内容 | +| --- | --- | +| 默认行为 | etcd **默认**为**所有其他操作**确保线性一致 | +| 代价 | 线性化请求**必须走 Raft 共识流程** | +| 可选降级 | 为获得更低的读延迟与更高吞吐,客户端可把请求的一致性模式配置为 **`serializable`** | + +**对使用者的意义**:默认就能拿到"最新值",写业务代码不需要自己处理"读到旧值"的问题;代价是每个读都要过一次 Raft。 + +**违反后果**:若读不是线性一致的,就会读到"t2 时刻当前、t3 时刻已陈旧"的值——即**读到过去**。典型症状:刚写入成功,另一个客户端读到的还是旧值。 + +### 7.7 serializable 读的语义 + +**保证内容**:设置为 `serializable` 的请求使用**成员本地读**,**无需与其他节点达成共识**即可本地服务;相对 quorum **可能访问到陈旧数据**,但**消除了线性化访问对实时共识的性能惩罚**。 + +| 对比项 | linearizable(默认) | serializable | +| --- | --- | --- | +| 是否走 Raft 共识 | **是** | **否**(本地服务) | +| 数据新鲜度 | 反映**集群当前共识** | 相对 quorum **可能陈旧** | +| 延迟 | 更高 | **更低** | +| 吞吐 | 更低 | **更高** | +| 可用性 | 依赖 live consensus | 不依赖 | + +**对使用者的意义**:适合"能容忍短暂陈旧、但要高吞吐/低延迟"的读,例如本地缓存刷新、监控采集、大范围列举。 + +**违反后果(你必须自己承担的后果)**: + +- 可能**读到旧值**(stale read)——不要用它做"写完立刻校验"; +- 在分区节点上读,可能读到**明显落后**的数据; +- 用 `serializable` 做 compare-and-swap 的前置读会**放大竞态窗口**。 + +> 📌 官方文档未提供:`dev-guide/interacting_v3.md` **未给出** `serializable` 读的 etcdctl 命令行示例(见 [12.14](#1214-官方文档未提供的命令))。proto 层的开关是 `RangeRequest.serializable`(见 [6.3.1](#631-rangerequest))。 + +> 💡 补充:用 `serializable` 读时,判断"我读到的值有多旧"的实用方法是比对响应 `header.revision` 与 `etcdctl get <不存在的key> -w=json` 得到的当前 revision(见 [12.11](#1211-压缩-revision-compact)),两者差值就是陈旧程度。 + +### 7.8 watch 的六条保证 + +> 引自:[etcd API guarantees](https://etcd.io/docs/v3.7/learning/api_guarantees/) — "Watches make guarantees about events" + +| # | 保证 | 原文内容 | +| --- | --- | --- | +| 1 | **Ordered(有序)** | 事件按 **revision** 排序。一个事件绝不会在时间上早于某个已投递事件的情况下出现在 watch 上。对**不含嵌套 TXN** 的事务,生成事件的顺序**保证与其操作列表顺序相同**;对**含嵌套 TXN** 的事务,事件顺序**不做规定** | +| 2 | **Unique(唯一)** | 一个事件绝不会在同一 watch 上出现**两次** | +| 3 | **Reliable(可靠)** | 在**可用历史窗口内**,事件序列绝不会丢弃任何子序列。若事件在时间上排序为 **a < b < c**,则只要 watch 收到了 a 和 c,就**保证也收到 b**(前提是 b 在可用历史窗口内) | +| 4 | **Atomic(原子)** | 事件列表保证涵盖**完整的 revision**;同一 revision 中对多个 key 的更新**不会被拆分**到多个事件列表中 | +| 5 | **Resumable(可恢复)** | 中断的 watch 可通过建立一个**新的 watch** 来恢复,起点为断连前在 watch 事件中收到的**最后一个 revision 之后**(只要该 revision 仍在历史窗口内) | +| 6 | **Bookmarkable(可打书签)** | Progress notification(进度通知)事件保证**截止到某一 revision 的所有事件都已投递完毕** | + +逐条展开为「保证内容 + 对使用者的意义 + 违反后果」: + +#### 7.8.1 Ordered(有序) + +- **保证内容**:按 revision 排序;不会出现"比已投递事件更早"的事件。 +- **对使用者的意义**:可以只记录"当前 revision",用它做断点续传的游标;无需自己排序。 +- **违反后果**:若乱序,消费端状态机会**回退**(例如把新配置覆盖成旧配置)。 + +#### 7.8.2 Unique(唯一) + +- **保证内容**:同一 watch 上事件不重复。 +- **对使用者的意义**:消费端**不需要幂等去重**(但重试重建 watch 时仍可能看到重叠区间,见 Resumable)。 +- **违反后果**:重复事件会导致**重复扣减 / 重复触发**类业务 bug。 + +#### 7.8.3 Reliable(可靠) + +- **保证内容**:可用历史窗口内不丢子序;收到 a 和 c 就必定收到中间的 b。 +- **对使用者的意义**:**这是 etcd 相对其他 KV 存储最关键的差异之一**(见 [2.6](#26-竞品对比表) 的 Change Notification 行)。只要你的 watcher 没落后于 compaction,就不会"漏事件"。 +- **违反后果**:漏事件意味着缓存与集群状态**永久分叉**,且不会有任何报错。 + +#### 7.8.4 Atomic(原子) + +- **保证内容**:一个事件列表涵盖完整 revision,不跨 revision 拆分。 +- **对使用者的意义**:可以把一个 response 当作"一个事务的效果"来原子地应用。 +- **违反后果**:拆分开来会让消费端看到**事务的中间态**。 + +#### 7.8.5 Resumable 可恢复 + +- **保证内容**:用"断连前最后收到的 revision **之后**"作为起点新建 watch 即可恢复。 +- **对使用者的意义**:重连逻辑的标准写法 —— 保存 `WatchResponse.Header.Revision`,重连时用它(或 +1 语义的起点)重建。 +- **违反后果**:如果起点不在历史窗口内(已被 compaction),watch 会被取消并带回 **`compact_revision`**,此时**必须全量重建状态**(先 `get` 全量,再从当前 revision 继续 watch),否则状态永久缺失。 + +#### 7.8.6 Bookmarkable 可打书签 + +- **保证内容**:progress notification 事件保证"截止到某 revision 的所有事件都已投递完毕"。 +- **对使用者的意义**:即使长时间无写入,也能通过 progress notify 推进"我已同步到哪个 revision"的认知;配合 `WatchCreateRequest.progress_notify` 使用(见 [6.4.1](#641-watchcreaterequest))。 +- **违反后果**:无法区分"没有事件"和"连接假死",故障检测会失效。 + +```bash +# 交互式 watch 会话中用 progress 命令请求进度通知 +$ etcdctl watch -i +$ watch a +$ progress +progress notify: 1 +# in another terminal: etcdctl put x 0 +# in another terminal: etcdctl put y 1 +$ progress +progress notify: 3 +``` + +> 原文 Note(info):progress notify 响应中的 revision 号来自 **watch 流所连接的那个本地 etcd 节点**。若该节点**被分区、不属于 quorum**,此 progress notify revision 可能**低于**对未分区节点做 quorum read 得到的 revision。 + +### 7.9 watch 不保证线性一致 + +**保证内容(这是"不保证",原文明确写出)**:etcd **不保证 watch 操作的线性一致性**。用户应**自行校验 watch 事件的 revision**,以确保与其他操作的正确排序。 + +**对使用者的意义**: + +| 场景 | 正确做法 | +| --- | --- | +| 用 watch 维护缓存 | 记录 `header.revision`;与 quorum read 得到的 revision 比较,判断是否落后 | +| 把 watch 与其他操作排序 | **显式比较 revision**,不要假设"watch 收到 = 当前最新" | +| 判断节点是否落后 | progress notify 的 revision 来自**本地节点**,可能低于 quorum read 的 revision(见 [7.8.6](#786-bookmarkable-可打书签)) | + +**违反后果**:把 watch 当成线性一致读来用,会因为事件延迟无上界([7.2](#72-时延特性))而读到"过期的当前状态",且不会有任何错误提示。 + +### 7.10 lease 语义 + +**保证内容**(原文完整段落要点): + +| 项 | 内容 | +| --- | --- | +| 主要用例 | lease 机制的主要用例是**实现分布式协调机制**(如**分布式锁**) | +| 机制简单性 | lease 可用 **`grant` API 创建**、用 **`put` API 附加到 key**、用 **`revoke` API 撤销**,并会按**墙上时钟的 TTL** 过期 | +| 使用者义务 | 用户需了解这些 API 与用法的**重要属性**,才能**正确**实现分布式协调机制 | + +**对使用者的意义**:lease 只是"带 TTL 的、可续约的、到期删 key 的契约"。它的语义与 [2.12](#212-lock-与-lease-的使用注意) 的限制必须一起读:**lease 本身不保证互斥**,TTL 是**物理时间间隔**,服务端与客户端各自用自己的时钟度量。 + +**违反后果**: + +- 把 lease 当互斥锁 → 出现"服务端已撤销、客户端仍以为持有"的**双持有**; +- 用 etcd 的 lock 保护**外部资源** → 无效(外部资源必须自己提供版本号校验与副本一致性); +- keepalive 断连 → TTL 到期后 key 被删,业务误判为"资源已释放"。 + +> 📌 官方文档指向:`api_guarantees.md` 的 Lease APIs 一节明确链接到 `../why/#notes-on-the-usage-of-lock-and-lease`,即本册 [2.12](#212-lock-与-lease-的使用注意)。两节必须合并阅读。 + +### 7.11 操作完成的定义 + +**保证内容**: + +| 项 | 内容 | +| --- | --- | +| 何时算完成 | 一个 etcd 操作在**通过共识提交**、因而被 etcd 存储引擎"执行"(**永久存储**)时,被视为完成 | +| 客户端如何知道 | 客户端**收到 etcd server 响应**时即可知道操作已完成 | +| 不确定情形 | 若客户端**超时**,或客户端与 etcd 成员之间出现**网络中断**,客户端可能对操作状态**不确定** | +| leader 选举时 | etcd 在发生 leader 选举时也可能**中止(abort)**操作,并且在这种情况下**不会**向客户端未完成的请求发送 **`abort` 响应** | + +**对使用者的意义**: + +- "收到响应" == "已提交并持久化",可以放心地向下游宣告成功; +- **没有收到响应 ≠ 没有成功**。超时/断连时状态未知,必须用带 revision 条件的**幂等重试**(`Txn` + `mod_revision` 比较)来消除不确定性; +- leader 选举期间**不会**收到 abort 响应 —— 所以不要指望靠错误码区分"abort"与"超时"。 + +**违反后果**:把超时当作失败直接重试,会产生**重复写入**;把超时当作成功,会产生**丢写**。唯一安全的做法是可重试的幂等语义。 + +### 7.12 revision 语义 + +**保证内容**: + +| 项 | 内容 | +| --- | --- | +| 单个递增 revision | 修改 key-value 存储的 etcd 操作会被赋予**单个递增的 revision** | +| 事务 | 一个事务操作可能**多次修改** key-value 存储,但**只被赋予一个 revision** | +| key-value 对的 revision | 被该操作修改的 key-value 对的 revision 属性,其值与**操作的 revision 相同** | +| 逻辑时钟 | revision 可用作 key-value 存储的**逻辑时钟**:revision 较大的 key-value 对在 revision 较小的**之后**被修改 | +| "并发"的定义 | **两个 revision 相同的 key-value 对是被一个操作"并发"修改的** | + +**对使用者的意义**: + +| 用法 | 说明 | +| --- | --- | +| 全序判据 | 比较两个 key 的 `mod_revision` 即可判断先后,无需外部时钟 | +| 并发判据 | `mod_revision` **相同** ⇒ 由同一个操作修改 ⇒ 在 etcd 语义下它们"同时"发生 | +| 快照读 | 用同一个 revision 读一批 key,得到一致的快照视图([5.9.1](#591-用-revision-做一致的多-key-视图)) | +| 锁 / fencing | revision 可充当 **fencing token** 的等价物(见 [2.12](#212-lock-与-lease-的使用注意)) | + +**违反后果**:若 revision 不唯一/不单调,则"用 revision 做游标、做快照、做 fencing"的全部上层机制都会失效,且失效方式通常是**静默**的。 + +--- + +## 8. 持久化存储文件 + +> 引自:[etcd persistent storage files](https://etcd.io/docs/v3.7/learning/persistent-storage-files/) + +本文说明 etcd 的持久化存储格式:**命名(naming)、内容(content)** 以及可用于检视它们的**工具**,面向需要**数据恢复**的 etcd 开发者。前置阅读:etcd data model 概览(本册 [第 5 章](#5-数据模型))与 **Raft 论文**(尤其是 **"5.3 Log replication"** 一节)。 + +### 8.1 目录结构与长期存在的文件 + +| 文件名 | 用途 | +| --- | --- | +| `./member/snap/db` | **bbolt b+tree**,存放**全部已应用的数据**、成员授权信息与元数据。它知道最后应用的 WAL 日志索引(**`"consistent_index"`**) | +| `./member/snap/0000000000000002-0000000000049425.snap`、`./member/snap/0000000000000002-0000000000061ace.snap` | 周期性生成的 **legacy v2 store 快照**,包含:基本成员信息、etcd-version。自 etcd v3 起,其内容与 `/snap/db` 文件**冗余**。这些文件**周期性(30s)被清理**,保留最近的 **`--max-snapshots=5`** 个 | +| `/member/snap/000000000007a178.snap.db` | 副本落后太多时**从 leader 下载的完整 bbolt 快照**,内容与 `./member/snap/db` 同类。用于两种场景:① 响应 leader 要求从快照恢复的请求;② **服务器启动期间**,发现最后一个快照(`.snap.db` 文件)的 index **比当前 `snap.db` 中的 `consistent_index` 更新**时。注意:各副本上周期性生成的快照只以 **`*.snap`** 文件形式产生(**不是** `snap.db`),因此**不保证**最近的快照(WAL 日志中)有对应的 `*.snap.db` 文件;但这种情况下后端(`snap/db`)理应比该快照更新。该文件**在恢复完成后不会被删除**(全部内容会被填充进 `./member/snap/db`),并**周期性(30s)被清理**,同样保留 **`--max-snapshots=5`** 个;由于这些文件可能达 **O(GBs)**,存在**磁盘空间耗尽风险** | +| `./member/wal/000000000000000f-00000000000b38c7.wal`(及 `...000e-...`、`...000d-...`) | **Raft 的 Write Ahead Logs**,包含 Raft 已接受的**近期事务**、周期性快照或 **CRC** 记录。保留最近的 **`--max-wals=5`** 个文件;每个文件约 **`~64*10^6` 字节**,**超过该硬编码大小时切文件**,因此文件可能**略超**该大小(所以预分配的 `0.tmp` 并不能完全防止磁盘写满)。若快照过于稀少,实际文件数可能**多于 `--max-wals=5`**,因为**文件系统级锁**会保护这些文件、阻止其被过早删除 | +| `./member/wal/0.tmp`(或 `.../1.tmp`) | 为**下一个 WAL 文件预分配的空间**,用于避免 Raft 因 WAL 容量不足而**卡死且无法发出告警** | + +目录结构总览(整理自上表): + +```text +default.etcd/ +└── member/ + ├── snap/ + │ ├── db # bbolt b+tree(主数据) + │ ├── 0000000000000002-0000000000049425.snap # legacy v2 快照 + │ ├── 0000000000000002-0000000000061ace.snap + │ ├── 000000000007a178.snap.db # 从 leader 下载的完整 bbolt 快照 + │ ├── *.snap.broken # 无法加载的快照被重命名 + │ ├── tmp071677638 # 下载中的临时 bbolt 文件 + │ └── db.tmp.071677638 # defragment 过程中的临时后端 + └── wal/ + ├── 000000000000000d-....wal + ├── 000000000000000e-....wal + ├── 000000000000000f-00000000000b38c7.wal + └── 0.tmp # 预分配的下一个 WAL 文件 +``` + +### 8.2 临时文件 + +| 文件 | 用途 | +| --- | --- | +| `./member/snap/0000000000000002-000000000007a178.snap.broken` | 快照文件在**无法被加载**时被重命名为 **`broken`**。尝试加载最新文件发生在 etcd **启动时**,或 **`etcdctl` 的 backup/migrate 命令**期间 | +| `./member/snap/tmp071677638`(随机后缀) | 副本在响应 leader 的 **`msgSnap`** 请求(即要求从给定快照恢复存储)时创建的**临时(bbolt)文件**。内容完整获取成功后被重命名为 **`/member/snap/[SNAPSHOT-INDEX].snap.db`**。若服务器在下载中途死亡/被杀,这些文件**会留在磁盘上且永不被自动清理**,体积可能达 **GB 级**。见 **`etcd/issues/12837`**,**已在 etcd 3.5 修复** | +| `/member/snap/db.tmp.071677638`(随机后缀) | **defragmentation(碎片整理)过程中**包含后端内容(`/member/snap/db`)副本的临时文件。成功后被重命名为 **`/member/snap/db`**,替换原后端。etcd server **启动时**这些文件会被 **pruned(清理)** | + +> 💡 补充:运维上最常踩的两个坑都在这张表里 —— ① `tmp*` 残留(3.5 之前)会静默吃掉几十 GB 磁盘;② defrag 需要**与 db 等量**的空闲磁盘(`db.tmp.*` 是一份完整副本)。做 defrag 前务必检查磁盘余量。 + +### 8.3 bbolt db 文件 + +该文件(`member/snap/db`)包含 etcd 的**主要内容**,已应用到 **Raft 日志的特定位置**(见 **`consistent_index`**)。 + +#### 8.3.1 物理组织 + +| 要点 | 说明 | +| --- | --- | +| 结构 | 物理上组织为 **b+tree** | +| 页面修改 | b-tree 的**物理页从不被原地修改**(脚注 [^1]:**bbolt 文件开头的 metadata 页是原地修改的**) | +| 写入方式 | 内容会被**复制到新页**(从 **freepages 列表**回收),旧页在**没有可能访问它的打开事务**时被加入 **free-pages 列表** | +| 事务可见性 | 因此一个打开的 **RO 事务能看到存储的**一致历史状态** | +| RW 事务 | **RW 事务是排他的**,会阻塞**其他所有 RW 事务** | +| 大 value | 大 value 存储在**多个连续页**上 | +| 碎片化 | 页回收过程加上需要分配**不同大小的连续页区域**,可能导致 bbolt 存储**碎片化不断加剧** | +| 文件大小 | bbolt 文件**自身从不收缩**;只有在 **defragmentation** 过程中,文件才会被重写为一个**尾部带有一定 free pages 缓冲、并且尺寸被截断**的新文件 | + +#### 8.3.2 逻辑组织:bucket 与 key(as of version 3.5) + +bbolt 存储被划分为 **bucket**,每个 bucket 内存放**按字典序排列**的 key(`byte[] -> value byte[]`)。etcd 使用的 bucket 与 key 如下(**逐项照录**): + +| bucket | key | value | 说明 | +| --- | --- | --- | --- | +| **alarm** | `rpcpb.Alarm: {MemberID, Alarm: NONE\|NOSPACE\|CORRUPT}` | 示例值 `nil` | 表示某个成员中被诊断出的问题 | +| **auth** | `"authRevision"` | `""`(空)或 `BigEndian.PutUint64` | 任何 **Role 或 User 的变更**都会在事务提交时**递增**该字段;该值仅用于授权过程中的**乐观锁** | +| **authRoles** | `[roleName]` 字符串 | `authpb.Role` 序列化结果 | 角色表 | +| **authUsers** | `[userName]` 字符串 | `authpb.User` 序列化结果 | 用户表 | +| **cluster** | `"clusterVersion"` | 示例值 `"3.5.0"` 字符串 | 表示共识一致的公共存储版本的 **minor** 版本 | +| **cluster** | `"downgrade"` | JSON:`{ "target-version": "3.4.0", "enabled": true/false }` | 持久化**最近一次 `Downgrade RPC` 请求**配置的意图,**自 v3.5 起** | +| **key**(核心 bucket) | `[revisionId]`,使用 `bytesToRev{main,sub}` 编码 | `mvccpb.KeyValue` 序列化 proto(`key, create_rev, mod_rev, version, value, lease id`) | **key-value 的删除以结尾加 `'t'` 的方式序列化(作为 "Tombstone")** | +| **lease** | — | `leasepb.Lease` 序列化 proto(`ID, TTL, RemainingTTL`) | 注意:**`LeaseCheckpoint` 只扩展 `RemainingTTL`**,只有 **`TTL` 来自最初的 Grant**;**TTL 以秒为单位持久化**(相对于未定义的 'now'),因此**崩溃循环(crash-looping)的服务器不会释放 lease** | +| **members** | 十六进制字符串的 `[memberId]`,如 `"8e9e05c52164694d"` | JSON 字符串序列化的 `Member` 结构:`{"id":10276657743932975437,"peerURLs":["http://localhost:2380"],"name":"default","clientURLs":["http://localhost:2379"]}` | 表示共识一致的集群成员信息 | +| **members_removed** | 十六进制 `[memberId]` | `[]byte("removed")` | 记录所有被移除成员的 ID,用于校验**被移除成员永不以相同 ID 再次加入**。该字段当前(3.4)**只从 store V2 读取,从不从 V3 读取**(见 PR 12820) | +| **meta** | `"consistent_index"` | uint64 字节(BigEndian) | **最后一个应用到 bolt DB 存储的 WAL entry 的偏移** | +| **meta** | `"scheduledCompactRev"` | `bytesToRev{main,sub}` 编码(**16 字节**) | 用于在 compaction 请求后**发生崩溃时重新初始化 compaction** | +| **meta** | `"finishedCompactRev"` | `bytesToRev{main,sub}` 编码(**16 字节**) | 存储**最近一次成功 compact 到**的 revision | +| **meta** | `"confState"` | — | **自 etcd 3.5 起**(PR 12962) | +| **meta** | `"term"` | — | **自 etcd 3.5 起**(PR 12964) | +| **meta** | `"storage-version"` | — | 存储版本 | + +#### 8.3.3 检视工具 + +| 工具 | 用途 | 命令 | +| --- | --- | --- | +| `bbolt` 命令行工具 | **列出所有 bucket** | `go run go.etcd.io/bbolt/cmd/bbolt buckets ./default.etcd/member/snap/db` | +| `bbolt` 命令行工具 | **读取特定 key/value** | `go run go.etcd.io/bbolt/cmd/bbolt get ./default.etcd/member/snap/db cluster clusterVersion` | +| `etcd-dump-db` | **列出 v3 etcd 后端(bbolt)内容** | `go run go.etcd.io/etcd/v3/tools/etcd-dump-db list-bucket default.etcd`(输出 `alarm`、`auth` …) | + +```bash +# 列出 bucket +$ go run go.etcd.io/bbolt/cmd/bbolt buckets ./default.etcd/member/snap/db + +# 读取 clusterVersion +$ go run go.etcd.io/bbolt/cmd/bbolt get ./default.etcd/member/snap/db cluster clusterVersion + +# 列出 v3 后端内容 +$ go run go.etcd.io/etcd/v3/tools/etcd-dump-db list-bucket default.etcd +``` + +### 8.4 WAL 文件 + +WAL 是 **Raft 的持久化存储**,用于存放**提案(proposals)**。leader **先把提案存入自己的日志**,然后(**并发地**)用 Raft 协议**复制到 follower**;每个 follower 在**向 leader 确认复制之前**,会先把提案持久化到**自己的 WAL** 中。 + +#### 8.4.1 与经典 Raft 模型的两点不同 + +| # | 差异 | 说明 | +| --- | --- | --- | +| 1 | 不只存 entry | 它不仅持久化**带索引的 entry**,还持久化 **Raft 快照(轻量)** 与 **hard-state**,因此**成员的整个 Raft 状态可以仅从 WAL 日志恢复** | +| 2 | append-only | 它是 **append-only(只追加)** 的:entry **不在原地被覆盖**,而是**追加**到文件后面、**具有相同 index 的新 entry 取代先前的那个** | + +#### 8.4.2 文件命名 + +WAL 日志文件按模式 **`"%016x-%016x.wal", seq, index`** 命名,例:`./member/wal/0000000000000010-00000000000bf1e6.wal`。 + +| 成分 | 含义 | +| --- | --- | +| ① WAL 日志文件的**序号(seq)** | 十六进制编码 | +| ② 文件中**第一条 entry 或 snapshot 的 index** | 十六进制编码 | + +> 特例:**第一个文件 `0000000000000000-0000000000000000.wal`** 含有 **index=0** 的**初始 snapshot 记录**。 + +#### 8.4.3 物理内容:Frames(帧) + +WAL 文件是 **Frames(帧)** 的序列,每个 frame 包含(**逐项照录**): + +| # | 组成 | 说明 | +| --- | --- | --- | +| 1 | **LittleEndian** 编码的 **uint64** | 含被序列化的 **`walpb.Record`** 的**长度**(脚注 [^2]:这与多数 uint 使用 **bigendian** 的写法**不一致**) | +| 2 | **Padding** | 若干 **0 字节**,使整个 frame 的尺寸对齐(**mod 8**) | +| 3 | 序列化后的 **`walpb.Record`** 数据 | 见下表 | + +`walpb.Record` 的字段: + +| 字段 | 说明 | +| --- | --- | +| `type` | **int** 编码的**枚举**,决定下面 `data` 字段的**解释方式** | +| `data` | 依 `type` 而定,通常是**序列化后的 proto** | +| `crc` | 自 WAL 日志创建以来,该副本上**所有日志记录**中**全部 `data` 字段(不含 type)** 组合的 **RC-32 校验和**。注意 CRC 会考虑**所有**记录(即使那些**没有被 Raft 提交**的) | + +> 文件在**当前文件超过 `64*10^6` 字节**时被 **"cut"**(开始写新文件)。 + +#### 8.4.4 逻辑内容 + +WAL 文件在逻辑层包含(**逐项照录**): + +| 逻辑记录 | 说明 | +| --- | --- | +| `Raftpb.Entry:` | 由 Raft leader **复制的近期提案**;其中一部分被视为 **'committed'**,其余则**可能被逻辑覆盖** | +| `Raftpb.HardState(term,commit,vote):` | **周期性(非常频繁)**的信息;指出哪条日志 entry 是 **'committed'**(已复制到多数服务器),因此保证**不会被改变/覆盖**,可应用到后端(**v2、v3**);还包含 **"term"**(指示是否有选举相关的变化)与 **vote**(当前 term 内该副本投票给的成员) | +| `walpb.Snapshot(term, index):` | Raft 状态的**周期性快照**(**不含 DB 内容**,只有**快照日志 index 与 Raft term**)。**V2 store 内容**存放在**单独的 `*.store` 文件**中;**V3 store 内容**维护在 **bbolt 文件**中,一旦 entry 被应用进去,它就成为**隐式快照** | +| **crc32 checksum record** | 位于**每个文件开头**,用于对该文件**剩余部分恢复 CRC 校验** | +| `etcdserverpb.Metadata(node_id, cluster_id)` | 标识该日志所属的**集群与副本** | + +#### 8.4.5 每个 WAL 文件的顺序构成 + +每个 WAL 文件按顺序由以下内容构成: + +| # | 内容 | 适用文件 | +| --- | --- | --- | +| 1 | **CRC-32 frame** | 来自所有**先前文件**的 running crc;**第一个文件为 0** | +| 2 | **Metadata frame** | cluster 与 replica ID | +| 3 | **Empty Snapshot frame**(`Index:0, Term:0`) | **仅初始 WAL 文件**。其作用是维持"**所有 entry 都'前接'一个 snapshot**"这一不变式 | +| 3' | **HardState frame** | **非初始(第 2 个及以后)WAL 文件** | +| 4 | entry、hard-state 与 snapshot 记录的**混合** | 所有文件 | + +#### 8.4.6 重复 index 与覆盖规则 + +WAL 日志**可以对同一 index 包含多条 entry**(见 **Raft 论文图 7** 描述的情况)。 + +| 规则 | 说明 | +| --- | --- | +| 只追加 | etcd WAL 只追加,因此 entry 通过**追加具有相同 index 的新 entry** 被覆盖 | +| 读取时 | 读取 WAL 时,**逻辑上会用较新的 entry 覆盖旧的** | +| 最终值 | 只有满足 **`entry.index <= HardState.commit`** 的 entry 的**最后一个版本**可被视为**最终值** | +| 未提交 | **index > `HardState.commit`** 的 entry **仍可能改变** | + +#### 8.4.7 不变式(invariants) + +| 项 | 期望 | +| --- | --- | +| **terms** | 期望是**单调**的 | +| **indexes ①** | 从**某个 snapshot** 开始 | +| **indexes ②** | 在该 snapshot 之后,只要处于**同一 'term'** 就**顺序增长** | +| **indexes ③** | 若 **term 变化**,index **可以下降**,但下降到的新值必须**高于最新的 `HardState.commit`** | +| **indexes ④** | 新的 snapshot 可以以**任意 `>= HardState.commit` 的 index** 发生,从而开启新的 index 序列 | + +#### 8.4.8 检视工具 + +```bash +go install go.etcd.io/etcd/v3/tools/etcd-dump-logs@latest +go run go.etcd.io/etcd/v3/tools/etcd-dump-logs --start-index=0 aname.etcd +``` + +注意事项(原文三条): + +| # | 注意 | +| --- | --- | +| ① | 该工具**只显示 Entries**,**不显示** WAL 中的全部记录(**Snapshots、HardStates**) | +| ② | 工具会**自动应用 'overrides'** —— 若某 entry 被同一 index 下更新的 entry 覆盖,**只打印最终值** | +| ③ | 工具还会打印**未提交**的 entry(来自 **LOG 尾部**),且**不含 `HardState.commitIndex` 信息**,因此**无从判断**这些 entry 是否为最终值 | + +### 8.5 snapshot 文件 + +#### 8.5.1 文件命名 + +**命名模式**:`member/snap/{term}-{index}.snap`,由 **`("%016x-%016x.snap")`** 生成,使用两个十六进制成分: + +| 成分 | 含义 | +| --- | --- | +| **term** | 快照产生时的 **Raft term**(**两次选举之间**的周期) | +| **index** | 快照产生时**最后应用的提案的 index** | + +#### 8.5.2 创建 + +`*.snap` 文件由 **`Snapshotter.SaveSnap`** 方法创建,有 **2 个触发器**: + +| # | 触发器 | 说明 | +| --- | --- | --- | +| 1 | 每(约)**`--snapshotCount=`**(**默认 100'000**)个**已应用的提案**创建一个新文件 | 这是**近似值**,因为提案可能**成批到达**,且只在**批处理结束时**考虑快照,最后快照过程是**异步调度**的。该 flag 名称(`--snapshotCount`)相当**误导**,因为它驱动的是**上一次快照 index 与最后应用提案 index 之间的 index 差值** | +| 2 | **Raft 要求副本从快照恢复** | 副本通过网络(**`msgSnap` 消息**)接收快照时,也会把它作为(轻量的)**checkpoint 写入 WAL 日志**,这保证了 **WAL 日志尾部总是有一个有效 snapshot 后跟 entry**,从而抑制 WAL 日志中潜在的**连续性缺失** | + +> 原文脚注 [^3]:WAL 日志开头 **index:0** 的**初始快照没有对应的 `*.snap` 文件**;**旧的 `*.snap` 文件或 WAL 日志也可能被清理**。当前这些文件**大致**与 WAL 日志中的 **Snapshot entry 1-1 关联**。随着 **store v2 退役**,预计这些文件将**完全停止写入**(**3.5.x 为 opt-in,3.6.x 为强制**)。 + +#### 8.5.3 内容结构 + +文件包含序列化的 **`snapdb.snapshot`** proto,即 **`(uint32 crc, bytes data)`**;其 `data` 字段中是 **`Raftpb.Snapshot`**:**`(bytes data, SnapshotMetadata{index, term, conf} metadata)`**;**再嵌套的 `data`** 中是 **JSON 序列化的 store v2 内容**。 + +```text +snapdb.snapshot +├── crc (uint32) +└── data (bytes) + └── Raftpb.Snapshot + ├── data (bytes) ── JSON 序列化的 store v2 内容 + └── metadata SnapshotMetadata{ index, term, conf } +``` + +JSON 内容具体包括: + +| 项 | 内容 | +| --- | --- | +| Term | — | +| Index | — | +| Membership data | `/0/members/8e9e05c52164694d/attributes -> {"name":"default","clientURLs":["http://localhost:2379"]}` | +| Membership data | `/0/members/8e9e05c52164694d/RaftAttributes -> "{\"peerURLs\":[\"http://localhost:2380\"]}"` | +| Storage version | `/0/version -> 3.5.0` | + +#### 8.5.4 protoc 解码示例 + +在 **etcd 根目录**执行: + +```bash +cat default.etcd/member/snap/0000000000000002-0000000000049425.snap | + protoc --decode=snappb.snapshot \ + server/etcdserver/api/snap/snappb/snap.proto \ + -I $(go list -f '{{.Dir}}' github.com/gogo/protobuf/proto)/.. \ + -I . \ + -I $(go list -m -f '{{.Dir}}' github.com/gogo/protobuf)/protobuf +``` + +> 原文注解:类似地,可提取 **`data`** 字段并按 **`Raftpb.Snapshot`** 解码。 + +#### 8.5.5 示例 JSON(etcd 3.4 `*.snap` 文件中的 store v2 内容) + +顶层对象含 **`Root`** 与 **`WatcherHub`** 两个键。 + +```json +{ + "Root": { + "Path": "/", + "CreatedIndex": 0, + "ModifiedIndex": 0, + "ExpireTime": "0001-01-01T00:00:00Z", + "Value": "", + "Children": { + "0": { + "Path": "/0", + "Children": { + "members": { + "CreatedIndex": 1, + "ModifiedIndex": 1, + "Children": { + "8e9e05c52164694d": { + "Children": { + "attributes": { "CreatedIndex": 2, "ModifiedIndex": 2, + "Value": "{\"name\":\"default\",\"clientURLs\":[\"http://localhost:2379\"]}" }, + "RaftAttributes": { "CreatedIndex": 1, "ModifiedIndex": 1, + "Value": "{\"peerURLs\":[\"http://localhost:2380\"]}" } + } + } + } + }, + "version": { "CreatedIndex": 3, "ModifiedIndex": 3, "Value": "3.5.0" } + } + }, + "1": {} + } + }, + "WatcherHub": { + "EventHistory": { + "Queue": { + "Events": [ + { "action": "create", "node": { "key": "/0/members/8e9e05c52164694d/RaftAttributes", "modifiedIndex": 1, "createdIndex": 1 } }, + { "action": "set", "node": { "key": "/0/members/8e9e05c52164694d/attributes", "modifiedIndex": 2, "createdIndex": 2 } }, + { "action": "set", "node": { "key": "/0/version", "modifiedIndex": 3, "createdIndex": 3 } } + ] + } + } + } +} +``` + +结构说明(整理自原文逐项描述): + +| 路径 | index | 值 | +| --- | --- | --- | +| `/0/members` | CreatedIndex / ModifiedIndex = **1** | — | +| `/0/members/8e9e05c52164694d/attributes` | **2** | `{"name":"default","clientURLs":["http://localhost:2379"]}` | +| `/0/members/8e9e05c52164694d/RaftAttributes` | **1** | `{"peerURLs":["http://localhost:2380"]}` | +| `/0/version` | **3** | `"3.5.0"` | +| `/1` | — | 空节点 | +| `WatcherHub.EventHistory.Queue.Events` | — | 事件数组,每个元素形如 `{"action":"create"|"set","node":{"key":...,"value":...,"modifiedIndex":N,"createdIndex":N}}`,依次记录 `RaftAttributes`(create, index 1)、`attributes`(set, index 2)、`/0/version`(set, index 3) | + +### 8.6 版本间格式变化 + +> 📌 官方文档未提供:`learning/persistent-storage-files.md` 的 **Changes** 小节保留用于描述不同 etcd 版本之间文件格式的变化,**原文当前为空**。 + +--- + +## 9. 系统限制 + +> 引自:[System limits](https://etcd.io/docs/v3.7/dev-guide/limit/) + +### 9.1 请求大小限制 + +| 项 | 值 | +| --- | --- | +| 设计定位 | etcd 被设计用于处理**元数据类型的小 key-value 对** | +| 大请求的影响 | 更大的请求也能工作,但可能**增加其他请求的延迟** | +| **默认最大请求大小** | **1.5 MiB** | +| 配置方式 | 通过 etcd server 的 **`--max-request-bytes`** flag 配置 | + +### 9.2 存储大小限制 + +| 项 | 值 | +| --- | --- | +| **默认存储大小限制** | **2 GiB** | +| 配置方式 | 通过 **`--quota-backend-bytes`** flag 配置 | +| **普通环境下建议的最大值** | **8 GiB** | +| 超过建议值的行为 | 若配置值超过它,etcd 会在**启动时发出警告** | + +### 9.3 官方文档未提供的其他限制 + +> 📌 官方文档未提供:`dev-guide/limit.md`(v3.7)原文**仅含上述两项限制**,**未列出 watch 数量等其他数值型限制**。本章不补充任何来源不明的数字。 + +> 💡 补充:虽然 limit 页没写,但**不得超过 8 GiB 的建议**在实践中通常与 [8.1](#81-目录结构与长期存在的文件) 里提到的 WAL/`*.snap.db` 磁盘占用叠加考虑——配额只约束 `member/snap/db`,不约束 WAL 与快照文件,因此实际磁盘规划要在配额之外再留出 WAL 与快照的空间(WAL 单文件约 `~64*10^6` 字节,最多 `--max-wals=5` 个;`*.snap.db` 可能达 O(GBs))。 + +--- + +## 10. etcd 特性清单 + +> 引自:[etcd features](https://etcd.io/docs/v3.7/dev-guide/features/) + +etcd 特性(feature)分为**三个阶段**:**experimental(实验)**、**stable(稳定)** 和 **unsafe(不安全)**。可通过运行 **`etcd --help`** 获取特性列表。 + +### 10.1 三个阶段总览 + +| 阶段 | 默认状态 | 是否受 feature deprecation 策略约束 | 识别方式 | +| --- | --- | --- | --- | +| **Experimental** | **默认禁用** | **否**(移除无需遵循策略,除非毕业为 stable) | flag 名称带 **`--experimental`** 前缀 | +| **Stable** | **可能默认启用** | **是** | etcd 中最常见的特性阶段 | +| **Unsafe** | **默认禁用** | **否** | 在用法文档的 **`Unsafe feature:`** 小节下列出 | + +### 10.2 Experimental + +为了**尽早获得反馈**,任何新特性通常先作为实验特性加入。实验特性可通过 flag 名称识别——应带有 **`--experimental`** 前缀。使用时需注意(**逐条照录**): + +| # | 注意事项 | +| --- | --- | +| 1 | 由于**缺乏用户测试**,它**可能有 bug**;启用后**可能不按预期工作** | +| 2 | **默认禁用** | +| 3 | 对这类特性的支持**可能随时被取消,且不另行通知** | +| 3.1 | 它可能在下一个 **minor 或 major 版本**中被移除,**无需遵循 feature deprecation 策略**,除非它**毕业(graduate)为 stable** | +| 3.2 | 项目团队欢迎用户报告与实验特性相关的问题,但这类问题的**优先级可能低于**稳定特性相关问题 | +| 4 | 当实验性 feature flag **毕业到 stable 阶段时即被弃用**;用户应**尽快改用稳定 feature flag** | + +### 10.3 Stable + +这是 etcd 中**最常见**的特性阶段,其特征为(**逐条照录**): + +| # | 特征 | +| --- | --- | +| 1 | 作为 etcd **受支持发行版的一部分获得支持** | +| 2 | **可能默认启用** | +| 3 | 终止支持**必须遵循 feature deprecation 策略** | + +### 10.4 Unsafe + +| 项 | 内容 | +| --- | --- | +| 出现频率 | Unsafe 特性**很少见** | +| 文档位置 | 在 etcd 用法文档中的 **`Unsafe feature:`** 小节下列出 | +| 默认状态 | **默认禁用** | +| 使用要求 | 应**谨慎**并**按文档**使用 | +| 移除规则 | 可在下一个 **minor 或 major 版本**中被移除,**无需遵循 feature deprecation 策略** | + +### 10.5 Feature Deprecation + +#### 10.5.1 Experimental 的弃用 + +实验特性在**毕业到 stable 阶段时即被弃用**。 + +| 步骤 | 内容 | +| --- | --- | +| 文档表现 | 该实验特性文档会显示弃用消息,并**推荐使用相关的稳定特性 flag**,例如:**`DEPRECATED. Use instead.`** | +| 移除时机 | 被弃用的特性将在**随后的发行版**中被移除 | + +#### 10.5.2 Stable 的弃用 + +随着项目演进,稳定特性有时也需要被弃用和移除。此时(**逐条照录**): + +| 步骤 | 内容 | +| --- | --- | +| 1 | 特性文档会在**计划弃用的发行版之前**显示**警告消息**,例如:**`To be deprecated in .`**。若已有新特性计划取代该 `To be deprecated` 特性,文档还会给出提示,例如:**`Use instead.`** | +| 2 | 该特性将在**计划的发行版**中被弃用。届时特性文档会显示弃用消息并推荐使用相关的稳定特性,例如:**`DEPRECATED. Use instead.`** | +| 3 | 被弃用的特性将在**随后的发行版**中被移除 | + +```text +弃用流程示意(三个阶段 × 两条路径) + +Experimental --(graduate)--> 文档显示 "DEPRECATED. Use instead." + --> 随后的发行版中移除 + +Stable --(计划弃用)--> 文档先显示 "To be deprecated in ." / "Use ...instead." + --> 计划发行版中显示 "DEPRECATED. Use instead." + --> 随后的发行版中移除 + +Unsafe --> 可在下一个 minor/major 版本直接移除(无需遵循策略) +``` + +### 10.6 查看特性清单的方法 + +```bash +$ etcd --help +``` + +> 原文说明:可通过运行 **`etcd --help`** 获取特性列表。 + +> 💡 补充:升级/变更版本时,建议把 `etcd --help` 输出中带 **`--experimental`** 前缀的 flag 全部记录下来——它们是唯一"可能随时被取消且不另行通知"的一类,也是升级后最容易突然失效的配置项。 + +--- + +## 11. 设计原理 + +本章覆盖四份官方设计文档。每节按 **是什么 / 为什么 / 内部机制 / 怎么用 / 注意事项** 五问展开。 + +### 11.1 客户端设计 + +> 引自:[etcd client design](https://etcd.io/docs/v3.7/learning/design-client/) — 作者:Gyuho Lee (github.com/gyuho, AWS)、Joe Betz (github.com/jpbetz, Google) + +#### 是什么 + +etcd server 自身经过了多年的**故障注入测试**,绝大多数复杂逻辑(**集群成员关系对客户端透明**、**Raft 层把提案转发给 leader**)都在服务端完成。但"服务端正确"与"客户端+服务端组合正确"是两件事:理想情况下服务端对外呈现**一个逻辑集群视图**,**客户端负责在多个副本之间做自动 failover**。 + +#### 为什么:客户端的四项要求 + +| 要求 | 内容 | +| --- | --- | +| ***Correctness*** | 请求可能因服务端故障失败,但**绝不破坏一致性保证** —— 全局顺序属性、**不写入损坏数据**、可变操作的 **at-most once 语义**、**watch 不会看到部分事件** | +| ***Liveness*** | 服务端可能短暂失败或断连,客户端**无论如何都要能向前推进**;除非显式配置,客户端**不应因等待离线节点而死锁**(参考 **etcd#8980**)。理想做法是用 **HTTP/2 ping** 探测不可用节点并带**清晰错误信息**切换 | +| ***Effectiveness*** | 以**最小资源开销**运行 —— endpoint 切换后**旧 TCP 连接应优雅关闭**(**etcd#9212**);failover 要能**有效预测下一个副本**,不在已失败的节点上浪费重试 | +| ***Portability*** | 官方客户端实现应**可移植到其他语言绑定**,错误处理与重试策略在**各语言间保持一致**;由于 etcd **全面押注 gRPC**,实现须紧贴 **gRPC 长期设计目标**(如可插拔重试策略要与 [gRPC retry A6](https://github.com/grpc/proposal/blob/master/A6-client-retries.md) 兼容);**两个客户端版本之间升级应无中断** | + +#### 内部机制(一):术语表 + +| 术语 | 含义 | +| --- | --- | +| *clientv3* | etcd **v3 API** 的**官方 Go 客户端** | +| *clientv3-grpc1.0* | 基于 **`grpc-go v1.0.x`**,用于最新的 etcd **v3.1** | +| *clientv3-grpc1.7* | 基于 **`grpc-go v1.7.x`**,用于最新的 etcd **v3.2 和 v3.3** | +| *clientv3-grpc1.23* | 基于 **`grpc-go v1.23.x`**,用于最新的 etcd **v3.4** | +| *Balancer* | etcd 客户端**负载均衡器**,实现**重试与 failover** 机制,应在**多个 endpoint 间自动均衡负载** | +| *Endpoints* | 客户端可连接的 etcd server endpoint 列表,**典型为 3 个或 5 个 client URL** | +| *Pinned endpoint* | 配多个 endpoint 时,**<= v3.3** 的 balancer **只挑一个建 TCP 连接**以节省总连接数;**v3.4** 中 balancer 对**每个请求轮转** pinned endpoint,负载更均匀 | +| *Client Connection* | 通过 **gRPC Dial** 与某台 etcd server 建立的 **TCP 连接** | +| *Sub Connection* | gRPC **`SubConn`** 接口,**每个 sub-connection 含一组地址**;一个 `ClientConn` 可映射**多个 `SubConn`**(如 example.com 解析为 `10.10.10.1`、`10.10.10.2` 两个 sub-connection)。etcd **v3.4 balancer 用内部 resolver 为每个 endpoint 建一个 sub-connection** | +| *Transient disconnect* | gRPC server 返回状态错误 **`code Unavailable`** | + +#### 内部机制(二):三个组件与两类错误 + +etcd client 实现**三个组件**: + +| # | 组件 | 职责 | +| --- | --- | --- | +| 1 | **balancer** | 建立到 etcd 集群的 **gRPC 连接** | +| 2 | **API client** | 向 etcd server **发送 RPC** | +| 3 | **error handler** | 判断失败请求应**重试**还是**切换 endpoint** | + +各语言在建连方式(如配置 TLS)、Protocol Buffer 编解码、流式 RPC 处理上可能不同,但**服务端返回的错误是一致的**,因此**错误处理与重试策略也应一致**。 + +两类典型错误: + +| 错误 | 类别 | 处理 | +| --- | --- | --- | +| `"rpc error: code = Unavailable desc = etcdserver: request timed out"` | **瞬时错误** | **应重试** | +| `rpc error: code = InvalidArgument desc = etcdserver: key is not provided` | 请求本身**非法** | **不应重试** | + +解析方式: + +| 语言 | 解析方式 | +| --- | --- | +| Go 客户端 | **`google.golang.org/grpc/status.FromError`** | +| Java 客户端 | **`io.grpc.Status.fromThrowable`** | + +#### 内部机制(三):三代客户端演进 + +| 版本 | 适用 etcd | gRPC | Balancer 行为 | 局限 | +| --- | --- | --- | --- | --- | +| **clientv3-grpc1.0** | v3.1 | `grpc-go v1.0.x` | 配置多个 endpoint 时**维持多条 TCP 连接**,然后**挑一个地址**发送全部客户端请求;pinned 地址**一直保持到 client 对象关闭**(Figure 1)。客户端收到错误时**随机挑另一个地址重试** | 开多条 TCP 连接虽然 failover 更快但**更耗资源**;balancer **不理解节点健康状态与集群成员关系**,可能**卡在某个已失败或被分区的节点上** | +| **clientv3-grpc1.7** | v3.2 / v3.3 | `grpc-go v1.7.x` | **只维持一条**到选定 etcd server 的 TCP 连接。给定多个 endpoint 时**先尝试连所有**,一旦某个连接成功即 **pin 该地址并关闭其余连接**(Figure 2)。来自服务端或客户端网络故障的错误都送进客户端 error handler(Figure 3)。error handler 依据**错误码与错误消息**决定"原地重试"还是"切换地址"(Figure 4、Figure 5) | 见下 | +| **clientv3-grpc1.23** | v3.4 | `grpc-go v1.23.x` | **简化 balancer failover 逻辑**:不再维护可能过时的 unhealthy endpoint 列表,而是**只要与当前 endpoint 断开就 round robin 到下一个 endpoint**;**不对 endpoint 状态做任何假设**,因此不需要复杂的状态跟踪 | 见下 | + +**clientv3-grpc1.7 的流式 RPC 处理**:`Watch`、`KeepAlive` 这类 stream RPC **通常不设超时**;客户端改为发送**周期性 HTTP/2 ping** 检查 pinned endpoint 状态,服务端**不应答 ping** 时 balancer 切到其他 endpoint(Figure 6)。 + +**clientv3-grpc1.7 的三条局限**: + +| # | 局限 | +| --- | --- | +| 1 | **HTTP/2 keepalive 只是简单的 gRPC server ping 机制,不理解集群成员关系**,因此**无法检测网络分区** —— 被分区的 gRPC server **仍能回应 ping**,balancer 会**卡在分区节点上**。理想情况是在请求超时之前就由 keepalive ping 探测到分区并触发切换(**etcd#8673**、Figure 7) | +| 2 | balancer 维护 **unhealthy endpoint 列表**:断连地址被加入 "unhealthy" 列表,直到一段 **wait duration** 之后才重新可用;该 wait duration **硬编码为 dial timeout,默认值 5 秒**。会产生**误判**:节点 A 刚被拉黑就恢复了,但接下来 **5 秒仍不可用**(Figure 8)。**`clientv3-grpc1.0` 存在同样问题** | +| 3 | 上游 gRPC Go 已迁移到**新 balancer 接口**,底层兼容虽勉强维持,但 etcd 客户端仍受**细微破坏性变更**影响(**grpc-go#1649**);gRPC 维护者也建议**不要依赖旧 balancer 接口**(**grpc-go#1942**)。新特性(如 retry policy)**不会 backport** 到 gRPC 1.7 分支,故**服务端与客户端都必须迁移到最新 gRPC** | + +**clientv3-grpc1.23 的设计要点**: + +| 项 | 内容 | +| --- | --- | +| 设计目标 | 简化 balancer failover 逻辑;**升级到 `clientv3-grpc1.23` 应该没有问题**:所有改动都是**内部的**,并保持**向后兼容** | +| 连接模型 | 给定多个 endpoint 时,创建**多个 sub-connection(每个 endpoint 一个)**;而 `clientv3-grpc1.7` 只创建**一条到 pinned endpoint** 的连接(Figure 9)。例如 **5 节点集群中 `clientv3-grpc1.23` 需要 5 条 TCP 连接,`clientv3-grpc1.7` 只需 1 条**。保留 TCP 连接池会**消耗更多资源**,但换来**更灵活的负载均衡与更好的 failover 性能** | +| 负载均衡策略 | **默认负载均衡策略是 round robin**,但容易扩展为其他类型(如 **power of two、pick leader** 等) | +| 实现差异 | `clientv3-grpc1.23` 使用 **gRPC resolver group** 并实现 **balancer picker policy**,把复杂均衡工作交给**上游 gRPC**;`clientv3-grpc1.7` **手工管理**每条 gRPC 连接与 failover。`clientv3-grpc1.23` 把重试实现在 **gRPC interceptor chain** 中,自动处理 gRPC 内部错误并支持 **backoff** 等高级重试策略;`clientv3-grpc1.7` **手工解释** gRPC 错误来决定重试 | +| 局限 | 可缓存各 endpoint 状态做改进(提前 ping 维持健康候选列表、断开时优先健康 endpoint),但会复杂化实现,**留待后续版本**;客户端 keepalive ping **仍不理解网络分区**,流式请求可能卡在分区节点,需要能**理解集群成员关系的高级健康检查服务**(**etcd#8673**);当前重试逻辑是以 **interceptor 手工实现**的,未来可用**官方 gRPC retries** 简化 | + +#### 怎么用:配置项与默认值清单 + +| 项 | 值 / 默认 | +| --- | --- | +| unhealthy endpoint 等待时长 | **硬编码为 dial timeout,默认 5 秒** | +| 默认负载均衡策略 | **round robin** | +| 每 endpoint 连接数(grpc1.23) | **1 个 sub-connection**;**5 节点集群 = 5 条 TCP 连接** | +| 每 endpoint 连接数(grpc1.7) | **1 条 TCP 连接(pinned)** | +| 流式 RPC 健康探测 | **周期性 HTTP/2 ping(keepalive)** | + +连接数对照表: + +| 集群规模 | clientv3-grpc1.23 | clientv3-grpc1.7 | clientv3-grpc1.0 | +| --- | --- | --- | --- | +| 3 节点 | 3 条 TCP | 1 条(pinned) | 多条(挑 1 个发全部请求) | +| 5 节点 | **5 条 TCP** | **1 条** | 多条 | + +#### 注意事项 + +| # | 注意 | +| --- | --- | +| 1 | **不是所有错误都该重试**。`Unavailable` 可重试;`InvalidArgument`(如 `key is not provided`)重试无意义 | +| 2 | **dial timeout 默认 5 秒同时充当 unhealthy endpoint 的冷却时长**(grpc1.7),会出现"节点已恢复但还要等 5 秒"的误判 | +| 3 | **keepalive ping 无法检测网络分区**(grpc1.7 与 grpc1.23 都存在该问题,见 etcd#8673),流式请求可能卡在分区节点 | +| 4 | **必须迁移到最新 gRPC** —— 新特性不会 backport 到 gRPC 1.7 | + +> 📌 官方文档未提供:`learning/design-client.md` 是**架构设计文档**,**未逐项列出 `clientv3.Config` 的字段名**(如 `DialTimeout`、`MaxCallSendMsgSize`、`AutoSyncInterval` 等),也未出现 **`clientv3.New`、`kv.Put`、`OpGet`** 等 Go API 名。上表仅包含源文中明确出现的值,未做任何补充。 + +> 💡 补充:工程实践上,客户端初始化建议至少显式设置 `DialTimeout`(默认 5 秒在跨可用区场景下可能偏短)与 `Endpoints`(填**全部**成员地址而不是只填一个,这样 balancer 才有 failover 空间)。这一条不是官方文档原文。 + +### 11.2 Learner 设计 + +> 引自:[etcd learner design](https://etcd.io/docs/v3.7/learning/design-learner/) — 作者:Gyuho Lee (github.com/gyuho, AWS)、Joe Betz (github.com/jpbetz, Google) + +#### 是什么 + +为填补成员重配置的可用性缺口,[Raft §4.2.1](https://github.com/ongardie/dissertation/blob/master/stanford.pdf) 引入新的节点状态 **"Learner"**:以**非投票成员**身份加入集群,**直到追上 leader 的日志**。 + +#### 为什么:成员重配置的三大运维挑战 + +**挑战 1 —— 新成员压垮 leader** + +| 环节 | 现象 | +| --- | --- | +| 起步 | 新加入的 etcd 成员**从零数据起步**,需要从 leader **拉取大量更新**直到追上 leader 的日志 | +| 后果 | **leader 网络更容易过载**,导致发往 follower 的**心跳被阻塞或丢弃** | +| 连锁 | follower 可能 **election-timeout** 而发起**新一轮 leader 选举** | +| 结论 | 即"**带新成员的集群更易发生 leader 选举**";选举本身与随后向新成员传播更新的过程都容易造成**集群不可用**(Figure 1) | + +**挑战 2 —— 网络分区场景(四个子场景,quorum 数字逐条保留)** + +| 子场景 | 情形 | quorum | 结果 | +| --- | --- | --- | --- | +| 基础 | 若 leader 仍保有 **active quorum** | — | 集群**继续正常运作**(Figure 2) | +| **2.1 Leader isolation** | leader 被孤立 | — | leader 监控每个 follower 的进度;一旦**失去与 quorum 的连接**,leader **退回 follower**,影响集群可用性(Figure 3) | +| **2.2 Cluster Split 3+1** | 3 节点集群加入新成员后集群规模变 **4**、**quorum 变 3**;若新节点恰好与 leader 处于同一分区 | **3** | leader 仍保有 **active quorum 3**,**不发生选举**,可用性**不受影响**(Figure 4) | +| **2.3 Cluster Split 2+2** | 2-2 分区 | **3** | 两边**都没有 quorum 3**,**触发 leader 选举**(Figure 5) | +| **2.4 Quorum Lost** | **先发生分区、再加入新成员**。已分区的 3 节点集群本就有一个断连 follower,加入新成员时 **quorum 从 2 变 3** | **2 → 3** | 此时 **4 个节点只有 2 个活跃**,**失去 quorum 并启动新一轮选举**(Figure 6) | + +关键结论(原文): + +- 因为 **`member add` 会改变 quorum 大小**,**始终推荐先 `member remove` 再替换不健康节点**; +- 向 **1 节点集群**加成员会把 **quorum 变为 2**,前一个 leader 一旦发现 quorum 不活跃就**立即触发选举**; +- 根因是 **`member add` 是两步过程**:先执行 member add 命令,再启动新节点进程(Figure 7)。 + +**挑战 3 —— 集群配置错误** + +| 环节 | 现象 | +| --- | --- | +| 两步流程 | 成员重配置是两步:**`etcdctl member add`** + **以给定 peer URL 启动 etcd 进程** | +| 致命点 | **`member add` 命令不论 URL 是否有效都会被应用**;若第一步用了**非法 URL**,第二步根本起不来 | +| 不可逆 | 一旦集群**丢失 quorum**,成员变更就**无法回滚**(Figure 8) | +| 多节点情形 | 两个成员 down(一个故障、一个配置错误)、两个 up,但要变更成员资格**至少需要 3 票**(Figure 9) | +| 运维代价 | 此时运维必须**手工**用 **`etcd --force-new-cluster`** flag 重建集群 | + +由此引出四个核心问题(原文): + +1. 能否让成员重配置**不改变 quorum 大小**从而降低破坏性? +2. 能否让新节点**先空闲**、只向 leader 请求**最少更新**直到追上? +3. 能否让配置错误**始终可回滚**(错误的 `member add` 命令绝不该搞挂集群)? +4. 用户加成员时还需要关心网络拓扑吗?`member add` API 能否不关心节点位置与正在发生的分区? + +> 背景补充(原文):etcd 已成为 **Kubernetes 的关键服务**,因此这些问题格外重要。 + +#### 内部机制(一):v3.4 已实现的特性 + +| # | 特性 | +| --- | --- | +| 1 | 运维加 learner 节点的工作量应尽可能小:**`member add --learner`** 命令添加 learner,它以**非投票成员**身份加入集群,但**仍然接收 leader 的全部数据**(Figure 10) | +| 2 | learner 追上 leader 进度后,可用 **`member promote` API** 提升为**投票成员**,此后才**计入 quorum**(Figure 11) | +| 3 | etcd server **校验 promote 请求**以确保操作安全:**只有日志追上 leader 之后**,learner 才能被提升为投票成员(Figure 12) | +| 4 | learner 在被提升前只作为 **standby 节点**:**leadership 不能转移给 learner**;**learner 拒绝客户端的读写**(客户端 balancer **不应把请求路由到 learner**),这意味着 learner **不需要向 leader 发起 Read Index 请求**。该限制简化了 v3.4 中 learner 的初始实现(Figure 13) | +| 5 | 此外,etcd **限制集群可拥有的 learner 总数**,避免 leader 因日志复制过载。**learner 永远不会自我提升**;etcd 只提供 learner **状态信息与安全检查**,**是否 promote 由集群运维做最终决定** | + +#### 内部机制(二):未来版本计划特性(4 项) + +| # | 计划 | 说明 | +| --- | --- | --- | +| 1 | *Make learner state only and default* | 把**新成员默认状态设为 learner** 将大幅提升成员重配置安全性,因为 **learner 不改变 quorum 大小**,**配置错误总能回滚且不会丢 quorum** | +| 2 | *Make voting-member promotion fully automatic* | learner 追上 leader 日志后集群**自动提升**。etcd 要求用户定义**若干阈值**,满足条件后 learner 自动提升为投票成员;从用户视角 **`member add` 用法不变但更安全** | +| 3 | *Make learner standby failover node* | learner 作为 **standby 节点**加入,当集群可用性受影响时**自动提升** | +| 4 | *Make learner read-only* | learner 可作为**永不提升的只读节点**。**弱一致模式**下 learner 只从 leader 接收数据、**不处理写**,本地提供读可**大幅降低 leader 负载**但可能**返回过期数据**;**强一致模式**下 learner 向 leader 请求 **read index** 以提供最新数据,但仍**拒绝写** | + +#### 内部机制(三):v3.4 实现细节(附录) + +| 项 | 内容 | +| --- | --- | +| **向 `MemberAdd` API 暴露 "Learner" 节点类型** | etcd client 在 **`MemberAdd` API** 上增加一个 **learner 节点 flag**;etcd server handler 以 **`pb.ConfChangeAddLearnerNode`** 类型应用成员变更条目。命令被应用后,server 以 **`etcd --initial-cluster-state=existing`** flag 加入集群。该 learner 节点**既不能投票也不计入 quorum** | +| **leadership 转移** | etcd server **绝不能把 leadership 转移给 learner**(它可能仍然落后且不计入 quorum) | +| **learner 数量限制** | etcd 把集群可拥有的 **learner 数量限制为一个**;learner 越多,leader 要传播的数据越多 | +| **learner 拒绝的请求** | 客户端可以与 learner 节点通信,但 learner **拒绝除 `serializable read` 与 `member status API` 之外的所有请求**(为简化初始实现)。未来 learner 可扩展为**持续镜像集群数据的只读 server** | +| **客户端 balancer 义务** | **客户端 balancer 必须提供 helper 函数排除 learner 节点 endpoint**,否则发往 learner 的请求会失败;客户端 **sync member 调用**与 **endpoints update 调用**都要把 **learner 节点类型纳入考虑** | +| **响应应指出 learner** | **`MemberList` 与 `MemberStatus` 的响应应指出哪个节点是 learner** | +| **新增 `MemberPromote` API** | 在 Raft 内部,对 learner 节点发起**第二次 `MemberAdd` 调用**即将其提升为投票成员。leader 维护每个 follower 与 learner 的**进度**。若 learner **尚未完成其 snapshot message**,则**拒绝** promote 请求 | + +**接受 promote 的充要条件**: + +| # | 条件 | +| --- | --- | +| 1 | learner 节点处于**健康状态** | +| 2 | learner 与 leader **已同步**,或差距在阈值内(例如待复制给 learner 的**条目数少于 snapshot count 的 1/10**,即提升后 leader 大概率也不必再发 snapshot 给该 learner) | + +> 原文强调:所有这些逻辑**硬编码在 `etcdserver` 包中,不可配置**。 + +#### 内部机制(四):Learner vs. Mirror Maker + +| 项 | Mirror Maker | Learner | +| --- | --- | --- | +| 实现 | etcd 用 **watch API** 实现 "mirror maker",**持续把 key 的创建与更新中继到另一个集群**;完成初始同步后镜像通常**延迟开销很低** | Raft learner,复制 Raft 日志 | +| 重叠点 | 二者在"**复制现有数据用于只读**"上有重叠 | 同左 | +| 一致性 | **镜像不保证 linearizability** | 保证 | +| 顺序 | 镜像**没有顺序保证**:网络断连期间先前的 key-value 可能已被丢弃,**客户端需自行校验 watch 响应的顺序** | 有 | +| 结论 | **要最低延迟(如跨数据中心)用 mirror,代价是一致性** | **要保留全部历史数据及其顺序用 learner** | + +#### 怎么用:相关 RPC 与参数汇总 + +| 类别 | 名称 | +| --- | --- | +| RPC / API | **`MemberAdd`**(带 learner flag)、**`MemberPromote`**、**`MemberList`**、**`MemberStatus`** | +| 内部类型 | **`pb.ConfChangeAddLearnerNode`** | +| etcdctl 命令 | **`member add --learner`**、**`member promote`**、**`member remove`** | +| server flag | **`--initial-cluster-state=existing`**、**`--force-new-cluster`** | + +参考 issue(原文给出):原始 issue [etcd#9161](https://github.com/etcd-io/etcd/issues/9161);use case [etcd#3715](https://github.com/etcd-io/etcd/issues/3715)、[etcd#8888](https://github.com/etcd-io/etcd/issues/8888)、[etcd#10114](https://github.com/etcd-io/etcd/issues/10114)。 + +#### 注意事项 + +| # | 注意 | +| --- | --- | +| 1 | **learner 数量上限**:正文表述为"限制 learner 总数",附录明确为"**限制为一个**"。本册**以附录为准** | +| 2 | **learner 拒绝读写**:除 `serializable read` 与 `member status API` 之外的所有请求都会被拒 | +| 3 | **客户端必须排除 learner endpoint**,否则请求失败 | +| 4 | **promote 前必须已追上**,否则被拒绝;promote 逻辑**硬编码、不可配置** | +| 5 | **`member add` 会改变 quorum**:始终推荐**先 `member remove` 再替换不健康节点** | +| 6 | **learner 永远不会自我提升**,是否 promote 由运维决定 | + +> 📌 官方文档未提供:源文中**没有出现任何 `--experimental-*` 参数**(v3.4 中 learner 相关曾以实验特性引入,但本页未提及,故本册不列)。 + +> 💡 补充:把 learner 用在实践中最典型的姿势是——**加新成员时先加 learner,等它追上再 promote**,这样在整个追赶过程中 quorum 大小不变,不会出现"加节点把集群搞挂"的事故。这也是 [11.4](#114-运行时重配置设计) 两阶段模型在运维上的具体落地。 + +### 11.3 认证设计 v3 + +> 引自:[etcd v3 authentication design](https://etcd.io/docs/v3.7/learning/design-auth-v3/) + +#### 是什么 + +v3 协议用 **gRPC** 作为传输层(而非 v2 的 RESTful 接口),因此 v3 auth 是**基于连接的认证(connection based authentication)**,而 v2 是较慢的**逐请求认证(per-request)**。v3 对认证机制有**明确的定义与实现**,修复了 v2 的缺陷。 + +#### 为什么:不复用 v2 auth 的三条理由 + +| # | 理由 | +| --- | --- | +| 1 | gRPC 传输层带来迭代改进机会:**每连接认证**取代**逐请求认证**,更快 | +| 2 | **v2 auth 的语义在一致性推理上很笨拙** | +| 3 | v3 是**扁平 key space**(不同于 v2 的目录结构),权限检查以**区间匹配(interval matching)**实现 | + +**功能性需求(逐条照录)**: + +| 需求 | 说明 | +| --- | --- | +| 每连接认证,而非每请求 | ① 基于 **User ID + password** 的认证,实现在 **gRPC API** 上;② **认证策略变更后必须刷新认证** | +| 功能应与 v2 一样简单实用 | v3 提供**扁平 key space**(不同于 v2 的目录结构),权限检查以**区间匹配(interval matching)**实现 | +| 应提供比 v2 auth 更强的一致性保证 | — | + +**主要改动(3 条)**: + +| # | 改动 | +| --- | --- | +| 1 | 客户端在发送认证请求前,必须先建立一条**专用于认证的独立连接** | +| 2 | 向 Raft 命令(**`etcdserverpb.InternalRaftRequest`**)中加入**权限信息**(**user ID** 与 **authorized revision**) | +| 3 | 每个请求在**状态机层**做权限检查,**而不是在 API 层** | + +#### 内部机制(一):权限元数据的一致性 + +| 论点 | 内容 | +| --- | --- | +| 存放位置 | auth 的元数据必须与其他数据一样,存放在**由 etcd Raft 协议控制的存储**中,否则会牺牲整个集群的**可用性与一致性** | +| 错误方案 | 若读写权限元数据需要**每个节点一致同意(超过 quorum)**,那么**单节点故障就能停掉整个集群** | +| 更坏的方案 | 要求**所有节点同时同意** ⇒ "只要任一成员 down,普通读写请求的权限检查就完不成" —— 即使集群仍有可用 quorum | +| 结论 | 一致同意方案最终会**降低可用性**;**基于 Raft 的 quorum 共识就够了**,因为**一致的顺序自然蕴含一致性** | + +**v2 的反面教材**: + +| 项 | v2 的问题 | +| --- | --- | +| 检查位置 | 每个权限检查由**收到客户端请求的那个 etcd 成员**(**包括 follower**)处理(`server/etcdserver/api/v2http/client.go`) | +| 后果 | 检查可能基于**过期的元数据** | +| 现象 | 运维执行 etcdctl 后 auth 配置**不能立即生效**,且**无从得知过期元数据会活跃多久** | +| 实践 | 配置变更在命令执行后**立即反映**,但在**高负载场景**下不一致状态会被延长,导致**反直觉的结果**(需要类似 [PR#4317](https://github.com/etcd-io/etcd/pull/4317#issuecomment-179037582) 的 workaround) | + +#### 内部机制(二):为什么权限不一致对线性化请求不安全 + +| 论点 | 内容 | +| --- | --- | +| 最危险的是写 | 即使运维**禁用了某用户的写权限**,若写操作**只相对 key-value store 排序**而**未相对认证系统排序**,该写**仍可能成功** | +| 攻击面 | 缺少对 **auth store** 与 **key-value store** 两者的**统一排序**,系统就容易遭受 **stale permission 攻击** | +| 解决方案 | 权限检查逻辑必须**加入 etcd 的状态机**:每个状态机在 **apply 阶段**基于自身权限信息检查请求(此时 **auth 信息必然不过期**) | + +#### 内部机制(三):认证流程 + +| 步骤 | 内容 | +| --- | --- | +| 1 | 客户端首先必须创建一条**仅用于认证**的 gRPC 连接,提交 **user ID 与 password** | +| 2 | etcd server 返回认证应答:成功返回 **authentication token**,失败返回**错误** | +| 3 | 客户端随后可用该 **token** 在 API 请求中出示凭据 | + +**为什么用于请求 token 的那条连接通常会被丢弃**(原文解释): + +> 因为它**无法承载新 token 的凭据** —— **gRPC 没有提供在创建连接(调用 `grpc.Dial()`)之后追加 per-RPC credential 的机制**,客户端无法把通过该连接拿到的 token 再赋给这条连接,因此**需要一个新连接来使用 token**。 + +#### 内部机制(四):`Authenticate()` RPC 与性能取舍 + +`Authenticate()` RPC 基于给定的 **user name 与 password** 生成认证 token。 + +| 项 | 内容 | +| --- | --- | +| 密码存储 | etcd 使用 Go 的 **`bcrypt`** 包保存与校验密码 | +| 校验代价 | `bcrypt` 的校验机制**计算代价高**,在普通 **x64** 服务器上耗时接近 **100ms** | +| 若放在状态机 apply 阶段 | 会有性能问题:**整个 etcd 集群每秒只能处理约 10 个 `Authenticate()` 请求** | +| 实际做法 | 为性能考虑,v3 auth 把**密码校验放在 etcd 的 API 层**(可在 raft 之外**并行化**) | + +**由此引入的 TOCTOU(time-of-check/time-of-use)权限漏洞**(原文 6 步场景): + +| 步 | 事件 | +| --- | --- | +| 1 | 客户端 A 发送 **`Authenticate()`** 请求 | +| 2 | API 层处理 `Authenticate()` 的**密码校验**部分 | +| 3 | 另一客户端 B 发送 **`ChangePassword()`**,服务端完成它 | +| 4 | 状态机层处理"为 A 的 `Authenticate()` **获取 revision number**"的部分 | +| 5 | 服务端向 A 返回**成功** | +| 6 | 此时 A 是用**已过时的密码**完成认证的 | + +**修复机制**:API 层执行基于 **auth store revision number** 的 **version number validation**: + +| 步骤 | 内容 | +| --- | --- | +| 1 | 密码校验期间,API 层**保存 auth store 的 revision number** | +| 2 | 校验成功后**比较保存值与最新值** | +| 3 | 若不同(说明有人更新了 auth 元数据),则**重试校验** | +| 效果 | 该机制可避免**基于过时密码的校验成功** | + +#### 内部机制(五):在 API 层解析 token + +| 环节 | 机制 | +| --- | --- | +| 客户端侧 | 通过 `Authenticate()` 认证之后,客户端可像无 auth 时一样创建 gRPC 连接;除既有初始化流程外,还必须把 token 关联到新建连接上,这由 **`grpc.WithPerRPCCredentials()`** 提供 | +| 请求携带 | 客户端的**每个**认证请求都携带 token | +| 服务端获取 | 服务端可用 **`grpc.metadata.FromIncomingContext()`** 获取 token,从而得知**请求发起者**及其**被授权的时间** | +| 写入 Raft | 这些信息由 **API 层**填入 raft 日志条目(**`etcdserverpb.InternalRaftRequest`**)的头部:**`etcdserverpb.RequestHeader.Username`** 与 **`etcdserverpb.RequestHeader.AuthRevision`** | + +#### 内部机制(六):在状态机中检查权限 + +`etcdserverpb.RequestHeader` 中的 auth 信息在**状态机的 apply 阶段**被检查:这一步基于 **auth store 的最新 revision**,检查**用户是否被授予了**对所请求 key 的权限。 + +#### 内部机制(七):两种 token —— simple 与 JWT + +| | **simple token** | **JWT token** | +| --- | --- | --- | +| 设计用途 | **不面向生产**,仅用于**开发测试** | **生产部署应使用** | +| 签名 | **未做密码学签名** | **已密码学签名并可校验** | +| 服务端状态 | **有状态**:server 必须记录 **token 与 user 的对应关系** | **无状态(stateless)** | +| 元数据 | — | token 本身可包含 **username、revision** 等元数据,服务端**无需记住** token 与元数据的对应关系 | + +> **Note(warning,原文保留)**:simple token 存在已知问题 [**#18437**](https://github.com/etcd-io/etcd/issues/18437)。etcd server **内部在 API 层解析 token**,而 **simple token 是有状态的**,该过程**不受 linearizable check 保护**:某个 etcd 成员可能**还没处理完前一个认证请求**就收到下一个请求,此时可能向客户端返回 **"invalid auth token"** 错误。该问题在**网络状况良好**的节点上**很少见**,但在**延迟显著**时会发生。**workaround:应用程序可自行实现重试机制来处理该错误。** + +#### 怎么用(一):直接设置 JWT token + +除标准 `Authenticate()` RPC 流程外,etcd 支持**在客户端层面直接设置 JWT token**,允许应用在 **etcd 之外**管理 JWT token 的完整生命周期(**生成、校验、轮换**)。 + +**适用场景(3 条)**: + +| # | 场景 | +| --- | --- | +| 1 | 由 **etcd 之外的独立 token 管理系统**负责 JWT 生成与生命周期 | +| 2 | 应用通过**外部机制**(如**环境变量、配置服务**)获得**预先签名**的 JWT token | +| 3 | token 生命周期必须**完全由客户端应用管理**,而非由 etcd 自动生成 | + +**典型流程(6 步)**: + +| 步 | 内容 | +| --- | --- | +| 1 | **外部权威机构(非 etcd)**生成包含 **username 及其他 claim** 的**签名 JWT token** | +| 2 | 应用收到该**预签名 token**,并以此配置 etcd 客户端 | +| 3 | 客户端**直接在请求中提交** JWT token(**不调用 `Authenticate()`**) | +| 4 | etcd server 使用其**配置的公钥校验 token 签名**,并依据 token 中的 **username** 授予访问权 | +| 5 | token 过期前,应用从**外部权威机构**获取新 token | +| 6 | 应用用更新后的 token **创建新的客户端**(**token 更新需要重建 client**) | + +**与标准认证的差异**: + +| | 标准 `Authenticate()` 流程 | 直接设置 JWT | +| --- | --- | --- | +| 起点 | 客户端用 **username/password** 调用 `Authenticate()` | 客户端以**预签名 JWT** 初始化 | +| token 来源 | **etcd 生成并返回** token | **不调用 `Authenticate()`** | +| 使用 | 客户端**自动**在后续请求中携带 | token **直接用于所有请求** | +| 刷新 | 需**再次调用 `Authenticate()`** | 客户端应用**自行负责**在过期前获取新 token 并**管理客户端生命周期** | + +#### 怎么用(二):`AuthStatus` 无需有效 token + +为支持自建 JWT 的应用,**`AuthStatus` RPC** 被设计为允许客户端**判断认证是否启用**并获取**当前 `authRevision`**。 + +**为什么重要(原文论证)**:这在**恢复场景**中很重要 —— token 已过期时,客户端需要**最新 revision** 才能从**外部 token provider** 获取新的有效 token。缺少该能力,**过期 token 会阻止客户端得知当前 `authRevision`**,从而陷入"**无法生成新 token**"的**死锁**。 + +#### 注意事项:KVS 模型与文件系统模型的差异 + +| 项 | etcd v3(KVS) | 类文件系统系统(Chubby、ZooKeeper) | +| --- | --- | --- | +| 权限授予形式 | 可以**精确的 key 名**或 **key 区间 `["start key", "end key")`** 的形式授予用户 | **inode 式数据结构**可承载权限信息 | +| 对不存在的 key 授权 | **是可能的**,用户需注意**非预期的权限授予** | **不可能**(**sticky bit** 情形除外) | +| 元数据查找 | 需要**多次查找元数据**;最坏情况下的查找成本是"**用户被授予的全部 key 与区间之和**" | 每个 inode 自带权限元数据 | +| 成本可否避免 | **无法避免**,因为 v3 的**扁平 key space** 与 Unix 文件模型**完全不同** | — | +| 是否构成问题 | 实践中**不构成严重问题**,因为元数据**足够小、能受益于缓存** | — | + +> 📌 官方文档未提供:`learning/design-auth-v3.md` 是**设计文档**,源文中**未出现** **user/role/permission 的具体数据模型定义**、**未出现 `READ` / `WRITE` / `READWRITE` 权限类型枚举**、**未出现 root 用户与开启 auth 的 etcdctl 操作步骤及命令**(如 `etcdctl user add`、`etcdctl role grant-permission`、`etcdctl auth enable`、`--auth-token` 等)。按抓取规则不得凭已有知识补充;操作层面的内容见 `op-guide/security.md` 等页面(本册可在 [4.10](#410-第一个-put-与-get-示例) 看到 `demo.md` 给出的 auth 操作脚本,在 [6.8](#68-auth-service) 看到 `Permission` / `Role` / `User` 的 proto 字段)。 + +> 💡 补充:把设计文档与 proto 对照可以得到一条实践规则——**权限是"区间"而非"前缀匹配"**。`RoleGrantPermission` 允许授予任意 `["start key","end key")` 区间,因此 `range_end` 填错(例如填成 `\0`)会一次性授权"所有大于等于 key 的 key",这是最容易发生的越权配置。 + +### 11.4 运行时重配置设计 + +> 引自:[Design of runtime reconfiguration](https://etcd.io/docs/v3.7/op-guide/runtime-reconf-design/) + +#### 是什么 + +运行时重配置是分布式系统(尤其是 etcd 这种**基于共识**的系统)中**最困难、最容易出错**的特性之一。本文说明 etcd 运行时重配置命令的**设计思路及其问题解法**。 + +#### 为什么:必须两阶段 + +出于**安全原因**,etcd 中**每一次运行时重配置都必须经过两个阶段**(以加成员为例):**先通知集群新配置,再启动新成员**。 + +**Phase 1 —— 告知集群新配置(Inform cluster of new configuration)** + +| 项 | 内容 | +| --- | --- | +| 做法 | 要向 etcd 集群加入成员,需通过 **API 调用**请求把新成员加入集群 | +| 唯一性 | 这是向既有集群添加新成员的**唯一途径** | +| 返回时机 | 该 API 调用在**集群就配置变更达成一致后**才返回 | + +**Phase 2 —— 启动新成员(Start new member)** + +| 项 | 内容 | +| --- | --- | +| 配置 | 需指定正确的 **`initial-cluster`** 并把 **`initial-cluster-state`** 设为 **`existing`** | +| 启动校验 | 成员启动时会先**联系既有集群**,并**校验当前集群配置与 `initial-cluster` 中指定的期望配置一致** | +| 完成 | 新成员成功启动后,集群即**达到期望配置** | + +#### 内部机制:为何拆成两个离散阶段 + +强制用户对集群成员变更保持**显式操作**,反而带来**更多灵活性**与**更清晰的可推理性**。举例(原文): + +| 例子 | 行为 | +| --- | --- | +| 添加与既有成员 **ID 相同**的成员 | 该操作会在 **phase one 立即失败**,且**不影响运行中的集群** | +| 防止误加成员 | 若新 etcd 成员在**集群接受配置变更之前**就尝试加入,它**不会被集群接受** | + +**如果没有这套显式工作流会怎样**(原文举例):etcd 就容易遭受**非预期的成员变更**。例如: + +> 在 **systemd** 这类 init 系统下运行 etcd 时,成员若通过 membership API 被移除,**systemd 会重启 etcd**,而 etcd 启动时又**会尝试重新加入集群** —— 只要通过 API 移除成员、且 systemd 配置为失败后重启,这个**循环就会反复发生**。 + +**etcd 的立场(原文)**:**运行时重配置应当是低频操作**,因此刻意保持"**显式、由用户驱动**",以确保配置安全、让集群始终在**显式控制**下平稳运行。 + +#### 注意事项(一):永久失去 quorum 需要新建集群 + +| 项 | 内容 | +| --- | --- | +| 情形 | 若集群**永久性地失去多数派成员** | +| 做法 | 需要用**旧的数据目录**启动一个**新集群**来恢复先前的状态 | +| 为何不支持"强制移除" | 技术上完全可以强制移除故障成员来恢复,但 etcd **决定不支持**这种方式,因为它**绕过了正常的共识提交阶段**,是**不安全**的 | +| 危险后果 | 若要移除的成员**实际上并未死亡**,或同一集群中**通过不同成员**做了强制移除,etcd 最终会得到一个**具有相同 clusterID 的分裂集群** —— 这**非常危险且事后极难排查修复** | +| 官方建议 | 在正确部署下,永久失去多数派的可能性很低,但问题**足够严重、值得特别对待**。**强烈建议阅读 disaster recovery 文档并在投产前为永久多数派丢失做好准备** | + +本页引用的链接:`../runtime-configuration/#add-a-new-member`、`../recovery/`。 + +#### 注意事项(二):不要用公共 discovery service 做运行时重配置 + +| 项 | 内容 | +| --- | --- | +| 正确用途 | 公共 discovery service **只应用于集群引导(bootstrapping)** | +| 加成员的正确做法 | 要把成员加入既有集群,请使用**运行时重配置 API** | +| discovery service 的设计目的 | 在**云环境**中引导 etcd 集群(此时**所有成员的 IP 地址事先未知**)。成功引导后**所有成员 IP 都已确定**,**技术上不再需要** discovery service | + +**依赖它做运行时重配置的三个麻烦(逐条照录)**: + +| # | 麻烦 | +| --- | --- | +| 1 | 它为集群的**整个生命周期**(而不只是引导期)引入**外部依赖**;集群与公共 discovery service 之间一旦出现**网络问题**,集群就会**受累** | +| 2 | 公共 discovery service 必须在集群**整个生命周期**中**反映正确的运行时配置**,为此必须提供**安全机制来防范恶意行为**,而这**很难做到** | +| 3 | 公共 discovery service 必须保存**数以万计**的集群配置;etcd 的公共 discovery service 后端**尚未为该量级负载做好准备** | + +**结论(原文)**:若确实需要支持运行时重配置的 discovery service,**最佳选择是自建私有的**。 + +#### 怎么用:关键配置名与命令汇总 + +| 类别 | 名称 | +| --- | --- | +| 配置 / flag | **`initial-cluster`**、**`initial-cluster-state=existing`** | +| 阶段动作 | **Phase 1**:通过 API 请求加入成员(**集群达成一致后返回**);**Phase 2**:以 `initial-cluster` + `initial-cluster-state=existing` 启动新成员,启动时**校验集群配置** | +| 恢复手段 | 用**旧 data directory** 启动新集群(见 `../recovery/`);**不支持**强制移除成员 | +| 相关命令页 | `../runtime-configuration/#add-a-new-member`(add a new member) | + +#### 注意事项(三):operational checklist + +```text +加成员(务必按序): + 1. etcdctl member add --peer-urls= # Phase 1:集群达成一致 + 2. 用返回的 initial-cluster 启动新成员进程, + 并设 --initial-cluster-state=existing # Phase 2:启动并校验 + 3. (可选)先加为 learner:member add --learner, + 确认追上后再 member promote # 见 11.2 + +换掉不健康节点: + 1. etcdctl member remove # 先移除 + 2. 再按上面的流程加新成员 # member add 会改变 quorum +``` + +> 💡 补充:上表最后一步"先加 learner 再 promote"是把 [11.2](#112-learner-设计) 与本节合并后的推荐做法;本节原文只强调"先 `member remove` 再替换"来自 `design-learner.md`,而两阶段模型来自本页。 + +> 📌 官方文档未提供:`op-guide/runtime-reconf-design.md` 明确描述的是**两阶段(Phase 1 / Phase 2)**模型,源文中**没有"四个阶段/状态机"的表述**,也**没有 quorum 计算公式或成员变更限制清单**(如一次只能变更一个成员、2 节点集群的限制等)。本页出现的唯一数值化 quorum 讨论实际位于 `learning/design-learner.md`(3 节点加成员后规模变 4、quorum 变 3;先分区再加成员 quorum 由 2 变 3),已在 [11.2](#112-learner-设计) 收录。此处未做任何补充。 + +--- + +## 12. etcdctl v3 交互入门 + +> 引自:[Interacting with etcd](https://etcd.io/docs/v3.7/dev-guide/interacting_v3/) + +用户主要通过 **put/get key 的 value** 与 etcd 交互。本节用 **etcdctl**(etcd 的命令行工具)说明,所述概念同样适用于 **gRPC API 与客户端库 API**。 + +### 12.1 环境变量与 API 版本 + +etcdctl 与 etcd 通信所用的 API 版本可通过环境变量 **`ETCDCTL_API`** 设为 **`2`** 或 **`3`**。 + +| 版本 | 默认值 | +| --- | --- | +| master(**3.4**)上的 etcdctl | 默认使用 **v3 API** | +| **3.3 及更早** | 默认为 **v2 API** | + +```bash +export ETCDCTL_API=3 +``` + +> **重要注意(原文)**:**用 v2 API 创建的 key 无法通过 v3 API 查询**。v3 的 `etcdctl get` 查一个 v2 key 会**退出码 0 且不返回任何 key 数据**,这是**预期行为**。 + +### 12.2 查看版本 + +`etcdctl version` 与 Server API version 有助于确定该用哪些命令: + +```bash +$ etcdctl version +etcdctl version: 3.1.0-alpha.0+git +API version: 3.1 +``` + +> 📌 版本不一致提示:本页示例中出现的版本号为 **`etcdctl version: 3.1.0-alpha.0+git`** / **`API version: 3.1`**,与文档站点 v3.7 的当前版本不一致(**源文未随版本更新**)。本册照录原文输出,不做改写。 + +### 12.3 写 key put + +应用通过写 key 把键值存入 etcd 集群。**每个存储的 key 都会通过 Raft 协议复制到所有 etcd 集群成员**,以实现一致性与可靠性。 + +```bash +$ etcdctl put foo bar +OK +``` + +也可以通过**挂载 lease** 让 key 只在指定时间段内有效。把 key `foo1` 的值设为 `bar1`,有效期 **10s**: + +```bash +$ etcdctl put foo1 bar1 --lease=1234abcd +OK +``` + +> Note(info,原文保留):上述命令中的 lease id **`1234abcd`** 指**创建 10s lease 时返回的 id**,该 id 随后可挂载到 key 上。 + +### 12.4 读 key get + +应用可从 etcd 集群读取 key 的值。查询可以读**单个 key**,也可以读**一个区间**的 key。 + +假设集群已存有以下 key: + +```bash +foo = bar +foo1 = bar1 +foo2 = bar2 +foo3 = bar3 +``` + +| 场景 | 命令 | 输出 | +| --- | --- | --- | +| 读 key `foo` 的值 | `etcdctl get foo` | `foo` / `bar` | +| 以 **hex** 格式读 | `etcdctl get foo --hex` | `\x66\x6f\x6f # Key` / `\x62\x61\x72 # Value` | +| **只读值**(不输出 key) | `etcdctl get foo --print-value-only` | `bar` | +| 范围读取 `[foo, foo3)` | `etcdctl get foo foo3` | `foo`/`bar`、`foo1`/`bar1`、`foo2`/`bar2` | +| **前缀**读取 | `etcdctl get --prefix foo` | 4 个 key 全部返回 | +| 前缀 + **限制数量 2** | `etcdctl get --prefix --limit=2 foo` | `foo`/`bar`、`foo1`/`bar1` | +| 用 **RangeStream** RPC 读 | `etcdctl get --stream --prefix foo` | 与 unary `Range` 结果相同 | + +```bash +$ etcdctl get foo +foo +bar +``` + +```bash +$ etcdctl get foo --hex +\x66\x6f\x6f # Key +\x62\x61\x72 # Value +``` + +```bash +$ etcdctl get foo --print-value-only +bar +``` + +```bash +$ etcdctl get foo foo3 +foo +bar +foo1 +bar1 +foo2 +bar2 +``` + +> Note(info,原文保留):**`foo3` 被排除**,因为区间是**半开区间 `[foo, foo3)`**,不含 `foo3`。 + +```bash +$ etcdctl get --prefix foo +foo +bar +foo1 +bar1 +foo2 +bar2 +foo3 +bar3 +``` + +```bash +$ etcdctl get --prefix --limit=2 foo +foo +bar +foo1 +bar1 +``` + +```bash +$ etcdctl get --stream --prefix foo +foo +bar +foo1 +bar1 +foo2 +bar2 +foo3 +bar3 +``` + +> 原文注解:**`--stream` 不支持 `--order`、`--sort-by` 以及 revision 过滤器。** + +### 12.5 读历史版本 + +应用可能需要读取 key **被覆盖前**的版本:例如**回滚到旧配置**,或通过访问 key 历史**在多次请求间获得对多个 key 的一致视图**。由于对 etcd 集群 key-value store 的**每次修改都会递增集群的全局 revision**,应用可通过提供**一个较旧的 etcd revision** 来读取被取代的 key。 + +假设集群已有: + +```bash +foo = bar # revision = 2 +foo1 = bar1 # revision = 3 +foo = bar_new # revision = 4 +foo1 = bar1_new # revision = 5 +``` + +```bash +$ etcdctl get --prefix foo # access the most recent versions of keys +foo +bar_new +foo1 +bar1_new + +$ etcdctl get --prefix --rev=4 foo # access the versions of keys at revision 4 +foo +bar_new +foo1 +bar1 + +$ etcdctl get --prefix --rev=3 foo # access the versions of keys at revision 3 +foo +bar +foo1 +bar1 + +$ etcdctl get --prefix --rev=2 foo # access the versions of keys at revision 2 +foo +bar + +$ etcdctl get --prefix --rev=1 foo # access the versions of keys at revision 1 +``` + +### 12.6 读大于等于指定 key 的 key + +假设集群已有: + +```bash +a = 123 +b = 456 +z = 789 +``` + +读取**字节值大于等于 key `b`** 的 key: + +```bash +$ etcdctl get --from-key b +b +456 +z +789 +``` + +### 12.7 删除 key del + +应用可删除**单个 key** 或**一个区间**的 key。假设集群已有: + +```bash +foo = bar +foo1 = bar1 +foo3 = bar3 +zoo = val +zoo1 = val1 +zoo2 = val2 +a = 123 +b = 456 +z = 789 +``` + +| 场景 | 命令 | 输出 | +| --- | --- | --- | +| 删除 key `foo` | `etcdctl del foo` | `1 # one key is deleted` | +| 删除 `[foo, foo9)` 区间 | `etcdctl del foo foo9` | `2 # two keys are deleted` | +| 删除 `zoo` 并**返回被删的键值对** | `etcdctl del --prev-kv zoo` | `1` / `zoo` / `val` | +| 删除有 `zoo` **前缀**的 key | `etcdctl del --prefix zoo` | `2 # two keys are deleted` | +| 删除**字节值 >= key `b`** 的 key | `etcdctl del --from-key b` | `2 # two keys are deleted` | + +```bash +$ etcdctl del foo +1 # one key is deleted +``` + +```bash +$ etcdctl del foo foo9 +2 # two keys are deleted +``` + +```bash +$ etcdctl del --prev-kv zoo +1 # one key is deleted +zoo # deleted key +val # the value of the deleted key +``` + +```bash +$ etcdctl del --prefix zoo +2 # two keys are deleted +``` + +```bash +$ etcdctl del --from-key b +2 # two keys are deleted +``` + +### 12.8 watch key 变化 + +应用可 watch **一个 key** 或**一个区间**的 key 以监控更新。 + +```bash +$ etcdctl watch foo +# in another terminal: etcdctl put foo bar +PUT +foo +bar +``` + +```bash +$ etcdctl watch foo --hex +# in another terminal: etcdctl put foo bar +PUT +\x66\x6f\x6f # Key +\x62\x61\x72 # Value +``` + +```bash +$ etcdctl watch foo foo9 +# in another terminal: etcdctl put foo bar +PUT +foo +bar +# in another terminal: etcdctl put foo1 bar1 +PUT +foo1 +bar1 +``` + +```bash +$ etcdctl watch --prefix foo +# in another terminal: etcdctl put foo bar +PUT +foo +bar +# in another terminal: etcdctl put fooz1 barz1 +PUT +fooz1 +barz1 +``` + +用**交互式会话**同时 watch 多个 key `foo` 与 `zoo`: + +```bash +$ etcdctl watch -i +$ watch foo +$ watch zoo +# in another terminal: etcdctl put foo bar +PUT +foo +bar +# in another terminal: etcdctl put zoo val +PUT +zoo +val +``` + +### 12.9 watch 历史变更 + +若应用**始终保持连接**,`watch` 已足够;但若应用或 etcd 发生故障,**故障期间的变更就无法实时收到**。为保证变更一定被投递,应用必须能 watch key 的**历史变更** —— 就像读历史版本一样,可在 watch 上指定**一个历史 revision**。 + +假设已完成如下操作序列: + +```bash +$ etcdctl put foo bar # revision = 2 +OK +$ etcdctl put foo1 bar1 # revision = 3 +OK +$ etcdctl put foo bar_new # revision = 4 +OK +$ etcdctl put foo1 bar1_new # revision = 5 +OK +``` + +```bash +# watch for changes on key `foo` since revision 2 +$ etcdctl watch --rev=2 foo +PUT +foo +bar +PUT +foo +bar_new +``` + +```bash +# watch for changes on key `foo` since revision 3 +$ etcdctl watch --rev=3 foo +PUT +foo +bar_new +``` + +只 watch **最近一次**历史变更(**附带上一次的值**): + +```bash +# watch for changes on key `foo` and return last revision value along with modified value +$ etcdctl watch --prev-kv foo +# in another terminal: etcdctl put foo bar_latest +PUT +foo # key +bar_new # last value of foo key before modification +foo # key +bar_latest # value of foo key after modification +``` + +### 12.10 watch progress + +应用可能需要检查 watch 的**进度**,以判断 watch 流的**更新程度**(例如用 watch 更新缓存时,需要知道缓存相对 **quorum read** 的 revision 是否已过期)。可在**交互式** watch 会话中用 **`progress`** 命令请求 etcd server 在 watch 流中发送一条 **progress notify** 更新: + +```bash +$ etcdctl watch -i +$ watch a +$ progress +progress notify: 1 +# in another terminal: etcdctl put x 0 +# in another terminal: etcdctl put y 1 +$ progress +progress notify: 3 +``` + +> Note(info,原文保留):progress notify 响应中的 revision 号来自 **watch 流所连接的那个本地 etcd 节点**。若该节点**被分区、不属于 quorum**,此 progress notify revision 可能**低于**对未分区节点做 **quorum read** 得到的 revision。 + +### 12.11 压缩 revision compact + +etcd 保留 revision 以便应用读取历史版本,但为避免历史**无界累积**,必须压缩过去的 revision。压缩后 etcd **移除历史 revision、释放资源**;**所有 revision 早于压缩点的被取代数据将不可访问**。 + +```bash +$ etcdctl compact 5 +compacted revision 5 + +# any revisions before the compacted one are not accessible +$ etcdctl get --rev=4 foo +Error: rpc error: code = 11 desc = etcdserver: mvcc: required revision has been compacted +``` + +> Note(info,原文保留):etcd server 的**当前 revision** 可用 `get` 命令以 **json 格式**查询**任意 key(存在或不存在均可)**。下例中的 `mykey` 在 etcd server 中**并不存在**: + +```bash +$ etcdctl get mykey -w=json +{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":15,"raft_term":4}} +``` + +> 💡 补充:这条"查不存在的 key 拿当前 revision"的技巧非常实用——它等价于一次极轻量的"现在集群到哪个 revision 了"的探测,可用于实现 [7.8.5](#785-resumable-可恢复) 里"先取当前 revision、再建 watch"的初始化逻辑。 + +### 12.12 lease 三连 + +#### 12.12.1 授予 lease(LEASE GRANT) + +应用可从 etcd 集群为 key 授予 lease。key 挂到 lease 上后,其**生命周期与 lease 绑定**,而 lease 由 **TTL(time-to-live)**控制。 + +| 规则 | 说明 | +| --- | --- | +| 最小 TTL | 每个 lease 在 grant 时由应用指定**最小 TTL 值** | +| 实际 TTL | lease 的实际 TTL **至少**为该最小值,具体由 **etcd 集群选择** | +| 到期 | 一旦 lease 的 TTL 到期,lease 过期,其挂载的**所有 key 都被删除** | + +```bash +# grant a lease with 60 second TTL +$ etcdctl lease grant 60 +lease 32695410dcc0ca06 granted with TTL(60s) + +# attach key foo to lease 32695410dcc0ca06 +$ etcdctl put --lease=32695410dcc0ca06 foo bar +OK +``` + +#### 12.12.2 撤销 lease(LEASE REVOKE) + +应用按 **lease ID** 撤销 lease;撤销会删除该 lease 上挂载的**所有 key**。 + +```bash +$ etcdctl lease grant 60 +lease 32695410dcc0ca06 granted with TTL(60s) +$ etcdctl put --lease=32695410dcc0ca06 foo bar +OK +``` + +```bash +$ etcdctl lease revoke 32695410dcc0ca06 +lease 32695410dcc0ca06 revoked + +$ etcdctl get foo +# empty response since foo is deleted due to lease revocation +``` + +#### 12.12.3 保持 lease 存活(LEASE KEEP-ALIVE) + +应用可通过**刷新 TTL** 使 lease 不过期。 + +```bash +$ etcdctl lease grant 60 +lease 32695410dcc0ca06 granted with TTL(60s) +``` + +```bash +$ etcdctl lease keep-alive 32695410dcc0ca06 +lease 32695410dcc0ca06 keepalived with TTL(60) +lease 32695410dcc0ca06 keepalived with TTL(60) +lease 32695410dcc0ca06 keepalived with TTL(60) +... +``` + +#### 12.12.4 查询 lease 信息(LEASE TIMETOLIVE) + +应用可能需要了解 lease 信息以便**续约**、检查 lease **是否仍存在或已过期**,也可能需要知道**某个 lease 上挂了哪些 key**。 + +```bash +# grant a lease with 500 second TTL +$ etcdctl lease grant 500 +lease 694d5765fc71500b granted with TTL(500s) + +# attach key zoo1 to lease 694d5765fc71500b +$ etcdctl put zoo1 val1 --lease=694d5765fc71500b +OK + +# attach key zoo2 to lease 694d5765fc71500b +$ etcdctl put zoo2 val2 --lease=694d5765fc71500b +OK +``` + +查询 lease 信息: + +```bash +$ etcdctl lease timetolive 694d5765fc71500b +lease 694d5765fc71500b granted with TTL(500s), remaining(258s) +``` + +查询 lease 信息及其挂载的 key: + +```bash +$ etcdctl lease timetolive --keys 694d5765fc71500b +lease 694d5765fc71500b granted with TTL(500s), remaining(132s), attached keys([zoo2 zoo1]) + +# if the lease has expired or does not exist it will give the below response: +Error: etcdserver: requested lease not found +``` + +### 12.13 flag 清单 + +本页出现的 flag / 环境变量(**原样保留**): + +| 类别 | 名称 | 值 / 用途 | +| --- | --- | --- | +| 环境变量 | **`ETCDCTL_API`** | `2` 或 `3` | +| flag | **`--lease=`** | 挂载 lease id | +| flag | **`--hex`** | 以 hex 格式输出 / watch | +| flag | **`--print-value-only`** | 只读值,不输出 key | +| flag | **`--prefix`** | 前缀匹配 | +| flag | **`--limit=`** | 限制返回数量 | +| flag | **`--stream`** | 使用 `RangeStream` RPC | +| flag | **`--rev=`** | 指定历史 revision | +| flag | **`--from-key`** | 大于等于指定 key | +| flag | **`--prev-kv`** | 返回修改/删除前的 kv | +| flag | **`-i`** | 交互式 watch | +| flag | **`--order`** | 排序(`--stream` 不支持) | +| flag | **`--sort-by`** | 排序字段(`--stream` 不支持) | +| flag | **`-w=json`** | json 格式输出 | + +命令一览(本页出现的所有子命令): + +| 子命令 | 用途 | +| --- | --- | +| `etcdctl version` | 查看 etcdctl 与 API 版本 | +| `etcdctl put` | 写 key(可带 `--lease=`) | +| `etcdctl get` | 读 key(可带 `--hex`、`--print-value-only`、`--prefix`、`--limit=`、`--stream`、`--rev=`、`--from-key`、`-w=json`) | +| `etcdctl del` | 删 key(可带 `--prev-kv`、`--prefix`、`--from-key`) | +| `etcdctl watch` | watch key(可带 `--hex`、`--prefix`、`--rev=`、`--prev-kv`、`-i`) | +| `etcdctl compact` | 压缩 revision | +| `etcdctl lease grant` | 授予 lease | +| `etcdctl lease revoke` | 撤销 lease | +| `etcdctl lease keep-alive` | 续约 | +| `etcdctl lease timetolive` | 查询 lease 信息(可带 `--keys`) | + +### 12.14 官方文档未提供的命令 + +> 📌 官方文档未提供:`dev-guide/interacting_v3.md` **未包含 `TXN`(事务)命令**的示例与输出,源文到此为止;按规则未做补充。 + +> 💡 补充:需要事务示例时,可参考 proto 层的 `TxnRequest` 语义(见 [6.3.6](#636-transactiontxn))与 Go 客户端的 `cli.Txn(ctx).If(...).Then(...).Else(...).Commit()` 写法(见 [5.9.2](#592-用-txn-做-compare-and-swap进阶分布式互斥的基础))。etcdctl 侧的交互式 `txn` 不在本页覆盖范围内。 + +--- + +## 13. 附录 + +### 13.1 官方页面与章节映射表 + +本册覆盖的官方页面共 **20 个**(含 2 个作为方法清单补充来源的 API 参考页)。逐条标注覆盖状态: + +| # | 官方页面标题 | 线上 URL | 源文件 | 本册章节 | 状态 | +| --- | --- | --- | --- | --- | --- | +| 1 | v3.7 docs | https://etcd.io/docs/v3.7/ | `_index.md` | [1. 本册导读](#1-本册导读)、[2. 概述](#2-概述) | 已覆盖 | +| 2 | Install | https://etcd.io/docs/v3.7/install/ | `install.md` | [4. 快速上手](#4-快速上手)(4.1–4.6) | 已覆盖 | +| 3 | Quickstart | https://etcd.io/docs/v3.7/quickstart/ | `quickstart.md` | [4.7 五分钟启动单节点](#47-五分钟启动单节点) | 已覆盖 | +| 4 | Demo | https://etcd.io/docs/v3.7/demo/ | `demo.md` | [4.10 第一个 put 与 get 示例](#410-第一个-put-与-get-示例) | 已覆盖 | +| 5 | Set up a local cluster | https://etcd.io/docs/v3.7/dev-guide/local_cluster/ | `dev-guide/local_cluster.md` | [4.8](#48-启动三节点本地集群)、[4.9](#49-测试容错) | 已覆盖 | +| 6 | etcd versus other key-value stores | https://etcd.io/docs/v3.7/learning/why/ | `learning/why.md` | [2. 概述](#2-概述)(2.1–2.12 全量) | 已覆盖 | +| 7 | Glossary | https://etcd.io/docs/v3.7/learning/glossary/ | `learning/glossary.md` | [3. 术语表](#3-术语表)(24 条全量) | 已覆盖 | +| 8 | Data model | https://etcd.io/docs/v3.7/learning/data_model/ | `learning/data_model.md` | [5. 数据模型](#5-数据模型) | 已覆盖 | +| 9 | etcd API | https://etcd.io/docs/v3.7/learning/api/ | `learning/api.md` | [6. etcd API](#6-etcd-api)(6.1–6.8) | 已覆盖 | +| 10 | API reference | https://etcd.io/docs/v3.7/dev-guide/api_reference_v3/ | `dev-guide/api_reference_v3.md` | [6. etcd API](#6-etcd-api)(方法清单与字段表补充来源) | 已覆盖 | +| 11 | API reference: concurrency | https://etcd.io/docs/v3.7/dev-guide/api_concurrency_reference_v3/ | `dev-guide/api_concurrency_reference_v3.md` | [6.9 Lock service](#69-lock-service)、[6.10 Election service](#610-election-service) | 已覆盖 | +| 12 | etcd API guarantees | https://etcd.io/docs/v3.7/learning/api_guarantees/ | `learning/api_guarantees.md` | [7. API 保证](#7-api-保证)(全量逐条) | 已覆盖 | +| 13 | etcd persistent storage files | https://etcd.io/docs/v3.7/learning/persistent-storage-files/ | `learning/persistent-storage-files.md` | [8. 持久化存储文件](#8-持久化存储文件) | 已覆盖 | +| 14 | System limits | https://etcd.io/docs/v3.7/dev-guide/limit/ | `dev-guide/limit.md` | [9. 系统限制](#9-系统限制) | 已覆盖 | +| 15 | etcd features | https://etcd.io/docs/v3.7/dev-guide/features/ | `dev-guide/features.md` | [10. etcd 特性清单](#10-etcd-特性清单) | 已覆盖 | +| 16 | etcd client design | https://etcd.io/docs/v3.7/learning/design-client/ | `learning/design-client.md` | [11.1 客户端设计](#111-客户端设计) | 已覆盖 | +| 17 | etcd learner design | https://etcd.io/docs/v3.7/learning/design-learner/ | `learning/design-learner.md` | [11.2 Learner 设计](#112-learner-设计) | 已覆盖 | +| 18 | etcd v3 authentication design | https://etcd.io/docs/v3.7/learning/design-auth-v3/ | `learning/design-auth-v3.md` | [11.3 认证设计 v3](#113-认证设计-v3) | 已覆盖 | +| 19 | Design of runtime reconfiguration | https://etcd.io/docs/v3.7/op-guide/runtime-reconf-design/ | `op-guide/runtime-reconf-design.md` | [11.4 运行时重配置设计](#114-运行时重配置设计) | 已覆盖 | +| 20 | Interacting with etcd | https://etcd.io/docs/v3.7/dev-guide/interacting_v3/ | `dev-guide/interacting_v3.md` | [12. etcdctl v3 交互入门](#12-etcdctl-v3-交互入门) | 已覆盖 | + +### 13.2 本册未覆盖的页面 + +全套 v3.7 文档共 **79 页**纳入,本册覆盖其中 **20 页**。其余页面按主题划入兄弟分册(下表为主题级归类,供导航用): + +| 主题分组 | 代表页面(源文件) | 归属分册 | +| --- | --- | --- | +| 集群搭建、配置、安全、维护、监控、恢复、性能、故障 | `op-guide/clustering.md`、`op-guide/configuration.md`、`op-guide/security.md`、`op-guide/maintenance.md`、`op-guide/monitoring.md`、`op-guide/recovery.md`、`op-guide/performance.md`、`op-guide/failures.md`、`op-guide/hardware.md`、`op-guide/supported-platform.md`、`op-guide/versioning.md`、`op-guide/container.md`、`op-guide/kubernetes.md`、`op-guide/gateway.md`、`op-guide/grpc_proxy.md`、`op-guide/data_corruption.md`、`op-guide/runtime-configuration.md`、`op-guide/authentication/*` | 《03-运维运营篇》 | +| 调优、基准测试、指标、升级、降级、feature gates | `tuning.md`、`benchmarks/*`、`metrics/*`、`upgrades/*`、`downgrades/*`、`feature-gates/index.md` | 《03-运维运营篇》 | +| 开发实战:任务指南、API 网关、grpc naming、嵌入式、集成 | `tasks/developer/*`、`tasks/operator/*`、`dev-guide/api_grpc_gateway.md`、`dev-guide/grpc_naming.md`、`dev-guide/golang_embed_pkg.md`、`dev-guide/discovery_protocol.md`、`integrations.md` | 《02-开发实战篇》 | +| 其他 | `faq.md`、`reporting_bugs.md`、`dev-internal/*`、`triage/*` | 视主题分入两册 | + +### 13.3 本册发现的三处源文疑点 + +| # | 页面 | 疑点 | 本册处理 | +| --- | --- | --- | --- | +| 1 | `learning/glossary.md` | 素材批注写"术语条目共 **23** 条",但源文实际列出 **24** 个术语名 | 以**源文实际条目数 24** 为准,全量收录(见 [3.1](#31-全量术语表)) | +| 2 | `dev-guide/interacting_v3.md` | 示例版本号为 `etcdctl version: 3.1.0-alpha.0+git` / `API version: 3.1`,与 v3.7 站点版本不一致(源文未随版本更新) | **照录原文输出**,并在 [12.2](#122-查看版本) 显式标注不一致 | +| 3 | `learning/design-learner.md` | learner 数量上限出现两处表述:正文"限制 learner **总数**"、附录"限制为**一个**" | 以**附录为准**(见 [11.2](#112-learner-设计) 注意事项第 1 条) | + +--- + +> 本册完。兄弟分册:《02-开发实战篇》《03-运维运营篇》。 + diff --git a/数据库/ETCD/etcd-v3.7-学习手册-02-开发实战篇.md b/数据库/ETCD/etcd-v3.7-学习手册-02-开发实战篇.md new file mode 100644 index 0000000..83b1f6e --- /dev/null +++ b/数据库/ETCD/etcd-v3.7-学习手册-02-开发实战篇.md @@ -0,0 +1,2474 @@ +# etcd v3.7 学习手册 · 02 开发实战篇 + +> 基于 https://etcd.io/docs/v3.7/ 整理 +> 文档版本:v3.7.0 +> 整理日期:2026-09-18 +> 本册定位:开发实战篇(写代码用 etcd)。兄弟分册:《01-核心概念篇》《03-运维运营篇》。 +> 说明:etcd v3.7 官方 tasks/developer 页面只提供 etcdctl 示例,**未包含 Go clientv3 代码示例**。凡本文出现的 Go 代码,均为基于官方文档 API 语义的工程化示例,必须以 `> 💡 补充:` 显式标注,不得伪装成官方文档原文。 + +--- + +## 目录 + +- [1. 本册导读](#1-本册导读) +- [2. 开发前置 数据模型速览与连接方式](#2-开发前置-数据模型速览与连接方式) +- [3. 写入数据 writing-to-etcd](#3-写入数据-writing-to-etcd) +- [4. 读取数据 reading-from-etcd](#4-读取数据-reading-from-etcd) +- [5. 前缀查询 how-to-get-key-by-prefix](#5-前缀查询-how-to-get-key-by-prefix) +- [6. 删除数据 how-to-delete-keys](#6-删除数据-how-to-delete-keys) +- [7. 事务写 how-to-transactional-write](#7-事务写-how-to-transactional-write) +- [8. Watch 机制 how-to-watch-keys](#8-watch-机制-how-to-watch-keys) +- [9. Lease 租约 how-to-create-lease](#9-lease-租约-how-to-create-lease) +- [10. 分布式锁与选主 how-to-create-locks](#10-分布式锁与选主-how-to-create-locks) +- [11. gRPC 直连与命名发现 grpc_naming](#11-grpc-直连与命名发现-grpc_naming) +- [12. gRPC-gateway api_grpc_gateway](#12-grpc-gateway-api_grpc_gateway) +- [13. 通过代理接入 etcd gateway 与 gRPC proxy](#13-通过代理接入-etcd-gateway-与-grpc-proxy) +- [14. 在 Go 应用里嵌入 etcd golang_embed_pkg](#14-在-go-应用里嵌入-etcd-golang_embed_pkg) +- [15. Go 模块划分 dev-internal-modules](#15-go-模块划分-dev-internal-modules) +- [16. gRPC API 速查表](#16-grpc-api-速查表) +- [17. 附录 官方页面与章节映射表](#17-附录-官方页面与章节映射表) + +--- + +## 1. 本册导读 + +本册回答一个问题:**在应用代码里怎么用 etcd**。所有内容来自 etcd v3.7 官方文档的 developer 任务页、dev-guide 页与 API 参考页,命令、flag 名、默认值、字段名一律保留英文原文。 + +### 1.1 覆盖的官方页面 + +| # | 页面标题 | 线上 URL | 本册章节 | +| - | -------- | -------- | -------- | +| 1 | Developer Tasks(目录页,仅 front matter) | https://etcd.io/docs/v3.7/tasks/developer/ | 1 | +| 2 | Writing to etcd | https://etcd.io/docs/v3.7/tasks/developer/writing-to-etcd/ | 3 | +| 3 | Reading from etcd | https://etcd.io/docs/v3.7/tasks/developer/reading-from-etcd/ | 4 | +| 4 | How to get keys by prefix | https://etcd.io/docs/v3.7/tasks/developer/how-to-get-key-by-prefix/ | 5 | +| 5 | How to delete keys | https://etcd.io/docs/v3.7/tasks/developer/how-to-delete-keys/ | 6 | +| 6 | How to make multiple writes in a transaction | https://etcd.io/docs/v3.7/tasks/developer/how-to-transactional-write/ | 7 | +| 7 | How to watch keys | https://etcd.io/docs/v3.7/tasks/developer/how-to-watch-keys/ | 8 | +| 8 | How to create lease | https://etcd.io/docs/v3.7/tasks/developer/how-to-create-lease/ | 9 | +| 9 | How to create locks | https://etcd.io/docs/v3.7/tasks/developer/how-to-create-locks/ | 10 | +| 10 | How to conduct leader election in etcd cluster | https://etcd.io/docs/v3.7/tasks/operator/how-to-conduct-elections/ | 10 | +| 11 | gRPC naming and discovery | https://etcd.io/docs/v3.7/dev-guide/grpc_naming/ | 11 | +| 12 | Why gRPC gateway | https://etcd.io/docs/v3.7/dev-guide/api_grpc_gateway/ | 12 | +| 13 | etcd gateway | https://etcd.io/docs/v3.7/op-guide/gateway/ | 13 | +| 14 | gRPC proxy | https://etcd.io/docs/v3.7/op-guide/grpc_proxy/ | 13 | +| 15 | Embedding etcd in a Go Application | https://etcd.io/docs/v3.7/dev-guide/golang_embed_pkg/ | 14 | +| 16 | Golang modules | https://etcd.io/docs/v3.7/dev-internal/modules/ | 15 | +| 17 | API reference | https://etcd.io/docs/v3.7/dev-guide/api_reference_v3/ | 16 | +| 18 | API reference: concurrency | https://etcd.io/docs/v3.7/dev-guide/api_concurrency_reference_v3/ | 10、16 | +| 19 | Interacting with etcd | https://etcd.io/docs/v3.7/dev-guide/interacting_v3/ | 3、4、6、8、9 | +| 20 | Data model / etcd API / etcd API guarantees | https://etcd.io/docs/v3.7/learning/data_model/ 、https://etcd.io/docs/v3.7/learning/api/ 、https://etcd.io/docs/v3.7/learning/api_guarantees/ | 2、16(详见《01-核心概念篇》) | +| 21 | etcd client design | https://etcd.io/docs/v3.7/learning/design-client/ | 2 | + +> 引自:[Developer Tasks](https://etcd.io/docs/v3.7/tasks/developer/)(该页 description 为 `Step-by-step guides for developers using etcd as a key-value store in their applications.`,8 个子页按 `weight` 排列依次为:Reading from etcd(200)、Writing to etcd(250)、How to get keys by prefix(300)、How to delete keys(400)、How to make multiple writes in a transaction(500)、How to watch keys(600)、How to create lease(700)、How to create locks(800)) + +### 1.2 读者对象与前置依赖 + +| 项 | 说明 | +| -- | ---- | +| 读者对象 | 需要把 etcd 当作键值存储写进应用的开发工程师;需要选型 gRPC / HTTP/JSON / 代理接入方式的架构师 | +| 前置依赖(官方页面写明) | 安装 `etcd` 与 `etcdctl`;一个运行中的 etcd 集群;读写练习还需搭建本地集群 | +| 语言前提 | 本册 Go 示例使用 `go.etcd.io/etcd/client/v3`(module 划分见第 15 章);其余语言可走第 12 章的 gRPC-gateway | +| 不需要先看 | 《01-核心概念篇》里的数据模型细节,本册第 2 章给出速览版本 | + +### 1.3 与另外两册的分工 + +| 分册 | 关注点 | 与本册边界 | +| ---- | ------ | ---------- | +| 《01-核心概念篇》 | 数据模型物理/逻辑视图、Raft、API guarantees、持久化文件格式 | 本册只做 revision / lease / watch 三件套的"够用版"速览,实现原理一律指向该册 | +| 《02-开发实战篇》(本册) | 读写删、事务、watch、lease、锁选主、gRPC / gateway / proxy / embed / modules 接入方式 | 只讲"怎么写代码、怎么配命令" | +| 《03-运维运营篇》 | 部署、容器 / Kubernetes、配置 flag、集群维护、备份恢复、监控 | 本册第 13 章的 gateway / gRPC proxy 只讲"客户端接入视角",部署与运维细节归该册 | + +### 1.4 关于本册 Go 代码的重要声明 + +etcd v3.7 官方 `tasks/developer/` 下的 9 个页面**全部只给 etcdctl 示例,没有任何 Go clientv3 代码**。本册因此需要大量自行编写 Go 示例,规则如下: + +1. 凡本册出现的 Go 代码块,其上方必有一行 `> 💡 补充:` 说明其来源; +2. `> 💡 补充:` 后面的内容会注明"基于官方 API 语义编写"还是"引自官方页面原文"; +3. 唯一含 Go 代码的官方页面是 `dev-guide/grpc_naming.md`、`op-guide/grpc_proxy.md`、`upgrades/*`(本册第 11、13 章引用),这些也会显式标注出处,不混入自编示例; +4. 凡官方源文没有的东西(枚举全部取值、gRPC code 对照表、`clientv3.Config` 字段清单、`embed.Config` 字段清单),本册要么不写,要么在标注 `💡 补充` 后给出工程化版本,并提示以 `pkg.go.dev` / `etcdctl --help` 为准。 + +### 1.5 怎么读本册(三条阅读路径) + +**路径 A:先把功能跑通(推荐第一次接触 etcd 的开发者)。** 按第 2 章配好 `ETCDCTL_API=3` 与 endpoint,然后顺着第 3 → 4 → 5 → 6 章把增删改查跑一遍,理解 `header.revision` 每次写入都在变。此时先不要碰事务与 watch。 + +**路径 B:要写并发安全的代码。** 第 2 章的 revision / version 区别必须先看懂,然后直奔第 7 章(事务与 CAS)。etcd 的并发控制完全建立在"比较 + 原子提交"上,理解 `mod_revision` 与 `version` 两个比较目标之后,第 10 章的锁与选主只是这套机制的上层封装。 + +**路径 C:要决定接入方式。** 应用是 Go 技术栈,直接用第 2 章的 `clientv3`;没有 gRPC 栈的语言看第 12 章的 gRPC-gateway;客户端数量多、watch 量大或需要 keyspace 隔离时看第 13 章的 gRPC proxy;需要服务注册发现看第 11 章。第 14、15 章是"要不要把 etcd 编进自己进程"与"该依赖哪个 module"的决策依据。 + +无论哪条路径,第 16 章都当作工具页翻,不必通读;第 17 章给出每个官方页面与本册章节的对应关系,方便回查原文。 + +--- + +## 2. 开发前置 数据模型速览与连接方式 + +> 引自:[Data model](https://etcd.io/docs/v3.7/learning/data_model/)、[etcd API](https://etcd.io/docs/v3.7/learning/api/)、[Interacting with etcd](https://etcd.io/docs/v3.7/dev-guide/interacting_v3/)、[etcd client design](https://etcd.io/docs/v3.7/learning/design-client/) + +### 2.1 三件套如何配合工作 + +三件套不是三个孤立功能,而是同一套"全局逻辑时钟 + 存活凭证 + 事件流"的组合: + +- **先写后读,用 revision 对齐**:一次 `put` 让全局 revision +1,响应 `header.revision` 就是这次写入的全局序号;后续按这个序号读历史版本、或从它继续 watch,就不会漏事件。 +- **lease 给数据装上寿命**:把 key 绑到 lease 上,进程活着就一直续期,进程挂了 TTL 一到 key 自动消失,并产生 delete 事件——服务注册、会话、锁、选主都建立在这个"不会有人来清理的自动清理"上。 +- **watch 把变更变成流**:watch 不是轮询,而是服务端按 revision 顺序把事件推给客户端;断线后用最后观测到的 revision 续传即可,这也是它能扛住长时间断连的原因。 + +> 工程建议(非官方原文):设计 key 时就把这三件事想清楚——哪些 key 需要绑 lease(有生命周期的)、哪些 key 会被 watch(变更要广播的)、哪些 key 要做 CAS(会并发写的)。这三类 key 最好不要混在同一个前缀下,否则 watch 风暴与 CAS 冲突会互相放大。 + +### 2.2 三件套:revision / lease / watch + +| 概念 | 一句话含义 | 开发时怎么用 | 深读 | +| ---- | ---------- | ------------ | ---- | +| revision | key 空间维护的**全局逻辑时钟**。存储创建时初始 revision 为 **1**;每个**原子性变更操作**(一个事务可能含多个操作,但只产生一个 revision)都会创建新 revision | 用 `--rev` 读历史版本;用 watch 的 `start_revision` 断点续传;用 `header.revision` 判断写入后的全局版本 | 《01-核心概念篇》 | +| lease | 带 TTL 的存活凭证。集群授予 lease 一个 TTL,若 TTL 周期内未收到 keepAlive 即过期;每个 key **最多附加一个 lease**,lease 过期或撤销时其上所有 key 被删除,并各生成一个 delete 事件 | 服务注册、会话保活、锁与选主的底层机制 | 本册第 9、10 章 | +| watch | 基于事件的异步监控接口,从给定 revision(当前或历史)持续等待变化并流式返回 | 配置下发、缓存失效、leader 变更感知 | 本册第 8 章 | + +配套两个容易混的概念(详见《01-核心概念篇》): + +- **generation**:一个 key 从创建到删除的生命周期; +- **version**:key 的版本,创建时从 **1** 开始,每次修改递增,删除时重置为 **0** 并终结当前 generation。 + +> 提示:revision 是**集群级**计数器,version 是**单 key 级**计数器。做 CAS 时,通常用 `mod_revision`(最后一次修改的 revision)或 `version`,不要用 `create_revision` 判断"是否被改过"。 + +### 2.3 连接方式一:etcdctl 的 API 版本与 endpoint + +```bash +export ETCDCTL_API=3 +``` + +> 引自:[Interacting with etcd](https://etcd.io/docs/v3.7/dev-guide/interacting_v3/)(原文:`The API version used by etcdctl to speak to etcd may be set to version 2 or 3 via the ETCDCTL_API environment variable.`) + +注意:**用 v2 API 创建的 key 无法通过 v3 API 查询**;对 v2 key 执行 v3 的 `etcdctl get` 会**返回 0 且没有 key 数据**,这是预期行为。 + +endpoint 设置三条等价路径: + +| 方式 | 写法 | 出处 | +| ---- | ---- | ---- | +| 命令行 flag | `etcdctl --endpoints=$ENDPOINTS get foo` | 全部 developer 任务页 | +| 环境变量 | `ETCDCTL_ENDPOINTS`(容器内让 etcdctl 默认连本节点,见 Kubernetes 示例) | op-guide/kubernetes 的环境变量段 | +| 默认值 | `--endpoints=[127.0.0.1:2379]` / `--endpoints="127.0.0.1:2379"` | how-to-get-key-by-prefix / how-to-delete-keys / how-to-watch-keys | + +查看版本(用于确认命令与 API 版本是否匹配): + +```bash +$ etcdctl version +etcdctl version: 3.1.0-alpha.0+git +API version: 3.1 +``` + +### 2.4 连接方式二:Go clientv3 + +官方推荐的新用法一律使用 `go.etcd.io/etcd/client/v3`(module 说明见第 15 章)。 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写(官方 `tasks/developer/` 页面无 Go 代码)。 + +```go +cli, err := clientv3.New(clientv3.Config{ + Endpoints: []string{"http://127.0.0.1:2379"}, + DialTimeout: 5 * time.Second, +}) +if err != nil { + log.Fatal(err) +} +defer cli.Close() + +ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) +defer cancel() + +if err := cli.Sync(ctx); err != nil { // 从集群刷新 endpoint 列表 + log.Fatal(err) +} +``` + +`clientv3` 还支持从 URL 直接建客户端(官方 grpc_naming 页面用法见第 11 章): + +> 💡 补充:下面这一行 `clientv3.NewFromURL` 的用法引自官方页面 [gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/)(非本手册杜撰)。 + +```go +cli, err := clientv3.NewFromURL("http://localhost:2379") +``` + +> 💡 补充:上面两行中的 `clientv3.NewFromURL` 用法引自官方页面 [gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/),`clientv3.New` 用法引自 [gRPC proxy](https://etcd.io/docs/v3.7/op-guide/grpc_proxy/);第一行中的 `Endpoints`/`DialTimeout` 字段取值与包裹的示例结构为本手册补充。 + +#### clientv3.Config 常用字段表 + +> 💡 补充:官方文档**未逐项列出** `clientv3.Config` 的字段名与默认值(client design 页面仅记录了 dial timeout 默认 5 秒)。下表为工程化常用字段速查,最终以 `pkg.go.dev/go.etcd.io/etcd/client/v3` 为准。 + +| 字段 | 类型 | 默认值 | 说明 | +| ---- | ---- | ------ | ---- | +| `Endpoints` | `[]string` | 无(必填);etcdctl 侧默认 `127.0.0.1:2379` | 客户端可连的 etcd server endpoint 列表,典型 3 个或 5 个 client URL | +| `DialTimeout` | `time.Duration` | 官方 client design 页记录的 dial timeout 默认 **5 秒** | 建连超时;该值同时被 grpc1.7 balancer 用作 unhealthy endpoint 的等待时长 | +| `AutoSyncInterval` | `time.Duration` | `0`(关闭自动同步) | 非 0 时周期性自动 `Sync` 集群 endpoint 列表 | +| `DialKeepAliveTime` | `time.Duration` | 官方页面未给默认值 | 客户端发起 HTTP/2 keepalive ping 的间隔(流式 RPC 用它探测不可用节点) | +| `DialKeepAliveTimeout` | `time.Duration` | 官方页面未给默认值 | keepalive ping 的应答超时 | +| `MaxCallSendMsgSize` | `int` | 官方页面未给默认值 | 单条 gRPC 消息发送上限,需与服务端 `--max-request-bytes`(默认 **1.5 MiB**)对齐 | +| `MaxCallRecvMsgSize` | `int` | 官方页面未给默认值 | 单条 gRPC 消息接收上限 | +| `Username` / `Password` | `string` | 空(未开启认证) | 对应 `Auth` service 的 `Authenticate` | +| `TLS` | `*tls.Config` | `nil`(明文) | 启用客户端到服务器的 TLS | +| `Context` | `context.Context` | `context.Background()` | 控制 client 生命周期 | +| `Logger` | `*zap.Logger` | `nil`(使用默认 logger) | 客户端日志 | + +#### 客户端的故障切换行为(写业务代码前必须知道) + +> 引自:[etcd client design](https://etcd.io/docs/v3.7/learning/design-client/) + +| 项 | 官方记录 | +| -- | -------- | +| 客户端三组件 | balancer(建立 gRPC 连接)、API client(发 RPC)、error handler(判断重试还是切换 endpoint) | +| 默认负载均衡策略 | **round robin** | +| 连接模型 | `clientv3-grpc1.23` 为**每个 endpoint 建一个 sub-connection**(5 节点集群 = 5 条 TCP 连接);`clientv3-grpc1.7` 只维持一条到 pinned endpoint 的连接 | +| unhealthy endpoint 等待时长 | 硬编码为 dial timeout,**默认 5 秒** | +| 流式 RPC 健康探测 | 周期性 HTTP/2 ping(keepalive);该机制**不理解网络分区**,被分区的 gRPC server 仍能回应 ping,流式请求可能卡在分区节点(etcd#8673) | +| 该重试的错误 | `rpc error: code = Unavailable desc = etcdserver: request timed out`(瞬时错误,应重试) | +| 不该重试的错误 | `rpc error: code = InvalidArgument desc = etcdserver: key is not provided`(请求本身非法) | + +Go 侧用 `google.golang.org/grpc/status.FromError` 解析错误码,Java 侧用 `io.grpc.Status.fromThrowable`。 + +--- + +## 3. 写入数据 writing-to-etcd + +> 引自:[Writing to etcd](https://etcd.io/docs/v3.7/tasks/developer/writing-to-etcd/) + +**前提**:安装 `etcdctl`。 + +**做法**:用 `put` 子命令写入一个键值对。 + +```bash +etcdctl --endpoints=$ENDPOINTS put foo "Hello World!" +``` + +- `foo` 是 key 名;`"Hello World!"` 是用引号包裹的 value。 +- 写入成功后 etcd 会**递增全局 revision**。 + +> 说明:官方该页**未给出** `lease`、`prev-kv` 等额外 flag 的说明,本册按下表补齐(均对应 `PutRequest` 的 proto 字段,见第 16 章)。 + +### 3.1 put 相关 flag + +| flag | 默认 | 含义 | 来源 | +| ---- | ---- | ---- | ---- | +| `--endpoints` | `"127.0.0.1:2379"`(继承自父命令) | gRPC endpoints | how-to-delete-keys / how-to-watch-keys 的"继承自父命令的选项" | +| `--lease` | 未设置(0 = 无 lease) | 把 key 绑定到指定 lease ID;lease 到期时该 key 自动删除 | dev-guide/interacting_v3 的 `put foo1 bar1 --lease=1234abcd`;how-to-create-lease | +| `--prev-kv` | `false` | 修改前先取回上一个 kv 对,随 put 响应返回 | `PutRequest.prev_kv`(api.md) | +| `--ignore-value` | `false` | 用当前 value 更新 key;**key 不存在则报错** | `PutRequest.ignore_value`(api.md) | +| `--ignore-lease` | `false` | 用当前 lease 更新 key;**key 不存在则报错** | `PutRequest.ignore_lease`(api.md) | + +### 3.2 PutRequest / PutResponse 字段 + +| 消息 | 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ---- | ------ | +| PutRequest | `key` | bytes | 要写入的 key | — | +| PutRequest | `value` | bytes | 关联的 value | — | +| PutRequest | `lease` | int64 | 关联到 key 的 lease ID | `0` = 无 lease | +| PutRequest | `prev_kv` | bool | 修改前先取回上一个 kv 对,随 put 响应返回 | `false` | +| PutRequest | `ignore_value` | bool | 用当前 value 更新 key;key 不存在则报错 | `false` | +| PutRequest | `ignore_lease` | bool | 用当前 lease 更新 key;key 不存在则报错 | `false` | +| PutResponse | `header` | ResponseHeader | 见 3.3 | — | +| PutResponse | `prev_kv` | mvccpb.KeyValue | 被本次 put 覆盖的 kv 对(仅 `prev_kv=true` 时) | — | + +### 3.3 返回字段(含 header revision) + +`ResponseHeader` 是每个响应都带的公共头: + +| 字段 | 类型 | 说明 | +| ---- | ---- | ---- | +| `cluster_id` | uint64 | 生成该响应的集群 ID(应用可据此确认正在与预期集群通信) | +| `member_id` | uint64 | 生成该响应的成员 ID | +| `revision` | int64 | **请求被应用时的 kv store revision**;未与 kv store 交互的调用中为 `0` | +| `raft_term` | uint64 | 生成响应时成员的 Raft term(可用于检测是否完成了新一轮 leader 选举) | + +etcdctl 的默认输出只有一行 `OK`;加 `--write-out="json"` 可看到 `header` 全貌(下例取自 gRPC-gateway 的等价响应,字段名与 gRPC 一致): + +```json +{"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"2","raft_term":"3"}} +``` + +> 引自:[Why gRPC gateway](https://etcd.io/docs/v3.7/dev-guide/api_grpc_gateway/) + +### 3.4 Go 写入示例 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。 + +```go +resp, err := cli.Put(ctx, "foo", "Hello World!", clientv3.WithPrevKV()) +if err != nil { + log.Fatal(err) +} +// header.revision:本次写入被应用后的全局 revision +fmt.Printf("revision=%d cluster=%d member=%d\n", + resp.Header.Revision, resp.Header.ClusterId, resp.Header.MemberId) + +if resp.PrevKv != nil { // 仅 WithPrevKV() 时非空 + fmt.Printf("old value=%q version=%d mod_revision=%d\n", + resp.PrevKv.Value, resp.PrevKv.Version, resp.PrevKv.ModRevision) +} +``` + +带 lease 写入(lease 用第 9 章的方式先创建): + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。 + +```go +leaseResp, err := cli.Grant(ctx, 60) // TTL 60 秒 +if err != nil { + log.Fatal(err) +} +if _, err := cli.Put(ctx, "sample", "value", clientv3.WithLease(leaseResp.ID)); err != nil { + log.Fatal(err) +} +``` + +--- + +## 4. 读取数据 reading-from-etcd + +> 引自:[Reading from etcd](https://etcd.io/docs/v3.7/tasks/developer/reading-from-etcd/)、[Interacting with etcd](https://etcd.io/docs/v3.7/dev-guide/interacting_v3/) + +**前提**:安装 `etcdctl`。用 `get` 子命令读取。 + +```bash +$ etcdctl --endpoints=$ENDPOINTS get foo +foo +Hello World! +$ +``` + +### 4.1 全部读 flag 逐一说明 + +| flag | 默认值 | 含义 | 出处 | +| ---- | ------ | ---- | ---- | +| `--prefix` | `false` | 取出所有带该前缀的 key(范围查询) | how-to-get-key-by-prefix、interacting_v3 | +| `--limit` | `0`(无限制) | 返回 key 数量上限 | interacting_v3 `get --prefix --limit=2 foo`;`RangeRequest.limit` | +| `--rev` | `0`(最新) | 读取该 revision 时间点的版本;已被压缩则返回 `ErrCompacted` | interacting_v3 `get --prefix --rev=4 foo`;`RangeRequest.revision` | +| `--from-key` | `false` | 按字节比较,读取**大于等于**给定 key 的全部 key | interacting_v3 `get --from-key b` | +| `--sort-by` | 未设置(`SortOrder=NONE`) | 排序目标:`CREATE` / `KEY` / `MOD` / `VALUE` / `VERSION` | 对应 `RangeRequest.sort_target`;官方页面仅出现 `--sort-by` 名称(见 4.6 说明) | +| `--order` | 未设置(`NONE`) | 排序方向:`ASCEND` / `DESCEND` | 对应 `RangeRequest.sort_order`;interacting_v3 提到 `--stream` 不支持 `--order` | +| `--keys-only` | `false` | 只返回 key 不返回 value | `RangeRequest.keys_only` | +| `--count-only` | `false` | 只返回区间内 key 的数量 | `RangeRequest.count_only` | +| `--consistency` | `l`(linearizable) | `l` / `linearizable` 走 Raft 共识;`s` / `serializable` 用成员本地读 | 对应 `RangeRequest.serializable`(api.md / api_guarantees.md) | +| `--hex` | `false` | 以十六进制输出 key/value | interacting_v3 `get foo --hex` | +| `--print-value-only` | `false` | 只打印 value | interacting_v3 `get foo --print-value-only` | +| `--stream` | `false` | 改用 `RangeStream` RPC 分片流式返回 | interacting_v3 `get --stream --prefix foo` | +| `--write-out` / `-w` | 简单文本 | 输出格式,源文中出现 `json`(`--write-out="json"`、`-w=json`)与 `table`(`-wtable`) | reading-from-etcd、interacting_v3、op-guide/kubernetes | +| `--endpoints` | `"127.0.0.1:2379"` | gRPC endpoints | how-to-get-key-by-prefix | + +> 说明:上表中带具体示例的 flag(`--prefix`、`--limit`、`--rev`、`--from-key`、`--hex`、`--print-value-only`、`--stream`、`--write-out`)在 v3.7 官方页面有原文出处;`--sort-by`、`--order`、`--keys-only`、`--count-only`、`--consistency` 官方页面**未逐项列出**,其语义与 `RangeRequest` 的 `sort_target` / `sort_order` / `keys_only` / `count_only` / `serializable` 字段一一对应(字段表见 4.3),实际取值以 `etcdctl get --help` 为准。 + +### 4.2 单 key 读、JSON 输出与 base64 + +```bash +$ etcdctl --endpoints=$ENDPOINTS --write-out="json" get foo +{"header":{"cluster_id":289318470931837780,"member_id":14947050114012957595,"revision":3,"raft_term":4, +"kvs":[{"key":"Zm9v","create_revision":2,"mod_revision":3,"version":2,"value":"SGVsbG8gV29ybGQh"}]}} +$ +``` + +- `header` 携带 `cluster_id`、`member_id`、`revision`、`raft_term`; +- `kvs` 数组每条记录含 `key`、`create_revision`、`mod_revision`、`version`、`value`; +- **key 与 value 在 JSON 中是 base64 编码**(`Zm9v` = `foo`,`SGVsbG8gV29ybGQh` = `Hello World!`)。 + +> 源文疑点(如实记录):该页正文称 `write-out="json"` 时 "the key is not returned",但同一示例输出中确实包含 `"key":"Zm9v"`,两者矛盾,以实际输出为准。 + +小技巧:对**任意 key(存在或不存在)**执行 `get -w=json` 可以拿到集群当前 revision: + +```bash +$ etcdctl get mykey -w=json +{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":15,"raft_term":4}} +``` + +### 4.3 RangeRequest 字段全表 + +| 字段 | 类型 | 说明 | 默认值 / 取值 | +| ---- | ---- | ---- | ------------- | +| `key` | bytes | 区间的第一个 key;若未给 `range_end`,则只查找 `key` 本身 | — | +| `range_end` | bytes | 区间上界 `[key, range_end)` | 未给 = 单 key;`'\0'` = 所有 >= key 的 key;key+1 = 前缀匹配;key 与 range_end 均为 `'\0'` = 所有 key | +| `limit` | int64 | 返回 key 数量上限 | `0` = 无限制 | +| `revision` | int64 | 读取的时间点 revision | <=0 取最新;已被压缩则返回 `ErrCompacted` | +| `sort_order` | SortOrder | 返回结果的排序方向 | `NONE = 0`、`ASCEND = 1`、`DESCEND = 2` | +| `sort_target` | SortTarget | 用于排序的 kv 字段 | `KEY = 0`、`VERSION = 1`、`CREATE = 2`、`MOD = 3`、`VALUE = 4` | +| `serializable` | bool | 使用 serializable 成员本地读(默认 linearizable);延迟低吞吐高但可能读到陈旧数据 | `false` = linearizable | +| `keys_only` | bool | 只返回 key 不返回 value | `false` | +| `count_only` | bool | 只返回区间内 key 的数量 | `false` | +| `min_mod_revision` | int64 | mod revision 下界,更小者被过滤 | `0` = 不过滤 | +| `max_mod_revision` | int64 | mod revision 上界,更大者被过滤 | `0` = 不过滤 | +| `min_create_revision` | int64 | create revision 下界,更小者被过滤 | `0` = 不过滤 | +| `max_create_revision` | int64 | create revision 上界,更大者被过滤 | `0` = 不过滤 | + +### 4.4 RangeResponse 字段 + +| 字段 | 类型 | 说明 | +| ---- | ---- | ---- | +| `header` | ResponseHeader | 见 3.3 | +| `kvs` | (slice of) mvccpb.KeyValue | 匹配到的 kv 列表;设置 `count_only` 时为空 | +| `more` | bool | 若设置了 `limit`,指示区间内是否还有更多 key | +| `count` | int64 | 区间内实际数量,**不受 `limit` 与 Min/Max Create/Mod Revision 过滤影响** | + +### 4.5 历史版本读、前缀读、限量读、from-key 读(官方示例原样) + +```bash +$ etcdctl get --prefix foo # 最新版本 +foo +bar_new +foo1 +bar1_new + +$ etcdctl get --prefix --rev=4 foo # revision 4 时的版本 +foo +bar_new +foo1 +bar1 + +$ etcdctl get --prefix --rev=3 foo +foo +bar +foo1 +bar1 + +$ etcdctl get --prefix --rev=2 foo +foo +bar + +$ etcdctl get --prefix --rev=1 foo # 空 +``` + +```bash +$ etcdctl get foo foo3 # 半开区间 [foo, foo3),foo3 被排除 +foo +bar +foo1 +bar1 +foo2 +bar2 + +$ etcdctl get --prefix --limit=2 foo +foo +bar +foo1 +bar1 + +$ etcdctl get --from-key b # 读 >= b 的全部 key(a=123, b=456, z=789) +b +456 +z +789 +``` + +压缩之后读旧 revision 会失败: + +```bash +$ etcdctl compact 5 +compacted revision 5 + +$ etcdctl get --rev=4 foo +Error: rpc error: code = 11 desc = etcdserver: mvcc: required revision has been compacted +``` + +### 4.6 JSON 与 table 输出差异 + +| 维度 | 默认(简单文本) | `--write-out="json"` | `-wtable` | +| ---- | ---------------- | -------------------- | --------- | +| 内容 | 每个 key 一行、value 一行 | `header` 四字段 + `kvs` 数组(含 `create_revision`/`mod_revision`/`version`) | 人类可读的列式表格 | +| key/value 编码 | 原文 | **base64** | 原文 | +| 用途 | 人肉观察 | 脚本解析、取 `header.revision` | 人肉观察多列结果(源文在 `member list --write-out table` 中使用) | +| 备注 | — | 源文正文称"不返回 key",但示例输出含 key(见 4.2 疑点) | 源文未给出 `get -wtable` 的完整输出样例 | + +### 4.7 RangeStream:大区间读 + +> 引自:[etcd API](https://etcd.io/docs/v3.7/learning/api/) + +`RangeStream` 返回与 `Range` 相同的结果集,但服务端把响应**切分成一系列 chunk 流式**下发,避免两端在内存里缓冲整个大区间。它接受相同的 `RangeRequest`,客户端收到 `RangeStreamResponse { RangeResponse range_response = 1; }` 流。 + +| 规则 | 说明 | +| ---- | ---- | +| `kvs` | 每个 chunk 携带**互不相交**的一个切片;按到达顺序拼接即等于单次 `Range` 的结果 | +| `header` / `more` / `count` | **只在最后一个 chunk 上填充**,且仅在流无错完成时;之前 chunk 为零值 | +| 出错 | 流以错误结束时**没有任何 chunk** 携带有效的 `header`、`more`、`count` | +| 一致性 | 流中每个 chunk 基于**同一 revision**;若请求未设 `revision`,服务端在流开始时捕获最新已提交 revision 并全程复用 | +| 限制 | **不支持自定义排序**与 revision 过滤器(`min_mod_revision`、`max_mod_revision`、`min_create_revision`、`max_create_revision`),使用这些的请求返回 `Unimplemented`;**etcd gRPC proxy 也不支持 `RangeStream`** | + +etcdctl 侧:`--stream` 不支持 `--order`、`--sort-by` 与 revision 过滤器。Go 侧可用 `clientv3.GetStreamToGetResponse` 把多个 chunk 合并(用 `proto.Merge`)成单个 `RangeResponse`。 + +### 4.8 Go 读取示例 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。 + +```go +// 单 key 读 + 历史版本读 +resp, err := cli.Get(ctx, "foo") +if err != nil { + log.Fatal(err) +} +for _, kv := range resp.Kvs { + fmt.Printf("key=%s value=%s create_rev=%d mod_rev=%d version=%d lease=%d\n", + kv.Key, kv.Value, kv.CreateRevision, kv.ModRevision, kv.Version, kv.Lease) +} +fmt.Println("header.revision =", resp.Header.Revision) + +// 读 revision=4 时的历史版本 +hist, err := cli.Get(ctx, "foo", clientv3.WithRev(4)) +``` + +第二个示例,覆盖前缀 / 限量 / 排序 / 计数 / 只取 key: + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。 + +```go +// 前缀 + 限量 + 按 key 升序 +r1, _ := cli.Get(ctx, "web", + clientv3.WithPrefix(), + clientv3.WithLimit(2), + clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend)) + +// from-key:读 >= "b" 的全部 key +r2, _ := cli.Get(ctx, "b", clientv3.WithFromKey()) + +// 只数数量 / 只取 key +r3, _ := cli.Get(ctx, "web", clientv3.WithPrefix(), clientv3.WithCountOnly()) +r4, _ := cli.Get(ctx, "web", clientv3.WithPrefix(), clientv3.WithKeysOnly()) + +// serializable 本地读(可能陈旧,但延迟低) +r5, _ := cli.Get(ctx, "foo", clientv3.WithSerializable()) + +_ = r1.More // 还有更多 key(配合 WithLimit 做分页) +_ = r3.Count // 区间内 key 总数 +``` + +### 4.9 读一致性与输出格式怎么选 + +> 工程建议(非官方原文,依据官方 `RangeRequest.serializable` 与 API guarantees 的语义推导)。 + +| 场景 | 建议取值 | 理由 | +| ---- | -------- | ---- | +| 读配置后要立刻据此做写决策(如库存、配额、开关) | 默认 `--consistency=l`(linearizable) | 只有线性一致才能保证读到"最新已提交"的值;陈旧读可能导致超卖 | +| 大批量扫描、监控采集、对短暂陈旧不敏感 | `--consistency=s`(serializable) | 成员本地读,无需走 Raft 共识,延迟低、吞吐高 | +| 要拿到"当前集群 revision"做后续对齐 | 任意 key 上 `-w=json` 读 `header.revision` | 官方给出的取当前 revision 的做法 | +| 脚本解析 | `-w=json` | 唯一机器可读且带 `header` 的输出;注意 key/value 是 base64,需解码 | +| 人肉排查 | 默认文本或 `-wtable` | 不需要解码中文/二进制 value | +| 大区间(万级 key 以上) | `--stream`(`RangeStream`) | 分 chunk 下发,避免两端缓冲整个结果集;但**不支持排序与 revision 过滤**,且 gRPC proxy 不支持 | + +还有一个容易踩的点:`--limit` 与 `count` 的含义不同。`limit` 限制的是**本次返回的 kvs 条数**,`count` 是**区间内满足条件的总数**,不受 `limit` 影响。因此"分页 + 显示总数"可以直接用 `count`,而判断是否还有下一页要看 `more`。 + +--- + +## 5. 前缀查询 how-to-get-key-by-prefix + +> 引自:[How to get keys by prefix](https://etcd.io/docs/v3.7/tasks/developer/how-to-get-key-by-prefix/) + +**前提**:安装 `etcdctl`;搭建本地集群。 + +```bash +$ etcdctl --endpoints=$ENDPOINTS get PREFIX --prefix +``` + +- 全局选项:`--endpoints=[127.0.0.1:2379], gRPC endpoints` +- 选项:`--prefix, get a range of keys with matching prefix` + +示例: + +```bash +etcdctl --endpoints=$ENDPOINTS put web1 value1 +etcdctl --endpoints=$ENDPOINTS put web2 value2 +etcdctl --endpoints=$ENDPOINTS put web3 value3 + +etcdctl --endpoints=$ENDPOINTS get web --prefix +``` + +### 5.1 range_end 语义(前缀查询的底层) + +> 引自:[etcd API](https://etcd.io/docs/v3.7/learning/api/) + +etcd 数据模型在**扁平二进制 key space** 上索引所有 key,按区间 `[a, b)` 列出。约定由 `key` 与 `range_end` 两个字段表示: + +| 写法 | 含义 | +| ---- | ---- | +| `range_end` 未给出或为空 | 区间只包含 `key` 本身(单 key 查找) | +| `range_end` = `key` 加一(如 `"aa"+1 == "ab"`、`"a\xff"+1 == "b"`) | 所有以 `key` 为前缀的 key(前缀匹配) | +| `range_end` = `\0` | 所有**大于等于** `key` 的 key | +| `key` 与 `range_end` 都是 `\0` | **所有 key** | + +`--prefix` 就是让 etcdctl 帮你算出"key+1"这个 `range_end`。gRPC-gateway 里需要自己算,例如取 `foo` 前缀时 `range_end` 传 `Zm9w`(即 `foo` → `fop`,见第 12 章)。 + +### 5.2 clientv3 的 range 选项 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写(官方该页只有 etcdctl 示例)。各选项与 `RangeRequest` 字段的对应关系为:`WithPrefix` → `range_end = key+1`;`WithFromKey` → `range_end = '\0'`;`WithLimit` → `limit`;`WithSort` → `sort_target` + `sort_order`。 + +```go +// 前缀查询 +resp, err := cli.Get(ctx, "web", clientv3.WithPrefix()) +if err != nil { + log.Fatal(err) +} +fmt.Println("count =", resp.Count, "more =", resp.More) +for _, kv := range resp.Kvs { + fmt.Printf("%s = %s\n", kv.Key, kv.Value) +} + +// 等价手写区间:[web, wez) 不是通用写法,通用做法是 WithPrefix; +// 需要显式半开区间时可直接用 Get(ctx, start, clientv3.WithRange(end)) +r2, _ := cli.Get(ctx, "web1", clientv3.WithRange("web3")) // [web1, web3) +``` + +### 5.3 WithLimit + More 做分页 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。分页依赖 `RangeResponse.more` 与 `count` 字段(字段语义见 4.4)。 + +```go +start := "web" +for { + resp, err := cli.Get(ctx, start, + clientv3.WithPrefix(), + clientv3.WithLimit(100), + clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend)) + if err != nil { + log.Fatal(err) + } + for _, kv := range resp.Kvs { + handle(kv) + } + if !resp.More { // 没有更多 key,结束 + break + } + // 下一页:从最后一个 key 的下一个字节串开始 + start = string(resp.Kvs[len(resp.Kvs)-1].Key) + "\x00" +} +``` + +### 5.4 WithSort 的排序目标与方向 + +| 目标(SortTarget) | 值 | 方向(SortOrder) | 值 | +| ------------------ | -- | ----------------- | -- | +| `KEY` | 0 | `NONE` | 0 | +| `VERSION` | 1 | `ASCEND` | 1 | +| `CREATE` | 2 | `DESCEND` | 2 | +| `MOD` | 3 | — | — | +| `VALUE` | 4 | — | — | + +> 引自:[etcd API](https://etcd.io/docs/v3.7/learning/api/)(枚举定义 `SortOrder { NONE = 0; ASCEND = 1; DESCEND = 2; }`、`SortTarget { KEY = 0; VERSION = 1; CREATE = 2; MOD = 3; VALUE = 4; }`) + +### 5.5 前缀设计的工程建议 + +> 工程建议(非官方原文,依据"扁平二进制 key space + 字典序索引"这一官方事实推导)。 + +- etcd **没有目录**,所谓"目录"只是 `/` 分隔的前缀约定;前缀查询之所以快,是因为 key 空间在字节串上维护了**字典序索引**。 +- 前缀末尾要不要带 `/` 要想清楚:`/svc/order` 会同时匹配 `/svc/order-created` 这类"看起来不是同族"的 key,而 `/svc/order/` 不会。建议**统一带尾斜杠**。 +- 前缀查询天然是全序扫描的起点,前缀选得太粗(例如直接用 `/`)会扫到整个 keyspace;建议按"服务 / 租户 / 业务对象"分层,如 `/tenantA/order/2026-09-18/xxx`。 +- 需要逆序或按时间排序时,用 `WithSort` 指定 `SortTarget` 与 `SortOrder`(见 5.4),而不是在客户端排序——服务端排可以利用索引。 +- 大前缀下的遍历务必配 `WithLimit` 做分页(见 5.3),一次性拉全量容易触发请求大小上限(默认 **1.5 MiB**,服务端 `--max-request-bytes`)。 + +--- + +## 6. 删除数据 how-to-delete-keys + +> 引自:[How to delete keys](https://etcd.io/docs/v3.7/tasks/developer/how-to-delete-keys/)、[Interacting with etcd](https://etcd.io/docs/v3.7/dev-guide/interacting_v3/) + +**前提**:安装 `etcd` 与 `etcdctl`。用 `del` 删除指定的单个 key 或一段 key 区间。 + +```bash +etcdctl del $KEY [$END_KEY] +``` + +### 6.1 del 的 flag + +| flag | 默认值 | 含义 | +| ---- | ------ | ---- | +| `--prefix` | `[=false]` | 删除所有带该前缀的 key | +| `--prev-kv` | `[=false]` | 在删除的同时返回被删掉的键值对(便于审计或回滚) | +| `--from-key` | `[=false]` | 按字节比较,删除所有**大于等于**给定 key 的 key | +| `--range` | `[=false]` | 立即删除一段区间的 key(不延迟) | +| `--endpoints` | `"127.0.0.1:2379"`(继承自父命令) | gRPC endpoints | + +### 6.2 删除官方示例 + +```bash +etcdctl --endpoints=$ENDPOINTS put key myvalue +etcdctl --endpoints=$ENDPOINTS del key + +etcdctl --endpoints=$ENDPOINTS put k1 value1 +etcdctl --endpoints=$ENDPOINTS put k2 value2 +etcdctl --endpoints=$ENDPOINTS del k --prefix +``` + +带返回值的删除(源数据:`foo = bar`、`foo1 = bar1`、`foo3 = bar3`、`zoo = val`、`zoo1 = val1`、`zoo2 = val2`、`a = 123`、`b = 456`、`z = 789`): + +```bash +$ etcdctl del foo +1 # one key is deleted + +$ etcdctl del foo foo9 +2 # two keys are deleted + +$ etcdctl del --prev-kv zoo +1 # one key is deleted +zoo # deleted key +val # the value of the deleted key + +$ etcdctl del --prefix zoo +2 # two keys are deleted + +$ etcdctl del --from-key b +2 # two keys are deleted +``` + +### 6.3 DeleteRange 字段与 deleted 数量 + +| 消息 | 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ---- | ------ | +| DeleteRangeRequest | `key` | bytes | 要删除的区间的第一个 key | — | +| DeleteRangeRequest | `range_end` | bytes | 区间 `[key, range_end)` 的尾后 key | 未给 = 仅 key;key+1 = 前缀;`'\0'` = 所有 >= key | +| DeleteRangeRequest | `prev_kv` | bool | 删除前取回旧 kv 对,随 delete 响应返回 | `false` | +| DeleteRangeResponse | `header` | ResponseHeader | 见 3.3 | — | +| DeleteRangeResponse | `deleted` | int64 | **删除的 key 数量** | — | +| DeleteRangeResponse | `prev_kvs` | (slice of) mvccpb.KeyValue | 本次删掉的所有 kv 对 | — | + +语义补充:`DeleteRange` 会**递增 revision**,并为**每个被删 key 生成一个 delete 事件**(第 16 章 KV 方法表)。 + +### 6.4 Go 删除示例 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。 + +```go +// 单 key 删除 + 返回被删内容 +resp, err := cli.Delete(ctx, "zoo", clientv3.WithPrevKV()) +if err != nil { + log.Fatal(err) +} +fmt.Println("deleted =", resp.Deleted, "revision =", resp.Header.Revision) +for _, kv := range resp.PrevKvs { + fmt.Printf("deleted key=%s old value=%s\n", kv.Key, kv.Value) +} + +// 前缀删除 +r2, err := cli.Delete(ctx, "zoo", clientv3.WithPrefix()) + +// from-key 删除(删 >= "b" 的全部 key) +r3, err := cli.Delete(ctx, "b", clientv3.WithFromKey()) +``` + +### 6.5 删除会带来的三个后果 + +> 依据官方 `DeleteRange` 语义(KV 方法表)与 data model 的 generation / tombstone 规则整理。 + +1. **revision 同样会 +1**:删除不是"抹掉历史",而是在新的 revision 上记录一次变更;因此删除也会产生事件、也会推进全局逻辑时钟。 +2. **每个被删 key 产生一个 delete 事件**:所有 watch 该 key(或其前缀)的客户端都会收到 `DELETE` 事件,事件里的 `kv.mod_revision` 就是删除发生时的 revision。做本地缓存的业务必须处理 `DELETE`,否则缓存里会留下幽灵数据。 +3. **version 归零、generation 终结**:删除会把 key 的 version 重置为 **0** 并生成 tombstone,终结当前 generation;下次再写入同一个 key,version 从 **1** 重新计数。因此**不要用 version 判断"这个 key 一共被改过多少次"**,跨 generation 的 version 不可比,要跨生命周期统计请用 mod_revision 或自己记录。 + +补充:删除区间用 `[key, range_end)` 半开区间;`--from-key` 会删掉所有**大于等于**给定 key 的 key,误用会删掉半个 keyspace,执行前建议先用同样的条件 `get` 一遍确认范围。 + +--- + +## 7. 事务写 how-to-transactional-write + +> 引自:[How to make multiple writes in a transaction](https://etcd.io/docs/v3.7/tasks/developer/how-to-transactional-write/) + +**前提**:安装 `etcd` 与 `etcdctl`;一个运行中的 etcd 集群。 + +**术语**:`txn` 是 "transaction" 的缩写,它从标准输入读取多个 etcd 请求,并作为**单个原子事务**提交。一个事务由三部分组成:一组条件(conditions)、全部条件为真时要执行的请求列表、任一条件为假时要执行的请求列表。`compare` 子句即事务中的条件检查,只有当键值存储的当前状态满足预期条件时才继续应用变更,从而保证数据一致性、避免并发冲突。 + +查看帮助: + +```bash +etcdctl txn --help +``` + +### 7.1 compare / If / Then / Else 结构 + +| 结构 | etcdctl 交互模式的段名 | proto 字段 | 语义 | +| ---- | ---------------------- | ---------- | ---- | +| 条件 | `compares:` | `TxnRequest.compare` | 谓词列表(**合取**),全部为真才走 Then | +| 条件为真 | `success requests (get, put, delete):` | `TxnRequest.success` | compare 求值为 true 时按序应用 | +| 条件为假 | `failure requests (get, put, delete):` | `TxnRequest.failure` | compare 求值为 false 时按序应用 | + +### 7.2 变量与 flag(原样保留,含官方笔误) + +| 项 | 说明 | +| -- | ---- | +| `/users/{/email` | 表示该用户 email 地址的 etcd key(**原文如此,含多余左花括号**) | +| `/users//phone` | 表示该用户电话号码的 etcd key | +| `--interactive` | 允许手工输入事务数据的 flag | + +### 7.3 etcdctl 交互式 txn 完整示例 + +步骤 1:准备初始数据 + +```bash + etcdctl put /users/12345/email "old.address@johndoe.com" + etcdctl put /users/12345/phone "123-456-7890" +``` + +步骤 2:执行事务 + +```bash + etcdctl txn --interactive + + compares: + value("/users/12345/email") = "old.address@johndoe.com" + + success requests (get, put, delete): + put /users/12345/email "new.address@johndoe.com" + put /users/12345/phone "098-765-4321" + + failure requests (get, put, delete): + get /users/12345/email +``` + +三段对应关系: + +- **Compare**:检查 email 当前值是否为 `old.address@johndoe.com`,只有符合预期事务才继续; +- **Success(Then)**:比较为真时,同时更新 email 与 phone; +- **Failure(Else)**:比较失败时,读取 `/users/12345/email` 的当前值,以便判断事务为何没有执行。 + +### 7.4 比较算子与比较目标 + +etcdctl 交互式写法使用 `=`、`!=`、`>`、`<`(源文示例中出现 `=`),对应 proto 的 `CompareResult` 与 `CompareTarget`: + +| 维度 | 取值 | 值 | +| ---- | ---- | -- | +| CompareResult(比较算子) | `EQUAL` | 0 | +| CompareResult | `GREATER` | 1 | +| CompareResult | `LESS` | 2 | +| CompareResult | `NOT_EQUAL` | 3 | +| CompareTarget(比较目标) | `VERSION` | 0 | +| CompareTarget | `CREATE` | 1 | +| CompareTarget | `MOD` | 2 | +| CompareTarget | `VALUE` | 3 | + +> 引自:[etcd API](https://etcd.io/docs/v3.7/learning/api/) + +`Compare` 消息字段(v3.7 `api/etcdserverpb/rpc.proto`,见第 16 章): + +| 字段 | 类型 | 说明 | +| ---- | ---- | ---- | +| `result` | CompareResult | 本次比较的逻辑比较操作 | +| `target` | CompareTarget | 要检查的 kv 字段 | +| `key` | bytes | 比较操作的主体 key | +| `target_union` | oneof | 比较目标的联合类型:`version` / `create_revision` / `mod_revision` / `value` | +| `version` | int64 | 给定 key 的 version | +| `create_revision` | int64 | 给定 key 的 creation revision | +| `mod_revision` | int64 | 给定 key 的最后修改 revision | +| `value` | bytes | 给定 key 的 value | +| `lease` | int64 | 给定 key 的 lease id | +| `range_end` | bytes | 将该 target 与 `[key, range_end)` 内所有 key 比较 | + +对照到 etcdctl 交互式写法,就是"比较目标 + key"这一层: + +| 交互式写法 | 对应 target | 含义 | +| ---------- | ----------- | ---- | +| `value("") = ""` | `VALUE` | 比较 value | +| `create_revision("") = N` | `CREATE` | 比较 create revision | +| `mod_revision("") = N` | `MOD` | 比较 mod revision(CAS 常用) | +| `version("") = N` | `VERSION` | 比较 version | + +### 7.5 Txn 的 RequestOp / ResponseOp 与响应 + +| 消息 | 字段 | 类型 | 说明 | +| ---- | ---- | ---- | ---- | +| TxnRequest | `compare` | (slice of) Compare | 谓词列表(合取);全部成功则按序处理 `success` 并回各自响应,失败则按序处理 `failure` | +| TxnRequest | `success` | (slice of) RequestOp | compare 为 true 时应用 | +| TxnRequest | `failure` | (slice of) RequestOp | compare 为 false 时应用 | +| RequestOp | `request` | oneof | `request_range` / `request_put` / `request_delete_range` / `request_txn` | +| ResponseOp | `response` | oneof | `response_range` / `response_put` / `response_delete_range` / `response_txn` | +| TxnResponse | `header` | ResponseHeader | 顶层 header | +| TxnResponse | `succeeded` | bool | compare 求值为 true 则为 true | +| TxnResponse | `responses` | (slice of) ResponseOp | 与所应用的 RequestOp 列表一一对应 | + +> 引自:[etcd API](https://etcd.io/docs/v3.7/learning/api/):**内层响应中的 `ResponseHeader` 不应以任何方式解读**;客户端若需获取最新 revision,应始终检查 `TxnResponse` 中**顶层**的 `ResponseHeader`。 + +其他硬性约束: + +- 事务在单个请求中原子处理多个请求;对修改操作,**存储 revision 只递增一次**,事务产生的所有事件拥有**相同 revision**; +- **同一事务内多次修改同一个 key 是被禁止的**(`request_put` 的 key 必须唯一,不得与其他 Put 或 Delete 共享 key); +- 对**不含嵌套 TXN** 的事务,操作执行顺序保证与其操作列表顺序相同(因此事务内的 GET 响应是稳定的);**含嵌套 TXN** 时执行顺序不做规定。 + +### 7.6 官方三条重要注意事项 + +1. **原子性(Atomicity)**:两个更新要么一起生效,要么都不生效;初始条件不满足时两个更新都不会被应用。 +2. **一致性(Consistency)**:用事务维护多个相关更新之间的数据一致性。 +3. **避免对同一 key 多次 put**:不要在单个事务里对同一个 key 写入多个值,否则会产生非预期结果;每个 key 在一个事务中只应更新一次。 + +### 7.7 Go Txn 写法 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写(官方该页仅提供 etcdctl 交互式示例)。`clientv3.Compare(...)` 对应 `Compare` 消息,`OpPut` / `OpGet` / `OpDelete` 对应 `RequestOp`。 + +```go +// 场景:确认 email 仍为旧值,才同时更新 email 与 phone +txnResp, err := cli.Txn(ctx). + If(clientv3.Compare(clientv3.Value("/users/12345/email"), "=", "old.address@johndoe.com")). + Then( + clientv3.OpPut("/users/12345/email", "new.address@johndoe.com"), + clientv3.OpPut("/users/12345/phone", "098-765-4321"), + ). + Else( + clientv3.OpGet("/users/12345/email"), + ).Commit() +if err != nil { + log.Fatal(err) +} +if txnResp.Succeeded { + fmt.Println("txn applied, revision =", txnResp.Header.Revision) // 只看顶层 header +} else { + // 读 Else 分支里 OpGet 的结果,判断为何失败 + for _, r := range txnResp.Responses { + get := r.GetResponseRange() + for _, kv := range get.Kvs { + fmt.Printf("current email = %s (mod_revision=%d)\n", kv.Value, kv.ModRevision) + } + } +} +``` + +### 7.8 典型场景一:乐观锁 CAS(用 mod_revision) + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。思路来自 `Compare.target = MOD` 的语义。 + +```go +// 读-改-写:只有在我读到的版本之后没人改过,才写入 +getResp, err := cli.Get(ctx, "/config/feature-flag") +if err != nil { + log.Fatal(err) +} +if len(getResp.Kvs) == 0 { + log.Fatal("key not found") +} +kv := getResp.Kvs[0] + +txnResp, err := cli.Txn(ctx). + If(clientv3.Compare(clientv3.ModRevision("/config/feature-flag"), "=", kv.ModRevision)). + Then(clientv3.OpPut("/config/feature-flag", "new-value")). + Else(clientv3.OpGet("/config/feature-flag")). + Commit() +if err != nil { + log.Fatal(err) +} +if !txnResp.Succeeded { + // 被并发修改,重读重试即可 + log.Println("CAS failed, retry") +} +``` + +要点: + +- 用 `mod_revision` 而不是 `value` 做比较,可以覆盖"A 改成 B、B 又改回 A"这种 ABA 场景; +- 失败分支里直接 `OpGet` 拿最新值,省掉一次额外 RTT; +- 事务成功后**只信顶层 header 的 revision**。 + +### 7.9 典型场景二:库存扣减 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。 + +```go +// 库存扣减:判断余量足够再扣,整个“判断 + 扣减”在一个事务里 +const key = "/sku/10001/stock" + +resp, err := cli.Get(ctx, key) +if err != nil { + log.Fatal(err) +} +cur := parseStock(resp.Kvs) // 当前库存 +want := cur - 1 // 拟扣减后的值 +if cur <= 0 { + return ErrSoldOut +} + +txnResp, err := cli.Txn(ctx). + If( + // 1) 库存没有被别人动过 + clientv3.Compare(clientv3.ModRevision(key), "=", resp.Kvs[0].ModRevision), + // 2) 值仍然等于我读到的值(双保险) + clientv3.Compare(clientv3.Value(key), "=", formatStock(cur)), + ). + Then(clientv3.OpPut(key, formatStock(want))). + Commit() +if err != nil { + log.Fatal(err) +} +if !txnResp.Succeeded { + return ErrConcurrentUpdate // 让上层重试 +} +``` + +注意:etcd 事务里**没有算术运算**,"扣减"必须在客户端算好新值再写回,靠 `compare` 保证中间没有被别的写者插队;这就是典型的乐观并发控制(compare-and-swap)。超高并发下单点热点请配合批处理或分片,避免所有请求都打到同一 key。 + +--- + +## 8. Watch 机制 how-to-watch-keys + +> 引自:[How to watch keys](https://etcd.io/docs/v3.7/tasks/developer/how-to-watch-keys/)、[Interacting with etcd](https://etcd.io/docs/v3.7/dev-guide/interacting_v3/)、[etcd API](https://etcd.io/docs/v3.7/learning/api/)、[etcd API guarantees](https://etcd.io/docs/v3.7/learning/api_guarantees/) + +**前提**:安装 `etcd` 与 `etcdctl`。用 `watch` 订阅未来的变更通知。 + +```bash +etcdctl watch $KEY [$END_KEY] +``` + +### 8.1 watch 的 flag + +| flag | 默认值 | 含义 | +| ---- | ------ | ---- | +| `-i, --interactive` | `[=false]` | 交互模式,可连续输入多个 watch 请求 | +| `--prefix` | `[=false]` | 监听整个前缀 | +| `--rev` | `0` | 从哪个 revision 开始监听;**默认 0 表示只监听当前时刻之后的变更** | +| `--prev-kv` | `[=false]` | 让事件里带上变更发生前的那个键值对 | +| `--progress-notify` | `[=false]` | 让服务端周期性推送 watch 进度通知 | +| `--endpoints` | `"127.0.0.1:2379"`(继承自父命令) | gRPC endpoints | + +### 8.2 revision 语义与事件类型 + +- `--rev` 指定从哪个 revision 开始监听(默认 0,即只监听当前时刻之后的变更),这是 watch 的 revision 语义核心——指定历史 revision 可以**回放自该版本以来的全部事件**; +- 整个可用事件历史可从**最后一次 compaction revision** 开始 watch; +- 事件用 `Event` 消息表达: + +| 字段 | 类型 | 说明 | +| ---- | ---- | ---- | +| `type` | EventType | `PUT` = 有新数据存入该 key;`DELETE` = key 被删除 | +| `kv` | KeyValue | PUT 含当前 kv 对,**`kv.Version=1` 的 PUT 表示 key 被创建**;DELETE / EXPIRE 含被删 key,其 `mod_revision` 设为删除时的 revision | +| `prev_kv` | KeyValue | 事件发生前的 kv 对;为节省带宽,**仅当 watch 显式开启时才填充** | + +### 8.3 官方 watch 示例 + +```bash +etcdctl --endpoints=$ENDPOINTS watch stock1 +etcdctl --endpoints=$ENDPOINTS put stock1 1000 + +etcdctl --endpoints=$ENDPOINTS watch stock --prefix +etcdctl --endpoints=$ENDPOINTS put stock1 10 +etcdctl --endpoints=$ENDPOINTS put stock2 20 +``` + +第一个 watch 只盯 `stock1`;第二个 watch 盯 `stock` 前缀,`put stock1 10` 与 `put stock2 20` 都会各自产生事件。 + +```bash +$ etcdctl watch foo +# in another terminal: etcdctl put foo bar +PUT +foo +bar + +$ etcdctl watch foo foo9 # 区间 [foo, foo9) +PUT +foo +bar +PUT +foo1 +bar1 +``` + +历史回放(`--rev`): + +```bash +$ etcdctl put foo bar # revision = 2 +$ etcdctl put foo1 bar1 # revision = 3 +$ etcdctl put foo bar_new # revision = 4 +$ etcdctl put foo1 bar1_new # revision = 5 + +# watch for changes on key `foo` since revision 2 +$ etcdctl watch --rev=2 foo +PUT +foo +bar +PUT +foo +bar_new + +# watch for changes on key `foo` since revision 3 +$ etcdctl watch --rev=3 foo +PUT +foo +bar_new +``` + +带变更前值(`--prev-kv`): + +```bash +$ etcdctl watch --prev-kv foo +# in another terminal: etcdctl put foo bar_latest +PUT +foo # key +bar_new # last value of foo key before modification +foo # key +bar_latest # value of foo key after modification +``` + +交互模式监听多个 key,以及用 `progress` 命令查询进度: + +```bash +$ etcdctl watch -i +$ watch foo +$ watch zoo +# in another terminal: etcdctl put foo bar +PUT +foo +bar +# in another terminal: etcdctl put zoo val +PUT +zoo +val +``` + +```bash +$ etcdctl watch -i +$ watch a +$ progress +progress notify: 1 +# in another terminal: etcdctl put x 0 +# in another terminal: etcdctl put y 1 +$ progress +progress notify: 3 +``` + +> 注意(官方 alert):progress notify 响应里的 revision 来自**该 watch 流所连接的本地 etcd 节点**。若该节点被分区、不在 quorum 内,这个 progress notify revision **可能低于**对非分区节点做 quorum 读得到的 revision。 + +### 8.4 WatchCreateRequest 字段 + +| 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ------ | +| `key` | bytes | 要注册的监听 key | — | +| `range_end` | bytes | 监听区间 `[key, range_end)` | 未给 = 仅 key;`'\0'` = 所有 >= key;key+1 = 前缀 | +| `start_revision` | int64 | 从该 revision 开始监听(**含**) | 未给 = "now" | +| `progress_notify` | bool | 无近期事件时服务端周期性发送无事件的 WatchResponse;频率由服务端按当前负载决定 | `false` | +| `filters` | (slice of) FilterType | 服务端下发前过滤事件(`NOPUT` / `NODELETE`) | — | +| `prev_kv` | bool | 获取事件发生前的上一个 KV;**若已被压缩则不返回** | `false` | +| `watch_id` | int64 | 若提供且非 0,则赋给该 watcher(创建非同步,用于保证同一流上多 watcher 的顺序);复用已用 ID 会返回错误 | `0` = 服务端分配 | +| `fragment` | bool | 将大 revision 拆分为多个 watch 响应 | `false` | + +### 8.5 WatchResponse 字段 + +| 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ------ | +| `header` | ResponseHeader | — | — | +| `watch_id` | int64 | 该响应对应的 watcher ID | — | +| `created` | bool | 是 create watch 请求的响应则为 true;客户端应记录 `watch_id`,后续事件同 `watch_id` | `false` | +| `canceled` | bool | 是 cancel 请求、或 `start_revision` 已被压缩时为 true;此后不再发送事件 | `false` | +| `compact_revision` | int64 | 在已压缩 index 上监听时被设为最小 index;客户端应视为 canceled 且**不应再用同一 `start_revision` 创建 watcher** | `0` | +| `cancel_reason` | string | 取消 watcher 的原因 | — | +| `fragment` | bool | 大 watch 响应被拆分时为 true | `false` | +| `events` | (slice of) mvccpb.Event | 事件列表 | — | + +取消:`WatchCancelRequest { int64 watch_id = 1; }`——传入要取消的 watch ID,取消后不再传输事件。 + +### 8.6 watch 的六条保证 + 一条例外 + +> 引自:[etcd API guarantees](https://etcd.io/docs/v3.7/learning/api_guarantees/) + +| # | 保证 | 含义 | +| - | ---- | ---- | +| 1 | Ordered(有序) | 事件按 revision 排序;不含嵌套 TXN 的事务,事件顺序与操作列表顺序相同;含嵌套 TXN 则不做规定 | +| 2 | Unique(唯一) | 一个事件绝不会在同一 watch 上出现两次 | +| 3 | Reliable(可靠) | 在可用历史窗口内,事件序列不会丢弃子序列:收到 a 和 c 则保证也收到 b | +| 4 | Atomic(原子) | 事件列表保证涵盖**完整的 revision**;同一 revision 对多个 key 的更新不会被拆分到多个事件列表 | +| 5 | Resumable(可恢复) | 中断的 watch 可用**新的 watch** 恢复,起点为断连前收到的最后一个 revision **之后**(只要仍在历史窗口内) | +| 6 | Bookmarkable(可打书签) | Progress notification 保证截止到某一 revision 的所有事件都已投递完毕 | + +**重要例外**:etcd **不保证 watch 操作的线性一致性**。用户应自行校验 watch 事件的 revision,以确保与其他操作的正确排序。 + +时延特性:调用 KV API 会**立即生效**,而 Watch API 的返回带有**无界的延迟**——正常工作的集群中事件应在发生后约 **10ms** 出现,但**没有上限**,不健康的集群中事件可能永远不到达。 + +### 8.7 断线重连与 revision 续传 + +正确姿势(依据 Resumable + Bookmarkable 两条保证): + +1. 建立 watch 时记录 `header.revision`(或 `progress-notify` 推送的 revision); +2. 每处理完一个事件,把 `event.Kv.ModRevision` 记为 `lastRev`; +3. 断线后,**不要**从 `lastRev` 起,而是从 `lastRev + 1` 起重新 watch; +4. 若服务端返回 `canceled=true` 且 `compact_revision != 0`,说明你要的起点已被压缩,**不能**用同一 `start_revision` 重建,必须全量 `Get` 一次拿当前快照,再从其 `header.revision + 1` 继续 watch; +5. 起点被压缩的另一种表现是 `ErrCompacted`(`etcdserver: mvcc: required revision has been compacted`)。 + +### 8.8 Go WatchChannel 用法 + +> 💡 补充:以下 Go 示例基于官方 API 语义编写(官方该页仅提供 etcdctl 示例)。 + +```go +// 从当前时刻开始监听前缀 +watchChan := cli.Watch(ctx, "stock", clientv3.WithPrefix()) +for wresp := range watchChan { + for _, ev := range wresp.Events { + switch ev.Type { + case clientv3.EventTypePut: + fmt.Printf("PUT %s = %s (rev=%d, version=%d)\n", + ev.Kv.Key, ev.Kv.Value, ev.Kv.ModRevision, ev.Kv.Version) + case clientv3.EventTypeDelete: + fmt.Printf("DELETE %s (mod_rev=%d)\n", ev.Kv.Key, ev.Kv.ModRevision) + } + if ev.PrevKv != nil { // 需要 WithPrevKV() + fmt.Printf(" prev value = %s\n", ev.PrevKv.Value) + } + } +} +``` + +带历史回放与 prev-kv: + +> 💡 补充:以下 Go 示例基于官方 API 语义编写,非官方文档原文。 + +```go +rch := cli.Watch(ctx, "foo", + clientv3.WithRev(2), // 从 revision 2 开始(含) + clientv3.WithPrevKV(), // 带变更前的值 + clientv3.WithProgressNotify(), // 周期性 progress 通知 +) +for wresp := range rch { + if wresp.Canceled { // 被取消:常见原因是起点被压缩或消费者太慢 + log.Printf("watch canceled: reason=%s compact_revision=%d", + wresp.CancelReason, wresp.CompactRevision) + return + } + if wresp.IsProgressNotify() { + log.Printf("progress: revision=%d", wresp.Header.Revision) // 可打书签 + continue + } + for _, ev := range wresp.Events { + handle(ev) + lastRev = ev.Kv.ModRevision // 记录续传起点 + } +} +``` + +### 8.9 watch 泄漏与慢消费者的坑 + +| 坑 | 现象 | 规避 | +| -- | ---- | ---- | +| watch 泄漏 | 业务代码忘记 `cancel()` 或 channel 无人消费,watcher 在服务端一直存在 | 每个 watch 都绑定一个明确生命周期的 `context`;退出路径上 `cancel()` | +| 慢消费者被取消 | 客户端处理速度跟不上事件产生速度,服务端缓冲积压,最终**取消该 watcher** 并在 `compact_revision` 里给出可用最小 revision;`cancel_reason` 说明原因 | watch 与业务处理解耦:watch 协程只负责把事件丢进有界队列,业务另起消费者;监控 `Canceled` / `CompactRevision` | +| 起点被压缩后重复失败 | 用旧的 `start_revision` 无限重建 watch,一直拿到 canceled | 一旦 `Canceled && CompactRevision != 0`,改为"全量 Get + 从新 revision 续 watch" | +| 误把 progress revision 当全局最新 | 该 revision 来自**本地节点**,节点被分区时会偏低 | 关键路径用 quorum 读(默认 linearizable)校验 | +| 单 watch 流塞太多 key | 一个 stream 上多 watcher 虽然省连接,但顺序与背压相互影响 | 高价值 key 独立 watch;同一 stream 内可用 `watch_id` 保证顺序 | +| watch 不保证线性一致 | 事件顺序可靠,但与其它操作之间的实时先后无保证 | 业务侧自行比对 revision | + +### 8.10 watch 驱动本地缓存的实践要点 + +> 工程建议(非官方原文,依据 watch 的六条保证与 `WatchResponse` 字段语义推导)。 + +标准姿势是"**先全量、后增量**": + +1. 启动时先 `Get` 一次全量数据,记下响应的 `header.revision`(记为 `rev0`); +2. 立刻用 `WithRev(rev0 + 1)` 建立 watch——注意是 **+1**,否则会重复收到 `rev0` 那一次的事件(幂等的话重复也无害,但会浪费一次处理); +3. 之后所有事件按 `ModRevision` 顺序应用到本地缓存; +4. 断线后从"最后处理成功的 revision + 1"重连; +5. 一旦收到 `Canceled=true` 且 `CompactRevision != 0`,说明中间的历史已被压缩,必须回到第 1 步全量重建; +6. 用 `WithProgressNotify()` 定期拿到"截止某 revision 已投递完毕"的书签,用于判断缓存是否已经追上(注意该 revision 来自**本地节点**,节点被分区时可能偏低,关键路径要用 quorum 读校验)。 + +另外两条硬约束:watch **不保证线性一致性**,所以它只能保证"事件顺序正确",不能保证"事件与你在别处发起的读操作之间的实时先后";watch 事件的到达**没有时间上限**,不要用"多久没收到事件"来判断"没有变更",要用 progress notify 或主动读。 + +--- + +## 9. Lease 租约 how-to-create-lease + +> 引自:[How to create lease](https://etcd.io/docs/v3.7/tasks/developer/how-to-create-lease/)、[Interacting with etcd](https://etcd.io/docs/v3.7/dev-guide/interacting_v3/)、[etcd API](https://etcd.io/docs/v3.7/learning/api/) + +用 `lease` 实现带 TTL 的写入。 + +```bash +etcdctl --endpoints=$ENDPOINTS lease grant 300 +# lease 2be7547fbc6a5afa granted with TTL(300s) + +etcdctl --endpoints=$ENDPOINTS put sample value --lease=2be7547fbc6a5afa +etcdctl --endpoints=$ENDPOINTS get sample + +etcdctl --endpoints=$ENDPOINTS lease keep-alive 2be7547fbc6a5afa +etcdctl --endpoints=$ENDPOINTS lease revoke 2be7547fbc6a5afa +# or after 300 seconds +etcdctl --endpoints=$ENDPOINTS get sample +``` + +### 9.1 流程与语义(逐步) + +| 步 | 命令 | 语义 | +| -- | ---- | ---- | +| 1 | `lease grant 300` | 申请 TTL 为 300 秒的租约,返回租约 ID `2be7547fbc6a5afa`(输出 `lease 2be7547fbc6a5afa granted with TTL(300s)`) | +| 2 | `put sample value --lease=2be7547fbc6a5afa` | 把 key `sample` 绑定到该租约;`--lease` 指定租约 ID,租约到期时该 key 会被自动删除 | +| 3 | `get sample` | 在租约有效期内可以读到该 key | +| 4 | `lease keep-alive 2be7547fbc6a5afa` | 持续为该租约续期(**需客户端保持运行**,否则租约仍会到期) | +| 5 | `lease revoke 2be7547fbc6a5afa` | 主动撤销租约,绑定其上的 key 立即失效;不撤销则 300 秒后同样失效,此时 `get sample` 读不到内容 | + +### 9.2 lease 子命令一览(官方示例覆盖范围内) + +| 子命令 | 示例 | 输出 / 语义 | +| ------ | ---- | ----------- | +| `lease grant ` | `etcdctl lease grant 60` | `lease 32695410dcc0ca06 granted with TTL(60s)` | +| `lease revoke ` | `etcdctl lease revoke 32695410dcc0ca06` | `lease 32695410dcc0ca06 revoked`;所有附加 key 被删除,`get foo` 返回空 | +| `lease keep-alive ` | `etcdctl lease keep-alive 32695410dcc0ca06` | `lease 32695410dcc0ca06 keepalived with TTL(60)`(持续输出,每轮一行) | +| `lease timetolive ` | `etcdctl lease timetolive 694d5765fc71500b` | `lease 694d5765fc71500b granted with TTL(500s), remaining(258s)` | +| `lease timetolive --keys ` | `etcdctl lease timetolive --keys 694d5765fc71500b` | `lease 694d5765fc71500b granted with TTL(500s), remaining(132s), attached keys([zoo2 zoo1])` | +| 过期 / 不存在 | — | `Error: etcdserver: requested lease not found` | + +> 关于"一次性续期":v3.7 官方页面**没有**给出 `etcdctl lease keep-alive --once` 这类 flag 的示例;一次性续期在 Go client 侧对应 `clientv3.KeepAliveOnce`(见 9.5,标注为补充)。gRPC 层只有双向流的 `LeaseKeepAlive`。 + +### 9.3 Lease 机制要点 + +- Lease 是检测客户端**存活(liveness)**的机制;集群授予 lease 一个 TTL,**在给定 TTL 周期内未收到 keepAlive 即过期**; +- 每个 key **最多只能附加一个 lease**; +- lease 过期或被撤销时,附加到该 lease 的所有 key 都会被删除;**每个过期的 key 都会在事件历史中生成一个 delete 事件**(因此 watch 能观察到租约到期); +- 保活走 `LeaseKeepAlive` 建立的**双向流**:客户端发 `LeaseKeepAliveRequest { int64 ID = 1; }`,服务端回 `LeaseKeepAliveResponse { header, ID, TTL }`(`TTL` 为刷新后的剩余存活秒数)。 + +### 9.4 Lease 相关字段表 + +| 消息 | 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ---- | ------ | +| LeaseGrantRequest | `TTL` | int64 | 建议的 time-to-live,单位**秒**;过期的 lease 返回 **-1** | — | +| LeaseGrantRequest | `ID` | int64 | 请求的 lease ID | `0` = 由 lessor 选择 | +| LeaseGrantResponse | `header` / `ID` / `TTL` / `error` | — | `ID` 授予的 lease ID;`TTL` 服务端选定的 TTL 秒数;`error` 为 string(源文未给取值说明) | — | +| LeaseRevokeRequest | `ID` | int64 | 要撤销的 lease ID;撤销后关联 key 全部删除 | — | +| LeaseKeepAliveRequest | `ID` | int64 | 要保活的 lease ID | — | +| LeaseKeepAliveResponse | `header` / `ID` / `TTL` | — | `TTL` 为新的 TTL | — | +| LeaseTimeToLiveRequest | `ID` | int64 | 该 lease 的 ID | — | +| LeaseTimeToLiveRequest | `keys` | bool | `true` 则查询附加到该 lease 的所有 key | `false` | +| LeaseTimeToLiveResponse | `header` / `ID` / `TTL` / `grantedTTL` / `keys` | — | `TTL` 剩余秒数(将在 TTL+1 秒内过期);`grantedTTL` 创建/续期时授予的初始秒数;`keys` 为 (slice of) bytes | — | +| LeaseLeasesResponse | `header` / `leases` | — | `leases` 为 (slice of) LeaseStatus,LeaseStatus 仅有 `ID` | — | +| LeaseCheckpoint | `ID` / `remaining_TTL` | int64 | 要 checkpoint 的 lease ID / 到期前剩余时间 | — | + +### 9.5 Go Lease 示例(含一次性续期) + +> 💡 补充:以下 Go 示例基于官方 API 语义编写(官方该页仅提供 etcdctl 示例)。 + +```go +// 1) 创建 60 秒租约 +grantResp, err := cli.Grant(ctx, 60) +if err != nil { + log.Fatal(err) +} +leaseID := grantResp.ID + +// 2) 绑定 key +if _, err := cli.Put(ctx, "sample", "value", clientv3.WithLease(leaseID)); err != nil { + log.Fatal(err) +} + +// 3) 持续保活:拿到的是一个 channel,需要持续读取 +ka, err := cli.KeepAlive(ctx, leaseID) // 返回 <-chan *LeaseKeepAliveResponse +if err != nil { + log.Fatal(err) +} +go func() { + for resp := range ka { + log.Printf("keepalived, new TTL = %d", resp.TTL) + } +}() + +// 4) 一次性续期(阻塞一次,返回刷新后的 TTL) +if _, err := cli.KeepAliveOnce(ctx, leaseID); err != nil { + log.Fatal(err) +} + +// 5) 查询租约信息(含附加 key) +ttlResp, err := cli.TimeToLive(ctx, leaseID, clientv3.WithAttachedKeys()) +if err != nil { + log.Fatal(err) +} +log.Printf("grantedTTL=%d ttl=%d keys=%v", ttlResp.GrantedTTL, ttlResp.TTL, ttlResp.Keys) + +// 6) 主动撤销(其上 key 立即删除) +if _, err := cli.Revoke(ctx, leaseID); err != nil { + log.Fatal(err) +} +``` + +### 9.6 keepalive 的心跳间隔坑 + +| 坑 | 说明 | 建议 | +| -- | ---- | ---- | +| 心跳间隔与 TTL 的关系 | TTL 是"至少"值,由 etcd 集群选定;keepalive 必须在 TTL 内到达 | 续约周期取 TTL 的 1/3 甚至更小,留出重试余量 | +| 只有客户端在跑才续得上 | 官方明确:`keep-alive` 需客户端保持运行,否则租约仍会到期 | 把 keepalive 协程与业务进程生命周期绑定,进程退出要主动 `Revoke` 或等 TTL | +| GC / STW / 网络抖动 | 续约请求迟到即过期,key 被删、watch 收到 delete 事件 | TTL 不要设得太紧(秒级 TTL 风险高),并对 delete 事件做"误删"补偿逻辑 | +| 读 keepalive channel | 不消费 keepalive 响应 channel 会导致续约响应堆积 | 有 goroutine 持续读,或直接用 `KeepAliveOnce` 自己做定时循环 | +| 崩溃循环的服务端不释放 lease | 官方文档记录:TTL 以秒为单位持久化,`LeaseCheckpoint` 只扩展 `RemainingTTL`,**崩溃循环(crash-looping)的服务器不会释放 lease** | 不要依赖"服务端重启来清理 lease";客户端自己保证 `Revoke` | +| 分区节点 | keepalive 流可能连在被分区的节点上(HTTP/2 ping 不理解分区) | 配合 endpoint 切换与超时,必要时重建 client | + +--- + +## 10. 分布式锁与选主 how-to-create-locks + +> 引自:[How to create locks](https://etcd.io/docs/v3.7/tasks/developer/how-to-create-locks/)、[How to conduct leader election in etcd cluster](https://etcd.io/docs/v3.7/tasks/operator/how-to-conduct-elections/)、[API reference: concurrency](https://etcd.io/docs/v3.7/dev-guide/api_concurrency_reference_v3/)、[etcd API](https://etcd.io/docs/v3.7/learning/api/) + +### 10.1 etcdctl lock + +`LOCK` 以给定名字获取一个**分布式互斥锁(distributed mutex)**。一旦拿到锁就会一直持有,**直到 etcdctl 进程终止**——这是官方页给出的 unlock 语义:锁的释放由进程退出(进而租约过期)触发,而非显式命令。 + +```bash +etcdctl --endpoints=$ENDPOINTS lock mutex1 +``` + +| 选项 | 说明 | +| ---- | ---- | +| `endpoints` | 集群中机器地址的逗号分隔列表 | +| `ttl` | 锁会话的超时时间(秒) | + +### 10.2 Lock service(gRPC)字段与语义 + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `Lock` | LockRequest | LockResponse | 在给定命名的锁上获取分布式共享锁;成功返回一个在持锁期间一直存在的唯一 key,可配合事务保证更新只在持锁期间发生;锁一直持有到对该 key 调用 `Unlock` 或持有者关联 lease 过期 | +| `Unlock` | UnlockRequest | UnlockResponse | 传入 `Lock` 返回的 key 释放锁;下一个等待该锁的 `Lock` 调用者被唤醒并获得锁所有权 | + +| 消息 | 字段 | 类型 | 说明 | +| ---- | ---- | ---- | ---- | +| LockRequest | `name` | bytes | 要获取的分布式共享锁的标识 | +| LockRequest | `lease` | int64 | 绑定到锁所有权的 lease ID;**lease 过期或被撤销且正持有锁时锁自动释放**;**同一 lease 重复 Lock 视为单次获取,重复加锁是 no-op** | +| LockResponse | `header` | etcdserverpb.ResponseHeader | — | +| LockResponse | `key` | bytes | Lock 调用者持锁期间在 etcd 上存在的 key;**用户不应修改它,否则锁行为未定义** | +| UnlockRequest | `key` | bytes | Lock 授予的锁所有权 key | +| UnlockResponse | `header` | etcdserverpb.ResponseHeader | — | + +### 10.3 Election service(gRPC)字段与语义 + +| 方法 | 请求消息 | 响应消息 | 用途 | 语义要点 | +| ---- | -------- | -------- | ---- | -------- | +| `Campaign` | CampaignRequest | CampaignResponse | 等待获取选举的领导权,成功返回表示领导权的 `LeaderKey` | `LeaderKey` 可用于发布新值、事务性地守卫"仍持有领导权"的 API 请求、以及退出选举 | +| `Proclaim` | ProclaimRequest | ProclaimResponse | 用新值更新 leader 已发布的值 | — | +| `Leader` | LeaderRequest | LeaderResponse | 返回当前选举公告(若有) | — | +| `Observe` | LeaderRequest | LeaderResponse | 按当选 leader 发布顺序,**流式推送**选举公告 | 请求/响应消息与 `Leader` 相同,区别在流式 | +| `Resign` | ResignRequest | ResignResponse | 释放选举领导权,使其他竞选者可获取该选举的领导权 | — | + +| 消息 | 字段 | 类型 | 说明 | +| ---- | ---- | ---- | ---- | +| CampaignRequest | `name` | bytes | 本次竞选的选举标识 | +| CampaignRequest | `lease` | int64 | 绑定到选举领导权的 lease ID;**在 resign 之前 lease 过期或被撤销,领导权转移给下一个竞选者(若有)** | +| CampaignRequest | `value` | bytes | 竞选者胜出时设置的初始公告值 | +| CampaignResponse | `header` / `leader` | — | `leader` 为 LeaderKey,描述持有该选举领导权所用资源 | +| LeaderRequest | `name` | bytes | 要查询领导权信息的选举标识 | +| LeaderResponse | `header` / `kv` | — | `kv` 为代表最新 leader 更新的 mvccpb.KeyValue | +| ProclaimRequest | `leader` / `value` | LeaderKey / bytes | 对该选举的持有权 / 用于覆盖 leader 当前值的更新 | +| ResignRequest | `leader` | LeaderKey | 要通过辞职让出的领导权 | + +`LeaderKey`: + +| 字段 | 类型 | 说明 | +| ---- | ---- | ---- | +| `name` | bytes | 与领导权 key 对应的选举标识 | +| `key` | bytes | 表示选举所有权的不透明 key;**key 被删除则失去领导权** | +| `rev` | int64 | key 的创建 revision;**可在事务中通过比对 key 的创建 revision 与 `rev` 来检验是否拥有该选举** | +| `lease` | int64 | 选举 leader 的 lease ID | + +### 10.4 etcdctl elect(选主命令) + +```bash +etcdctl --endpoints=$ENDPOINTS elect [proposal] +``` + +| 项 | 说明 | +| -- | ---- | +| `--endpoints` | 每个 etcd 集群成员的地址 | +| `election-name` | 选举的字符串标识;**所有参与竞选者必须使用同一个选举名** | +| `leader-name`(proposal) | 新 leader 的提案值 | + +```bash +./etcdctl elect my-election proposal1 +my-election/694d99fafea88404 +proposal1 + +another election: +./etcdctl elect new-election proposal1 +new-election/694d99fafea8840f +proposal1 +``` + +> 注意:`elect` 是**阻塞式**的——客户端成为 leader 后命令保持持有,退出即释放领导权。 + +### 10.5 Go concurrency 包:Session / Mutex / Election + +> 💡 补充:etcd v3.7 的 `tasks/developer/how-to-create-locks` 页面**没有 Go 代码**,concurrency 参考页只描述 gRPC service(`Lock` / `Election`)。下面 Go 示例基于上述官方 API 语义与 `go.etcd.io/etcd/client/v3/concurrency` 的公开 API 编写,字段与语义以 [pkg.go.dev](https://pkg.go.dev/go.etcd.io/etcd/client/v3/concurrency) 为准。 + +```go +// 1) 建立带 TTL 的会话(Session 内部会创建 lease 并自动 keepalive) +s, err := concurrency.NewSession(cli, concurrency.WithTTL(15)) +if err != nil { + log.Fatal(err) +} +defer s.Close() // Close 会撤销 lease,锁自动释放 + +// 2) 互斥锁 +m := concurrency.NewMutex(s, "/locks/order-create") +if err := m.Lock(ctx); err != nil { // 阻塞直到拿到锁 + log.Fatal(err) +} +defer func() { + if err := m.Unlock(ctx); err != nil { // 释放,唤醒下一个等待者 + log.Print(err) + } +}() +doCriticalWork() +``` + +> 💡 补充:接上例,以下选主示例同样基于官方 `Election` service 语义编写,非官方文档原文。 + +```go +// 3) 选主 +e := concurrency.NewElection(s, "/election/order-worker") + +if err := e.Campaign(ctx, "node-1"); err != nil { // 阻塞直到成为 leader + log.Fatal(err) +} +leaderRev := e.Rev() // LeaderKey.rev,可在事务里守卫领导权 + +// 成为 leader 后更新公告值 +if err := e.Proclaim(ctx, "node-1@10.0.0.1:8080"); err != nil { + log.Fatal(err) +} + +// 观察 leader 变化(Observe 为流式) +ch := e.Observe(ctx) +go func() { + for resp := range ch { + for _, kv := range resp.Kvs { + log.Printf("leader = %s (mod_rev=%d)", kv.Value, kv.ModRevision) + } + } +}() + +// 主动放弃领导权,让下一个竞选者接手 +if err := e.Resign(ctx); err != nil { + log.Fatal(err) +} +``` + +`Leader()` 用于查询当前选举公告(对应 gRPC `Leader` 方法),`e.Key()` 返回 `LeaderKey.key`。 + +### 10.6 锁的公平性与 session 过期风险 + +| 议题 | 官方语义 / 结论 | +| ---- | --------------- | +| 公平性 | etcd 客户端的**分布式共享锁使用 create revision 来等待锁归属**——等待者按各自 key 的 create revision 排队,先到先得;`Unlock` 时"下一个等待该锁的 Lock 调用者被唤醒并获得锁所有权" | +| 锁的释放条件 | 对 `LockResponse.key` 调用 `Unlock`,**或**持有者关联 lease 过期/被撤销 | +| 同一 lease 重复加锁 | 视为**单次获取**,重复加锁是 **no-op**(不要指望用它做重入计数) | +| 不要改锁 key | `LockResponse.key` 是锁所有权凭证,**用户不应修改它,否则锁行为未定义** | +| session 过期风险 | Session 的 TTL 就是锁的 TTL:进程 GC 停顿、网络分区、keepalive 迟到 → lease 过期 → **锁自动释放,而业务可能还在临界区里**。必须做" fencing":在写操作的事务里用 `LeaderKey.rev`(比对 key 的 create revision 与 `rev`)或锁 key 的 create revision 做守卫,确认仍持有锁 | +| 选主过期风险 | `Campaign` 的 lease 在 resign 之前过期或被撤销,**领导权转移给下一个竞选者(若有)**;旧 leader 必须通过守卫事务发现这一点并停止工作 | +| TTL 取值 | 太短容易误释放,太长故障切换慢;需在"故障检测速度"与"抖动容忍度"之间权衡 | + +--- + +## 11. gRPC 直连与命名发现 grpc_naming + +> 引自:[gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/) + +etcd 提供一个 **gRPC resolver**,支持一套替代性命名系统:从 etcd 获取 endpoint 来发现 gRPC 服务。**底层机制基于监听以服务名为前缀的 key 的更新**。 + +> 注意:该特性是 **experimental(实验性)** 的,因为它依赖 `google.golang.org/grpc/resolver` 包,而该包在 grpc-go 中仍处于实验状态。 + +### 11.1 初始化 resolver(官方原文 Go 代码) + +> 💡 补充:以下代码引自官方页面原文(非本手册杜撰)——[gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/)。 + +```go +import ( + clientv3 "go.etcd.io/etcd/client/v3" + etcdnaming "go.etcd.io/etcd/client/v3/naming/resolver" + + "google.golang.org/grpc" +) + +... + +cli, err := clientv3.NewFromURL("http://localhost:2379") +if err != nil { + // ... +} +r, err := etcdnaming.NewBuilder(cli) +if err != nil { + // ... +} +conn, gerr := grpc.NewClient("my-service", grpc.WithResolvers(r), ...) +``` + +### 11.2 endpoint 的组织格式 + +etcd resolver 把**解析目标前缀后跟一个 "/"** 下的所有 key(例如 `"foo/bar/my-service/"`)视为潜在服务端点,其值是 **JSON 编码**的(历史上为 go-grpc 的 `naming.Update`)。**创建 key 即加入 endpoint,删除 key 即移除 endpoint**。 + +关于"endpoint 格式",源文实际出现的两种 `grpc.NewClient` 目标写法: + +| 写法 | 出处 | +| ---- | ---- | +| `grpc.NewClient("my-service", grpc.WithResolvers(r), ...)` | 与 go-grpc 一起使用的初始化示例 | +| `grpc.NewClient("etcd:///foo", grpc.WithResolvers(etcdResolver), grpc.WithDefaultServiceConfig(...))` | 多 endpoint 轮询负载均衡示例 | + +> 说明:本手册不额外假设 `etcd:///` 之外的具体 scheme 语义;实际解析规则以源文所述"**解析目标前缀 + `/` 下的所有 key**"为准(例如目标前缀 `foo/bar/my-service` 对应 `foo/bar/my-service/` 下的所有 key)。 + +### 11.3 添加 endpoint + +etcdctl: + +```bash +ETCDCTL_API=3 etcdctl put foo/bar/my-service/1.2.3.4 '{"Addr":"1.2.3.4"}' +``` + +Go(`endpoints.Manager`,官方原文): + +> 💡 补充:以下代码引自官方页面原文(非本手册杜撰)——[gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/)。 + +```go +em := endpoints.NewManager(client, "foo/bar/my-service") +err := em.AddEndpoint(context.TODO(),"foo/bar/my-service/e1", endpoints.Endpoint{Addr:"1.2.3.4"}) +``` + +服务有多个 endpoint 时用 grpc 内置的 **round-robin** 负载均衡器: + +> 💡 补充:以下代码引自官方页面原文(非本手册杜撰)——[gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/)。 + +```go +conn, gerr := grpc.NewClient("etcd:///foo", grpc.WithResolvers(etcdResolver), +grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`)) +``` + +### 11.4 删除 endpoint + +```bash +ETCDCTL_API=3 etcdctl del foo/bar/my-service/1.2.3.4 +``` + +> 💡 补充:以下代码引自官方页面原文(非本手册杜撰)——[gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/)。 + +```go +em := endpoints.NewManager(client, "foo/bar/my-service") +err := em.DeleteEndpoint(context.TODO(), "foo/bar/my-service/e1") +``` + +### 11.5 带 lease 注册 endpoint + +带 lease 注册可保证:当主机无法维持 keepalive 心跳(例如机器故障)时,它会被从服务中移除。 + +```bash +lease=`ETCDCTL_API=3 etcdctl lease grant 5 | cut -f2 -d' '` +ETCDCTL_API=3 etcdctl put --lease=$lease my-service/1.2.3.4 '{"Addr":"1.2.3.4"}' +ETCDCTL_API=3 etcdctl lease keep-alive $lease +``` + +> 💡 补充:以下代码引自官方页面原文(非本手册杜撰)——[gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/)。 + +```go +em := endpoints.NewManager(client, "foo/bar/my-service") +err := em.AddEndpoint(context.TODO(), "foo/bar/my-service/e1", endpoints.Endpoint{Addr:"1.2.3.4"}) +``` + +> 说明:源文在"带 lease 注册"小节的 Go 侧写法与 11.3 的 `AddEndpoint` 完全相同;要让 key 随主机下线而消失,实践上需把该 key 绑到 lease 上(结合第 9 章的 lease 用法)。 + +### 11.6 在单个事务里原子更新 endpoints + +> 💡 补充:以下代码引自官方页面原文(非本手册杜撰);源文该代码块未标注语言,本手册按 Go 处理——[gRPC naming and discovery](https://etcd.io/docs/v3.7/dev-guide/grpc_naming/)。 + +```go +em := endpoints.NewManager(c, "foo") + +err := em.Update(context.TODO(), []*endpoints.UpdateWithOpts{ + endpoints.NewDeleteUpdateOpts("foo/bar/my-service/e1", endpoints.Endpoint{Addr: "1.2.3.4"}), + endpoints.NewAddUpdateOpts("foo/bar/my-service/e1", endpoints.Endpoint{Addr: "1.2.3.14"})}) +``` + +### 11.7 与 gRPC proxy 的关系 + +gRPC proxy 的 **`--resolver-prefix` 注册正是作为 etcd gRPC naming 的 endpoint provider**(见第 13 章),两者配套使用可让客户端自动发现一组 proxy endpoint。 + +--- + +## 12. gRPC-gateway api_grpc_gateway + +> 引自:[Why gRPC gateway](https://etcd.io/docs/v3.7/dev-guide/api_grpc_gateway/) + +### 12.1 作用 + +etcd v3 的消息协议使用 gRPC。etcd 项目提供基于 gRPC 的 Go client 和命令行工具 etcdctl。对于**没有 gRPC 支持的语言**,etcd 提供 JSON **gRPC gateway**:它是一个 RESTful 代理,把 **HTTP/JSON 请求翻译成 gRPC 消息**。 + +### 12.2 映射规则与注意事项 + +- gateway 接受针对 etcd protocol buffer 消息定义的 **JSON mapping**; +- 关键点:**`key` 和 `value` 字段被定义为 byte array,因此在 JSON 中必须 base64 编码**; +- 示例均用 `curl`,但任何 HTTP/JSON 客户端都同样可用; +- **gRPC-gateway 不支持使用 TLS Common Name 做认证**。 + +endpoint 路径随版本变化: + +| 版本 | 可用路径 | +| ---- | -------- | +| etcd v3.2 及之前 | 仅 `[CLIENT-URL]/v3alpha/*` | +| etcd v3.3 | `[CLIENT-URL]/v3beta/*`,同时保留 `[CLIENT-URL]/v3alpha/*` | +| etcd v3.4 | `[CLIENT-URL]/v3/*`,同时保留 `[CLIENT-URL]/v3beta/*`;**`[CLIENT-URL]/v3alpha/*` 已废弃** | +| etcd v3.5 及之后 | 仅 `[CLIENT-URL]/v3/*`;**`[CLIENT-URL]/v3beta/*` 已废弃** | + +### 12.3 Put 和 get keys + +用 `/v3/kv/range` 与 `/v3/kv/put` 读写 key(`foo` 的 base64 是 `Zm9v`,`bar` 是 `YmFy`): + +```bash +curl -L http://localhost:2379/v3/kv/put \ + -X POST -d '{"key": "Zm9v", "value": "YmFy"}' +# {"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"2","raft_term":"3"}} + +curl -L http://localhost:2379/v3/kv/range \ + -X POST -d '{"key": "Zm9v"}' +# {"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"2","raft_term":"3"},"kvs":[{"key":"Zm9v","create_revision":"2","mod_revision":"2","version":"1","value":"YmFy"}],"count":"1"} + +# get all keys prefixed with "foo" +curl -L http://localhost:2379/v3/kv/range \ + -X POST -d '{"key": "Zm9v", "range_end": "Zm9w"}' +# {"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"2","raft_term":"3"},"kvs":[{"key":"Zm9v","create_revision":"2","mod_revision":"2","version":"1","value":"YmFy"}],"count":"1"} +``` + +前缀查询通过 `range_end` 实现(`Zm9v` 的下一值 `Zm9w`,即 `foo` → `fop`)。 + +### 12.4 Watch keys + +用 `/v3/watch` 服务 watch key(注意用 `curl -N` 且后台运行): + +```bash +curl -N http://localhost:2379/v3/watch \ + -X POST -d '{"create_request": {"key":"Zm9v"} }' & +# {"result":{"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"1","raft_term":"2"},"created":true}} + +curl -L http://localhost:2379/v3/kv/put \ + -X POST -d '{"key": "Zm9v", "value": "YmFy"}' >/dev/null 2>&1 +# {"result":{"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"2","raft_term":"2"},"events":[{"kv":{"key":"Zm9v","create_revision":"2","mod_revision":"2","version":"1","value":"YmFy"}}]}} +``` + +### 12.5 Transactions + +用 `/v3/kv/txn` 发起事务。target 为 `CREATE`: + +```bash +curl -L http://localhost:2379/v3/kv/txn \ + -X POST \ + -d '{"compare":[{"target":"CREATE","key":"Zm9v","createRevision":"2"}],"success":[{"requestPut":{"key":"Zm9v","value":"YmFy"}}]}' +# {"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"3","raft_term":"2"},"succeeded":true,"responses":[{"response_put":{"header":{"revision":"3"}}}]} +``` + +target 为 `VERSION`: + +```bash +curl -L http://localhost:2379/v3/kv/txn \ + -X POST \ + -d '{"compare":[{"version":"4","result":"EQUAL","target":"VERSION","key":"Zm9v"}],"success":[{"requestRange":{"key":"Zm9v"}}]}' +# {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"6","raft_term":"3"},"succeeded":true,"responses":[{"response_range":{"header":{"revision":"6"},"kvs":[{"key":"Zm9v","create_revision":"2","mod_revision":"6","version":"4","value":"YmZ6"}],"count":"1"}}]} +``` + +### 12.6 Authentication + +用 `/v3/auth` 建立认证:创建 root 用户、创建 root 角色、授予角色、启用 auth。 + +```bash +# create root user +curl -L http://localhost:2379/v3/auth/user/add \ + -X POST -d '{"name": "root", "password": "pass"}' +# {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"1","raft_term":"2"}} + +# create root role +curl -L http://localhost:2379/v3/auth/role/add \ + -X POST -d '{"name": "root"}' +# {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"1","raft_term":"2"}} + +# grant root role +curl -L http://localhost:2379/v3/auth/user/grant \ + -X POST -d '{"user": "root", "role": "root"}' +# {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"1","raft_term":"2"}} + +# enable auth +curl -L http://localhost:2379/v3/auth/enable -X POST -d '{}' +# {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"1","raft_term":"2"}} +``` + +用 `/v3/auth/authenticate` 换取 token: + +```bash +curl -L http://localhost:2379/v3/auth/authenticate \ + -X POST -d '{"name": "root", "password": "pass"}' +# {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"1","raft_term":"2"},"token":"sssvIpwfnLAcWAQH.9"} +``` + +把 token 放进 `Authorization` 头即可带凭据访问: + +```bash +curl -L http://localhost:2379/v3/kv/put \ + -H 'Authorization: sssvIpwfnLAcWAQH.9' \ + -X POST -d '{"key": "Zm9v", "value": "YmFy"}' +# {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"2","raft_term":"2"}} +``` + +### 12.7 错误响应(v3.6 起变化) + +gRPC gateway 把 gRPC status 翻译为 HTTP 状态码和 JSON 错误体。**自 etcd v3.6 起,升级到 grpc-gateway v2 后错误处理发生变化**:行为对齐 `google.rpc.Status`(`code`、`message`、`details`),遵循 Google API error model。历史版本中还存在顶层 `error` 字段,但**该字段在 etcd v3.6 及以上不再支持**(v3.7 同样不支持)。 + +- 客户端应**以 HTTP 状态码作为成功/失败的主要判据**; +- 请求失败时应以 `message` 字段作为错误信息的主要来源,`details` 用于补充上下文。 + +### 12.8 Swagger + +生成的 Swagger API 定义位于 `rpc.swagger.json`(页面内链接为 `../apispec/swagger/rpc.swagger.json`)。 + +### 12.9 与 gRPC 的取舍 + +| 维度 | gRPC(clientv3 / 各语言 gRPC 客户端) | gRPC-gateway(HTTP/JSON) | +| ---- | ------------------------------------ | ------------------------- | +| 适用语言 | 有 gRPC 支持的语言:官方主推 Go `clientv3` | **没有 gRPC 支持的语言**;任何 HTTP/JSON 客户端(含 curl)都可用 | +| 传输效率 | 二进制 protobuf,带宽与 CPU 更省 | JSON + base64(key/value 必须 base64),**体积更大** | +| 流式能力 | Watch、LeaseKeepAlive 双向流原生支持 | `/v3/watch` 需 `curl -N` 长连接;keepalive 流不友好 | +| 类型保真 | protobuf 原生类型(int64 等) | JSON 数字为字符串(如 `"revision":"2"`),需按字符串解析大整数 | +| 认证 | 支持完整 `Auth` service 与 mTLS | **不支持使用 TLS Common Name 做认证**;token 用 `Authorization` 头传递 | +| 错误判据 | gRPC status code | HTTP 状态码为主 + `message` / `details` | +| 一致性 | 默认 linearizable,可选 serializable | 由后端 gRPC 调用决定,语义一致 | +| 典型用法 | 服务端应用、控制面组件 | 脚本、运维平台、无 gRPC 栈的语言、快速联调 | + +### 12.10 从 curl 联调到生产客户端 + +> 工程建议(非官方原文,依据 gRPC-gateway 的映射规则与注意事项推导)。 + +- **base64 是硬要求**:`key` 与 `value` 在 JSON 里必须是 base64,忘记编码会得到"看起来没报错但数据不对"的结果;注意是**标准 base64**,不要与 URL-safe 变体混用。 +- **整数是字符串**:响应里的 `revision`、`cluster_id`、`member_id`、`count` 都是字符串(如 `"revision":"2"`)。它们本该是 64 位整数,JSON 数字精度不够,客户端必须用能处理大整数的类型解析,不能当 32 位 int 处理。 +- **错误判据变了**:v3.6 起对齐 `google.rpc.Status`,历史版本里的顶层 `error` 字段**已不再支持**;客户端要以 HTTP 状态码为主、`message` 为辅、`details` 补充上下文。 +- **不要指望 Common Name 认证**:gRPC-gateway 不支持使用 TLS Common Name 做认证,要走 etcd 的 `Auth` service 拿 token,放进 `Authorization` 头。 +- **watch 要用长连接**:`/v3/watch` 用 `curl -N` 且通常后台运行;生产环境如果既要 watch 又要 keepalive 双向流,还是回到 gRPC 客户端更稳妥。 +- **路径随版本变**:v3.5 及之后只有 `[CLIENT-URL]/v3/*`,`v3beta` / `v3alpha` 已废弃,别把旧脚本直接搬上来。 + +--- + +## 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/) + +两者都能让客户端"少操心 endpoint",但工作层与能力完全不同。 + +| 维度 | etcd gateway | gRPC proxy | +| ---- | ------------ | ---------- | +| 工作层 | **TCP 层**(简单 TCP 代理) | **gRPC 层(L7)**,无状态反向代理 | +| 是否检查请求 | **无状态且透明**:既不检查客户端请求,也不干预集群响应 | 理解 gRPC 语义,可合并与缓存 | +| TLS | **不终止 TLS**、不代客户端做 TLS 握手、也不校验连接是否安全 | **可终止**来自安全集群的 TLS,对外提供未加密本地 endpoint | +| endpoint 选择 | 支持多个 endpoint,**简单轮询(round-robin)**;只路由到可用 endpoint | 启动时**随机挑选**一个 endpoint 服务全部请求,故障时才切换 | +| 能力 | 仅转发 | **合并 watch**(N→1)、**合并 lease stream**(N→1)、**缓存 key range 请求** | +| 目标 | 稳定的本地 endpoint,隐藏集群变更 | 降低核心集群总处理负载、提升可扩展性、防滥用客户端 | +| 不支持 | 缓存、watch 合并、批处理 | `RangeStream`;对未指定 revision 的 watch 不保证从最新 revision 开始 | + +### 13.1 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 启动与 flag + +静态 endpoint: + +```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 做服务发现(先有 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 +``` + +```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 | `--endpoints` | `127.0.0.1:2379` | 转发客户端连接的目标,逗号分隔;**必须带端口**;非法示例 `https://127.0.0.1:2379`(gateway 不终止 TLS) | +| etcd cluster | `--discovery-srv` | 未设置 | 通过 SRV 记录引导集群 endpoint 的 DNS 域名 | +| Network | `--listen-addr` | `127.0.0.1:23790` | 接受客户端请求所绑定的接口与端口 | +| Network | `--retry-delay` | `1m0s` | 重连失败 endpoint 前的延迟;非法示例 `"123"`(需带时间单位) | +| Security | `--insecure-discovery` | `false` | 接受不安全或易受中间人攻击的 SRV 记录 | +| Security | `--trusted-ca-file` | 未设置 | 客户端 TLS CA 文件路径,用于校验 SRV 发现返回的 endpoint;**仅**用于认证被发现的 endpoint,不用于创建数据传输连接(gateway 永不终止 TLS) | + +### 13.3 gRPC proxy:启动与基本使用 + +```bash +$ etcd grpc-proxy start --endpoints=infra0.example.com,infra1.example.com,infra2.example.com --listen-addr=127.0.0.1:2379 +``` + +```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 +``` + +### 13.4 可代理的 API 范围与一致性注意事项 + +| 能力 | 官方说明 | 一致性影响 | +| ---- | -------- | ---------- | +| 可扩展 watch | proxy 把同一 key/range 上的多个客户端 watcher(`c-watcher`)合并为连到 etcd server 的单个 watcher(`s-watcher`),再广播事件。N 个客户端 watch 同一 key 时,把 server 上 watch 负载从 N 降到 1 | **当 watch revision 未指定时,proxy 不保证 `c-watcher` 从最新的 store revision 开始 watch**:直连可能从 revision 1000 开始,经 proxy 可能从 990 开始。取消操作有类似限制:watcher 被取消时,etcd server 的 revision 可能大于取消响应的 revision | +| 可扩展 lease | 支持 **lease stream 合并**:N 个客户端更新 lease 时,把 server 上的流负载从 N 降到 1 | 依赖 proxy 的转发时延,TTL 取值需留出余量 | +| 防滥用客户端 | 在**不破坏一致性要求**的前提下缓存请求响应,保护 server 免受紧循环(tight for loop)客户端冲击 | 缓存不会破坏一致性 | +| `RangeStream` | **不支持** | 使用 `--stream` 的客户端不要走 proxy | +| 批量/写事务 | 源文未逐项列出可代理的 RPC 清单;proxy 是 L7 反向代理,理解 gRPC 语义 | 未列出部分不做推断 | + +> 官方提示:watch revision 与取消 revision 的两点偏差"对多数场景无碍",将来可能提供让 watcher 绕过 proxy 以获得更精确 revision 的选项。若业务强依赖精确 revision(如做增量同步),应直连 etcd 或显式指定 `start_revision`。 + +### 13.5 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: + +> 💡 补充:以下代码引自官方页面原文(非本手册杜撰)——[gRPC proxy](https://etcd.io/docs/v3.7/op-guide/grpc_proxy/)。 + +```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,`member list` 只返回它自己的 `advertise-client-url`: + +```bash +$ etcd grpc-proxy start --endpoints=localhost:2379 \ + --listen-addr=127.0.0.1:23792 \ + --advertise-client-url=127.0.0.1:23792 +``` + +### 13.6 Namespacing + +当应用期望对**整个 key space** 拥有完全控制权、而 etcd 集群又被其他应用共享时,可用 `--namespace` 让 proxy 对 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 +``` + +### 13.7 TLS 终止 + +先启动 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 +``` + +```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 & +``` + +```bash +$ ETCDCTL_API=3 etcdctl --endpoints=http://localhost:12379 put abc def +# OK +``` + +### 13.8 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 +``` + +### 13.9 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`。 + +### 13.10 客户端如何配置 + +| 场景 | 客户端配置 | +| ---- | ---------- | +| 走 etcd gateway | `Endpoints` 填 gateway 的 `--listen-addr`(默认 `127.0.0.1:23790`);etcdctl 用 `--endpoints=127.0.0.1:23790`。TLS 由客户端与 etcd server 端到端建立,gateway 不参与 | +| 走 gRPC proxy(固定地址) | `Endpoints` 填 proxy 的 `--listen-addr`;etcdctl 用 `ETCDCTL_API=3 etcdctl --endpoints=127.0.0.1:23790 ...` | +| 走 gRPC proxy(自动发现) | 启动时带 `--resolver-prefix` / `--resolver-ttl` / `--advertise-client-url`,客户端连任一 proxy 后调用 `cli.Sync(ctx)`(见 13.5) | +| 需要 keyspace 隔离 | 用 `--namespace=my-prefix/`,客户端完全无感 | +| 强依赖精确 watch revision 或使用 `RangeStream` | **不要用 proxy**(见 13.4),直连 etcd | + +### 13.11 接入方式选型建议 + +> 工程建议(非官方原文,依据两页的"何时使用 / 何时不要用"与能力边界推导)。 + +| 你的情况 | 建议 | +| -------- | ---- | +| 一台机器上跑多个访问同一 etcd 集群的应用,不想每个应用都维护 endpoint 列表 | **etcd gateway**:每机一个,应用连 `127.0.0.1:23790`,集群变更只改 gateway | +| 已经跑在 Kubernetes 上 | 都不要:直接用 DNS 名或系统托管的虚拟 IP(kube-proxy 就等价于 gateway) | +| 想提升性能 / 降低集群负载 / watch 客户端特别多 | **gRPC proxy**(watch 与 lease 合并、range 缓存);gateway 明确不是为性能设计的 | +| 需要 watch revision 精确到"从最新 store revision 开始" | **不要走 proxy**(未指定 revision 时可能从更早的 revision 开始),直连 | +| 客户端用了 `RangeStream`(etcdctl `--stream`) | **不要走 proxy**(proxy 不支持) | +| 多应用共享一个 etcd 集群,想各自独占 keyspace | **gRPC proxy 的 `--namespace`**,客户端无感 | +| 应用是 Go,需要服务注册发现 | `grpc_naming` resolver(**experimental**)+ proxy 的 `--resolver-prefix` | +| etcd 集群开了 TLS,本机应用不想配证书 | **gRPC proxy 做 TLS 终止**,对外提供未加密本地 endpoint | + +兜底原则:gateway 解决"地址管理",proxy 解决"规模与隔离",两者都不解决"一致性增强"——任何代理都不会让读变得更强一致,只会引入额外的 revision 偏差风险。 + +--- + +## 14. 在 Go 应用里嵌入 etcd golang_embed_pkg + +> 引自:[Embedding etcd in a Go Application](https://etcd.io/docs/v3.7/dev-guide/golang_embed_pkg/) + +**如实说明**:该官方页面正文(去除 front matter 后)**只有一段话**: + +> etcd 的 `embed` Go package 提供了一种把 etcd server **直接嵌入应用程序**的简单方式。更多细节见 embed 包文档:https://pkg.go.dev/go.etcd.io/etcd/server/v3/embed + +页面本身**未包含** `embed.New(embed.Config{...})` 代码骨架,也**未列出** `Config` 的任何字段。同目录(`dev-guide`)下没有其他 embed 相关文档页面。因此本册不把它当成有内容的章节来写,字段级信息一律以 pkg.go.dev 为准。 + +典型使用场景(工程化归纳,非官方原文):单元测试与集成测试里起一个真实 etcd、单机工具/边缘场景希望零外部依赖、需要把 etcd 作为库发布。 + +> 💡 补充:以下骨架基于官方"embed 包提供直接嵌入方式"这一句语义编写,**字段名与默认值以 https://pkg.go.dev/go.etcd.io/etcd/server/v3/embed 为准**,官方页面未给出。 + +```go +import ( + "time" + + "go.etcd.io/etcd/server/v3/embed" +) + +cfg := embed.NewConfig() // 建议从 NewConfig() 的默认值出发,避免漏字段 +cfg.Dir = "/tmp/etcd-embed-data" // 数据目录(含 member/snap、member/wal) +cfg.LogLevel = "info" + +// 监听地址必须能被客户端与 peer 访问 +// cfg.ListenClientUrls / cfg.ListenPeerUrls / cfg.AdvertiseClientUrls / cfg.AdvertisePeerUrls +// cfg.Name / cfg.InitialCluster + +e, err := embed.StartEtcd(cfg) +if err != nil { + log.Fatal(err) +} +defer e.Close() + +select { +case <-e.Server.ReadyNotify(): + log.Print("etcd server is ready") +case <-time.After(60 * time.Second): + e.Server.Stop() // 超时未就绪,按官方建议直接停 + log.Fatal("etcd took too long to start") +} +``` + +> 注意:`embed` 属于 `go.etcd.io/etcd/server/v3` module,而该 module 的代码属于 etcd **内部实现**,"**不应被外部项目消费**",包布局与 API 可能在小版本之间变化(见第 15 章)。生产环境请权衡这一风险。 + +--- + +## 15. Go 模块划分 dev-internal-modules + +> 引自:[Golang modules](https://etcd.io/docs/v3.7/dev-internal/modules/) + +etcd 项目**自 3.5 版本起**被组织为多个 golang module,托管在**单一仓库**中。 + +### 15.1 各 module 路径与职责 + +| module | 职责 | 对应用开发者的意义 | +| ------ | ---- | ------------------ | +| `go.etcd.io/etcd/api/v3` | API 定义(protos 及 proto 生成的库),定义客户端与服务端之间的通信协议 | 只用消息类型时引它,最轻 | +| `go.etcd.io/etcd/pkg/v3` | etcd 使用的工具包集合,本身不特定于 etcd;一个包只有**未来可能被移出到独立仓库**时才应放这里;**避免**往这里放自身依赖很重的代码(会自动成为 client 库的依赖,client 库要保持轻量) | 间接依赖,不建议直接引 | +| `go.etcd.io/etcd/client/v3` | 通过网络(grpc)访问 etcd 的客户端库,**所有新的 etcd 用法都推荐使用它** | **本册所有 Go 示例使用的 module** | +| `go.etcd.io/etcd/client/v2` | 通过 HTTP 协议访问 etcd 的**遗留**客户端库,**已废弃**;所有新用法应依赖 /v3 库 | 不要用 | +| `go.etcd.io/etcd/raft/v3` | 分布式一致性协议的实现,**不应包含 etcd 特定代码** | 想单独用 raft 时引它 | +| `go.etcd.io/etcd/server/v3` | etcd 实现。该包中的代码属于 etcd **内部**,**不应被外部项目消费**;包布局和 API **可能在小版本之间变化** | 含 `embed`;外部项目慎用 | +| `go.etcd.io/etcd/etcdctl/v3` | 访问和管理 etcd 的命令行工具 | — | +| `go.etcd.io/etcd/tests/v3` | 包含 etcd 所有集成测试的模块。注意:所有单元测试(快速且不需要跨模块依赖的)都应保留在被测试代码所在的本地模块中 | — | +| `go.etcd.io/bbolt` | 持久化 b-tree 实现,托管在独立仓库 https://github.com/etcd-io/bbolt | 后端存储实现 | + +### 15.2 Operations(操作规范) + +1. 所有 etcd module 应**以相同版本发布**,例如 `go.etcd.io/etcd/client/v3@v3.5.10` 必须依赖 `go.etcd.io/etcd/api/v3@v3.5.10`。版本一致性更新脚本: + + ```bash + % DRY_RUN=false TARGET_VERSION="v3.5.10" ./scripts/release_mod.sh update_versions + ``` + +2. 已发布的 module 应按 https://golang.org/ref/mod#vcs-version 规则打 tag,即**每个 module 有自己的 tag**。打 tag 脚本: + + ```bash + % DRY_RUN=false REMOTE_REPO="origin" ./scripts/release_mod.sh push_mod_tags + ``` + +3. 所有 etcd module 应依赖**相同版本的底层依赖**。校验方式: + + ```bash + % PASSES="dep" ./test.sh + ``` + +4. `go.mod` 文件**不得包含未被使用的依赖**,且必须符合 `go mod tidy` 格式。校验方式: + + ```bash + % PASSES="mod_tidy" ./test.sh + ``` + +5. 要在所有 module 上触发动作(例如自动格式化所有文件),请使用/扩展脚本: + + ```bash + % ./scripts/fix.sh + ``` + +### 15.3 Future(演进方向) + +- 把 **etcdmigrate/etcdadm** 从 etcdctl 二进制中拆出去,使 etcdctl 明确成为网络 client API 的命令行封装,而 etcdmigrate/etcdadm 支持对 etcd 存储文件的直接物理操作; +- 把 **etcd-proxy** 从 `./etcd` 二进制中拆出去,因为它包含更多实验性代码,带来额外风险与依赖; +- **废弃 v2 协议**支持。 + +> 依赖规则小结:应用只应依赖 `client/v3`(必要时加 `api/v3`);看到 `server/v3`、`client/v2` 出现在业务 `go.mod` 里,就是这个模块划分要阻止的用法。 + +--- + +## 16. gRPC API 速查表 + +> 引自:[API reference](https://etcd.io/docs/v3.7/dev-guide/api_reference_v3/)、[API reference: concurrency](https://etcd.io/docs/v3.7/dev-guide/api_concurrency_reference_v3/) +> 说明:两份源文均标注 "This API reference is autogenerated from the named `.proto` files."。本节为**全量速查**:8 个 service、48 个方法;非核心细节(枚举全部取值、gRPC code 与 etcd 错误对照)官方文档未提供,见末尾 16.10。 + +### 16.1 service 总清单 + +| # | Service | proto 文件 | 方法数 | +| - | ------- | ---------- | ------ | +| 1 | `KV` | api/etcdserverpb/rpc.proto | 5 | +| 2 | `Watch` | api/etcdserverpb/rpc.proto | 1 | +| 3 | `Lease` | api/etcdserverpb/rpc.proto | 5 | +| 4 | `Cluster` | api/etcdserverpb/rpc.proto | 5 | +| 5 | `Maintenance` | api/etcdserverpb/rpc.proto | 8 | +| 6 | `Auth` | api/etcdserverpb/rpc.proto | 17 | +| 7 | `Lock` | server/etcdserver/api/v3lock/v3lockpb/v3lock.proto | 2 | +| 8 | `Election` | server/etcdserver/api/v3election/v3electionpb/v3election.proto | 5 | + +**合计:8 个 service,48 个方法。** `Election` 与 `Lock` 只在 concurrency 参考页出现;`KV` / `Watch` / `Lease` / `Cluster` / `Maintenance` / `Auth` 只在 API reference 页出现。 + +### 16.2 service `KV` + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `Range` | RangeRequest | RangeResponse | 从 kv store 读取区间内的 keys | +| `Put` | PutRequest | PutResponse | 写入给定 key;使 store revision +1 并产生一个事件 | +| `DeleteRange` | DeleteRangeRequest | DeleteRangeResponse | 删除给定区间的 key;revision +1,每个被删 key 产生一个 delete 事件 | +| `Txn` | TxnRequest | TxnResponse | 在单个事务里处理多个请求,完成后产生同一 revision 的事件;不允许同一 txn 内多次修改同一 key | +| `Compact` | CompactionRequest | CompactionResponse | 压缩事件历史;需定期压缩否则历史无限增长 | + +`RangeRequest` 字段(完整)见 4.3;`PutRequest` / `PutResponse` 见 3.2;`DeleteRange` 见 6.3;`Txn` / `Compare` 见 7.5、7.4。 + +`CompactionRequest`: + +| 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ------ | +| `revision` | int64 | 压缩操作的 kv store revision;所有 revision 小于它的被取代 key 会被移除 | — | +| `physical` | bool | RPC 等待压缩被物理应用到本地数据库,压缩条目从后端彻底删除 | `false` | + +`CompactionResponse`:`header`。 + +### 16.3 service `Watch` + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `Watch` | WatchRequest(流) | WatchResponse(流) | 监听正在发生或已发生的事件;输入流用于创建/取消 watcher,输出流发送事件;一次 RPC 可监听多个 key 区间,可从最后压缩 revision 起监听整个事件历史 | + +| 消息 | 字段 | 类型 | 说明 | +| ---- | ---- | ---- | ---- | +| WatchRequest | `request_union` | oneof | 创建 / 取消 / progress 请求联合 | +| WatchRequest | `create_request` | WatchCreateRequest | 创建 | +| WatchRequest | `cancel_request` | WatchCancelRequest | 取消 | +| WatchRequest | `progress_request` | WatchProgressRequest | 请求尽快在响应流中发送 watch 流 progress 状态(该消息无业务字段) | +| WatchCancelRequest | `watch_id` | int64 | 要取消的 watcher id,取消后不再传输事件 | + +`WatchCreateRequest` 字段见 8.4;`WatchResponse` 字段见 8.5。 + +### 16.4 service `Lease` + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `LeaseGrant` | LeaseGrantRequest | LeaseGrantResponse | 创建租约;TTL 内未收到 keepAlive 则过期,附加的 key 全部过期删除,每个过期 key 产生 delete 事件 | +| `LeaseRevoke` | LeaseRevokeRequest | LeaseRevokeResponse | 撤销租约,附加的 key 全部过期删除 | +| `LeaseKeepAlive` | LeaseKeepAliveRequest(流) | LeaseKeepAliveResponse(流) | 客户端→服务端流式 keepAlive 请求、服务端→客户端流式响应的双向流保活 | +| `LeaseTimeToLive` | LeaseTimeToLiveRequest | LeaseTimeToLiveResponse | 查询租约信息 | +| `LeaseLeases` | LeaseLeasesRequest | LeaseLeasesResponse | 列出所有现存租约(请求无业务字段) | + +字段表见 9.4。 + +### 16.5 service `Cluster` + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `MemberAdd` | MemberAddRequest | MemberAddResponse | 向集群添加成员 | +| `MemberRemove` | MemberRemoveRequest | MemberRemoveResponse | 从集群移除已有成员 | +| `MemberUpdate` | MemberUpdateRequest | MemberUpdateResponse | 更新成员配置 | +| `MemberList` | MemberListRequest | MemberListResponse | 列出集群所有成员 | +| `MemberPromote` | MemberPromoteRequest | MemberPromoteResponse | 将成员从 raft learner(无投票权)提升为 raft 投票成员 | + +| 消息 | 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ---- | ------ | +| MemberAddRequest | `peerURLs` | (slice of) string | 新成员与集群通信使用的 URL 列表 | — | +| MemberAddRequest | `isLearner` | bool | 新成员是否为 raft learner | `false` | +| MemberRemoveRequest | `ID` | uint64 | 要移除的成员 ID | — | +| MemberUpdateRequest | `ID` | uint64 | 要更新的成员 ID | — | +| MemberUpdateRequest | `peerURLs` | (slice of) string | 该成员新的通信 URL 列表 | — | +| MemberPromoteRequest | `ID` | uint64 | 要提升的成员 ID | — | +| MemberListRequest | `linearizable` | bool | 是否线性化读 | `false` | + +`Member` 消息字段: + +| 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ------ | +| `ID` | uint64 | 成员 ID | — | +| `name` | string | 人类可读的成员名;成员未启动时为空字符串 | `""` | +| `peerURLs` | (slice of) string | 成员暴露给集群用于通信的 URL 列表 | — | +| `clientURLs` | (slice of) string | 成员暴露给客户端的 URL 列表;未启动时为空 | — | +| `isLearner` | bool | 是否为 raft learner | `false` | + +响应:`MemberAddResponse` = `header` + `member` + `members`(添加后全量);`MemberRemoveResponse` / `MemberUpdateResponse` / `MemberPromoteResponse` = `header` + `members`;`MemberListResponse` = `header` + `members`。 + +### 16.6 service `Maintenance` + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `Alarm` | AlarmRequest | AlarmResponse | 激活、去激活、查询集群健康告警 | +| `Status` | StatusRequest | StatusResponse | 获取成员状态(请求无业务字段) | +| `Defragment` | DefragmentRequest | DefragmentResponse | 碎片整理成员后端数据库以回收存储空间(请求无业务字段) | +| `Hash` | HashRequest | HashResponse | 计算整个后端 keyspace(含 key、lease 等 bucket)的哈希;**仅供测试**,不持有 MVCC 锁,勿用于生产(请求无业务字段) | +| `HashKV` | HashKVRequest | HashKVResponse | 计算给定 revision 前所有 MVCC key 的哈希,只遍历 "key" bucket | +| `Snapshot` | SnapshotRequest | SnapshotResponse | 以流方式将成员整个后端快照发给客户端(请求无业务字段) | +| `MoveLeader` | MoveLeaderRequest | MoveLeaderResponse | 请求当前 leader 将领导权移交给 transferee | +| `Downgrade` | DowngradeRequest | DowngradeResponse | 请求降级、校验可行性或取消集群版本降级;自 etcd 3.5 起支持 | + +| 消息 | 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ---- | ------ | +| AlarmRequest | `action` | AlarmAction | GET 告警状态 / ACTIVATE 激活 / DEACTIVATE 去激活 | — | +| AlarmRequest | `memberID` | uint64 | 关联告警的成员 ID | `0` = 覆盖所有成员 | +| AlarmRequest | `alarm` | AlarmType | 本次请求考虑的告警类型 | — | +| HashKVRequest | `revision` | int64 | 哈希操作的 kv store revision | — | +| MoveLeaderRequest | `targetID` | uint64 | 新 leader 的节点 ID | — | +| DowngradeRequest | `action` | DowngradeAction | VALIDATE 目标版本 / DOWNGRADE 集群版本 / CANCEL 当前降级任务 | — | +| DowngradeRequest | `version` | string | 降级的目标版本 | — | + +`StatusResponse`(常用): + +| 字段 | 类型 | 说明 | +| ---- | ---- | ---- | +| `header` | ResponseHeader | — | +| `version` | string | 响应成员使用的集群协议版本 | +| `dbSize` | int64 | 后端数据库物理分配大小(字节) | +| `leader` | uint64 | 响应成员认为的当前 leader 成员 ID | +| `raftIndex` | uint64 | 当前 raft committed index | +| `raftTerm` | uint64 | 当前 raft term | +| `raftAppliedIndex` | uint64 | 当前 raft applied index | +| `errors` | (slice of) string | 告警/健康信息与状态 | +| `dbSizeInUse` | int64 | 后端数据库逻辑使用大小(字节) | +| `isLearner` | bool | 是否为 raft learner | +| `storageVersion` | string | db 文件版本,可能滞后于目标集群版本 | +| `dbSizeQuota` | int64 | 配置的存储配额字节数(`--quota-backend-bytes` 传入值) | +| `downgradeInfo` | DowngradeInfo | 是否存在降级流程 | + +其他响应:`AlarmResponse` = `header` + `alarms`(`AlarmMember`:`memberID` uint64、`alarm` AlarmType);`HashResponse` = `header` + `hash`(uint32);`HashKVResponse` = `header` + `hash` + `compact_revision` + `hash_revision`;`DowngradeResponse` = `header` + `version`;`DowngradeInfo` = `enabled`(bool)+ `targetVersion`(string);`SnapshotResponse` = `header`(首条表示快照时间点)+ `remaining_bytes`(uint64)+ `blob`(bytes)+ `version`(string)。 + +### 16.7 service `Auth` + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `AuthEnable` | AuthEnableRequest | AuthEnableResponse | 启用认证(请求无业务字段) | +| `AuthDisable` | AuthDisableRequest | AuthDisableResponse | 关闭认证(请求无业务字段) | +| `AuthStatus` | AuthStatusRequest | AuthStatusResponse | 展示认证状态(请求无业务字段) | +| `Authenticate` | AuthenticateRequest | AuthenticateResponse | 处理认证请求 | +| `UserAdd` | AuthUserAddRequest | AuthUserAddResponse | 新增用户;**用户名不能为空** | +| `UserGet` | AuthUserGetRequest | AuthUserGetResponse | 获取用户详细信息 | +| `UserList` | AuthUserListRequest | AuthUserListResponse | 获取所有用户列表(请求无业务字段) | +| `UserDelete` | AuthUserDeleteRequest | AuthUserDeleteResponse | 删除指定用户 | +| `UserChangePassword` | AuthUserChangePasswordRequest | AuthUserChangePasswordResponse | 修改指定用户密码 | +| `UserGrantRole` | AuthUserGrantRoleRequest | AuthUserGrantRoleResponse | 授予指定用户一个角色 | +| `UserRevokeRole` | AuthUserRevokeRoleRequest | AuthUserRevokeRoleResponse | 撤销指定用户的一个角色 | +| `RoleAdd` | AuthRoleAddRequest | AuthRoleAddResponse | 新增角色;**角色名不能为空** | +| `RoleGet` | AuthRoleGetRequest | AuthRoleGetResponse | 获取角色详细信息 | +| `RoleList` | AuthRoleListRequest | AuthRoleListResponse | 获取所有角色列表(请求无业务字段) | +| `RoleDelete` | AuthRoleDeleteRequest | AuthRoleDeleteResponse | 删除指定角色 | +| `RoleGrantPermission` | AuthRoleGrantPermissionRequest | AuthRoleGrantPermissionResponse | 授予角色某个 key 或区间的权限 | +| `RoleRevokePermission` | AuthRoleRevokePermissionRequest | AuthRoleRevokePermissionResponse | 撤销角色某个 key 或区间的权限 | + +| 消息 | 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ---- | ------ | +| AuthenticateRequest | `name` / `password` | string | 用户名 / 密码 | — | +| AuthenticateResponse | `header` / `token` | ResponseHeader / string | `token` 为授权 token,可用于后续 RPC | — | +| AuthStatusResponse | `enabled` | bool | 认证是否启用 | `false` | +| AuthStatusResponse | `authRevision` | uint64 | auth store 当前 revision | — | +| AuthUserAddRequest | `name` / `password` / `options` / `hashedPassword` | string / string / authpb.UserAddOptions / string | 用户名 / 密码 / 创建选项 / 已哈希密码 | — | +| AuthUserChangePasswordRequest | `name` / `password` / `hashedPassword` | string | 待改密的用户名 / 新密码(**该字段将在 API 层被移除**)/ 新密码(**该字段将在 API 层被初始化**) | — | +| AuthUserDeleteRequest | `name` | string | 要删除的用户名 | — | +| AuthUserGrantRoleRequest | `user` / `role` | string | 被授予角色的用户名 / 要授予的角色名 | — | +| AuthUserRevokeRoleRequest | `name` / `role` | string | 用户名 / 角色名 | — | +| AuthUserGetResponse | `roles` | (slice of) string | 用户的角色列表 | — | +| AuthUserListResponse | `users` | (slice of) string | 全部用户名 | — | +| AuthRoleAddRequest | `name` | string | 要加入认证系统的角色名 | — | +| AuthRoleDeleteRequest / AuthRoleGetRequest | `role` | string | 角色名 | — | +| AuthRoleGetResponse | `perm` | (slice of) authpb.Permission | 角色权限列表 | — | +| AuthRoleListResponse | `roles` | (slice of) string | 全部角色名 | — | +| AuthRoleGrantPermissionRequest | `name` / `perm` | string / authpb.Permission | 被授予权限的角色名 / 要授予的权限 | — | +| AuthRoleRevokePermissionRequest | `role` / `key` / `range_end` | string / bytes / bytes | 角色名 / 权限 key / 权限区间尾后 key | — | + +其余 Auth 响应消息(AuthEnableResponse、AuthDisableResponse、AuthUserAddResponse、AuthUserChangePasswordResponse、AuthUserDeleteResponse、AuthUserGrantRoleResponse、AuthUserRevokeRoleResponse、AuthRoleAddResponse、AuthRoleDeleteResponse、AuthRoleGrantPermissionResponse、AuthRoleRevokePermissionResponse)**均只含** `header`(ResponseHeader)。 + +关联的 authpb 消息: + +| 消息 | 字段 | 类型 | 说明 | +| ---- | ---- | ---- | ---- | +| Permission | `permType` / `key` / `range_end` | Type / bytes / bytes | 权限类型(单一实体)/ 权限 key / 权限区间尾后 key | +| Role | `name` / `keyPermission` | bytes / (slice of) Permission | authRoles bucket 中的单条记录 / 该角色的权限列表 | +| User | `name` / `password` / `roles` / `options` | bytes / bytes / (slice of) string / UserAddOptions | authUsers bucket 中的单条记录 | +| UserAddOptions | `no_password` | bool | 无密码标志 | + +### 16.8 service `Lock`(concurrency) + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `Lock` | LockRequest | LockResponse | 在给定命名的锁上获取分布式共享锁 | +| `Unlock` | UnlockRequest | UnlockResponse | 传入 Lock 返回的 key 释放锁 | + +字段与语义见 10.2。 + +### 16.9 service `Election`(concurrency) + +| 方法 | 请求消息 | 响应消息 | 用途 | +| ---- | -------- | -------- | ---- | +| `Campaign` | CampaignRequest | CampaignResponse | 等待获取选举的领导权 | +| `Proclaim` | ProclaimRequest | ProclaimResponse | 用新值更新 leader 已发布的值 | +| `Leader` | LeaderRequest | LeaderResponse | 返回当前选举公告(若有) | +| `Observe` | LeaderRequest | LeaderResponse | 按当选 leader 发布顺序流式推送选举公告 | +| `Resign` | ResignRequest | ResignResponse | 释放选举领导权 | + +字段与语义见 10.3。 + +### 16.10 跨 service 公共消息 + +| 消息 | 字段 | 类型 | 说明 | 默认值 | +| ---- | ---- | ---- | ---- | ------ | +| ResponseHeader | `cluster_id` | uint64 | 发送响应的集群 ID | — | +| ResponseHeader | `member_id` | uint64 | 发送响应的成员 ID | — | +| ResponseHeader | `revision` | int64 | 请求被应用时的 kv store revision;未与 kv store 交互的调用中未设置(即 0);**watch progress 响应中 header.revision 表示进度**,该流后续收到的所有事件 revision 都保证大于它 | 0 | +| ResponseHeader | `raft_term` | uint64 | 请求被应用时的 raft term | — | +| KeyValue | `key` | bytes | key 字节;**不允许为空** | — | +| KeyValue | `create_revision` | int64 | 该 key 最后一次创建的 revision | — | +| KeyValue | `mod_revision` | int64 | 该 key 最后一次修改的 revision | — | +| KeyValue | `version` | int64 | key 的版本;删除会将其重置为 0,任何修改使其递增 | — | +| KeyValue | `value` | bytes | key 持有的 value | — | +| KeyValue | `lease` | int64 | 附加到 key 的 lease ID;lease 过期时 key 被删除 | `0` = 无 lease | +| Event | `type` | EventType | PUT 表示有新数据存入该 key,DELETE 表示 key 被删除 | — | +| Event | `kv` | KeyValue | PUT 含当前 kv 对(`kv.Version=1` 表示 key 被创建);DELETE/EXPIRE 含被删 key,其 mod_revision 设为删除时的 revision | — | +| Event | `prev_kv` | KeyValue | 事件发生前的 kv 对 | — | +| Lease | `ID` / `TTL` / `RemainingTTL` | int64 | lease ID / TTL / 剩余 TTL | — | + +### 16.11 源文未包含的内容(不凭知识补充) + +- 两份源文均**无** gRPC code 与 etcd 错误的对照表; +- 均**无** enum 的具体成员值定义(`SortOrder`、`SortTarget`、`CompareResult`、`CompareTarget`、`AlarmType`、`AlarmAction`、`FilterType`、`EventType`、`DowngradeAction`、`Type` 等;本册出现的枚举取值均来自 `learning/api.md`,已在正文中标注出处); +- 均**无** gRPC-gateway HTTP 映射(见第 12 章,来自 `api_grpc_gateway.md`); +- 均**无**代码示例。 + +源文中与错误/异常相关的全部原文表述仅 4 处: + +| 出处 | 原文表述 | +| ---- | -------- | +| `RangeRequest.revision` | 若该 revision 已被压缩,返回 `ErrCompacted` 作为响应 | +| `LeaseGrantRequest.TTL` | 过期的 lease 将返回 -1(`Expired lease will return -1`) | +| `LeaseGrantResponse.error` | string 类型的 `error` 字段(源文未给取值说明) | +| `WatchResponse.cancel_reason` | string 类型,指示 watcher 被取消的原因(源文未给取值说明) | + +--- + +## 17. 附录 官方页面与章节映射表 + +| # | 官方页面 | 线上 URL | 本册章节 | +| - | -------- | -------- | -------- | +| 1 | Developer Tasks(目录页,仅 front matter,无正文) | https://etcd.io/docs/v3.7/tasks/developer/ | 1 | +| 2 | Writing to etcd | https://etcd.io/docs/v3.7/tasks/developer/writing-to-etcd/ | 3 | +| 3 | Reading from etcd | https://etcd.io/docs/v3.7/tasks/developer/reading-from-etcd/ | 4 | +| 4 | How to get keys by prefix | https://etcd.io/docs/v3.7/tasks/developer/how-to-get-key-by-prefix/ | 5 | +| 5 | How to delete keys | https://etcd.io/docs/v3.7/tasks/developer/how-to-delete-keys/ | 6 | +| 6 | How to make multiple writes in a transaction | https://etcd.io/docs/v3.7/tasks/developer/how-to-transactional-write/ | 7 | +| 7 | How to watch keys | https://etcd.io/docs/v3.7/tasks/developer/how-to-watch-keys/ | 8 | +| 8 | How to create lease | https://etcd.io/docs/v3.7/tasks/developer/how-to-create-lease/ | 9 | +| 9 | How to create locks | https://etcd.io/docs/v3.7/tasks/developer/how-to-create-locks/ | 10 | +| 10 | How to conduct leader election in etcd cluster | https://etcd.io/docs/v3.7/tasks/operator/how-to-conduct-elections/ | 10 | +| 11 | Interacting with etcd(etcdctl 读写/watch/lease 示例) | https://etcd.io/docs/v3.7/dev-guide/interacting_v3/ | 3、4、6、8、9 | +| 12 | gRPC naming and discovery(experimental) | https://etcd.io/docs/v3.7/dev-guide/grpc_naming/ | 11 | +| 13 | Why gRPC gateway | https://etcd.io/docs/v3.7/dev-guide/api_grpc_gateway/ | 12 | +| 14 | etcd gateway | https://etcd.io/docs/v3.7/op-guide/gateway/ | 13 | +| 15 | gRPC proxy | https://etcd.io/docs/v3.7/op-guide/grpc_proxy/ | 13 | +| 16 | Embedding etcd in a Go Application(**stub 页**) | https://etcd.io/docs/v3.7/dev-guide/golang_embed_pkg/ | 14 | +| 17 | Golang modules | https://etcd.io/docs/v3.7/dev-internal/modules/ | 15 | +| 18 | API reference(6 service / 41 方法) | https://etcd.io/docs/v3.7/dev-guide/api_reference_v3/ | 16 | +| 19 | API reference: concurrency(2 service / 7 方法) | https://etcd.io/docs/v3.7/dev-guide/api_concurrency_reference_v3/ | 10、16 | +| 20 | Data model | https://etcd.io/docs/v3.7/learning/data_model/ | 2(详见《01-核心概念篇》) | +| 21 | etcd API | https://etcd.io/docs/v3.7/learning/api/ | 2、4、5、7、8、16 | +| 22 | etcd API guarantees | https://etcd.io/docs/v3.7/learning/api_guarantees/ | 2、8 | +| 23 | etcd client design | https://etcd.io/docs/v3.7/learning/design-client/ | 2 | +| 24 | System limits(请求 1.5 MiB / 存储 2 GiB) | https://etcd.io/docs/v3.7/dev-guide/limit/ | 2(消息大小上限) | + +**覆盖页面数:24 页**(`tasks/developer/` 9 页含目录页、`dev-guide/` 7 页、`op-guide/` 2 页、`learning/` 4 页、`dev-internal/` 1 页、`tasks/operator/` 1 页;其中 `golang_embed_pkg` 为 stub 页,已在本册第 14 章如实说明)。 + +### 17.1 本册记录的官方源文疑点(忠实保留,不做修正) + +| # | 页面 | 疑点 | +| - | ---- | ---- | +| 1 | reading-from-etcd | 正文称 `--write-out="json"` 时 "the key is not returned",但同一示例输出中确实包含 `"key":"Zm9v"` | +| 2 | how-to-transactional-write | 变量表 key 写作 `/users/{/email`,**多了一个左花括号** | +| 3 | 多个 developer 页面 | 前置链接指向 `https://etcd.io/docs/v3.6/...`(install、local_cluster、api),而页面本身属 v3.7 | +| 4 | how-to-watch-keys | 源文未逐字给出事件输出样例(PUT / DELETE 事件类型由示例行为推得) | +| 5 | tasks/developer/_index.md | 仅含 front matter,无正文 | +| 6 | golang_embed_pkg | 页面为 stub,仅一段正文 + pkg.go.dev 链接,无代码与 `Config` 字段 | +| 7 | how-to-create-lease / interacting_v3 | 官方页面未提供"一次性续期"的 etcdctl flag 示例(本册 9.2 已注明) | +| 8 | api_reference_v3 / api_concurrency_reference_v3 | 无 gRPC code 对照表、无 enum 取值表、无代码示例(本册 16.11 已注明) | + +--- + +> 本册完。命令与字段名请以 `etcdctl --help` 与 https://etcd.io/docs/v3.7/ 为准;Go API 以 https://pkg.go.dev/go.etcd.io/etcd/client/v3 为准。 + + diff --git a/数据库/ETCD/etcd-v3.7-学习手册-03-运维运营篇.md b/数据库/ETCD/etcd-v3.7-学习手册-03-运维运营篇.md new file mode 100644 index 0000000..c441555 --- /dev/null +++ b/数据库/ETCD/etcd-v3.7-学习手册-03-运维运营篇.md @@ -0,0 +1,5922 @@ +# 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]` 命名不一致 | 并列保留,不擅自统一 | diff --git a/监控/.DS_Store b/监控/.DS_Store new file mode 100644 index 0000000..f6a207f Binary files /dev/null and b/监控/.DS_Store differ diff --git a/监控/Prometheus/Prometheus-学习手册-01-入门与架构篇.md b/监控/Prometheus/Prometheus-学习手册-01-入门与架构篇.md new file mode 100644 index 0000000..23fdeed --- /dev/null +++ b/监控/Prometheus/Prometheus-学习手册-01-入门与架构篇.md @@ -0,0 +1,1495 @@ +# Prometheus 知识学习手册 · 第 1 篇:入门与架构 + +> 基于 [prometheus.io 官方文档](https://prometheus.io/docs/introduction/overview/) 整理 +> 文档版本:Prometheus v3.15.0(最新稳定版,2026-10 发布)| 抓取日期:2026-10-08 +> 数据来源:prometheus/prometheus@v3.15.0、prometheus/alertmanager、prometheus/docs 官方仓库源文件(与官网 latest 内容一致) +> +> **全册结构(共 4 篇)** +> - **第 1 篇:入门与架构** ← 本篇 +> - 第 2 篇:PromQL 与查询 +> - 第 3 篇:配置与运维 +> - 第 4 篇:Alertmanager 与生态(告警、插桩、可视化、规范) + +--- + +## 目录 + +- [第 1 章 概述](#第-1-章-概述) + - [1.1 Prometheus 是什么](#11-prometheus-是什么) + - [1.2 解决什么问题](#12-解决什么问题) + - [1.3 核心特点](#13-核心特点) + - [1.4 适用场景:何时该用、何时不该用](#14-适用场景何时该用何时不该用) + - [1.5 竞品对比](#15-竞品对比) +- [第 2 章 快速上手](#第-2-章-快速上手) + - [2.1 环境要求](#21-环境要求) + - [2.2 下载与安装](#22-下载与安装) + - [2.3 5 分钟跑通](#23-5-分钟跑通) + - [2.4 官方 Hello World 教程完整步骤](#24-官方-hello-world-教程完整步骤) + - [2.5 验证安装成功](#25-验证安装成功) + - [2.6 常见启动问题](#26-常见启动问题) +- [第 3 章 核心概念](#第-3-章-核心概念) + - [3.1 数据模型详解](#31-数据模型详解) + - [3.2 四种指标类型](#32-四种指标类型) + - [3.3 Jobs 与 Instances](#33-jobs-与-instances) + - [3.4 术语表](#34-术语表) +- [第 4 章 架构与设计](#第-4-章-架构与设计) + - [4.1 设计哲学与动机](#41-设计哲学与动机) + - [4.2 整体架构与组件关系](#42-整体架构与组件关系) + - [4.3 Agent 模式(对比 Server 模式)](#43-agent-模式对比-server-模式) + - [4.4 API 稳定性承诺](#44-api-稳定性承诺) +- [第 5 章 生态与版本](#第-5-章-生态与版本) + - [5.1 路线图](#51-路线图) + - [5.2 发布周期与 LTS 策略](#52-发布周期与-lts-策略) + - [5.3 学习资源](#53-学习资源) + - [5.4 官方 FAQ 整理](#54-官方-faq-整理) +- [附录:本篇覆盖清单](#附录本篇覆盖清单) + +--- + +## 第 1 章 概述 + +### 1.1 Prometheus 是什么 + +`Prometheus` 是一个开源的**系统监控与告警工具箱**(monitoring and alerting toolkit)。 + +- 最初由 `SoundCloud` 构建,**2012 年**启动。 +- **2016 年**作为继 `Kubernetes` 之后**第二个**托管项目加入 `Cloud Native Computing Foundation`(CNCF,cncf.io)。 +- 现为**独立开源项目**,独立于任何公司维护。 +- 它是 `Kubernetes` 唯一直接支持的系统监控方案,也是云原生生态的事实标准。 + +其数据本质是:以**时间序列数据(time series data)**采集并存储指标——即每条指标信息都带记录时的时间戳,并附带可选的键值对 `labels`。 + +> 💡 补充:Prometheus 的“复数”在官方 FAQ 中经研究确定为 **Prometheis**;记不住时可直接用 “Prometheus instances”。 + +> 引自:[Overview](https://prometheus.io/docs/introduction/overview/) | [Frequently asked questions](https://prometheus.io/docs/introduction/faq/) | [Getting started with Prometheus](https://prometheus.io/docs/tutorials/getting_started/) + +### 1.2 解决什么问题 + +要理解 Prometheus 解决什么问题,先理解 **metrics(指标)** 与 **time series(时间序列)**。 + +- **metrics**:通俗意义上的**数值测量**。 +- **time series**:随时间变化的记录。 + +**为什么需要它**:不同应用关心的测量内容不同—— + +| 系统类型 | 典型关注指标 | +| --- | --- | +| Web 服务器 | 请求耗时(request times) | +| 数据库 | CPU 使用率、活跃连接数、活跃查询数 | + +以“Web 服务器变慢”为例:请求计数(request count)指标显示请求量变高,据此即可判断“变慢是因为请求量高”,并据此扩容服务器。 + +官方教程给出的指标定义自问法(非常实用): + +> **“what all information will be important for me to debug if any issue occurs in my application?”** +> (如果我的应用出了问题,哪些信息对调试重要?) + +Prometheus 就是系统化地回答这个问题的工具:定义指标 → 暴露指标 → 抓取存储 → 查询告警。 + +> 引自:[Overview](https://prometheus.io/docs/introduction/overview/) | [Getting started with Prometheus](https://prometheus.io/docs/tutorials/getting_started/) + +### 1.3 核心特点 + +官方 Overview 列出的核心特性(Features)逐条如下: + +| # | 特性 | 说明 | +| --- | --- | --- | +| 1 | 多维数据模型(multi-dimensional data model) | 时间序列由 `metric name` 与 key/value pairs(即 labels)标识 | +| 2 | `PromQL` | 灵活查询语言,用于充分利用该多维性 | +| 3 | 单节点自治 | 不依赖分布式存储,single server nodes are autonomous | +| 4 | Pull 模型采集 | 时间序列采集通过 HTTP 上的 **pull model** 完成 | +| 5 | 支持 Push | 通过中间网关(**push gateway**)支持推送时间序列 | +| 6 | 服务发现 | targets 通过 **service discovery** 或静态配置(static configuration)发现 | +| 7 | 多种可视化 | 支持多种绘图(graphing)与仪表盘(dashboarding)模式 | + +**可靠性定位**:Prometheus 的设计目标是**可靠性**——它是故障时快速诊断的首选系统。每个 Prometheus server 都是独立的,不依赖网络存储或其他远程服务,因此即使基础设施的其他部分损坏,Prometheus 仍可依赖,而且无需搭建大量基础设施。 + +> 💡 补充(来自官方 FAQ):Prometheus 的取舍是最看重“**可靠地把告警送达人类**”(reliably delivering alerts to humans)。这一点在第 1.4 节“何时不该用”中会看到其反面影响。 + +> 引自:[Overview](https://prometheus.io/docs/introduction/overview/) + +### 1.4 适用场景:何时该用、何时不该用 + +#### 何时该用(When does it fit) + +- **适合记录任何纯数值的时间序列**。 +- 既适配**以机器为中心的监控**(machine-centric monitoring),也适配**高度动态的面向服务架构**(highly dynamic service-oriented architectures)。 +- 在**微服务**场景下,多维数据采集与多维查询是显著优势。 +- 需要**高可靠性、快速故障诊断**的场景——Prometheus server 独立自治,不依赖网络存储或远程服务。 + +#### 何时不该用(When does it not fit) + +- Prometheus **重可靠性而非 100% 准确性**。 +- 若需要 **100% 准确**(典型例子:**按请求计费 per-request billing**),**不适合**——因为采集的数据可能不够详尽、不够完整。 +- 正确做法:**另用一个系统专门做计费数据采集**,Prometheus 负责其余监控。 + +#### 选择决策速查表 + +| 你的需求 | 是否适合 Prometheus | 说明 | +| --- | --- | --- | +| 记录纯数值时间序列 | ✅ 适合 | 核心用例 | +| 机器/主机监控 | ✅ 适合 | 配合 Node Exporter | +| 高度动态的云原生 / 微服务 | ✅ 适合 | 多维模型 + 服务发现 | +| 需要可靠的告警送达 | ✅ 适合 | 设计目标即“可靠地把告警送达人类” | +| 按请求计费(per-request billing) | ❌ 不适合 | 需 100% 准确,另建专用系统 | +| 日志/事件采集 | ❌ 不适合 | 见 FAQ:应改用 `Grafana Loki` 或 `OpenSearch` | +| 长期历史数据(超单节点容量) | ⚠️ 需外部方案 | 借助 remote write 到长期存储系统(如 Thanos 等) | + +> 引自:[Overview](https://prometheus.io/docs/introduction/overview/) | [Frequently asked questions](https://prometheus.io/docs/introduction/faq/) + +### 1.5 竞品对比 + +本节基于官方 [Comparison to alternatives](https://prometheus.io/docs/introduction/comparison/) 页面整理,对比 Prometheus 与 Graphite、InfluxDB、OpenTSDB、Nagios、Sensu 等系统。 + +#### 1.5.1 总览对比表 + +| 维度 | Prometheus | Graphite | InfluxDB | OpenTSDB | Nagios | Sensu | +| --- | --- | --- | --- | --- | --- | --- | +| 类型 | 完整监控与告警系统 | 被动时序数据库 + 查询 + 绘图 | 开源时序数据库(有商用集群版) | 基于 Hadoop/HBase 的分布式时序库 | 基于脚本 exit code 的告警系统 | 开源可观测性 pipeline | +| 主动抓取 | ✅ 内置 | ❌ 交给外部组件 | ❌ 需外部 | ❌ 需外部 | 主动 check(脚本) | ✅ 事件驱动 | +| 查询语言 | `PromQL`(强,支持聚合/切片/预测/join) | 有查询语言(弱于 PromQL) | 有(弱于 PromQL) | API 仅支持简单聚合与数学运算 | ❌ 无查询语言 | 有限 | +| 多维标签 | ✅ labels(key-value,显式) | ❌ 点分 metric name 隐式编码 | ✅ 称 `tags`(另有 `fields`) | ✅ 称 `tags`(同 Prometheus labels) | ❌ 无 labels 概念 | ✅ labels/annotations | +| 告警/通知 | ✅ Alertmanager(分组、去重、静默、限流) | ❌ 交给外部 | 需搭配 `Kapacitor` | ❌ | ✅ 但无分组/路由/去重 | ✅ 事件处理强 | +| 存储 | 本地 TSDB(block + WAL),接受任意间隔样本 | `Whisper`(RRD 风格,要求固定间隔,过期覆盖) | 时间分片 LSM tree + WAL | Hadoop/HBase 之上 | 除当前状态外无存储 | 嵌入式 `etcd` 或外部 `PostgreSQL` | +| 水平扩展 | 单节点自治,超容量需按边界显式分片 | 集群方案更成熟适合长历史 | 商用版分布式集群,易水平扩展 | 易水平扩展(需运维 Hadoop/HBase) | 单 server,文件配置 | 组件可集群 | +| 开源模式 | 完全开源、独立项目 | 开源 | open-core(单一商业公司) | 开源 | 开源 | open-core(单一商业公司) | + +#### 1.5.2 vs. Graphite + +- **Scope(范围)**:Graphite 侧重做被动的时序数据库 + 查询语言 + 绘图,其他关注点交给外部组件。Prometheus 是**全套监控与趋势系统**,内置主动抓取、存储、查询、绘图、基于时序数据的告警;它知道“世界应是什么样”(哪些端点应存在、哪些时序模式意味着异常)并主动找故障。 +- **Data model(数据模型)**:Graphite 存的是命名时序的数值样本。Graphite 用点分隔的 metric name **隐式编码维度**;Prometheus 则用显式的 key-value pairs(`labels`)附加于 metric name,便于按标签过滤、分组、匹配。Graphite 搭配 StatsD 时常只存所有实例的**聚合数据**,不保留 instance 维度,**无法下钻到具体问题实例**。 + + 编码示例对比: + + ```text + # Graphite/StatsD 编码(仅一个聚合值,丢失实例维度) + stats.api-server.tracks.post.500 -> 93 + ``` + + ```text + # Prometheus 编码(假设三个 api-server 实例,可下钻到实例) + api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance=""} -> 34 + api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance=""} -> 28 + api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance=""} -> 31 + ``` + +- **Storage(存储)**:Graphite 用 `Whisper` 格式存本地磁盘,RRD 风格,**要求样本按固定间隔到达**;每个时序单独一个文件,过时后新样本覆盖旧样本。Prometheus 本地 TSDB 用**内存 head**(由 write-ahead log 保护)保存近期样本,再以**基于时间的 block 目录**持久化(含 chunks 目录、index 与 metadata),**接受任意间隔的样本**;保留期可配置,适合大量短生命周期、频繁变化的时序。 +- **Summary**:Prometheus 数据模型与查询语言更丰富、更易运行集成;若需要**可长期持有历史数据的集群方案**,Graphite 可能更合适。 + +#### 1.5.3 vs. InfluxDB + +- InfluxDB 是开源时序数据库,有商用扩展/集群选项;其项目**发布晚于 Prometheus 开发开始近一年**。公平比较需连同 `Kapacitor` 一起看(两者合计对应 Prometheus + Alertmanager 的问题域)。 +- **功能对应关系**: + + | InfluxDB 侧能力 | Prometheus 侧对应 | + | --- | --- | + | InfluxDB continuous queries | Prometheus recording rules | + | Kapacitor 全部范围 | Prometheus recording rules + alerting rules + Alertmanager 通知功能 | + + Prometheus 查询语言更强;Alertmanager 另有分组、去重、静默。 +- **Data model / storage**:InfluxDB 的 labels 称 `tags`,另有二级 labels 称 `fields`(用途更受限);支持最高**纳秒**分辨率时间戳,类型 float64、int64、bool、string。Prometheus 支持 **float64**(字符串支持有限)与**毫秒**分辨率时间戳。InfluxDB 用按时间分片的 log-structured merge tree + write ahead log,更适合事件日志;Prometheus 基于 block 的本地 TSDB **针对监控时序优化**。 +- **Architecture**:Prometheus server 各自独立运行,核心功能(抓取、规则处理、告警)**仅依赖本地存储**;开源版 InfluxDB 类似。商用 InfluxDB 按设计是分布式存储集群(存储与查询由多节点处理),更易水平扩展但**需从一开始管理分布式存储复杂性**;Prometheus 更易运行,但超单节点容量后需按 scalability 边界(产品、服务、数据中心等)**显式分片**。独立 server(可并行冗余运行)带来更好可靠性与故障隔离。 +- **Kapacitor**:开源版无内置 rules/alerting/notifications 的分布式/冗余选项,可由用户手动分片扩展;Influx 提供 Enterprise Kapacitor 支持 HA/冗余告警。Prometheus 与 Alertmanager 则通过**运行冗余 Prometheus 副本 + Alertmanager 高可用模式**,提供**完全开源**的冗余选项。 +- **Summary**: + - 相似处:都用 labels(InfluxDB 称 `tags`)支持多维指标;都用基本相同的数据压缩算法;都有广泛集成(包括互相);都有扩展钩子。 + - InfluxDB 更优:做事件日志时;商用版聚类更适合长期数据存储;副本间最终一致视图。 + - Prometheus 更优:主要做 metrics 时;查询语言、告警、通知更强;绘图与告警的可用性与 uptime 更高。 + - 维护模式:InfluxDB 由单一商业公司按 open-core 模式维护(闭源聚类、托管、支持等增值功能);Prometheus 是**完全开源独立项目**。 + +#### 1.5.4 vs. OpenTSDB + +- 基于 `Hadoop` 与 `HBase` 的分布式时序数据库。Scope 差异同 Graphite 处。 +- **Data model**:与 Prometheus **几乎相同**——时序由任意 key-value pairs 标识(OpenTSDB `tags` = Prometheus `labels`)。某 metric 的全部数据存于一起,**限制 metric 基数(cardinality)**。差异:Prometheus 允许 label value 使用任意字符,OpenTSDB 更受限;OpenTSDB **缺少完整查询语言**,API 仅支持简单聚合与数学运算。 +- **Storage**:OpenTSDB 存储实现在 Hadoop/HBase 之上,易水平扩展,但需**从头接受运行 Hadoop/HBase 集群的复杂性**;Prometheus 初期更易运行,但超过单节点容量后需显式分片。 +- **Summary**:Prometheus 查询语言丰富、可处理更高基数、是完整监控系统的一部分;若**已运行 Hadoop** 且更看重长期存储,OpenTSDB 是好选择。 + +#### 1.5.5 vs. Nagios + +- 起源于 1990 年代(名为 **NetSaint**)。 +- **Scope**:主要基于**脚本 exit codes** 告警,称为 "checks";可静默单个告警,但**无分组、路由或去重**。插件生态丰富(如将 perfData 管道到 Graphite 等时序库,或用 `NRPE` 在远程机器执行 checks)。 +- **Data model**:以 **host** 为基础,每 host 可含一个或多个 services,每 service 执行一个 check;**无 labels 概念,无查询语言**。 +- **Storage**:除当前 check 状态外**无存储**;有插件可存储数据用于可视化。 +- **Architecture**:Nagios server 独立;所有 check 配置通过文件。 +- **Summary**:适合**小型/静态系统**的基础监控(blackbox probing 足够时)。若做 **whitebox monitoring** 或环境**动态/基于云**,选 Prometheus。 + +#### 1.5.6 vs. Sensu + +- 开源监控与可观测性 pipeline,有商用发行版提供扩展性功能,**可复用现有 Nagios 插件**。 +- **Scope**:是 observability pipeline,侧重把可观测性数据作为 `Events` 流处理与告警,提供可扩展框架用于 event filtering、aggregation、transformation、processing(含向其他系统发告警、把事件存到第三方系统)。其事件处理能力范围类似 **Prometheus alerting rules + Alertmanager**。 +- **Data model**:Sensu `Events` 以结构化格式表示服务健康与/或 metrics,由 `entity` 名(如 server、云实例、container 或 service)、event name 及可选 key-value metadata(称 "labels" 或 "annotations")标识。Event payload 可含一个或多个 metric `points`,以 JSON 对象表示,含 `name`、`tags`(key/value pairs)、`timestamp`、`value`(总是 float)。 +- **Storage**:当前与近期事件状态信息及实时 inventory 存于嵌入式数据库(`etcd`)或外部 RDBMS(`PostgreSQL`)。 +- **Architecture**:所有组件可集群,以实现高可用与更高事件处理吞吐。 +- **Summary**: + - 共同点:都有可扩展的动态云环境发现机制;都支持通过 labels/annotations 收集多维指标;都有广泛集成;Sensu 原生支持从所有 Prometheus exporters 收集指标;都可转发可观测性数据到第三方平台。 + - Sensu 更优:收集处理**混合**可观测性数据(metrics 与/或 events);整合多监控工具且需支持 metrics 与 Nagios 风格插件/check 脚本;事件处理平台更强。 + - Prometheus 更优:主要**收集与评估 metrics**;监控**同构 Kubernetes 基础设施**(若 100% 工作负载在 K8s,Prometheus 的 K8s 集成更好);查询语言更强且内置历史数据分析支持。 + - 维护模式:Sensu 由单一商业公司按 open-core 模式维护;Prometheus 是完全开源独立项目。 + +> 引自:[Comparison to alternatives](https://prometheus.io/docs/introduction/comparison/) | [Frequently asked questions](https://prometheus.io/docs/introduction/faq/) + +--- + +## 第 2 章 快速上手 + +### 2.1 环境要求 + +- Prometheus 使用 **`Go`** 编写,**只需对应操作系统的编译二进制**即可运行。 +- 运行只需两样东西:**二进制 + 配置文件**(配置文件使用 **YAML** 格式)。 +- 官方为多数 Prometheus 组件提供**预编译二进制**(precompiled binaries)。 + +> 💡 补充(工程经验,非官方原文):Prometheus 官方只发布 Linux/macOS/Windows 的可执行文件,本身是静态编译的单一二进制,无需额外运行时依赖。生产环境建议运行在 Linux 服务器上,并为其预留独立的数据盘(TSDB 数据目录 `--storage.tsdb.path`,默认 `./data`)。 + +> 引自:[Installation](https://prometheus.io/docs/prometheus/latest/installation/) | [Getting started with Prometheus](https://prometheus.io/docs/tutorials/getting_started/) | [Getting started](https://prometheus.io/docs/prometheus/latest/getting_started/) + +### 2.2 下载与安装 + +官方 Installation 页面给出的安装方式逐条如下。 + +#### 2.2.1 方式一:预编译二进制 + +- 为多数官方 Prometheus 组件提供预编译二进制,版本列表见 。 +- 下载后解包: + + ```bash + tar xvfz prometheus-*.tar.gz + cd prometheus-* + ``` + +- 服务器为**单一二进制** `prometheus`(Windows 下为 `prometheus.exe`)。用 `--help` 查看所有选项: + + ```bash + ./prometheus --help + usage: prometheus [] + + The Prometheus monitoring server + + . . . + ``` + +> 💡 补充(来自官方 Tutorials 教程):也可下载后把二进制目录加入系统 `PATH`,之后即可直接以 `prometheus --config.file=prometheus.yml` 启动。 + +#### 2.2.2 方式二:从源码构建 + +- 见各仓库的 `Makefile` targets(例如 prometheus/prometheus 仓库)。 + +#### 2.2.3 方式三:Docker + +- 所有 Prometheus 服务提供 Docker 镜像,位于 [Quay.io](https://quay.io/repository/prometheus/prometheus) 或 [Docker Hub](https://hub.docker.com/r/prom/prometheus/)。 +- 最简启动(以示例配置启动并暴露 9090 端口): + + ```bash + docker run -p 9090:9090 prom/prometheus + ``` + +- **镜像使用 volume 存储实际 metrics**。生产部署**强烈建议使用 named volume**,以便升级时管理数据。 +- **命令行参数注意**:镜像以若干默认命令行参数启动(可在 Dockerfile 中查看);若想追加参数,需**自行重新加上原有参数**,否则会被覆盖。 + +**挂载配置示例(二选一)**: + +```bash +# 只挂载单个配置文件 +docker run -p 9090:9090 -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus + +# 挂载整个配置目录 +docker run -p 9090:9090 -v /path/to/config:/etc/prometheus prom/prometheus +``` + +**保存数据(持久化)**:数据存于容器内 `/prometheus` 目录,**容器重启即清空**;需设置持久化存储: + +```bash +docker volume create prometheus-data +docker run -p 9090:9090 -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml -v prometheus-data:/prometheus prom/prometheus +``` + +**自定义镜像(把配置烤入镜像)**: + +```Dockerfile +FROM prom/prometheus +ADD prometheus.yml /etc/prometheus/ +``` + +```bash +docker build -t my-prometheus . +docker run -p 9090:9090 my-prometheus +``` + +- **更高级选项**:启动时用工具动态渲染配置,或由守护进程定期更新。 + +#### 2.2.4 方式四:配置管理系统(第三方贡献) + +| 工具 | 项目地址 | +| --- | --- | +| Ansible | `prometheus-community/ansible` | +| Chef | `rayrod2030/chef-prometheus` | +| Puppet | `puppet/prometheus` | +| SaltStack | `saltstack-formulas/prometheus-formula` | + +> 引自:[Installation](https://prometheus.io/docs/prometheus/latest/installation/) + +### 2.3 5 分钟跑通 + +这一节把“下载 → 写配置 → 启动 → 首次查询 → 用 UI”串成一条最小可用路径,素材来自官方 [First steps with Prometheus](https://prometheus.io/docs/introduction/first_steps/)。 + +#### 2.3.1 配置文件详解 + +Prometheus 配置为 **YAML**,下载包自带示例配置 `prometheus.yml`,注释以 `#` 前缀。 + +```yaml +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + # - "first.rules" + # - "second.rules" + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ['localhost:9090'] +``` + +配置由**三个配置块**组成: + +| 配置块 | 作用 | 关键字段说明 | +| --- | --- | --- | +| `global` | 全局默认 | `scrape_interval` 控制抓取频率(本例 15 秒,可**按 target 单独覆盖**);`evaluation_interval` 控制规则求值频率,规则用于创建新时间序列与生成告警 | +| `rule_files` | 规则文件位置 | 指定要加载的规则文件位置;示例中暂无规则 | +| `scrape_configs` | 监控哪些资源 | 默认单 job 名为 `prometheus`,抓取 Prometheus server 自身暴露的时间序列;含单个静态配置 target:`localhost` 的 `9090` 端口 | + +- **默认抓取路径为 `/metrics`**,即完整 URL 为 。 + +#### 2.3.2 启动 + +```bash +./prometheus --config.file=prometheus.yml +``` + +启动后: + +- 可在 查看自身状态页; +- 约需 **30 秒**采集自身 HTTP 指标端点数据; +- 可在 验证。 + +#### 2.3.3 首次查询(表达式浏览器) + +访问 ,选择 **"Table"** 标签页。 + +示例指标 `promhttp_metric_handler_requests_total`(Prometheus 自身已处理的 `/metrics` 请求总数): + +```promql +promhttp_metric_handler_requests_total +``` + +返回若干时间序列(各带最新值):metric name 相同但 labels 不同,labels 用于标识不同请求状态。 + +只查看 HTTP `200`: + +```promql +promhttp_metric_handler_requests_total{code="200"} +``` + +统计返回的时间序列数量: + +```promql +count(promhttp_metric_handler_requests_total) +``` + +#### 2.3.4 UI 使用(绘图界面) + +- 访问 的 **"Graph"** 标签页。 +- 示例(每秒 200 状态码请求速率): + + ```promql + rate(promhttp_metric_handler_requests_total{code="200"}[1m]) + ``` + +- 在 (或 **Status → Target health**)查看各 target 健康状态。 + +#### 2.3.5 监控其他 target + +- 建议参考其他 exporter 文档,起点为官方指南 **`Monitoring Linux or macOS host metrics using a node exporter`**()。 + +> 引自:[First steps with Prometheus](https://prometheus.io/docs/introduction/first_steps/) + +### 2.4 官方 Hello World 教程完整步骤 + +本节按官方 [Getting started](https://prometheus.io/docs/prometheus/latest/getting_started/)(核心文档中的 “Hello World” 式教程)的完整流程展开:下载并在本地运行 Prometheus,配置其抓取自身与一个示例应用,再通过查询、规则、图表使用采集到的时序数据。 + +#### 2.4.1 步骤 1:下载并解包 + +```bash +tar xvfz prometheus-*.tar.gz +cd prometheus-* +``` + +#### 2.4.2 步骤 2:编写自监控配置 + +保存为 `prometheus.yml`: + +```yaml +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + external_labels: + monitor: 'codelab-monitor' +scrape_configs: + - job_name: 'prometheus' + scrape_interval: 5s + static_configs: + - targets: ['localhost:9090'] +``` + +要点:`global.scrape_interval` 全局 15s;job 级 `scrape_interval: 5s` **覆盖**全局值;`external_labels` 添加 `monitor: 'codelab-monitor'`。 + +#### 2.4.3 步骤 3:启动 + +```bash +./prometheus --config.file=prometheus.yml +``` + +- 默认数据库存于 `./data`(flag `--storage.tsdb.path`)。 +- 状态页: +- metrics 端点: + +#### 2.4.4 步骤 4:用表达式浏览器探索数据 + +访问 并选择 **"Graph"** 标签页: + +```promql +prometheus_target_interval_length_seconds +``` + +```promql +prometheus_target_interval_length_seconds{quantile="0.99"} +``` + +```promql +count(prometheus_target_interval_length_seconds) +``` + +其中 `prometheus_target_interval_length_seconds` 表示 target 抓取之间的实际耗时。 + +绘图示例: + +```promql +rate(prometheus_tsdb_head_chunks_created_total[1m]) +``` + +#### 2.4.5 步骤 5:启动示例 target(Node Exporter) + +```bash +tar -xzvf node_exporter-*.*.tar.gz +cd node_exporter-*.* +./node_exporter --web.listen-address 127.0.0.1:8080 +./node_exporter --web.listen-address 127.0.0.1:8081 +./node_exporter --web.listen-address 127.0.0.1:8082 +``` + +#### 2.4.6 步骤 6:将多个端点归入一个 job 并分组 + +将三个端点归入一个名为 `node` 的 job,前两个为 production、第三个为 canary: + +```yaml +scrape_configs: + - job_name: 'node' + scrape_interval: 5s + static_configs: + - targets: ['localhost:8080', 'localhost:8081'] + labels: + group: 'production' + - targets: ['localhost:8082'] + labels: + group: 'canary' +``` + +在 或 **Status → Target health** 查看:三个端点状态应为 `UP`;若为 `DOWN`,检查对应端口上 Node Exporter 是否仍在运行。 + +示例返回序列: + +```promql +node_cpu_seconds_total{cpu="0",group="production",instance="localhost:8080",job="node",mode="idle"} +``` + +在查询选项菜单(表达式输入框右侧三点按钮)→ **Explore metrics** 可浏览全部 metric name。 + +#### 2.4.7 步骤 7:定义 Recording rule + +将表达式 `avg by (job, instance, mode) (rate(node_cpu_seconds_total[5m]))` 预录为 `job_instance_mode:node_cpu_seconds:avg_rate5m`,保存为 `prometheus.rules.yml`: + +```yaml +groups: +- name: cpu-node + rules: + - record: job_instance_mode:node_cpu_seconds:avg_rate5m + expr: avg by (job, instance, mode) (rate(node_cpu_seconds_total[5m])) +``` + +在 `prometheus.yml` 中: + +- 加入 `rule_files: ['prometheus.rules.yml']`; +- 在 `global` 中加入 `evaluation_interval: 15s`(Evaluate rules every 15 seconds)。 + +#### 2.4.8 步骤 8:重载配置与优雅关闭 + +**重载配置(无需重启)**:发送 `SIGHUP` 信号。 + +```bash +kill -s SIGHUP +``` + +**优雅关闭**(推荐用信号/中断): + +- Linux 上发送 `SIGTERM` 或 `SIGINT`: + + ```bash + kill -s + ``` + +- 或在控制终端按中断字符(默认 `^C`)。 + +> 💡 补充(来自官方 FAQ):除 `SIGHUP` 外,也可向 `/-/reload` 端点发 **HTTP POST** 重载配置;各组件会尽量**优雅处理失败的变更**。 + +> 引自:[Getting started](https://prometheus.io/docs/prometheus/latest/getting_started/) | [Frequently asked questions](https://prometheus.io/docs/introduction/faq/) + +#### 2.4.9 Tutorials 版快速上手(含 node_exporter) + +官方 [Tutorials → Getting started with Prometheus](https://prometheus.io/docs/tutorials/getting_started/) 给出了更精简的版本: + +**第一步:只抓取自身。** + +```yaml +global: + scrape_interval: 15s + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ["localhost:9090"] +``` + +含义:`scrape_interval` 定义抓取频率;`scrape_configs` 含 job 名称与抓取目标。Prometheus 默认监听 **9090** 端口,故把 `localhost:9090` 加入 targets。 + +启动: + +```bash +prometheus --config.file=prometheus.yml +``` + +此时 Prometheus 已运行并**每 15s 抓取自身指标**。 + +**第二步:接入 node exporter。** 先运行 exporter(机器指标 exporter,下载:): + +```bash +./node_exporter +``` + +再加入 `scrape_configs`: + +```yaml +global: + scrape_interval: 15s + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ["localhost:9090"] + - job_name: node_exporter + static_configs: + - targets: ["localhost:9100"] +``` + +**抓取原理示例**:以 web server 为例,用 Prometheus client library 添加 instrumentation 代码暴露指标;配置 Prometheus 从该 web server(监听 IP 端口 7500)按固定间隔(如每分钟)抓取: + +| Time Stamp | Request Count (metric) | +| --- | --- | +| 11:00:00 | 0 | +| 11:01:00 | 1 | +| 11:02:00 | 3 | + +- 11:00:00 首次抓取 request count 存为 `0`; +- 11:01:00 处理 1 个请求,count 增至 `1`; +- 11:02:00 再处理 2 个请求,count = 1 + 2 = `3`。 +- 抓取频率由用户控制。 +- 备注:该表仅用于理解,**Prometheus 并非以此确切格式存储值**。 + +**查询 API**:Prometheus 提供 API 查询已抓取并存储的指标,用于查询指标、创建 dashboard/chart 等,使用 `PromQL`。 + +> 引自:[Getting started with Prometheus](https://prometheus.io/docs/tutorials/getting_started/) + +### 2.5 验证安装成功 + +| 检查项 | 方法 | 预期结果 | +| --- | --- | --- | +| 进程已启动 | 访问 | 能看到自身状态页 | +| metrics 端点正常 | 访问 | 返回文本格式指标(约启动 30 秒后有数据) | +| 目标健康 | 或 **Status → Target health** | `prometheus` target 状态为 `UP` | +| 查询可用 | 选 Table,执行 `promhttp_metric_handler_requests_total` | 返回带不同 labels 的多条时间序列 | +| 规则已加载(配置规则后) | | 能看到已定义的 record/alert 规则 | +| 告警可查(接入 Alertmanager 后) | | 能看到告警及状态 | + +> 引自:[First steps with Prometheus](https://prometheus.io/docs/introduction/first_steps/) | [Getting started](https://prometheus.io/docs/prometheus/latest/getting_started/) + +### 2.6 常见启动问题 + +> 💡 补充:以下内容为基于官方文档各处的**工程实践提示**,官方未单列“常见启动问题”章节,故以排错清单形式给出。 + +| 现象 | 可能原因 | 排查方向 | +| --- | --- | --- | +| 状态页打不开 | 端口被占用 / 监听地址不对 | 检查 `--web.listen-address`(默认 `0.0.0.0:9090`);用 `lsof -i :9090` 查占用 | +| target 状态 `DOWN` | 被监控进程未运行 / 端口错误 | 官方指引:检查对应端口上 exporter(如 Node Exporter)是否仍在运行 | +| 配置文件报错无法启动 | YAML 语法错误 / 字段拼写错误 | 用 `promtool check config prometheus.yml` 校验(见官方 promtool 文档) | +| 启动后查询无数据 | 抓取尚未完成 | 官方说明:约需 **30 秒**才开始采集到自身 HTTP 指标端点数据 | +| Docker 中数据丢失 | 数据存于容器内 `/prometheus`,容器重启即清空 | 使用 named volume:`-v prometheus-data:/prometheus` | +| Docker 中自定义参数不生效 | 镜像有默认命令行参数,追加参数会覆盖默认参数 | 需**自行重新加上**原有默认参数 | +| 配置改了不生效 | 未重载 | 发送 `SIGHUP`,或向 `/-/reload` 发 HTTP POST | +| 关闭时数据/状态异常 | 直接 kill 未优雅退出 | 使用 `SIGTERM`/`SIGINT` 优雅关闭 | + +> 引自:[First steps with Prometheus](https://prometheus.io/docs/introduction/first_steps/) | [Installation](https://prometheus.io/docs/prometheus/latest/installation/) | [Getting started](https://prometheus.io/docs/prometheus/latest/getting_started/) | [Frequently asked questions](https://prometheus.io/docs/introduction/faq/) + +--- + +## 第 3 章 核心概念 + +### 3.1 数据模型详解 + +#### 3.1.1 时间序列(Time Series) + +- Prometheus 把**所有数据**都存储为 **_time series_(时间序列)**:属于**同一 metric** 且**同一组 labeled dimensions** 的、带时间戳的值的流。 +- 除已存储的序列外,Prometheus 还可能**因查询生成临时派生序列**。 +- 每条时间序列由 **metric name** 和**可选的键值对 labels** 唯一标识。 + +#### 3.1.2 样本(Sample) + +- 每个 **sample** 由两部分组成: + + | 组成 | 说明 | + | --- | --- | + | 值 | 一个 `float64` 值,或一个 **native histogram** 值 | + | 时间戳 | **毫秒精度**时间戳 | + +> 💡 补充(来自官方 FAQ 的 Implementation 一节):之所以样本值都是 **64 位浮点**,是为了简化设计。`IEEE 754` 双精度二进制浮点格式支持整数精度至 **2^53**。原生 64 位整数仅在需要 2^53 以上、2^63 以下整数精度时才有帮助。原则上可实现不同样本值类型(含大整数,超过 64 位),但目前非优先。即便 counter 每秒递增一百万次,也需超过 **285 年**才会遇到精度问题。 + +#### 3.1.3 Metric name(指标名) + +命名规则: + +- **SHOULD** 描述被测量系统的通用特征(如 `http_requests_total` 表示收到的 HTTP 请求总数); +- **MAY** 使用任意 UTF-8 字符; +- **SHOULD** 匹配正则 `[a-zA-Z_:][a-zA-Z0-9_:]*`,否则在 PromQL 中使用时需**加引号**; +- 冒号 `:` **保留给用户自定义 recording rules**,exporters 或直接插桩 **SHOULD NOT** 使用。 + +#### 3.1.4 Metric labels(指标标签) + +- 用于捕获同一 metric name 的不同实例(如所有用 `POST` 方法访问 `/api/tracks` 的 HTTP 请求),这种模型称为 **"dimensional data model"(维度化数据模型)**。 +- **任何 label 值变化(含增删 label)都会创建新的时间序列。** +- 命名规则: + + | 对象 | 规则 | + | --- | --- | + | label name | **MAY** 任意 UTF-8;以 `__`(双下划线)开头 **MUST** 保留给 Prometheus 内部使用;**SHOULD** 匹配 `[a-zA-Z_][a-zA-Z0-9_]*` | + | label value | **MAY** 含任意 UTF-8 | + | 空 label value | **等价于该 label 不存在** | + +#### 3.1.5 命名规则速查表 + +| 对象 | SHOULD 匹配的正则 | 备注 | +| --- | --- | --- | +| metric name | `[a-zA-Z_:][a-zA-Z0-9_:]*` | `:` 保留给 recording rules;不匹配则 PromQL 中需加引号 | +| label name | `[a-zA-Z_][a-zA-Z0-9_]*` | `__` 前缀为 Prometheus 内部保留 | +| label value | 无字符集限制 | 可为任意 UTF-8;空值等于 label 不存在 | + +#### 3.1.6 UTF-8 说明(重要警示) + +> **WARNING**:metric/label name 的 UTF-8 支持在 **Prometheus v3.0.0** 才加入。生态(下游 PromQL 兼容项目、工具、第三方插桩、collectors)**可能尚未跟进**。为兼容性,建议沿用上文 **SHOULD** 字符集。 + +#### 3.1.7 Notation(记法) + +标准记法: + +```text +{