bigywater.tech
微服务 · 44

etcd 前缀查询与大目录的性能

把服务实例存成 etcd 的一个前缀目录,实例多了查询要多久?分页怎么保证一致?频繁更新会让数据库涨多大?compaction 和 defrag 各做什么?这篇用 10 万个 key 的前缀量出来。

318.1 对 2.1 ms
10 万个 key 的前缀,完整查询与 limit 100(或 count_only)的耗时;完整查询响应 36 MB
27 对 0
扫描 10 万个 key 的前缀时,另有线程在写,分页不固定 revision 与固定 revision 读到的、比扫描起点还新的 key 数
59.8 对 30.5 MB
compact 之后 etcd 的 dbSize 与 dbSizeInUse(10 万次更新后);defrag 之后 dbSize 才回到 30.5 MB

前缀查询的代价

实测 · 本机执行输出
  key 数   | 完整查询                       | keys_only                | count_only         | limit 100           | 完整 + serializable         
  --------+----------------------------+--------------------------+--------------------+---------------------+---------------------------
  1,000   | 3.9 ms(3.0) · 358 KB       | 1.7 ms(1.3) · 84 KB      | 0.5 ms(0.5) · 0 KB | 0.7 ms(0.6) · 36 KB | 3.7 ms(2.9) · 358 KB      
  10,000  | 30.8 ms(23.2) · 3592 KB    | 14.0 ms(9.8) · 858 KB    | 0.5 ms(0.5) · 0 KB | 0.8 ms(0.7) · 36 KB | 31.0 ms(22.9) · 3592 KB   
  100,000 | 318.1 ms(225.4) · 36107 KB | 139.2 ms(94.1) · 8763 KB | 2.1 ms(2.1) · 0 KB | 2.1 ms(2.0) · 36 KB | 310.4 ms(217.5) · 36107 KB
  完整查询的代价随 key 数线性增长,而且响应是 base64 编码的 JSON,体积比原始数据大;keys_only 省掉 value,count_only 只返回个数,limit 100 只取 100 条,代价几乎不随 N 增长。服务发现的客户端每次变化都全量查询的话,实例数一大就很贵,应该首次全量加 watch 增量

完整查询随 key 数线性增长;limit、count_only 几乎不随 N 增长。

分页与一致性

实测 · 本机执行输出
  读法                       | 读到的 key 数 | 请求数 | 总用时    | 扫描期间的写入数 | 读到的、版本比“第一页读取时的 revision”还新的 key 数
  -------------------------+-----------+-----+--------+----------+-----------------------------------
  (a) 一次完整查询               | 100000    | 1   | 0.32 s | 14       | 0                                 
  (b) 每页 1000,不固定 revision | 100000    | 100 | 0.50 s | 31       | 27                                
  (c) 每页 1000,固定 revision  | 100000    | 100 | 0.50 s | 30       | 0                                 
  一次完整查询是一个一致的快照,但响应很大;分页不固定 revision 时,各页读自不同的时刻,读到的是混合了新旧版本的视图;固定 revision 的分页每一页都读同一个快照,代价是该 revision 不能被 compaction 压缩掉

分页不固定 revision 读到新旧混合的视图;固定 revision 得到一致的快照。

历史与 defrag

实测 · 本机执行输出
  阶段                      | dbSize(文件大小) | dbSizeInUse(实际在用)
  ------------------------+--------------+------------------
  初始(5000 个 key 各写一次之后)   | 30.0 MB      | 30.0 MB          
  更新 20 轮(10 万次写入)之后      | 59.0 MB      | 59.0 MB          
  compact 到当前 revision 之后 | 59.8 MB      | 30.5 MB          
  defrag 之后               | 30.5 MB      | 30.4 MB          
  每次更新都会产生一个新 revision,旧版本留在数据库里;compaction 把旧版本标记为可回收,但文件不会变小(dbSize 不变,dbSizeInUse 变小);defrag 才真正把空闲页还给文件系统。服务注册这类频繁续约、更新的场景,不定期 compaction 和 defrag,数据库会一直长大

更新留下旧版本;compaction 回收但不缩小文件,defrag 才缩小。

分步演示

etcd 前缀查询与大目录
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。etcd 3.7.2(Homebrew,单节点,每次运行前清空数据目录);通过 HTTP/JSON 网关访问(base64 的 JSON,耗时包含解析,括号里是不含解析的时间);批量写入用 Txn(一次 128 个 put,etcd 默认 --max-txn-ops=128);扫描期间的写入是另一个线程每 10 ms 一次,写的是尚未扫到的 key;三节共用同一个 etcd 实例,所以第三节的 dbSize 底数含前两节的数据
    • 没有验证:clientv3 直接用 gRPC 的响应大小与延迟(没有 base64 JSON 的膨胀);etcd 的 --auto-compaction-mode / retention;key 更长、value 更大的情形;多节点集群上的 range 读(线性一致读要经过 ReadIndex);--quota-backend-bytes 到顶后的 NOSPACE 告警
    京ICP备2026067502号-1京公网安备11010502063487号