3. 压测工具选型与部署:benchmark 工具、ghz、自定义压测脚本的搭建与配置

在多集群 etcd 压测场景中,工具选型直接影响测试结果的可靠性与可复现性。本章围绕三类主流压测工具——etcd 官方 benchmark、通用 gRPC 压测工具 ghz、以及基于 clientv3 的自定义脚本——展开部署与配置说明,并给出选型对比与典型部署拓扑。

3.1 工具选型对比

维度 benchmark (etcd 官方) ghz (gRPC 通用) 自定义脚本 (clientv3)
协议层 直接调用 etcd v3 gRPC 通用 gRPC 反射/Protobuf etcd clientv3 封装
安装方式 go install / 预编译二进制 go install / 预编译二进制 自行编写 + go build
并发模型 固定 goroutine 池 worker 池 + 异步报告 完全可控 (协程/连接池)
指标输出 延迟分位值、QPS、错误数 延迟分布、RPS、状态码 自定义 Prometheus/日志
多集群支持 需手动切换 endpoint 多 target 轮询 原生多 client 管理
适用场景 单集群快速基准测试 跨集群 gRPC 接口压测 复杂业务模型、长稳测试
选型建议: 基准测试阶段优先使用 benchmark 工具;多集群横向对比时使用 ghz;需要模拟真实业务读写比例、事务、租约等复杂场景时,必须采用自定义脚本。

3.2 benchmark 工具部署与配置

etcd 官方 benchmark 工具位于 etcd/tools/benchmark 目录下,支持 put、range、txn、watch 等核心操作。部署方式如下:

# 方式一:从源码编译(推荐,版本对齐)
git clone https://github.com/etcd-io/etcd.git --branch v3.5.15
cd etcd/tools/benchmark
go build -o benchmark .

# 方式二:直接 go install
go install go.etcd.io/etcd/v3/tools/benchmark@latest

典型配置参数:

参数 示例值 说明
--endpoints 10.0.1.10:2379,10.0.1.11:2379 etcd 集群端点列表
--conns 10 gRPC 长连接数
--clients 50 并发客户端数(goroutine)
--total 100000 总请求数
--key-size 32 Key 字节长度
--val-size 1024 Value 字节长度
--rate 10000 限速(请求/秒,0 为不限)
# 示例:100 并发写入 50 万 key,value 2KB
./benchmark put \
  --endpoints=https://10.0.1.10:2379,https://10.0.1.11:2379 \
  --conns=20 \
  --clients=100 \
  --total=500000 \
  --key-size=64 \
  --val-size=2048 \
  --rate=0
注意: benchmark 工具默认使用 etcd 的根证书进行 TLS 认证。若集群启用了双向 TLS,需额外指定 --cacert、--cert、--key 参数。生产环境压测前务必确认证书链有效,否则会因 TLS 握手失败导致压测中断。

3.3 ghz 部署与配置

ghz 是通用的 gRPC 压测工具,支持反射协议和 Protobuf 文件两种模式。对于 etcd,推荐使用 Protobuf 文件模式以获得更精确的请求构造。

# 安装 ghz
go install github.com/bojand/ghz/cmd/ghz@latest

# 验证安装
ghz --version

etcd 压测配置示例:

# 使用 Protobuf 文件构造 Put 请求
ghz \
  --proto /path/to/etcd/api/etcdserverpb/rpc.proto \
  --call etcdserverpb.KV.Put \
  --data '{"key":"dGVzdC1rZXk=","value":"dGVzdC12YWx1ZQ=="}' \
  --insecure \
  --concurrency=50 \
  --total=100000 \
  --rps=5000 \
  10.0.1.10:2379
参数 说明 etcd 场景建议
--proto 指定 Protobuf 定义文件 使用 etcd 源码中的 rpc.proto
--call 完整 gRPC 方法名 etcdserverpb.KV.Put / Range / Txn
--data 请求体 JSON(字段需 Base64 编码) key/value 必须为 Base64 字符串
--concurrency 并发 worker 数 通常 50~200
--rps 每秒请求数限制 0 表示不限速
--insecure 跳过 TLS 验证 测试环境使用;生产环境用 --cacert
多集群压测技巧: ghz 支持 --multiple-targets 模式,可将多个 etcd 集群端点写入一个文件,实现一轮压测同时向多个集群发送请求,便于横向对比延迟与吞吐。

3.4 自定义压测脚本搭建

当官方工具无法满足复杂业务模型时(如混合读写、事务、租约续期、watch 流),需要基于 etcd clientv3 编写自定义压测脚本。以下是一个最小化框架:

package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    clientv3 "go.etcd.io/etcd/client/v3"
    "go.uber.org/zap"
)

type LoadConfig struct {
    Endpoints   []string
    Concurrency int
    TotalOps    int
    KeyPrefix   string
    ValueSize   int
    ReadRatio   float64 // 0.0 ~ 1.0
}

type Stats struct {
    mu       sync.Mutex
    ops      int
    errors   int
    latencies []time.Duration
}

func runWorker(cli *clientv3.Client, cfg *LoadConfig, stats *Stats, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < cfg.TotalOps/cfg.Concurrency; i++ {
        start := time.Now()
        key := fmt.Sprintf("%s/key-%d", cfg.KeyPrefix, i)
        val := make([]byte, cfg.ValueSize)

        var err error
        if float64(i)/float64(cfg.TotalOps/cfg.Concurrency) < cfg.ReadRatio {
            _, err = cli.Get(context.Background(), key)
        } else {
            _, err = cli.Put(context.Background(), key, string(val))
        }

        stats.mu.Lock()
        stats.ops++
        if err != nil {
            stats.errors++
        }
        stats.latencies = append(stats.latencies, time.Since(start))
        stats.mu.Unlock()
    }
}

func main() {
    cfg := &LoadConfig{
        Endpoints:   []string{"10.0.1.10:2379", "10.0.1.11:2379"},
        Concurrency: 50,
        TotalOps:    100000,
        KeyPrefix:   "/bench/load",
        ValueSize:   1024,
        ReadRatio:   0.7, // 70% 读,30% 写
    }

    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   cfg.Endpoints,
        DialTimeout: 5 * time.Second,
        Logger:      zap.NewNop(),
    })
    if err != nil {
        panic(err)
    }
    defer cli.Close()

    stats := &Stats{}
    var wg sync.WaitGroup
    for i := 0; i < cfg.Concurrency; i++ {
        wg.Add(1)
        go runWorker(cli, cfg, stats, &wg)
    }
    wg.Wait()

    fmt.Printf("Total Ops: %d, Errors: %d\n", stats.ops, stats.errors)
}
扩展建议: 生产级自定义脚本应加入 Prometheus 直方图采集、优雅退出信号处理、动态调整并发数、以及多集群 client 池管理。推荐使用 clientv3.NewCtx 结合 grpc.WithBalancer 实现客户端侧负载均衡。

3.5 压测部署拓扑

下图展示了一个典型的多集群 etcd 压测部署架构,压测节点与 etcd 集群之间通过独立网络平面隔离,避免压测流量干扰集群内部心跳与选举。

多集群 etcd 压测部署拓扑 压测节点组 benchmark / ghz 自定义脚本 Prometheus 采集 压测 网络平面 10Gbps etcd 集群 A 节点1: 10.0.1.10 节点2: 10.0.1.11 节点3: 10.0.1.12 3 节点 / Raft etcd 集群 B 节点1: 10.0.2.10 节点2: 10.0.2.11 节点3: 10.0.2.12 3 节点 / Raft 监控与存储 Prometheus Grafana Elasticsearch 部署要点 独立网络平面 客户端连接池隔离
部署关键原则:
  • 压测节点与 etcd 集群之间使用独立交换机或 VLAN,避免压测流量与集群内部心跳、选举、快照流量争抢带宽。
  • 每个压测客户端与 etcd 集群之间保持 10~20 个 gRPC 长连接,连接数过多会导致 etcd 侧文件描述符耗尽。
  • 多集群压测时,每个集群使用独立的 client 实例,避免连接复用导致请求路由错误。
  • 监控数据采集(Prometheus)应走独立的管理网络平面,不占用压测网络带宽。

3.6 配置清单与验证步骤

步骤 操作 验证方法
1 安装 benchmark / ghz / 编译自定义脚本 benchmark --version / ghz --version
2 配置 etcd 端点与 TLS 证书 etcdctl endpoint health 返回健康
3 执行单连接单并发探测(smoke test) 延迟 < 5ms,无错误
4 逐步增加并发数至目标值 观察 QPS 曲线是否线性增长
5 开启限速(--rate)避免过载 错误率 < 0.1%
6 多集群同时压测 各集群指标独立采集,无串扰
常见部署问题:
  • 连接泄漏: 自定义脚本中未调用 client.Close(),导致 etcd 侧连接数持续增长。建议在 defer 中显式关闭,并监控 etcd 的 etcd_network_client_active_connections 指标。
  • Protobuf 版本不匹配: ghz 使用的 rpc.proto 必须与目标 etcd 版本完全一致,否则反序列化失败。建议直接从目标 etcd 二进制或源码中提取 proto 文件。
  • 限速参数冲突: benchmark 的 --rate 与 --clients 同时设置时,实际 QPS 取两者较小值。若需要精确控制吞吐,建议只设置 --rate 并将 --clients 设为一个较大的值。