☰
etcd 故障注入与一致性验证:functional 测试框架与 etcd-proxy 网络故障模拟实战
2026/10/15 6:13:44 网站建设 项目流程
  • 云原生
  • 后端
  • 微服务
  • DevOps
  • AI 应用

【免费下载链接】rainbond

无需学习 Kubernetes 的容器平台,在 Kubernetes 上构建、部署、组装和管理应用,无需 K8s 专业知识,全流程图形化管理

项目地址:https://gitcode.com/goodrain/rainbond
点击查看免费下载

etcd 的functional测试套件(本仓库内位于 third_party/coreos-etcd/functional)专门用于验证 etcd 在各类系统故障与网络故障下的正确行为:它会在高压力负载下搭建 etcd 集群,持续向集群注入故障,并要求集群在数秒内完成恢复。本文将完整讲解这套框架的组件架构、全部故障用例、配置清单,以及配套的 etcd-proxy 网络故障模拟工具,并给出可直接运行的本地与 Docker 实操步骤。

etcd functional testing 的定位与核心思想

etcd 是分布式系统的基础组件,其正确性依赖于 Raft 共识协议在"网络分区、节点宕机、进程被杀、数据丢失"等异常场景下依然成立。常规单元测试与集成测试很难覆盖这类故障组合,因此 etcd 官方在functional目录下实现了一套独立的故障注入(failure injection)与一致性验证框架。

其核心思路(见 functional/README.md):

在高压力负载下搭建 etcd 集群,持续向集群注入各类系统与网络故障,然后期望 etcd 集群在数秒内自行恢复。这套方法曾多次帮助定位到极其隐蔽的关键 Bug。

也就是说,functional 测试不是一个"跑一次就结束"的用例,而是一个长时间循环的混沌测试(chaos testing)引擎:每一轮(round)选择若干故障用例(case),注入 → 等待 → 恢复 → 校验一致性,如此往复。

全部故障用例的枚举定义在 rpcpb.Case,而一份可直接运行的示例配置是仓库根部的 functional.yaml。

框架架构:tester、agent、proxy、runner 四组件

从 functional 目录的源码结构可以清晰地看到这套框架由四个独立进程协作:

组件入口源码职责
etcd-testercmd/etcd-tester/main.go测试编排者(client 端),读取配置、调度故障用例、驱动压力器与一致性校验
etcd-agentcmd/etcd-agent/main.go部署在每台被测节点上的代理(server 端),负责启停 etcd 进程、执行黑盒/延迟等网络操作
etcd-proxycmd/etcd-proxy/main.go网络故障模拟层,可对客户端或对等(peer)流量注入延迟、暂停、丢包
etcd-runnercmd/etcd-runner/main.go运行 election、watcher、lock-racer、lease-renewer 等负载场景

tester 与 agent 之间通过 gRPC 双向流通信,协议定义在 functional/rpcpb/rpc.proto:Transport服务承载Request/Response,其中Request携带Operation(要执行的操作)、Member(节点配置)与Tester(tester 侧配置)。agent 收到操作后分派执行,其操作分发逻辑在 agent/handler.go。

故障用例体系:rpcpb.Case 全分类

所有故障用例都以枚举形式定义在 rpcpb.Case,可以归为以下几大类:

1. 进程级故障(SIGTERM / SIGQUIT 系列)

  • SIGTERM_ONE_FOLLOWER/SIGTERM_LEADER:停止随机一个 follower 或当前 leader,但保留磁盘数据目录,等待delay-ms后重启恢复。SIGTERM leader 场景会触发选举超时,期望行为是新 leader 被选出、旧 leader 回归为 follower。
  • SIGTERM_QUORUM/SIGTERM_ALL:停止多数派节点或全部节点,使集群整体不可用,恢复后集群重新可用。
  • SIGQUIT_AND_REMOVE_ONE_FOLLOWER/SIGQUIT_AND_REMOVE_LEADER:杀进程并删除数据目录、从集群中移除成员(成员重组),恢复时以全新数据加入新成员,用于模拟"整台机器被销毁后运维新加机器"的场景。
  • SIGQUIT_AND_REMOVE_QUORUM_AND_RESTORE_LEADER_SNAPSHOT_FROM_SCRATCH:最极端的灾难恢复场景——多数派节点连同数据全部销毁,集群完全不可用,只能从幸存 leader 的最新快照恢复种子成员,再逐步扩容回 3 节点集群。

2. 网络黑洞(blackhole)系列

对目标节点的 peer 端口完全丢弃收发数据包,等价于网络分区:

  • BLACKHOLE_PEER_PORT_TX_RX_ONE_FOLLOWER:隔离一个 follower;
  • BLACKHOLE_PEER_PORT_TX_RX_LEADER:隔离 leader,触发选举超时后新 leader 产生;
  • BLACKHOLE_PEER_PORT_TX_RX_QUORUM/..._ALL:隔离多数派或全部节点,使集群暂时不可用。

3. 网络延迟(delay)系列

对 peer 端口注入固定或随机延迟,模拟慢网络:

  • DELAY_PEER_PORT_TX_RX_ONE_FOLLOWER/RANDOM_DELAY_...:延迟一个 follower;
  • DELAY_PEER_PORT_TX_RX_LEADER/RANDOM_DELAY_...:延迟 leader,可能触发重新选举;
  • DELAY_PEER_PORT_TX_RX_QUORUM/..._ALL:延迟多数派或全网节点。

4. 触发快照(UNTIL_TRIGGER_SNAPSHOT)变体

SIGTERM_*_UNTIL_TRIGGER_SNAPSHOT、BLACKHOLE_*_UNTIL_TRIGGER_SNAPSHOT等用例不只等待固定delay-ms,而是持续注入压力直到 leader 提交的条目数超过snapshot-count,强制触发快照,从而验证"落后节点通过快照追赶主日志"的机制。相关实现见 tester/case.go 中的 caseUntilSnapshot:其判定逻辑是持续比较lastRev - startRev > snapshotCount,且对慢网络场景把重试次数放大 5 倍。

5. 无故障与特殊用例

  • NO_FAIL_WITH_STRESS:不注入故障但持续打压力,验证集群在恢复期不出现死锁;
  • NO_FAIL_WITH_NO_STRESS_FOR_LIVENESS:既不注入故障也不打压力,验证集群活性(README 注释指出这是参考 Google Chubby 的故障注入测试方法);
  • FAILPOINTS:向 etcd 运行时注入 gofail failpoint,触发关键代码路径 panic;
  • EXTERNAL:运行外部故障注入脚本。

从源码看,每个用例都实现 tester/case.go 中的 Case 接口:Inject(注入故障)、Recover(恢复故障)、Desc(描述)、TestCase(枚举类型)。框架通过caseByFunc、caseFollower、caseLeader、caseQuorum、caseAll几个组合器将"故障动作"与"目标选择策略"解耦(case.go),并用caseDelay包裹实现"注入后等待 delay-ms"(case_delay.go)。

functional.yaml 配置详解

functional.yaml 是 tester 的示例配置,包含agent-configs(每个节点的 etcd 与 agent 配置)与tester-config(测试编排配置)两部分。以第一个节点为例:

agent-configs: - etcd-exec-path: ./bin/etcd agent-addr: 127.0.0.1:19027 failpoint-http-addr: http://127.0.0.1:7381 base-dir: /tmp/etcd-functional-1 etcd-log-path: /tmp/etcd-functional-1/etcd.log etcd-client-proxy: false etcd-peer-proxy: true etcd-client-endpoint: 127.0.0.1:1379 etcd: name: s1 >PASSES=functional ./test

PASSES=functional会只跑 functional 测试目标。该命令依赖仓库根部的 test 脚本按PASSES变量分派测试任务。

运行方式二:Docker 运行

先构建 functional 的 Docker 镜像(在仓库根目录执行):

pushd .. make build-docker-functional popd

build-docker-functional目标定义在 Makefile:它先把functional/Dockerfile中的 Go 版本占位符替换为当前GO_VERSION,再构建gcr.io/etcd-development/etcd-functional:go$(GO_VERSION)镜像。

然后运行示例脚本启动 3 个 agent(对应 3 节点本地 etcd 集群):

# run 3 agents for 3-node local etcd cluster ./scripts/docker-local-agent.sh 1 ./scripts/docker-local-agent.sh 2 ./scripts/docker-local-agent.sh 3 # to run only 1 tester round ./scripts/docker-local-tester.sh

其中 scripts/docker-local-agent.sh 会以--net=host方式启动容器并执行./bin/etcd-agent --network tcp --address 127.0.0.1:{1|2|3}9027,scripts/docker-local-tester.sh 则启动./bin/etcd-tester --config ./functional.yaml执行一轮测试。

etcd-proxy:模拟各类网络条件的代理层

etcd-proxy是一个位于 etcd 与客户端/对等节点之间的 TCP 代理层,专门用于模拟各种网络状况,是 functional 框架中网络类故障(delay/blackhole)的底层执行者。其构建与自测步骤如下(完整示例见 cmd/etcd-proxy/main.go 的 Usage):

$ ./build $ ./bin/etcd $ make build-functional $ ./bin/etcd-proxy --help $ ./bin/etcd-proxy --from localhost:23790 --to localhost:2379 --http-port 2378 --verbose $ ETCDCTL_API=3 ./bin/etcdctl --endpoints localhost:2379 put foo bar $ ETCDCTL_API=3 ./bin/etcdctl --endpoints localhost:23790 put foo bar

命令行参数含义:

  • --from:代理对外监听地址(默认localhost:23790);
  • --to:实际转发目标(默认localhost:2379);
  • --http-port:etcd-proxy 管理 API 的 HTTP 端口(默认2378);
  • --verbose:开启 verbose 日志。

代理层通过 HTTP 管理接口暴露 6 个故障注入端点,每个端点都支持tx(发送方向)与rx(接收方向)变体,方法语义统一为PUT/POST注入、GET查询、DELETE撤销:

端点注入效果撤销方式
/delay-tx、/delay-rx在发送/接收方向注入延迟DELETE
/pause-tx、/pause-rx暂停转发(挂起连接)DELETE
/blackhole-tx、/blackhole-rx直接丢弃数据包DELETE

代理开销:单请求低于 500μs

README 明确记录"Proxy overhead per request is under 500μs"。这一结论可以通过仓库自带的 tools/benchmark 复现:分别对直连端点localhost:2379与代理端点localhost:23790施加相同负载并对比:

$ go build -v -o ./bin/benchmark ./tools/benchmark $ ./bin/benchmark \ --endpoints localhost:2379 \ --conns 5 \ --clients 15 \ put \ --key-size 48 \ --val-size 50000 \ --total 10000

README 记录的一次直连基准输出为:总耗时 8.4611 秒,平均 0.0121 秒,吞吐约 1181.88 req/s;同样负载经过localhost:23790代理后为:总耗时 9.1128 秒,平均 0.0131 秒,吞吐约 1097.36 req/s,可见代理引入的开销很小,符合"单请求低于 500μs"的预期。

实战一:注入发送方向延迟(delay-tx)

$ curl -L http://localhost:2378/delay-tx -X PUT \ -d "latency=5s&random-variable=100ms" # added send latency 5s±100ms (current latency 4.92143955s) $ curl -L http://localhost:2378/delay-tx # current send latency 4.92143955s $ ETCDCTL_API=3 ./bin/etcdctl \ --endpoints localhost:23790 \ --command-timeout=3s \ put foo bar # Error: context deadline exceeded $ curl -L http://localhost:2378/delay-tx -X DELETE # removed latency 4.92143955s $ curl -L http://localhost:2378/delay-tx # current send latency 0s $ ETCDCTL_API=3 ./bin/etcdctl \ --endpoints localhost:23790 \ --command-timeout=3s \ put foo bar # OK

注入 5s±100ms 的发送延迟后,由于客户端命令超时设为 3 秒,put操作如期返回context deadline exceeded;撤销延迟后立即恢复OK。对应实现见 etcd-proxy 的 /delay-tx 处理器:通过time.ParseDuration解析latency与random-variable两个表单参数,再调用p.DelayTx(lat, rv),GET返回当前延迟p.LatencyTx()。

实战二:暂停客户端转发(pause-tx)

$ curl -L http://localhost:2378/pause-tx -X PUT # paused forwarding [tcp://localhost:23790 -> tcp://localhost:2379] $ ETCDCTL_API=3 ./bin/etcdctl \ --endpoints localhost:23790 \ put foo bar # Error: context deadline exceeded $ curl -L http://localhost:2378/pause-tx -X DELETE # unpaused forwarding [tcp://localhost:23790 -> tcp://localhost:2379]

暂停转发会挂起 TCP 连接不再转发数据,客户端请求全部超时;DELETE撤销后转发恢复。

实战三:丢弃客户端数据包(blackhole-tx)

$ curl -L http://localhost:2378/blackhole-tx -X PUT # blackholed; dropping packets [tcp://localhost:23790 -> tcp://localhost:2379] $ ETCDCTL_API=3 ./bin/etcdctl --endpoints localhost:23790 put foo bar # Error: context deadline exceeded $ curl -L http://localhost:2378/blackhole-tx -X DELETE # unblackholed; restart forwarding [tcp://localhost:23790 -> tcp://localhost:2379]

与 pause 不同,blackhole 是直接丢弃数据包(而非挂起),效果同样是客户端请求超时。

实战四:触发 leader 选举(隔离当前 leader)

利用 functional/Procfile-proxy 可以在本机拉起一个 3 节点 etcd 集群,并为每个节点分别配置 client 与 peer 两侧的代理。该 Procfile 的端口规划(以 s1 为例):etcd 监听1379/1380,对外宣称13790/13800,client 代理管理端口1378、peer 代理管理端口1381,s2/s3 依次使用 2 系列与 3 系列端口。

$ ./build $ make build-functional $ rm -rf /tmp/etcd-proxy-data.s* $ goreman -f ./functional/Procfile-proxy start $ ETCDCTL_API=3 ./bin/etcdctl \ --endpoints localhost:13790,localhost:23790,localhost:33790 \ member list # isolate s1 when s1 is the current leader $ curl -L http://localhost:1381/blackhole-tx -X PUT $ curl -L http://localhost:1381/blackhole-rx -X PUT # s1 becomes follower after election timeout

对 leader(假设为 s1)的 peer 代理管理端口1381同时注入blackhole-tx与blackhole-rx,等于把 s1 从集群中完全隔离。经过 election timeout 后,其余节点会选出新 leader,原 leader s1 降级为 follower。这正是 functional 框架中BLACKHOLE_PEER_PORT_TX_RX_LEADER用例在 case_network_blackhole.go 中的手动版:agent 收到Operation_BLACKHOLE_PEER_PORT_TX_RX后,对其持有的 peer 代理逐一调用px.BlackholeTx(); px.BlackholeRx()(见 agent/handler.go)。

测试主循环:压力、故障注入、一致性校验

tester 的主循环实现在 tester/cluster_run.go,每个 round 的执行顺序如下(doRound):

  1. 等健康:WaitHealth()确认集群全健康后才开始;
  2. 启压力:调用stresser.Stress()开始向集群打压力(NO_FAIL_WITH_NO_STRESS_FOR_LIVENESS除外);
  3. 注入故障:fa.Inject(clus)按当前用例注入;
  4. 恢复故障:fa.Recover(clus)撤销注入;
  5. 暂停压力并校验:stresser.Pause()暂停压力器,随后WaitHealth()等待集群恢复健康;
  6. 一致性检查:runCheckers()运行 KV_HASH / LEASE_EXPIRE 等校验器。

之后还会对每轮做 KV compact(按约 50000 entries/s 估算超时),并且每 500 轮执行一次 defrag。任一环节失败都会触发cleanup():先归档故障数据目录(SIGQUIT_ETCD_AND_ARCHIVE_DATA),再重启 etcd 并重建压力器与校验器。

压力器与校验器的类型都由配置驱动,对应源码:

  • 压力器:tester/stresser.go 中newStresser按Stressers配置创建keyStresser(KV)、leaseStresser(LEASE)以及基于 etcd-runner 的 ELECTION_RUNNER、WATCH_RUNNER、LOCK_RACER_RUNNER、LEASE_RUNNER;
  • 一致性校验器:tester/checker_kv_hash.go 的 KV_HASH 校验会拉取所有成员的 revision 与存储 hash,要求两者完全一致(最多重试 7 次,每次间隔 1 秒),一旦发现集群不稳定即判定失败;LEASE_EXPIRE 则校验租约按预期过期。

构建脚本与产物

functional/build 脚本一次构建全部四个二进制(要求从仓库根目录运行):

CGO_ENABLED=0 go build -v -installsuffix cgo -ldflags "-s" -o ./bin/etcd-agent ./cmd/functional/cmd/etcd-agent CGO_ENABLED=0 go build -v -installsuffix cgo -ldflags "-s" -o ./bin/etcd-proxy ./cmd/functional/cmd/etcd-proxy CGO_ENABLED=0 go build -v -installsuffix cgo -ldflags "-s" -o ./bin/etcd-runner ./cmd/functional/cmd/etcd-runner CGO_ENABLED=0 go build -v -installsuffix cgo -ldflags "-s" -o ./bin/etcd-tester ./cmd/functional/cmd/etcd-tester

Makefile 中的make build-functional即封装了该脚本,并在构建后逐一执行-help做冒烟验证。

总结

etcd functional 测试框架通过"agent 控制进程、proxy 控制网络、tester 编排调度、runner 生成负载、checker 校验一致性"的分层设计,把分布式系统最常见的故障——进程被杀、数据丢失、网络延迟、网络分区——转化为可枚举、可复现、可持续循环的混沌测试用例(rpcpb.Case)。而 etcd-proxy 既是这套框架的底层执行组件,也可以脱离框架独立使用,通过 6 个 HTTP 端点手动模拟延迟、暂停与丢包,用于本地调试客户端超时处理、验证 Raft 选举行为等场景。无论是自动化回归还是手动故障演练,这套工具链都提供了完整的实操路径。

  • 云原生
  • 后端
  • 微服务
  • DevOps
  • AI 应用

【免费下载链接】rainbond

无需学习 Kubernetes 的容器平台,在 Kubernetes 上构建、部署、组装和管理应用,无需 K8s 专业知识,全流程图形化管理

项目地址:https://gitcode.com/goodrain/rainbond
点击查看免费下载

相关推荐

上一篇:pandoc 的 `native_spans` 扩展:用原生 Span 语义精确控制 HTML 阅读器中 `<span>` 标签的解析
下一篇:ZeroOmega插件开发实战:如何创建自定义代理类型

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询