Dubbo 从 Zookeeper 迁到 Nacos 后,运维少记了 3 个端口号:两个注册中心的 4 笔账
一个 Dubbo 项目,两台 Zookeeper 服务器,五个端口号
2018 年进公司接手的第一个 Dubbo 项目。注册中心用的 Zookeeper。运维给了我一张纸条,上面写着五个端口号:
- 2181:ZK 客户端连接
- 2888:ZK 集群内部通信(Leader ↔ Follower)
- 3888:ZK Leader 选举
- 20880:Dubbo 服务端口
- 22222:Dubbo 监控端口
我说"记不住"。运维说"习惯了就好"。
后来切到 Nacos。纸条上的端口号减到两个:8848(HTTP)和 9848(gRPC)。Dubbo 的服务端口不用和注册中心耦合了------注册信息通过 Nacos 的 API 透传,不是像 ZK 那样把 Dubbo URL 直接写进 ZNode 的 data 里。
这篇文章不是要证明 Nacos 比 Zookeeper 好。Zookeeper 当了八年 Dubbo 默认注册中心,它在一致性上的能力至今没人能替代。但注册中心的角色正在被重新定义------不再是"一个分布式协调器",而是"服务治理平台的一个模块"。Nacos 对这个定义的理解,恰好踩中了 Dubbo 3.x 时代的需求。
⚠️ 本文对比基于 Zookeeper 3.8.x 和 Nacos 2.3.x(2024年12月发布)。如果你还在用 ZK 3.4.x,本文的 ZooDefs 和 session 机制描述仍然适用。
全景:四个维度,两笔旧账
#mermaid-svg-aJcR4doDD45t888p{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aJcR4doDD45t888p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aJcR4doDD45t888p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aJcR4doDD45t888p .error-icon{fill:#552222;}#mermaid-svg-aJcR4doDD45t888p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aJcR4doDD45t888p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aJcR4doDD45t888p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aJcR4doDD45t888p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aJcR4doDD45t888p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aJcR4doDD45t888p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aJcR4doDD45t888p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aJcR4doDD45t888p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aJcR4doDD45t888p .marker.cross{stroke:#333333;}#mermaid-svg-aJcR4doDD45t888p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aJcR4doDD45t888p p{margin:0;}#mermaid-svg-aJcR4doDD45t888p .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aJcR4doDD45t888p .cluster-label text{fill:#333;}#mermaid-svg-aJcR4doDD45t888p .cluster-label span{color:#333;}#mermaid-svg-aJcR4doDD45t888p .cluster-label span p{background-color:transparent;}#mermaid-svg-aJcR4doDD45t888p .label text,#mermaid-svg-aJcR4doDD45t888p span{fill:#333;color:#333;}#mermaid-svg-aJcR4doDD45t888p .node rect,#mermaid-svg-aJcR4doDD45t888p .node circle,#mermaid-svg-aJcR4doDD45t888p .node ellipse,#mermaid-svg-aJcR4doDD45t888p .node polygon,#mermaid-svg-aJcR4doDD45t888p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aJcR4doDD45t888p .rough-node .label text,#mermaid-svg-aJcR4doDD45t888p .node .label text,#mermaid-svg-aJcR4doDD45t888p .image-shape .label,#mermaid-svg-aJcR4doDD45t888p .icon-shape .label{text-anchor:middle;}#mermaid-svg-aJcR4doDD45t888p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aJcR4doDD45t888p .rough-node .label,#mermaid-svg-aJcR4doDD45t888p .node .label,#mermaid-svg-aJcR4doDD45t888p .image-shape .label,#mermaid-svg-aJcR4doDD45t888p .icon-shape .label{text-align:center;}#mermaid-svg-aJcR4doDD45t888p .node.clickable{cursor:pointer;}#mermaid-svg-aJcR4doDD45t888p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aJcR4doDD45t888p .arrowheadPath{fill:#333333;}#mermaid-svg-aJcR4doDD45t888p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aJcR4doDD45t888p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aJcR4doDD45t888p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aJcR4doDD45t888p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aJcR4doDD45t888p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aJcR4doDD45t888p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aJcR4doDD45t888p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aJcR4doDD45t888p .cluster text{fill:#333;}#mermaid-svg-aJcR4doDD45t888p .cluster span{color:#333;}#mermaid-svg-aJcR4doDD45t888p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aJcR4doDD45t888p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aJcR4doDD45t888p rect.text{fill:none;stroke-width:0;}#mermaid-svg-aJcR4doDD45t888p .icon-shape,#mermaid-svg-aJcR4doDD45t888p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aJcR4doDD45t888p .icon-shape p,#mermaid-svg-aJcR4doDD45t888p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aJcR4doDD45t888p .icon-shape .label rect,#mermaid-svg-aJcR4doDD45t888p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aJcR4doDD45t888p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aJcR4doDD45t888p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aJcR4doDD45t888p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Nacos
AP+CP 可切换
服务模型
Namespace→Group→Service
显式心跳 + 服务端探测
注册+配置+治理
一个控制台
Zookeeper
CP,Zab 协议
树状存储模型
Session 心跳
只做协调
不做治理
两个系统在注册中心这个角色上的根本分歧:Zookeeper 是一棵分布式协调树,Nacos 是一个服务治理平台。前者精确但窄,后者宽但运维重。
🔗 前面拆过 Nacos 的 AP/CP 分叉源码:临时实例走左边,持久化走右边------拆开那 3 行 if
账本一:数据模型------树 vs 服务
ZK 的存储模型是一棵树。每个节点叫 ZNode,有路径、有数据、有子节点。
Dubbo 把服务注册信息存成这样的结构:
/dubbo/com.example.OrderService/providers/
├── dubbo://10.0.1.10:20880/com.example.OrderService?...
├── dubbo://10.0.1.11:20880/com.example.OrderService?...
每一个 provider URL 是一个临时 ZNode。客户端会话断了,ZK 自动删掉这个 ZNode,消费者 watcher 收到通知,更新本地列表。
这套机制的问题是:ZNode 里存的是 Dubbo URL,不是服务实例。 你想在注册中心层面做权重调整、流量路由、元数据过滤,必须先解析 URL 字符串,改它的参数,再写回 ZNode。没有控制台能帮你做这件事。
Nacos 的存储模型是服务→实例:
Namespace: prod
├── Group: DEFAULT_GROUP
│ ├── Service: com.example.OrderService
│ │ ├── Cluster: DEFAULT
│ │ │ ├── Instance: 10.0.1.10:20880
│ │ │ │ ├── weight: 100
│ │ │ │ ├── healthy: true
│ │ │ │ ├── metadata: {"version": "v1"}
│ │ │ ├── Instance: 10.0.1.11:20880
这是一个专门为服务发现设计的数据模型。实例有独立的属性字段(权重、健康状态、元数据),不是塞在一个 URL 字符串里。你可以在 Nacos 控制台直接调整某个实例的权重------不用解析 URL、不用重写 ZNode。
账本二:一致性------CP 的代价
ZK 用 Zab 协议保强一致。每条写入都要过半节点确认。
这在"注册中心"场景下带来三个实际问题:
问题一:写不可用。 3 节点的 ZK 集群挂 2 台,剩余的节点进入只读模式------不能注册新服务,不能下线实例。Dubbo 的注册写入直接失败,抛 KeeperException.ConnectionLossException。
问题二:Session 过期风暴。 ZK 的心跳是 Session 机制。客户端和服务端之间有一个 sessionTimeout(默认 40 秒)。如果网络闪断超过 40 秒,ZK 认为客户端挂了,删掉它名下所有临时 ZNode。Dubbo 侧表现为"这个客户端注册的所有服务同时消失"------然后网络恢复,所有服务同时重新注册。集群短时间内承受大量写入。
问题三:Watcher 是一次性的。 ZK 的 watcher 触发一次后就失效了。Dubbo 客户端每次收到通知后要重新注册 watcher。大量实例变更时,watcher 的注册和触发会产生雪崩效应。Dubbo 2.7 之后做了 watcher 的批量处理和重试机制来缓解这个问题,但机制本身的"一次性"特点没变。
Nacos 2.x 对这三个问题的处理方式不同:
- 临时实例走 Distro(AP),3 节点挂 2 台仍可写入(上一篇 5.4 里有那 3 行 if)。
- 心跳是显式上报(客户端主动发),不是 Session 机制。超时时间默认 15 秒,可以按服务调。
- 订阅是持续监听(gRPC 双向流),不是一次性 watcher。不用每次重新注册。
账本三:配置管理------ZK 能做,但像用螺丝刀切菜
ZK 最早被用作配置中心------ZNode 存配置内容,客户端 watch ZNode,变更后收到通知。阿里巴巴早期的 Diamond 配置中心就是基于这个思路。
但 ZK 做配置管理有几个硬伤:
- 没有版本历史。 改完就覆盖了,回滚只能靠外部备份。
- 没有灰度发布。 所有 watch 该 ZNode 的客户端同时收到变更通知,不能分批推。
- 没有审计追踪。 谁在什么时间改了什么、改之前的值是什么------查不到。
- 大数据量不适合 ZK 的 ZNode 模型。 ZK 的单个 ZNode 数据量上限是 1MB。超过 1MB 的配置文件(比如一个巨大的 JSON 或 YAML)要拆成多个 ZNode。
Nacos 的配置管理是独立模块。版本历史、一键回滚、Group 分层灰度、变更审计------这些在 ZK 那边需要二次开发的功能,在 Nacos 这边是开箱即用的。
账本四:运维------少记 3 个端口,多维护 1 个 MySQL
| 运维动作 | Zookeeper | Nacos(基于 2.3.x) |
|---|---|---|
| 部署复杂度 | 1 个 tar.gz,解压即用 | 需要 MySQL(cluster 模式),多一个 gRPC 端口 9848 |
| 集群搭建 | zkServer.sh start + zoo.cfg 配 peer |
cluster.conf 手动配置 + MySQL 初始化 |
| JVM 调优 | 堆内存 + GC 调优(ZK 对 GC 敏感) | 堆内存 + gRPC 线程池 |
| 控制台 | ❌ 无内置(需 zkUI / zkWeb) | ✅ 完整服务+配置管理控制台 |
| 端口数 | 3 个(2181/2888/3888) | 3 个(8848/9848/9849) |
| 外部依赖 | 无 | MySQL |
运维成本其实是 Nacos 输掉的一笔账。ZK 零外部依赖------一个 JVM 就能跑,集群搭建基本没有前置步骤。Nacos 需要 MySQL,cluster 模式下还要初始化和维护。
但 Dubbo 用户迁移到 Nacos 后,运维总成本通常是下降的。 原因是:用 ZK 做注册中心意味着你要另外选型配置中心(Apollo / Consul / 自研),选型消息队列、选型监控平台。每多一个组件,运维多维护一套。Nacos 把注册和配置合并后,虽然 Nacos 本身运维比 ZK 重,但省掉了配置中心那套环境。
⚠️ 适用边界:如果你的团队已经有成熟的配置中心(比如 Apollo)且没有迁移计划,Zookeeper + Apollo 是稳定的组合,不需要为了统一而推倒重来。
一张图带走:选 ZK 还是 Nacos
#mermaid-svg-pViSZ3Ca9B7TsDin{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pViSZ3Ca9B7TsDin .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pViSZ3Ca9B7TsDin .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pViSZ3Ca9B7TsDin .error-icon{fill:#552222;}#mermaid-svg-pViSZ3Ca9B7TsDin .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pViSZ3Ca9B7TsDin .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pViSZ3Ca9B7TsDin .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pViSZ3Ca9B7TsDin .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pViSZ3Ca9B7TsDin .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pViSZ3Ca9B7TsDin .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pViSZ3Ca9B7TsDin .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pViSZ3Ca9B7TsDin .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pViSZ3Ca9B7TsDin .marker.cross{stroke:#333333;}#mermaid-svg-pViSZ3Ca9B7TsDin svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pViSZ3Ca9B7TsDin p{margin:0;}#mermaid-svg-pViSZ3Ca9B7TsDin .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pViSZ3Ca9B7TsDin .cluster-label text{fill:#333;}#mermaid-svg-pViSZ3Ca9B7TsDin .cluster-label span{color:#333;}#mermaid-svg-pViSZ3Ca9B7TsDin .cluster-label span p{background-color:transparent;}#mermaid-svg-pViSZ3Ca9B7TsDin .label text,#mermaid-svg-pViSZ3Ca9B7TsDin span{fill:#333;color:#333;}#mermaid-svg-pViSZ3Ca9B7TsDin .node rect,#mermaid-svg-pViSZ3Ca9B7TsDin .node circle,#mermaid-svg-pViSZ3Ca9B7TsDin .node ellipse,#mermaid-svg-pViSZ3Ca9B7TsDin .node polygon,#mermaid-svg-pViSZ3Ca9B7TsDin .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pViSZ3Ca9B7TsDin .rough-node .label text,#mermaid-svg-pViSZ3Ca9B7TsDin .node .label text,#mermaid-svg-pViSZ3Ca9B7TsDin .image-shape .label,#mermaid-svg-pViSZ3Ca9B7TsDin .icon-shape .label{text-anchor:middle;}#mermaid-svg-pViSZ3Ca9B7TsDin .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pViSZ3Ca9B7TsDin .rough-node .label,#mermaid-svg-pViSZ3Ca9B7TsDin .node .label,#mermaid-svg-pViSZ3Ca9B7TsDin .image-shape .label,#mermaid-svg-pViSZ3Ca9B7TsDin .icon-shape .label{text-align:center;}#mermaid-svg-pViSZ3Ca9B7TsDin .node.clickable{cursor:pointer;}#mermaid-svg-pViSZ3Ca9B7TsDin .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pViSZ3Ca9B7TsDin .arrowheadPath{fill:#333333;}#mermaid-svg-pViSZ3Ca9B7TsDin .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pViSZ3Ca9B7TsDin .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pViSZ3Ca9B7TsDin .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pViSZ3Ca9B7TsDin .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pViSZ3Ca9B7TsDin .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pViSZ3Ca9B7TsDin .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pViSZ3Ca9B7TsDin .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pViSZ3Ca9B7TsDin .cluster text{fill:#333;}#mermaid-svg-pViSZ3Ca9B7TsDin .cluster span{color:#333;}#mermaid-svg-pViSZ3Ca9B7TsDin div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pViSZ3Ca9B7TsDin .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pViSZ3Ca9B7TsDin rect.text{fill:none;stroke-width:0;}#mermaid-svg-pViSZ3Ca9B7TsDin .icon-shape,#mermaid-svg-pViSZ3Ca9B7TsDin .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pViSZ3Ca9B7TsDin .icon-shape p,#mermaid-svg-pViSZ3Ca9B7TsDin .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pViSZ3Ca9B7TsDin .icon-shape .label rect,#mermaid-svg-pViSZ3Ca9B7TsDin .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pViSZ3Ca9B7TsDin .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pViSZ3Ca9B7TsDin .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pViSZ3Ca9B7TsDin :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Dubbo 2.7 以下
只需注册中心
Dubbo 3.x
只需注册中心
Dubbo 任意版本
需要注册+配置
Spring Cloud 体系
你用什么框架?
有什么需求?
Zookeeper
稳定可靠
零外部依赖
Nacos 或 ZK
都可,Nacos 原生适配更好
Nacos
唯一选择
ZK 不适合做配置中心
Nacos
Spring Cloud Alibaba
默认选择
⚠️ ZK 3.8.x
sessionTimeout 设为 40s+
开启 reconfig 功能
⚠️ 需要额外维护 MySQL
端口 8848+9848+9849
截图保存。三条判断标准:Dubbo 版本、是否需要配置中心、是不是 Spring Cloud 体系。每条路径下面附了运维注意事项。
不适用 ZK→Nacos 迁移的两种场景
场景一:集群体量巨大但只做注册,已有成熟的配置管理。 如果你的 ZK 集群已经稳定跑了三年、部署了几百个 Dubbo 服务、配置中心用 Apollo 且满意------不需要迁。迁移的风险(数据不一致、中间状态、回滚验证)大于收益。
场景二:对 ZK 的强一致性有强依赖。 如果你的 Dubbo 服务注册必须强一致(不能容忍任何节点间数据不一致),Nacos 的临时实例(Distro AP 模式)不适合。持久化实例可以用 Raft(CP),但 Nacos 的 Raft 实现只用于持久化实例和服务元数据,不像 ZK 那样所有写入都过 Zab。
你们 Dubbo 项目用什么注册中心?评论区留数字:1=Zookeeper 稳如老狗 2=Zookeeper→正在迁 Nacos 3=已切 Nacos 4=Nacos + Zookeeper 双注册过渡中。顺便说说迁移过程中最让你头疼的那一步。