etcd是一个由CoreOS团队开发的开源项目,旨在提供一个高可用的、分布式的、一致的键值存储,用于配置共享和服务发现。尽管它看起来像一个键值存储,但etcd的设计目标远远超出了传统数据库的功能范围。
etcd的核心特性包括:
尽管etcd具有键值存储的特性,但它通常不被归类为传统的关系型数据库或非关系型数据库。相反,它被视为一种专门用于配置管理、服务发现和分布式协调的特殊类型存储系统。etcd的设计目标是提供高可用性、一致性和简单的API,以支持复杂的分布式系统。
以 CentOS 7 为例,可以通过 yum install -y etcd
进行安装。
然而通过系统工具安装的 etcd 版本比较滞后,如果需要安装最新版本的 etcd ,可以通过二进制包、源码编译以及 Docker 容器安装。
脚本来源: Github etcd-io/etcd
# etcd 版本
ETCD_VER=v3.5.18
# etcd 下载地址
DOWNLOAD_URL=https://github.com/etcd-io/etcd/releases/download
rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
rm -rf /tmp/etcd-download-test && mkdir -p /tmp/etcd-download-test
curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd-download-test --strip-components=1
rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
/tmp/etcd-download-test/etcd --version
/tmp/etcd-download-test/etcdctl version
/tmp/etcd-download-test/etcdutl version
# start a local etcd server
/tmp/etcd-download-test/etcd
# write,read to etcd
/tmp/etcd-download-test/etcdctl --endpoints=localhost:2379 put foo bar
/tmp/etcd-download-test/etcdctl --endpoints=localhost:2379 get foo
etcd uses gcr.io/etcd-development/etcd as a primary container registry, and quay.io/coreos/etcd as secondary.
ETCD_VER=v3.5.18
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 有三种集群化启动的配置方案,分别为静态配置启动、etcd动态发现、DNS发现。
特性 | DNS 自发现 | etcd 自发现服务 | 静态配置 |
---|---|---|---|
核心原理 | 依赖 DNS SRV 记录解析节点地址 | 通过预生成的发现 URL(托管于外部 etcd 服务)自动发现节点 | 手动指定所有节点的 IP 或域名 |
适用场景 | 动态 IP 环境(如 Kubernetes) | 初始化新集群,无现有节点引导时使用 | 固定 IP 的物理机/虚拟机 |
扩展性 | 新增节点无需修改已有配置,自动加入 | 仅用于集群初始化,后续扩容需其他机制 | 需手动更新所有节点的配置 |
复杂度 | 需维护 DNS SRV 和 A/CNAME 记录 | 需管理发现 URL(生成、过期处理) | 配置简单,但维护成本高 |
依赖服务 | DNS 服务器 | 外部的 etcd 发现服务(公有或自建) | 无 |
典型用例 | 云环境动态集群(如 AWS、GCP) | 首次启动无静态配置的集群 | 本地测试或小型固定环境 |
TLS 要求 | 强制要求 TLS 加密 | 不强制,但建议启用 | 可选 |
动态节点变更 | 支持(通过更新 DNS 记录) | 仅初始化阶段有效,后续需手动干预 | 不支持,需重启集群 |
故障恢复能力 | 节点重启后自动重连 DNS 解析 | 依赖发现 URL 的有效性 | 依赖静态配置的持久性 |
搭建 etcd 集群,环境信息如下:
HostName | ip | 客户端交互端口 | peer 通信端口 |
---|---|---|---|
etcd1 | 10.0.1.10 | 2379 | 2380 |
etcd2 | 10.0.1.11 | 2379 | 2380 |
etcd3 | 10.0.1.12 | 2379 | 2380 |
静态启动 etcd 集群要求:在启动整个集群之前,已经预先清楚所要配置的集群大小,以及集群上各节点的地址和端口信息的场景。
配置方式:在启动etcd服务时,通过命令行参数或环境变量来配置集群信息。
二进制部署启动ectd服务:
etcd -name etcd0 \
--listen-client-urls http://10.0.1.10:2379 \
--advertise-client-urls http://10.0.1.10:2379 \
--initial-advertise-peer-urls http://10.0.1.10:2380 \
--listen-peer-urls http://10.0.1.10:2380 \
--initial-cluster etcd0=http://10.0.1.10:2380,etcd1=http://10.0.1.11:2380,etcd2=http://10.0.1.12:2380 \
--initial-cluster-state new \
--initial-cluster-token tkn
配置项说明:
--name # etcd集群中的节点名,这里可以随意,可区分且不重复就行
--listen-peer-urls # 监听的用于节点之间通信的url,可监听多个,集群内部将通过这些url进行数据交互(如选举,数据同步等)
--initial-advertise-peer-urls # 建议用于节点之间通信的url,节点间将以该值进行通信。
--listen-client-urls # 监听的用于客户端通信的url,同样可以监听多个。
--advertise-client-urls # 建议用于客户端通信的url,该值用于 etcd 代理或 etcd 成员与 etcd 节点通信。
--initial-cluster-token # etcd-cluster-1,节点的 token 值,设置该值后集群将生成唯一 id,并为每个节点也生成唯一 id,当使用相同配置文件再启动一个集群时,只要该 token 值不一样,etcd 集群就不会相互影响。
--initial-cluster # 也就是集群中所有的 initial-advertise-peer-urls 的合集。
--initial-cluster-state # new,新建集群的标志
docker 部署 etcd集群:
docker run \
-p 2379:2379 \
-p 2380:2380 \
--volume=/var/lib/etcd:/etcd-data \
--name ectd gcr.io/etcd-development/etcd:3.5.18 \
/usr/local/bin/etcd \
--name ectd0
--data-dir=/etcd-data \
--initial-advertise-peer-urls http://10.0.1.10:2380 \
--listen-peer-urls http://0.0.0.0:2380 \
--advertise-client-urls http://10.0.1.10:2379 \
--listen-client-urls http://0.0.0.0:2379 \
--initial-cluster etcd0=http://10.0.1.10:2380,etcd1=http://10.0.1.11:2380,etcd2=http://10.0.1.12:2380 \
--initial-cluster-state new \
--initial-cluster-token etcd-cluster-1
原理:
搭建步骤:
# 1、安装 dnsmasq 来提供 DNS服务
sudo yum install -y dnsmasq
# 2、编辑`/etc/dnsmasq.conf`文件,添加以下配置以支持SRV记录
cat > /etc/dnsmasq.conf << EOF
# SRV记录(核心发现机制)
_etcd-server-ssl._tcp.example.com. 300 IN SRV 0 0 2380 etcd-1.example.com.
_etcd-server-ssl._tcp.example.com. 300 IN SRV 0 0 2380 etcd-2.example.com.
_etcd-server-ssl._tcp.example.com. 300 IN SRV 0 0 2380 etcd-3.example.com.
# A记录(节点IP解析)
etcd-1.example.com. 300 IN A 10.0.1.10
etcd-2.example.com. 300 IN A 10.0.1.11
etcd-3.example.com. 300 IN A 10.0.1.12
EOF
# 3、启动 dnsmasq,且添加开机自启
sudo systemctl start dnsmasq
sudo systemctl enable dnsmasq
# 4、下载安装 etcd
ETCD_VER=v3.5.18
curl -L https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd-download-test --strip-components=1
# 5、启动ectd节点
etcd \
--name etcd-1 \
--discovery-srv example.com \ # 指定DNS域名
--initial-cluster-token etcd-cluster \ # 集群唯一标识
--initial-advertise-peer-urls https://etcd-1.example.com:2380 \
--listen-peer-urls https://0.0.0.0:2380 \
--listen-client-urls https://0.0.0.0:2379 \
--advertise-client-urls https://etcd-1.example.com:2379 \
--cert-file=/etc/etcd/ssl/etcd.pem \ # TLS证书
--key-file=/etc/etcd/ssl/etcd-key.pem \
--peer-cert-file=/etc/etcd/ssl/etcd.pem \
--peer-key-file=/etc/etcd/ssl/etcd-key.pem \
--trusted-ca-file=/etc/etcd/ssl/ca.pem \
--peer-trusted-ca-file=/etc/etcd/ssl/ca.pem
# 6、查看集群成员列表
ETCDCTL_API=3 etcdctl \
--endpoints=https://etcd-1.example.com:2379 \
--cacert=/etc/etcd/ssl/ca.pem \
--cert=/etc/etcd/ssl/etcd.pem \
--key=/etc/etcd/ssl/etcd-key.pem \
member list
注意事项:
常见故障排查:
# 检查SRV记录解析
dig +short SRV _etcd-server-ssl._tcp.example.com
# 检查A记录解析
dig +short etcd-1.example.com
journalctl -u etcd
--initial-advertise-peer-urls
与SRV记录中的地址完全一致。原理:新的etcd节点在启动时不会直接指定集群中其他节点的地址,而是通过一个预先存在的etcd集群(通常称为“bootstrap集群”或“辅助集群”)来获取这些信息。这个预先存在的etcd集群充当了服务发现的媒介,新的节点通过向它发送请求来获取集群的当前状态和其他节点的地址。
注意:etcd v3版本之后,官方推荐使用etcd的DNS SRV记录或静态配置(通过命令行参数或配置文件)来进行服务发现,而不是discovery URL。即discovery URL的方式在etcd v3版本中已经不再是推荐的做法,且可能在未来的版本中被移除。
ectd 启动命令:
# etcd1 启动
$ /opt/etcd/bin/etcd --name etcd1 \
--data-dir /opt/etcd/data \
--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://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
--discovery
参数来指定一个discovery URL。
discovery URL的格式通常是 https://
是discovery服务的地址,discovery服务是etcd提供的一个特定服务,如果不想自己搭建,也可以 使用 etcd官方提供的公共discovery服务(discovery.etcd.io)
是一个唯一的标识符,用于区分不同的集群,可用UUID。启动etcd服务前,需先在 discovery服务上设置一个特定的键(key),该键用于配置 discovery 服务中某个集群的大小。
完整示例:
# 生成一个新的discovery令牌
uuid=$(uuidgen)
cluster_size=3
# 指定预期的集群大小
curl -X PUT "http://:2379/v2/keys/_etcd/registry/${uuid}/_config/size" -d value=${cluster_size}
# 启动etcd节点时使用discovery URL
etcd --discovery "http:///v2/keys/${uuid}" --name <node-name> --initial-advertise-peer-urls <peer-url>
etcd v3 技术架构图:
etcd的架构可以按照分层模型进行划分,主要包括以下几个层次:
ETCD 作为高可用的分布式键值存储系统,其处理客户端请求的全过程严格遵循 Raft 协议,确保数据强一致性。
数据处理流程图解:
客户端请求
│
▼
ETCD 节点(Leader/Follower)
│
├─ 写请求 → Leader 处理:
│ 1. 写 WAL → 2. Raft 复制 → 3. Apply 到 BoltDB → 4. 响应客户端
│
└─ 读请求:
├─ 线性读:Leader 确认身份 → 读 BoltDB → 响应
└─ 串行读:直接读本地 BoltDB → 响应
详细的数据处理流程:
关键组件协作:
数据一致性保障:
性能优化:
ETCD gRPC Proxy 是一个面向高性能场景的智能代理层,通过请求合并、响应缓存、负载均衡等机制显著提升集群的读吞吐量,同时简化客户端逻辑。
适用于需要缓解 Leader 压力、优化高频读操作的场景(如 Kubernetes 大规模集群)。
结合监控和调优,可最大化发挥其性能优势。
(1)请求合并(Request Coalescing)
(2)响应缓存(Caching)
(3)负载均衡
(4)透明重试与故障转移
(5)指标与监控
(1)架构示意图
+------------+ +------------+
| Client A | | Client B |
+-----+------+ +------+-----+
| |
| |
+-----+---------------------+-----+
| ETCD gRPC Proxy |
| (gRPC Layer, Cache, Load Balancer)|
+-----+---------------------+-----+
| |
+----------+----------+ +-----+--------+
| ETCD Node 1 | | ETCD Node 2 |
| (Leader/Follower) | | (Follower) |
+----------------------+ +---------------+
(2)核心组件
(1)启动命令示例
# 启动 gRPC Proxy,指定 ETCD 集群节点
etcd grpc-proxy start \
--endpoints=http://etcd-node1:2379,http://etcd-node2:2379 \
--listen-addr=0.0.0.0:23790 \
--cache-size=10000 \
--enable-caching=true \
--metrics=extensive
etcd grpc-proxy start \
--discovery-srv=example.com \ # 指定 DNS 域名
--listen-addr=0.0.0.0:23790 \ # Proxy 监听地址
--advertise-client-url=http://proxy.example.com:23790 \ # 客户端访问地址
--cluster-service-name=etcd-server \ # 服务名(与 SRV 记录中的 _etcd-server 对应)
--dns-tls=skip-verify \ # 可选:跳过 DNS 查询的 TLS 验证
--cache-size=10000 \ # 启用缓存
--metrics=extensive # 输出详细指标
etcd grpc-proxy start \
--discovery-srv=example.com \
--listen-addr=0.0.0.0:23790 \
--cluster-service-name=etcd-server \
--cert-file=/etc/etcd/proxy.crt \ # Proxy 的客户端证书
--key-file=/etc/etcd/proxy.key \ # Proxy 的私钥
--trusted-ca-file=/etc/etcd/ca.crt \ # ETCD 集群的 CA 证书
--client-cert-auth \ # 要求 ETCD 验证 Proxy 的证书
--dns-tls=secure # 启用 DNS-over-TLS
(2)关键参数
参数 | 说明 |
---|---|
--endpoints |
ETCD 集群的初始节点列表(用于动态发现成员) |
--listen-addr |
Proxy 监听的地址和端口(默认 0.0.0.0:23790) |
--cache-size |
缓存的最大键值条目数(默认 0,即不启用缓存) |
--enable-caching |
启用读请求缓存(需配合 --cache-size) |
--metrics |
指标输出模式(basic/extensive) |
--retry-delay |
请求失败后的重试间隔(默认 1s) |
--advertise-client-url |
对外公布的客户端访问地址(用于集群内通信) |
--discovery-srv |
DNS 域名(如 example.com),用于查找 _etcd-server._tcp.example.com SRV 记录。 |
--cluster-service-name |
服务名称前缀(与 SRV 记录中的 _ 匹配,默认为 etcd-server)。 |
--dns-tls |
DNS 查询的 TLS 验证模式,可选值:none、skip-verify(跳过验证)、secure(需配置 CA)。 |
--advertise-client-url |
Proxy 对外公布的客户端访问地址(用于服务注册或客户端连接)。 |
(3)客户端连接示例
客户端直接连接 Proxy 地址(而非具体 ETCD 节点):
# 使用 etcdctl 客户端
ETCDCTL_API=3 etcdctl --endpoints=http://proxy:23790 get mykey
(1)高频读场景
(2)提升读吞吐量
(3)弱化客户端逻辑
(1)线性一致性读优化
(2) 限流与熔断
(3)TLS 终止
etcd grpc-proxy start \
--listen-addr=0.0.0.0:23790 \
--endpoints=https://etcd-node:2379 \
--cert-file=client.crt \
--key-file=client.key \
--trusted-ca-file=ca.crt
(1)缓存策略优化
(2)资源分配
(3)监控指标关注
指标名称 | 说明 | 健康阈值 |
---|---|---|
grpc_proxy_cache_hits | 缓存命中次数 | 命中率 > 60% |
grpc_proxy_request_duration | 请求处理延迟 | P99 < 100ms |
grpc_proxy_upstream_errors | 后端 ETCD 节点错误次数 | 持续增长需报警 |
grpc_proxy_coalesced_requests | 合并的请求数 | 值越高说明优化效果越好 |
特性 | ETCD Gateway | ETCD gRPC Proxy |
---|---|---|
协议层 | TCP 层代理 | gRPC 层代理(理解协议内容) |
动态成员发现 | 支持 | 支持 |
高级功能 | 无 | 支持缓存、请求合并、故障注入等 |
TLS 终止 | 不支持 | 支持 |
适用场景 | 简单转发、隐藏集群拓扑 | 高频读、需要缓存和协议优化的场景 |
(1)Kubernetes 集群优化
ETCD Gateway 是 ETCD 官方提供的一个轻量级代理组件,主要用于简化客户端与 ETCD 集群之间的连接管理和负载均衡。它的核心目标是降低客户端配置复杂度,并在集群节点变更时提供透明的连接重定向能力。
ETCD Gateway 功能较为基础,对于需要高级流量控制或协议优化的场景,建议结合 gRPC Proxy 使用。
(1)解决的问题
(2)适用场景
(1) 透明代理
(2)节点发现机制
(3)请求转发逻辑
(1)架构示意图
+------------+ +------------+
| Client A | | Client B |
+-----+------+ +------+-----+
| |
| |
+-----+---------------------+-----+
| ETCD Gateway |
| (Proxy Layer, Port 23790) |
+-----+---------------------+-----+
| |
+----------+----------+ +-----+--------+
| ETCD Node 1 | | ETCD Node 2 |
| (Leader/Follower) | | (Follower) |
+----------------------+ +---------------+
(2)高可用部署
(1)启动 Gateway
$ etcd gateway start --help
start the gateway
Usage:
etcd gateway start [flags]
Flags:
--discovery-srv string DNS domain used to bootstrap initial cluster
--discovery-srv-name string service name to query when using DNS discovery
--endpoints strings comma separated etcd cluster endpoints (default [127.0.0.1:2379])
-h, --help help for start
--insecure-discovery accept insecure SRV records
--listen-addr string listen address (default "127.0.0.1:23790") 默认监听端口
--retry-delay duration duration of delay before retrying failed endpoints (default 1m0s) 重试连接到失败的端点延迟时间,默认为 1m0s
--trusted-ca-file string path to the client server TLS CA file for verifying the discovered endpoints when discovery-srv is provided.
底层实现就是启动两个协程:一个定时监控etcd集群的服务器是否存活,一个负责处理请求。
在启动网关的时候,需要获取对应的ip地址(两种方式,一种是通过dns服务,一种是直接域名)。
(2)示例
etcd gateway start \
--endpoints=http://etcd-node1:2379,http://etcd-node2:2379 \
--listen-addr=0.0.0.0:23790
# 若 etcd 集群启用 TLS,需为网关配置相同的 CA 证书、客户端证书和密钥
etcd gateway start \
--discovery-srv example.com \ # 指定 DNS 域名(SRV 记录所在域)
--listen-addr 0.0.0.0:23790 \ # 网关监听地址(客户端连接此端口)
--cert-file /etc/etcd/ssl/client.pem \ # 客户端证书
--key-file /etc/etcd/ssl/client-key.pem \ # 客户端私钥
--cacert-file /etc/etcd/ssl/ca.pem \ # CA 根证书
--insecure-discovery=false \ # 强制启用 TLS 验证(默认 false)
--debug # 可选:开启调试日志
(3)关键参数
参数 | 说明 |
---|---|
–endpoints | ETCD 集群的初始节点地址列表(用于动态发现成员) |
–listen-addr | Gateway 监听的地址和端口(默认 0.0.0.0:23790) |
–retry-delay | 节点连接失败后的重试延迟(默认 1s) |
–debug | 启用调试日志 |
–discovery-srv | 指定用于查询 SRV 记录的 DNS 域名(如 example.com)。 |
–listen-addr | 网关监听的客户端连接地址(默认 0.0.0.0:23790)。 |
TLS 相关参数 | 与 etcd 集群通信所需的证书和密钥,需确保与集群配置一致。 |
–insecure-discovery | 设置为 false 时强制验证 TLS 证书;若为 true 则禁用验证(仅测试环境使用)。 |
(4)检查网关日志
journalctl -u etcd-gateway -f
预期输出:
INFO: Resolved SRV records: [etcd-1.example.com:2380 etcd-2.example.com:2380 etcd-3.example.com:2380]
INFO: Updated endpoints: [https://etcd-1.example.com:2379 https://etcd-2.example.com:2379 https://etcd-3.example.com:2379]
(5)客户端连接示例
客户端直接连接 Gateway 地址(而非具体 ETCD 节点):
# 使用 etcdctl 客户端
ETCDCTL_API=3 etcdctl --endpoints=http://gateway:23790 get mykey
(1)功能限制
(2)性能影响
特性 | ETCD Gateway | ETCD gRPC Proxy |
---|---|---|
协议层 | TCP 层代理 | gRPC 层代理(理解协议内容) |
动态成员发现 | 支持 | 支持 |
高级功能 | 无 | 支持缓存、请求合并、故障注入等 |
TLS 终止 | 不支持 | 支持 |
适用场景 | 简单转发、隐藏集群拓扑 | 需要高级流量控制的场景 |
Watch 机制 允许客户端实时监听键(Key)或键范围(Key Range)的变化事件(如 Put、Delete 等),并接收异步通知。其设计目标是:
(1)客户端发起监听
示例命令(etcdctl):
# 监听键 /foo 的变更(仅新事件)
etcdctl watch /foo
# 监听前缀 /service/ 下的所有键,从修订版本 1000 开始
etcdctl watch --prefix /service/ --rev=1000
(2) 服务端处理监听
(3)客户端处理事件
(1) 事件类型
类型 | 触发条件 | 说明 |
---|---|---|
PUT | 键被创建或更新 | 包含新值(KeyValue) |
DELETE | 键被删除 | 包含被删除的键及其最后版本信息 |
(2)事件数据结构(protobuf)
message Event {
enum EventType {
PUT = 0;
DELETE = 1;
}
EventType type = 1;
KeyValue kv = 2; // 仅 PUT 事件有效
KeyValue prev_kv = 3; // 仅当监听时指定 prev_kv 时有效
}
(1) MVCC(多版本并发控制)
(2) Watcher 注册与事件分发
(3)历史事件回放
(1)可靠事件传递
(2)过滤与条件监听
(3)性能优化
(1)服务发现与健康检查
(2)配置中心
(3)分布式协同
(1)gRPC 接口
message WatchRequest {
oneof request_union {
WatchCreateRequest create_request = 1; // 创建监听
WatchCancelRequest cancel_request = 2; // 取消监听
}
}
(2)Go 客户端示例
import (
"context"
"go.etcd.io/etcd/clientv3"
)
func main() {
cli, _ := clientv3.New(clientv3.Config{Endpoints: []string{"localhost:2379"}})
defer cli.Close()
// 监听前缀 /service/ 的所有变更
watchChan := cli.Watch(context.Background(), "/service/", clientv3.WithPrefix())
for resp := range watchChan {
for _, ev := range resp.Events {
switch ev.Type {
case clientv3.EventTypePut:
fmt.Printf("PUT: %s -> %s\n", ev.Kv.Key, ev.Kv.Value)
case clientv3.EventTypeDelete:
fmt.Printf("DELETE: %s\n", ev.Kv.Key)
}
}
}
}
(1)监听性能优化
(2)错误处理
(3)安全与权限
etcdctl role add watch-role
etcdctl role grant-permission watch-role --prefix read /data/
etcdctl user add user1
etcdctl user grant-role user1 watch-role
Kubernetes 重度依赖 ETCD 的 Watch 机制实现组件协同:
Lease(租约) 是 ETCD 中用于管理键值对(Key-Value)生命周期的机制。
通过为键值对绑定 Lease,可以自动删除过期的键,实现以下功能:
(1)创建 Lease
# 创建一个 TTL 为 60 秒的 Lease
etcdctl lease grant 60
输出:lease 32695410dcc0ca06 granted with TTL(60s)
返回值:唯一 Lease ID(如 32695410dcc0ca06)。
(2)将键绑定到 Lease
# 将键 /service/node1 绑定到 Lease
etcdctl put /service/node1 10.1.1.1 --lease=32695410dcc0ca06
效果:当 Lease 过期时,所有绑定的键会被自动删除。
(3)续约(KeepAlive)
etcdctl lease keep-alive 32695410dcc0ca06
(4)查看 Lease 信息
etcdctl lease timetolive 32695410dcc0ca06
输出:lease 32695410dcc0ca06 granted with TTL(60s), remaining(52s)etcdctl lease timetolive 32695410dcc0ca06 --keys
输出:… attached keys: [/service/node1](5)撤销 Lease
主动释放:
etcdctl lease revoke 32695410dcc0ca06
效果:立即删除所有绑定的键,并释放 Lease。
(1)存储结构
(2)过期检测
(3)续约逻辑
(1)gRPC 服务
ETCD 通过 LeaseService 提供 gRPC 接口:
(2)客户端库示例(Go)
import (
"context"
"time"
"go.etcd.io/etcd/clientv3"
)
func main() {
cli, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
defer cli.Close()
// 创建 Lease(TTL=10秒)
lease, _ := cli.Grant(context.TODO(), 10)
// 绑定键
cli.Put(context.TODO(), "/service/node1", "10.1.1.1", clientv3.WithLease(lease.ID))
// 自动续约(每 5 秒一次)
keepAlive, _ := cli.KeepAlive(context.TODO(), lease.ID)
go func() {
for range keepAlive {
// 续约成功
}
}()
}
(1)服务注册与发现
(2)分布式锁
(3)临时配置项
(1)TTL 设置
(2)性能影响
(3)一致性与容错
(1)Lease 存储结构
type Lease struct {
ID int64 // Lease ID
TTL int64 // 初始 TTL(秒)
Expiry time.Time // 到期时间
KeyToHash map[string]struct{}// 绑定的键(哈希集合)
}
(2)过期键删除流程
(3)续约的 Raft 流程
(1)关键监控指标
指标名称 | 说明 | 健康阈值 |
---|---|---|
etcd_server_lease_expired_total | 已过期的 Lease 总数 | 突增可能表示客户端异常 |
etcd_server_lease_renewed_total | 续约次数 | 反映客户端活跃度 |
etcd_server_lease_ttl_buckets | Lease TTL 分布直方图 | 检查 TTL 设置是否合理 |
(2)调试命令
etcdctl lease list
etcdctl lease timetolive <LEASE_ID> --keys
分布式系统领域,etcd 事务是保障数据一致性的核心机制。
etcd的事务是基于乐观锁实现的,它的事务调用方式是经典的CAS,不支持回滚,即要么全部成功要么全部失败。
语法:
Txn().If(cond1, cond2, ...).Then(op1, op2, ...,).Else(op1, op2)
Txn()
: 开始一个事务If(cond1, cond2, ...)
: 定义事务的提交条件。这些条件通常是关于某个 key 的比较操作,例如检查 key 是否存在,或者当前的值是否等于预期的值。Then(op1, op2, ...)
: 如果所有条件都满足,那么就执行这里的操作。操作可以是 put、delete 或者 get 等。Else(op1, op2)
: 如果条件不满足,那么就执行这里的操作。示例:
client.Txn(ctx).If(
clientv3.Compare(clientv3.Value("/registry/pods/nginx"), "=", "v1"),
clientv3.Compare(clientv3.Version("/registry/locks"), "=", 2)
).Then(
clientv3.OpPut("/registry/config", "updated"),
clientv3.OpDelete("/registry/temp")
).Else(
clientv3.OpGet("/registry/status")
).Commit()
▶ 服务端执行阶段:
为了简化 etcd 事务实现的过程,etcd v3 提供了 STM,会自动处理冲突以及重试。
// STM is an interface for software transactional memory.
type STM interface {
// Get returns the value for a key and inserts the key in the txn's read set.
// If Get fails, it aborts the transaction with an error, never returning.
Get(key ...string) string
// Put adds a value for a key to the write set.
Put(key, val string, opts ...v3.OpOption)
// Rev returns the revision of a key in the read set.
Rev(key string) int64
// Del deletes a key.
Del(key string)
// commit attempts to apply the txn's changes to the server.
commit() *v3.TxnResponse
reset()
}
stm后两个级别第一次都是是线性读,后面才是串行读。
每次读写请求都会创建事务,只有写事务会导致全局的事务版本号增加,串行读不会导致版本号增加。
stm的事务隔离级别,分为四种:
Serializable和SerializableSnapshot 一个对读进行检查,一个对读写都进行检查。
上面的检查不通过,会进行重试,直到通过为止。
stm的读请求都是一次事务,写请求是放在一起进行提交的。
func (s *stm) Get(keys ...string) string {
if wv := s.wset.get(keys...); wv != nil {
return wv.val
}
return respToValue(s.fetch(keys...))
}
func (ws writeSet) get(keys ...string) *stmPut {
for _, key := range keys {
if wv, ok := ws[key]; ok {
return &wv
}
}
return nil
}
func (s *stm) fetch(keys ...string) *v3.GetResponse {
if len(keys) == 0 {
return nil
}
ops := make([]v3.Op, len(keys))
for i, key := range keys {
if resp, ok := s.rset[key]; ok {
return resp
}
ops[i] = v3.OpGet(key, s.getOpts...)
}
txnresp, err := s.client.Txn(s.ctx).Then(ops...).Commit()
if err != nil {
panic(stmError{err})
}
s.rset.add(keys, txnresp)
return (*v3.GetResponse)(txnresp.Responses[0].GetResponseRange())
}
func (s *stm) commit() *v3.TxnResponse {
txnresp, err := s.client.Txn(s.ctx).If(s.conflicts()...).Then(s.wset.puts()...).Commit()
if err != nil {
panic(stmError{err})
}
if txnresp.Succeeded {
return txnresp
}
return nil
}
条件索引优化:优先使用版本比较而非值比较
操作合并技巧:将多个 key 更新合并到单个事务
事务大小控制:建议单个事务不超过 1MB 数据量
线性读优化:通过 quorum read 提升读性能
# 事务冲突检测
ERROR: rpc error: code = Aborted desc = etcdserver: request timed out
# 处理建议:
1. 指数退避重试策略
2. 事务拆分(将非必要操作移出事务)
3. 增加 etcd 集群时钟同步精度
4. 监控 metrics: etcd_server_slow_apply_total
5. 内核级增强特性
etcd 事务机制深度集成了 Raft 共识算法与 MVCC 存储模型,在 Kubernetes 等云原生系统中实现了以下关键功能:
建议在生产环境中结合 etcdctl 的 txn 命令进行事务调试,并通过 grafana 监控 etcd_txn_total 等关键指标,实现事务性能的持续优化。
服务发现(Service Discovery)要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。从本质上说,服务发现就是想要了解集群中是否有进程在监听udp或tcp端口,并且通过名字就可以进行查找和连接。
要解决服务发现的问题,需要有下面三大支柱,缺一不可。
在分布式系统中,最为适用的组件间通信方式是消息发布与订阅机制。具体而言,即构建一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们关心的主题,一旦相关主题有消息发布,就会实时通知订阅者。通过这种方式可以实现分布式系统配置的集中式管理与实时动态更新。
在分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。这样的实现虽然会导致一定程度上数据写入性能的下降,但是却能实现数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上。
不同系统都在etcd上对同一个目录进行注册,同时设置Watcher监控该目录的变化(如果对子目录的变化也有需要,可以设置成递归模式),当某个系统更新了etcd的目录,那么设置了Watcher的系统就会收到通知,并作出相应处理。
因为etcd使用Raft算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。
分布式队列的常规用法与场景五中所描述的分布式锁的控制时序用法类似,即创建一个先进先出的队列,保证顺序。
另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行。这种方法的实现可以在/queue这个目录中另外建立一个/queue/condition节点。
通过etcd来进行监控实现起来非常简单并且实时性强,用到了以下两点特性。
前面几个场景已经提到Watcher机制,当某个节点消失或有变动时,Watcher会第一时间发现并告知用户。。
节点可以设置TTL key,比如每隔30s向etcd发送一次心跳使代表该节点仍然存活,否则说明节点消失。
这样就可以第一时间检测到各节点的健康状态,以完成集群的监控要求。
另外,使用分布式锁,可以完成Leader竞选。对于一些长时间CPU计算或者使用IO操作,只需要由竞选出的Leader计算或处理一次,再把结果复制给其他Follower即可,从而避免重复劳动,节省计算资源。
Leader应用的经典场景是在搜索系统中建立全量索引。如果每个机器分别进行索引的建立,不但耗时,而且不能保证索引的一致性。通过在etcd的CAS机制竞选Leader,由Leader进行索引计算,再将计算结果分发到其它节点。
etcd实现的这些功能,Zookeeper都能实现。那么为什么要用etcd而非直接使用Zookeeper呢?
Zookeeper 缺点:
etcd 优点: