bigywater.tech
ZooKeeper · 89

注册中心数据模型设计

用 ZooKeeper 做注册中心,先要定节点怎么摆:按服务分层还是平铺?实例信息放节点名还是节点数据?这篇用 1000 个服务、5000 个实例做对比,量读取字节、无关变更引起的回调数、URL 作节点名的陷阱和 watch 数量。

69 vs 85,024 B
读一个服务的实例列表:分层 vs 平铺
0 vs 104
100 次无关服务的变更引起的回调:分层 vs 平铺
1 vs 201
一个服务 200 个实例:元信息放名字 vs 放数据的 watch 数

分层 vs 平铺

实测 · 本机执行输出
  建节点:分层 /h 下每个服务 5 个实例(共 1000 个服务目录);平铺 /f 下 5000 个节点
  消费者想知道 svc0042 的实例列表:分层 get_children 返回 69 字节;平铺要拉整个目录 85024 字节
  随机改 100 次不同服务的实例列表(每次一建一删):订阅 svc0042 的分层消费者回调 0 次;订阅整个平铺目录的消费者回调 104 次(每次回调都要重新拉 85024 字节)

分层让目录等于「订阅边界」:消费者只关心自己依赖的服务,所以只 watch 那几个目录。平铺的目录越大,每次变更触发的 get_children 响应越大(响应里是全部子节点名,见大目录)。

节点名里放什么

实测 · 本机执行输出
  直接 create /reg/prov/dubbo://10.0.0.1:20880/com.foo.Bar?weight=100 → NoNodeError(/ 被当成层级分隔符,试图在不存在的 dubbo:/10.0.0.1:20880/... 下建节点)
  URL 编码后 create 成功:/reg/prov/dubbo%3A%2F%2F10.0.0.1%3A20880%2Fcom.foo.Bar%3Fweight%3D100
  读回列表并解码: ['dubbo://10.0.0.1:20880/com.foo.Bar?weight=100']

Dubbo 的注册中心就是这样:提供者的完整 URL 经 URL 编码后作为 /dubbo/<服务>/providers/ 下的节点名。具体源码见第 101 篇。

元信息:名字还是数据

实测 · 本机执行输出
  (开始前服务端上的 watch 总数 = 0)
  元信息编码进节点名(只有目录上的 1 个 children watch):服务端为该消费者维护 1 个 watch;元信息放节点数据(每个实例一个 DataWatch + 目录一个 ChildrenWatch):201 个 watch
  改一个实例的权重:数据方式下消费者收到 1 次回调(只针对那个实例);名字方式要删旧节点、建新节点,消费者的 children 回调 2 次,每次回调后要重新读整个 200 项的目录

常用折中:不变的信息(地址、协议)放节点名,会变的(权重、状态)放数据并只对确实要变的实例订阅;或者把动态信息放到另一个目录里。

分步演示

注册中心数据模型
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / ZooKeeper 3.9.6(brew 安装的 jar,用 java -cp 直接启动自己的实例,OpenJDK 17 Temurin,JVM 堆 128 MiB)/ 本机回环端口 7300+,数据在临时目录;默认 tickTime=2000;计时用 time.perf_counter,数字每次运行会有小幅抖动。kazoo 2.11.0;watch 数取自 mntr 的 zk_watch_count(我观察到 wchs 没有统计 children watch);随机服务的变更次数只有 100 次、一次实验,回调数是该次的实际值
    • 没有验证:Dubbo 与 Spring Cloud 真实的目录结构(见第 101、106 篇)、数万实例规模、大目录的 get_children 在 ACL 打开时的耗时
    京ICP备2026067502号-1京公网安备11010502063487号