使用 TongZK 做服务注册与服务发现服务一旦从单实例扩展为多实例调用方就会面对一个很实际的问题订单服务当前有哪些可用实例某个实例扩容、重启或异常退出后地址列表怎样及时更新把地址写死在配置文件里短期很省事实例一多就会变成频繁改配置、重启和排查不一致的问题。服务注册与服务发现要解决的正是“服务提供者如何声明自己还活着”以及“服务消费者如何获得并更新可用地址”这两个问题。TongZK 面向 ZooKeeper 生态提供兼容能力适合承担这类强一致服务目录和状态感知工作。业务 Java 应用通常可以继续使用org.apache.zookeeper.*客户端 API而不需要把导入改为com.tongtech.tongzk。后者主要用于 TongZK 服务端内部代码、日志和扩展分析。本文以order-service为例介绍如何用临时顺序节点登记服务实例用 Watcher 感知目录变化并说明会话、下线和生产运维中容易被忽略的边界。一、先把服务目录设计清楚服务注册不是把所有服务信息放到一个大节点里而是利用 znode 的层次结构组织目录。下面是一种便于理解的最小模型/services /order-service # 持久目录节点 /instance-0000000001 # 临时顺序节点数据为 10.10.1.21:8080 /instance-0000000002 # 临时顺序节点数据为 10.10.1.22:8080这里有三个关键设计。/services和/services/order-service是持久节点用来保存服务目录结构。每个实例在服务目录下创建临时顺序节点节点数据保存该实例的访问地址或小体量元数据。消费者对服务目录的子节点列表注册 Watcher目录中的实例节点新增或删除后消费者重新读取地址列表。临时节点把“实例是否仍和 TongZK 保持会话”与“该实例是否出现在服务目录中”关联起来。实例正常关闭客户端连接时节点会删除进程异常退出或网络持续中断时节点会在对应 Session 过期后删除。临时顺序节点并不是唯一选择。若业务已有全局唯一的实例 ID也可以用唯一实例 ID 创建普通临时节点。本文选择临时顺序节点是为了避免多个实例同时启动时发生节点名冲突并方便观察每个注册动作对应的目录变化。二、服务提供者创建临时顺序节点完成注册下面的示例把注册逻辑封装为一个小类。它假定外层应用已经完成与 TongZK 的连接管理重点展示“确保目录存在”和“创建临时顺序节点”两个动作。importjava.nio.charset.StandardCharsets;importorg.apache.zookeeper.CreateMode;importorg.apache.zookeeper.KeeperException;importorg.apache.zookeeper.ZooDefs;importorg.apache.zookeeper.ZooKeeper;publicfinalclassServiceRegistryimplementsAutoCloseable{privatestaticfinalStringROOT_PATH/services;privatefinalZooKeeperclient;privatefinalStringservicePath;privateStringregisteredPath;publicServiceRegistry(ZooKeeperclient,StringserviceName){this.clientclient;this.servicePathROOT_PATH/serviceName;}publicStringregister(Stringendpoint)throwsKeeperException,InterruptedException{ensurePersistent(ROOT_PATH);ensurePersistent(servicePath);registeredPathclient.create(servicePath/instance-,endpoint.getBytes(StandardCharsets.UTF_8),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);returnregisteredPath;}privatevoidensurePersistent(Stringpath)throwsKeeperException,InterruptedException{try{client.create(path,newbyte[0],ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);}catch(KeeperException.NodeExistsExceptionignored){// Multiple instances may create the same directory concurrently.}}publicStringgetRegisteredPath(){returnregisteredPath;}Overridepublicvoidclose()throwsInterruptedException{client.close();}}ZooDefs.Ids.OPEN_ACL_UNSAFE只适合本地验证让示例聚焦在注册流程。生产环境必须按照应用身份、服务路径和最小权限原则设置 ACL并与认证、TLS、网络隔离和密钥管理策略一起验证。应用启动后可以使用多个 TongZK 节点构成连接串例如StringconnectStringtongzk-1.example.com:2181,tongzk-2.example.com:2181,tongzk-3.example.com:2181;ZooKeeperclientnewZooKeeper(connectString,30_000,event-System.out.println(TongZK event: event));ServiceRegistryregistrynewServiceRegistry(client,order-service);Stringpathregistry.register(10.10.1.21:8080);System.out.println(registered at path);这里使用的仍是 ZooKeeper 原始客户端 API。TongZK 服务端兼容范围内的 ZooKeeper 客户端可作为业务接入选择但连接串、客户端版本、认证、TLS、ACL、会话超时、网络策略和实际运行行为仍应在目标环境验证。示例为了突出节点操作省略了应用框架中的连接就绪等待、统一异常处理、重连和优雅停机钩子。真实服务不应在new ZooKeeper(...)后立刻假定连接已经完成应在收到连接成功状态后再执行注册并在应用关闭时调用close()。三、服务消费者先读列表再注册 Watcher消费者的目标不是保存一个永远不变的地址列表而是维护一份可以随目录变化刷新出来的本地快照。一个可靠的基本流程是对/services/order-service调用getChildren并注册 Watcher。读取每个实例节点的数据得到当前可用 endpoint 列表。收到子节点变化事件后在业务线程池中再次执行“读取列表并注册 Watcher”。将新快照交给本地负载均衡、连接池或调用组件。下面的代码展示这个模式。它省略了特定 RPC 框架的负载均衡实现只负责维护 endpoint 快照。importjava.nio.charset.StandardCharsets;importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;importjava.util.concurrent.Executor;importjava.util.function.Consumer;importorg.apache.zookeeper.KeeperException;importorg.apache.zookeeper.WatchedEvent;importorg.apache.zookeeper.Watcher;importorg.apache.zookeeper.ZooKeeper;publicfinalclassServiceDiscovery{privatefinalZooKeeperclient;privatefinalStringservicePath;privatefinalExecutorrefreshExecutor;privatefinalConsumerListStringonChanged;privatefinalConsumerExceptiononError;privatevolatileListStringendpointsCollections.emptyList();publicServiceDiscovery(ZooKeeperclient,StringservicePath,ExecutorrefreshExecutor,ConsumerListStringonChanged,ConsumerExceptiononError){this.clientclient;this.servicePathservicePath;this.refreshExecutorrefreshExecutor;this.onChangedonChanged;this.onErroronError;}publicvoidstart()throwsKeeperException,InterruptedException{refreshAndWatch();}publicListStringgetEndpoints(){returnendpoints;}privatevoidrefreshAndWatch()throwsKeeperException,InterruptedException{ListStringchildrenclient.getChildren(servicePath,this::handleChildEvent);Collections.sort(children);ListStringlatestnewArrayList();for(Stringchild:children){try{byte[]dataclient.getData(servicePath/child,false,null);latest.add(newString(data,StandardCharsets.UTF_8));}catch(KeeperException.NoNodeExceptionignored){// The instance disappeared after getChildren; the next refresh reconciles it.}}endpointsCollections.unmodifiableList(newArrayList(latest));onChanged.accept(endpoints);}privatevoidhandleChildEvent(WatchedEventevent){if(event.getType()!Watcher.Event.EventType.NodeChildrenChanged){return;}refreshExecutor.execute(()-{try{refreshAndWatch();}catch(KeeperException|InterruptedExceptione){if(einstanceofInterruptedException){Thread.currentThread().interrupt();}onError.accept(e);}});}}这段代码有两个容易被忽略的点。第一Watcher 是一次性通知。收到NodeChildrenChanged事件后不能只更新一次内存列表就结束必须再次调用带 Watcher 的getChildren为后续变化重新注册监听。第二不要在 Watcher 回调线程里直接做耗时的网络调用、复杂路由计算或批量重建连接。示例把刷新任务交给refreshExecutor避免事件处理线程被业务逻辑阻塞。本文的 Watcher 只监听服务目录的子节点增删不监听实例节点数据变化。生产中应尽量把实例 endpoint 视为注册后不可变的数据如确实需要更新实例元数据应采用删除后重新注册或为每个实例节点补充getDataWatcher 并处理其一次性重注册。还要把 Watcher 定位为“状态变化提示”而不是可靠消息队列。收到通知后消费者应重新读取服务目录的最新快照不能假设每一个实例的每一次变化都会以可业务消费的消息形式逐条传递。四、Session 决定实例何时真正下线临时节点与客户端 Session 绑定这也是服务注册能自动清理异常实例的基础。理解这层关系能避免把网络瞬断误判成实例已经下线。场景服务目录中的临时节点提供者与消费者应做什么提供者正常关闭客户端连接关闭后节点删除消费者收到目录变化后刷新列表。提供者进程异常退出Session 过期后节点删除消费者不能期待立刻删除应结合 Session 超时和应用健康检查判断。短暂网络抖动在 Session 未过期前节点可能仍存在提供者应处理重连状态消费者不应仅凭一次Disconnected事件立即清空地址。Session 已过期原临时节点会失效提供者要创建新的客户端连接并重新注册消费者应重新建立监听和刷新快照。Session 过期不是普通的短暂断连。客户端 Session 一旦过期旧客户端不能通过简单重连恢复原来的临时节点和 Watcher 状态。应用需要按自身生命周期策略创建新客户端、重新注册实例或重新拉取服务目录。因此服务发现通常还需要和应用自身的连接探测、调用超时、熔断、重试和负载均衡配合。TongZK 提供的是强一致服务目录和状态变化感知不替代完整的流量治理体系。五、用命令行和管控台验证目录变化开发联调时可以先通过 TongZK CLI 查看服务目录。以下命令以第 4 篇部署的三节点集群为例cd/opt/tongzk ./bin/tongzkCli.sh-servertongzk-1.example.com:2181进入 CLI 后执行ls /services/order-service get /services/order-service/instance-0000000001验证流程可以按下面四步走启动第一个提供者确认目录下出现一个临时实例节点。启动第二个提供者确认消费者本地快照增加一个 endpoint。正常停止其中一个提供者确认对应节点删除消费者刷新列表。在测试环境模拟异常退出观察节点在 Session 超时后消失并验证提供者恢复后是否重新注册。除了命令行TongZK 可视化管理控制台也能直接浏览和管理 znode 树。对运维人员来说它适合确认服务目录层级、实例节点数据、访问权限和异常残留对应用团队来说它能帮助快速区分“注册逻辑没有执行”“实例 Session 尚未过期”和“消费者没有重新注册 Watcher”等不同问题。六、上线前的设计清单服务注册看起来只有“创建节点”和“监听变化”两步生产落地时却需要把可用性和安全性一起设计。主题建议关注点服务路径按业务域和服务名规划层级避免多个团队随意复用同一目录。实例数据保持小体量明确使用host:port还是 JSON 元数据并约定字段兼容策略。身份与权限不要沿用示例中的开放 ACL按服务账号授权并验证认证、TLS 和密钥轮换。Session 参数根据网络质量、故障发现时效和误剔除成本设定超时并在压测和故障演练中验证。Watcher 处理每次触发后重新注册把刷新逻辑放在可控线程池并准备错误重试和周期性对账。消费端容错将目录变化与连接池、超时、重试、熔断和负载均衡协同避免一次事件直接影响全部流量。运维可见性用控制台和监控观察集群健康、会话、连接、服务目录、延迟和异常节点。发布与回退灰度验证注册、发现、实例下线和 Session 过期路径保留服务发现配置和应用版本的回退方案。如果项目正在由 ZooKeeper 迁移到 TongZK还应验证既有服务注册客户端的版本、连接串、ACL、认证、TLS、Session 行为和 Watcher 行为。兼容能力可以降低改造面但不代表这些运行时行为可以跳过现场验证。七、常见误用1. 把持久节点当作实例存活标记实例节点若使用普通持久节点进程异常退出后很容易留下过期地址消费者可能继续向失效实例发起调用。服务目录节点可以是持久节点但具体实例通常应使用临时节点表达会话存活状态。2. 收到 Watcher 通知后不重新注册Watcher 一次触发后就失效。只在启动时注册一次监听会导致消费者只感知到第一次目录变化之后的扩缩容和下线都不会再刷新本地列表。3. 把服务目录当成完整的健康检查系统临时节点反映的是 Session 状态不等于每个业务接口都健康。实例虽然仍保持 Session也可能因为线程池耗尽、依赖超时或业务降级而无法处理请求。调用侧仍要有超时、熔断和健康检查机制。4. 在生产中使用开放 ACL开放 ACL 便于快速演示却会让任何满足网络访问条件的客户端拥有过多操作权限。生产环境应根据服务身份和路径权限设计最小授权并把认证、TLS、网络隔离和审计一起纳入上线验收。八、总结用 TongZK 做服务注册与服务发现核心是让服务提供者通过临时节点表达存活状态让消费者通过 Watcher 感知服务目录变化并刷新 endpoint 快照。临时节点解决异常实例自动清理的基础问题Watcher 减少服务消费者的无效轮询而 Session 机制决定了“什么时候可以认为实例真正下线”。在此基础上TongZK 的 ZooKeeper 生态兼容、可视化 znode 管理和企业级运维能力能够让服务目录从单纯的代码逻辑变成更可观察、可治理的基础设施能力。下一篇文章将继续介绍如何使用 TongZK 实现配置中心重点讲 znode 数据模型、配置变更通知和强一致配置管理的适用边界。